made-market 0.1.1 → 0.2.0

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.
@@ -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 全量重建 PG 索引
45
+ rebuild-from-store 管理命令:从存储 manifests 全量重建 SQLite 索引
43
46
 
44
47
  选项
45
48
  --home <目录> 包目录根,默认 $MADE_HOME 或 ~/made
46
- --registry <路径|URL> 索引来源。默认优先 $MADE_MARKET_REGISTRY(如市场服务的索引),
47
- 不可用或未设置时回落到本包自带的索引(离线可用)
48
- --server <URL> 市场服务地址(register/login/publish;默认 $MADE_MARKET_SERVER,
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
- --kind theme|control|agent 只列某一类(list / search)
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
- // 索引来源按需加载:--registry > $MADE_MARKET_REGISTRY > 包内自带(远端失败自动回落,离线可用)。
182
- // rebuild-from-store 与 install --server(API 模式)不依赖全量索引。
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
- // 配了 --server / $MADE_MARKET_SERVER:按包直查 API,不拉全量索引(大市场友好)。
236
- if (options.server || process.env.MADE_MARKET_SERVER) {
237
- const { packages, base } = await resolveViaApi(options.args, options.server || process.env.MADE_MARKET_SERVER);
238
- await install(options.args, { home: options.home, registry: base, index: { packages }, force: options.force, dryRun: options.dryRun });
239
- return;
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
- const { createPool, initSchema, rebuildFromStore } = await import('../server/db.mjs');
282
- const { createStore } = await import('../server/store.mjs');
283
- if (!process.env.DATABASE_URL) throw new Error('rebuild-from-store 需要 DATABASE_URL 指向 PG 索引库');
284
- const db = createPool(process.env.DATABASE_URL);
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
- await db.close();
338
+ db.close();
291
339
  }
292
340
  return;
293
341
  }
