apiskill 0.1.3 → 0.1.4
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 +89 -0
- package/README.zh.md +89 -0
- package/docs/cli.md +27 -18
- package/docs/cli.zh.md +27 -18
- package/package.json +4 -3
- package/scripts/apiskill-cli.mjs +3 -3
- package/scripts/lib/apiskill-core.mjs +5 -4
- package/scripts/lib/openapi-store.mjs +142 -51
- package/scripts/mcp-server.mjs +2 -2
- package/scripts/test-cli-concurrency.mjs +110 -0
- package/tsconfig.json +3 -3
package/README.md
CHANGED
|
@@ -52,6 +52,95 @@ apiskill mock
|
|
|
52
52
|
|
|
53
53
|
CLI and MCP are primarily designed for AI coding agents. Agents should check the cache first, initialize it only when needed, and query a focused endpoint instead of reading the entire OpenAPI document. Run `apiskill --help` for all CLI commands. In MCP clients, start with `apiskill_check`, use `apiskill_search_endpoints` or `apiskill_query_api` to locate an API, and call `apiskill_help` for the complete tool list.
|
|
54
54
|
|
|
55
|
+
### AI Agent CLI CRUD Protocol
|
|
56
|
+
|
|
57
|
+
Use `--json` on commands that support it and parse the response instead of scraping human-readable output. For an isolated project cache, set `APISKILL_CACHE_DIR` to an absolute writable directory before every CLI call. Without it, the global installation uses `~/.apiskill/cache`.
|
|
58
|
+
|
|
59
|
+
1. Check the cache, then create a blank document when no upstream document exists:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
apiskill check --json
|
|
63
|
+
apiskill document create --title "My API" --doc-version 1.0.0 --description "Local API contract" --json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Read `meta.versionId` from the create response and reuse that exact value for every write. `api create` now defaults to the latest version when `--version` is omitted, but agents should still pass it explicitly so every write targets the intended project document.
|
|
67
|
+
|
|
68
|
+
2. Save an API configuration as `api-config.json`:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"api": {
|
|
73
|
+
"method": "post",
|
|
74
|
+
"path": "/api/v1/users/{id}",
|
|
75
|
+
"summary": "Create user",
|
|
76
|
+
"operationId": "createUser",
|
|
77
|
+
"tags": ["Users"],
|
|
78
|
+
"parameters": [
|
|
79
|
+
{ "name": "id", "location": "path", "required": true, "type": "string" }
|
|
80
|
+
],
|
|
81
|
+
"requestBody": {
|
|
82
|
+
"required": true,
|
|
83
|
+
"contentType": "application/json",
|
|
84
|
+
"fields": [
|
|
85
|
+
{ "name": "name", "type": "string", "required": true },
|
|
86
|
+
{ "name": "email", "type": "string", "format": "email" }
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
"responses": [
|
|
90
|
+
{
|
|
91
|
+
"status": "200",
|
|
92
|
+
"description": "User created",
|
|
93
|
+
"contentType": "application/json",
|
|
94
|
+
"fields": [
|
|
95
|
+
{ "name": "success", "type": "boolean", "required": true },
|
|
96
|
+
{ "name": "userId", "type": "string" }
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
3. Create, inspect, edit, and delete the operation:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
APISKILL_VERSION_ID="value-from-meta.versionId"
|
|
108
|
+
|
|
109
|
+
apiskill api create --version "$APISKILL_VERSION_ID" --file ./api-config.json --json
|
|
110
|
+
apiskill api list --version "$APISKILL_VERSION_ID" --query user --method POST --json
|
|
111
|
+
apiskill api query POST '/api/v1/users/{id}' --version "$APISKILL_VERSION_ID" --format cli
|
|
112
|
+
|
|
113
|
+
# Edit the returned CLI config and save it as api-config.updated.json.
|
|
114
|
+
# POST and the path below identify the original operation; the file contains its replacement.
|
|
115
|
+
apiskill api edit POST '/api/v1/users/{id}' --version "$APISKILL_VERSION_ID" --file ./api-config.updated.json --json
|
|
116
|
+
|
|
117
|
+
# Use the replacement method/path if the edit changed either value.
|
|
118
|
+
apiskill api delete PATCH '/api/v1/users/{id}' --version "$APISKILL_VERSION_ID" --json
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
For a batch of operations, pass the same `APISKILL_VERSION_ID` to every command. For compatibility with older CLI releases, an agent should wait for each write to finish before starting the next one, then verify the final set:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
apiskill api create --version "$APISKILL_VERSION_ID" --file ./users.get.json --json
|
|
125
|
+
apiskill api create --version "$APISKILL_VERSION_ID" --file ./users.create.json --json
|
|
126
|
+
apiskill api create --version "$APISKILL_VERSION_ID" --file ./users.delete.json --json
|
|
127
|
+
apiskill api list --version "$APISKILL_VERSION_ID" --json
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
The current CLI also serializes writes to the same cache across processes, so an AI tool that accidentally launches these commands in parallel will not lose earlier operations.
|
|
131
|
+
|
|
132
|
+
Agent rules:
|
|
133
|
+
|
|
134
|
+
- `api query` defaults to JSON and does not accept `--json`; use `--format cli` for a normalized configuration that can be edited and written back.
|
|
135
|
+
- `api edit ORIGINAL_METHOD ORIGINAL_PATH` locates the old operation. The replacement config may change its method or path.
|
|
136
|
+
- Quote paths containing `{id}` or other shell-sensitive characters.
|
|
137
|
+
- `api list --query` searches operation metadata and parameters, not response field names. Use exact `api query METHOD PATH` when method and path are known.
|
|
138
|
+
- A blank document with zero paths makes `check --json` return `ok: false` until at least one API is added. The document still exists; inspect `versionsCount` and `latestVersion`.
|
|
139
|
+
- Missing operations and invalid commands return a nonzero process exit code. Agents should treat that as failure and inspect stderr.
|
|
140
|
+
- `--config '<json-or-yaml>'` is equivalent to `--file`; files are safer for large or nested configurations.
|
|
141
|
+
- After a batch write, run `api list --version ... --json` and verify that every expected method/path exists before reporting success.
|
|
142
|
+
- OpenAPI identifies an operation by its method/path pair. Creating the same pair again intentionally replaces it; `meta.paths` counts distinct paths, not the total number of operations.
|
|
143
|
+
|
|
55
144
|
## Why This Tool Exists
|
|
56
145
|
|
|
57
146
|
Since large AI models became available, the way developers use AI for coding has changed quickly. At first, many of us worked in the ChatGPT web UI by copying code, errors, and API documentation back and forth. Later, tools such as Cursor, Codex, and Claude Code made it possible for AI assistants to work inside an entire project, so the workflow moved from isolated prompts toward project-aware development.
|
package/README.zh.md
CHANGED
|
@@ -52,6 +52,95 @@ apiskill mock
|
|
|
52
52
|
|
|
53
53
|
CLI 和 MCP 主要面向 AI 编码 Agent。Agent 应先检查缓存,只在缺少文档时初始化,然后按当前任务精确查询接口,避免每次读取整份 OpenAPI 文档。运行 `apiskill --help` 可以查看全部 CLI 命令;在 MCP 客户端中先调用 `apiskill_check`,再使用 `apiskill_search_endpoints` 或 `apiskill_query_api` 定位接口,调用 `apiskill_help` 可查看完整工具列表。
|
|
54
54
|
|
|
55
|
+
### AI Agent CLI 增删改查协议
|
|
56
|
+
|
|
57
|
+
支持 `--json` 的命令应优先使用 JSON 输出并解析字段,不要抓取人类可读文本。需要按项目隔离缓存时,每次调用前将 `APISKILL_CACHE_DIR` 设置为绝对可写目录;未设置时,全局安装默认使用 `~/.apiskill/cache`。
|
|
58
|
+
|
|
59
|
+
1. 检查缓存;没有上游文档时创建空白文档:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
apiskill check --json
|
|
63
|
+
apiskill document create --title "My API" --doc-version 1.0.0 --description "Local API contract" --json
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
从创建结果读取 `meta.versionId`,后续所有写操作都复用这个精确值。现在 `api create` 省略 `--version` 时会默认写入最新版本,但 Agent 仍应显式传入,确保每次都写入目标项目文档。
|
|
67
|
+
|
|
68
|
+
2. 将接口配置保存为 `api-config.json`:
|
|
69
|
+
|
|
70
|
+
```json
|
|
71
|
+
{
|
|
72
|
+
"api": {
|
|
73
|
+
"method": "post",
|
|
74
|
+
"path": "/api/v1/users/{id}",
|
|
75
|
+
"summary": "创建用户",
|
|
76
|
+
"operationId": "createUser",
|
|
77
|
+
"tags": ["Users"],
|
|
78
|
+
"parameters": [
|
|
79
|
+
{ "name": "id", "location": "path", "required": true, "type": "string" }
|
|
80
|
+
],
|
|
81
|
+
"requestBody": {
|
|
82
|
+
"required": true,
|
|
83
|
+
"contentType": "application/json",
|
|
84
|
+
"fields": [
|
|
85
|
+
{ "name": "name", "type": "string", "required": true },
|
|
86
|
+
{ "name": "email", "type": "string", "format": "email" }
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
"responses": [
|
|
90
|
+
{
|
|
91
|
+
"status": "200",
|
|
92
|
+
"description": "用户创建成功",
|
|
93
|
+
"contentType": "application/json",
|
|
94
|
+
"fields": [
|
|
95
|
+
{ "name": "success", "type": "boolean", "required": true },
|
|
96
|
+
{ "name": "userId", "type": "string" }
|
|
97
|
+
]
|
|
98
|
+
}
|
|
99
|
+
]
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
3. 新增、读取、修改并删除接口:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
APISKILL_VERSION_ID="value-from-meta.versionId"
|
|
108
|
+
|
|
109
|
+
apiskill api create --version "$APISKILL_VERSION_ID" --file ./api-config.json --json
|
|
110
|
+
apiskill api list --version "$APISKILL_VERSION_ID" --query user --method POST --json
|
|
111
|
+
apiskill api query POST '/api/v1/users/{id}' --version "$APISKILL_VERSION_ID" --format cli
|
|
112
|
+
|
|
113
|
+
# 修改返回的 CLI 配置并保存为 api-config.updated.json。
|
|
114
|
+
# 下面的 POST 和路径用于定位旧接口,文件中保存替换后的新配置。
|
|
115
|
+
apiskill api edit POST '/api/v1/users/{id}' --version "$APISKILL_VERSION_ID" --file ./api-config.updated.json --json
|
|
116
|
+
|
|
117
|
+
# 如果修改时改变了 method 或 path,删除时使用替换后的值。
|
|
118
|
+
apiskill api delete PATCH '/api/v1/users/{id}' --version "$APISKILL_VERSION_ID" --json
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
批量写入多个接口时,每条命令都必须使用同一个 `APISKILL_VERSION_ID`。为了兼容旧版 CLI,Agent 应等待上一条写命令完成后再执行下一条,最后检查完整接口列表:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
apiskill api create --version "$APISKILL_VERSION_ID" --file ./users.get.json --json
|
|
125
|
+
apiskill api create --version "$APISKILL_VERSION_ID" --file ./users.create.json --json
|
|
126
|
+
apiskill api create --version "$APISKILL_VERSION_ID" --file ./users.delete.json --json
|
|
127
|
+
apiskill api list --version "$APISKILL_VERSION_ID" --json
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
当前版本还会在多个进程之间串行化同一缓存目录的写操作,因此 AI 工具即使意外并行执行这些命令,也不会再丢失先写入的接口。
|
|
131
|
+
|
|
132
|
+
Agent 执行规则:
|
|
133
|
+
|
|
134
|
+
- `api query` 默认输出 JSON,不支持 `--json`;需要可修改并回写的标准配置时使用 `--format cli`。
|
|
135
|
+
- `api edit ORIGINAL_METHOD ORIGINAL_PATH` 的前两个参数定位旧接口,新配置可以改变 method 或 path。
|
|
136
|
+
- 包含 `{id}` 等 shell 特殊字符的路径必须加引号。
|
|
137
|
+
- `api list --query` 搜索接口元数据和参数,不搜索响应字段名。已知 method 和 path 时使用精确的 `api query METHOD PATH`。
|
|
138
|
+
- 空白文档没有任何 path 时,`check --json` 会返回 `ok: false`,直到至少添加一个 API。文档并未丢失,可检查 `versionsCount` 和 `latestVersion`。
|
|
139
|
+
- 接口不存在或命令参数无效时,进程返回非零退出码。Agent 应视为失败并读取 stderr。
|
|
140
|
+
- `--config '<json-or-yaml>'` 与 `--file` 等效;复杂或嵌套配置优先使用文件,避免 shell 转义错误。
|
|
141
|
+
- 批量写入结束后,执行 `api list --version ... --json`,逐一核对预期的 method/path 均存在,再报告任务成功。
|
|
142
|
+
- OpenAPI 使用 method/path 组合唯一标识接口;再次创建相同组合会按预期替换原接口。`meta.paths` 统计的是不同路径数,不是接口操作总数。
|
|
143
|
+
|
|
55
144
|
## 为什么开发这个工具
|
|
56
145
|
|
|
57
146
|
自从 AI 大模型面世这几年,开发人员使用 AI 写代码的方式一直在变化。最开始,很多人是在 ChatGPT 网页端来回复制粘贴代码、报错和接口文档;后来 Cursor、Codex、Claude Code 这类可以集成整个项目的桌面端或本地开发工具出现,AI 辅助开发逐渐从单次问答变成了围绕整个项目上下文协作。
|
package/docs/cli.md
CHANGED
|
@@ -39,17 +39,17 @@ node scripts/apiskill-cli.mjs --help
|
|
|
39
39
|
Check whether a usable cache is available:
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
apiskill check
|
|
43
|
+
apiskill check --json
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
If no cache exists, `check` prints import examples.
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
apiskill import https://example.com/openapi.json
|
|
50
|
+
apiskill crawl https://example.com/swagger
|
|
51
|
+
apiskill import-file ./openapi.yaml
|
|
52
|
+
apiskill import-curl --file ./request.curl
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
Use `--auth username:password` with `import` or `crawl` when the document endpoint requires basic auth.
|
|
@@ -59,25 +59,27 @@ Use `--auth username:password` with `import` or `crawl` when the document endpoi
|
|
|
59
59
|
When there is no upstream OpenAPI document yet, create a blank local document version:
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
apiskill document create --title "My API" --doc-version 1.0.0
|
|
63
|
+
apiskill document create --title "My API" --doc-version 1.0.0 --description "Internal service contract" --json
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
The created document is saved as the latest cached version.
|
|
66
|
+
The created document is saved as the latest cached version. Read `meta.versionId` from the JSON response and pass it explicitly to later writes. `api create` defaults to the latest version when `--version` is omitted, but explicit version IDs keep agent workflows deterministic.
|
|
67
67
|
|
|
68
68
|
## Query Versions And APIs
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
apiskill versions
|
|
72
|
+
apiskill versions --json
|
|
73
|
+
apiskill query /admin/api/v1/user/list --method GET
|
|
74
|
+
apiskill query user --method POST --limit 10
|
|
75
|
+
apiskill api list --query user --method post
|
|
76
|
+
apiskill api query GET /api/v1/user --format cli
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
`query` behaves like the MCP `apiskill_query_api` tool: exact single matches return one API config, while multiple matches return candidates.
|
|
80
80
|
|
|
81
|
+
`api query` defaults to JSON and does not accept `--json`. Use `--format cli` to get a normalized config that can be edited and sent back to `api edit`.
|
|
82
|
+
|
|
81
83
|
## Start A MOCK Server
|
|
82
84
|
|
|
83
85
|
After an API document is cached, start a local MOCK API server:
|
|
@@ -93,11 +95,18 @@ The MOCK server generates local API routes from the current OpenAPI paths, HTTP
|
|
|
93
95
|
## Create, Edit, And Delete Manual APIs
|
|
94
96
|
|
|
95
97
|
```bash
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
APISKILL_VERSION_ID="value-from-meta.versionId"
|
|
99
|
+
apiskill api create --version "$APISKILL_VERSION_ID" --file ./api-config.yaml --json
|
|
100
|
+
apiskill api edit GET /api/v1/user --version "$APISKILL_VERSION_ID" --file ./api-config.json --json
|
|
101
|
+
apiskill api delete GET /api/v1/user --version "$APISKILL_VERSION_ID" --json
|
|
99
102
|
```
|
|
100
103
|
|
|
104
|
+
The method and path passed to `api edit` identify the original operation. The replacement file may contain a different method or path. Quote paths that contain `{id}` or other shell-sensitive characters.
|
|
105
|
+
|
|
106
|
+
When adding several APIs, reuse the exact same version ID for every command. Agents should run writes sequentially for compatibility with older releases and finish with `apiskill api list --version "$APISKILL_VERSION_ID" --json` to verify every expected method/path. The current CLI also uses a cross-process cache write lock, so accidentally parallel commands are serialized instead of overwriting one another.
|
|
107
|
+
|
|
108
|
+
An OpenAPI operation is unique by method/path. Creating the same pair again replaces that operation. `meta.paths` is the number of distinct paths, so use `api list` when validating the total operation count.
|
|
109
|
+
|
|
101
110
|
CLI config can be JSON or YAML with root key `api`, `config`, or `operation`.
|
|
102
111
|
|
|
103
112
|
Minimal JSON example:
|
package/docs/cli.zh.md
CHANGED
|
@@ -39,17 +39,17 @@ node scripts/apiskill-cli.mjs --help
|
|
|
39
39
|
先检查当前是否有可用缓存:
|
|
40
40
|
|
|
41
41
|
```bash
|
|
42
|
-
|
|
43
|
-
|
|
42
|
+
apiskill check
|
|
43
|
+
apiskill check --json
|
|
44
44
|
```
|
|
45
45
|
|
|
46
46
|
如果没有可用缓存,`check` 会输出导入文档的示例。
|
|
47
47
|
|
|
48
48
|
```bash
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
49
|
+
apiskill import https://example.com/openapi.json
|
|
50
|
+
apiskill crawl https://example.com/swagger
|
|
51
|
+
apiskill import-file ./openapi.yaml
|
|
52
|
+
apiskill import-curl --file ./request.curl
|
|
53
53
|
```
|
|
54
54
|
|
|
55
55
|
如果文档地址需要 basic auth,`import` 和 `crawl` 可以加 `--auth username:password`。
|
|
@@ -59,25 +59,27 @@ npm run cli -- import-curl --file ./request.curl
|
|
|
59
59
|
如果还没有上游 OpenAPI 文档,可以先创建一份本地空白文档版本:
|
|
60
60
|
|
|
61
61
|
```bash
|
|
62
|
-
|
|
63
|
-
|
|
62
|
+
apiskill document create --title "My API" --doc-version 1.0.0
|
|
63
|
+
apiskill document create --title "My API" --doc-version 1.0.0 --description "Internal service contract" --json
|
|
64
64
|
```
|
|
65
65
|
|
|
66
|
-
|
|
66
|
+
创建后的文档会保存为最新缓存版本。从 JSON 结果读取 `meta.versionId`,后续写操作都显式传入这个值。`api create` 省略 `--version` 时会默认写入最新版本,但显式版本 ID 能让 Agent 的操作目标保持确定。
|
|
67
67
|
|
|
68
68
|
## 查询版本和接口
|
|
69
69
|
|
|
70
70
|
```bash
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
71
|
+
apiskill versions
|
|
72
|
+
apiskill versions --json
|
|
73
|
+
apiskill query /admin/api/v1/user/list --method GET
|
|
74
|
+
apiskill query user --method POST --limit 10
|
|
75
|
+
apiskill api list --query user --method post
|
|
76
|
+
apiskill api query GET /api/v1/user --format cli
|
|
77
77
|
```
|
|
78
78
|
|
|
79
79
|
`query` 和 MCP 的 `apiskill_query_api` 行为一致:精确单条匹配时返回一个接口配置,多条匹配时返回候选列表。
|
|
80
80
|
|
|
81
|
+
`api query` 默认输出 JSON,不支持 `--json`。需要可修改并传给 `api edit` 的标准配置时使用 `--format cli`。
|
|
82
|
+
|
|
81
83
|
## 启动 MOCK 服务
|
|
82
84
|
|
|
83
85
|
已有 API 文档缓存后,可以启动本地 MOCK API 服务:
|
|
@@ -93,11 +95,18 @@ MOCK 服务会根据当前 OpenAPI 文档里的接口路径、HTTP 方法和响
|
|
|
93
95
|
## 创建、编辑、删除手动接口
|
|
94
96
|
|
|
95
97
|
```bash
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
98
|
+
APISKILL_VERSION_ID="value-from-meta.versionId"
|
|
99
|
+
apiskill api create --version "$APISKILL_VERSION_ID" --file ./api-config.yaml --json
|
|
100
|
+
apiskill api edit GET /api/v1/user --version "$APISKILL_VERSION_ID" --file ./api-config.json --json
|
|
101
|
+
apiskill api delete GET /api/v1/user --version "$APISKILL_VERSION_ID" --json
|
|
99
102
|
```
|
|
100
103
|
|
|
104
|
+
`api edit` 后面的 method 和 path 用于定位旧接口,替换文件可以包含不同的 method 或 path。路径中包含 `{id}` 等 shell 特殊字符时应加引号。
|
|
105
|
+
|
|
106
|
+
批量新增多个接口时,每条命令必须复用同一个版本 ID。为了兼容旧版本,Agent 应串行执行写命令,并在结束后运行 `apiskill api list --version "$APISKILL_VERSION_ID" --json`,核对所有预期的 method/path。当前 CLI 也使用跨进程缓存写锁,意外并行的命令会被串行处理,不再互相覆盖。
|
|
107
|
+
|
|
108
|
+
OpenAPI 使用 method/path 组合唯一标识接口;再次创建相同组合会替换原接口。`meta.paths` 是不同路径的数量,校验接口操作总数时应以 `api list` 为准。
|
|
109
|
+
|
|
101
110
|
CLI 配置可以是 JSON 或 YAML,根字段支持 `api`、`config` 或 `operation`。
|
|
102
111
|
|
|
103
112
|
最小 JSON 示例:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apiskill",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"dev": "vite --host 0.0.0.0 && node scripts/mcp-server.mjs",
|
|
23
23
|
"build": "tsc -b && vite build",
|
|
24
|
+
"test:cli-concurrency": "node scripts/test-cli-concurrency.mjs",
|
|
24
25
|
"prepack": "npm run build",
|
|
25
26
|
"preview": "vite preview --host 0.0.0.0",
|
|
26
27
|
"mcp": "node scripts/mcp-server.mjs",
|
|
@@ -32,13 +33,13 @@
|
|
|
32
33
|
"lucide-react": "^0.468.0",
|
|
33
34
|
"react": "^18.3.1",
|
|
34
35
|
"react-dom": "^18.3.1",
|
|
35
|
-
"typescript": "^5.8.3",
|
|
36
36
|
"vite": "^6.0.7",
|
|
37
37
|
"yaml": "^2.8.3"
|
|
38
38
|
},
|
|
39
39
|
"devDependencies": {
|
|
40
40
|
"@types/node": "^22.10.2",
|
|
41
41
|
"@types/react": "^18.3.18",
|
|
42
|
-
"@types/react-dom": "^18.3.5"
|
|
42
|
+
"@types/react-dom": "^18.3.5",
|
|
43
|
+
"typescript": "^7.0.2"
|
|
43
44
|
}
|
|
44
45
|
}
|
package/scripts/apiskill-cli.mjs
CHANGED
|
@@ -49,7 +49,7 @@ Examples:
|
|
|
49
49
|
apiskill query /admin/api/v1/activity --method GET
|
|
50
50
|
apiskill api list --query user --method post
|
|
51
51
|
apiskill api query GET /api/v1/user --format cli
|
|
52
|
-
apiskill api create --file ./api-config.yaml
|
|
52
|
+
apiskill api create --version <versionId> --file ./api-config.yaml
|
|
53
53
|
apiskill api edit GET /api/v1/user --file ./api-config.json --version 20260429T000000Z-manual-user
|
|
54
54
|
apiskill api delete GET /api/v1/user --version 20260429T000000Z-manual-user
|
|
55
55
|
|
|
@@ -239,8 +239,8 @@ api
|
|
|
239
239
|
|
|
240
240
|
api
|
|
241
241
|
.command('create')
|
|
242
|
-
.description('Create an API operation
|
|
243
|
-
.option('-v, --version <versionId>', 'Version id to
|
|
242
|
+
.description('Create an API operation in a cached version.')
|
|
243
|
+
.option('-v, --version <versionId>', 'Version id. Defaults to latest; creates a manual version only if the cache is empty.')
|
|
244
244
|
.option('-f, --file <file>', 'JSON/YAML/CLI config file')
|
|
245
245
|
.option('-c, --config <text>', 'Inline JSON/YAML/CLI config')
|
|
246
246
|
.option('-j, --json', 'Print JSON')
|
|
@@ -22,10 +22,11 @@ export const importExamples = {
|
|
|
22
22
|
'Open the local URL, then import a direct OpenAPI JSON/YAML URL, crawl Swagger UI / Knife4j / Redoc, upload a file, or paste a curl command.',
|
|
23
23
|
],
|
|
24
24
|
cli: [
|
|
25
|
-
'
|
|
26
|
-
'
|
|
27
|
-
'
|
|
28
|
-
'
|
|
25
|
+
'apiskill import https://example.com/openapi.json',
|
|
26
|
+
'apiskill crawl https://example.com/swagger',
|
|
27
|
+
'apiskill import-file ./openapi.yaml',
|
|
28
|
+
'apiskill import-curl --file ./request.curl',
|
|
29
|
+
'apiskill document create --title "My API" --doc-version 1.0.0 --json',
|
|
29
30
|
],
|
|
30
31
|
mcp: [
|
|
31
32
|
{ tool: 'apiskill_import_url', arguments: { url: 'https://example.com/openapi.json' } },
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
|
-
import { mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { mkdir, open, readFile, readdir, stat, unlink, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { basename, dirname, resolve } from 'node:path';
|
|
4
4
|
import { parse as parseYaml } from 'yaml';
|
|
5
5
|
import { getDefaultCacheDir } from './cache-paths.mjs';
|
|
@@ -11,6 +11,9 @@ const versionsDir = resolve(cacheDir, 'versions');
|
|
|
11
11
|
const latestMetaPath = resolve(cacheDir, 'latest-import.json');
|
|
12
12
|
const legacyCachePath = resolve(cacheDir, 'openapi-cache.json');
|
|
13
13
|
const legacyCacheMetaPath = resolve(cacheDir, 'import-meta.json');
|
|
14
|
+
const writeLockPath = resolve(cacheDir, '.write.lock');
|
|
15
|
+
const writeLockTimeoutMs = 15_000;
|
|
16
|
+
const malformedLockStaleMs = 30_000;
|
|
14
17
|
|
|
15
18
|
export function parseOpenApiText(text) {
|
|
16
19
|
const cleaned = text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
|
|
@@ -94,20 +97,22 @@ export async function listCachedVersions() {
|
|
|
94
97
|
}
|
|
95
98
|
|
|
96
99
|
export async function saveImportedDocument({ document, mode, inputUrl, resolvedUrl, versionId }) {
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
100
|
+
return withCacheWriteLock(async () => {
|
|
101
|
+
if (!isOpenApiDocument(document)) throw new Error('不是有效的 OpenAPI/Swagger 文档');
|
|
102
|
+
const existing = versionId ? await readCachedDocument(versionId) : undefined;
|
|
103
|
+
const savedAt = new Date();
|
|
104
|
+
const nextVersionId = existing?.meta?.versionId || `${formatVersionDate(savedAt)}-${mode}-${slugify(sourceName(resolvedUrl || inputUrl))}`;
|
|
105
|
+
return writeVersion(document, {
|
|
106
|
+
...(existing?.meta ?? {}),
|
|
107
|
+
savedPath: existing?.meta?.savedPath,
|
|
108
|
+
versionId: nextVersionId,
|
|
109
|
+
mode,
|
|
110
|
+
inputUrl,
|
|
111
|
+
resolvedUrl: resolvedUrl || inputUrl,
|
|
112
|
+
title: document.info?.title || '',
|
|
113
|
+
version: document.info?.version || '',
|
|
114
|
+
savedAt: savedAt.toISOString(),
|
|
115
|
+
});
|
|
111
116
|
});
|
|
112
117
|
}
|
|
113
118
|
|
|
@@ -118,50 +123,61 @@ export async function createBlankDocument({
|
|
|
118
123
|
environmentName = '',
|
|
119
124
|
environmentBaseUrl = '',
|
|
120
125
|
} = {}) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
126
|
+
return withCacheWriteLock(async () => {
|
|
127
|
+
const document = createManualDocument(text(title) || 'API Skill Document', text(version) || '1.0.0', text(description));
|
|
128
|
+
const savedAt = new Date();
|
|
129
|
+
const versionId = `${formatVersionDate(savedAt)}-document-${slugify(document.info?.title || 'api-skill-document')}`;
|
|
130
|
+
return writeVersion(document, {
|
|
131
|
+
versionId,
|
|
132
|
+
mode: 'document',
|
|
133
|
+
inputUrl: 'manual-document',
|
|
134
|
+
resolvedUrl: 'manual-document',
|
|
135
|
+
title: document.info?.title || '',
|
|
136
|
+
version: document.info?.version || '',
|
|
137
|
+
environmentName: text(environmentName).slice(0, 80),
|
|
138
|
+
environmentBaseUrl: text(environmentBaseUrl),
|
|
139
|
+
savedAt: savedAt.toISOString(),
|
|
140
|
+
});
|
|
134
141
|
});
|
|
135
142
|
}
|
|
136
143
|
|
|
137
144
|
export async function saveManualOperation({ versionId, config, replaceTarget }) {
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
145
|
+
return withCacheWriteLock(async () => {
|
|
146
|
+
const normalized = normalizeManualConfig(config);
|
|
147
|
+
const existing = versionId ? await readCachedDocument(versionId) : await readLatestCachedDocumentIfExists();
|
|
148
|
+
let document = existing?.document ?? createManualDocument();
|
|
149
|
+
if (replaceTarget?.method && replaceTarget?.path) {
|
|
150
|
+
document = deleteOperation(document, replaceTarget.method, replaceTarget.path);
|
|
151
|
+
}
|
|
152
|
+
document = applyOperation(document, normalized);
|
|
153
|
+
const savedAt = new Date();
|
|
154
|
+
const nextVersionId = existing?.meta?.versionId || `${formatVersionDate(savedAt)}-manual-${slugify(normalized.operationId || normalized.summary || normalized.path)}`;
|
|
155
|
+
return writeVersion(document, {
|
|
156
|
+
...(existing?.meta ?? {}),
|
|
157
|
+
versionId: nextVersionId,
|
|
158
|
+
mode: existing?.meta?.mode || 'manual',
|
|
159
|
+
inputUrl: existing?.meta?.inputUrl || 'manual-api-config',
|
|
160
|
+
resolvedUrl: existing?.meta?.resolvedUrl || 'manual-api-config',
|
|
161
|
+
title: document.info?.title || 'Manual API Config',
|
|
162
|
+
version: document.info?.version || 'manual',
|
|
163
|
+
savedAt: savedAt.toISOString(),
|
|
164
|
+
});
|
|
156
165
|
});
|
|
157
166
|
}
|
|
158
167
|
|
|
168
|
+
async function readLatestCachedDocumentIfExists() {
|
|
169
|
+
if (!existsSync(latestMetaPath) && !existsSync(legacyCachePath)) return undefined;
|
|
170
|
+
return readCachedDocument();
|
|
171
|
+
}
|
|
172
|
+
|
|
159
173
|
export async function deleteManualOperation({ versionId, method, path }) {
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
174
|
+
return withCacheWriteLock(async () => {
|
|
175
|
+
const existing = await readCachedDocument(required(versionId, '必须指定 --version'));
|
|
176
|
+
const document = deleteOperation(existing.document, method, path);
|
|
177
|
+
return writeVersion(document, {
|
|
178
|
+
...existing.meta,
|
|
179
|
+
savedAt: new Date().toISOString(),
|
|
180
|
+
});
|
|
165
181
|
});
|
|
166
182
|
}
|
|
167
183
|
|
|
@@ -494,6 +510,81 @@ async function writeVersion(document, metaInput) {
|
|
|
494
510
|
return { document, meta };
|
|
495
511
|
}
|
|
496
512
|
|
|
513
|
+
async function withCacheWriteLock(task) {
|
|
514
|
+
await mkdir(cacheDir, { recursive: true });
|
|
515
|
+
const startedAt = Date.now();
|
|
516
|
+
const token = `${process.pid}-${startedAt}-${Math.random().toString(36).slice(2)}`;
|
|
517
|
+
|
|
518
|
+
while (true) {
|
|
519
|
+
try {
|
|
520
|
+
const handle = await open(writeLockPath, 'wx');
|
|
521
|
+
let initialized = false;
|
|
522
|
+
try {
|
|
523
|
+
await handle.writeFile(JSON.stringify({ pid: process.pid, token, createdAt: new Date().toISOString() }));
|
|
524
|
+
initialized = true;
|
|
525
|
+
} finally {
|
|
526
|
+
await handle.close().catch(() => {});
|
|
527
|
+
if (!initialized) await unlink(writeLockPath).catch(() => {});
|
|
528
|
+
}
|
|
529
|
+
break;
|
|
530
|
+
} catch (error) {
|
|
531
|
+
if (error?.code !== 'EEXIST') throw error;
|
|
532
|
+
await removeAbandonedWriteLock();
|
|
533
|
+
if (Date.now() - startedAt >= writeLockTimeoutMs) {
|
|
534
|
+
throw new Error('等待缓存写入锁超时,请确认没有卡住的 API Skill 进程后重试');
|
|
535
|
+
}
|
|
536
|
+
await sleep(15 + Math.floor(Math.random() * 25));
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
try {
|
|
541
|
+
return await task();
|
|
542
|
+
} finally {
|
|
543
|
+
await releaseWriteLock(token);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
async function removeAbandonedWriteLock() {
|
|
548
|
+
let lock;
|
|
549
|
+
try {
|
|
550
|
+
lock = JSON.parse(await readFile(writeLockPath, 'utf8'));
|
|
551
|
+
} catch (error) {
|
|
552
|
+
if (error?.code === 'ENOENT') return;
|
|
553
|
+
try {
|
|
554
|
+
const lockStat = await stat(writeLockPath);
|
|
555
|
+
if (Date.now() - lockStat.mtimeMs >= malformedLockStaleMs) await unlink(writeLockPath);
|
|
556
|
+
} catch {}
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if (isProcessAlive(lock?.pid)) return;
|
|
561
|
+
try {
|
|
562
|
+
const current = JSON.parse(await readFile(writeLockPath, 'utf8'));
|
|
563
|
+
if (current?.token === lock?.token) await unlink(writeLockPath);
|
|
564
|
+
} catch {}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async function releaseWriteLock(token) {
|
|
568
|
+
try {
|
|
569
|
+
const current = JSON.parse(await readFile(writeLockPath, 'utf8'));
|
|
570
|
+
if (current?.token === token) await unlink(writeLockPath);
|
|
571
|
+
} catch {}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
function isProcessAlive(pid) {
|
|
575
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
576
|
+
try {
|
|
577
|
+
process.kill(pid, 0);
|
|
578
|
+
return true;
|
|
579
|
+
} catch (error) {
|
|
580
|
+
return error?.code === 'EPERM';
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function sleep(milliseconds) {
|
|
585
|
+
return new Promise((resolvePromise) => setTimeout(resolvePromise, milliseconds));
|
|
586
|
+
}
|
|
587
|
+
|
|
497
588
|
function assertSafeVersionId(versionId) {
|
|
498
589
|
if (!/^[a-zA-Z0-9_.-]+$/.test(versionId)) throw new Error('versionId 不合法');
|
|
499
590
|
}
|
package/scripts/mcp-server.mjs
CHANGED
|
@@ -178,11 +178,11 @@ const tools = [
|
|
|
178
178
|
},
|
|
179
179
|
{
|
|
180
180
|
name: 'apiskill_create_api',
|
|
181
|
-
description: 'Create a manual API operation. If versionId is omitted, this
|
|
181
|
+
description: 'Create a manual API operation. If versionId is omitted, this appends to the latest version or creates a manual version when the cache is empty.',
|
|
182
182
|
inputSchema: {
|
|
183
183
|
type: 'object',
|
|
184
184
|
properties: {
|
|
185
|
-
versionId: { type: 'string', description: 'Optional version id
|
|
185
|
+
versionId: { type: 'string', description: 'Optional version id. Defaults to latest, or a new manual version when the cache is empty.' },
|
|
186
186
|
configText: { type: 'string', description: 'JSON/YAML/CLI config text. Use root key api, config, or operation.' },
|
|
187
187
|
config: { type: 'object', description: 'Manual API config object.' },
|
|
188
188
|
},
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
3
|
+
import { tmpdir } from 'node:os';
|
|
4
|
+
import { dirname, join, resolve } from 'node:path';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
import { spawn } from 'node:child_process';
|
|
7
|
+
|
|
8
|
+
const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
9
|
+
const cliPath = resolve(projectRoot, 'scripts/apiskill-cli.mjs');
|
|
10
|
+
const cacheDir = await mkdtemp(join(tmpdir(), 'apiskill-cli-concurrency-'));
|
|
11
|
+
const endpointCount = 16;
|
|
12
|
+
const implicitVersionEndpointCount = 8;
|
|
13
|
+
|
|
14
|
+
try {
|
|
15
|
+
const created = JSON.parse(
|
|
16
|
+
await runCli(['document', 'create', '--title', 'Concurrent CLI Test', '--doc-version', '1.0.0', '--json']),
|
|
17
|
+
);
|
|
18
|
+
const versionId = created.meta.versionId;
|
|
19
|
+
|
|
20
|
+
await Promise.all(
|
|
21
|
+
Array.from({ length: endpointCount }, (_, index) => {
|
|
22
|
+
const number = index + 1;
|
|
23
|
+
return runCli([
|
|
24
|
+
'api',
|
|
25
|
+
'create',
|
|
26
|
+
'--version',
|
|
27
|
+
versionId,
|
|
28
|
+
'--config',
|
|
29
|
+
JSON.stringify({
|
|
30
|
+
api: {
|
|
31
|
+
method: 'get',
|
|
32
|
+
path: `/api/v1/concurrent/${number}`,
|
|
33
|
+
summary: `Concurrent endpoint ${number}`,
|
|
34
|
+
responses: [{ status: '200', description: 'Success' }],
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
'--json',
|
|
38
|
+
]);
|
|
39
|
+
}),
|
|
40
|
+
);
|
|
41
|
+
|
|
42
|
+
const endpoints = JSON.parse(await runCli(['api', 'list', '--version', versionId, '--json']));
|
|
43
|
+
assert.equal(endpoints.length, endpointCount, `expected ${endpointCount} endpoints, received ${endpoints.length}`);
|
|
44
|
+
assert.deepEqual(
|
|
45
|
+
new Set(endpoints.map((endpoint) => endpoint.path)),
|
|
46
|
+
new Set(Array.from({ length: endpointCount }, (_, index) => `/api/v1/concurrent/${index + 1}`)),
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const implicitCreated = JSON.parse(
|
|
50
|
+
await runCli(['document', 'create', '--title', 'Implicit Latest Version Test', '--doc-version', '1.0.0', '--json']),
|
|
51
|
+
);
|
|
52
|
+
const implicitVersionId = implicitCreated.meta.versionId;
|
|
53
|
+
await Promise.all(
|
|
54
|
+
Array.from({ length: implicitVersionEndpointCount }, (_, index) => {
|
|
55
|
+
const number = index + 1;
|
|
56
|
+
return runCli([
|
|
57
|
+
'api',
|
|
58
|
+
'create',
|
|
59
|
+
'--config',
|
|
60
|
+
JSON.stringify({
|
|
61
|
+
api: {
|
|
62
|
+
method: 'post',
|
|
63
|
+
path: `/api/v1/implicit/${number}`,
|
|
64
|
+
summary: `Implicit latest endpoint ${number}`,
|
|
65
|
+
responses: [{ status: '201', description: 'Created' }],
|
|
66
|
+
},
|
|
67
|
+
}),
|
|
68
|
+
'--json',
|
|
69
|
+
]);
|
|
70
|
+
}),
|
|
71
|
+
);
|
|
72
|
+
const implicitEndpoints = JSON.parse(await runCli(['api', 'list', '--version', implicitVersionId, '--json']));
|
|
73
|
+
assert.equal(
|
|
74
|
+
implicitEndpoints.length,
|
|
75
|
+
implicitVersionEndpointCount,
|
|
76
|
+
`expected ${implicitVersionEndpointCount} implicit-version endpoints, received ${implicitEndpoints.length}`,
|
|
77
|
+
);
|
|
78
|
+
assert.ok(implicitEndpoints.every((endpoint) => endpoint.path.startsWith('/api/v1/implicit/')));
|
|
79
|
+
|
|
80
|
+
console.log(
|
|
81
|
+
`CLI concurrency test passed: retained ${endpointCount}/${endpointCount} explicit-version and ${implicitVersionEndpointCount}/${implicitVersionEndpointCount} latest-version parallel writes.`,
|
|
82
|
+
);
|
|
83
|
+
} finally {
|
|
84
|
+
await rm(cacheDir, { recursive: true, force: true });
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function runCli(args) {
|
|
88
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
89
|
+
const child = spawn(process.execPath, [cliPath, ...args], {
|
|
90
|
+
cwd: projectRoot,
|
|
91
|
+
env: { ...process.env, APISKILL_CACHE_DIR: cacheDir },
|
|
92
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
93
|
+
});
|
|
94
|
+
let stdout = '';
|
|
95
|
+
let stderr = '';
|
|
96
|
+
child.stdout.setEncoding('utf8');
|
|
97
|
+
child.stderr.setEncoding('utf8');
|
|
98
|
+
child.stdout.on('data', (chunk) => {
|
|
99
|
+
stdout += chunk;
|
|
100
|
+
});
|
|
101
|
+
child.stderr.on('data', (chunk) => {
|
|
102
|
+
stderr += chunk;
|
|
103
|
+
});
|
|
104
|
+
child.on('error', rejectPromise);
|
|
105
|
+
child.on('close', (code) => {
|
|
106
|
+
if (code === 0) resolvePromise(stdout);
|
|
107
|
+
else rejectPromise(new Error(`apiskill ${args.join(' ')} failed (${code}): ${stderr || stdout}`));
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
package/tsconfig.json
CHANGED
|
@@ -10,12 +10,12 @@
|
|
|
10
10
|
"strict": true,
|
|
11
11
|
"forceConsistentCasingInFileNames": true,
|
|
12
12
|
"module": "ESNext",
|
|
13
|
-
"moduleResolution": "
|
|
13
|
+
"moduleResolution": "bundler", // ✅ 改为 bundler
|
|
14
14
|
"resolveJsonModule": true,
|
|
15
15
|
"isolatedModules": true,
|
|
16
16
|
"noEmit": true,
|
|
17
17
|
"jsx": "react-jsx"
|
|
18
18
|
},
|
|
19
|
-
"include": ["src"],
|
|
19
|
+
"include": ["src", "vite-env.d.ts"],
|
|
20
20
|
"references": []
|
|
21
|
-
}
|
|
21
|
+
}
|