@migor/cli 0.7.2 → 0.8.1

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/templates.js CHANGED
@@ -1,4 +1,23 @@
1
- /** 命名工具与所有文件模板(零依赖) */
1
+ /** 命名工具与脚手架模板加载(零依赖)
2
+ *
3
+ * 模板是 `packages/cli/templates/` 下的**真文件**,不再是本文件里的字符串 ——
4
+ * 字符串不过编译器:模板代码对仓库自己的 typecheck/lint 不可见,缺陷要等用户
5
+ * npm install 后才暴露(discover 路径「生产必崩」就是这么漏出去的)。真文件化后
6
+ * 模板被仓库自己的工具链全程照看:`tsconfig.templates.json` 把它纳入 tsc
7
+ * ('@migor/agentia' 经 paths 映射到框架 src),Biome 照常收 lint/format。
8
+ *
9
+ * 占位符纪律:token 只许出现在**字符串 / 注释 / 标识符**位置
10
+ * (`__PROJECT_NAME__` / `__NAME__` / `__CLASS_NAME__` / `__METHOD_NAME__`),
11
+ * 不许出现在类型位置或语法关键位置 —— 这样模板文件自身就是合法的 TS/JSON,
12
+ * 能过 tsc 与 Biome。渲染 = 读文件 + replaceAll。
13
+ *
14
+ * 点文件在包里用**无点文件名**(`gitignore` / `env` / `env.example`,写出时才补点):
15
+ * `.env` 会被仓库根 .gitignore 吞掉(根本进不了版本库),`.gitignore` 会被
16
+ * npm pack 静默剥掉(CRA 当年就是这么踩的)—— 模板文件必须在 tarball 里活着。
17
+ */
18
+ import { existsSync, readFileSync } from 'node:fs';
19
+ import { dirname, join } from 'node:path';
20
+ import { fileURLToPath } from 'node:url';
2
21
  const NAME_RE = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