@@ -0,0 +1,208 @@
1
+ /**
2
+ * 管控插件的开放能力清单(apiVersion 1 的能力面真源)。
3
+ *
4
+ * 单一真源,三处消费:init 脚手架把它生成进包里(capabilities.json +
5
+ * contracts.d.ts,作者写插件时编辑器直接有提示)、市场网站说明页按它排版、
6
+ * 桥接器侧的实现(made 仓库 enhance.mjs)是它的运行时对应物。改能力面时
7
+ * 先改这里,再同步 made 仓库 AGENTS.md 的清单表与 site/capabilities.html。
8
+ *
9
+ * 两条供给线:
10
+ * - board:事件与命令,清单由固件上报(announceBoardCatalog),扩清单要刷固件;
11
+ * - bridge:桥接器进程内露出的句柄(enhance.mjs),重启桥接器即生效。
12
+ */
13
+
14
+ export const CAPABILITIES = {
15
+ apiVersion: 1,
16
+ /** 能力清单版本:加能力不升它;改既有能力的语义/形状才升,并同步桥接器。 */
17
+ schemaVersion: 1,
18
+ board: {
19
+ events: [
20
+ { name: 'boot.click', cadence: 'edge', desc: '按键短按(语音意图)' },
21
+ { name: 'boot.double', cadence: 'edge', desc: '双击(确认)' },
22
+ { name: 'boot.triple', cadence: 'edge', desc: '三击(取消)' },
23
+ { name: 'boot.long', cadence: 'edge', desc: '长按(回主页)' },
24
+ ],
25
+ commands: [
26
+ { name: 'caption.show', desc: '屏幕字幕,fields.text 为正文' },
27
+ { name: 'audio.play', desc: '播放音频,可附带 WAV:16-bit PCM、单声道、8k–48kHz、≤256KB' },
28
+ { name: 'theme.apply', desc: '应用主题插件' },
29
+ { name: 'volume.set', desc: '设置音量' },
30
+ { name: 'agent.select', desc: '切换当前助手' },
31
+ { name: 'session.create', desc: '新建会话' },
32
+ { name: 'session.select', desc: '切换会话' },
33
+ { name: 'session.delete', desc: '删除会话' },
34
+ { name: 'voice.start', desc: '主动点亮录音' },
35
+ { name: 'task.confirm', desc: '批准待确认任务' },
36
+ { name: 'task.cancel', desc: '取消任务' },
37
+ ],
38
+ },
39
+ /** onJob 收到的任务生命周期,type 字段的全部取值(按典型顺序排列)。 */
40
+ jobLifecycle: ['created', 'confirmed', 'started', 'progress', 'completed', 'handed_off', 'failed', 'cancelled'],
41
+ /** 回合管道:按插件注册顺序成链,返回字符串即 override,最后一个说话的算数。 */
42
+ turnPipes: [
43
+ { name: 'onTurnInput', context: ['instruction', 'job', 'agentId', 'config', 'bridge'], desc: '改写本轮实际执行的指令;存储里的任务原文不动' },
44
+ { name: 'onTurnOutput', context: ['result', 'job', 'agentId', 'status', 'config', 'bridge'], desc: '仅在任务完成时调用,改写回给用户的结果' },
45
+ ],
46
+ /** bridge 门面:事件、钩子、通知的上下文里都拿得到。 */
47
+ bridge: [
48
+ { name: 'agents', signature: 'agents()', desc: '全部助手(含可用性与探测原因)' },
49
+ { name: 'agent', signature: 'agent(id)', desc: '按 id 查单个助手' },
50
+ { name: 'jobs', signature: 'jobs()', desc: '全部任务' },
51
+ { name: 'job', signature: 'job(id)', desc: '按 id 查单个任务' },
52
+ { name: 'sessions', signature: 'sessions(agentId?)', desc: '会话清单,可按助手过滤' },
53
+ { name: 'createTask', signature: 'createTask({instruction, agentId?, projectId?, title?})', desc: '建会话并落一个待确认任务,返回 {job, session};限流 10 次/分钟' },
54
+ { name: 'submitTurn', signature: 'submitTurn({instruction, sessionId})', desc: '向既有会话追加一轮(同样落待确认)' },
55
+ { name: 'confirmJob', signature: 'confirmJob(id)', desc: '批准待确认任务,批准后立即执行' },
56
+ { name: 'cancelJob', signature: 'cancelJob(id)', desc: '取消任务' },
57
+ { name: 'themes', signature: 'themes()', desc: '已安装主题清单' },
58
+ { name: 'devices', signature: 'devices()', desc: '当前在线设备 id 列表' },
59
+ { name: 'send', signature: 'send(deviceId, command, audio?)', desc: '向指定设备入队一条板侧命令(须在板侧清单内)' },
60
+ { name: 'pending', signature: 'pending(deviceId)', desc: '该设备尚未取走的命令队列' },
61
+ ],
62
+ limits: {
63
+ hookTimeoutMs: 1500,
64
+ createTaskPerMinute: 10,
65
+ audioFormat: '16-bit PCM WAV,单声道,8k–48kHz,≤256KB',
66
+ },
67
+ };
68
+
69
+ /** 生成进包里的 capabilities.json(作者可读,也方便脚本化消费)。 */
70
+ export const capabilitiesJson = () => `${JSON.stringify(CAPABILITIES, null, 2)}\n`;
71
+
72
+ /**
73
+ * 生成进包里的 contracts.d.ts:只描述插件作者看得见、用得上的接口面。
74
+ * 类型仅给编辑器提示,运行时不加载(plugin.mjs 是纯 JS)。
75
+ */
76
+ export const contractsDts = () => `/**
77
+ * 码得管控插件 · 接口契约(apiVersion ${CAPABILITIES.apiVersion})。
78
+ * 由 npx made-market init 生成,运行时对应桥接器的 bridge/src/plugins/enhance.mjs。
79
+ * 能力清单(事件/命令/句柄/限制)见同目录 capabilities.json。
80
+ */
81
+
82
+ export interface BoardEvent {
83
+ name: string;
84
+ fields: Record<string, string>;
85
+ }
86
+
87
+ export interface BoardCommand {
88
+ name: string;
89
+ fields?: Record<string, string>;
90
+ }
91
+
92
+ export interface PluginConfig {
93
+ /** 各插件的设置:pluginSettings[<你的插件 id>] */
94
+ pluginSettings: Record<string, Record<string, string>>;
95
+ /** 用户当前默认助手 id */
96
+ defaultProvider: string;
97
+ [key: string]: unknown;
98
+ }
99
+
100
+ export interface JobView {
101
+ id: string;
102
+ sessionId: string;
103
+ instruction: string;
104
+ status: string;
105
+ /** 执行该任务的助手 id */
106
+ provider: string;
107
+ projectId?: string;
108
+ /** 任务来源:'voice' | 'plugin' | ... */
109
+ source?: string;
110
+ result?: string;
111
+ progress?: string;
112
+ createdAt: string;
113
+ updatedAt: string;
114
+ [key: string]: unknown;
115
+ }
116
+
117
+ export interface SessionView {
118
+ id: string;
119
+ provider: string;
120
+ title?: string;
121
+ [key: string]: unknown;
122
+ }
123
+
124
+ export interface AgentView {
125
+ id: string;
126
+ label: string;
127
+ available: boolean;
128
+ reason: string;
129
+ mode: string;
130
+ capabilities?: unknown;
131
+ }
132
+
133
+ /** 桥接器句柄:事件、钩子、通知的上下文里都拿得到(context.bridge)。 */
134
+ export interface Bridge {
135
+ agents(): AgentView[];
136
+ agent(id: string): AgentView | undefined;
137
+ jobs(): JobView[];
138
+ job(id: string): JobView | undefined;
139
+ sessions(agentId?: string): SessionView[];
140
+ createTask(options: { instruction: string; agentId?: string; projectId?: string; title?: string }):
141
+ { job: JobView; session: SessionView };
142
+ submitTurn(options: { instruction: string; sessionId: string }): JobView;
143
+ confirmJob(id: string): JobView;
144
+ cancelJob(id: string): JobView;
145
+ themes(): Array<Record<string, unknown>>;
146
+ devices(): string[];
147
+ send(deviceId: string, command: BoardCommand, audio?: Uint8Array): unknown;
148
+ pending(deviceId: string): unknown[];
149
+ }
150
+
151
+ export type JobEventType =
152
+ | 'created' | 'confirmed' | 'started' | 'progress'
153
+ | 'completed' | 'handed_off' | 'failed' | 'cancelled';
154
+
155
+ export interface PluginBase {
156
+ apiVersion: 1;
157
+ kind: 'board-plugin';
158
+ id: string;
159
+ label: string;
160
+ /** 健康探测:工作台据此显示 available / reason。 */
161
+ probe: (config: PluginConfig) => { available: boolean; reason?: string };
162
+
163
+ /** 板侧事件:events 里登记过的才会被上报。返回 {commands} 或调 context.send 入队。 */
164
+ onEvent(context: {
165
+ event: BoardEvent;
166
+ deviceId: string;
167
+ config: PluginConfig;
168
+ send: (command: BoardCommand, audio?: Uint8Array) => unknown;
169
+ bridge: Bridge;
170
+ }): Promise<{ commands?: BoardCommand[] } | void> | { commands?: BoardCommand[] } | void;
171
+
172
+ onConnect?(context: {
173
+ deviceId: string;
174
+ config: PluginConfig;
175
+ send: (command: BoardCommand, audio?: Uint8Array) => unknown;
176
+ bridge: Bridge;
177
+ }): Promise<void> | void;
178
+
179
+ onDisconnect?(context: { deviceId: string }): Promise<void> | void;
180
+
181
+ /** 任务生命周期通知(${CAPABILITIES.jobLifecycle.join(' / ')})。 */
182
+ onJob?(context: {
183
+ type: JobEventType;
184
+ job: JobView;
185
+ config: PluginConfig;
186
+ bridge: Bridge;
187
+ }): Promise<void> | void;
188
+
189
+ /** 回合管道:返回字符串即 override。超时 ${CAPABILITIES.limits.hookTimeoutMs}ms 或抛错则沿用上一环。 */
190
+ onTurnInput?(context: {
191
+ instruction: string;
192
+ job: JobView;
193
+ agentId: string;
194
+ config: PluginConfig;
195
+ bridge: Bridge;
196
+ }): Promise<string | void> | string | void;
197
+
198
+ /** 仅在任务完成时调用;返回字符串即改写回给用户的结果。 */
199
+ onTurnOutput?(context: {
200
+ result: string;
201
+ job: JobView;
202
+ agentId: string;
203
+ status: 'completed';
204
+ config: PluginConfig;
205
+ bridge: Bridge;
206
+ }): Promise<string | void> | string | void;
207
+ }
208
+ `;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "made-market",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "码得(made)公开插件市场的命令行工具:列出、搜索、安装主题/管控/agent 包,以及登录后发布新包。市场服务端在 github.com/jobop/made-market 仓库内,不随本包分发。",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -80,7 +80,7 @@
80
80
  ],
