create-renre-extension 0.0.22-beta.5
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/dist/index.d.ts +2 -0
- package/dist/index.js +351 -0
- package/dist/index.js.map +1 -0
- package/package.json +31 -0
- package/src/index.ts +19 -0
- package/src/scaffold.test.ts +159 -0
- package/src/scaffold.ts +73 -0
- package/src/templates/mcp.test.ts +112 -0
- package/src/templates/mcp.ts +149 -0
- package/src/templates/shared.ts +19 -0
- package/src/templates/standard.test.ts +117 -0
- package/src/templates/standard.ts +123 -0
- package/tsconfig.json +11 -0
- package/tsconfig.lint.json +12 -0
- package/tsup.config.ts +14 -0
- package/vitest.config.ts +11 -0
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/scaffold.ts
|
|
4
|
+
import path from "path";
|
|
5
|
+
import fse from "fs-extra";
|
|
6
|
+
|
|
7
|
+
// src/templates/shared.ts
|
|
8
|
+
function getTsconfig() {
|
|
9
|
+
const config = {
|
|
10
|
+
compilerOptions: {
|
|
11
|
+
target: "ES2022",
|
|
12
|
+
module: "NodeNext",
|
|
13
|
+
moduleResolution: "NodeNext",
|
|
14
|
+
declaration: true,
|
|
15
|
+
outDir: "./dist",
|
|
16
|
+
rootDir: "./src",
|
|
17
|
+
strict: true,
|
|
18
|
+
esModuleInterop: true,
|
|
19
|
+
skipLibCheck: true
|
|
20
|
+
},
|
|
21
|
+
include: ["src"],
|
|
22
|
+
exclude: ["dist", "node_modules"]
|
|
23
|
+
};
|
|
24
|
+
return `${JSON.stringify(config, null, 2)}
|
|
25
|
+
`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// src/templates/standard.ts
|
|
29
|
+
function getStandardPackageJson(name2) {
|
|
30
|
+
const pkg = {
|
|
31
|
+
name: name2,
|
|
32
|
+
version: "0.0.1",
|
|
33
|
+
description: `A RenreKit extension: ${name2}`,
|
|
34
|
+
type: "module",
|
|
35
|
+
main: "./dist/index.js",
|
|
36
|
+
scripts: {
|
|
37
|
+
build: "node build.js",
|
|
38
|
+
dev: "tsc --watch"
|
|
39
|
+
},
|
|
40
|
+
dependencies: {
|
|
41
|
+
"@renre-kit/extension-sdk": ">=0.0.1"
|
|
42
|
+
},
|
|
43
|
+
devDependencies: {
|
|
44
|
+
esbuild: "^0.21.0",
|
|
45
|
+
typescript: "^5.7.0"
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
return `${JSON.stringify(pkg, null, 2)}
|
|
49
|
+
`;
|
|
50
|
+
}
|
|
51
|
+
function getStandardManifest(name2) {
|
|
52
|
+
const manifest = {
|
|
53
|
+
name: name2,
|
|
54
|
+
title: name2,
|
|
55
|
+
version: "0.0.1",
|
|
56
|
+
description: "A RenreKit extension",
|
|
57
|
+
type: "standard",
|
|
58
|
+
main: "dist/index.js",
|
|
59
|
+
engines: {
|
|
60
|
+
"renre-kit": ">=0.0.1",
|
|
61
|
+
"extension-sdk": ">=0.0.1"
|
|
62
|
+
},
|
|
63
|
+
commands: {
|
|
64
|
+
hello: {
|
|
65
|
+
handler: "dist/commands/hello.js",
|
|
66
|
+
description: "Say hello"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
return `${JSON.stringify(manifest, null, 2)}
|
|
71
|
+
`;
|
|
72
|
+
}
|
|
73
|
+
function getStandardEntryPoint(name2) {
|
|
74
|
+
return `import type { HookContext } from '@renre-kit/extension-sdk/node';
|
|
75
|
+
|
|
76
|
+
export function onInit(context: HookContext): void {
|
|
77
|
+
context.sdk.deployAgentAssets();
|
|
78
|
+
console.log('${name2} initialized in', context.projectDir);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function onDestroy(context: HookContext): void {
|
|
82
|
+
context.sdk.cleanupAgentAssets();
|
|
83
|
+
console.log('${name2} destroyed in', context.projectDir);
|
|
84
|
+
}
|
|
85
|
+
`;
|
|
86
|
+
}
|
|
87
|
+
function getStandardCommandHandler(name2) {
|
|
88
|
+
return `import { defineCommand } from '@renre-kit/extension-sdk/node';
|
|
89
|
+
|
|
90
|
+
export default defineCommand({
|
|
91
|
+
handler: () => {
|
|
92
|
+
return { output: 'Hello from ${name2}!', exitCode: 0 };
|
|
93
|
+
},
|
|
94
|
+
});
|
|
95
|
+
`;
|
|
96
|
+
}
|
|
97
|
+
function getStandardBuildJs() {
|
|
98
|
+
return `import { readFileSync, rmSync } from 'node:fs';
|
|
99
|
+
|
|
100
|
+
import { buildExtension, archiveDist } from '@renre-kit/extension-sdk/node';
|
|
101
|
+
|
|
102
|
+
rmSync('dist', { recursive: true, force: true });
|
|
103
|
+
|
|
104
|
+
const manifest = JSON.parse(readFileSync('manifest.json', 'utf-8'));
|
|
105
|
+
|
|
106
|
+
await buildExtension({
|
|
107
|
+
entryPoints: [
|
|
108
|
+
{ in: 'src/index.ts', out: 'index' },
|
|
109
|
+
{ in: 'src/commands/hello.ts', out: 'commands/hello' },
|
|
110
|
+
],
|
|
111
|
+
outdir: 'dist',
|
|
112
|
+
external: [],
|
|
113
|
+
splitting: true,
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
await archiveDist('dist', manifest.version);
|
|
117
|
+
`;
|
|
118
|
+
}
|
|
119
|
+
function getStandardSkillMd(name2) {
|
|
120
|
+
return `---
|
|
121
|
+
name: hello
|
|
122
|
+
description: This tool should be used when the user wants to say hello or get a greeting from the ${name2} extension
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
# ${name2}
|
|
126
|
+
|
|
127
|
+
## Description
|
|
128
|
+
|
|
129
|
+
A RenreKit extension that provides additional functionality.
|
|
130
|
+
|
|
131
|
+
## Commands
|
|
132
|
+
|
|
133
|
+
### hello
|
|
134
|
+
|
|
135
|
+
Say hello from the extension.
|
|
136
|
+
|
|
137
|
+
**Usage:**
|
|
138
|
+
\`\`\`
|
|
139
|
+
renre ${name2} hello
|
|
140
|
+
\`\`\`
|
|
141
|
+
|
|
142
|
+
## Configuration
|
|
143
|
+
|
|
144
|
+
No configuration required.
|
|
145
|
+
`;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// src/templates/mcp.ts
|
|
149
|
+
function getMcpPackageJson(name2) {
|
|
150
|
+
const pkg = {
|
|
151
|
+
name: name2,
|
|
152
|
+
version: "0.0.1",
|
|
153
|
+
description: `A RenreKit MCP extension: ${name2}`,
|
|
154
|
+
type: "module",
|
|
155
|
+
main: "./dist/server.js",
|
|
156
|
+
scripts: {
|
|
157
|
+
build: "node build.js",
|
|
158
|
+
dev: "tsc --watch"
|
|
159
|
+
},
|
|
160
|
+
dependencies: {
|
|
161
|
+
"@renre-kit/extension-sdk": ">=0.0.1"
|
|
162
|
+
},
|
|
163
|
+
devDependencies: {
|
|
164
|
+
esbuild: "^0.21.0",
|
|
165
|
+
typescript: "^5.7.0"
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
return `${JSON.stringify(pkg, null, 2)}
|
|
169
|
+
`;
|
|
170
|
+
}
|
|
171
|
+
function getMcpManifest(name2) {
|
|
172
|
+
const manifest = {
|
|
173
|
+
name: name2,
|
|
174
|
+
title: name2,
|
|
175
|
+
version: "0.0.1",
|
|
176
|
+
description: "A RenreKit MCP extension",
|
|
177
|
+
type: "mcp",
|
|
178
|
+
main: "dist/index.js",
|
|
179
|
+
engines: {
|
|
180
|
+
"renre-kit": ">=0.0.1",
|
|
181
|
+
"extension-sdk": ">=0.0.1"
|
|
182
|
+
},
|
|
183
|
+
commands: {},
|
|
184
|
+
mcp: {
|
|
185
|
+
transport: "stdio",
|
|
186
|
+
command: "node",
|
|
187
|
+
args: ["dist/server.js"]
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
return `${JSON.stringify(manifest, null, 2)}
|
|
191
|
+
`;
|
|
192
|
+
}
|
|
193
|
+
function getMcpServerEntryPoint(name2) {
|
|
194
|
+
return `import { createInterface } from 'node:readline';
|
|
195
|
+
|
|
196
|
+
interface JsonRpcRequest {
|
|
197
|
+
jsonrpc: string;
|
|
198
|
+
id: number | string;
|
|
199
|
+
method: string;
|
|
200
|
+
params?: Record<string, unknown>;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
interface JsonRpcResponse {
|
|
204
|
+
jsonrpc: string;
|
|
205
|
+
id: number | string;
|
|
206
|
+
result?: unknown;
|
|
207
|
+
error?: { code: number; message: string };
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function handleRequest(request: JsonRpcRequest): JsonRpcResponse {
|
|
211
|
+
switch (request.method) {
|
|
212
|
+
case 'hello':
|
|
213
|
+
return {
|
|
214
|
+
jsonrpc: '2.0',
|
|
215
|
+
id: request.id,
|
|
216
|
+
result: { output: 'Hello from ${name2}!', exitCode: 0 },
|
|
217
|
+
};
|
|
218
|
+
default:
|
|
219
|
+
return {
|
|
220
|
+
jsonrpc: '2.0',
|
|
221
|
+
id: request.id,
|
|
222
|
+
error: { code: -32601, message: \`Method not found: \${request.method}\` },
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
const rl = createInterface({ input: process.stdin });
|
|
228
|
+
|
|
229
|
+
rl.on('line', (line: string) => {
|
|
230
|
+
try {
|
|
231
|
+
const request = JSON.parse(line) as JsonRpcRequest;
|
|
232
|
+
const response = handleRequest(request);
|
|
233
|
+
process.stdout.write(JSON.stringify(response) + '\\n');
|
|
234
|
+
} catch {
|
|
235
|
+
const errorResponse: JsonRpcResponse = {
|
|
236
|
+
jsonrpc: '2.0',
|
|
237
|
+
id: 0,
|
|
238
|
+
error: { code: -32700, message: 'Parse error' },
|
|
239
|
+
};
|
|
240
|
+
process.stdout.write(JSON.stringify(errorResponse) + '\\n');
|
|
241
|
+
}
|
|
242
|
+
});
|
|
243
|
+
`;
|
|
244
|
+
}
|
|
245
|
+
function getMcpBuildJs() {
|
|
246
|
+
return `import { readFileSync, rmSync } from 'node:fs';
|
|
247
|
+
|
|
248
|
+
import { buildExtension, archiveDist } from '@renre-kit/extension-sdk/node';
|
|
249
|
+
|
|
250
|
+
rmSync('dist', { recursive: true, force: true });
|
|
251
|
+
|
|
252
|
+
const manifest = JSON.parse(readFileSync('manifest.json', 'utf-8'));
|
|
253
|
+
|
|
254
|
+
await buildExtension({
|
|
255
|
+
entryPoints: [
|
|
256
|
+
{ in: 'src/server.ts', out: 'server' },
|
|
257
|
+
],
|
|
258
|
+
outdir: 'dist',
|
|
259
|
+
external: [],
|
|
260
|
+
splitting: true,
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
await archiveDist('dist', manifest.version);
|
|
264
|
+
`;
|
|
265
|
+
}
|
|
266
|
+
function getMcpSkillMd(name2) {
|
|
267
|
+
return `---
|
|
268
|
+
name: hello
|
|
269
|
+
description: This tool should be used when the user wants to say hello or test the ${name2} MCP extension
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
# ${name2}
|
|
273
|
+
|
|
274
|
+
## Description
|
|
275
|
+
|
|
276
|
+
A RenreKit MCP extension that communicates via JSON-RPC over stdio.
|
|
277
|
+
|
|
278
|
+
## Commands
|
|
279
|
+
|
|
280
|
+
### hello
|
|
281
|
+
|
|
282
|
+
Say hello from the MCP extension.
|
|
283
|
+
|
|
284
|
+
**Usage:**
|
|
285
|
+
\`\`\`
|
|
286
|
+
renre ${name2} hello
|
|
287
|
+
\`\`\`
|
|
288
|
+
|
|
289
|
+
## Configuration
|
|
290
|
+
|
|
291
|
+
No configuration required.
|
|
292
|
+
`;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// src/scaffold.ts
|
|
296
|
+
function getStandardFiles(name2, extDir) {
|
|
297
|
+
return [
|
|
298
|
+
{ filePath: path.join(extDir, "package.json"), content: getStandardPackageJson(name2) },
|
|
299
|
+
{ filePath: path.join(extDir, "manifest.json"), content: getStandardManifest(name2) },
|
|
300
|
+
{ filePath: path.join(extDir, "src", "index.ts"), content: getStandardEntryPoint(name2) },
|
|
301
|
+
{
|
|
302
|
+
filePath: path.join(extDir, "src", "commands", "hello.ts"),
|
|
303
|
+
content: getStandardCommandHandler(name2)
|
|
304
|
+
},
|
|
305
|
+
{ filePath: path.join(extDir, "tsconfig.json"), content: getTsconfig() },
|
|
306
|
+
{ filePath: path.join(extDir, "build.js"), content: getStandardBuildJs() },
|
|
307
|
+
{ filePath: path.join(extDir, "SKILL.md"), content: getStandardSkillMd(name2) }
|
|
308
|
+
];
|
|
309
|
+
}
|
|
310
|
+
function getMcpFiles(name2, extDir) {
|
|
311
|
+
return [
|
|
312
|
+
{ filePath: path.join(extDir, "package.json"), content: getMcpPackageJson(name2) },
|
|
313
|
+
{ filePath: path.join(extDir, "manifest.json"), content: getMcpManifest(name2) },
|
|
314
|
+
{ filePath: path.join(extDir, "src", "server.ts"), content: getMcpServerEntryPoint(name2) },
|
|
315
|
+
{ filePath: path.join(extDir, "tsconfig.json"), content: getTsconfig() },
|
|
316
|
+
{ filePath: path.join(extDir, "build.js"), content: getMcpBuildJs() },
|
|
317
|
+
{ filePath: path.join(extDir, "SKILL.md"), content: getMcpSkillMd(name2) }
|
|
318
|
+
];
|
|
319
|
+
}
|
|
320
|
+
async function scaffoldExtension(name2, type2, outputDir2) {
|
|
321
|
+
const extDir = path.join(outputDir2, name2);
|
|
322
|
+
if (await fse.pathExists(extDir)) {
|
|
323
|
+
throw new Error(`Directory "${name2}" already exists`);
|
|
324
|
+
}
|
|
325
|
+
const files = type2 === "mcp" ? getMcpFiles(name2, extDir) : getStandardFiles(name2, extDir);
|
|
326
|
+
for (const file of files) {
|
|
327
|
+
await fse.ensureDir(path.dirname(file.filePath));
|
|
328
|
+
await fse.writeFile(file.filePath, file.content, "utf-8");
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// src/index.ts
|
|
333
|
+
var args = process.argv.slice(2);
|
|
334
|
+
var name = args[0] ?? "my-extension";
|
|
335
|
+
var typeFlag = args.indexOf("--type");
|
|
336
|
+
var type = typeFlag !== -1 && args[typeFlag + 1] === "mcp" ? "mcp" : "standard";
|
|
337
|
+
var outputDir = process.cwd();
|
|
338
|
+
try {
|
|
339
|
+
await scaffoldExtension(name, type, outputDir);
|
|
340
|
+
console.log(`
|
|
341
|
+
Extension "${name}" created successfully!`);
|
|
342
|
+
console.log(`
|
|
343
|
+
Next steps:`);
|
|
344
|
+
console.log(` cd ${name}`);
|
|
345
|
+
console.log(` pnpm install`);
|
|
346
|
+
console.log(` pnpm run build`);
|
|
347
|
+
} catch (err) {
|
|
348
|
+
console.error("Failed to create extension:", err instanceof Error ? err.message : String(err));
|
|
349
|
+
process.exit(1);
|
|
350
|
+
}
|
|
351
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/scaffold.ts","../src/templates/shared.ts","../src/templates/standard.ts","../src/templates/mcp.ts","../src/index.ts"],"sourcesContent":["import path from 'node:path';\n\nimport fse from 'fs-extra';\n\nimport {\n getStandardPackageJson,\n getStandardManifest,\n getStandardEntryPoint,\n getStandardCommandHandler,\n getStandardTsconfig,\n getStandardSkillMd,\n getStandardBuildJs,\n} from './templates/standard.js';\nimport {\n getMcpPackageJson,\n getMcpManifest,\n getMcpServerEntryPoint,\n getMcpTsconfig,\n getMcpSkillMd,\n getMcpBuildJs,\n} from './templates/mcp.js';\n\nexport type ExtensionType = 'standard' | 'mcp';\n\ninterface FileEntry {\n filePath: string;\n content: string;\n}\n\nfunction getStandardFiles(name: string, extDir: string): FileEntry[] {\n return [\n { filePath: path.join(extDir, 'package.json'), content: getStandardPackageJson(name) },\n { filePath: path.join(extDir, 'manifest.json'), content: getStandardManifest(name) },\n { filePath: path.join(extDir, 'src', 'index.ts'), content: getStandardEntryPoint(name) },\n {\n filePath: path.join(extDir, 'src', 'commands', 'hello.ts'),\n content: getStandardCommandHandler(name),\n },\n { filePath: path.join(extDir, 'tsconfig.json'), content: getStandardTsconfig() },\n { filePath: path.join(extDir, 'build.js'), content: getStandardBuildJs() },\n { filePath: path.join(extDir, 'SKILL.md'), content: getStandardSkillMd(name) },\n ];\n}\n\nfunction getMcpFiles(name: string, extDir: string): FileEntry[] {\n return [\n { filePath: path.join(extDir, 'package.json'), content: getMcpPackageJson(name) },\n { filePath: path.join(extDir, 'manifest.json'), content: getMcpManifest(name) },\n { filePath: path.join(extDir, 'src', 'server.ts'), content: getMcpServerEntryPoint(name) },\n { filePath: path.join(extDir, 'tsconfig.json'), content: getMcpTsconfig() },\n { filePath: path.join(extDir, 'build.js'), content: getMcpBuildJs() },\n { filePath: path.join(extDir, 'SKILL.md'), content: getMcpSkillMd(name) },\n ];\n}\n\nexport async function scaffoldExtension(\n name: string,\n type: ExtensionType,\n outputDir: string,\n): Promise<void> {\n const extDir = path.join(outputDir, name);\n\n if (await fse.pathExists(extDir)) {\n throw new Error(`Directory \"${name}\" already exists`);\n }\n\n const files = type === 'mcp' ? getMcpFiles(name, extDir) : getStandardFiles(name, extDir);\n\n for (const file of files) {\n await fse.ensureDir(path.dirname(file.filePath));\n await fse.writeFile(file.filePath, file.content, 'utf-8');\n }\n}\n","/** Generates a tsconfig.json for any extension type. */\nexport function getTsconfig(): string {\n const config = {\n compilerOptions: {\n target: 'ES2022',\n module: 'NodeNext',\n moduleResolution: 'NodeNext',\n declaration: true,\n outDir: './dist',\n rootDir: './src',\n strict: true,\n esModuleInterop: true,\n skipLibCheck: true,\n },\n include: ['src'],\n exclude: ['dist', 'node_modules'],\n };\n return `${JSON.stringify(config, null, 2) }\\n`;\n}\n","export function getStandardPackageJson(name: string): string {\n const pkg = {\n name,\n version: '0.0.1',\n description: `A RenreKit extension: ${name}`,\n type: 'module',\n main: './dist/index.js',\n scripts: {\n build: 'node build.js',\n dev: 'tsc --watch',\n },\n dependencies: {\n '@renre-kit/extension-sdk': '>=0.0.1',\n },\n devDependencies: {\n esbuild: '^0.21.0',\n typescript: '^5.7.0',\n },\n };\n return `${JSON.stringify(pkg, null, 2) }\\n`;\n}\n\nexport function getStandardManifest(name: string): string {\n const manifest = {\n name,\n title: name,\n version: '0.0.1',\n description: 'A RenreKit extension',\n type: 'standard',\n main: 'dist/index.js',\n engines: {\n 'renre-kit': '>=0.0.1',\n 'extension-sdk': '>=0.0.1',\n },\n commands: {\n hello: {\n handler: 'dist/commands/hello.js',\n description: 'Say hello',\n },\n },\n };\n return `${JSON.stringify(manifest, null, 2) }\\n`;\n}\n\nexport function getStandardEntryPoint(name: string): string {\n return `import type { HookContext } from '@renre-kit/extension-sdk/node';\n\nexport function onInit(context: HookContext): void {\n context.sdk.deployAgentAssets();\n console.log('${name} initialized in', context.projectDir);\n}\n\nexport function onDestroy(context: HookContext): void {\n context.sdk.cleanupAgentAssets();\n console.log('${name} destroyed in', context.projectDir);\n}\n`;\n}\n\nexport function getStandardCommandHandler(name: string): string {\n return `import { defineCommand } from '@renre-kit/extension-sdk/node';\n\nexport default defineCommand({\n handler: () => {\n return { output: 'Hello from ${name}!', exitCode: 0 };\n },\n});\n`;\n}\n\nexport { getTsconfig as getStandardTsconfig } from './shared.js';\n\nexport function getStandardBuildJs(): string {\n return `import { readFileSync, rmSync } from 'node:fs';\n\nimport { buildExtension, archiveDist } from '@renre-kit/extension-sdk/node';\n\nrmSync('dist', { recursive: true, force: true });\n\nconst manifest = JSON.parse(readFileSync('manifest.json', 'utf-8'));\n\nawait buildExtension({\n entryPoints: [\n { in: 'src/index.ts', out: 'index' },\n { in: 'src/commands/hello.ts', out: 'commands/hello' },\n ],\n outdir: 'dist',\n external: [],\n splitting: true,\n});\n\nawait archiveDist('dist', manifest.version);\n`;\n}\n\nexport function getStandardSkillMd(name: string): string {\n return `---\nname: hello\ndescription: This tool should be used when the user wants to say hello or get a greeting from the ${name} extension\n---\n\n# ${name}\n\n## Description\n\nA RenreKit extension that provides additional functionality.\n\n## Commands\n\n### hello\n\nSay hello from the extension.\n\n**Usage:**\n\\`\\`\\`\nrenre ${name} hello\n\\`\\`\\`\n\n## Configuration\n\nNo configuration required.\n`;\n}\n","export function getMcpPackageJson(name: string): string {\n const pkg = {\n name,\n version: '0.0.1',\n description: `A RenreKit MCP extension: ${name}`,\n type: 'module',\n main: './dist/server.js',\n scripts: {\n build: 'node build.js',\n dev: 'tsc --watch',\n },\n dependencies: {\n '@renre-kit/extension-sdk': '>=0.0.1',\n },\n devDependencies: {\n esbuild: '^0.21.0',\n typescript: '^5.7.0',\n },\n };\n return `${JSON.stringify(pkg, null, 2) }\\n`;\n}\n\nexport function getMcpManifest(name: string): string {\n const manifest = {\n name,\n title: name,\n version: '0.0.1',\n description: 'A RenreKit MCP extension',\n type: 'mcp',\n main: 'dist/index.js',\n engines: {\n 'renre-kit': '>=0.0.1',\n 'extension-sdk': '>=0.0.1',\n },\n commands: {},\n mcp: {\n transport: 'stdio',\n command: 'node',\n args: ['dist/server.js'],\n },\n };\n return `${JSON.stringify(manifest, null, 2) }\\n`;\n}\n\nexport function getMcpServerEntryPoint(name: string): string {\n return `import { createInterface } from 'node:readline';\n\ninterface JsonRpcRequest {\n jsonrpc: string;\n id: number | string;\n method: string;\n params?: Record<string, unknown>;\n}\n\ninterface JsonRpcResponse {\n jsonrpc: string;\n id: number | string;\n result?: unknown;\n error?: { code: number; message: string };\n}\n\nfunction handleRequest(request: JsonRpcRequest): JsonRpcResponse {\n switch (request.method) {\n case 'hello':\n return {\n jsonrpc: '2.0',\n id: request.id,\n result: { output: 'Hello from ${name}!', exitCode: 0 },\n };\n default:\n return {\n jsonrpc: '2.0',\n id: request.id,\n error: { code: -32601, message: \\`Method not found: \\${request.method}\\` },\n };\n }\n}\n\nconst rl = createInterface({ input: process.stdin });\n\nrl.on('line', (line: string) => {\n try {\n const request = JSON.parse(line) as JsonRpcRequest;\n const response = handleRequest(request);\n process.stdout.write(JSON.stringify(response) + '\\\\n');\n } catch {\n const errorResponse: JsonRpcResponse = {\n jsonrpc: '2.0',\n id: 0,\n error: { code: -32700, message: 'Parse error' },\n };\n process.stdout.write(JSON.stringify(errorResponse) + '\\\\n');\n }\n});\n`;\n}\n\nexport { getTsconfig as getMcpTsconfig } from './shared.js';\n\nexport function getMcpBuildJs(): string {\n return `import { readFileSync, rmSync } from 'node:fs';\n\nimport { buildExtension, archiveDist } from '@renre-kit/extension-sdk/node';\n\nrmSync('dist', { recursive: true, force: true });\n\nconst manifest = JSON.parse(readFileSync('manifest.json', 'utf-8'));\n\nawait buildExtension({\n entryPoints: [\n { in: 'src/server.ts', out: 'server' },\n ],\n outdir: 'dist',\n external: [],\n splitting: true,\n});\n\nawait archiveDist('dist', manifest.version);\n`;\n}\n\nexport function getMcpSkillMd(name: string): string {\n return `---\nname: hello\ndescription: This tool should be used when the user wants to say hello or test the ${name} MCP extension\n---\n\n# ${name}\n\n## Description\n\nA RenreKit MCP extension that communicates via JSON-RPC over stdio.\n\n## Commands\n\n### hello\n\nSay hello from the MCP extension.\n\n**Usage:**\n\\`\\`\\`\nrenre ${name} hello\n\\`\\`\\`\n\n## Configuration\n\nNo configuration required.\n`;\n}\n","import { scaffoldExtension } from './scaffold.js';\n\nconst args = process.argv.slice(2);\nconst name = args[0] ?? 'my-extension';\nconst typeFlag = args.indexOf('--type');\nconst type = typeFlag !== -1 && args[typeFlag + 1] === 'mcp' ? 'mcp' : 'standard';\nconst outputDir = process.cwd();\n\ntry {\n await scaffoldExtension(name, type, outputDir);\n console.log(`\\nExtension \"${name}\" created successfully!`);\n console.log(`\\nNext steps:`);\n console.log(` cd ${name}`);\n console.log(` pnpm install`);\n console.log(` pnpm run build`);\n} catch (err: unknown) {\n console.error('Failed to create extension:', err instanceof Error ? err.message : String(err));\n process.exit(1);\n}\n"],"mappings":"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","names":["name","name","name","type","outputDir"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-renre-extension",
|
|
3
|
+
"version": "0.0.22-beta.5",
|
|
4
|
+
"description": "Scaffolding tool for creating RenreKit extensions",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"create-renre-extension": "./dist/index.js"
|
|
8
|
+
},
|
|
9
|
+
"main": "./dist/index.js",
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"scripts": {
|
|
12
|
+
"build": "tsup",
|
|
13
|
+
"dev": "tsup --watch",
|
|
14
|
+
"test": "vitest run",
|
|
15
|
+
"test:coverage": "vitest run --coverage",
|
|
16
|
+
"lint": "eslint --cache src/",
|
|
17
|
+
"lint:duplication": "jscpd src/ --config ../../.jscpd.json",
|
|
18
|
+
"typecheck": "tsc --noEmit"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"fs-extra": "^11.2.0"
|
|
22
|
+
},
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/fs-extra": "^11.0.0",
|
|
25
|
+
"@types/node": "^20",
|
|
26
|
+
"@vitest/coverage-istanbul": "^2.1.0",
|
|
27
|
+
"tsup": "^8.3.0",
|
|
28
|
+
"typescript": "^5.7.0",
|
|
29
|
+
"vitest": "^2.1.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { scaffoldExtension } from './scaffold.js';
|
|
2
|
+
|
|
3
|
+
const args = process.argv.slice(2);
|
|
4
|
+
const name = args[0] ?? 'my-extension';
|
|
5
|
+
const typeFlag = args.indexOf('--type');
|
|
6
|
+
const type = typeFlag !== -1 && args[typeFlag + 1] === 'mcp' ? 'mcp' : 'standard';
|
|
7
|
+
const outputDir = process.cwd();
|
|
8
|
+
|
|
9
|
+
try {
|
|
10
|
+
await scaffoldExtension(name, type, outputDir);
|
|
11
|
+
console.log(`\nExtension "${name}" created successfully!`);
|
|
12
|
+
console.log(`\nNext steps:`);
|
|
13
|
+
console.log(` cd ${name}`);
|
|
14
|
+
console.log(` pnpm install`);
|
|
15
|
+
console.log(` pnpm run build`);
|
|
16
|
+
} catch (err: unknown) {
|
|
17
|
+
console.error('Failed to create extension:', err instanceof Error ? err.message : String(err));
|
|
18
|
+
process.exit(1);
|
|
19
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
import os from 'node:os';
|
|
3
|
+
|
|
4
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
5
|
+
import fse from 'fs-extra';
|
|
6
|
+
|
|
7
|
+
import { scaffoldExtension } from './scaffold.js';
|
|
8
|
+
|
|
9
|
+
describe('scaffoldExtension', () => {
|
|
10
|
+
let tmpDir: string;
|
|
11
|
+
|
|
12
|
+
beforeEach(async () => {
|
|
13
|
+
tmpDir = await fse.mkdtemp(path.join(os.tmpdir(), 'renre-test-'));
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
afterEach(async () => {
|
|
17
|
+
await fse.remove(tmpDir);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
describe('standard extension', () => {
|
|
21
|
+
it('should create all required files', async () => {
|
|
22
|
+
await scaffoldExtension('test-ext', 'standard', tmpDir);
|
|
23
|
+
|
|
24
|
+
const extDir = path.join(tmpDir, 'test-ext');
|
|
25
|
+
expect(await fse.pathExists(path.join(extDir, 'package.json'))).toBe(true);
|
|
26
|
+
expect(await fse.pathExists(path.join(extDir, 'manifest.json'))).toBe(true);
|
|
27
|
+
expect(await fse.pathExists(path.join(extDir, 'src', 'index.ts'))).toBe(true);
|
|
28
|
+
expect(await fse.pathExists(path.join(extDir, 'src', 'commands', 'hello.ts'))).toBe(true);
|
|
29
|
+
expect(await fse.pathExists(path.join(extDir, 'tsconfig.json'))).toBe(true);
|
|
30
|
+
expect(await fse.pathExists(path.join(extDir, 'build.js'))).toBe(true);
|
|
31
|
+
expect(await fse.pathExists(path.join(extDir, 'SKILL.md'))).toBe(true);
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should generate correct package.json', async () => {
|
|
35
|
+
await scaffoldExtension('my-plugin', 'standard', tmpDir);
|
|
36
|
+
|
|
37
|
+
const pkgPath = path.join(tmpDir, 'my-plugin', 'package.json');
|
|
38
|
+
const pkg = (await fse.readJson(pkgPath)) as Record<string, unknown>;
|
|
39
|
+
expect(pkg['name']).toBe('my-plugin');
|
|
40
|
+
expect(pkg['version']).toBe('0.0.1');
|
|
41
|
+
expect(pkg['type']).toBe('module');
|
|
42
|
+
expect(pkg['main']).toBe('./dist/index.js');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
it('should generate correct manifest.json', async () => {
|
|
46
|
+
await scaffoldExtension('my-plugin', 'standard', tmpDir);
|
|
47
|
+
|
|
48
|
+
const manifestPath = path.join(tmpDir, 'my-plugin', 'manifest.json');
|
|
49
|
+
const manifest = (await fse.readJson(manifestPath)) as Record<string, unknown>;
|
|
50
|
+
expect(manifest['name']).toBe('my-plugin');
|
|
51
|
+
expect(manifest['type']).toBe('standard');
|
|
52
|
+
expect(manifest['main']).toBe('dist/index.js');
|
|
53
|
+
const commands = manifest['commands'] as Record<string, Record<string, string>>;
|
|
54
|
+
expect(commands['hello']!['handler']).toBe('dist/commands/hello.js');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should include engines field in manifest', async () => {
|
|
58
|
+
await scaffoldExtension('my-plugin', 'standard', tmpDir);
|
|
59
|
+
|
|
60
|
+
const manifestPath = path.join(tmpDir, 'my-plugin', 'manifest.json');
|
|
61
|
+
const manifest = (await fse.readJson(manifestPath)) as Record<string, unknown>;
|
|
62
|
+
const engines = manifest['engines'] as Record<string, string>;
|
|
63
|
+
expect(engines).toBeDefined();
|
|
64
|
+
expect(engines['renre-kit']).toBe('>=0.0.1');
|
|
65
|
+
expect(engines['extension-sdk']).toBe('>=0.0.1');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should generate entry point with lifecycle hooks', async () => {
|
|
69
|
+
await scaffoldExtension('my-plugin', 'standard', tmpDir);
|
|
70
|
+
|
|
71
|
+
const entryPath = path.join(tmpDir, 'my-plugin', 'src', 'index.ts');
|
|
72
|
+
const content = await fse.readFile(entryPath, 'utf-8');
|
|
73
|
+
expect(content).toContain('export function onInit');
|
|
74
|
+
expect(content).toContain('export function onDestroy');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should generate command handler file', async () => {
|
|
78
|
+
await scaffoldExtension('my-plugin', 'standard', tmpDir);
|
|
79
|
+
|
|
80
|
+
const cmdPath = path.join(tmpDir, 'my-plugin', 'src', 'commands', 'hello.ts');
|
|
81
|
+
const content = await fse.readFile(cmdPath, 'utf-8');
|
|
82
|
+
expect(content).toContain('Hello from my-plugin!');
|
|
83
|
+
expect(content).toContain('export default defineCommand');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should generate SKILL.md with extension name', async () => {
|
|
87
|
+
await scaffoldExtension('my-plugin', 'standard', tmpDir);
|
|
88
|
+
|
|
89
|
+
const skillPath = path.join(tmpDir, 'my-plugin', 'SKILL.md');
|
|
90
|
+
const content = await fse.readFile(skillPath, 'utf-8');
|
|
91
|
+
expect(content).toContain('# my-plugin');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('mcp extension', () => {
|
|
96
|
+
it('should create all required files', async () => {
|
|
97
|
+
await scaffoldExtension('mcp-ext', 'mcp', tmpDir);
|
|
98
|
+
|
|
99
|
+
const extDir = path.join(tmpDir, 'mcp-ext');
|
|
100
|
+
expect(await fse.pathExists(path.join(extDir, 'package.json'))).toBe(true);
|
|
101
|
+
expect(await fse.pathExists(path.join(extDir, 'manifest.json'))).toBe(true);
|
|
102
|
+
expect(await fse.pathExists(path.join(extDir, 'src', 'server.ts'))).toBe(true);
|
|
103
|
+
expect(await fse.pathExists(path.join(extDir, 'tsconfig.json'))).toBe(true);
|
|
104
|
+
expect(await fse.pathExists(path.join(extDir, 'build.js'))).toBe(true);
|
|
105
|
+
expect(await fse.pathExists(path.join(extDir, 'SKILL.md'))).toBe(true);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('should generate correct manifest with mcp type', async () => {
|
|
109
|
+
await scaffoldExtension('mcp-ext', 'mcp', tmpDir);
|
|
110
|
+
|
|
111
|
+
const manifestPath = path.join(tmpDir, 'mcp-ext', 'manifest.json');
|
|
112
|
+
const manifest = (await fse.readJson(manifestPath)) as Record<string, unknown>;
|
|
113
|
+
expect(manifest['name']).toBe('mcp-ext');
|
|
114
|
+
expect(manifest['type']).toBe('mcp');
|
|
115
|
+
expect(manifest['main']).toBe('dist/index.js');
|
|
116
|
+
expect(manifest['mcp']).toBeDefined();
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('should include engines field in manifest', async () => {
|
|
120
|
+
await scaffoldExtension('mcp-ext', 'mcp', tmpDir);
|
|
121
|
+
|
|
122
|
+
const manifestPath = path.join(tmpDir, 'mcp-ext', 'manifest.json');
|
|
123
|
+
const manifest = (await fse.readJson(manifestPath)) as Record<string, string>;
|
|
124
|
+
const engines = manifest['engines'] as unknown as Record<string, string>;
|
|
125
|
+
expect(engines).toBeDefined();
|
|
126
|
+
expect(engines['renre-kit']).toBe('>=0.0.1');
|
|
127
|
+
expect(engines['extension-sdk']).toBe('>=0.0.1');
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('should generate server entry point with JSON-RPC handler', async () => {
|
|
131
|
+
await scaffoldExtension('mcp-ext', 'mcp', tmpDir);
|
|
132
|
+
|
|
133
|
+
const serverPath = path.join(tmpDir, 'mcp-ext', 'src', 'server.ts');
|
|
134
|
+
const content = await fse.readFile(serverPath, 'utf-8');
|
|
135
|
+
expect(content).toContain('Hello from mcp-ext!');
|
|
136
|
+
expect(content).toContain('JsonRpcRequest');
|
|
137
|
+
expect(content).toContain('handleRequest');
|
|
138
|
+
expect(content).toContain('createInterface');
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should generate package.json with server as main', async () => {
|
|
142
|
+
await scaffoldExtension('mcp-ext', 'mcp', tmpDir);
|
|
143
|
+
|
|
144
|
+
const pkgPath = path.join(tmpDir, 'mcp-ext', 'package.json');
|
|
145
|
+
const pkg = (await fse.readJson(pkgPath)) as Record<string, unknown>;
|
|
146
|
+
expect(pkg['main']).toBe('./dist/server.js');
|
|
147
|
+
});
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
describe('error handling', () => {
|
|
151
|
+
it('should throw if directory already exists', async () => {
|
|
152
|
+
await fse.ensureDir(path.join(tmpDir, 'existing-ext'));
|
|
153
|
+
|
|
154
|
+
await expect(scaffoldExtension('existing-ext', 'standard', tmpDir)).rejects.toThrow(
|
|
155
|
+
'Directory "existing-ext" already exists',
|
|
156
|
+
);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
});
|
package/src/scaffold.ts
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import path from 'node:path';
|
|
2
|
+
|
|
3
|
+
import fse from 'fs-extra';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
getStandardPackageJson,
|
|
7
|
+
getStandardManifest,
|
|
8
|
+
getStandardEntryPoint,
|
|
9
|
+
getStandardCommandHandler,
|
|
10
|
+
getStandardTsconfig,
|
|
11
|
+
getStandardSkillMd,
|
|
12
|
+
getStandardBuildJs,
|
|
13
|
+
} from './templates/standard.js';
|
|
14
|
+
import {
|
|
15
|
+
getMcpPackageJson,
|
|
16
|
+
getMcpManifest,
|
|
17
|
+
getMcpServerEntryPoint,
|
|
18
|
+
getMcpTsconfig,
|
|
19
|
+
getMcpSkillMd,
|
|
20
|
+
getMcpBuildJs,
|
|
21
|
+
} from './templates/mcp.js';
|
|
22
|
+
|
|
23
|
+
export type ExtensionType = 'standard' | 'mcp';
|
|
24
|
+
|
|
25
|
+
interface FileEntry {
|
|
26
|
+
filePath: string;
|
|
27
|
+
content: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function getStandardFiles(name: string, extDir: string): FileEntry[] {
|
|
31
|
+
return [
|
|
32
|
+
{ filePath: path.join(extDir, 'package.json'), content: getStandardPackageJson(name) },
|
|
33
|
+
{ filePath: path.join(extDir, 'manifest.json'), content: getStandardManifest(name) },
|
|
34
|
+
{ filePath: path.join(extDir, 'src', 'index.ts'), content: getStandardEntryPoint(name) },
|
|
35
|
+
{
|
|
36
|
+
filePath: path.join(extDir, 'src', 'commands', 'hello.ts'),
|
|
37
|
+
content: getStandardCommandHandler(name),
|
|
38
|
+
},
|
|
39
|
+
{ filePath: path.join(extDir, 'tsconfig.json'), content: getStandardTsconfig() },
|
|
40
|
+
{ filePath: path.join(extDir, 'build.js'), content: getStandardBuildJs() },
|
|
41
|
+
{ filePath: path.join(extDir, 'SKILL.md'), content: getStandardSkillMd(name) },
|
|
42
|
+
];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function getMcpFiles(name: string, extDir: string): FileEntry[] {
|
|
46
|
+
return [
|
|
47
|
+
{ filePath: path.join(extDir, 'package.json'), content: getMcpPackageJson(name) },
|
|
48
|
+
{ filePath: path.join(extDir, 'manifest.json'), content: getMcpManifest(name) },
|
|
49
|
+
{ filePath: path.join(extDir, 'src', 'server.ts'), content: getMcpServerEntryPoint(name) },
|
|
50
|
+
{ filePath: path.join(extDir, 'tsconfig.json'), content: getMcpTsconfig() },
|
|
51
|
+
{ filePath: path.join(extDir, 'build.js'), content: getMcpBuildJs() },
|
|
52
|
+
{ filePath: path.join(extDir, 'SKILL.md'), content: getMcpSkillMd(name) },
|
|
53
|
+
];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function scaffoldExtension(
|
|
57
|
+
name: string,
|
|
58
|
+
type: ExtensionType,
|
|
59
|
+
outputDir: string,
|
|
60
|
+
): Promise<void> {
|
|
61
|
+
const extDir = path.join(outputDir, name);
|
|
62
|
+
|
|
63
|
+
if (await fse.pathExists(extDir)) {
|
|
64
|
+
throw new Error(`Directory "${name}" already exists`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const files = type === 'mcp' ? getMcpFiles(name, extDir) : getStandardFiles(name, extDir);
|
|
68
|
+
|
|
69
|
+
for (const file of files) {
|
|
70
|
+
await fse.ensureDir(path.dirname(file.filePath));
|
|
71
|
+
await fse.writeFile(file.filePath, file.content, 'utf-8');
|
|
72
|
+
}
|
|
73
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getMcpPackageJson,
|
|
5
|
+
getMcpManifest,
|
|
6
|
+
getMcpServerEntryPoint,
|
|
7
|
+
getMcpTsconfig,
|
|
8
|
+
getMcpBuildJs,
|
|
9
|
+
getMcpSkillMd,
|
|
10
|
+
} from './mcp.js';
|
|
11
|
+
|
|
12
|
+
describe('mcp templates', () => {
|
|
13
|
+
describe('getMcpPackageJson', () => {
|
|
14
|
+
it('returns valid JSON with correct name and mcp description', () => {
|
|
15
|
+
const result = JSON.parse(getMcpPackageJson('mcp-ext')) as Record<string, unknown>;
|
|
16
|
+
expect(result['name']).toBe('mcp-ext');
|
|
17
|
+
expect(result['version']).toBe('0.0.1');
|
|
18
|
+
expect(result['type']).toBe('module');
|
|
19
|
+
expect(result['main']).toBe('./dist/server.js');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('includes esbuild and extension-sdk dependencies', () => {
|
|
23
|
+
const result = JSON.parse(getMcpPackageJson('mcp-ext')) as Record<string, unknown>;
|
|
24
|
+
const deps = result['dependencies'] as Record<string, string>;
|
|
25
|
+
const devDeps = result['devDependencies'] as Record<string, string>;
|
|
26
|
+
expect(deps['@renre-kit/extension-sdk']).toBe('>=0.0.1');
|
|
27
|
+
expect(devDeps['esbuild']).toBe('^0.21.0');
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('uses node build.js as build script', () => {
|
|
31
|
+
const result = JSON.parse(getMcpPackageJson('mcp-ext')) as Record<string, unknown>;
|
|
32
|
+
const scripts = result['scripts'] as Record<string, string>;
|
|
33
|
+
expect(scripts['build']).toBe('node build.js');
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
describe('getMcpManifest', () => {
|
|
38
|
+
it('returns valid JSON with mcp type and transport config', () => {
|
|
39
|
+
const result = JSON.parse(getMcpManifest('mcp-ext')) as Record<string, unknown>;
|
|
40
|
+
expect(result['name']).toBe('mcp-ext');
|
|
41
|
+
expect(result['type']).toBe('mcp');
|
|
42
|
+
expect(result['engines']).toBeDefined();
|
|
43
|
+
const mcp = result['mcp'] as Record<string, unknown>;
|
|
44
|
+
expect(mcp['transport']).toBe('stdio');
|
|
45
|
+
expect(mcp['command']).toBe('node');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('getMcpServerEntryPoint', () => {
|
|
50
|
+
it('includes JSON-RPC handler with extension name', () => {
|
|
51
|
+
const result = getMcpServerEntryPoint('mcp-ext');
|
|
52
|
+
expect(result).toContain('JsonRpcRequest');
|
|
53
|
+
expect(result).toContain('handleRequest');
|
|
54
|
+
expect(result).toContain('Hello from mcp-ext!');
|
|
55
|
+
expect(result).toContain('createInterface');
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe('getMcpTsconfig', () => {
|
|
60
|
+
it('returns valid JSON', () => {
|
|
61
|
+
const result = JSON.parse(getMcpTsconfig()) as Record<string, unknown>;
|
|
62
|
+
expect(result['compilerOptions']).toBeDefined();
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe('getMcpBuildJs', () => {
|
|
67
|
+
it('imports buildExtension and archiveDist from SDK', () => {
|
|
68
|
+
const result = getMcpBuildJs();
|
|
69
|
+
expect(result).toContain(
|
|
70
|
+
"import { buildExtension, archiveDist } from '@renre-kit/extension-sdk/node'",
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('cleans dist before building', () => {
|
|
75
|
+
const result = getMcpBuildJs();
|
|
76
|
+
expect(result).toContain("rmSync('dist', { recursive: true, force: true })");
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('reads manifest for version', () => {
|
|
80
|
+
const result = getMcpBuildJs();
|
|
81
|
+
expect(result).toContain("readFileSync('manifest.json', 'utf-8')");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('includes server entry point', () => {
|
|
85
|
+
const result = getMcpBuildJs();
|
|
86
|
+
expect(result).toContain("{ in: 'src/server.ts', out: 'server' }");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('enables code splitting', () => {
|
|
90
|
+
const result = getMcpBuildJs();
|
|
91
|
+
expect(result).toContain('splitting: true');
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('archives dist with version', () => {
|
|
95
|
+
const result = getMcpBuildJs();
|
|
96
|
+
expect(result).toContain('await archiveDist');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('outputs to dist directory', () => {
|
|
100
|
+
const result = getMcpBuildJs();
|
|
101
|
+
expect(result).toContain("outdir: 'dist'");
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe('getMcpSkillMd', () => {
|
|
106
|
+
it('includes extension name and MCP description', () => {
|
|
107
|
+
const result = getMcpSkillMd('mcp-ext');
|
|
108
|
+
expect(result).toContain('# mcp-ext');
|
|
109
|
+
expect(result).toContain('MCP');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
});
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
export function getMcpPackageJson(name: string): string {
|
|
2
|
+
const pkg = {
|
|
3
|
+
name,
|
|
4
|
+
version: '0.0.1',
|
|
5
|
+
description: `A RenreKit MCP extension: ${name}`,
|
|
6
|
+
type: 'module',
|
|
7
|
+
main: './dist/server.js',
|
|
8
|
+
scripts: {
|
|
9
|
+
build: 'node build.js',
|
|
10
|
+
dev: 'tsc --watch',
|
|
11
|
+
},
|
|
12
|
+
dependencies: {
|
|
13
|
+
'@renre-kit/extension-sdk': '>=0.0.1',
|
|
14
|
+
},
|
|
15
|
+
devDependencies: {
|
|
16
|
+
esbuild: '^0.21.0',
|
|
17
|
+
typescript: '^5.7.0',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
return `${JSON.stringify(pkg, null, 2) }\n`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getMcpManifest(name: string): string {
|
|
24
|
+
const manifest = {
|
|
25
|
+
name,
|
|
26
|
+
title: name,
|
|
27
|
+
version: '0.0.1',
|
|
28
|
+
description: 'A RenreKit MCP extension',
|
|
29
|
+
type: 'mcp',
|
|
30
|
+
main: 'dist/index.js',
|
|
31
|
+
engines: {
|
|
32
|
+
'renre-kit': '>=0.0.1',
|
|
33
|
+
'extension-sdk': '>=0.0.1',
|
|
34
|
+
},
|
|
35
|
+
commands: {},
|
|
36
|
+
mcp: {
|
|
37
|
+
transport: 'stdio',
|
|
38
|
+
command: 'node',
|
|
39
|
+
args: ['dist/server.js'],
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
return `${JSON.stringify(manifest, null, 2) }\n`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getMcpServerEntryPoint(name: string): string {
|
|
46
|
+
return `import { createInterface } from 'node:readline';
|
|
47
|
+
|
|
48
|
+
interface JsonRpcRequest {
|
|
49
|
+
jsonrpc: string;
|
|
50
|
+
id: number | string;
|
|
51
|
+
method: string;
|
|
52
|
+
params?: Record<string, unknown>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface JsonRpcResponse {
|
|
56
|
+
jsonrpc: string;
|
|
57
|
+
id: number | string;
|
|
58
|
+
result?: unknown;
|
|
59
|
+
error?: { code: number; message: string };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function handleRequest(request: JsonRpcRequest): JsonRpcResponse {
|
|
63
|
+
switch (request.method) {
|
|
64
|
+
case 'hello':
|
|
65
|
+
return {
|
|
66
|
+
jsonrpc: '2.0',
|
|
67
|
+
id: request.id,
|
|
68
|
+
result: { output: 'Hello from ${name}!', exitCode: 0 },
|
|
69
|
+
};
|
|
70
|
+
default:
|
|
71
|
+
return {
|
|
72
|
+
jsonrpc: '2.0',
|
|
73
|
+
id: request.id,
|
|
74
|
+
error: { code: -32601, message: \`Method not found: \${request.method}\` },
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const rl = createInterface({ input: process.stdin });
|
|
80
|
+
|
|
81
|
+
rl.on('line', (line: string) => {
|
|
82
|
+
try {
|
|
83
|
+
const request = JSON.parse(line) as JsonRpcRequest;
|
|
84
|
+
const response = handleRequest(request);
|
|
85
|
+
process.stdout.write(JSON.stringify(response) + '\\n');
|
|
86
|
+
} catch {
|
|
87
|
+
const errorResponse: JsonRpcResponse = {
|
|
88
|
+
jsonrpc: '2.0',
|
|
89
|
+
id: 0,
|
|
90
|
+
error: { code: -32700, message: 'Parse error' },
|
|
91
|
+
};
|
|
92
|
+
process.stdout.write(JSON.stringify(errorResponse) + '\\n');
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
`;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export { getTsconfig as getMcpTsconfig } from './shared.js';
|
|
99
|
+
|
|
100
|
+
export function getMcpBuildJs(): string {
|
|
101
|
+
return `import { readFileSync, rmSync } from 'node:fs';
|
|
102
|
+
|
|
103
|
+
import { buildExtension, archiveDist } from '@renre-kit/extension-sdk/node';
|
|
104
|
+
|
|
105
|
+
rmSync('dist', { recursive: true, force: true });
|
|
106
|
+
|
|
107
|
+
const manifest = JSON.parse(readFileSync('manifest.json', 'utf-8'));
|
|
108
|
+
|
|
109
|
+
await buildExtension({
|
|
110
|
+
entryPoints: [
|
|
111
|
+
{ in: 'src/server.ts', out: 'server' },
|
|
112
|
+
],
|
|
113
|
+
outdir: 'dist',
|
|
114
|
+
external: [],
|
|
115
|
+
splitting: true,
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
await archiveDist('dist', manifest.version);
|
|
119
|
+
`;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function getMcpSkillMd(name: string): string {
|
|
123
|
+
return `---
|
|
124
|
+
name: hello
|
|
125
|
+
description: This tool should be used when the user wants to say hello or test the ${name} MCP extension
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
# ${name}
|
|
129
|
+
|
|
130
|
+
## Description
|
|
131
|
+
|
|
132
|
+
A RenreKit MCP extension that communicates via JSON-RPC over stdio.
|
|
133
|
+
|
|
134
|
+
## Commands
|
|
135
|
+
|
|
136
|
+
### hello
|
|
137
|
+
|
|
138
|
+
Say hello from the MCP extension.
|
|
139
|
+
|
|
140
|
+
**Usage:**
|
|
141
|
+
\`\`\`
|
|
142
|
+
renre ${name} hello
|
|
143
|
+
\`\`\`
|
|
144
|
+
|
|
145
|
+
## Configuration
|
|
146
|
+
|
|
147
|
+
No configuration required.
|
|
148
|
+
`;
|
|
149
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Generates a tsconfig.json for any extension type. */
|
|
2
|
+
export function getTsconfig(): string {
|
|
3
|
+
const config = {
|
|
4
|
+
compilerOptions: {
|
|
5
|
+
target: 'ES2022',
|
|
6
|
+
module: 'NodeNext',
|
|
7
|
+
moduleResolution: 'NodeNext',
|
|
8
|
+
declaration: true,
|
|
9
|
+
outDir: './dist',
|
|
10
|
+
rootDir: './src',
|
|
11
|
+
strict: true,
|
|
12
|
+
esModuleInterop: true,
|
|
13
|
+
skipLibCheck: true,
|
|
14
|
+
},
|
|
15
|
+
include: ['src'],
|
|
16
|
+
exclude: ['dist', 'node_modules'],
|
|
17
|
+
};
|
|
18
|
+
return `${JSON.stringify(config, null, 2) }\n`;
|
|
19
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
getStandardPackageJson,
|
|
5
|
+
getStandardManifest,
|
|
6
|
+
getStandardEntryPoint,
|
|
7
|
+
getStandardCommandHandler,
|
|
8
|
+
getStandardTsconfig,
|
|
9
|
+
getStandardBuildJs,
|
|
10
|
+
getStandardSkillMd,
|
|
11
|
+
} from './standard.js';
|
|
12
|
+
|
|
13
|
+
describe('standard templates', () => {
|
|
14
|
+
describe('getStandardPackageJson', () => {
|
|
15
|
+
it('returns valid JSON with correct name', () => {
|
|
16
|
+
const result = JSON.parse(getStandardPackageJson('my-ext')) as Record<string, unknown>;
|
|
17
|
+
expect(result['name']).toBe('my-ext');
|
|
18
|
+
expect(result['version']).toBe('0.0.1');
|
|
19
|
+
expect(result['type']).toBe('module');
|
|
20
|
+
expect(result['main']).toBe('./dist/index.js');
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it('includes esbuild and extension-sdk dependencies', () => {
|
|
24
|
+
const result = JSON.parse(getStandardPackageJson('my-ext')) as Record<string, unknown>;
|
|
25
|
+
const deps = result['dependencies'] as Record<string, string>;
|
|
26
|
+
const devDeps = result['devDependencies'] as Record<string, string>;
|
|
27
|
+
expect(deps['@renre-kit/extension-sdk']).toBe('>=0.0.1');
|
|
28
|
+
expect(devDeps['esbuild']).toBe('^0.21.0');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('uses node build.js as build script', () => {
|
|
32
|
+
const result = JSON.parse(getStandardPackageJson('my-ext')) as Record<string, unknown>;
|
|
33
|
+
const scripts = result['scripts'] as Record<string, string>;
|
|
34
|
+
expect(scripts['build']).toBe('node build.js');
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
describe('getStandardManifest', () => {
|
|
39
|
+
it('returns valid JSON with correct structure', () => {
|
|
40
|
+
const result = JSON.parse(getStandardManifest('my-ext')) as Record<string, unknown>;
|
|
41
|
+
expect(result['name']).toBe('my-ext');
|
|
42
|
+
expect(result['type']).toBe('standard');
|
|
43
|
+
expect(result['engines']).toBeDefined();
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('getStandardEntryPoint', () => {
|
|
48
|
+
it('includes lifecycle hooks with extension name', () => {
|
|
49
|
+
const result = getStandardEntryPoint('my-ext');
|
|
50
|
+
expect(result).toContain('export function onInit');
|
|
51
|
+
expect(result).toContain('export function onDestroy');
|
|
52
|
+
expect(result).toContain('my-ext');
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
describe('getStandardCommandHandler', () => {
|
|
57
|
+
it('generates a defineCommand export with name', () => {
|
|
58
|
+
const result = getStandardCommandHandler('my-ext');
|
|
59
|
+
expect(result).toContain('export default defineCommand');
|
|
60
|
+
expect(result).toContain('Hello from my-ext!');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('getStandardTsconfig', () => {
|
|
65
|
+
it('returns valid JSON', () => {
|
|
66
|
+
const result = JSON.parse(getStandardTsconfig()) as Record<string, unknown>;
|
|
67
|
+
expect(result['compilerOptions']).toBeDefined();
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
describe('getStandardBuildJs', () => {
|
|
72
|
+
it('imports buildExtension and archiveDist from SDK', () => {
|
|
73
|
+
const result = getStandardBuildJs();
|
|
74
|
+
expect(result).toContain(
|
|
75
|
+
"import { buildExtension, archiveDist } from '@renre-kit/extension-sdk/node'",
|
|
76
|
+
);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('cleans dist before building', () => {
|
|
80
|
+
const result = getStandardBuildJs();
|
|
81
|
+
expect(result).toContain("rmSync('dist', { recursive: true, force: true })");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('reads manifest for version', () => {
|
|
85
|
+
const result = getStandardBuildJs();
|
|
86
|
+
expect(result).toContain("readFileSync('manifest.json', 'utf-8')");
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('includes index and commands/hello entry points', () => {
|
|
90
|
+
const result = getStandardBuildJs();
|
|
91
|
+
expect(result).toContain("{ in: 'src/index.ts', out: 'index' }");
|
|
92
|
+
expect(result).toContain("{ in: 'src/commands/hello.ts', out: 'commands/hello' }");
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('enables code splitting', () => {
|
|
96
|
+
const result = getStandardBuildJs();
|
|
97
|
+
expect(result).toContain('splitting: true');
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('archives dist with version', () => {
|
|
101
|
+
const result = getStandardBuildJs();
|
|
102
|
+
expect(result).toContain('await archiveDist');
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('outputs to dist directory', () => {
|
|
106
|
+
const result = getStandardBuildJs();
|
|
107
|
+
expect(result).toContain("outdir: 'dist'");
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
describe('getStandardSkillMd', () => {
|
|
112
|
+
it('includes extension name', () => {
|
|
113
|
+
const result = getStandardSkillMd('my-ext');
|
|
114
|
+
expect(result).toContain('# my-ext');
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
export function getStandardPackageJson(name: string): string {
|
|
2
|
+
const pkg = {
|
|
3
|
+
name,
|
|
4
|
+
version: '0.0.1',
|
|
5
|
+
description: `A RenreKit extension: ${name}`,
|
|
6
|
+
type: 'module',
|
|
7
|
+
main: './dist/index.js',
|
|
8
|
+
scripts: {
|
|
9
|
+
build: 'node build.js',
|
|
10
|
+
dev: 'tsc --watch',
|
|
11
|
+
},
|
|
12
|
+
dependencies: {
|
|
13
|
+
'@renre-kit/extension-sdk': '>=0.0.1',
|
|
14
|
+
},
|
|
15
|
+
devDependencies: {
|
|
16
|
+
esbuild: '^0.21.0',
|
|
17
|
+
typescript: '^5.7.0',
|
|
18
|
+
},
|
|
19
|
+
};
|
|
20
|
+
return `${JSON.stringify(pkg, null, 2) }\n`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function getStandardManifest(name: string): string {
|
|
24
|
+
const manifest = {
|
|
25
|
+
name,
|
|
26
|
+
title: name,
|
|
27
|
+
version: '0.0.1',
|
|
28
|
+
description: 'A RenreKit extension',
|
|
29
|
+
type: 'standard',
|
|
30
|
+
main: 'dist/index.js',
|
|
31
|
+
engines: {
|
|
32
|
+
'renre-kit': '>=0.0.1',
|
|
33
|
+
'extension-sdk': '>=0.0.1',
|
|
34
|
+
},
|
|
35
|
+
commands: {
|
|
36
|
+
hello: {
|
|
37
|
+
handler: 'dist/commands/hello.js',
|
|
38
|
+
description: 'Say hello',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
return `${JSON.stringify(manifest, null, 2) }\n`;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function getStandardEntryPoint(name: string): string {
|
|
46
|
+
return `import type { HookContext } from '@renre-kit/extension-sdk/node';
|
|
47
|
+
|
|
48
|
+
export function onInit(context: HookContext): void {
|
|
49
|
+
context.sdk.deployAgentAssets();
|
|
50
|
+
console.log('${name} initialized in', context.projectDir);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function onDestroy(context: HookContext): void {
|
|
54
|
+
context.sdk.cleanupAgentAssets();
|
|
55
|
+
console.log('${name} destroyed in', context.projectDir);
|
|
56
|
+
}
|
|
57
|
+
`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function getStandardCommandHandler(name: string): string {
|
|
61
|
+
return `import { defineCommand } from '@renre-kit/extension-sdk/node';
|
|
62
|
+
|
|
63
|
+
export default defineCommand({
|
|
64
|
+
handler: () => {
|
|
65
|
+
return { output: 'Hello from ${name}!', exitCode: 0 };
|
|
66
|
+
},
|
|
67
|
+
});
|
|
68
|
+
`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export { getTsconfig as getStandardTsconfig } from './shared.js';
|
|
72
|
+
|
|
73
|
+
export function getStandardBuildJs(): string {
|
|
74
|
+
return `import { readFileSync, rmSync } from 'node:fs';
|
|
75
|
+
|
|
76
|
+
import { buildExtension, archiveDist } from '@renre-kit/extension-sdk/node';
|
|
77
|
+
|
|
78
|
+
rmSync('dist', { recursive: true, force: true });
|
|
79
|
+
|
|
80
|
+
const manifest = JSON.parse(readFileSync('manifest.json', 'utf-8'));
|
|
81
|
+
|
|
82
|
+
await buildExtension({
|
|
83
|
+
entryPoints: [
|
|
84
|
+
{ in: 'src/index.ts', out: 'index' },
|
|
85
|
+
{ in: 'src/commands/hello.ts', out: 'commands/hello' },
|
|
86
|
+
],
|
|
87
|
+
outdir: 'dist',
|
|
88
|
+
external: [],
|
|
89
|
+
splitting: true,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
await archiveDist('dist', manifest.version);
|
|
93
|
+
`;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function getStandardSkillMd(name: string): string {
|
|
97
|
+
return `---
|
|
98
|
+
name: hello
|
|
99
|
+
description: This tool should be used when the user wants to say hello or get a greeting from the ${name} extension
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
# ${name}
|
|
103
|
+
|
|
104
|
+
## Description
|
|
105
|
+
|
|
106
|
+
A RenreKit extension that provides additional functionality.
|
|
107
|
+
|
|
108
|
+
## Commands
|
|
109
|
+
|
|
110
|
+
### hello
|
|
111
|
+
|
|
112
|
+
Say hello from the extension.
|
|
113
|
+
|
|
114
|
+
**Usage:**
|
|
115
|
+
\`\`\`
|
|
116
|
+
renre ${name} hello
|
|
117
|
+
\`\`\`
|
|
118
|
+
|
|
119
|
+
## Configuration
|
|
120
|
+
|
|
121
|
+
No configuration required.
|
|
122
|
+
`;
|
|
123
|
+
}
|
package/tsconfig.json
ADDED
package/tsup.config.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { defineConfig } from 'tsup';
|
|
2
|
+
|
|
3
|
+
export default defineConfig({
|
|
4
|
+
entry: ['src/index.ts'],
|
|
5
|
+
format: ['esm'],
|
|
6
|
+
dts: true,
|
|
7
|
+
clean: true,
|
|
8
|
+
sourcemap: true,
|
|
9
|
+
target: 'es2022',
|
|
10
|
+
outDir: 'dist',
|
|
11
|
+
banner: {
|
|
12
|
+
js: '#!/usr/bin/env node',
|
|
13
|
+
},
|
|
14
|
+
});
|
package/vitest.config.ts
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { defineConfig } from 'vitest/config';
|
|
2
|
+
import { createCoverageConfig } from '../../vitest.shared.js';
|
|
3
|
+
|
|
4
|
+
export default defineConfig({
|
|
5
|
+
test: {
|
|
6
|
+
globals: true,
|
|
7
|
+
passWithNoTests: true,
|
|
8
|
+
include: ['src/**/*.test.ts'],
|
|
9
|
+
...createCoverageConfig(),
|
|
10
|
+
},
|
|
11
|
+
});
|