3
22
  export function isValidName(name) {
4
23
  return NAME_RE.test(name);
@@ -33,308 +52,111 @@ export const CAPABILITY_DIRS = {
33
52
  export const CAPABILITY_DIR_LIST = CAPABILITY_TYPES.map((t) => CAPABILITY_DIRS[t]);
34
53
  /** 显式注册表位置(`agentia g` / `agentia add` 维护,`agentia doctor` 校验) */
35
54
  export const REGISTRY_PATH = 'src/registry.ts';
36
- // ---------- 注册表模板 ----------
55
+ // ---------- 注册表标记(registry.ts codemod 的锚点,模板文件里就是这几行字面量) ----------
37
56
  export const IMPORTS_END_MARKER = '// @agentia:imports-end';
38
57
  export const ENTRIES_END_MARKER = '// @agentia:entries-end';
39
- /** 空注册表模板(无任何能力条目) */
40
- export function emptyRegistryTemplate() {
41
- return `// Agentia 能力注册表 —— 由 agentia CLI 维护(agentia g 自动更新,也可手工编辑)
42
- // @agentia:imports
43
- ${IMPORTS_END_MARKER}
44
- import type { Provider } from '@migor/agentia';
45
-
46
- /** 显式装配路线:createApp({ providers, system: ... })(与 discover 目录扫描二选一或混用) */
47
- export const providers: Provider[] = [
48
- // @agentia:entries
49
- ${ENTRIES_END_MARKER}
50
- ];
51
- `;
58
+ // ---------- 模板文件加载 ----------
59
+ /**
60
+ * 定位模板文件:发布物里在 dist/templates/(构建时由 scripts/copy-assets.mjs 拷入),
61
+ * 源码直跑(tsx src/…)时在包根 templates/。找不到就**明确失败**,不生成缺件项目。
62
+ */
63
+ function templatePath(rel) {
64
+ const here = dirname(fileURLToPath(import.meta.url));
65
+ const candidates = [
66
+ join(here, 'templates', rel), // 发布物:dist/templates/
67
+ join(here, '..', 'templates', rel), // 源码直跑:src/ → 包根 templates/
68
+ ];
69
+ for (const p of candidates) {
70
+ if (existsSync(p))
71
+ return p;
72
+ }
73
+ throw new Error(`找不到脚手架模板 ${rel}。查找过:\n ${candidates.join('\n ')}`);
74
+ }
75
+ /** 读模板文件并替换占位符(token 只许在字符串/注释/标识符位置,见文件头说明) */
76
+ function renderTemplate(rel, vars = {}) {
77
+ let text = readFileSync(templatePath(rel), 'utf8');
78
+ for (const [token, value] of Object.entries(vars))
79
+ text = text.replaceAll(token, value);
80
+ return text;
81
+ }
82
+ /** 能力模板的三类占位符:名字本身(注释/描述串)+ 类名 + 方法名 */
83
+ function capabilityVars(name) {
84
+ return {
85
+ __NAME__: name,
86
+ __CLASS_NAME__: kebabToPascal(name),
87
+ __METHOD_NAME__: kebabToSnake(name),
88
+ };
52
89
  }
53
90
  // ---------- create 命令模板 ----------
91
+ /**
92
+ * 脚手架 package.json(templates/package.json)。两处口径钉在这里,改动前先读:
93
+ * - `scripts.dev` 走 CLI 的 dev(tsx watch + 本地 inspector 面板):与文档/其它命令同一条路,
94
+ * 而不是「npm run dev 少一个面板、CLI dev 多一个面板」两种 dev;`npm run dev -- "你的问题"`
95
+ * 的参数由 CLI 原样透传给脚本。
96
+ * - `@migor/cli` 装进 devDependencies(而不是让用户每次 npx 去 registry 拉):工程内
97
+ * `npx agentia …` 走本地 bin —— 离线可用,且版本被 pin 住与框架同批。
98
+ * 这两条 pin(框架 + CLI)是**版本发布面**(scripts/release-surface.mjs),格式不能动。
99
+ */
54
100
  export function projectPackageJson(name) {
55
- return `${JSON.stringify({
56
- name,
57
- private: true,
58
- type: 'module',
59
- scripts: {
60
- dev: 'tsx src/main.ts',
61
- build: 'tsc -p tsconfig.json && node scripts/copy-assets.mjs',
62
- start: 'node dist/main.js',
63
- typecheck: 'tsc --noEmit -p tsconfig.json',
64
- },
65
- dependencies: { '@migor/agentia': '^0.7.2' },
66
- devDependencies: {
67
- tsx: '^4.19.0',
68
- typescript: '^7.0.2',
69
- '@types/node': '^22.0.0',
70
- },
71
- }, null, 2)}\n`;
101
+ return renderTemplate('package.json', { __PROJECT_NAME__: name });
72
102
  }
103
+ /**
104
+ * 脚手架 tsconfig(templates/tsconfig.json)。两处口径:
105
+ * - `rootDir: src` + `outDir: dist`:编译产物落 dist/(npm run build → npm start 跑
106
+ * dist/main.js);.md 文本资产由 scripts/copy-assets.mjs 跟随同相对路径拷过去
107
+ * (asset(import.meta.url, './x.md') 按**文件位置**解析,.md 必须跟着 .js 走)。
108
+ * - `types: ['node']`:框架零运行时依赖(不再有厂商 SDK 经传递链把 @types/node 带进
109
+ * 编译程序),node 全局类型必须显式声明。
110
+ */
73
111
  export function projectTsconfig() {
74
- return `${JSON.stringify({
75
- compilerOptions: {
76
- target: 'ES2022',
77
- module: 'NodeNext',
78
- moduleResolution: 'NodeNext',
79
- lib: ['ES2022'],
80
- strict: true,
81
- // 编译产物落 dist/(npm run build → npm start 跑 dist/main.js);
82
- // .md 文本资产由 scripts/copy-assets.mjs 跟随同相对路径拷过去
83
- // (asset(import.meta.url, './x.md') 按**文件位置**解析,.md 必须跟着 .js 走)
84
- rootDir: 'src',
85
- outDir: 'dist',
86
- // 框架零运行时依赖(不再有厂商 SDK 经传递链把 @types/node 带进编译程序),
87
- // node 全局类型必须显式声明
88
- types: ['node'],
89
- esModuleInterop: true,
90
- skipLibCheck: true,
91
- },
92
- include: ['src'],
93
- }, null, 2)}\n`;
112
+ return renderTemplate('tsconfig.json');
94
113
  }
114
+ /**
115
+ * 入口 main.ts(templates/src/main.ts)。`CAPABILITY_DIRS` 数组在模板里是字面量
116
+ * `['tools', 'skills', 'prompts', 'subagents']`,必须与上面 CAPABILITY_DIR_LIST 去掉
117
+ * `src/` 前缀后保持一致(顺序即装配顺序)—— templates.test.mjs 钉着这条对应关系。
118
+ */
95
119
  export function mainTs(name) {
96
- return `import { createApp, loadEnvFile, SystemPrompt } from '@migor/agentia';
97
-
98
- // 读同目录的 .env(key 写文件里即可,不必每次 export)。框架**不自动**读 .env ——
99
- // 读哪个文件、什么时候读由这里决定;已存在的真实环境变量优先,不会被文件覆盖。
100
- // 想换路径/顺序:loadEnvFile({ path: '.env.local' }) 或直接删掉这一行改用自己的加载器。
101
- loadEnvFile();
102
-
103
- const app = await createApp({
104
- name: '${name}',
105
- discover: [${CAPABILITY_DIR_LIST.map((p) => `'${p}'`).join(', ')}], // 四分类目录,顺序即装配顺序
106
- system: new SystemPrompt().add('role', '你是 ${name} 的主 agent,按任务自主调度菜单里的能力。', true),
107
- });
108
-
109
- const { result } = await app.run(
110
- [{ role: 'user', content: process.argv[2] ?? '介绍一下你自己' }],
111
- );
112
-
113
- // 注意:run 失败**不会抛**(硬失败被记进 result.error 与 trace 后正常返回)—— 不显式检查就会
114
- // 「打印一行空白 + 退出 0」,让首次运行(比如忘了配 ANTHROPIC_API_KEY)看起来像成功。
115
- if (result.error) {
116
- console.error(\`run 失败(stopReason=\${result.stopReason}):\${result.error.message}\`);
117
- console.error('提示:模型调用读 ANTHROPIC_API_KEY —— 填进 .env(首行 loadEnvFile() 会读)或 export 均可;');
118
- console.error(' 换端点 / 注入自定义 client 见项目内 AGENTS.md。');
119
- process.exitCode = 1;
120
- }
121
- if (result.finalText) console.log(result.finalText);
122
- `;
120
+ return renderTemplate('src/main.ts', { __PROJECT_NAME__: name });
123
121
  }
124
122
  export function projectReadme(name) {
125
- return `# ${name}
126
-
127
- 基于 [Agentia](https://github.com/retrychx/agentia) 框架的 agent 应用。
128
-
129
- ## 目录约定
130
-
131
- 四分类目录,一能力一文件夹,每个能力是一个 default export 的类,用装饰器声明:
132
-
133
- - \`src/tools/<name>/\` —— \`@Tool\` 工具:主 agent 可调用(input → value)
134
- - \`src/skills/<name>/\` —— \`@Skill\` 技能:方法体内通过 \`ctx.llm()\` 调 LLM
135
- - \`src/subagents/<name>/\` —— \`@SubAgent\` 子代理:按 system 角色设定独立跑一轮
136
- - \`src/prompts/<name>/\` —— \`@Prompt\` 文本资产:.md 文件,按需拉取进上下文
137
-
138
- 目录名就是类型,不用记别名。
139
-
140
- ## 两条装配路线
141
-
142
- 1. **目录扫描**:\`createApp({ discover: [...] })\` 启动期按给定顺序扫各目录下的 \`<name>/index.ts\`,default export 为类时以文件夹名为 DI token 注册(见 \`src/main.ts\`)。
143
- 2. **显式装配**:\`createApp({ providers, system })\`,providers 来自 \`src/registry.ts\` 注册表(由 \`agentia g\` 自动维护,也可手工编辑)。
144
-
145
- 两者二选一或混用。
146
-
147
- ## 生成能力
148
-
149
- \`\`\`bash
150
- agentia g tool my-tool # → src/tools/my-tool/
151
- agentia g skill my-skill # → src/skills/my-skill/
152
- agentia g prompt my-prompt # → src/prompts/my-prompt/(含 asset.md)
153
- agentia g subagent my-agent # → src/subagents/my-agent/(含 system.md)
154
- \`\`\`
155
-
156
- 生成的能力自动登记到 \`src/registry.ts\`。
157
-
158
- ## 运行
159
-
160
- 需要 Anthropic API key —— 填进脚手架已生成的 \`.env\` 即可(本文件在 \`.gitignore\` 里):
161
-
162
- \`\`\`bash
163
- # .env
164
- ANTHROPIC_API_KEY=sk-ant-...
165
- \`\`\`
166
-
167
- \`\`\`bash
168
- npm run dev -- "你的问题"
169
- \`\`\`
170
-
171
- ## 构建与生产运行
172
-
173
- \`\`\`bash
174
- npm run build # tsc → dist/ + .md 文本资产跟随拷贝(asset() 按文件位置解析,必须跟着 .js 走)
175
- npm start -- "你的问题" # 跑编译产物 dist/main.js(部署/Docker 用这条)
176
- \`\`\`
177
-
178
- 也可以用环境变量(适合 CI / 容器)——**真实环境变量优先,不会被 \`.env\` 覆盖**:
179
-
180
- \`\`\`bash
181
- export ANTHROPIC_API_KEY=sk-ant-...
182
- npm run dev -- "你的问题"
183
- \`\`\`
184
-
185
- \`.env\` 由 \`src/main.ts\` 首行的 \`loadEnvFile()\` 读取。框架**不会自动读**它 ——
186
- 读哪个文件、什么时候读由你的启动代码决定(这样「换目录跑」不会悄悄改变行为)。
187
- `;
123
+ return renderTemplate('README.md', { __PROJECT_NAME__: name });
188
124
  }
189
125
  export function projectGitignore() {
190
- return `node_modules
191
- dist
192
-
193
- # 本地环境变量(可能含 key)—— 绝不提交
194
- .env
195
- .env.local
196
- `;
126
+ return renderTemplate('gitignore');
197
127
  }
198
128
  /** 脚手架生成的 .env:填上就能跑,已进 .gitignore */
199
129
  export function projectDotEnv() {
200
- return `# 模型 API key —— 填上后 npm run dev / npm start 直接可用
201
- # 本文件已被 .gitignore 忽略:不要提交,也不要把 key 写进 README / AGENTS.md
202
- ANTHROPIC_API_KEY=
203
- `;
130
+ return renderTemplate('env');
204
131
  }
205
132
  /** 随脚手架提交的 .env.example:进版本库的变量清单,值一律留空 */
206
133
  export function projectDotEnvExample() {
207
- return `# 复制成 .env 再填值(.env 已 gitignore,不要提交)
208
- ANTHROPIC_API_KEY=
209
-
210
- # 可选
211
- # ANTHROPIC_BASE_URL=https://api.anthropic.com
212
- # AGENTIA_MODEL=claude-opus-5
213
- `;
134
+ return renderTemplate('env.example');
135
+ }
136
+ /** 空注册表模板(无任何能力条目;templates/src/registry.ts) */
137
+ export function emptyRegistryTemplate() {
138
+ return renderTemplate('src/registry.ts');
214
139
  }
215
140
  /** 脚手架的 scripts/copy-assets.mjs:把 src 下的 .md 文本资产拷进 dist/(同相对路径) */
216
141
  export function copyAssetsMjs() {
217
- return `// 把能力文件夹里的 .md 文本资产拷进 dist/(与编译产物同相对路径)。
218
- // asset(import.meta.url, './x.md') 按**文件位置**解析,所以 .md 必须跟着 .js 走。
219
- import { cpSync, readdirSync } from 'node:fs';
220
- import { join } from 'node:path';
221
- import { fileURLToPath } from 'node:url';
222
-
223
- const here = fileURLToPath(new URL('.', import.meta.url));
224
- const srcDir = join(here, '..', 'src');
225
- const outDir = join(here, '..', 'dist');
226
-
227
- function walk(dir) {
228
- const out = [];
229
- for (const e of readdirSync(dir, { withFileTypes: true })) {
230
- const p = join(dir, e.name);
231
- if (e.isDirectory()) out.push(...walk(p));
232
- else if (e.name.endsWith('.md')) out.push(p);
233
- }
234
- return out;
235
- }
236
-
237
- let n = 0;
238
- for (const f of walk(srcDir)) {
239
- cpSync(f, join(outDir, f.slice(srcDir.length + 1)));
240
- n++;
241
- }
242
- console.log(\`[copy-assets] \${n} 个 .md 资产 → dist/\`);
243
- `;
142
+ return renderTemplate('scripts/copy-assets.mjs');
244
143
  }
245
144
  // ---------- g 命令能力模板 ----------
246
145
  export function toolIndexTs(name) {
247
- const cls = kebabToPascal(name);
248
- const method = kebabToSnake(name);
249
- return `import { Tool } from '@migor/agentia';
250
-
251
- /** ${name} 工具能力 */
252
- export default class ${cls} {
253
- @Tool({
254
- description: '示例工具:回显输入',
255
- schema: {
256
- type: 'object',
257
- properties: { text: { type: 'string' } },
258
- required: ['text'],
259
- additionalProperties: false,
260
- },
261
- strict: true,
262
- })
263
- ${method}(input: { text: string }): string {
264
- return \`echo: \${input.text}\`;
265
- }
266
- }
267
- `;
146
+ return renderTemplate('capabilities/tool/index.ts', capabilityVars(name));
268
147
  }
269
148
  export function promptIndexTs(name) {
270
- const cls = kebabToPascal(name);
271
- const method = kebabToSnake(name);
272
- return `import { Prompt, asset } from '@migor/agentia';
273
-
274
- /** ${name} 文本资产能力 */
275
- export default class ${cls} {
276
- @Prompt({ description: '${name} 文本资产(描述何时该拉取)' })
277
- ${method}(): string {
278
- return asset(import.meta.url, './asset.md');
279
- }
280
- }
281
- `;
149
+ return renderTemplate('capabilities/prompt/index.ts', capabilityVars(name));
282
150
  }
283
151
  export function promptAssetMd(name) {
284
- return `# ${name}
285
-
286
- 在此编写提示文本资产(按需被主 agent 拉取进上下文)。
287
- `;
152
+ return renderTemplate('capabilities/prompt/asset.md', { __NAME__: name });
288
153
  }
289
154
  export function subagentIndexTs(name) {
290
- const cls = kebabToPascal(name);
291
- const method = kebabToSnake(name);
292
- return `import { SubAgent, asset } from '@migor/agentia';
293
-
294
- /** ${name} 子代理能力 */
295
- export default class ${cls} {
296
- @SubAgent({
297
- description: '示例子代理:按 system.md 的角色设定独立处理任务',
298
- schema: {
299
- type: 'object',
300
- properties: { task: { type: 'string' } },
301
- required: ['task'],
302
- additionalProperties: false,
303
- },
304
- system: asset(import.meta.url, './system.md'),
305
- })
306
- // 方法体不会执行:@SubAgent 只读取方法名与装饰器元数据,调用时由框架按 system 另起 agent 执行
307
- ${method}(_input: { task: string }): void {}
308
- }
309
- `;
155
+ return renderTemplate('capabilities/subagent/index.ts', capabilityVars(name));
310
156
  }
311
157
  export function subagentSystemMd(name) {
312
- return `# ${name}
313
-
314
- 在此编写子代理的角色描述(作为其子 agent 的 system prompt)。
315
- `;
158
+ return renderTemplate('capabilities/subagent/system.md', { __NAME__: name });
316
159
  }
317
160
  export function skillIndexTs(name) {
318
- const cls = kebabToPascal(name);
319
- const method = kebabToSnake(name);
320
- return `import { Skill } from '@migor/agentia';
321
- import type { SkillContext } from '@migor/agentia';
322
-
323
- /** ${name} 技能能力 */
324
- export default class ${cls} {
325
- @Skill({
326
- description: '示例技能:就给定主题调用 LLM 产出要点',
327
- schema: {
328
- type: 'object',
329
- properties: { topic: { type: 'string' } },
330
- required: ['topic'],
331
- additionalProperties: false,
332
- },
333
- })
334
- async ${method}(input: { topic: string }, ctx: SkillContext): Promise<string> {
335
- const r = await ctx.llm({ prompt: \`就「\${input.topic}」给出三个要点\` });
336
- return r.text;
337
- }
338
- }
339
- `;
161
+ return renderTemplate('capabilities/skill/index.ts', capabilityVars(name));
340
162
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@migor/cli",
3
- "version": "0.7.2",
3
+ "version": "0.8.1",
4
4
  "description": "Agentia 框架命令行工具:create 脚手架 + g 能力生成 + dev 本地可观测面板(trace inspector)",
5
5
  "type": "module",
6
6
  "bin": {
@@ -29,7 +29,7 @@
29
29
  "access": "public"
30
30
  },
31
31
  "scripts": {
32
- "build": "tsc -p tsconfig.json && node scripts/copy-assets.mjs",
32
+ "build": "tsc -p tsconfig.json && tsc --noEmit -p tsconfig.templates.json && node scripts/copy-assets.mjs",
33
33
  "test": "node --test \"test/*.test.mjs\"",
34
34
  "prepublishOnly": "node ../../scripts/check-release.mjs && npm run build"
35
35
  },