@syllm/brickly-sdk 0.1.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.
@@ -0,0 +1,470 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.generateDependencyTypes = generateDependencyTypes;
4
+ const node_fs_1 = require("node:fs");
5
+ const node_path_1 = require("node:path");
6
+ const RESERVED_WORDS = new Set([
7
+ 'break',
8
+ 'case',
9
+ 'catch',
10
+ 'class',
11
+ 'const',
12
+ 'continue',
13
+ 'debugger',
14
+ 'default',
15
+ 'delete',
16
+ 'do',
17
+ 'else',
18
+ 'export',
19
+ 'extends',
20
+ 'finally',
21
+ 'for',
22
+ 'function',
23
+ 'if',
24
+ 'import',
25
+ 'in',
26
+ 'instanceof',
27
+ 'new',
28
+ 'return',
29
+ 'super',
30
+ 'switch',
31
+ 'this',
32
+ 'throw',
33
+ 'try',
34
+ 'typeof',
35
+ 'var',
36
+ 'void',
37
+ 'while',
38
+ 'with',
39
+ 'yield',
40
+ 'enum',
41
+ 'implements',
42
+ 'interface',
43
+ 'let',
44
+ 'package',
45
+ 'private',
46
+ 'protected',
47
+ 'public',
48
+ 'static',
49
+ 'await'
50
+ ]);
51
+ function generateDependencyTypes(options = {}) {
52
+ const manifestPath = (0, node_path_1.resolve)(options.manifestPath ?? 'manifest.json');
53
+ const brickRoot = (0, node_path_1.resolve)(options.brickRoot ?? (0, node_path_1.dirname)(manifestPath));
54
+ const bricksDir = (0, node_path_1.resolve)(options.bricksDir ?? (0, node_path_1.dirname)(brickRoot));
55
+ const outDir = (0, node_path_1.resolve)(options.outDir ?? (0, node_path_1.resolve)(brickRoot, 'runtime', 'node', '_generated', 'deps'));
56
+ const sdkModule = options.sdkModule ?? inferSdkModule(outDir, brickRoot);
57
+ const sourceManifest = readManifest(manifestPath);
58
+ const warnings = [];
59
+ const dependencies = resolveDependencies(sourceManifest, {
60
+ bricksDir,
61
+ dependencyManifestPaths: options.dependencyManifestPaths ?? {},
62
+ warnings
63
+ });
64
+ const files = [];
65
+ (0, node_fs_1.mkdirSync)(outDir, { recursive: true });
66
+ for (const dependency of dependencies) {
67
+ const basePath = (0, node_path_1.resolve)(outDir, dependency.brickId);
68
+ (0, node_fs_1.writeFileSync)(`${basePath}.js`, renderDependencyJs(dependency), 'utf8');
69
+ (0, node_fs_1.writeFileSync)(`${basePath}.d.ts`, renderDependencyDts(dependency), 'utf8');
70
+ files.push(`${basePath}.js`, `${basePath}.d.ts`);
71
+ }
72
+ (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(outDir, 'index.js'), renderIndexJs(dependencies), 'utf8');
73
+ (0, node_fs_1.writeFileSync)((0, node_path_1.resolve)(outDir, 'index.d.ts'), renderIndexDts(dependencies, sdkModule), 'utf8');
74
+ files.push((0, node_path_1.resolve)(outDir, 'index.js'), (0, node_path_1.resolve)(outDir, 'index.d.ts'));
75
+ return {
76
+ manifestPath,
77
+ outDir,
78
+ sdkModule,
79
+ dependencyCount: dependencies.length,
80
+ commandCount: dependencies.reduce((sum, dependency) => sum + dependency.commands.length, 0),
81
+ files,
82
+ warnings
83
+ };
84
+ }
85
+ function readManifest(manifestPath) {
86
+ if (!(0, node_fs_1.existsSync)(manifestPath)) {
87
+ throw new Error(`manifest.json 不存在:${manifestPath}`);
88
+ }
89
+ const value = JSON.parse((0, node_fs_1.readFileSync)(manifestPath, 'utf8'));
90
+ if (!value || typeof value !== 'object' || typeof value.id !== 'string') {
91
+ throw new Error(`manifest.json 缺少有效 id:${manifestPath}`);
92
+ }
93
+ return value;
94
+ }
95
+ function resolveDependencies(sourceManifest, context) {
96
+ const dependencyEntries = Object.entries(sourceManifest.dependencies ?? {});
97
+ const namespaceByBrick = uniqueNamespaces(dependencyEntries.map(([brickId]) => brickId));
98
+ const dependencies = [];
99
+ for (const [brickId, spec] of dependencyEntries) {
100
+ const manifestPath = context.dependencyManifestPaths[brickId] ?? (0, node_path_1.resolve)(context.bricksDir, brickId, 'manifest.json');
101
+ const optional = typeof spec === 'object' && spec !== null && spec.optional === true;
102
+ if (!(0, node_fs_1.existsSync)(manifestPath)) {
103
+ const message = `依赖 Brick ${brickId} 未找到:${manifestPath}`;
104
+ if (optional) {
105
+ context.warnings.push(message);
106
+ continue;
107
+ }
108
+ throw new Error(message);
109
+ }
110
+ const manifest = readManifest(manifestPath);
111
+ const requestedCommands = typeof spec === 'object' && spec !== null ? spec.commands : undefined;
112
+ const commands = selectCommands(manifest, requestedCommands, brickId, context.warnings);
113
+ if (commands.length === 0) {
114
+ context.warnings.push(`依赖 Brick ${brickId} 没有可生成的命令。`);
115
+ }
116
+ dependencies.push({
117
+ brickId,
118
+ namespace: namespaceByBrick.get(brickId) ?? toSafeIdentifier(lastBrickSegment(brickId)),
119
+ manifest,
120
+ commands: commands.map((command) => ({
121
+ command,
122
+ functionName: toCommandFunctionName(command.id),
123
+ inputTypeName: `${toPascalCase(command.id)}Input`,
124
+ outputTypeName: `${toPascalCase(command.id)}Output`
125
+ }))
126
+ });
127
+ }
128
+ return dependencies;
129
+ }
130
+ function selectCommands(manifest, requestedCommands, brickId, warnings) {
131
+ const commands = manifest.commands ?? [];
132
+ if (requestedCommands && requestedCommands.length > 0) {
133
+ const byId = new Map(commands.map((command) => [command.id, command]));
134
+ const selected = new Map();
135
+ if (requestedCommands.includes('*')) {
136
+ for (const command of commands) {
137
+ if (command.hidden !== true)
138
+ selected.set(command.id, command);
139
+ }
140
+ }
141
+ for (const commandId of requestedCommands.filter((item) => item !== '*')) {
142
+ const command = byId.get(commandId);
143
+ if (!command) {
144
+ warnings.push(`依赖 Brick ${brickId} 未声明命令 ${commandId},已跳过。`);
145
+ continue;
146
+ }
147
+ selected.set(command.id, command);
148
+ }
149
+ return [...selected.values()];
150
+ }
151
+ warnings.push(`依赖 Brick ${brickId} 未声明 commands,已跳过生成调用包装。`);
152
+ return [];
153
+ }
154
+ function renderDependencyJs(dependency) {
155
+ const lines = [
156
+ "'use strict'",
157
+ '',
158
+ `const BRICK_ID = ${JSON.stringify(dependency.brickId)}`,
159
+ '',
160
+ 'function assertInvokeTarget(target, functionName) {',
161
+ " if (target && typeof target.invoke === 'function') return",
162
+ ' throw new TypeError(',
163
+ " `${functionName} 需要传入 BricklyRuntime 或 CommandContext,例如 ${functionName}(ctx, input);也可以先 const client = bind(ctx),再调用 client.${functionName}(input)。`",
164
+ ' )',
165
+ '}',
166
+ ''
167
+ ];
168
+ for (const item of dependency.commands) {
169
+ lines.push(renderCommandJsDoc(dependency, item));
170
+ lines.push(`function ${item.functionName}(target, input, options) {`, ` assertInvokeTarget(target, ${JSON.stringify(item.functionName)})`, ` return target.invoke(BRICK_ID, ${JSON.stringify(item.command.id)}, input, options)`, '}', '');
171
+ }
172
+ lines.push('/**', ' * 绑定 BricklyRuntime 或 CommandContext,得到不需要重复传 target 的依赖调用对象。', ' */', 'function bind(target) {', ' return {');
173
+ for (const item of dependency.commands) {
174
+ lines.push(` ${JSON.stringify(item.functionName)}: (input, options) => ${item.functionName}(target, input, options),`);
175
+ }
176
+ lines.push(' }', '}', '', 'exports.BRICK_ID = BRICK_ID', 'exports.bind = bind');
177
+ for (const item of dependency.commands) {
178
+ lines.push(`exports.${item.functionName} = ${item.functionName}`);
179
+ }
180
+ lines.push('');
181
+ return lines.join('\n');
182
+ }
183
+ function renderDependencyDts(dependency) {
184
+ const lines = [
185
+ `export const BRICK_ID: ${JSON.stringify(dependency.brickId)}`,
186
+ '',
187
+ 'export interface InvokeOptions {',
188
+ ' /** 目标 Brick Profile ID;不传则由宿主选择目标 Brick 默认 Profile。 */',
189
+ ' profileId?: string',
190
+ '}',
191
+ '',
192
+ 'export interface BrickInvokeTarget {',
193
+ ' invoke<T = unknown>(',
194
+ ' brickId: string,',
195
+ ' commandId: string,',
196
+ ' input?: unknown,',
197
+ ' options?: InvokeOptions',
198
+ ' ): Promise<T>',
199
+ '}',
200
+ ''
201
+ ];
202
+ for (const item of dependency.commands) {
203
+ lines.push(renderInputType(item), '', renderOutputType(item), '', renderCommandDts(dependency, item), '');
204
+ }
205
+ lines.push(`export interface Bound${toPascalCase(dependency.namespace)} {`);
206
+ for (const item of dependency.commands) {
207
+ lines.push(renderMethodComment(item, ' '), ` ${item.functionName}(input: ${item.inputTypeName}, options?: InvokeOptions): Promise<${item.outputTypeName}>`);
208
+ }
209
+ lines.push('}', '');
210
+ lines.push(`export function bind(target: BrickInvokeTarget): Bound${toPascalCase(dependency.namespace)}`, '');
211
+ return lines.join('\n');
212
+ }
213
+ function renderIndexJs(dependencies) {
214
+ const lines = ["'use strict'", ''];
215
+ for (const dependency of dependencies) {
216
+ lines.push(`exports.${dependency.namespace} = require('./${dependency.brickId}')`);
217
+ }
218
+ lines.push('', 'exports.byBrickId = {');
219
+ for (const dependency of dependencies) {
220
+ lines.push(` ${JSON.stringify(dependency.brickId)}: exports.${dependency.namespace},`);
221
+ }
222
+ lines.push('}');
223
+ lines.push('');
224
+ return lines.join('\n');
225
+ }
226
+ function renderIndexDts(dependencies, sdkModule) {
227
+ const lines = [];
228
+ for (const dependency of dependencies) {
229
+ lines.push(`export * as ${dependency.namespace} from './${dependency.brickId}'`);
230
+ }
231
+ if (dependencies.length > 0) {
232
+ lines.push('', 'export const byBrickId: {');
233
+ for (const dependency of dependencies) {
234
+ lines.push(` ${JSON.stringify(dependency.brickId)}: typeof import('./${dependency.brickId}')`);
235
+ }
236
+ lines.push('}');
237
+ }
238
+ if (dependencies.length > 0) {
239
+ lines.push('');
240
+ for (const dependency of dependencies) {
241
+ const importedTypes = dependency.commands.flatMap((item) => [
242
+ `${item.inputTypeName} as ${typeAlias(dependency, item.inputTypeName)}`,
243
+ `${item.outputTypeName} as ${typeAlias(dependency, item.outputTypeName)}`
244
+ ]);
245
+ lines.push(`import type { ${importedTypes.join(', ')} } from './${dependency.brickId}'`);
246
+ }
247
+ lines.push('');
248
+ for (const moduleName of commandMapModuleTargets(sdkModule)) {
249
+ lines.push(`declare module ${JSON.stringify(moduleName)} {`, ' interface CommandMap {');
250
+ for (const dependency of dependencies) {
251
+ lines.push(` ${JSON.stringify(dependency.brickId)}: {`);
252
+ for (const item of dependency.commands) {
253
+ lines.push(` ${JSON.stringify(item.command.id)}: { input: ${typeAlias(dependency, item.inputTypeName)}; output: ${typeAlias(dependency, item.outputTypeName)} }`);
254
+ }
255
+ lines.push(' }');
256
+ }
257
+ lines.push(' }', '}', '');
258
+ }
259
+ }
260
+ return lines.join('\n');
261
+ }
262
+ function renderCommandJsDoc(dependency, item) {
263
+ const commandName = localizedText(item.command.name);
264
+ const description = localizedText(item.command.description);
265
+ const lines = ['/**', ` * ${escapeComment(commandName || item.command.id)}`];
266
+ if (description) {
267
+ lines.push(' *', ...wrapComment(description).map((line) => ` * ${line}`));
268
+ }
269
+ lines.push(' *', ` * @param {import('./${dependency.brickId}').BrickInvokeTarget} target - BricklyRuntime 或 CommandContext。`, ` * @param {import('./${dependency.brickId}').${item.inputTypeName}} input - 命令输入。`, ` * @param {import('./${dependency.brickId}').InvokeOptions} [options] - 调用选项。`, ` * @returns {Promise<import('./${dependency.brickId}').${item.outputTypeName}>}`);
270
+ const inputs = item.command.io?.inputs ?? [];
271
+ for (const input of inputs) {
272
+ const text = socketComment(input);
273
+ if (text)
274
+ lines.push(` * @param input.${input.name} - ${escapeComment(text)}`);
275
+ }
276
+ lines.push(' */');
277
+ return lines.join('\n');
278
+ }
279
+ function renderCommandDts(dependency, item) {
280
+ return [
281
+ renderMethodComment(item),
282
+ `export function ${item.functionName}(`,
283
+ ' target: BrickInvokeTarget,',
284
+ ` input: ${item.inputTypeName},`,
285
+ ' options?: InvokeOptions',
286
+ `): Promise<${item.outputTypeName}>`
287
+ ].join('\n');
288
+ }
289
+ function renderMethodComment(item, prefix = '') {
290
+ const commandName = localizedText(item.command.name);
291
+ const description = localizedText(item.command.description);
292
+ const lines = [`${prefix}/**`, `${prefix} * ${escapeComment(commandName || item.command.id)}`];
293
+ if (description) {
294
+ lines.push(`${prefix} *`, ...wrapComment(description).map((line) => `${prefix} * ${line}`));
295
+ }
296
+ lines.push(`${prefix} */`);
297
+ return lines.join('\n');
298
+ }
299
+ function renderInputType(item) {
300
+ const inputs = item.command.io?.inputs ?? [];
301
+ if (inputs.length === 0) {
302
+ return `export type ${item.inputTypeName} = Record<string, never>`;
303
+ }
304
+ const lines = [`export interface ${item.inputTypeName} {`];
305
+ for (const input of inputs) {
306
+ const text = socketComment(input);
307
+ if (text) {
308
+ lines.push(' /**');
309
+ for (const line of wrapComment(text))
310
+ lines.push(` * ${line}`);
311
+ lines.push(' */');
312
+ }
313
+ const optional = input.required === true ? '' : '?';
314
+ lines.push(` ${input.name}${optional}: ${socketToTsType(input)}`);
315
+ }
316
+ lines.push('}');
317
+ return lines.join('\n');
318
+ }
319
+ function renderOutputType(item) {
320
+ const outputs = item.command.io?.outputs ?? [];
321
+ if (outputs.length === 0)
322
+ return `export type ${item.outputTypeName} = unknown`;
323
+ if (outputs.length === 1)
324
+ return `export type ${item.outputTypeName} = ${socketToTsType(outputs[0])}`;
325
+ const lines = [`export interface ${item.outputTypeName} {`];
326
+ for (const output of outputs) {
327
+ const text = socketComment(output);
328
+ if (text) {
329
+ lines.push(' /**');
330
+ for (const line of wrapComment(text))
331
+ lines.push(` * ${line}`);
332
+ lines.push(' */');
333
+ }
334
+ lines.push(` ${output.name}: ${socketToTsType(output)}`);
335
+ }
336
+ lines.push('}');
337
+ return lines.join('\n');
338
+ }
339
+ function socketToTsType(socket) {
340
+ const enumType = enumToTsType(socket.enum);
341
+ const baseType = enumType ??
342
+ {
343
+ string: 'string',
344
+ number: 'number',
345
+ boolean: 'boolean',
346
+ json: 'unknown',
347
+ image: 'string',
348
+ audio: 'string',
349
+ video: 'string',
350
+ file: 'string',
351
+ binary: '{ $file: string }',
352
+ stream: 'AsyncIterable<unknown>',
353
+ any: 'unknown'
354
+ }[socket.type];
355
+ return socket.multi === true ? `Array<${baseType}>` : baseType;
356
+ }
357
+ function enumToTsType(values) {
358
+ if (!values || values.length === 0)
359
+ return undefined;
360
+ const literals = values
361
+ .map((item) => (typeof item === 'object' ? item.value : item))
362
+ .filter((value) => typeof value === 'string' || typeof value === 'number');
363
+ if (literals.length === 0)
364
+ return undefined;
365
+ return literals.map((value) => JSON.stringify(value)).join(' | ');
366
+ }
367
+ function socketComment(socket) {
368
+ const parts = [socket.label, socket.description];
369
+ if (socket.default !== undefined)
370
+ parts.push(`默认值:${JSON.stringify(socket.default)}`);
371
+ return parts.filter(Boolean).join('。');
372
+ }
373
+ function localizedText(value) {
374
+ if (!value)
375
+ return '';
376
+ if (typeof value === 'string')
377
+ return value;
378
+ return value['zh-CN'] ?? value.zh ?? value.en ?? Object.values(value)[0] ?? '';
379
+ }
380
+ function wrapComment(text) {
381
+ const normalized = escapeComment(text).replace(/\s+/g, ' ').trim();
382
+ if (normalized.length <= 90)
383
+ return [normalized];
384
+ const lines = [];
385
+ let rest = normalized;
386
+ while (rest.length > 90) {
387
+ let index = rest.lastIndexOf(' ', 90);
388
+ if (index <= 0)
389
+ index = 90;
390
+ lines.push(rest.slice(0, index));
391
+ rest = rest.slice(index).trimStart();
392
+ }
393
+ if (rest)
394
+ lines.push(rest);
395
+ return lines;
396
+ }
397
+ function escapeComment(text) {
398
+ return text.replace(/\*\//g, '* /');
399
+ }
400
+ function toCommandFunctionName(commandId) {
401
+ const identifier = toSafeIdentifier(commandId);
402
+ return RESERVED_WORDS.has(identifier) ? `${identifier}Command` : identifier;
403
+ }
404
+ function toSafeIdentifier(value) {
405
+ const normalized = toCamelCase(value);
406
+ const clean = normalized.replace(/[^A-Za-z0-9_$]/g, '');
407
+ const fallback = clean || 'dependency';
408
+ return /^[A-Za-z_$]/.test(fallback) ? fallback : `_${fallback}`;
409
+ }
410
+ function toCamelCase(value) {
411
+ const words = value.split(/[^A-Za-z0-9]+/).filter(Boolean);
412
+ if (words.length === 0)
413
+ return '';
414
+ const [first, ...rest] = words;
415
+ return first.toLowerCase() + rest.map(capitalize).join('');
416
+ }
417
+ function toPascalCase(value) {
418
+ const words = value.split(/[^A-Za-z0-9]+/).filter(Boolean);
419
+ const result = words.map(capitalize).join('');
420
+ return result || 'Value';
421
+ }
422
+ function capitalize(value) {
423
+ if (!value)
424
+ return '';
425
+ return value.slice(0, 1).toUpperCase() + value.slice(1);
426
+ }
427
+ function lastBrickSegment(brickId) {
428
+ return brickId.split('.').filter(Boolean).at(-1) ?? brickId;
429
+ }
430
+ function uniqueNamespaces(brickIds) {
431
+ const seen = new Set();
432
+ const result = new Map();
433
+ for (const brickId of brickIds) {
434
+ const segments = brickId.split('.').filter(Boolean);
435
+ let namespace = '';
436
+ for (let size = 1; size <= segments.length; size++) {
437
+ namespace = toSafeIdentifier(segments.slice(-size).join('-'));
438
+ if (!seen.has(namespace))
439
+ break;
440
+ }
441
+ let unique = namespace;
442
+ let suffix = 2;
443
+ while (seen.has(unique)) {
444
+ unique = `${namespace}${suffix}`;
445
+ suffix += 1;
446
+ }
447
+ seen.add(unique);
448
+ result.set(brickId, unique);
449
+ }
450
+ return result;
451
+ }
452
+ function inferSdkModule(outDir, brickRoot) {
453
+ const sdkDir = (0, node_path_1.resolve)(brickRoot, 'runtime', 'node', '_sdk');
454
+ const sdkTypes = (0, node_path_1.resolve)(sdkDir, 'index.d.ts');
455
+ if (!(0, node_fs_1.existsSync)(sdkTypes))
456
+ return '@syllm/brickly-sdk';
457
+ let path = (0, node_path_1.relative)(outDir, sdkDir).replace(/\\/g, '/');
458
+ if (!path.startsWith('.'))
459
+ path = `./${path}`;
460
+ return path;
461
+ }
462
+ function typeAlias(dependency, typeName) {
463
+ return `${toPascalCase(dependency.namespace)}${typeName}`;
464
+ }
465
+ function commandMapModuleTargets(sdkModule) {
466
+ const targets = [sdkModule, `${sdkModule}/api`];
467
+ if (sdkModule === '@syllm/brickly-sdk')
468
+ targets.push('@syllm/brickly-sdk/dist/api');
469
+ return [...new Set(targets)];
470
+ }
package/package.json ADDED
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "@syllm/brickly-sdk",
3
+ "version": "0.1.0",
4
+ "description": "Brickly Brick Node runtime 官方 SDK",
5
+ "license": "MIT",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "bin": {
9
+ "brickly-typegen": "dist/typegen-cli.js"
10
+ },
11
+ "files": [
12
+ "dist",
13
+ "README.md"
14
+ ],
15
+ "scripts": {
16
+ "build": "tsc -p tsconfig.json",
17
+ "test": "tsx --test tests/runtime.test.ts tests/api.test.ts tests/typegen.test.ts",
18
+ "typegen": "tsx src/typegen-cli.ts",
19
+ "sync:demo-window-lab": "node scripts/sync-to-brick.mjs com.brickly.demo-window-lab",
20
+ "prepublishOnly": "npm run build"
21
+ },
22
+ "devDependencies": {
23
+ "typescript": "^5.9.3",
24
+ "@types/node": "^22.19.1",
25
+ "tsx": "^4.19.2"
26
+ },
27
+ "engines": {
28
+ "node": ">=18.0.0"
29
+ }
30
+ }