made-market 0.3.0 → 0.4.1
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/bin/made-market.mjs +4 -4
- package/lib/capabilities.mjs +3 -0
- package/lib/market.mjs +62 -7
- package/lib/plugin.mjs +2 -1
- package/package.json +1 -1
- package/src/init.mjs +99 -13
- package/src/install.mjs +6 -2
package/bin/made-market.mjs
CHANGED
|
@@ -105,7 +105,7 @@ async function resolveViaApi(ids, server) {
|
|
|
105
105
|
const base = String(server).replace(/\/+$/, '');
|
|
106
106
|
const packages = [];
|
|
107
107
|
for (const id of ids) {
|
|
108
|
-
const response = await fetch(`${base}/api/packages/${encodeURIComponent(id)}`, { signal: AbortSignal.timeout(
|
|
108
|
+
const response = await fetch(`${base}/api/packages/${encodeURIComponent(id)}`, { signal: AbortSignal.timeout(30000) });
|
|
109
109
|
if (!response.ok) throw new Error(`查询 ${id} 失败:HTTP ${response.status}`);
|
|
110
110
|
const versions = await response.json();
|
|
111
111
|
const latest = versions.reduce((best, item) => (!best || semverGreater(item.version, best.version) ? item : best), null);
|
|
@@ -222,8 +222,8 @@ async function main() {
|
|
|
222
222
|
console.log('\n下一步:');
|
|
223
223
|
console.log(' 1. 改掉 market.json / theme.json(或 plugin.mjs)里的占位文案');
|
|
224
224
|
console.log(` 2. 本地校验:npx made-market validate ${dir}`);
|
|
225
|
-
console.log(` 3. 注册并发布:npx made-market register <用户名> --server https://made-
|
|
226
|
-
console.log(` npx made-market publish ${dir} --server https://made-
|
|
225
|
+
console.log(` 3. 注册并发布:npx made-market register <用户名> --server https://market.made-world.link`);
|
|
226
|
+
console.log(` npx made-market publish ${dir} --server https://market.made-world.link`);
|
|
227
227
|
console.log(' 开发文档见市场主页「开发者指南」。');
|
|
228
228
|
return;
|
|
229
229
|
}
|
|
@@ -273,7 +273,7 @@ async function main() {
|
|
|
273
273
|
let entry;
|
|
274
274
|
if (!staticRegistry) {
|
|
275
275
|
const base = listServer();
|
|
276
|
-
const response = await fetch(`${base}/api/packages/${encodeURIComponent(id)}`, { signal: AbortSignal.timeout(
|
|
276
|
+
const response = await fetch(`${base}/api/packages/${encodeURIComponent(id)}`, { signal: AbortSignal.timeout(30000) });
|
|
277
277
|
if (!response.ok) throw new Error(`服务上没有包 ${id}(HTTP ${response.status})`);
|
|
278
278
|
const versions = await response.json();
|
|
279
279
|
entry = versions.reduce((best, item) => (!best || semverGreater(item.version, best.version) ? item : best), null);
|
package/lib/capabilities.mjs
CHANGED
|
@@ -34,6 +34,9 @@ export const CAPABILITIES = {
|
|
|
34
34
|
{ name: 'voice.start', desc: '主动点亮录音' },
|
|
35
35
|
{ name: 'task.confirm', desc: '批准待确认任务' },
|
|
36
36
|
{ name: 'task.cancel', desc: '取消任务' },
|
|
37
|
+
{ name: 'ui.swipe', desc: '语义化手势,fields.direction:left/right(切助手)、up(开抽屉)、down(收抽屉)' },
|
|
38
|
+
{ name: 'ui.tap', desc: '点击屏幕,fields.x/fields.y 为板面像素坐标(240x320),命中最深的可点击对象' },
|
|
39
|
+
{ name: 'debug.screenshot', desc: '截取当前画面回传桥接器转 PNG(存 stateDir/screenshots/)' },
|
|
37
40
|
],
|
|
38
41
|
},
|
|
39
42
|
/** onJob 收到的任务生命周期,type 字段的全部取值(按典型顺序排列)。 */
|
package/lib/market.mjs
CHANGED
|
@@ -38,12 +38,17 @@ const PAYLOAD_IGNORED = new Set(['market.json']);
|
|
|
38
38
|
export const sha256 = (buffer) => crypto.createHash('sha256').update(buffer).digest('hex');
|
|
39
39
|
export const hashFile = (file) => sha256(fs.readFileSync(file));
|
|
40
40
|
|
|
41
|
-
/** Deterministic, sorted payload listing — `market.json` never ships.
|
|
41
|
+
/** Deterministic, sorted payload listing — `market.json` never ships.
|
|
42
|
+
* 递归收子目录(如 agent 包的 sprite/),路径带 / 前缀;点文件与 market.json 永远跳过。 */
|
|
42
43
|
export function payloadFiles(dir) {
|
|
43
|
-
|
|
44
|
-
.filter((
|
|
45
|
-
.
|
|
46
|
-
|
|
44
|
+
const walk = (base, prefix = '') => fs.readdirSync(base, { withFileTypes: true })
|
|
45
|
+
.filter((entry) => !PAYLOAD_IGNORED.has(entry.name) && !entry.name.startsWith('.'))
|
|
46
|
+
.flatMap((entry) => {
|
|
47
|
+
if (entry.isDirectory()) return walk(path.join(base, entry.name), `${prefix}${entry.name}/`);
|
|
48
|
+
if (entry.isFile()) return [`${prefix}${entry.name}`];
|
|
49
|
+
return [];
|
|
50
|
+
});
|
|
51
|
+
return walk(dir).sort();
|
|
47
52
|
}
|
|
48
53
|
|
|
49
54
|
export function readPackages(root = ROOT) {
|
|
@@ -134,8 +139,57 @@ function checkMarketFields(pkg, fail, warn) {
|
|
|
134
139
|
/**
|
|
135
140
|
* @returns {{errors: string[], warnings: string[], installDir: string, entry: string, pluginId: string}}
|
|
136
141
|
*/
|
|
137
|
-
|
|
138
|
-
|
|
142
|
+
/** agent 包可选的 sprite/ 形象素材目录(规范见 made 仓库 docs/SPRITES.md)。
|
|
143
|
+
* 有目录就必须有合法 manifest;引用的 PNG 必须存在;整包 ≤512KB(分片下发协议约束)。 */
|
|
144
|
+
function validateSpriteDir(dir, fail, warn) {
|
|
145
|
+
const spriteDir = path.join(dir, 'sprite');
|
|
146
|
+
if (!fs.existsSync(spriteDir)) {
|
|
147
|
+
warn('agent 包没有 sprite/ 目录,设备将显示内置兜底形象(规范见 made 仓库 docs/SPRITES.md)');
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const manifestPath = path.join(spriteDir, 'manifest.json');
|
|
151
|
+
if (!fs.existsSync(manifestPath)) {
|
|
152
|
+
fail('sprite/ 缺少 manifest.json');
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
let sprite;
|
|
156
|
+
try {
|
|
157
|
+
sprite = JSON.parse(fs.readFileSync(manifestPath, 'utf8'));
|
|
158
|
+
} catch (error) {
|
|
159
|
+
fail(`sprite/manifest.json 不是合法 JSON:${error.message}`);
|
|
160
|
+
return;
|
|
161
|
+
}
|
|
162
|
+
if (!Number.isInteger(sprite.version) || sprite.version < 1) {
|
|
163
|
+
fail('sprite/manifest.json 的 version 必须是 ≥1 的整数(改素材必须递增,否则设备不更新)');
|
|
164
|
+
}
|
|
165
|
+
if (!Array.isArray(sprite.layers) || !sprite.layers.length) {
|
|
166
|
+
fail('sprite/manifest.json 的 layers 必须是非空数组');
|
|
167
|
+
}
|
|
168
|
+
const pngs = fs.readdirSync(spriteDir).filter((name) => name.endsWith('.png'));
|
|
169
|
+
const referenced = new Set();
|
|
170
|
+
for (const layer of sprite.layers || []) {
|
|
171
|
+
for (const file of layer.frames || (layer.file ? [layer.file] : [])) {
|
|
172
|
+
if (typeof file !== 'string') {
|
|
173
|
+
fail('sprite/manifest.json 的层素材字段必须是字符串');
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
referenced.add(file);
|
|
177
|
+
if (!/^[A-Za-z0-9_-]{1,16}\.png$/i.test(file)) {
|
|
178
|
+
// 固件把素材平铺缓存在 SPIFFS(对象名上限 32 字符),文件名必须短。
|
|
179
|
+
fail(`sprite/${file} 文件名不合规:1–16 个字母/数字/下划线/连字符 + .png(总长 ≤20)`);
|
|
180
|
+
}
|
|
181
|
+
if (!pngs.includes(file)) fail(`sprite/manifest.json 引用了不存在的 ${file}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
let total = 0;
|
|
185
|
+
for (const file of pngs) {
|
|
186
|
+
total += fs.statSync(path.join(spriteDir, file)).size;
|
|
187
|
+
if (!referenced.has(file)) warn(`sprite/${file} 没有被 manifest.json 引用,不会下发到设备`);
|
|
188
|
+
}
|
|
189
|
+
if (total > 512 * 1024) fail(`sprite/ 素材共 ${total} 字节,超过 512KB 上限`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
export function validatePackage(pkg) { const errors = [];
|
|
139
193
|
const warnings = [];
|
|
140
194
|
const fail = (message) => errors.push(message);
|
|
141
195
|
const warn = (message) => warnings.push(message);
|
|
@@ -235,6 +289,7 @@ export function validatePackage(pkg) {
|
|
|
235
289
|
for (const key of ['session', 'model', 'cancel', 'progress']) {
|
|
236
290
|
if (!scan.capabilityKeys.includes(key)) warn(`${source} 的 capabilities 缺少 ${key}`);
|
|
237
291
|
}
|
|
292
|
+
validateSpriteDir(pkg.dir, fail, warn);
|
|
238
293
|
}
|
|
239
294
|
if (spec?.fields?.length && !pkg.market.highlights?.length) warn('这个包有可配置项,建议在 market.json 里写 highlights');
|
|
240
295
|
}
|
package/lib/plugin.mjs
CHANGED
|
@@ -19,11 +19,12 @@ const MAX_VALUE = 500;
|
|
|
19
19
|
const MAX_PATTERN = 200;
|
|
20
20
|
const MAX_FILE = 16 * 1024;
|
|
21
21
|
|
|
22
|
-
/** Commands the device currently executes (made
|
|
22
|
+
/** Commands the device currently executes (made 仓库 AGENTS.md,板侧事件与命令). */
|
|
23
23
|
export const BOARD_COMMANDS = Object.freeze([
|
|
24
24
|
'caption.show', 'audio.play', 'theme.apply', 'volume.set', 'agent.select',
|
|
25
25
|
'session.create', 'session.select', 'session.delete', 'voice.start',
|
|
26
26
|
'task.confirm', 'task.cancel',
|
|
27
|
+
'ui.swipe', 'ui.tap', 'debug.screenshot',
|
|
27
28
|
]);
|
|
28
29
|
|
|
29
30
|
/** Events the device currently publishes. */
|
package/package.json
CHANGED
package/src/init.mjs
CHANGED
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
*/
|
|
8
8
|
import fs from 'node:fs';
|
|
9
9
|
import path from 'node:path';
|
|
10
|
+
import zlib from 'node:zlib';
|
|
10
11
|
import { PACKAGE_DIR } from '../lib/plugin.mjs';
|
|
11
12
|
import { renderIcon } from '../lib/icon.mjs';
|
|
12
13
|
import { capabilitiesJson, contractsDts } from '../lib/capabilities.mjs';
|
|
@@ -182,7 +183,7 @@ function agentMarketJson(id, pluginId) {
|
|
|
182
183
|
nameEn: 'My Agent Runtime',
|
|
183
184
|
summary: '一句话简介:接的是哪个模型服务、怎么驱动码得。',
|
|
184
185
|
summaryEn: 'One-line summary shown in the market listing.',
|
|
185
|
-
description: '
|
|
186
|
+
description: '详细描述会话建立方式、模型接入与取消行为。\n\n可选 sprite/ 目录:分层 PNG + manifest.json 定义助手在码得屏幕上的形象与动效,规范见 made 仓库 docs/SPRITES.md。眨眼/说话是 eyes/mouth 层切帧,位置全由 manifest 声明,固件零写死坐标;内置绿豆形象就是这套格式的参考实例。改素材必须递增 manifest 的 version。',
|
|
186
187
|
descriptionEn: 'Long description in English.',
|
|
187
188
|
author: '你的名字',
|
|
188
189
|
license: 'MIT',
|
|
@@ -196,25 +197,34 @@ function agentMarketJson(id, pluginId) {
|
|
|
196
197
|
|
|
197
198
|
/** agent 模板的 capabilities:四个键写成一行字面量,静态扫描(scanPluginSource)才认得。 */
|
|
198
199
|
function agentPluginMjs(pluginId) {
|
|
200
|
+
// 模板必须能直接通过桥接器 validatePlugin:capabilities 是元数据(session 取
|
|
201
|
+
// 'native' | 'external_unscoped',其余是布尔),probe 与 run 是顶层函数。
|
|
199
202
|
return `/**
|
|
200
|
-
* agent
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
+
* agent 运行时模板:把你的模型/CLI 接到码得。probe 决定「是否可用」的展示,
|
|
204
|
+
* run 执行一轮任务。capabilities 是能力声明(不是函数):
|
|
205
|
+
* session: 'native'(可跨轮续会话)| 'external_unscoped'(任务隔离受限)。
|
|
203
206
|
*/
|
|
204
|
-
const createSession = async ({ deviceId, config }) => {
|
|
205
|
-
// 建立(或复用)一个模型会话,返回会话句柄。
|
|
206
|
-
return { id: \`demo-\${deviceId}\`, model: 'demo-model' };
|
|
207
|
-
};
|
|
208
|
-
const currentModel = async ({ config }) => 'demo-model';
|
|
209
|
-
const cancelTask = async () => { /* 取消当前任务 */ };
|
|
210
|
-
const reportProgress = ({ state }) => { /* 上报进度 */ };
|
|
211
|
-
|
|
212
207
|
export default {
|
|
213
208
|
apiVersion: 1,
|
|
214
209
|
kind: 'coding-agent',
|
|
215
210
|
id: '${pluginId}',
|
|
216
211
|
label: '我的 Agent',
|
|
217
|
-
capabilities: { session:
|
|
212
|
+
capabilities: { session: 'native', model: true, cancel: true, progress: true },
|
|
213
|
+
model: { default: '', required: false },
|
|
214
|
+
|
|
215
|
+
probe: (config) => {
|
|
216
|
+
// 返回 { available, reason };reason 会显示在电脑端助手里。
|
|
217
|
+
return { available: true, reason: '模板助手:把 run 换成你的模型接入即可' };
|
|
218
|
+
},
|
|
219
|
+
|
|
220
|
+
run: async ({ job, project, config, signal, onProgress, onSession, model }) => {
|
|
221
|
+
// job.instruction 是用户指令;job.sessionId 非空表示续接既有会话。
|
|
222
|
+
// 首轮必须上报会话 id(onSession),后续轮经 job.sessionId 传回。
|
|
223
|
+
if (!job.sessionId) onSession(\`demo-\${Date.now()}\`);
|
|
224
|
+
onProgress('模板正在假装干活……');
|
|
225
|
+
// 真正的接入在这里:调你的模型 API / CLI 子进程,尊重 signal 取消。
|
|
226
|
+
return { status: 'completed', result: \`(模板)收到指令:\${job.instruction}\` };
|
|
227
|
+
},
|
|
218
228
|
};
|
|
219
229
|
`;
|
|
220
230
|
}
|
|
@@ -224,6 +234,73 @@ function agentIconJson() {
|
|
|
224
234
|
return renderIcon({ background: '#141C29', accent: '#5B8DEF', shapes: [[12, 12, 18, 18, '5B8DEF', 5]] });
|
|
225
235
|
}
|
|
226
236
|
|
|
237
|
+
// ── sprite/ 形象素材(规范真源:made 仓库 docs/SPRITES.md)────────────────
|
|
238
|
+
// 占位 PNG 是合法 RGBA 图,尺寸/透明度合法,替换成真图即可。整体 ≤512KB。
|
|
239
|
+
|
|
240
|
+
const CRC_TABLE = (() => {
|
|
241
|
+
const table = new Int32Array(256);
|
|
242
|
+
for (let n = 0; n < 256; n++) {
|
|
243
|
+
let c = n;
|
|
244
|
+
for (let k = 0; k < 8; k++) c = c & 1 ? 0xedb88320 ^ (c >>> 1) : c >>> 1;
|
|
245
|
+
table[n] = c;
|
|
246
|
+
}
|
|
247
|
+
return table;
|
|
248
|
+
})();
|
|
249
|
+
|
|
250
|
+
function crc32(buffer) {
|
|
251
|
+
let c = 0xffffffff;
|
|
252
|
+
for (const byte of buffer) c = CRC_TABLE[(c ^ byte) & 0xff] ^ (c >>> 8);
|
|
253
|
+
return (c ^ 0xffffffff) >>> 0;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function pngChunk(type, data) {
|
|
257
|
+
const length = Buffer.alloc(4);
|
|
258
|
+
length.writeUInt32BE(data.length);
|
|
259
|
+
const body = Buffer.concat([Buffer.from(type, 'ascii'), data]);
|
|
260
|
+
const crc = Buffer.alloc(4);
|
|
261
|
+
crc.writeUInt32BE(crc32(body));
|
|
262
|
+
return Buffer.concat([length, body, crc]);
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** 纯色 RGBA PNG(逐行相同,deflate 后只有几十字节)。 */
|
|
266
|
+
function placeholderPng(width, height, [r, g, b, a]) {
|
|
267
|
+
const ihdr = Buffer.alloc(13);
|
|
268
|
+
ihdr.writeUInt32BE(width, 0);
|
|
269
|
+
ihdr.writeUInt32BE(height, 4);
|
|
270
|
+
ihdr[8] = 8; // bit depth
|
|
271
|
+
ihdr[9] = 6; // color type: RGBA
|
|
272
|
+
const row = Buffer.alloc(1 + width * 4);
|
|
273
|
+
for (let x = 0; x < width; x++) {
|
|
274
|
+
row[1 + x * 4] = r;
|
|
275
|
+
row[2 + x * 4] = g;
|
|
276
|
+
row[3 + x * 4] = b;
|
|
277
|
+
row[4 + x * 4] = a;
|
|
278
|
+
}
|
|
279
|
+
const raw = Buffer.concat(Array.from({ length: height }, () => row));
|
|
280
|
+
return Buffer.concat([
|
|
281
|
+
Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]),
|
|
282
|
+
pngChunk('IHDR', ihdr),
|
|
283
|
+
pngChunk('IDAT', zlib.deflateSync(raw)),
|
|
284
|
+
pngChunk('IEND', Buffer.alloc(0)),
|
|
285
|
+
]);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
function spriteManifest() {
|
|
289
|
+
return {
|
|
290
|
+
version: 1,
|
|
291
|
+
canvas: { w: 360, h: 360 },
|
|
292
|
+
layers: [
|
|
293
|
+
// body 铺满画布;eyes/mouth 的 x/y 是层左上角在画布上的位置,z 小的在下。
|
|
294
|
+
{ id: 'body', file: 'body.png', x: 100, y: 62, z: 0 },
|
|
295
|
+
{ id: 'eyes', frames: ['eyes-open.png', 'eyes-closed.png'], x: 150, y: 150, z: 1,
|
|
296
|
+
blink: { period_ms: 4000, closed_ms: 120 } },
|
|
297
|
+
{ id: 'mouth', frames: ['mouth-closed.png', 'mouth-open.png'], x: 155, y: 215, z: 2, talk: true },
|
|
298
|
+
],
|
|
299
|
+
// 整体动效:呼吸缩放 / 左右轻摆;缺省用固件默认值。
|
|
300
|
+
motion: { breath: { scale_pct: 103, period_ms: 3000 }, sway_deg: 3, sway_period_ms: 1800 },
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
|
|
227
304
|
const SCHEMAS = {
|
|
228
305
|
theme: {
|
|
229
306
|
label: '主题',
|
|
@@ -262,6 +339,14 @@ const SCHEMAS = {
|
|
|
262
339
|
{ name: 'plugin.mjs', data: agentPluginMjs(pluginId) },
|
|
263
340
|
{ name: 'plugin.json', data: `${JSON.stringify({ id: pluginId, settings: [] }, null, 2)}\n` },
|
|
264
341
|
{ name: 'icon.json', data: `${JSON.stringify(agentIconJson(), null, 2)}\n` },
|
|
342
|
+
// 形象素材:分层 PNG + manifest。规范与字段说明见 made 仓库 docs/SPRITES.md。
|
|
343
|
+
// 改素材后必须递增 manifest.json 的 version,已安装的设备才会重新拉取。
|
|
344
|
+
{ name: 'sprite/manifest.json', data: `${JSON.stringify(spriteManifest(), null, 2)}\n` },
|
|
345
|
+
{ name: 'sprite/body.png', data: placeholderPng(160, 160, [0x10, 0x26, 0x29, 0xff]) },
|
|
346
|
+
{ name: 'sprite/eyes-open.png', data: placeholderPng(40, 16, [0x26, 0x21, 0x5c, 0xff]) },
|
|
347
|
+
{ name: 'sprite/eyes-closed.png', data: placeholderPng(40, 16, [0x10, 0x26, 0x29, 0xff]) },
|
|
348
|
+
{ name: 'sprite/mouth-closed.png', data: placeholderPng(32, 12, [0x26, 0x21, 0x5c, 0xff]) },
|
|
349
|
+
{ name: 'sprite/mouth-open.png', data: placeholderPng(32, 12, [0x71, 0x2b, 0x13, 0xff]) },
|
|
265
350
|
];
|
|
266
351
|
},
|
|
267
352
|
},
|
|
@@ -283,6 +368,7 @@ export function scaffold({ dir, kind, force = false }) {
|
|
|
283
368
|
fs.mkdirSync(dir, { recursive: true });
|
|
284
369
|
return schema.files(id).map((file) => {
|
|
285
370
|
const target = path.join(dir, file.name);
|
|
371
|
+
fs.mkdirSync(path.dirname(target), { recursive: true }); // sprite/ 等子目录
|
|
286
372
|
fs.writeFileSync(target, file.data);
|
|
287
373
|
return target;
|
|
288
374
|
});
|
package/src/install.mjs
CHANGED
|
@@ -22,7 +22,7 @@ import { extractTar } from '../lib/tar.mjs';
|
|
|
22
22
|
const BUNDLED_REGISTRY = path.join(PKG_ROOT, 'registry', 'index.json');
|
|
23
23
|
|
|
24
24
|
/** 官方市场:install/list/search 不带参数时的默认来源。 */
|
|
25
|
-
export const DEFAULT_SERVER = 'https://made-
|
|
25
|
+
export const DEFAULT_SERVER = 'https://market.made-world.link';
|
|
26
26
|
|
|
27
27
|
/**
|
|
28
28
|
* 索引来源:显式 --registry > $MADE_MARKET_REGISTRY > $MADE_MARKET_SERVER > 官方市场。
|
|
@@ -141,7 +141,11 @@ export async function install(ids, { home = '', registry = BUNDLED_REGISTRY, ind
|
|
|
141
141
|
if (!dryRun) {
|
|
142
142
|
fs.rmSync(target, { recursive: true, force: true });
|
|
143
143
|
fs.mkdirSync(target, { recursive: true });
|
|
144
|
-
for (const file of entries)
|
|
144
|
+
for (const file of entries) {
|
|
145
|
+
const dest = path.join(target, file.name);
|
|
146
|
+
fs.mkdirSync(path.dirname(dest), { recursive: true }); // sprite/ 等子目录
|
|
147
|
+
fs.writeFileSync(dest, file.data);
|
|
148
|
+
}
|
|
145
149
|
}
|
|
146
150
|
done.push({ entry, target, files: entries.map((file) => file.name) });
|
|
147
151
|
console.log(`${dryRun ? '将安装' : '已安装'} ${entry.name} · ${id} v${entry.version}`);
|