made-market 0.1.1 → 0.1.2
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 +68 -20
- package/lib/market.mjs +7 -0
- package/package.json +1 -1
- package/registry/index.json +11 -11
- package/src/init.mjs +257 -0
- package/src/install.mjs +13 -6
package/bin/made-market.mjs
CHANGED
|
@@ -13,10 +13,11 @@
|
|
|
13
13
|
import fs from 'node:fs';
|
|
14
14
|
import path from 'node:path';
|
|
15
15
|
import { fileURLToPath } from 'node:url';
|
|
16
|
-
import { ROOT } from '../lib/market.mjs';
|
|
17
|
-
import { install, loadRegistry, resolveHome, resolveRegistry } from '../src/install.mjs';
|
|
16
|
+
import { ROOT, validatePackage } from '../lib/market.mjs';
|
|
17
|
+
import { DEFAULT_SERVER, install, loadRegistry, resolveHome, resolveRegistry } from '../src/install.mjs';
|
|
18
18
|
import { publish } from '../src/publish.mjs';
|
|
19
19
|
import { login, logout, register, resolvePassword, whoami } from '../src/account.mjs';
|
|
20
|
+
import { scaffold } from '../src/init.mjs';
|
|
20
21
|
|
|
21
22
|
// 版本号读 CLI 自己的 package.json(cli/),发布后即 node_modules/made-market/package.json。
|
|
22
23
|
const CLI_PACKAGE_JSON = fileURLToPath(new URL('../package.json', import.meta.url));
|
|
@@ -30,6 +31,8 @@ const USAGE = `码得 · 插件市场 (made-market)
|
|
|
30
31
|
npx made-market <命令> [参数] [选项]
|
|
31
32
|
|
|
32
33
|
命令
|
|
34
|
+
init <目录> --kind <类型> 生成插件脚手架(可直接 publish 的最小包,SDK 入口)
|
|
35
|
+
validate <包目录> 本地校验一个包(与市场服务端同一套规则)
|
|
33
36
|
list 列出索引里的全部包
|
|
34
37
|
search <关键词> 在包名、简介和标签里搜索
|
|
35
38
|
info <包 ID> 看一个包的详情:文件、校验和、安装位置、创建人
|
|
@@ -39,17 +42,18 @@ const USAGE = `码得 · 插件市场 (made-market)
|
|
|
39
42
|
login <用户名> 登录市场服务(发布的前提;90 天会话)
|
|
40
43
|
whoami 看当前登录的账号
|
|
41
44
|
logout 退出登录(吊销服务端会话并删除本地凭证)
|
|
42
|
-
rebuild-from-store 管理命令:从存储 manifests 全量重建
|
|
45
|
+
rebuild-from-store 管理命令:从存储 manifests 全量重建 SQLite 索引
|
|
43
46
|
|
|
44
47
|
选项
|
|
45
48
|
--home <目录> 包目录根,默认 $MADE_HOME 或 ~/made
|
|
46
|
-
--registry <路径|URL>
|
|
47
|
-
|
|
48
|
-
--server <URL> 市场服务地址(register/login/publish
|
|
49
|
-
|
|
49
|
+
--registry <路径|URL> 索引来源。默认 $MADE_MARKET_REGISTRY > 官方市场(联网拉实时索引,
|
|
50
|
+
失败回落本包自带索引,离线可装示例包)
|
|
51
|
+
--server <URL> 市场服务地址(register/login/publish/install;install 默认官方市场,
|
|
52
|
+
可用 $MADE_MARKET_SERVER 覆盖,已登录时 publish 也可从凭证读取)
|
|
50
53
|
--token <t> 发布 token(publish;默认 $MADE_MARKET_TOKEN 或登录凭证)
|
|
51
54
|
--password <pw> 密码(register/login;缺省在 TTY 下不回显输入)
|
|
52
|
-
|
|
55
|
+
选项
|
|
56
|
+
--kind theme|control|agent init 的包类型 / list、search 只列某一类
|
|
53
57
|
--json 以 JSON 输出
|
|
54
58
|
--force 目标目录已存在时覆盖
|
|
55
59
|
--dry-run 只校验并列出将要写入的文件
|
|
@@ -178,8 +182,25 @@ async function main() {
|
|
|
178
182
|
return;
|
|
179
183
|
}
|
|
180
184
|
|
|
181
|
-
//
|
|
182
|
-
|
|
185
|
+
// 脚手架:不依赖索引。
|
|
186
|
+
if (options.command === 'init') {
|
|
187
|
+
const dir = options.args[0];
|
|
188
|
+
if (!dir) throw new Error('init 需要一个包目录名,例如:npx made-market init my-theme --kind theme');
|
|
189
|
+
const written = scaffold({ dir, kind: options.kind, force: options.force });
|
|
190
|
+
console.log(`脚手架已生成(${options.kind || '?'} 包,共 ${written.length} 个文件):`);
|
|
191
|
+
for (const file of written) console.log(` ${file}`);
|
|
192
|
+
console.log('\n下一步:');
|
|
193
|
+
console.log(' 1. 改掉 market.json / theme.json(或 plugin.mjs)里的占位文案');
|
|
194
|
+
console.log(` 2. 本地校验:npx made-market validate ${dir}`);
|
|
195
|
+
console.log(` 3. 注册并发布:npx made-market register <用户名> --server https://made-market.link`);
|
|
196
|
+
console.log(` npx made-market publish ${dir} --server https://made-market.link`);
|
|
197
|
+
console.log(' 开发文档见市场主页「开发者指南」。');
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// 索引来源按需加载:--registry > $MADE_MARKET_REGISTRY > 官方市场实时索引
|
|
202
|
+
// (远端失败自动回落包内自带索引,离线可装示例包)。
|
|
203
|
+
// rebuild-from-store 不依赖全量索引。
|
|
183
204
|
const loadIndex = () => loadRegistry(resolveRegistry(options.registry));
|
|
184
205
|
|
|
185
206
|
if (options.command === 'list') {
|
|
@@ -232,11 +253,17 @@ async function main() {
|
|
|
232
253
|
|
|
233
254
|
if (options.command === 'install') {
|
|
234
255
|
if (!options.args.length) throw new Error('install 需要至少一个包 ID');
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
256
|
+
// 默认走市场 API:按包直查最新版本,不拉全量索引(大市场友好),
|
|
257
|
+
// 新发布的包立即可装。显式 --registry 才按索引文件走(自建源/精确复现用)。
|
|
258
|
+
if (!options.registry) {
|
|
259
|
+
const server = options.server || process.env.MADE_MARKET_SERVER || DEFAULT_SERVER;
|
|
260
|
+
try {
|
|
261
|
+
const { packages, base } = await resolveViaApi(options.args, server);
|
|
262
|
+
await install(options.args, { home: options.home, registry: base, index: { packages }, force: options.force, dryRun: options.dryRun });
|
|
263
|
+
return;
|
|
264
|
+
} catch (error) {
|
|
265
|
+
console.error(`线上市场不可用(${error.message}),改用包内自带索引(仅示例包,离线兜底)。`);
|
|
266
|
+
}
|
|
240
267
|
}
|
|
241
268
|
// 归档从索引所在的位置取:远端索引配远端归档,自带索引配包内归档,可离线。
|
|
242
269
|
const { source, index } = await loadIndex();
|
|
@@ -244,6 +271,27 @@ async function main() {
|
|
|
244
271
|
return;
|
|
245
272
|
}
|
|
246
273
|
|
|
274
|
+
// 本地校验:与服务端同一套规则(cli/lib/market.mjs 的 validatePackage),
|
|
275
|
+
// 上架前先在本地把错误清零,省一轮发布失败。
|
|
276
|
+
if (options.command === 'validate') {
|
|
277
|
+
const dir = options.args[0];
|
|
278
|
+
if (!dir) throw new Error('validate 需要一个包目录');
|
|
279
|
+
const marketPath = path.join(dir, 'market.json');
|
|
280
|
+
if (!fs.existsSync(marketPath)) throw new Error(`${dir} 里没有 market.json`);
|
|
281
|
+
const market = JSON.parse(fs.readFileSync(marketPath, 'utf8'));
|
|
282
|
+
const pkg = { id: path.basename(path.resolve(dir)), kind: market.kind, dir, market };
|
|
283
|
+
const result = validatePackage(pkg);
|
|
284
|
+
for (const message of result.errors) console.error(` 错误:${message}`);
|
|
285
|
+
for (const message of result.warnings) console.warn(` 警告:${message}`);
|
|
286
|
+
if (result.errors.length) {
|
|
287
|
+
console.error(`校验未通过:${result.errors.length} 个错误,${result.warnings.length} 个警告。`);
|
|
288
|
+
process.exitCode = 1;
|
|
289
|
+
return;
|
|
290
|
+
}
|
|
291
|
+
console.log(`校验通过:${pkg.id} v${market.version}(${result.warnings.length} 个警告)。可以 publish。`);
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
|
|
247
295
|
if (options.command === 'publish') {
|
|
248
296
|
const dir = options.args[0];
|
|
249
297
|
if (!dir) throw new Error('publish 需要一个包目录');
|
|
@@ -278,16 +326,16 @@ async function main() {
|
|
|
278
326
|
}
|
|
279
327
|
|
|
280
328
|
if (options.command === 'rebuild-from-store') {
|
|
281
|
-
|
|
282
|
-
const {
|
|
283
|
-
|
|
284
|
-
const db =
|
|
329
|
+
// 索引库是服务端配套能力,rebuild-from-store 属管理命令,仅在仓库内可用(npm 包不含 server/)。
|
|
330
|
+
const { openDatabase, initSchema, rebuildFromStore } = await import('../../server/db.mjs');
|
|
331
|
+
const { createStore } = await import("../../server/store.mjs");
|
|
332
|
+
const db = openDatabase();
|
|
285
333
|
try {
|
|
286
334
|
await initSchema(db);
|
|
287
335
|
const count = await rebuildFromStore(db, createStore(), { log: (key) => console.log(` 同步 ${key}`) });
|
|
288
336
|
console.log(`索引重建完成:${count} 条版本事实。`);
|
|
289
337
|
} finally {
|
|
290
|
-
|
|
338
|
+
db.close();
|
|
291
339
|
}
|
|
292
340
|
return;
|
|
293
341
|
}
|
package/lib/market.mjs
CHANGED
|
@@ -12,7 +12,14 @@ import { checkTheme, THEME_NAME } from './theme.mjs';
|
|
|
12
12
|
import { BOARD_COMMANDS, BOARD_EVENTS, PACKAGE_DIR, checkNames, parseSettingsSpec, scanPluginSource } from './plugin.mjs';
|
|
13
13
|
import { MAX_JSON_BYTES, validateIcon } from './icon.mjs';
|
|
14
14
|
|
|
15
|
+
/** 仓库根(dev 时 = 仓库根;发布包里没有 packages/,这个只给仓库内工具与测试用)。 */
|
|
15
16
|
export const ROOT = path.resolve(fileURLToPath(new URL('../..', import.meta.url)));
|
|
17
|
+
/**
|
|
18
|
+
* 包根:lib/ 的上一级。仓库里是 cli/(自带 registry/index.json 的生成物),
|
|
19
|
+
* 发布包里就是包根(npm 包无 cli/ 前缀)。找包内自带资源一律用它,不能用 ROOT——
|
|
20
|
+
* 发布后 ROOT 会落到 node_modules/,这就是「找不到索引」bug 的根源。
|
|
21
|
+
*/
|
|
22
|
+
export const PKG_ROOT = path.resolve(fileURLToPath(new URL('..', import.meta.url)));
|
|
16
23
|
export const API_VERSION = 1;
|
|
17
24
|
export const SEMVER = /^\d+\.\d+\.\d+$/;
|
|
18
25
|
export const MAX_DESCRIPTION = 4000;
|
package/package.json
CHANGED
package/registry/index.json
CHANGED
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
],
|
|
81
81
|
"payloadBytes": 209717,
|
|
82
82
|
"archiveBytes": 39822,
|
|
83
|
-
"sha256": "
|
|
83
|
+
"sha256": "b656724a6be3466c0ad52010bfa9f2a3b043595b6b7b6bed90a01bdfcca3ba95",
|
|
84
84
|
"download": "packages/sunset-1.0.0.tar.gz",
|
|
85
85
|
"swatch": [
|
|
86
86
|
"#e0834a",
|
|
@@ -153,7 +153,7 @@
|
|
|
153
153
|
],
|
|
154
154
|
"payloadBytes": 212917,
|
|
155
155
|
"archiveBytes": 44910,
|
|
156
|
-
"sha256": "
|
|
156
|
+
"sha256": "3894aadcfdba77eb351abc4381d3b76141ed0d6a15d8b75a982a64092e1300fa",
|
|
157
157
|
"download": "packages/ocean-1.0.0.tar.gz",
|
|
158
158
|
"swatch": [
|
|
159
159
|
"#2fa3b8",
|
|
@@ -226,7 +226,7 @@
|
|
|
226
226
|
],
|
|
227
227
|
"payloadBytes": 198517,
|
|
228
228
|
"archiveBytes": 31125,
|
|
229
|
-
"sha256": "
|
|
229
|
+
"sha256": "ff66fd400882e4ce3017b32782825aca59e60e5a407c793b175d2c4500c1cb97",
|
|
230
230
|
"download": "packages/graphite-1.0.0.tar.gz",
|
|
231
231
|
"swatch": [
|
|
232
232
|
"#8e9aa6",
|
|
@@ -299,7 +299,7 @@
|
|
|
299
299
|
],
|
|
300
300
|
"payloadBytes": 206517,
|
|
301
301
|
"archiveBytes": 34163,
|
|
302
|
-
"sha256": "
|
|
302
|
+
"sha256": "24dbf64bb96520a16efbfbb553ccff91190ae4fde5b18444f3c6a7ee97b0993f",
|
|
303
303
|
"download": "packages/bamboo-1.0.0.tar.gz",
|
|
304
304
|
"swatch": [
|
|
305
305
|
"#5aa85e",
|
|
@@ -367,7 +367,7 @@
|
|
|
367
367
|
],
|
|
368
368
|
"payloadBytes": 1576,
|
|
369
369
|
"archiveBytes": 1067,
|
|
370
|
-
"sha256": "
|
|
370
|
+
"sha256": "c3f6afae9ef5f47b622c74c2faf7174ba2ba38a9db2d19651a992874fd6f5f9a",
|
|
371
371
|
"download": "packages/theme-cycler-1.0.0.tar.gz",
|
|
372
372
|
"sourcePath": "packages/control/theme-cycler"
|
|
373
373
|
},
|
|
@@ -430,7 +430,7 @@
|
|
|
430
430
|
],
|
|
431
431
|
"payloadBytes": 3023,
|
|
432
432
|
"archiveBytes": 1808,
|
|
433
|
-
"sha256": "
|
|
433
|
+
"sha256": "ab7acdc88895a391596182fa9425e1e81d0b7ec5de73c035b9fe0ee2a7a3a123",
|
|
434
434
|
"download": "packages/webhook-notify-1.0.0.tar.gz",
|
|
435
435
|
"sourcePath": "packages/control/webhook-notify"
|
|
436
436
|
},
|
|
@@ -492,7 +492,7 @@
|
|
|
492
492
|
],
|
|
493
493
|
"payloadBytes": 2370,
|
|
494
494
|
"archiveBytes": 1450,
|
|
495
|
-
"sha256": "
|
|
495
|
+
"sha256": "43512f16e9559f5984ab9cf827b9b8e0e7c5671bc1cd1cf211adc0b0344e9f37",
|
|
496
496
|
"download": "packages/key-chime-1.0.0.tar.gz",
|
|
497
497
|
"sourcePath": "packages/control/key-chime"
|
|
498
498
|
},
|
|
@@ -554,7 +554,7 @@
|
|
|
554
554
|
],
|
|
555
555
|
"payloadBytes": 3940,
|
|
556
556
|
"archiveBytes": 1959,
|
|
557
|
-
"sha256": "
|
|
557
|
+
"sha256": "c131b7a0e67a9e0a1e4ab23b9dcf5ec05f7bb73c8b43621fe849c6762009c4e0",
|
|
558
558
|
"download": "packages/alarm-clock-1.0.0.tar.gz",
|
|
559
559
|
"sourcePath": "packages/control/alarm-clock"
|
|
560
560
|
},
|
|
@@ -621,7 +621,7 @@
|
|
|
621
621
|
],
|
|
622
622
|
"payloadBytes": 3793,
|
|
623
623
|
"archiveBytes": 1612,
|
|
624
|
-
"sha256": "
|
|
624
|
+
"sha256": "7f3d1045be427b2d4cbe0b070cb63ca0fc1fe127c63201f9423f23970d37f736",
|
|
625
625
|
"download": "packages/demo-echo-1.0.0.tar.gz",
|
|
626
626
|
"icon": {
|
|
627
627
|
"format": "indexed4",
|
|
@@ -702,7 +702,7 @@
|
|
|
702
702
|
],
|
|
703
703
|
"payloadBytes": 6098,
|
|
704
704
|
"archiveBytes": 2672,
|
|
705
|
-
"sha256": "
|
|
705
|
+
"sha256": "0a33b62f6f186a80d370012ddd146f9c504daf182503cc23cb4c13bd9c7e35d3",
|
|
706
706
|
"download": "packages/ollama-agent-1.0.0.tar.gz",
|
|
707
707
|
"icon": {
|
|
708
708
|
"format": "indexed4",
|
|
@@ -786,7 +786,7 @@
|
|
|
786
786
|
],
|
|
787
787
|
"payloadBytes": 5814,
|
|
788
788
|
"archiveBytes": 2456,
|
|
789
|
-
"sha256": "
|
|
789
|
+
"sha256": "541eef0c992b636cfe5d0b8f1f03dffa15a58b414f061ed392971bd3723a6c0f",
|
|
790
790
|
"download": "packages/shell-agent-1.0.0.tar.gz",
|
|
791
791
|
"icon": {
|
|
792
792
|
"format": "indexed4",
|
package/src/init.mjs
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 脚手架:`npx made-market init <目录> --kind theme|control|agent`。
|
|
3
|
+
*
|
|
4
|
+
* 生成的就是一个能通过服务端校验、可直接 publish 的最小包——模板里每个
|
|
5
|
+
* 字段都带中文注释说明"这里填什么、有什么约束",开发者改掉占位文案就能上架。
|
|
6
|
+
* 资产(锁屏图/图标/提示音)给的是尺寸合法的占位二进制,替换成真图即可。
|
|
7
|
+
*/
|
|
8
|
+
import fs from 'node:fs';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
import { PACKAGE_DIR } from '../lib/plugin.mjs';
|
|
11
|
+
import { renderIcon } from '../lib/icon.mjs';
|
|
12
|
+
|
|
13
|
+
const THEME_NAME = /^[A-Za-z0-9_-]{1,64}$/;
|
|
14
|
+
|
|
15
|
+
/** 240×320 锁屏底图:深灰纯色 RGB565(0x2104 → 字节序 04 21)。 */
|
|
16
|
+
function placeholderRgb565(width, height) {
|
|
17
|
+
const buffer = Buffer.alloc(width * height * 2);
|
|
18
|
+
for (let i = 0; i < width * height; i++) buffer.writeUInt16LE(0x2104, i * 2);
|
|
19
|
+
return buffer;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 0.3 秒 16 kHz 16-bit 单声道静音 WAV(尺寸与格式合法,替换成真音频即可)。 */
|
|
23
|
+
function placeholderWav() {
|
|
24
|
+
const sampleRate = 16000;
|
|
25
|
+
const frames = Math.round(sampleRate * 0.3);
|
|
26
|
+
const pcm = Buffer.alloc(frames * 2);
|
|
27
|
+
const header = Buffer.alloc(44);
|
|
28
|
+
header.write('RIFF', 0, 'ascii');
|
|
29
|
+
header.writeUInt32LE(36 + pcm.length, 4);
|
|
30
|
+
header.write('WAVE', 8, 'ascii');
|
|
31
|
+
header.write('fmt ', 12, 'ascii');
|
|
32
|
+
header.writeUInt32LE(16, 16);
|
|
33
|
+
header.writeUInt16LE(1, 20); // PCM
|
|
34
|
+
header.writeUInt16LE(1, 22); // 单声道
|
|
35
|
+
header.writeUInt32LE(sampleRate, 24);
|
|
36
|
+
header.writeUInt32LE(sampleRate * 2, 28);
|
|
37
|
+
header.writeUInt16LE(2, 32);
|
|
38
|
+
header.writeUInt16LE(16, 34);
|
|
39
|
+
header.write('data', 36, 'ascii');
|
|
40
|
+
header.writeUInt32LE(pcm.length, 40);
|
|
41
|
+
return Buffer.concat([header, pcm]);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const THEME_COLORS = {
|
|
45
|
+
accent: '5B8DEF', btn_normal: '2E3A4E', btn_cancel: '3A4658', border: '6C8CC7',
|
|
46
|
+
text_primary: 'EDF3FF', text_secondary: 'B9C8E2', text_hint: '8395B4', text_accent: 'A8C6FF',
|
|
47
|
+
title: 'F2F7FF', bg_page: '141C29', bg_panel: '1D2836', bg_row: '263344', danger: 'D06A6A', lock_bg: '0F1620',
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
function themeMarketJson(id) {
|
|
51
|
+
return {
|
|
52
|
+
id,
|
|
53
|
+
kind: 'theme',
|
|
54
|
+
version: '0.1.0',
|
|
55
|
+
name: '我的主题',
|
|
56
|
+
nameEn: 'My Theme',
|
|
57
|
+
summary: '一句话简介(不超过 200 字节),会显示在市场目录里。',
|
|
58
|
+
summaryEn: 'One-line summary shown in the market listing.',
|
|
59
|
+
description: '详细描述:配色思路、锁屏图内容、提示音风格。支持多段。\n\n锁屏图为 240×320 的 RGB565(bg.bin),锁屏图标为 112×112(icon.bin),都随包提供,不需要刷固件。',
|
|
60
|
+
descriptionEn: 'Long description in English.',
|
|
61
|
+
author: '你的名字',
|
|
62
|
+
license: 'MIT',
|
|
63
|
+
tags: ['示例'],
|
|
64
|
+
glyph: 'palette',
|
|
65
|
+
highlights: ['14 项配色一次替换,圆屏上保持高对比'],
|
|
66
|
+
requires: { apiVersion: 1 },
|
|
67
|
+
changelog: [{ version: '0.1.0', notes: '首个版本', notesEn: 'First release' }],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function themeJson() {
|
|
72
|
+
return {
|
|
73
|
+
title: '我的主题',
|
|
74
|
+
colors: { ...THEME_COLORS },
|
|
75
|
+
sound: { file: 'chime.wav' },
|
|
76
|
+
lock: { icon: 'icon.bin', icon_w: 112, icon_h: 112, bg: 'bg.bin', bg_w: 240, bg_h: 320 },
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function controlMarketJson(id, pluginId) {
|
|
81
|
+
return {
|
|
82
|
+
id,
|
|
83
|
+
kind: 'control',
|
|
84
|
+
version: '0.1.0',
|
|
85
|
+
pluginId,
|
|
86
|
+
name: '我的管控插件',
|
|
87
|
+
nameEn: 'My Control Plugin',
|
|
88
|
+
summary: '一句话简介:这个插件响应板子的什么事件、下发什么命令。',
|
|
89
|
+
summaryEn: 'One-line summary shown in the market listing.',
|
|
90
|
+
description: '详细描述工作方式。事件(events)与命令(commands)都只能用板侧公开清单里的名字,写别的设备不会执行。',
|
|
91
|
+
descriptionEn: 'Long description in English.',
|
|
92
|
+
author: '你的名字',
|
|
93
|
+
license: 'MIT',
|
|
94
|
+
tags: ['示例'],
|
|
95
|
+
glyph: 'terminal',
|
|
96
|
+
highlights: ['至少登记一个事件(管控插件的要求)'],
|
|
97
|
+
requires: { apiVersion: 1 },
|
|
98
|
+
changelog: [{ version: '0.1.0', notes: '首个版本', notesEn: 'First release' }],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function controlPluginMjs(pluginId) {
|
|
103
|
+
return `/**
|
|
104
|
+
* 管控插件模板:改掉 id/label,实现 onConnect / onEvent,就能发布。
|
|
105
|
+
*
|
|
106
|
+
* events 只能填板侧公开清单里的名字(boot.click / boot.double / boot.triple /
|
|
107
|
+
* boot.long);commands 只能填 caption.show / audio.play / theme.apply /
|
|
108
|
+
* volume.set / agent.select / session.* / voice.start / task.*。
|
|
109
|
+
* 语音通过 send({ name: 'audio.play' }, wav) 入队,send 在 onEvent 返回后仍有效。
|
|
110
|
+
*/
|
|
111
|
+
export default {
|
|
112
|
+
apiVersion: 1,
|
|
113
|
+
kind: 'board-plugin',
|
|
114
|
+
id: '${pluginId}',
|
|
115
|
+
label: '我的插件',
|
|
116
|
+
events: ['boot.click'],
|
|
117
|
+
commands: ['caption.show'],
|
|
118
|
+
probe: (config) => {
|
|
119
|
+
// 健康探测:桥接器会显示 available / reason。
|
|
120
|
+
return { available: true, reason: '模板插件,尚未实现逻辑' };
|
|
121
|
+
},
|
|
122
|
+
onConnect({ deviceId, config, send }) {
|
|
123
|
+
// 设备连上来时触发(可选实现)。
|
|
124
|
+
},
|
|
125
|
+
onEvent({ event, deviceId, config, send }) {
|
|
126
|
+
if (event.name === 'boot.click') {
|
|
127
|
+
return { commands: [{ name: 'caption.show', fields: { text: '你好,码得!' } }] };
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
},
|
|
131
|
+
onDisconnect({ deviceId }) {
|
|
132
|
+
// 设备断开时触发(可选实现)。
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
`;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function controlPluginJson(pluginId) {
|
|
139
|
+
return {
|
|
140
|
+
id: pluginId,
|
|
141
|
+
settings: [],
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function agentMarketJson(id, pluginId) {
|
|
146
|
+
return {
|
|
147
|
+
id,
|
|
148
|
+
kind: 'agent',
|
|
149
|
+
version: '0.1.0',
|
|
150
|
+
pluginId,
|
|
151
|
+
name: '我的 Agent 运行时',
|
|
152
|
+
nameEn: 'My Agent Runtime',
|
|
153
|
+
summary: '一句话简介:接的是哪个模型服务、怎么驱动码得。',
|
|
154
|
+
summaryEn: 'One-line summary shown in the market listing.',
|
|
155
|
+
description: '详细描述会话建立方式、模型接入与取消行为。',
|
|
156
|
+
descriptionEn: 'Long description in English.',
|
|
157
|
+
author: '你的名字',
|
|
158
|
+
license: 'MIT',
|
|
159
|
+
tags: ['示例'],
|
|
160
|
+
glyph: 'spark',
|
|
161
|
+
highlights: ['capabilities 需要覆盖 session / model / cancel / progress'],
|
|
162
|
+
requires: { apiVersion: 1 },
|
|
163
|
+
changelog: [{ version: '0.1.0', notes: '首个版本', notesEn: 'First release' }],
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** agent 模板的 capabilities:四个键写成一行字面量,静态扫描(scanPluginSource)才认得。 */
|
|
168
|
+
function agentPluginMjs(pluginId) {
|
|
169
|
+
return `/**
|
|
170
|
+
* agent 运行时模板:把云端模型会话接到码得。capabilities 静态扫描会检查
|
|
171
|
+
* session / model / cancel / progress 四个键是否齐全(缺了只是告警,但建议补全)。
|
|
172
|
+
* agent 包不接收板侧事件。
|
|
173
|
+
*/
|
|
174
|
+
const createSession = async ({ deviceId, config }) => {
|
|
175
|
+
// 建立(或复用)一个模型会话,返回会话句柄。
|
|
176
|
+
return { id: \`demo-\${deviceId}\`, model: 'demo-model' };
|
|
177
|
+
};
|
|
178
|
+
const currentModel = async ({ config }) => 'demo-model';
|
|
179
|
+
const cancelTask = async () => { /* 取消当前任务 */ };
|
|
180
|
+
const reportProgress = ({ state }) => { /* 上报进度 */ };
|
|
181
|
+
|
|
182
|
+
export default {
|
|
183
|
+
apiVersion: 1,
|
|
184
|
+
kind: 'coding-agent',
|
|
185
|
+
id: '${pluginId}',
|
|
186
|
+
label: '我的 Agent',
|
|
187
|
+
capabilities: { session: createSession, model: currentModel, cancel: cancelTask, progress: reportProgress },
|
|
188
|
+
};
|
|
189
|
+
`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function agentIconJson() {
|
|
193
|
+
// 与桥接器同一套 renderIcon:画一个 48×48 圆角方块占位图标(合法 indexed4)。
|
|
194
|
+
return renderIcon({ background: '#141C29', accent: '#5B8DEF', shapes: [[12, 12, 18, 18, '5B8DEF', 5]] });
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const SCHEMAS = {
|
|
198
|
+
theme: {
|
|
199
|
+
label: '主题',
|
|
200
|
+
files(id) {
|
|
201
|
+
return [
|
|
202
|
+
{ name: 'market.json', data: `${JSON.stringify(themeMarketJson(id), null, 2)}\n` },
|
|
203
|
+
{ name: 'theme.json', data: `${JSON.stringify(themeJson(), null, 2)}\n` },
|
|
204
|
+
{ name: 'bg.bin', data: placeholderRgb565(240, 320) },
|
|
205
|
+
{ name: 'icon.bin', data: placeholderRgb565(112, 112) },
|
|
206
|
+
{ name: 'chime.wav', data: placeholderWav() },
|
|
207
|
+
];
|
|
208
|
+
},
|
|
209
|
+
},
|
|
210
|
+
control: {
|
|
211
|
+
label: '管控',
|
|
212
|
+
files(id) {
|
|
213
|
+
const pluginId = id.replace(/-/g, '_');
|
|
214
|
+
return [
|
|
215
|
+
{ name: 'market.json', data: `${JSON.stringify(controlMarketJson(id, pluginId), null, 2)}\n` },
|
|
216
|
+
{ name: 'plugin.mjs', data: controlPluginMjs(pluginId) },
|
|
217
|
+
{ name: 'plugin.json', data: `${JSON.stringify(controlPluginJson(pluginId), null, 2)}\n` },
|
|
218
|
+
{ name: 'settings.json', data: '{}\n' },
|
|
219
|
+
];
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
agent: {
|
|
223
|
+
label: 'agent 运行时',
|
|
224
|
+
files(id) {
|
|
225
|
+
const pluginId = id.replace(/-/g, '_');
|
|
226
|
+
return [
|
|
227
|
+
{ name: 'market.json', data: `${JSON.stringify(agentMarketJson(id, pluginId), null, 2)}\n` },
|
|
228
|
+
{ name: 'plugin.mjs', data: agentPluginMjs(pluginId) },
|
|
229
|
+
{ name: 'plugin.json', data: `${JSON.stringify({ id: pluginId, settings: [] }, null, 2)}\n` },
|
|
230
|
+
{ name: 'icon.json', data: `${JSON.stringify(agentIconJson(), null, 2)}\n` },
|
|
231
|
+
];
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* 生成脚手架。目录不存在则创建;已存在且非空时拒绝(除非 force)。
|
|
238
|
+
* @returns {string[]} 写入的文件路径列表
|
|
239
|
+
*/
|
|
240
|
+
export function scaffold({ dir, kind, force = false }) {
|
|
241
|
+
const schema = SCHEMAS[kind];
|
|
242
|
+
if (!schema) throw new Error(`kind 必须是 theme / control / agent 之一`);
|
|
243
|
+
const id = path.basename(path.resolve(dir));
|
|
244
|
+
if (!PACKAGE_DIR.test(id)) throw new Error(`包目录名 ${id} 必须符合 ^[a-z0-9][a-z0-9_-]{0,39}$(它会成为包 ID)`);
|
|
245
|
+
if (kind === 'theme' && !THEME_NAME.test(id)) throw new Error(`主题目录名 ${id} 不符合 ^[A-Za-z0-9_-]{1,64}$`);
|
|
246
|
+
if (fs.existsSync(dir) && fs.readdirSync(dir).length && !force) {
|
|
247
|
+
throw new Error(`目录 ${dir} 不是空的;确认要覆盖就加 --force`);
|
|
248
|
+
}
|
|
249
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
250
|
+
return schema.files(id).map((file) => {
|
|
251
|
+
const target = path.join(dir, file.name);
|
|
252
|
+
fs.writeFileSync(target, file.data);
|
|
253
|
+
return target;
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export const SCAFFOLD_KINDS = Object.keys(SCHEMAS);
|
package/src/install.mjs
CHANGED
|
@@ -15,17 +15,24 @@ import fs from 'node:fs';
|
|
|
15
15
|
import os from 'node:os';
|
|
16
16
|
import path from 'node:path';
|
|
17
17
|
import zlib from 'node:zlib';
|
|
18
|
-
import { KINDS, ROOT, sha256 } from '../lib/market.mjs';
|
|
18
|
+
import { KINDS, PKG_ROOT, ROOT, sha256 } from '../lib/market.mjs';
|
|
19
19
|
import { extractTar } from '../lib/tar.mjs';
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
/** 自带索引:仓库里是 cli/registry/(build 生成物),发布包里就是包根下的 registry/。 */
|
|
22
|
+
const BUNDLED_REGISTRY = path.join(PKG_ROOT, 'registry', 'index.json');
|
|
23
|
+
|
|
24
|
+
/** 官方市场:install/list/search 不带参数时的默认来源。 */
|
|
25
|
+
export const DEFAULT_SERVER = 'https://made-market.link';
|
|
22
26
|
|
|
23
27
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
28
|
+
* 索引来源:显式 --registry > $MADE_MARKET_REGISTRY > $MADE_MARKET_SERVER > 官方市场。
|
|
29
|
+
* 全部指向远端;远端不可用时 loadRegistry 自动回落包内自带索引(离线可用)。
|
|
26
30
|
*/
|
|
27
31
|
export function resolveRegistry(explicit = '') {
|
|
28
|
-
|
|
32
|
+
if (explicit) return explicit;
|
|
33
|
+
if (process.env.MADE_MARKET_REGISTRY) return process.env.MADE_MARKET_REGISTRY;
|
|
34
|
+
const server = (process.env.MADE_MARKET_SERVER || DEFAULT_SERVER).replace(/\/+$/, '');
|
|
35
|
+
return `${server}/registry/index.json`;
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
/** 包目录根:--home > MADE_HOME > ~/made。与桥接器 packagesHome() 保持同一套规则。 */
|
|
@@ -53,7 +60,7 @@ const usage = `用法:node tools/install.mjs <包 ID…> [选项]
|
|
|
53
60
|
|
|
54
61
|
选项
|
|
55
62
|
--home <目录> 包目录根,默认 $MADE_HOME 或 ~/made
|
|
56
|
-
--registry <路径|URL>
|
|
63
|
+
--registry <路径|URL> 索引来源。默认官方市场实时索引($MADE_MARKET_REGISTRY / $MADE_MARKET_SERVER 可覆盖),远端不可用时回落包内自带索引(离线可装示例包)
|
|
57
64
|
--force 目标目录已存在时覆盖
|
|
58
65
|
--dry-run 只校验并列出将要写入的文件
|
|
59
66
|
--list 列出索引里的全部包
|