@airongrong/plugin-cli 0.0.0-stage → 1.0.0-beta.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 AiToEarn
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,59 @@
1
- # Temporary Holding Version
1
+ # @airongrong/plugin-cli
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ AiToEarn 插件开发 CLI:脚手架、校验、打包、签名一条龙。面向插件作者,运行时不进宿主沙箱。
4
+
5
+ 插件产物(`.aitpkg`)刻意不经过 npm:零依赖、可离线安装、签名校验。本工具只是帮你生产合格的产物。
6
+
7
+ ## 使用
8
+
9
+ 无需安装,npx 直接用(也可以全局安装 `npm i -g @airongrong/plugin-cli`,命令名同为 `aitoearn-plugin`):
10
+
11
+ ```bash
12
+ # 脚手架
13
+ npx @airongrong/plugin-cli new channel.acme --type adapter
14
+
15
+ # 校验(清单 schema + 产物哈希 + 单文件 ESM 约束 + 路径穿越检查)
16
+ npx @airongrong/plugin-cli verify ./channel.acme
17
+ # 被拒绝的 entry 源码示例(报 AIT-PL-001):
18
+ # import axios from 'axios' —— 非允许依赖
19
+ # const m = await import('node:' + 'fs') —— 计算型动态 import
20
+ # 被拒绝的 entry.module 示例:
21
+ # "dist/../../evil.mjs" —— 路径穿越(AIT-PL-001)
22
+
23
+ # 打包 .aitpkg(store-only zip,无第三方依赖)
24
+ npx @airongrong/plugin-cli pack ./channel.acme
25
+
26
+ # 开发签名(adapter 必需)
27
+ npx @airongrong/plugin-cli keygen --out ~/.aitoearn
28
+ npx @airongrong/plugin-cli sign ./channel.acme --key ~/.aitoearn/dev.pem
29
+ ```
30
+
31
+ ## 命令
32
+
33
+ | 命令 | 说明 |
34
+ | --- | --- |
35
+ | `new <id> --type skill\|hook\|adapter` | 生成插件脚手架 |
36
+ | `verify [dir]` | 清单与产物校验(AIT-PL-001/002,恒时哈希比较 + 路径穿越检查) |
37
+ | `pack [dir]` | 产出 `<id>-<version>.aitpkg` |
38
+ | `sign [dir] --key <pem>` | ed25519 签名 entry 产物 |
39
+ | `keygen --out <dir>` | 生成开发用 ed25519 密钥对(私钥权限 0600) |
40
+ | `dev` / `test` | 本地热重载 / 回放测试(M1 提供) |
41
+
42
+ ## 本地开发
43
+
44
+ 本仓库(aitoearn-sdk)是独立 pnpm workspace,与私有后端仓库解耦:
45
+
46
+ ```bash
47
+ pnpm install
48
+ pnpm build # 全量 tsc 构建
49
+ pnpm test # vitest
50
+ pnpm typecheck
51
+ ```
52
+
53
+ ## 发布(维护者)
54
+
55
+ 走 changesets:改完代码执行 `pnpm changeset` 写变更说明,合入 main 后 CI 开 Version PR,
56
+ 合并 Version PR 即自动 `changeset publish` 到 npm(需仓库 secret `NPM_TOKEN`)。
57
+
58
+ 注意版本联动:`plugin-cli` 对 `@airongrong/plugin-api` / `@airongrong/capability` 是 workspace 依赖,
59
+ changesets 发版时会自动转成精确版本号并联动 bump。
@@ -0,0 +1,3 @@
1
+ /** CLI 输出:仓库 lint 禁止 console,这里统一走 process 标准流 */
2
+ export declare function printLine(text: string): void;
3
+ export declare function printErrorLine(text: string): void;
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ /** CLI 输出:仓库 lint 禁止 console,这里统一走 process 标准流 */
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ exports.printLine = printLine;
5
+ exports.printErrorLine = printErrorLine;
6
+ function printLine(text) {
7
+ process.stdout.write(`${text}\n`);
8
+ }
9
+ function printErrorLine(text) {
10
+ process.stderr.write(`${text}\n`);
11
+ }
package/dist/main.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
package/dist/main.js ADDED
@@ -0,0 +1,120 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ Object.defineProperty(exports, "__esModule", { value: true });
4
+ const promises_1 = require("node:fs/promises");
5
+ const node_path_1 = require("node:path");
6
+ const plugin_api_1 = require("@airongrong/plugin-api");
7
+ const commander_1 = require("commander");
8
+ const cli_output_1 = require("./cli-output");
9
+ const package_files_1 = require("./package-files");
10
+ const scaffold_1 = require("./scaffold");
11
+ const sign_1 = require("./sign");
12
+ const zip_1 = require("./zip");
13
+ const program = new commander_1.Command();
14
+ program.name('aitoearn-plugin').description('AiToEarn 插件开发 CLI');
15
+ program
16
+ .command('new')
17
+ .argument('<id>', '插件 id,如 channel.acme / hook.media-spec / skill.xhs-copywriting')
18
+ .requiredOption('--type <type>', 'skill | hook | adapter')
19
+ .option('--dir <dir>', '父目录', process.cwd())
20
+ .action(async (id, options) => {
21
+ const type = options.type;
22
+ if (!Object.values(plugin_api_1.PluginType).includes(type)) {
23
+ throw new Error(`--type 必须是 ${Object.values(plugin_api_1.PluginType).join(' | ')}`);
24
+ }
25
+ const dir = await (0, scaffold_1.scaffoldPlugin)((0, node_path_1.resolve)(options.dir), id, type);
26
+ (0, cli_output_1.printLine)(`已创建插件脚手架: ${dir}`);
27
+ (0, cli_output_1.printLine)('下一步: 实现 dist/index.mjs,回填 plugin.json entry.sha256,然后运行 verify');
28
+ });
29
+ program
30
+ .command('verify')
31
+ .argument('[dir]', '插件目录', process.cwd())
32
+ .action(async (dir) => {
33
+ const manifest = await (0, package_files_1.verifyPluginPackage)((0, node_path_1.resolve)(dir));
34
+ (0, cli_output_1.printLine)(`✓ ${manifest.id}@${manifest.version} 校验通过`);
35
+ });
36
+ program
37
+ .command('pack')
38
+ .argument('[dir]', '插件目录', process.cwd())
39
+ .action(async (dir) => {
40
+ const pkgDir = (0, node_path_1.resolve)(dir);
41
+ const manifest = await (0, package_files_1.verifyPluginPackage)(pkgDir);
42
+ const files = await (0, package_files_1.collectPackageFiles)(pkgDir);
43
+ const outPath = (0, node_path_1.join)(pkgDir, '..', `${manifest.id}-${manifest.version}.aitpkg`);
44
+ await (0, promises_1.writeFile)(outPath, (0, zip_1.buildZip)(files));
45
+ (0, cli_output_1.printLine)(`✓ 已打包 ${files.length} 个文件: ${outPath}`);
46
+ });
47
+ program
48
+ .command('sign')
49
+ .argument('[dir]', '插件目录', process.cwd())
50
+ .requiredOption('--key <path>', 'ed25519 私钥 PEM 路径')
51
+ .action(async (dir, options) => {
52
+ const { signature, modulePath } = await (0, sign_1.signPluginPackage)((0, node_path_1.resolve)(dir), (0, node_path_1.resolve)(options.key));
53
+ (0, cli_output_1.printLine)(`✓ 已签名: ${modulePath}.sig`);
54
+ (0, cli_output_1.printLine)(` signature: ${signature}`);
55
+ });
56
+ program
57
+ .command('keygen')
58
+ .requiredOption('--out <dir>', '输出目录', (0, node_path_1.join)(process.env['HOME'] ?? '~', '.aitoearn'))
59
+ .action(async (options) => {
60
+ const keys = await (0, sign_1.generateDevKeys)((0, node_path_1.resolve)(options.out));
61
+ (0, cli_output_1.printLine)(`✓ 开发密钥已生成: ${keys.privateKeyPath}`);
62
+ (0, cli_output_1.printLine)(` 公钥(加入宿主 trustedKeys): ${keys.publicKeyPath}`);
63
+ });
64
+ program
65
+ .command('cap')
66
+ .description('从 capability.yaml 派生五种产物(mcp / ui / openapi / sdk / validation)')
67
+ .requiredOption('--from <file>', 'capability.yaml 路径')
68
+ .option('--out <dir>', '输出目录', (0, node_path_1.join)(process.cwd(), 'generated'))
69
+ .option('--surface <surfaces>', '逗号分隔: mcp,ui,openapi,sdk,validation,all', 'all')
70
+ .action(async (options) => {
71
+ const { loadCapabilityManifest } = await import('@airongrong/capability');
72
+ const { generateAllSurfaces } = await import('@airongrong/capability');
73
+ const { mkdir, writeFile } = await import('node:fs/promises');
74
+ const manifest = await loadCapabilityManifest((0, node_path_1.resolve)(options.from));
75
+ const surfaces = generateAllSurfaces(manifest);
76
+ const wanted = options.surface.split(',').map(item => item.trim());
77
+ const all = wanted.includes('all');
78
+ await mkdir((0, node_path_1.resolve)(options.out), { recursive: true });
79
+ const written = [];
80
+ const dump = async (name, data, ext = 'json') => {
81
+ const file = (0, node_path_1.join)((0, node_path_1.resolve)(options.out), `${name}.${ext}`);
82
+ await writeFile(file, typeof data === 'string' ? data : `${JSON.stringify(data, null, 2)}\n`);
83
+ written.push(file);
84
+ };
85
+ if (all || wanted.includes('mcp')) {
86
+ await dump('mcp-tools', surfaces.mcp);
87
+ }
88
+ if (all || wanted.includes('ui')) {
89
+ await dump('ui-forms', surfaces.ui);
90
+ }
91
+ if (all || wanted.includes('openapi')) {
92
+ await dump('openapi', surfaces.openapi);
93
+ }
94
+ if (all || wanted.includes('sdk')) {
95
+ await dump('capability-client', surfaces.sdk, 'ts');
96
+ }
97
+ if (all || wanted.includes('validation')) {
98
+ await dump('validation-rules', surfaces.validation);
99
+ }
100
+ (0, cli_output_1.printLine)(`✓ 已从 capability.yaml v${manifest.version} 生成 ${written.length} 个产物 (${Object.keys(manifest.actions).length} 个能力)`);
101
+ for (const file of written) {
102
+ (0, cli_output_1.printLine)(` ${file}`);
103
+ }
104
+ });
105
+ program
106
+ .command('dev')
107
+ .description('本地热重载 + 宿主连接(M1 提供)')
108
+ .action(() => {
109
+ (0, cli_output_1.printLine)('plugin dev 将在 M1(渠道插件化阶段)提供');
110
+ });
111
+ program
112
+ .command('test')
113
+ .description('回放式测试(M1 提供)')
114
+ .action(() => {
115
+ (0, cli_output_1.printLine)('plugin test 将在 M1(渠道插件化阶段)提供');
116
+ });
117
+ program.parseAsync(process.argv).catch((error) => {
118
+ (0, cli_output_1.printErrorLine)(`✗ ${error.message}`);
119
+ process.exitCode = 1;
120
+ });
@@ -0,0 +1,14 @@
1
+ import type { PluginManifest } from '@airongrong/plugin-api';
2
+ export interface PluginPackageFiles {
3
+ dir: string;
4
+ manifest: PluginManifest;
5
+ entrySource?: string;
6
+ entryBytes?: Buffer;
7
+ }
8
+ export declare function readPluginPackage(dir: string): Promise<PluginPackageFiles>;
9
+ export declare function verifyPluginPackage(dir: string): Promise<PluginManifest>;
10
+ /** 收集打包文件:plugin.json + dist + skills + README/CHANGELOG */
11
+ export declare function collectPackageFiles(dir: string): Promise<Array<{
12
+ name: string;
13
+ data: Buffer;
14
+ }>>;
@@ -0,0 +1,72 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.readPluginPackage = readPluginPackage;
4
+ exports.verifyPluginPackage = verifyPluginPackage;
5
+ exports.collectPackageFiles = collectPackageFiles;
6
+ const node_crypto_1 = require("node:crypto");
7
+ const node_fs_1 = require("node:fs");
8
+ const promises_1 = require("node:fs/promises");
9
+ const node_path_1 = require("node:path");
10
+ const plugin_api_1 = require("@airongrong/plugin-api");
11
+ async function readPluginPackage(dir) {
12
+ const manifestPath = (0, node_path_1.join)(dir, 'plugin.json');
13
+ if (!(0, node_fs_1.existsSync)(manifestPath)) {
14
+ throw new plugin_api_1.PluginError(plugin_api_1.PluginErrorCode.InvalidManifest, `plugin.json 不存在: ${manifestPath}`);
15
+ }
16
+ const manifest = (0, plugin_api_1.verifyManifest)(JSON.parse(await (0, promises_1.readFile)(manifestPath, 'utf8')));
17
+ const pkg = { dir, manifest };
18
+ if (manifest.entry) {
19
+ const entryPath = (0, node_path_1.join)(dir, manifest.entry.module.replace(/^\.\//, ''));
20
+ pkg.entryBytes = await (0, promises_1.readFile)(entryPath);
21
+ pkg.entrySource = pkg.entryBytes.toString('utf8');
22
+ }
23
+ return pkg;
24
+ }
25
+ async function verifyPluginPackage(dir) {
26
+ const pkg = await readPluginPackage(dir);
27
+ const { manifest } = pkg;
28
+ if (pkg.entryBytes && pkg.entrySource !== undefined) {
29
+ const actual = (0, node_crypto_1.createHash)('sha256').update(pkg.entryBytes).digest('hex');
30
+ if (actual !== manifest.entry.sha256) {
31
+ throw new plugin_api_1.PluginError(plugin_api_1.PluginErrorCode.HashMismatch, `产物哈希不匹配(AIT-PL-002): 期望 ${manifest.entry.sha256},实际 ${actual},请重新打包`);
32
+ }
33
+ (0, plugin_api_1.assertPluginEntrySource)(pkg.entrySource, manifest.entry.module);
34
+ }
35
+ if ((0, node_fs_1.existsSync)((0, node_path_1.join)(dir, 'node_modules'))) {
36
+ throw new plugin_api_1.PluginError(plugin_api_1.PluginErrorCode.InvalidManifest, '插件禁止携带 node_modules(AIT-PL-001)');
37
+ }
38
+ return manifest;
39
+ }
40
+ /** 收集打包文件:plugin.json + dist + skills + README/CHANGELOG */
41
+ async function collectPackageFiles(dir) {
42
+ const files = [];
43
+ const includeDirs = ['dist', 'skills'];
44
+ for (const sub of includeDirs) {
45
+ const subDir = (0, node_path_1.join)(dir, sub);
46
+ if (!(0, node_fs_1.existsSync)(subDir)) {
47
+ continue;
48
+ }
49
+ const stack = [subDir];
50
+ while (stack.length > 0) {
51
+ const current = stack.pop();
52
+ const entries = await (0, promises_1.readdir)(current, { withFileTypes: true });
53
+ for (const entry of entries) {
54
+ const full = (0, node_path_1.join)(current, entry.name);
55
+ if (entry.isDirectory()) {
56
+ stack.push(full);
57
+ }
58
+ else {
59
+ files.push({ name: (0, node_path_1.relative)(dir, full), data: await (0, promises_1.readFile)(full) });
60
+ }
61
+ }
62
+ }
63
+ }
64
+ for (const doc of ['README.md', 'CHANGELOG.md']) {
65
+ const docPath = (0, node_path_1.join)(dir, doc);
66
+ if ((0, node_fs_1.existsSync)(docPath)) {
67
+ files.push({ name: doc, data: await (0, promises_1.readFile)(docPath) });
68
+ }
69
+ }
70
+ files.push({ name: 'plugin.json', data: await (0, promises_1.readFile)((0, node_path_1.join)(dir, 'plugin.json')) });
71
+ return files;
72
+ }
@@ -0,0 +1,2 @@
1
+ import { PluginType } from '@airongrong/plugin-api';
2
+ export declare function scaffoldPlugin(parentDir: string, id: string, type: PluginType): Promise<string>;
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.scaffoldPlugin = scaffoldPlugin;
4
+ const node_fs_1 = require("node:fs");
5
+ const promises_1 = require("node:fs/promises");
6
+ const node_path_1 = require("node:path");
7
+ const plugin_api_1 = require("@airongrong/plugin-api");
8
+ const TEMPLATES = {
9
+ [plugin_api_1.PluginType.Hook]: {
10
+ entry: `export async function activate() {
11
+ return {
12
+ hooks: {
13
+ async beforePublish(draft) {
14
+ if (!draft.text.trim()) {
15
+ throw new Error('正文不能为空')
16
+ }
17
+ },
18
+ },
19
+ }
20
+ }
21
+ `,
22
+ manifest: id => ({
23
+ id,
24
+ version: '0.1.0',
25
+ apiVersion: '^1.0.0',
26
+ type: 'hook',
27
+ description: 'TODO: 一句话描述钩子用途',
28
+ entry: { module: './dist/index.mjs', sha256: 'TODO: pnpm aitoearn-plugin verify 前用实际产物哈希替换' },
29
+ contributes: { hooks: ['beforePublish'] },
30
+ permissions: { network: [] },
31
+ engines: { aitoearn: '>=1.8.0 <3' },
32
+ license: 'MIT',
33
+ }),
34
+ },
35
+ [plugin_api_1.PluginType.Adapter]: {
36
+ entry: `export async function activate(host) {
37
+ return {
38
+ adapter: {
39
+ meta: { id: 'acme', label: 'Acme', capabilities: ['publish.text'] },
40
+ async authorize() { throw new Error('TODO') },
41
+ validate(draft) { return draft.text ? [] : [{ severity: 'error', message: '正文不能为空' }] },
42
+ async publish(draft) {
43
+ // const res = await host.http.request({ url: 'https://api.example.com/posts', method: 'POST', body: JSON.stringify({ text: draft.text }) })
44
+ return { externalId: draft.draftId, url: 'https://example.com/' + draft.draftId, publishedAt: new Date().toISOString() }
45
+ },
46
+ },
47
+ }
48
+ }
49
+ `,
50
+ manifest: id => ({
51
+ id,
52
+ version: '0.1.0',
53
+ apiVersion: '^1.0.0',
54
+ type: 'adapter',
55
+ description: 'TODO: 一句话描述渠道用途',
56
+ entry: { module: './dist/index.mjs', sha256: 'TODO: 用实际产物哈希替换' },
57
+ contributes: { channels: [{ id: 'acme', label: 'Acme', capabilities: ['publish.text'] }] },
58
+ permissions: { network: ['api.example.com'], secrets: ['oauth.access_token'] },
59
+ engines: { aitoearn: '>=1.8.0 <3' },
60
+ license: 'MIT',
61
+ }),
62
+ },
63
+ [plugin_api_1.PluginType.Skill]: {
64
+ entry: '',
65
+ manifest: id => ({
66
+ id,
67
+ version: '0.1.0',
68
+ apiVersion: '^1.0.0',
69
+ type: 'skill',
70
+ description: 'TODO: 一句话描述技能用途',
71
+ contributes: { skills: [id.split('.')[1]] },
72
+ permissions: { network: [] },
73
+ engines: { aitoearn: '>=1.8.0 <3' },
74
+ license: 'MIT',
75
+ }),
76
+ },
77
+ };
78
+ async function scaffoldPlugin(parentDir, id, type) {
79
+ if (!plugin_api_1.PLUGIN_ID_PATTERN.test(id)) {
80
+ throw new plugin_api_1.PluginError(plugin_api_1.PluginErrorCode.InvalidManifest, `插件 id 非法: ${id}(必须匹配 ${plugin_api_1.PLUGIN_ID_PATTERN.source},如 hook.media-spec / channel.acme / skill.xhs-copywriting)`);
81
+ }
82
+ const dir = (0, node_path_1.join)(parentDir, id);
83
+ if ((0, node_fs_1.existsSync)(dir)) {
84
+ throw new plugin_api_1.PluginError(plugin_api_1.PluginErrorCode.InvalidManifest, `目录已存在: ${dir}`);
85
+ }
86
+ const template = TEMPLATES[type];
87
+ await (0, promises_1.mkdir)((0, node_path_1.join)(dir, 'tests', 'fixtures'), { recursive: true });
88
+ await (0, promises_1.mkdir)((0, node_path_1.join)(dir, 'dist'), { recursive: true });
89
+ await (0, promises_1.writeFile)((0, node_path_1.join)(dir, 'plugin.json'), `${JSON.stringify(template.manifest(id), null, 2)}\n`);
90
+ if (template.entry) {
91
+ await (0, promises_1.writeFile)((0, node_path_1.join)(dir, 'dist', 'index.mjs'), template.entry);
92
+ }
93
+ await (0, promises_1.writeFile)((0, node_path_1.join)(dir, 'README.md'), `# ${id}\n\nTODO: 插件说明\n`);
94
+ await (0, promises_1.writeFile)((0, node_path_1.join)(dir, 'CHANGELOG.md'), `# 0.1.0\n\n- 初始版本\n`);
95
+ return dir;
96
+ }
package/dist/sign.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ /** 签名对象:entry 产物的 sha256 十六进制字符串(与宿主 loader 校验口径一致) */
2
+ export declare function signPluginPackage(dir: string, keyPath: string): Promise<{
3
+ signature: string;
4
+ modulePath: string;
5
+ }>;
6
+ export declare function generateDevKeys(outDir: string): Promise<{
7
+ privateKeyPath: string;
8
+ publicKeyPath: string;
9
+ }>;
package/dist/sign.js ADDED
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.signPluginPackage = signPluginPackage;
4
+ exports.generateDevKeys = generateDevKeys;
5
+ const node_crypto_1 = require("node:crypto");
6
+ const promises_1 = require("node:fs/promises");
7
+ const node_path_1 = require("node:path");
8
+ const plugin_api_1 = require("@airongrong/plugin-api");
9
+ const package_files_1 = require("./package-files");
10
+ /** 签名对象:entry 产物的 sha256 十六进制字符串(与宿主 loader 校验口径一致) */
11
+ async function signPluginPackage(dir, keyPath) {
12
+ const pkg = await (0, package_files_1.readPluginPackage)(dir);
13
+ if (!pkg.entryBytes || !pkg.manifest.entry) {
14
+ throw new plugin_api_1.PluginError(plugin_api_1.PluginErrorCode.InvalidManifest, '技能插件没有可签名的产物');
15
+ }
16
+ const privateKey = await (0, promises_1.readFile)(keyPath, 'utf8');
17
+ const digestHex = (0, node_crypto_1.createHash)('sha256').update(pkg.entryBytes).digest('hex');
18
+ const signature = (0, node_crypto_1.sign)(null, Buffer.from(digestHex), privateKey).toString('base64');
19
+ const modulePath = (0, node_path_1.join)(dir, pkg.manifest.entry.module.replace(/^\.\//, ''));
20
+ await (0, promises_1.writeFile)(`${modulePath}.sig`, signature);
21
+ return { signature, modulePath };
22
+ }
23
+ async function generateDevKeys(outDir) {
24
+ const { privateKey, publicKey } = (0, node_crypto_1.generateKeyPairSync)('ed25519');
25
+ const privateKeyPath = (0, node_path_1.join)(outDir, 'dev.pem');
26
+ const publicKeyPath = (0, node_path_1.join)(outDir, 'dev.pub');
27
+ await (0, promises_1.mkdir)((0, node_path_1.dirname)(privateKeyPath), { recursive: true });
28
+ await (0, promises_1.writeFile)(privateKeyPath, privateKey.export({ type: 'pkcs8', format: 'pem' }));
29
+ await (0, promises_1.writeFile)(publicKeyPath, publicKey.export({ type: 'spki', format: 'pem' }));
30
+ return { privateKeyPath, publicKeyPath };
31
+ }
package/dist/zip.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ /**
2
+ * .aitpkg 格式:zip(store-only,无压缩)+ CRC32。
3
+ * 刻意不引入第三方 zip 依赖:插件分发生态零 npm 依赖是方案底线。
4
+ */
5
+ export declare function crc32(buffer: Buffer): number;
6
+ export interface ZipEntryInput {
7
+ name: string;
8
+ data: Buffer;
9
+ }
10
+ export declare function buildZip(entries: ZipEntryInput[]): Buffer;
package/dist/zip.js ADDED
@@ -0,0 +1,88 @@
1
+ "use strict";
2
+ /**
3
+ * .aitpkg 格式:zip(store-only,无压缩)+ CRC32。
4
+ * 刻意不引入第三方 zip 依赖:插件分发生态零 npm 依赖是方案底线。
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.crc32 = crc32;
8
+ exports.buildZip = buildZip;
9
+ const CRC_TABLE = (() => {
10
+ const table = new Uint32Array(256);
11
+ for (let index = 0; index < 256; index += 1) {
12
+ let crc = index;
13
+ for (let bit = 0; bit < 8; bit += 1) {
14
+ crc = crc & 1 ? 0xEDB88320 ^ (crc >>> 1) : crc >>> 1;
15
+ }
16
+ table[index] = crc >>> 0;
17
+ }
18
+ return table;
19
+ })();
20
+ function crc32(buffer) {
21
+ let crc = 0xFFFFFFFF;
22
+ for (const byte of buffer) {
23
+ crc = CRC_TABLE[(crc ^ byte) & 0xFF] ^ (crc >>> 8);
24
+ }
25
+ return (crc ^ 0xFFFFFFFF) >>> 0;
26
+ }
27
+ function dosDateTime(date) {
28
+ return {
29
+ date: ((date.getFullYear() - 1980) << 9) | ((date.getMonth() + 1) << 5) | date.getDate(),
30
+ time: (date.getHours() << 11) | (date.getMinutes() << 5) | (date.getSeconds() >> 1),
31
+ };
32
+ }
33
+ function buildZip(entries) {
34
+ const chunks = [];
35
+ const central = [];
36
+ let offset = 0;
37
+ for (const entry of entries) {
38
+ const name = Buffer.from(entry.name, 'utf8');
39
+ const crc = crc32(entry.data);
40
+ const { date, time } = dosDateTime(new Date());
41
+ const local = Buffer.alloc(30);
42
+ local.writeUInt32LE(0x04034B50, 0);
43
+ local.writeUInt16LE(20, 4);
44
+ local.writeUInt16LE(0x0800, 6); // UTF-8 文件名
45
+ local.writeUInt16LE(0, 8); // store-only
46
+ local.writeUInt16LE(time, 10);
47
+ local.writeUInt16LE(date, 12);
48
+ local.writeUInt32LE(crc, 14);
49
+ local.writeUInt32LE(entry.data.length, 18);
50
+ local.writeUInt32LE(entry.data.length, 22);
51
+ local.writeUInt16LE(name.length, 26);
52
+ local.writeUInt16LE(0, 28);
53
+ chunks.push(local, name, entry.data);
54
+ central.push({ name, crc, size: entry.data.length, offset });
55
+ offset += local.length + name.length + entry.data.length;
56
+ }
57
+ const centralStart = offset;
58
+ for (const entry of central) {
59
+ const { date, time } = dosDateTime(new Date());
60
+ const header = Buffer.alloc(46);
61
+ header.writeUInt32LE(0x02014B50, 0);
62
+ header.writeUInt16LE(20, 4);
63
+ header.writeUInt16LE(20, 6);
64
+ header.writeUInt16LE(0x0800, 8);
65
+ header.writeUInt16LE(0, 10);
66
+ header.writeUInt16LE(time, 12);
67
+ header.writeUInt16LE(date, 14);
68
+ header.writeUInt32LE(entry.crc, 16);
69
+ header.writeUInt32LE(entry.size, 20);
70
+ header.writeUInt32LE(entry.size, 24);
71
+ header.writeUInt16LE(entry.name.length, 28);
72
+ header.writeUInt32LE(0, 30); // extra + comment
73
+ header.writeUInt16LE(0, 34);
74
+ header.writeUInt16LE(0, 36);
75
+ header.writeUInt32LE(0, 38);
76
+ header.writeUInt32LE(entry.offset, 42);
77
+ chunks.push(header, entry.name);
78
+ offset += header.length + entry.name.length;
79
+ }
80
+ const end = Buffer.alloc(22);
81
+ end.writeUInt32LE(0x06054B50, 0);
82
+ end.writeUInt16LE(central.length, 8);
83
+ end.writeUInt16LE(central.length, 10);
84
+ end.writeUInt32LE(offset - centralStart, 12);
85
+ end.writeUInt32LE(centralStart, 16);
86
+ chunks.push(end);
87
+ return Buffer.concat(chunks);
88
+ }
package/package.json CHANGED
@@ -1,6 +1,52 @@
1
1
  {
2
2
  "name": "@airongrong/plugin-cli",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "1.0.0-beta.0",
4
+ "description": "AiToEarn plugin developer CLI: scaffold, verify, pack, sign channel adapters / hooks / skills and generate surfaces from capability.yaml",
5
+ "license": "MIT",
6
+ "type": "commonjs",
7
+ "main": "./dist/main.js",
8
+ "types": "./dist/main.d.ts",
9
+ "bin": {
10
+ "aitoearn-plugin": "./dist/main.js"
11
+ },
12
+ "files": [
13
+ "dist",
14
+ "README.md",
15
+ "LICENSE"
16
+ ],
17
+ "engines": {
18
+ "node": ">=20"
19
+ },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/yikart/aitoearn-sdk.git",
26
+ "directory": "packages/plugin-cli"
27
+ },
28
+ "homepage": "https://github.com/yikart/aitoearn-sdk/tree/main/packages/plugin-cli#readme",
29
+ "bugs": {
30
+ "url": "https://github.com/yikart/aitoearn-sdk/issues"
31
+ },
32
+ "keywords": [
33
+ "aitoearn",
34
+ "plugin",
35
+ "cli",
36
+ "channel-adapter",
37
+ "capability",
38
+ "content-publish"
39
+ ],
40
+ "scripts": {
41
+ "build": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json && node scripts/postbuild.mjs",
42
+ "clean": "node -e \"require('node:fs').rmSync('dist',{recursive:true,force:true})\"",
43
+ "test": "vitest run",
44
+ "typecheck": "tsc -p tsconfig.json --noEmit",
45
+ "prepublishOnly": "node ../../scripts/prepublish-guard.mjs"
46
+ },
47
+ "dependencies": {
48
+ "@airongrong/capability": "1.0.0-beta.0",
49
+ "@airongrong/plugin-api": "1.0.0-beta.0",
50
+ "commander": "^14.0.3"
51
+ }
52
+ }