@icyouo/evt-cli 0.1.0 → 0.1.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/README.md CHANGED
@@ -9,7 +9,8 @@ evt-cli is a general-purpose HTTP CLI for running YAML-defined APIs and workflow
9
9
  - Runtime endpoints come only from `apis/*.yaml`.
10
10
  - Flows come only from `flows/*.yaml`.
11
11
  - Environments, base URLs, headers, and test fixtures come only from `profiles/*.json`.
12
- - The scanner is only for coverage comparison and generating missing YAML skeletons. It is not a runtime data source.
12
+ - Endpoint definitions are persisted as YAML. Scanner JSON is only an internal machine-readable handoff.
13
+ - The bundled skill scanner is preferred for discovery and YAML generation. The built-in regex scanner remains as a fallback for users without an agent or skill workflow.
13
14
  - The default data directory is bundled with the CLI package. You can also point to your own project data directory with `--config-root` or `EVT_CLI_ROOT`.
14
15
 
15
16
  ## Quick Start
@@ -83,6 +84,9 @@ For compatibility with existing projects, evt also reads `apis/`, `flows/`, `pro
83
84
  npm run ci:local
84
85
  ```
85
86
 
87
+ The npm package includes source code, scripts, tests, data examples, and skills
88
+ so users can run local checks and build the standalone local binary package.
89
+
86
90
  This runs:
87
91
 
88
92
  - `npm test`
@@ -100,6 +104,7 @@ The local package directory contains only:
100
104
 
101
105
  - `evt`
102
106
  - `data/`
107
+ - `skills/`
103
108
  - `README.md`
104
109
  - `README.zh-CN.md`
105
110
 
@@ -146,7 +151,35 @@ Each endpoint should include:
146
151
  - `response`
147
152
  - `dangerous: true` when the endpoint is destructive or sensitive
148
153
 
149
- ## Scan And Coverage
154
+ ## Skill Scan, Sync, And Coverage
155
+
156
+ evt-cli includes `skills/evt-api-scanner/SKILL.md`. Agents can use this skill to
157
+ discover source directories, write scanner config, generate YAML endpoint
158
+ definitions, and audit coverage.
159
+
160
+ Endpoint definitions are written to YAML under `data/apis/*.yaml`. The scanner
161
+ may use JSON internally, but JSON is not the endpoint definition format.
162
+
163
+ Discover API source paths and write `data/scanner.json`:
164
+
165
+ ```bash
166
+ evt api discover --config-root ./cli
167
+ ```
168
+
169
+ Generate or update YAML endpoint definitions:
170
+
171
+ ```bash
172
+ evt api sync --config-root ./cli
173
+ ```
174
+
175
+ Check YAML coverage:
176
+
177
+ ```bash
178
+ evt api coverage --config-root ./cli
179
+ ```
180
+
181
+ The built-in regex scanner is still available as a fallback. It is less complete
182
+ than the skill-guided workflow, but it works without an agent.
150
183
 
151
184
  Scan source code for candidate endpoints:
152
185
 
@@ -166,7 +199,8 @@ Generate missing YAML skeletons:
166
199
  npm run sync:api -- --config-root /path/to/project/cli
167
200
  ```
168
201
 
169
- The scanner supports `kotlin`, `swift`, `js`, and `dart`. It also supports external skill or AST scanner commands:
202
+ The scanner supports `kotlin`, `swift`, `js`, and `dart`. It also supports the
203
+ bundled skill scanner and external command scanners:
170
204
 
171
205
  ```json
172
206
  {
@@ -179,15 +213,16 @@ The scanner supports `kotlin`, `swift`, `js`, and `dart`. It also supports exter
179
213
  },
180
214
  {
181
215
  "language": "skill",
182
- "name": "ast-scanner",
183
- "command": "node",
184
- "args": ["tools/scan-endpoints.js"]
216
+ "name": "evt-api-scanner",
217
+ "skill": "evt-api-scanner"
185
218
  }
186
219
  ]
187
220
  }
188
221
  ```
189
222
 
190
- An external scanner can output a JSON array or `{ "endpoints": [] }`. Each endpoint should include at least:
223
+ An external scanner can output a JSON array or `{ "endpoints": [] }` as an
224
+ internal scan result. `evt api sync` converts that scan result into YAML. Each
225
+ intermediate endpoint should include at least:
191
226
 
192
227
  ```json
