@migor/cli 0.0.2 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/add.js ADDED
@@ -0,0 +1,60 @@
1
+ /** add 命令:npm install 第三方单元包并登记进 units.ts 注册表 */
2
+ import { spawnSync } from 'node:child_process';
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { join, resolve } from 'node:path';
5
+ import { registerPackage } from './registry.js';
6
+ /** 从 add 参数解析真实包名:支持 name[@version]、@scope/name[@version]、本地路径(file:) */
7
+ function resolvePackageName(arg) {
8
+ if (arg.startsWith('.') || arg.startsWith('/')) {
9
+ const pkgFile = join(resolve(arg), 'package.json');
10
+ if (!existsSync(pkgFile)) {
11
+ throw new Error(`本地包 ${arg} 缺少 package.json`);
12
+ }
13
+ const name = JSON.parse(readFileSync(pkgFile, 'utf8')).name;
14
+ if (typeof name !== 'string' || name.length === 0) {
15
+ throw new Error(`本地包 ${arg} 的 package.json 缺少 name 字段`);
16
+ }
17
+ return name;
18
+ }
19
+ // 去掉版本后缀:@scope/name@1.0.0 → @scope/name;name@1.0.0 → name
20
+ const at = arg.startsWith('@') ? arg.indexOf('@', 1) : arg.indexOf('@');
21
+ return at > 0 ? arg.slice(0, at) : arg;
22
+ }
23
+ /** 包名 → DI token:去 scope,转 kebab-case(fooBar → foo-bar,foo_bar → foo-bar) */
24
+ export function packageNameToToken(pkgName) {
25
+ const bare = pkgName.startsWith('@') ? pkgName.slice(pkgName.indexOf('/') + 1) : pkgName;
26
+ return bare
27
+ .replace(/([a-z0-9])([A-Z])/g, '$1-$2')
28
+ .replace(/[^a-zA-Z0-9]+/g, '-')
29
+ .toLowerCase()
30
+ .replace(/^-+|-+$/g, '');
31
+ }
32
+ export function addPackage(arg) {
33
+ const cwd = process.cwd();
34
+ const r = spawnSync('npm', ['install', arg], { cwd, stdio: 'inherit' });
35
+ if (r.error || r.status !== 0) {
36
+ console.error(`错误:npm install ${arg} 失败,units.ts 未改动`);
37
+ process.exitCode = 1;
38
+ return 1;
39
+ }
40
+ let pkgName;
41
+ try {
42
+ pkgName = resolvePackageName(arg);
43
+ }
44
+ catch (err) {
45
+ console.error(`错误:${err.message}`);
46
+ process.exitCode = 1;
47
+ return 1;
48
+ }
49
+ const token = packageNameToToken(pkgName);
50
+ const result = registerPackage(cwd, token, pkgName);
51
+ if (result === 'registered') {
52
+ console.log(`已登记到 units.ts:import ${pkgName} → { provide: '${token}' }`);
53
+ }
54
+ else {
55
+ console.log(`units.ts 中「${token}」已注册,跳过登记`);
56
+ }
57
+ console.log('约定提示:第三方单元包应 default export 一个 provider 类(或 Provider 对象);' +
58
+ '若该包不符合约定,请手工调整 units.ts 中的 import 与条目');
59
+ return 0;
60
+ }
package/dist/cli.js CHANGED
@@ -4,12 +4,18 @@ import { createProject } from './create.js';
4
4
  import { generateUnit } from './generate.js';
5
5
  import { isUnitType, isValidName, UNIT_TYPES } from './templates.js';
6
6
  import { RegistryError } from './registry.js';
