apiskill 0.1.4 → 0.1.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/README.md +1 -1
- package/README.zh.md +1 -1
- package/docs/cli.md +3 -2
- package/docs/cli.zh.md +3 -2
- package/docs/mcp.ja.md +2 -4
- package/docs/mcp.ko.md +2 -4
- package/docs/mcp.md +2 -4
- package/docs/mcp.zh.md +2 -4
- package/docs/web.md +1 -1
- package/docs/web.zh.md +1 -1
- package/mcp-config.example.json +1 -5
- package/package.json +2 -1
- package/scripts/apiskill-cli.mjs +2 -7
- package/scripts/lib/apiskill-core.mjs +6 -0
- package/scripts/lib/cache-paths.mjs +0 -1
- package/scripts/test-shared-cache.mjs +140 -0
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ npm install -g apiskill
|
|
|
17
17
|
apiskill run web
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Web, CLI, and MCP all use the same user-writable `~/.apiskill/cache` directory by default. `--cwd` only changes the Web process working directory and never changes this shared cache. For an isolated cache, set the same absolute `APISKILL_CACHE_DIR` for every Web, CLI, and MCP process. When developing this repository, you can also start it from the project root:
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
npm install
|
package/README.zh.md
CHANGED
|
@@ -17,7 +17,7 @@ npm install -g apiskill
|
|
|
17
17
|
apiskill run web
|
|
18
18
|
```
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
Web、CLI 和 MCP 默认统一使用用户可写的 `~/.apiskill/cache`,共同读写同一份文档和版本。`--cwd` 只改变 Web 进程的工作目录,不再改变共享缓存。需要隔离缓存时,必须为 Web、CLI 和 MCP 设置完全相同的绝对路径 `APISKILL_CACHE_DIR`。开发本仓库时也可以在项目根目录启动:
|
|
21
21
|
|
|
22
22
|
```bash
|
|
23
23
|
npm install
|
package/docs/cli.md
CHANGED
|
@@ -14,11 +14,12 @@ apiskill --help
|
|
|
14
14
|
apiskill run web
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
`apiskill run web
|
|
17
|
+
`apiskill run web`, CLI commands, and MCP all use `~/.apiskill/cache` by default. `--cwd` changes only the Web working directory and does not change the shared cache. For project isolation, set the same absolute `APISKILL_CACHE_DIR` for every Web, CLI, and MCP process:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
apiskill run web --port 8890
|
|
21
|
-
apiskill run web --cwd /path/to/project
|
|
21
|
+
APISKILL_CACHE_DIR=/path/to/project/.apiskill-cache apiskill run web --cwd /path/to/project
|
|
22
|
+
APISKILL_CACHE_DIR=/path/to/project/.apiskill-cache apiskill check --json
|
|
22
23
|
```
|
|
23
24
|
|
|
24
25
|
From the source project root:
|
package/docs/cli.zh.md
CHANGED
|
@@ -14,11 +14,12 @@ apiskill --help
|
|
|
14
14
|
apiskill run web
|
|
15
15
|
```
|
|
16
16
|
|
|
17
|
-
`apiskill run web
|
|
17
|
+
`apiskill run web`、其他 CLI 命令和 MCP 默认统一使用 `~/.apiskill/cache`。`--cwd` 只改变 Web 工作目录,不会改变共享缓存。需要按项目隔离时,必须为 Web、CLI 和 MCP 设置相同的绝对路径 `APISKILL_CACHE_DIR`:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
20
|
apiskill run web --port 8890
|
|
21
|
-
apiskill run web --cwd /path/to/project
|
|
21
|
+
APISKILL_CACHE_DIR=/path/to/project/.apiskill-cache apiskill run web --cwd /path/to/project
|
|
22
|
+
APISKILL_CACHE_DIR=/path/to/project/.apiskill-cache apiskill check --json
|
|
22
23
|
```
|
|
23
24
|
|
|
24
25
|
在源码项目根目录运行:
|
package/docs/mcp.ja.md
CHANGED
|
@@ -20,12 +20,10 @@ cwd = "/Users/dobby/dev/apiskill"
|
|
|
20
20
|
startup_timeout_sec = 10
|
|
21
21
|
tool_timeout_sec = 60
|
|
22
22
|
enabled = true
|
|
23
|
-
|
|
24
|
-
[mcp_servers.apiskill.env]
|
|
25
|
-
APISKILL_ROOT = "/Users/dobby/dev/apiskill"
|
|
26
|
-
APISKILL_CACHE_DIR = "/Users/dobby/dev/apiskill/cache"
|
|
27
23
|
```
|
|
28
24
|
|
|
25
|
+
キャッシュ環境変数を設定しない場合、MCP、Web、CLI は同じ `~/.apiskill/cache` を使用します。プロジェクトごとに分離する場合は、3 つのプロセスすべてに同じ絶対パスの `APISKILL_CACHE_DIR` を設定してください。
|
|
26
|
+
|
|
29
27
|
Codex を再起動して `/mcp` を実行します。`apiskill` と以下のツールが表示されるはずです。
|
|
30
28
|
|
|
31
29
|
## 読み取りツール
|
package/docs/mcp.ko.md
CHANGED
|
@@ -20,12 +20,10 @@ cwd = "/Users/dobby/dev/apiskill"
|
|
|
20
20
|
startup_timeout_sec = 10
|
|
21
21
|
tool_timeout_sec = 60
|
|
22
22
|
enabled = true
|
|
23
|
-
|
|
24
|
-
[mcp_servers.apiskill.env]
|
|
25
|
-
APISKILL_ROOT = "/Users/dobby/dev/apiskill"
|
|
26
|
-
APISKILL_CACHE_DIR = "/Users/dobby/dev/apiskill/cache"
|
|
27
23
|
```
|
|
28
24
|
|
|
25
|
+
캐시 환경 변수를 설정하지 않으면 MCP, Web, CLI는 모두 같은 `~/.apiskill/cache`를 사용합니다. 프로젝트별로 분리할 때는 세 프로세스에 동일한 절대 경로의 `APISKILL_CACHE_DIR`를 설정하세요.
|
|
26
|
+
|
|
29
27
|
Codex를 재시작하고 `/mcp`를 실행합니다. `apiskill`과 아래 도구들이 보여야 합니다.
|
|
30
28
|
|
|
31
29
|
## 읽기 도구
|
package/docs/mcp.md
CHANGED
|
@@ -22,12 +22,10 @@ cwd = "/Users/dobby/dev/apiskill"
|
|
|
22
22
|
startup_timeout_sec = 10
|
|
23
23
|
tool_timeout_sec = 60
|
|
24
24
|
enabled = true
|
|
25
|
-
|
|
26
|
-
[mcp_servers.apiskill.env]
|
|
27
|
-
APISKILL_ROOT = "/Users/dobby/dev/apiskill"
|
|
28
|
-
APISKILL_CACHE_DIR = "/Users/dobby/dev/apiskill/cache"
|
|
29
25
|
```
|
|
30
26
|
|
|
27
|
+
With no cache environment configured, MCP uses the same `~/.apiskill/cache` default as Web and CLI. For project isolation, set one absolute `APISKILL_CACHE_DIR` value and use that exact value for all three processes.
|
|
28
|
+
|
|
31
29
|
Restart Codex and run `/mcp`. You should see `apiskill` with the tools below.
|
|
32
30
|
|
|
33
31
|
## Read Tools
|
package/docs/mcp.zh.md
CHANGED
|
@@ -22,12 +22,10 @@ cwd = "/Users/dobby/dev/apiskill"
|
|
|
22
22
|
startup_timeout_sec = 10
|
|
23
23
|
tool_timeout_sec = 60
|
|
24
24
|
enabled = true
|
|
25
|
-
|
|
26
|
-
[mcp_servers.apiskill.env]
|
|
27
|
-
APISKILL_ROOT = "/Users/dobby/dev/apiskill"
|
|
28
|
-
APISKILL_CACHE_DIR = "/Users/dobby/dev/apiskill/cache"
|
|
29
25
|
```
|
|
30
26
|
|
|
27
|
+
不配置缓存环境变量时,MCP 与 Web、CLI 一样默认使用 `~/.apiskill/cache`。需要按项目隔离时,设置一个绝对路径 `APISKILL_CACHE_DIR`,并确保三个进程使用完全相同的值。
|
|
28
|
+
|
|
31
29
|
重启 Codex 后运行 `/mcp`,应该能看到 `apiskill` 和下面的工具。
|
|
32
30
|
|
|
33
31
|
## 只读工具
|
package/docs/web.md
CHANGED
|
@@ -11,7 +11,7 @@ npm install -g apiskill
|
|
|
11
11
|
apiskill run web
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Web, CLI, and MCP use the same user-writable `~/.apiskill/cache` directory by default. `--cwd` changes only the working directory. For an isolated cache, set the same absolute `APISKILL_CACHE_DIR` for all three processes. When developing this repository, you can also use:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
npm install
|
package/docs/web.zh.md
CHANGED
|
@@ -11,7 +11,7 @@ npm install -g apiskill
|
|
|
11
11
|
apiskill run web
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
Web、CLI 和 MCP 默认统一使用用户可写的 `~/.apiskill/cache`,共享同一份文档和版本。`--cwd` 只改变工作目录。需要隔离缓存时,必须为三个进程设置相同的绝对路径 `APISKILL_CACHE_DIR`。开发本仓库时也可以使用:
|
|
15
15
|
|
|
16
16
|
```bash
|
|
17
17
|
npm install
|
package/mcp-config.example.json
CHANGED
|
@@ -3,11 +3,7 @@
|
|
|
3
3
|
"apiskill": {
|
|
4
4
|
"command": "node",
|
|
5
5
|
"args": ["/Users/dobby/dev/apiskill/scripts/mcp-server.mjs"],
|
|
6
|
-
"cwd": "/Users/dobby/dev/apiskill"
|
|
7
|
-
"env": {
|
|
8
|
-
"APISKILL_ROOT": "/Users/dobby/dev/apiskill",
|
|
9
|
-
"APISKILL_CACHE_DIR": "/Users/dobby/dev/apiskill/cache"
|
|
10
|
-
}
|
|
6
|
+
"cwd": "/Users/dobby/dev/apiskill"
|
|
11
7
|
}
|
|
12
8
|
}
|
|
13
9
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "apiskill",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"files": [
|
|
6
6
|
"src",
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
"dev": "vite --host 0.0.0.0 && node scripts/mcp-server.mjs",
|
|
23
23
|
"build": "tsc -b && vite build",
|
|
24
24
|
"test:cli-concurrency": "node scripts/test-cli-concurrency.mjs",
|
|
25
|
+
"test:shared-cache": "node scripts/test-shared-cache.mjs",
|
|
25
26
|
"prepack": "npm run build",
|
|
26
27
|
"preview": "vite preview --host 0.0.0.0",
|
|
27
28
|
"mcp": "node scripts/mcp-server.mjs",
|
package/scripts/apiskill-cli.mjs
CHANGED
|
@@ -67,8 +67,7 @@ run
|
|
|
67
67
|
.option('-p, --port <number>', 'Web server port', '8888')
|
|
68
68
|
.option('--host <host>', 'Host to bind', '127.0.0.1')
|
|
69
69
|
.option('--strict-port', 'Fail if the requested port is already in use')
|
|
70
|
-
.option('--cwd <dir>', 'Project directory.
|
|
71
|
-
.option('--cache-dir <dir>', 'Cache directory. Defaults to ~/.apiskill/cache')
|
|
70
|
+
.option('--cwd <dir>', 'Project working directory. Does not change the shared cache directory')
|
|
72
71
|
.action(async (options) => {
|
|
73
72
|
await runWeb(options);
|
|
74
73
|
});
|
|
@@ -297,11 +296,7 @@ function registerMockCommand(command) {
|
|
|
297
296
|
|
|
298
297
|
async function runWeb(options) {
|
|
299
298
|
const projectRoot = resolve(options.cwd || process.cwd());
|
|
300
|
-
const cacheDir =
|
|
301
|
-
? resolve(projectRoot, options.cacheDir)
|
|
302
|
-
: options.cwd
|
|
303
|
-
? resolve(projectRoot, 'cache')
|
|
304
|
-
: getDefaultCacheDir();
|
|
299
|
+
const cacheDir = getDefaultCacheDir();
|
|
305
300
|
const port = Number(options.port) || 8888;
|
|
306
301
|
const host = options.host || '127.0.0.1';
|
|
307
302
|
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
saveManualOperation,
|
|
14
14
|
} from './openapi-store.mjs';
|
|
15
15
|
import { crawlOpenApi, importFromCurl, importFromLocalFile, importFromUrl } from './openapi-importer.mjs';
|
|
16
|
+
import { getDefaultCacheDir } from './cache-paths.mjs';
|
|
16
17
|
|
|
17
18
|
const MAX_DEPTH = 12;
|
|
18
19
|
|
|
@@ -37,6 +38,7 @@ export const importExamples = {
|
|
|
37
38
|
};
|
|
38
39
|
|
|
39
40
|
export async function checkCache() {
|
|
41
|
+
const cacheDir = getDefaultCacheDir();
|
|
40
42
|
const versions = await listCachedVersions();
|
|
41
43
|
try {
|
|
42
44
|
const { document, meta } = await readCachedDocument();
|
|
@@ -46,6 +48,7 @@ export async function checkCache() {
|
|
|
46
48
|
return {
|
|
47
49
|
ok: false,
|
|
48
50
|
message: 'No usable cached OpenAPI document found. The latest cache exists, but it has no paths.',
|
|
51
|
+
cacheDir,
|
|
49
52
|
versionsCount: versions.length,
|
|
50
53
|
latestVersion: metaSummary(meta),
|
|
51
54
|
importExamples,
|
|
@@ -54,6 +57,7 @@ export async function checkCache() {
|
|
|
54
57
|
return {
|
|
55
58
|
ok: true,
|
|
56
59
|
message: 'API Skill cache is ready.',
|
|
60
|
+
cacheDir,
|
|
57
61
|
versionsCount: versions.length,
|
|
58
62
|
latestVersion: {
|
|
59
63
|
...metaSummary(meta),
|
|
@@ -66,6 +70,7 @@ export async function checkCache() {
|
|
|
66
70
|
return {
|
|
67
71
|
ok: false,
|
|
68
72
|
message: 'No usable cached OpenAPI document found. Import or crawl a document first.',
|
|
73
|
+
cacheDir,
|
|
69
74
|
error: error instanceof Error ? error.message : 'Unknown cache read error',
|
|
70
75
|
versionsCount: versions.length,
|
|
71
76
|
latestVersion: versions.find((version) => version.latest) ?? versions[0],
|
|
@@ -76,6 +81,7 @@ export async function checkCache() {
|
|
|
76
81
|
|
|
77
82
|
export function formatCheckText(result) {
|
|
78
83
|
const lines = [];
|
|
84
|
+
if (result.cacheDir) lines.push(`Cache: ${result.cacheDir}`);
|
|
79
85
|
if (result.ok) {
|
|
80
86
|
lines.push('OK: API Skill cache is ready.');
|
|
81
87
|
if (result.latestVersion?.versionId) lines.push(`Latest version: ${result.latestVersion.versionId}`);
|
|
@@ -3,6 +3,5 @@ import { resolve } from 'node:path';
|
|
|
3
3
|
|
|
4
4
|
export function getDefaultCacheDir(env = process.env) {
|
|
5
5
|
if (env.APISKILL_CACHE_DIR) return resolve(env.APISKILL_CACHE_DIR);
|
|
6
|
-
if (env.APISKILL_ROOT) return resolve(env.APISKILL_ROOT, 'cache');
|
|
7
6
|
return resolve(homedir(), '.apiskill', 'cache');
|
|
8
7
|
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { createServer } from 'node:net';
|
|
3
|
+
import { mkdtemp, rm } from 'node:fs/promises';
|
|
4
|
+
import { homedir, tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join, resolve } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
import { spawn } from 'node:child_process';
|
|
8
|
+
import { getDefaultCacheDir } from './lib/cache-paths.mjs';
|
|
9
|
+
|
|
10
|
+
const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
11
|
+
const cliPath = resolve(projectRoot, 'scripts/apiskill-cli.mjs');
|
|
12
|
+
const cacheDir = await mkdtemp(join(tmpdir(), 'apiskill-shared-cache-'));
|
|
13
|
+
const port = await findFreePort();
|
|
14
|
+
let webProcess;
|
|
15
|
+
|
|
16
|
+
try {
|
|
17
|
+
assert.equal(getDefaultCacheDir({ APISKILL_ROOT: '/tmp/another-project' }), resolve(homedir(), '.apiskill', 'cache'));
|
|
18
|
+
|
|
19
|
+
const created = JSON.parse(
|
|
20
|
+
await runCli(['document', 'create', '--title', 'Shared Cache Test', '--doc-version', '1.0.0', '--json']),
|
|
21
|
+
);
|
|
22
|
+
const versionId = created.meta.versionId;
|
|
23
|
+
await runCli([
|
|
24
|
+
'api',
|
|
25
|
+
'create',
|
|
26
|
+
'--version',
|
|
27
|
+
versionId,
|
|
28
|
+
'--config',
|
|
29
|
+
JSON.stringify({
|
|
30
|
+
api: {
|
|
31
|
+
method: 'get',
|
|
32
|
+
path: '/api/v1/from-cli',
|
|
33
|
+
summary: 'Created from CLI',
|
|
34
|
+
responses: [{ status: '200', description: 'Success' }],
|
|
35
|
+
},
|
|
36
|
+
}),
|
|
37
|
+
'--json',
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
webProcess = spawn(
|
|
41
|
+
process.execPath,
|
|
42
|
+
[cliPath, 'run', 'web', '--host', '127.0.0.1', '--port', String(port), '--strict-port', '--cwd', projectRoot],
|
|
43
|
+
{
|
|
44
|
+
cwd: projectRoot,
|
|
45
|
+
env: { ...process.env, APISKILL_CACHE_DIR: cacheDir },
|
|
46
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
47
|
+
},
|
|
48
|
+
);
|
|
49
|
+
const webOutput = collectProcessOutput(webProcess);
|
|
50
|
+
await waitForWeb(port, webProcess, webOutput);
|
|
51
|
+
|
|
52
|
+
const webCacheResponse = await fetch(`http://127.0.0.1:${port}/api/openapi/cache`);
|
|
53
|
+
assert.equal(webCacheResponse.status, 200);
|
|
54
|
+
const webCache = await webCacheResponse.json();
|
|
55
|
+
assert.equal(webCache.meta.versionId, versionId);
|
|
56
|
+
assert.ok(webCache.document.paths['/api/v1/from-cli']?.get, 'Web did not read the API created by CLI');
|
|
57
|
+
|
|
58
|
+
const webWriteResponse = await fetch(`http://127.0.0.1:${port}/api/openapi/custom-operation`, {
|
|
59
|
+
method: 'POST',
|
|
60
|
+
headers: { 'content-type': 'application/json' },
|
|
61
|
+
body: JSON.stringify({
|
|
62
|
+
versionId,
|
|
63
|
+
config: {
|
|
64
|
+
method: 'post',
|
|
65
|
+
path: '/api/v1/from-web',
|
|
66
|
+
summary: 'Created from Web',
|
|
67
|
+
responses: [{ status: '201', description: 'Created' }],
|
|
68
|
+
},
|
|
69
|
+
}),
|
|
70
|
+
});
|
|
71
|
+
assert.equal(webWriteResponse.status, 200, await webWriteResponse.text());
|
|
72
|
+
|
|
73
|
+
const cliRead = JSON.parse(await runCli(['api', 'query', 'POST', '/api/v1/from-web', '--version', versionId]));
|
|
74
|
+
assert.equal(cliRead.path, '/api/v1/from-web');
|
|
75
|
+
assert.equal(cliRead.method, 'post');
|
|
76
|
+
|
|
77
|
+
const check = JSON.parse(await runCli(['check', '--json']));
|
|
78
|
+
assert.equal(check.cacheDir, cacheDir);
|
|
79
|
+
console.log(`Shared cache test passed: CLI and Web both read and wrote ${cacheDir}.`);
|
|
80
|
+
} finally {
|
|
81
|
+
if (webProcess && webProcess.exitCode === null) {
|
|
82
|
+
webProcess.kill('SIGTERM');
|
|
83
|
+
await new Promise((resolvePromise) => webProcess.once('close', resolvePromise));
|
|
84
|
+
}
|
|
85
|
+
await rm(cacheDir, { recursive: true, force: true });
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function runCli(args) {
|
|
89
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
90
|
+
const child = spawn(process.execPath, [cliPath, ...args], {
|
|
91
|
+
cwd: projectRoot,
|
|
92
|
+
env: { ...process.env, APISKILL_CACHE_DIR: cacheDir },
|
|
93
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
94
|
+
});
|
|
95
|
+
const output = collectProcessOutput(child);
|
|
96
|
+
child.on('error', rejectPromise);
|
|
97
|
+
child.on('close', (code) => {
|
|
98
|
+
if (code === 0) resolvePromise(output.stdout);
|
|
99
|
+
else rejectPromise(new Error(`apiskill ${args.join(' ')} failed (${code}): ${output.stderr || output.stdout}`));
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function collectProcessOutput(child) {
|
|
105
|
+
const output = { stdout: '', stderr: '' };
|
|
106
|
+
child.stdout.setEncoding('utf8');
|
|
107
|
+
child.stderr.setEncoding('utf8');
|
|
108
|
+
child.stdout.on('data', (chunk) => {
|
|
109
|
+
output.stdout += chunk;
|
|
110
|
+
});
|
|
111
|
+
child.stderr.on('data', (chunk) => {
|
|
112
|
+
output.stderr += chunk;
|
|
113
|
+
});
|
|
114
|
+
return output;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
async function waitForWeb(webPort, child, output) {
|
|
118
|
+
const deadline = Date.now() + 15_000;
|
|
119
|
+
while (Date.now() < deadline) {
|
|
120
|
+
if (child.exitCode !== null) throw new Error(`Web process exited early (${child.exitCode}): ${output.stderr || output.stdout}`);
|
|
121
|
+
try {
|
|
122
|
+
const response = await fetch(`http://127.0.0.1:${webPort}/api/openapi/versions`);
|
|
123
|
+
if (response.ok) return;
|
|
124
|
+
} catch {}
|
|
125
|
+
await new Promise((resolvePromise) => setTimeout(resolvePromise, 100));
|
|
126
|
+
}
|
|
127
|
+
throw new Error(`Timed out waiting for Web: ${output.stderr || output.stdout}`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function findFreePort() {
|
|
131
|
+
return new Promise((resolvePromise, rejectPromise) => {
|
|
132
|
+
const server = createServer();
|
|
133
|
+
server.once('error', rejectPromise);
|
|
134
|
+
server.listen(0, '127.0.0.1', () => {
|
|
135
|
+
const address = server.address();
|
|
136
|
+
const freePort = typeof address === 'object' && address ? address.port : 0;
|
|
137
|
+
server.close((error) => (error ? rejectPromise(error) : resolvePromise(freePort)));
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
}
|