193
228
  {
@@ -206,6 +241,9 @@ This JSON is used only for scanning, coverage, and sync. Runtime execution still
206
241
  ```bash
207
242
  evt profile list
208
243
  evt api list
244
+ evt api discover --config-root ./cli
245
+ evt api sync --config-root ./cli
246
+ evt api coverage --config-root ./cli
209
247
  evt api call todo.list --dry-run
210
248
  evt flow run login --profile local
211
249
  evt cache show
package/README.zh-CN.md CHANGED
@@ -9,7 +9,8 @@ evt-cli 是一个通用 HTTP CLI,用 YAML 定义接口、用 YAML 定义 flow
9
9
  - 运行时端点只来自 `apis/*.yaml`。
10
10
  - flow 只来自 `flows/*.yaml`。
11
11
  - 环境、base URL、headers、测试 fixtures 只来自 `profiles/*.json`。
12
- - 扫描器只用于覆盖率对比和生成缺失 YAML 骨架,不是运行时数据源。
12
+ - endpoint 定义持久化格式始终是 YAML。scanner JSON 只作为内部机器中间结果。
13
+ - 内置 skill scanner 优先用于发现、生成 YAML 和覆盖率审计。现有正则 scanner 保留给没有 agent 或 skill workflow 的用户兜底。
13
14
  - 默认数据目录是当前 CLI 包内目录;也可以通过 `--config-root` 或 `EVT_CLI_ROOT` 指向项目自己的数据目录。
14
15
 
15
16
  ## 快速开始
@@ -83,6 +84,8 @@ evt-cli 自带 `data/**/*.example.*`,用于开箱验证和复制参考。真
83
84
  npm run ci:local
84
85
  ```
85
86
 
87
+ npm 包会包含源码、脚本、测试、示例数据和 skills,用户可以自行运行本地校验并构建独立本地二进制包。
88
+
86
89
  执行内容:
87
90
 
88
91
  - `npm test`
@@ -100,6 +103,7 @@ npm run ci:local
100
103
 
101
104
  - `evt`
102
105
  - `data/`
106
+ - `skills/`
103
107
  - `README.md`
104
108
  - `README.zh-CN.md`
105
109
 
@@ -146,7 +150,34 @@ endpoints:
146
150
  - `response`
147
151
  - 必要时标记 `dangerous: true`
148
152
 
149
- ## 扫描和覆盖率
153
+ ## Skill 扫描、同步和覆盖率
154
+
155
+ evt-cli 内置 `skills/evt-api-scanner/SKILL.md`。agent 可以使用这个 skill
156
+ 发现源码目录、写入 scanner 配置、生成 YAML endpoint 定义,并做覆盖率审计。
157
+
158
+ endpoint 定义会写到 `data/apis/*.yaml`。scanner 可以在内部使用 JSON,但 JSON
159
+ 不是 endpoint 的最终定义格式。
160
+
161
+ 发现 API 源码路径并写入 `data/scanner.json`:
162
+
163
+ ```bash
164
+ evt api discover --config-root ./cli
165
+ ```
166
+
167
+ 生成或更新 YAML endpoint 定义:
168
+
169
+ ```bash
170
+ evt api sync --config-root ./cli
171
+ ```
172
+
173
+ 检查 YAML 覆盖率:
174
+
175
+ ```bash
176
+ evt api coverage --config-root ./cli
177
+ ```
178
+
179
+ 内置正则 scanner 仍然保留为兜底方案。它不如 skill workflow 完整,但不依赖
180
+ agent,也能让没有 agent 的用户勉强使用。
150
181
 
151
182
  扫描源码候选 endpoint:
152
183
 
@@ -166,7 +197,7 @@ npm run coverage:api -- --config-root /path/to/project/cli
166
197
  npm run sync:api -- --config-root /path/to/project/cli
167
198
  ```
168
199
 
169
- 扫描器支持 `kotlin`、`swift`、`js`、`dart`,也支持外部 skill/AST 扫描命令:
200
+ 扫描器支持 `kotlin`、`swift`、`js`、`dart`,也支持内置 skill scanner 和外部命令 scanner:
170
201
 
171
202
  ```json
172
203
  {
@@ -179,15 +210,15 @@ npm run sync:api -- --config-root /path/to/project/cli
179
210
  },
180
211
  {
181
212
  "language": "skill",
182
- "name": "ast-scanner",
183
- "command": "node",
184
- "args": ["tools/scan-endpoints.js"]
213
+ "name": "evt-api-scanner",
214
+ "skill": "evt-api-scanner"
185
215
  }
186
216
  ]