81
81
  "payloadBytes": 209717,
82
82
  "archiveBytes": 39822,
83
- "sha256": "f99ab016bda97781fba979cf5d31ffec18edf1ea91a2339c1d7e4cf63f36a0a7",
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": "a48c12c45c83185041a25ea0f408f8ce15a1632d4473440a0a1fbc7b5f674cd1",
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": "9dd6037414f9303ecc507c4348380baeaf8dd2a97c98c58748410c18b37c014e",
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": "76095ce19587f8e0bc7ba5aeb7ca62f1e622bc8ef14847fdb3479dbf0ea87b29",
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": "35c4768f607bb936b2e263e972895920c66cce74a8829d944cd86c6d2e5b0030",
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": "0c7c6453b78a76c327282dfd62f6d023e90dc12a2c7458a67b1b43e5e20e15ba",
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": "cea53eedab0232272ed6f5ebd5c6b8a7250ff08240fb2d754b16b1e7c9088d7a",
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": "bd4c7ae744be6a99dacaf663a0624e61e568f8da5e83566eb104127d39ddf11d",
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": "aaf99b7ab9af49712f9ff5da37695c4d41cf89e09ae6ec5168057dede7075546",
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": "8da05c474aa4b0d3d8913e16109b0ed887a4bab2d1d136527bb85bd798860cff",
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": "12de415fa0bb0bfaa3eb119c5e0930b3d3239836d184eab781dc1b36be93d06a",
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,291 @@
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
+ import { capabilitiesJson, contractsDts } from '../lib/capabilities.mjs';
13
+
14
+ const THEME_NAME = /^[A-Za-z0-9_-]{1,64}$/;
15
+
16
+ /** 240×320 锁屏底图:深灰纯色 RGB565(0x2104 → 字节序 04 21)。 */
17
+ function placeholderRgb565(width, height) {
18
+ const buffer = Buffer.alloc(width * height * 2);
19
+ for (let i = 0; i < width * height; i++) buffer.writeUInt16LE(0x2104, i * 2);
20
+ return buffer;
21
+ }
22
+
23
+ /** 0.3 秒 16 kHz 16-bit 单声道静音 WAV(尺寸与格式合法,替换成真音频即可)。 */
24
+ function placeholderWav() {
25
+ const sampleRate = 16000;
26
+ const frames = Math.round(sampleRate * 0.3);
27
+ const pcm = Buffer.alloc(frames * 2);
28
+ const header = Buffer.alloc(44);
29
+ header.write('RIFF', 0, 'ascii');
30
+ header.writeUInt32LE(36 + pcm.length, 4);
31
+ header.write('WAVE', 8, 'ascii');
32
+ header.write('fmt ', 12, 'ascii');
33
+ header.writeUInt32LE(16, 16);
34
+ header.writeUInt16LE(1, 20); // PCM
35
+ header.writeUInt16LE(1, 22); // 单声道
36
+ header.writeUInt32LE(sampleRate, 24);
37
+ header.writeUInt32LE(sampleRate * 2, 28);
38
+ header.writeUInt16LE(2, 32);
39
+ header.writeUInt16LE(16, 34);
40
+ header.write('data', 36, 'ascii');
41
+ header.writeUInt32LE(pcm.length, 40);
42
+ return Buffer.concat([header, pcm]);
43
+ }
44
+
45
+ const THEME_COLORS = {
46
+ accent: '5B8DEF', btn_normal: '2E3A4E', btn_cancel: '3A4658', border: '6C8CC7',
47
+ text_primary: 'EDF3FF', text_secondary: 'B9C8E2', text_hint: '8395B4', text_accent: 'A8C6FF',
48
+ title: 'F2F7FF', bg_page: '141C29', bg_panel: '1D2836', bg_row: '263344', danger: 'D06A6A', lock_bg: '0F1620',
49
+ };
50
+
51
+ function themeMarketJson(id) {
52
+ return {
53
+ id,
54
+ kind: 'theme',
55
+ version: '0.1.0',
56
+ name: '我的主题',
57
+ nameEn: 'My Theme',
58
+ summary: '一句话简介(不超过 200 字节),会显示在市场目录里。',
59
+ summaryEn: 'One-line summary shown in the market listing.',
60
+ description: '详细描述:配色思路、锁屏图内容、提示音风格。支持多段。\n\n锁屏图为 240×320 的 RGB565(bg.bin),锁屏图标为 112×112(icon.bin),都随包提供,不需要刷固件。',
61
+ descriptionEn: 'Long description in English.',
62
+ author: '你的名字',
63
+ license: 'MIT',
64
+ tags: ['示例'],
65
+ glyph: 'palette',
66
+ highlights: ['14 项配色一次替换,圆屏上保持高对比'],
67
+ requires: { apiVersion: 1 },
68
+ changelog: [{ version: '0.1.0', notes: '首个版本', notesEn: 'First release' }],
69
+ };
70
+ }
71
+
72
+ function themeJson() {
73
+ return {
74
+ title: '我的主题',
75
+ colors: { ...THEME_COLORS },
76
+ sound: { file: 'chime.wav' },
77
+ lock: { icon: 'icon.bin', icon_w: 112, icon_h: 112, bg: 'bg.bin', bg_w: 240, bg_h: 320 },
78
+ };
79
+ }
80
+
81
+ function controlMarketJson(id, pluginId) {
82
+ return {
83
+ id,
84
+ kind: 'control',
85
+ version: '0.1.0',
86
+ pluginId,
87
+ name: '我的管控插件',
88
+ nameEn: 'My Control Plugin',
89
+ summary: '一句话简介:这个插件响应板子的什么事件、下发什么命令。',
90
+ summaryEn: 'One-line summary shown in the market listing.',
91
+ description: '详细描述工作方式。事件(events)与命令(commands)都只能用板侧公开清单里的名字,写别的设备不会执行。',
92
+ descriptionEn: 'Long description in English.',
93
+ author: '你的名字',
94
+ license: 'MIT',
95
+ tags: ['示例'],
96
+ glyph: 'terminal',
97
+ highlights: ['至少登记一个事件(管控插件的要求)'],
98
+ requires: { apiVersion: 1 },
99
+ changelog: [{ version: '0.1.0', notes: '首个版本', notesEn: 'First release' }],
100
+ };
101
+ }
102
+
103
+ function controlPluginMjs(pluginId) {
104
+ return `/**
105
+ * 管控插件模板:改掉 id/label,挑需要的钩子实现,就能发布。
106
+ *
107
+ * 能力清单(板侧事件/命令、任务生命周期、bridge 句柄、限流)见同目录
108
+ * capabilities.json;接口类型见 contracts.d.ts(编辑器自动提示)。
109
+ *
110
+ * events/commands 只能填板侧清单里的名字:
111
+ * - 事件:boot.click / boot.double / boot.triple / boot.long
112
+ * - 命令:caption.show / audio.play / theme.apply / volume.set / agent.select /
113
+ * session.create / session.select / session.delete / voice.start / task.confirm / task.cancel
114
+ */
115
+ export default {
116
+ apiVersion: 1,
117
+ kind: 'board-plugin',
118
+ id: '${pluginId}',
119
+ label: '我的插件',
120
+ events: ['boot.click'],
121
+ commands: ['caption.show'],
122
+ probe: (config) => {
123
+ // 健康探测:桥接器会显示 available / reason。
124
+ return { available: true, reason: '模板插件,尚未实现逻辑' };
125
+ },
126
+
127
+ // ── 触发面一:板侧事件 ────────────────────────────────────────────
128
+ onEvent({ event, deviceId, config, send, bridge }) {
129
+ if (event.name === 'boot.click') {
130
+ // 两条出命令的路:返回 {commands},或稍后调 send(command, audio?)。
131
+ return { commands: [{ name: 'caption.show', fields: { text: '你好,码得!' } }] };
132
+ }
133
+ return null;
134
+ },
135
+
136
+ // ── 触发面二:任务生命周期(可选)────────────────────────────────
137
+ // type: created / confirmed / started / progress / completed / handed_off /
138
+ // failed / cancelled。注意:插件自建的任务不广播 created(防自激)。
139
+ // onJob({ type, job, bridge }) {
140
+ // if (type === 'completed' && job.provider === '某助手id') {
141
+ // // 例:把操作日志上报远端、或在板上播报完成。
142
+ // }
143
+ // },
144
+
145
+ // ── 触发面三:回合管道(可选,override 权限最高)─────────────────
146
+ // 按插件注册顺序成链,返回字符串即改写,最后一个说话的算数;
147
+ // 超时 1500ms 或抛错则沿用上一环,绝不阻断任务本身。
148
+ // onTurnOutput({ result, job, agentId }) {
149
+ // if (agentId !== '某助手id') return; // 只加工目标助手
150
+ // return \`\${result}\\n—— 由我的插件加工\`;
151
+ // },
152
+
153
+ // ── 自主行为:bridge 句柄(可选)────────────────────────────────
154
+ // bridge.agents() / bridge.createTask({...})(落待确认任务,限流 10 次/分钟)/
155
+ // bridge.confirmJob(id) / bridge.send(deviceId, {name:'audio.play'}, wav) ……
156
+ // 全部句柄与签名见 capabilities.json 的 bridge 一节。
157
+
158
+ onConnect({ deviceId, config, send, bridge }) {
159
+ // 设备连上来时触发(可选实现)。
160
+ },
161
+ onDisconnect({ deviceId }) {
162
+ // 设备断开时触发(可选实现)。
163
+ },
164
+ };
165
+ `;
166
+ }
167
+
168
+ function controlPluginJson(pluginId) {
169
+ return {
170
+ id: pluginId,
171
+ settings: [],
172
+ };
173
+ }
174
+
175
+ function agentMarketJson(id, pluginId) {
176
+ return {
177
+ id,
178
+ kind: 'agent',
179
+ version: '0.1.0',
180
+ pluginId,
181
+ name: '我的 Agent 运行时',
182
+ nameEn: 'My Agent Runtime',
183
+ summary: '一句话简介:接的是哪个模型服务、怎么驱动码得。',
184
+ summaryEn: 'One-line summary shown in the market listing.',
185
+ description: '详细描述会话建立方式、模型接入与取消行为。',
186
+ descriptionEn: 'Long description in English.',
187
+ author: '你的名字',
188
+ license: 'MIT',
189
+ tags: ['示例'],
190
+ glyph: 'spark',
191
+ highlights: ['capabilities 需要覆盖 session / model / cancel / progress'],
192
+ requires: { apiVersion: 1 },
193
+ changelog: [{ version: '0.1.0', notes: '首个版本', notesEn: 'First release' }],
194
+ };
195
+ }
196
+
197
+ /** agent 模板的 capabilities:四个键写成一行字面量,静态扫描(scanPluginSource)才认得。 */
198
+ function agentPluginMjs(pluginId) {
199
+ return `/**
200
+ * agent 运行时模板:把云端模型会话接到码得。capabilities 静态扫描会检查
201
+ * session / model / cancel / progress 四个键是否齐全(缺了只是告警,但建议补全)。
202
+ * agent 包不接收板侧事件。
203
+ */
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
+ export default {
213
+ apiVersion: 1,
214
+ kind: 'coding-agent',
215
+ id: '${pluginId}',
216
+ label: '我的 Agent',
217
+ capabilities: { session: createSession, model: currentModel, cancel: cancelTask, progress: reportProgress },
218
+ };
219
+ `;
220
+ }
221
+
222
+ function agentIconJson() {
223
+ // 与桥接器同一套 renderIcon:画一个 48×48 圆角方块占位图标(合法 indexed4)。
224
+ return renderIcon({ background: '#141C29', accent: '#5B8DEF', shapes: [[12, 12, 18, 18, '5B8DEF', 5]] });
225
+ }
226
+
227
+ const SCHEMAS = {
228
+ theme: {
229
+ label: '主题',
230
+ files(id) {
231
+ return [
232
+ { name: 'market.json', data: `${JSON.stringify(themeMarketJson(id), null, 2)}\n` },
233
+ { name: 'theme.json', data: `${JSON.stringify(themeJson(), null, 2)}\n` },
234
+ { name: 'bg.bin', data: placeholderRgb565(240, 320) },
235
+ { name: 'icon.bin', data: placeholderRgb565(112, 112) },
236
+ { name: 'chime.wav', data: placeholderWav() },
237
+ ];
238
+ },
239
+ },
240
+ control: {
241
+ label: '管控',
242
+ files(id) {
243
+ const pluginId = id.replace(/-/g, '_');
244
+ return [
245
+ { name: 'market.json', data: `${JSON.stringify(controlMarketJson(id, pluginId), null, 2)}\n` },
246
+ { name: 'plugin.mjs', data: controlPluginMjs(pluginId) },
247
+ { name: 'plugin.json', data: `${JSON.stringify(controlPluginJson(pluginId), null, 2)}\n` },
248
+ { name: 'settings.json', data: '{}\n' },
249
+ // 能力 SDK:清单(机器可读)+ 类型契约(编辑器提示)。随包发布,
250
+ // 装到 ~/made/plugins/<id>/ 后依然在,作者与使用者都能查。
251
+ { name: 'capabilities.json', data: capabilitiesJson() },
252
+ { name: 'contracts.d.ts', data: contractsDts() },
253
+ ];
254
+ },
255
+ },
256
+ agent: {
257
+ label: 'agent 运行时',
258
+ files(id) {
259
+ const pluginId = id.replace(/-/g, '_');
260
+ return [
261
+ { name: 'market.json', data: `${JSON.stringify(agentMarketJson(id, pluginId), null, 2)}\n` },
262
+ { name: 'plugin.mjs', data: agentPluginMjs(pluginId) },
263
+ { name: 'plugin.json', data: `${JSON.stringify({ id: pluginId, settings: [] }, null, 2)}\n` },
264
+ { name: 'icon.json', data: `${JSON.stringify(agentIconJson(), null, 2)}\n` },
265
+ ];
266
+ },
267
+ },
268
+ };
269
+
270
+ /**
271
+ * 生成脚手架。目录不存在则创建;已存在且非空时拒绝(除非 force)。
272
+ * @returns {string[]} 写入的文件路径列表
273
+ */
274
+ export function scaffold({ dir, kind, force = false }) {
275
+ const schema = SCHEMAS[kind];
276
+ if (!schema) throw new Error(`kind 必须是 theme / control / agent 之一`);
277
+ const id = path.basename(path.resolve(dir));
278
+ if (!PACKAGE_DIR.test(id)) throw new Error(`包目录名 ${id} 必须符合 ^[a-z0-9][a-z0-9_-]{0,39}$(它会成为包 ID)`);
279
+ if (kind === 'theme' && !THEME_NAME.test(id)) throw new Error(`主题目录名 ${id} 不符合 ^[A-Za-z0-9_-]{1,64}$`);
280
+ if (fs.existsSync(dir) && fs.readdirSync(dir).length && !force) {
281
+ throw new Error(`目录 ${dir} 不是空的;确认要覆盖就加 --force`);
282
+ }
283
+ fs.mkdirSync(dir, { recursive: true });
284
+ return schema.files(id).map((file) => {
285
+ const target = path.join(dir, file.name);
286
+ fs.writeFileSync(target, file.data);
287
+ return target;
288
+ });
289
+ }
290
+
291
+ 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
- const BUNDLED_REGISTRY = path.join(ROOT, 'registry', 'index.json');
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
- * 默认远端索引:显式 --registry > 环境变量 MADE_MARKET_REGISTRY > 包内自带索引。
25
- * 桥接包发布到 OSS 后,把 OSS 索引地址写进环境变量或作为 --registry 传入即可。
28
+ * 索引来源:显式 --registry > $MADE_MARKET_REGISTRY > $MADE_MARKET_SERVER > 官方市场。
29
+ * 全部指向远端;远端不可用时 loadRegistry 自动回落包内自带索引(离线可用)。
26
30
  */
27
31
  export function resolveRegistry(explicit = '') {
28
- return explicit || process.env.MADE_MARKET_REGISTRY || BUNDLED_REGISTRY;
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> 索引来源。默认优先 $MADE_MARKET_REGISTRY(远端),不可用或未设置时用包内自带索引(离线可用)
63
+ --registry <路径|URL> 索引来源。默认官方市场实时索引($MADE_MARKET_REGISTRY / $MADE_MARKET_SERVER 可覆盖),远端不可用时回落包内自带索引(离线可装示例包)
57
64
  --force 目标目录已存在时覆盖
58
65
  --dry-run 只校验并列出将要写入的文件
59
66
  --list 列出索引里的全部包