7
+ import { devServer } from './dev.js';
8
+ import { doctor } from './doctor.js';
9
+ import { addPackage } from './add.js';
7
10
  const USAGE = `agentia —— Agentia 框架命令行工具
8
11
 
9
12
  用法:
10
13
  agentia create <name> [--dir <parent>] 创建项目脚手架(目录 <parent|当前目录>/<name>/)
11
14
  agentia g <type> <name> 在当前目录生成单元(别名:generate)
12
15
  type: ${UNIT_TYPES.join(' | ')}
16
+ agentia dev 启动开发模式(tsx watch src/main.ts,热重载)
17
+ agentia doctor 装配体检(未登记/悬空单板/命名规范/重复条目)
18
+ agentia add <pkg> 安装第三方单元包并登记到 units.ts
13
19
  agentia --help 显示本帮助
14
20
 
15
21
  name 规则:小写字母开头的小写 kebab-case(如 hello、doc-reviewer)
@@ -71,6 +77,30 @@ function main(argv) {
71
77
  throw err;
72
78
  }
73
79
  }
80
+ if (command === 'dev') {
81
+ if (rest.length > 0)
82
+ return fail(`未知参数:${rest[0]}`);
83
+ return devServer();
84
+ }
85
+ if (command === 'doctor') {
86
+ if (rest.length > 0)
87
+ return fail(`未知参数:${rest[0]}`);
88
+ return doctor();
89
+ }
90
+ if (command === 'add') {
91
+ const [pkg, ...extra] = rest;
92
+ if (pkg === undefined || extra.length > 0) {
93
+ return fail('用法:agentia add <pkg>(npm 包名或本地包路径)');
94
+ }
95
+ try {
96
+ return addPackage(pkg);
97
+ }
98
+ catch (err) {
99
+ if (err instanceof RegistryError)
100
+ return fail(err.message);
101
+ throw err;
102
+ }
103
+ }
74
104
  console.error(`错误:未知命令「${command}」\n`);
75
105
  console.error(USAGE);
76
106
  process.exitCode = 1;
package/dist/create.js CHANGED
@@ -1,11 +1,11 @@
1
1
  /** create 命令:生成 Agentia 项目脚手架 */
2
2
  import { existsSync, mkdirSync, readdirSync, writeFileSync } from 'node:fs';
3
- import { join, resolve } from 'node:path';
3
+ import { dirname, join, resolve } from 'node:path';
4
4
  import { mainTs, projectGitignore, projectPackageJson, projectReadme, projectTsconfig, toolIndexTs, emptyRegistryTemplate, } from './templates.js';
5
5
  import { registerUnit } from './registry.js';
6
6
  function write(dir, rel, content) {
7
7
  const file = join(dir, rel);
8
- mkdirSync(join(file, '..'), { recursive: true });
8
+ mkdirSync(dirname(file), { recursive: true });
9
9
  writeFileSync(file, content, 'utf8');
10
10
  }
11
11
  export function createProject(name, parent) {
package/dist/dev.js ADDED
@@ -0,0 +1,30 @@
1
+ /** dev 命令:在项目根目录启动 tsx watch src/main.ts(热重载开发模式) */
2
+ import { spawn } from 'node:child_process';
3
+ import { existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+ export function devServer() {
6
+ const cwd = process.cwd();
7
+ const entry = join(cwd, 'src', 'main.ts');
8
+ if (!existsSync(entry)) {
9
+ console.error('错误:当前目录下未找到 src/main.ts,请先用 agentia create <name> 创建项目(或 cd 到项目根目录)');
10
+ process.exitCode = 1;
11
+ return 1;
12
+ }
13
+ const child = spawn('npx', ['tsx', 'watch', 'src/main.ts'], { cwd, stdio: 'inherit' });
14
+ // 把终端信号转发给子进程(tsx watch 需要收到 SIGINT 才能干净退出)
15
+ const forward = (sig) => () => {
16
+ if (!child.killed)
17
+ child.kill(sig);
18
+ };
19
+ process.on('SIGINT', forward('SIGINT'));
20
+ process.on('SIGTERM', forward('SIGTERM'));
21
+ child.on('error', (err) => {
22
+ console.error(`错误:启动 dev 失败:${err.message}`);
23
+ process.exitCode = 1;
24
+ });
25
+ child.on('exit', (code, sig) => {
26
+ // 子进程退出后同步退出码;被信号杀死视为正常(用户 Ctrl+C)
27
+ process.exitCode = sig ? 0 : (code ?? 0);
28
+ });
29
+ return 0;
30
+ }
package/dist/doctor.js ADDED
@@ -0,0 +1,87 @@
1
+ /** doctor 命令:静态体检装配健康度(只读文件系统 + units.ts 文本,不 import 用户代码) */
2
+ import { existsSync, readdirSync, readFileSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { isValidName } from './templates.js';
5
+ /** 解析 units.ts:imports(标识符 → 来源)+ entries(provide token → useClass 标识符) */
6
+ function parseRegistry(content) {
7
+ const imports = new Map();
8
+ for (const m of content.matchAll(/^import\s+(\w+)\s+from\s+'([^']+)'/gm)) {
9
+ imports.set(m[1], m[2]);
10
+ }
11
+ const entries = [];
12
+ for (const m of content.matchAll(/\{\s*provide:\s*'([^']+)'\s*,\s*useClass:\s*(\w+)/g)) {
13
+ entries.push({ token: m[1], ident: m[2] });
14
+ }
15
+ return { imports, entries };
16
+ }
17
+ export function doctor() {
18
+ const cwd = process.cwd();
19
+ const unitsDir = join(cwd, 'units');
20
+ const registryFile = join(cwd, 'units.ts');
21
+ const folders = existsSync(unitsDir)
22
+ ? readdirSync(unitsDir, { withFileTypes: true })
23
+ .filter((d) => d.isDirectory())
24
+ .map((d) => d.name)
25
+ : [];
26
+ const content = existsSync(registryFile) ? readFileSync(registryFile, 'utf8') : '';
27
+ const { imports, entries } = parseRegistry(content);
28
+ const warnings = [];
29
+ const errors = [];
30
+ const oks = [];
31
+ // 重复 provide 条目 → 错误
32
+ const seen = new Map();
33
+ for (const e of entries)
34
+ seen.set(e.token, (seen.get(e.token) ?? 0) + 1);
35
+ for (const [token, n] of seen) {
36
+ if (n > 1)
37
+ errors.push(`units.ts 存在 ${n} 条重复 provide: '${token}',请保留一条`);
38
+ }
39
+ const folderSet = new Set(folders);
40
+ const registeredTokens = new Set(entries.map((e) => e.token));
41
+ // 文件夹视角:命名规范 / 入口文件 / 是否登记
42
+ for (const name of folders) {
43
+ let broken = false;
44
+ if (!isValidName(name)) {
45
+ errors.push(`单元名「${name}」不符合 kebab-case 规范(units/${name}/)`);
46
+ broken = true;
47
+ }
48
+ if (!existsSync(join(unitsDir, name, 'index.ts'))) {
49
+ errors.push(`单元 units/${name}/ 缺少入口文件 index.ts`);
50
+ broken = true;
51
+ }
52
+ if (!registeredTokens.has(name)) {
53
+ warnings.push(`units/${name}/ 存在但 units.ts 未登记(运行 agentia g <type> ${name} 或手工登记)`);
54
+ }
55
+ else if (!broken) {
56
+ oks.push(`units/${name}:已登记且入口齐全`);
57
+ }
58
+ }
59
+ // 注册表视角:悬空单板(import 指向 ./units/ 但文件夹已不存在;add 引入的包无本地文件夹属正常)
60
+ for (const e of entries) {
61
+ if (!isValidName(e.token)) {
62
+ errors.push(`provide token「${e.token}」不符合 kebab-case 规范`);
63
+ }
64
+ const source = imports.get(e.ident);
65
+ if (source === undefined)
66
+ continue; // 无法定位 import(可能手工编写),跳过
67
+ if (source.startsWith('./units/')) {
68
+ const folder = source.split('/')[2];
69
+ if (folder !== undefined && !folderSet.has(folder)) {
70
+ warnings.push(`units.ts 条目 { provide: '${e.token}' } 指向 ${source},但 units/${folder}/ 不存在(悬空单板,请删除条目或恢复目录)`);
71
+ }
72
+ }
73
+ }
74
+ console.log('agentia doctor —— 装配体检\n');
75
+ for (const msg of oks)
76
+ console.log(` ✓ ${msg}`);
77
+ for (const msg of warnings)
78
+ console.log(` 警告:${msg}`);
79
+ for (const msg of errors)
80
+ console.log(` 错误:${msg}`);
81
+ console.log(`\n体检结果:${errors.length} 错误,${warnings.length} 警告`);
82
+ if (errors.length > 0) {
83
+ process.exitCode = 1;
84
+ return 1;
85
+ }
86
+ return 0;
87
+ }
package/dist/registry.js CHANGED
@@ -12,6 +12,18 @@ export function ensureRegistry(dir) {
12
12
  }
13
13
  return file;
14
14
  }
15
+ /** 在标记区插入 import 行与条目行;标记行缺失时报错 */
16
+ function insertAtMarkers(content, importLine, entryLine) {
17
+ const lines = content.split('\n');
18
+ const importsEnd = lines.findIndex((l) => l.trim() === IMPORTS_END_MARKER);
19
+ const entriesEnd = lines.findIndex((l) => l.trim() === ENTRIES_END_MARKER);
20
+ if (importsEnd === -1 || entriesEnd === -1) {
21
+ throw new RegistryError(`units.ts 缺少 ${IMPORTS_END_MARKER} / ${ENTRIES_END_MARKER} 标记行,无法自动登记;请手工维护该文件`);
22
+ }
23
+ lines.splice(entriesEnd, 0, entryLine);
24
+ lines.splice(importsEnd, 0, importLine);
25
+ return lines.join('\n');
26
+ }
15
27
  /** 把单元登记进 dir/units.ts;已存在同名条目则跳过。标记行缺失时报错。 */
16
28
  export function registerUnit(dir, name) {
17
29
  const file = ensureRegistry(dir);
@@ -21,14 +33,19 @@ export function registerUnit(dir, name) {
21
33
  if (content.includes(`'./units/${name}/index.js'`) || content.includes(`provide: '${name}'`)) {
22
34
  return 'already';
23
35
  }
24
- const lines = content.split('\n');
25
- const importsEnd = lines.findIndex((l) => l.trim() === IMPORTS_END_MARKER);
26
- const entriesEnd = lines.findIndex((l) => l.trim() === ENTRIES_END_MARKER);
27
- if (importsEnd === -1 || entriesEnd === -1) {
28
- throw new RegistryError(`units.ts 缺少 ${IMPORTS_END_MARKER} / ${ENTRIES_END_MARKER} 标记行,无法自动登记;请手工维护该文件`);
36
+ writeFileSync(file, insertAtMarkers(content, importLine, entryLine), 'utf8');
37
+ return 'registered';
38
+ }
39
+ /** 把第三方包(agentia add)登记进 dir/units.ts;token/import 来源已存在则跳过。 */
40
+ export function registerPackage(dir, token, importSource) {
41
+ const file = ensureRegistry(dir);
42
+ const content = readFileSync(file, 'utf8');
43
+ const pascal = kebabToPascal(token);
44
+ const importLine = `import ${pascal} from '${importSource}';`;
45
+ const entryLine = ` { provide: '${token}', useClass: ${pascal} },`;
46
+ if (content.includes(`provide: '${token}'`) || content.includes(`from '${importSource}'`)) {
47
+ return 'already';
29
48
  }
30
- lines.splice(entriesEnd, 0, entryLine);
31
- lines.splice(importsEnd, 0, importLine);
32
- writeFileSync(file, lines.join('\n'), 'utf8');
49
+ writeFileSync(file, insertAtMarkers(content, importLine, entryLine), 'utf8');
33
50
  return 'registered';
34
51
  }
package/dist/templates.js CHANGED
@@ -45,7 +45,7 @@ export function projectPackageJson(name) {
45
45
  dev: 'tsx src/main.ts',
46
46
  typecheck: 'tsc --noEmit -p tsconfig.json',
47
47
  },
48
- dependencies: { '@migor/agentia': '^0.0.2' },
48
+ dependencies: { '@migor/agentia': '^0.2.0' },
49
49
  devDependencies: {
50
50
  tsx: '^4.19.0',
51
51
  typescript: '^7.0.2',
package/package.json CHANGED
@@ -1,16 +1,27 @@
1
1
  {
2
2
  "name": "@migor/cli",
3
- "version": "0.0.2",
3
+ "version": "0.2.0",
4
4
  "description": "Agentia 框架命令行工具:create 脚手架 + g 单元生成器",
5
5
  "type": "module",
6
6
  "bin": {
7
7
  "agentia": "./dist/cli.js"
8
8
  },
9
- "files": ["dist"],
9
+ "files": [
10
+ "dist"
11
+ ],
10
12
  "repository": "retrychx/agentia",
11
- "keywords": ["agentia", "agent", "cli", "scaffold"],
12
- "engines": { "node": ">=18" },
13
- "publishConfig": { "access": "public" },
13
+ "keywords": [
14
+ "agentia",
15
+ "agent",
16
+ "cli",
17
+ "scaffold"
18
+ ],
19
+ "engines": {
20
+ "node": ">=18"
21
+ },
22
+ "publishConfig": {
23
+ "access": "public"
24
+ },
14
25
  "scripts": {
15
26
  "build": "tsc -p tsconfig.json",
16
27
  "prepublishOnly": "npm run build"