187
217
  }
188
218
  ```
189
219
 
190
- 外部 scanner 输出 JSON 数组,或 `{ "endpoints": [] }`。每个 endpoint 至少包含:
220
+ 外部 scanner 可以输出 JSON 数组,或 `{ "endpoints": [] }` 作为内部扫描结果。
221
+ `evt api sync` 会把这个扫描结果转换成 YAML。每个中间 endpoint 至少包含:
191
222
 
192
223
  ```json
193
224
  {
@@ -206,6 +237,9 @@ npm run sync:api -- --config-root /path/to/project/cli
206
237
  ```bash
207
238
  evt profile list
208
239
  evt api list
240
+ evt api discover --config-root ./cli
241
+ evt api sync --config-root ./cli
242
+ evt api coverage --config-root ./cli
209
243
  evt api call todo.list --dry-run
210
244
  evt flow run login --profile local
211
245
  evt cache show
@@ -30,11 +30,8 @@
30
30
  },
31
31
  {
32
32
  "language": "skill",
33
- "name": "ast-scanner",
34
- "command": "node",
35
- "args": [
36
- "tools/scan-endpoints.js"
37
- ]
33
+ "name": "evt-api-scanner",
34
+ "skill": "evt-api-scanner"
38
35
  }
39
36
  ]
40
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@icyouo/evt-cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Run YAML-defined HTTP APIs and interactive workflows from your terminal.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -17,14 +17,17 @@
17
17
  "files": [
18
18
  "bin/",
19
19
  "src/",
20
+ "scripts/",
21
+ "test/",
20
22
  "data/",
23
+ "skills/",
21
24
  "README.md",
22
25
  "README.zh-CN.md"
23
26
  ],
24
27
  "scripts": {
25
28
  "ci:local": "node scripts/local-ci.js",
26
29
  "test": "node --test",
27
- "check": "node --check bin/evt.js && node --check src/index.js && node --check src/api/liveTester.js && node --check scripts/sync-api-coverage.js && node --check scripts/check-api-coverage.js && node --check scripts/local-ci.js && node --check scripts/build-package.js",
30
+ "check": "node --check bin/evt.js && node --check src/index.js && node --check src/api/liveTester.js && node --check scripts/sync-api-coverage.js && node --check scripts/check-api-coverage.js && node --check scripts/local-ci.js && node --check scripts/build-package.js && node --check skills/evt-api-scanner/scripts/discover.js && node --check skills/evt-api-scanner/scripts/scan.js",
28
31
  "package:local": "node scripts/build-package.js",
29
32
  "sync:api": "node scripts/sync-api-coverage.js",
30
33
  "coverage:api": "node scripts/check-api-coverage.js"
@@ -0,0 +1,173 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const { spawnSync } = require("node:child_process");
6
+
7
+ const cliRoot = path.resolve(__dirname, "..");
8
+ const packageJson = require(path.join(cliRoot, "package.json"));
9
+ const distDir = path.join(cliRoot, "dist");
10
+ const packageName = "evt-cli";
11
+ const packageDir = path.join(distDir, packageName);
12
+ const entryId = "bin/evt.js";
13
+
14
+ function toPosix(file) {
15
+ return file.split(path.sep).join("/");
16
+ }
17
+
18
+ function normalizeModuleId(file) {
19
+ return path.posix.normalize(toPosix(path.relative(cliRoot, file)));
20
+ }
21
+
22
+ function resolveLocalModule(fromId, request) {
23
+ const base = path.posix.dirname(fromId);
24
+ const target = path.posix.normalize(path.posix.join(base, request));
25
+ const candidates = [
26
+ target,
27
+ `${target}.js`,
28
+ path.posix.join(target, "index.js")
29
+ ];
30
+ for (const candidate of candidates) {
31
+ const absolute = path.join(cliRoot, ...candidate.split("/"));
32
+ if (fs.existsSync(absolute) && fs.statSync(absolute).isFile()) {
33
+ return normalizeModuleId(absolute);
34
+ }
35
+ }
36
+ throw new Error(`Unable to resolve ${request} from ${fromId}`);
37
+ }
38
+
39
+ function findLocalRequires(source) {
40
+ const requires = [];
41
+ const pattern = /require\(\s*["']([^"']+)["']\s*\)/g;
42
+ let match;
43
+ while ((match = pattern.exec(source)) !== null) {
44
+ if (match[1].startsWith(".")) {
45
+ requires.push(match[1]);
46
+ }
47
+ }
48
+ return requires;
49
+ }
50
+
51
+ function collectModules(entry) {
52
+ const modules = new Map();
53
+ const queue = [entry];
54
+
55
+ while (queue.length > 0) {
56
+ const id = queue.shift();
57
+ if (modules.has(id)) continue;
58
+
59
+ const absolute = path.join(cliRoot, ...id.split("/"));
60
+ let source = fs.readFileSync(absolute, "utf8");
61
+ if (source.startsWith("#!")) {
62
+ source = source.replace(/^#!.*\n/, "");
63
+ }
64
+ modules.set(id, source);
65
+
66
+ for (const request of findLocalRequires(source)) {
67
+ const resolved = resolveLocalModule(id, request);
68
+ if (!modules.has(resolved)) {
69
+ queue.push(resolved);
70
+ }
71
+ }
72
+ }
73
+
74
+ return modules;
75
+ }
76
+
77
+ function renderExecutable(modules) {
78
+ const moduleEntries = Array.from(modules.entries())
79
+ .sort(([left], [right]) => left.localeCompare(right))
80
+ .map(([id, source]) => `modules[${JSON.stringify(id)}] = function(require, module, exports, __filename, __dirname) {\n${source}\n};`)
81
+ .join("\n\n");
82
+
83
+ return `#!/usr/bin/env node
84
+ const __nodeRequire = require;
85
+ const path = __nodeRequire("node:path");
86
+ const runtimeRoot = path.dirname(process.argv[1] ? path.resolve(process.argv[1]) : __filename);
87
+ const modules = Object.create(null);
88
+ const moduleCache = Object.create(null);
89
+
90
+ ${moduleEntries}
91
+
92
+ function resolveModule(fromId, request) {
93
+ if (!request.startsWith(".")) return request;
94
+ const base = path.posix.dirname(fromId);
95
+ const target = path.posix.normalize(path.posix.join(base, request));
96
+ const candidates = [target, target + ".js", path.posix.join(target, "index.js")];
97
+ for (const candidate of candidates) {
98
+ if (Object.prototype.hasOwnProperty.call(modules, candidate)) return candidate;
99
+ }
100
+ throw new Error("Unable to resolve " + request + " from " + fromId);
101
+ }
102
+
103
+ function loadModule(id) {
104
+ if (!Object.prototype.hasOwnProperty.call(modules, id)) {
105
+ return __nodeRequire(id);
106
+ }
107
+ if (moduleCache[id]) return moduleCache[id].exports;
108
+ const filename = path.join(runtimeRoot, ...id.split("/"));
109
+ const dirname = path.dirname(filename);
110
+ const module = { exports: {} };
111
+ moduleCache[id] = module;
112
+ const localRequire = (request) => loadModule(resolveModule(id, request));
113
+ modules[id](localRequire, module, module.exports, filename, dirname);
114
+ return module.exports;
115
+ }
116
+
117
+ loadModule(${JSON.stringify(entryId)});
118
+ `;
119
+ }
120
+
121
+ function copyDir(sourceDir, targetDir) {
122
+ fs.mkdirSync(targetDir, { recursive: true });
123
+ for (const entry of fs.readdirSync(sourceDir, { withFileTypes: true })) {
124
+ if (entry.name === ".DS_Store") continue;
125
+ const source = path.join(sourceDir, entry.name);
126
+ const target = path.join(targetDir, entry.name);
127
+ if (entry.isDirectory()) {
128
+ copyDir(source, target);
129
+ } else if (entry.isFile()) {
130
+ fs.copyFileSync(source, target);
131
+ }
132
+ }
133
+ }
134
+
135
+ function run(command, args) {
136
+ const result = spawnSync(command, args, {
137
+ cwd: distDir,
138
+ stdio: "inherit",
139
+ shell: false
140
+ });
141
+ if (result.status !== 0) {
142
+ process.exit(result.status || 1);
143
+ }
144
+ }
145
+
146
+ function cleanDist() {
147
+ fs.rmSync(distDir, { recursive: true, force: true });
148
+ fs.mkdirSync(packageDir, { recursive: true });
149
+ }
150
+
151
+ function main() {
152
+ cleanDist();
153
+
154
+ const executablePath = path.join(packageDir, "evt");
155
+ fs.writeFileSync(executablePath, renderExecutable(collectModules(entryId)));
156
+ fs.chmodSync(executablePath, 0o755);
157
+
158
+ copyDir(path.join(cliRoot, "data"), path.join(packageDir, "data"));
159
+ copyDir(path.join(cliRoot, "skills"), path.join(packageDir, "skills"));
160
+ fs.copyFileSync(path.join(cliRoot, "README.md"), path.join(packageDir, "README.md"));
161
+ fs.copyFileSync(path.join(cliRoot, "README.zh-CN.md"), path.join(packageDir, "README.zh-CN.md"));
162
+
163
+ const artifactName = `${packageName}-${packageJson.version}-${process.platform}-${process.arch}.tar.gz`;
164
+ run("tar", ["-czf", artifactName, packageName]);
165
+
166
+ console.log("\nLocal package artifacts:");
167
+ console.log(`- dist/${packageName}/`);
168
+ console.log(`- dist/${artifactName}`);
169
+ console.log("\nPackage contents are limited to evt, data examples, skills, README.md, and README.zh-CN.md.");
170
+ console.log("No npm publish or remote upload was performed.");
171
+ }
172
+
173
+ main();
@@ -0,0 +1,41 @@
1
+ #!/usr/bin/env node
2
+
3
+ const { loadApiRegistry } = require("../src/config/loaders");
4
+ const { compareScanToRegistry, parseCliScanOptions, scanServices } = require("../src/config/serviceScanner");
5
+ const { setConfigRoot } = require("../src/util/paths");
6
+
7
+ function runApiCoverage(argv = process.argv.slice(2), options = {}) {
8
+ const scanOptions = parseCliScanOptions(argv);
9
+ setConfigRoot(scanOptions.configRoot);
10
+ const registry = loadApiRegistry();
11
+ const scanned = scanServices(scanOptions);
12
+ const comparison = compareScanToRegistry(scanned, registry);
13
+ const missingSchema = Object.values(registry)
14
+ .filter((endpoint) => !endpoint.schema)
15
+ .map((endpoint) => endpoint.id);
16
+ const missingResponse = Object.values(registry)
17
+ .filter((endpoint) => !endpoint.response || Object.keys(endpoint.response).length === 0)
18
+ .map((endpoint) => endpoint.id);
19
+
20
+ const failed = comparison.missing.length > 0 || missingSchema.length > 0 || missingResponse.length > 0;
21
+ const payload = {
22
+ scanned: scanned.length,
23
+ covered: comparison.covered.length,
24
+ missing: comparison.missing.map((endpoint) => endpoint.id),
25
+ endpoints: Object.keys(registry).length,
26
+ missingSchema,
27
+ missingResponse
28
+ };
29
+
30
+ console.log(JSON.stringify(payload, null, 2));
31
+ if (failed && options.exitOnFailure) process.exit(1);
32
+ return { failed, payload };
33
+ }
34
+
35
+ if (require.main === module) {
36
+ runApiCoverage(process.argv.slice(2), { exitOnFailure: true });
37
+ }
38
+
39
+ module.exports = {
40
+ runApiCoverage
41
+ };
@@ -0,0 +1,38 @@
1
+ #!/usr/bin/env node
2
+
3
+ const fs = require("node:fs");
4
+ const path = require("node:path");
5
+ const { spawnSync } = require("node:child_process");
6
+
7
+ const cliRoot = path.resolve(__dirname, "..");
8
+ const distDir = path.join(cliRoot, "dist");
9
+
10
+ function run(command, args, options = {}) {
11
+ const label = [command, ...args].join(" ");
12
+ console.log(`\n> ${label}`);
13
+ const result = spawnSync(command, args, {
14
+ cwd: cliRoot,
15
+ stdio: "inherit",
16
+ shell: false,
17
+ ...options
18
+ });
19
+ if (result.status !== 0) {
20
+ process.exit(result.status || 1);
21
+ }
22
+ }
23
+
24
+ function cleanDist() {
25
+ fs.rmSync(distDir, { recursive: true, force: true });
26
+ fs.mkdirSync(distDir, { recursive: true });
27
+ }
28
+
29
+ function main() {
30
+ run("npm", ["test"]);
31
+ run("npm", ["run", "check"]);
32
+ run("node", ["bin/evt.js", "validate"]);
33
+ run("npm", ["run", "coverage:api"]);
34
+ cleanDist();
35
+ run("node", ["scripts/build-package.js"]);
36
+ }
37
+
38
+ main();