api-spec-cli 0.2.3 → 0.2.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 +390 -282
- package/package.json +55 -41
- package/src/cli.js +338 -172
- package/src/commands/add.js +163 -78
- package/src/commands/auth.js +32 -0
- package/src/commands/call.js +224 -207
- package/src/commands/fetch.js +63 -27
- package/src/commands/grep.js +14 -11
- package/src/commands/list.js +89 -80
- package/src/commands/show.js +16 -7
- package/src/commands/skill.js +40 -0
- package/src/commands/specs.js +22 -9
- package/src/commands/types.js +6 -2
- package/src/commands/usage.js +15 -0
- package/src/commands/validate.js +33 -7
- package/src/dotenv.js +38 -0
- package/src/glob.js +6 -1
- package/src/mcp-client.js +91 -63
- package/src/oauth/auth-flow.js +59 -0
- package/src/oauth/provider.js +191 -0
- package/src/oauth/tokens.js +59 -0
- package/src/output.js +65 -61
- package/src/registry.js +36 -10
- package/src/resolve.js +66 -65
- package/src/secrets.js +46 -0
- package/src/skill/SKILL.md +112 -0
- package/src/usage.js +62 -0
package/src/commands/grep.js
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
import { out } from "../output.js";
|
|
2
2
|
import { parseArgs } from "../args.js";
|
|
3
|
-
import { getRegistry, getEntry, getCachedSpec, saveCachedSpec } from "../registry.js";
|
|
3
|
+
import { getRegistry, getEntry, getCachedSpec, saveCachedSpec, allEntries } from "../registry.js";
|
|
4
4
|
import { fetchSpec } from "./fetch.js";
|
|
5
5
|
import { matchGlob } from "../glob.js";
|
|
6
6
|
|
|
7
7
|
export async function grepCmd(args) {
|
|
8
8
|
const { flags, positional } = parseArgs(args);
|
|
9
9
|
const pattern = positional[0];
|
|
10
|
-
if (!pattern)
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
if (!pattern)
|
|
11
|
+
throw new Error(
|
|
12
|
+
"Usage: spec grep <pattern> [--spec <name>]\n" +
|
|
13
|
+
" Glob patterns: * matches anything, ? matches one char\n" +
|
|
14
|
+
" Plain text: substring match across name and description"
|
|
15
|
+
);
|
|
15
16
|
|
|
16
17
|
const entries = flags.spec
|
|
17
18
|
? [getEntry(flags.spec)]
|
|
18
|
-
: getRegistry().filter((e) => e.enabled);
|
|
19
|
+
: allEntries(getRegistry()).filter((e) => e.enabled);
|
|
19
20
|
|
|
20
21
|
if (entries.length === 0) throw new Error("No registered specs. Run 'spec add' first.");
|
|
21
22
|
|
|
@@ -40,15 +41,17 @@ export async function grepCmd(args) {
|
|
|
40
41
|
}
|
|
41
42
|
} else if (spec.type === "openapi") {
|
|
42
43
|
for (const op of spec.operations) {
|
|
43
|
-
if (
|
|
44
|
-
|
|
44
|
+
if (
|
|
45
|
+
matchGlob(pattern, op.id) ||
|
|
46
|
+
matchGlob(pattern, op.path) ||
|
|
47
|
+
(op.summary && matchGlob(pattern, op.summary))
|
|
48
|
+
) {
|
|
45
49
|
matches.push({ id: op.id, method: op.method, path: op.path });
|
|
46
50
|
}
|
|
47
51
|
}
|
|
48
52
|
} else if (spec.type === "graphql") {
|
|
49
53
|
for (const op of spec.operations) {
|
|
50
|
-
if (matchGlob(pattern, op.name) ||
|
|
51
|
-
(op.description && matchGlob(pattern, op.description))) {
|
|
54
|
+
if (matchGlob(pattern, op.name) || (op.description && matchGlob(pattern, op.description))) {
|
|
52
55
|
matches.push({ id: op.name, kind: op.kind });
|
|
53
56
|
}
|
|
54
57
|
}
|
package/src/commands/list.js
CHANGED
|
@@ -1,80 +1,89 @@
|
|
|
1
|
-
import { out } from "../output.js";
|
|
2
|
-
import { parseArgs } from "../args.js";
|
|
3
|
-
import { resolveSpec } from "../resolve.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
const total = operations.length;
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
operations
|
|
79
|
-
|
|
80
|
-
}
|
|
1
|
+
import { out } from "../output.js";
|
|
2
|
+
import { parseArgs } from "../args.js";
|
|
3
|
+
import { resolveSpec } from "../resolve.js";
|
|
4
|
+
import { getUsage } from "../usage.js";
|
|
5
|
+
|
|
6
|
+
export async function listOperations(args) {
|
|
7
|
+
const opts = parseArgs(args);
|
|
8
|
+
const { flags } = opts;
|
|
9
|
+
|
|
10
|
+
const { spec } = await resolveSpec(flags);
|
|
11
|
+
|
|
12
|
+
const filter = flags.filter?.toLowerCase();
|
|
13
|
+
const compact = flags.compact !== "false";
|
|
14
|
+
const limit = parseInt(flags.limit) || 0;
|
|
15
|
+
const offset = parseInt(flags.offset) || 0;
|
|
16
|
+
const tag = flags.tag?.toLowerCase();
|
|
17
|
+
const top = parseInt(flags.top) || 0;
|
|
18
|
+
|
|
19
|
+
let operations;
|
|
20
|
+
|
|
21
|
+
if (spec.type === "openapi") {
|
|
22
|
+
let source = spec.operations;
|
|
23
|
+
if (tag) {
|
|
24
|
+
source = source.filter((op) => op.tags?.some((t) => t.toLowerCase().includes(tag)));
|
|
25
|
+
}
|
|
26
|
+
operations = source.map((op) =>
|
|
27
|
+
compact
|
|
28
|
+
? { id: op.id, method: op.method, path: op.path }
|
|
29
|
+
: {
|
|
30
|
+
id: op.id,
|
|
31
|
+
method: op.method,
|
|
32
|
+
path: op.path,
|
|
33
|
+
summary: op.summary,
|
|
34
|
+
tags: op.tags,
|
|
35
|
+
deprecated: op.deprecated,
|
|
36
|
+
}
|
|
37
|
+
);
|
|
38
|
+
} else if (spec.type === "mcp") {
|
|
39
|
+
operations = spec.tools.map((t) =>
|
|
40
|
+
compact
|
|
41
|
+
? { id: t.name, description: t.description }
|
|
42
|
+
: { id: t.name, description: t.description, inputSchema: t.inputSchema }
|
|
43
|
+
);
|
|
44
|
+
} else {
|
|
45
|
+
// graphql
|
|
46
|
+
operations = spec.operations.map((op) =>
|
|
47
|
+
compact
|
|
48
|
+
? { id: op.name, kind: op.kind }
|
|
49
|
+
: {
|
|
50
|
+
id: op.name,
|
|
51
|
+
kind: op.kind,
|
|
52
|
+
description: op.description,
|
|
53
|
+
args: op.args.map((a) => a.name),
|
|
54
|
+
returnType: op.returnType,
|
|
55
|
+
isDeprecated: op.isDeprecated,
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
if (tag) {
|
|
60
|
+
operations = operations.filter((op) => op.kind === tag);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (filter) {
|
|
65
|
+
operations = operations.filter((op) => JSON.stringify(op).toLowerCase().includes(filter));
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const total = operations.length;
|
|
69
|
+
|
|
70
|
+
if (top > 0) {
|
|
71
|
+
const usageMap = flags.spec ? getUsage(flags.spec) : {};
|
|
72
|
+
operations = operations
|
|
73
|
+
.map((op) => ({ op, count: usageMap[op.id]?.count ?? 0 }))
|
|
74
|
+
.sort((a, b) => b.count - a.count)
|
|
75
|
+
.map(({ op }) => op)
|
|
76
|
+
.slice(0, top);
|
|
77
|
+
} else {
|
|
78
|
+
if (offset > 0) operations = operations.slice(offset);
|
|
79
|
+
if (limit > 0) operations = operations.slice(0, limit);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
out({
|
|
83
|
+
type: spec.type,
|
|
84
|
+
total,
|
|
85
|
+
showing: operations.length,
|
|
86
|
+
offset: offset || 0,
|
|
87
|
+
operations,
|
|
88
|
+
});
|
|
89
|
+
}
|
package/src/commands/show.js
CHANGED
|
@@ -5,7 +5,10 @@ import { resolveSpec } from "../resolve.js";
|
|
|
5
5
|
export async function showOperation(args) {
|
|
6
6
|
const { flags, positional } = parseArgs(args);
|
|
7
7
|
const target = positional[0];
|
|
8
|
-
if (!target)
|
|
8
|
+
if (!target)
|
|
9
|
+
throw new Error(
|
|
10
|
+
"Usage: spec show <operationId-or-path> [--spec <name> | --openapi <url> | ...]"
|
|
11
|
+
);
|
|
9
12
|
|
|
10
13
|
const { spec } = await resolveSpec(flags);
|
|
11
14
|
|
|
@@ -21,10 +24,11 @@ export async function showOperation(args) {
|
|
|
21
24
|
function showOpenAPI(spec, target) {
|
|
22
25
|
const lower = target.toLowerCase();
|
|
23
26
|
|
|
24
|
-
const op = spec.operations.find(
|
|
25
|
-
o
|
|
26
|
-
|
|
27
|
-
|
|
27
|
+
const op = spec.operations.find(
|
|
28
|
+
(o) =>
|
|
29
|
+
o.id.toLowerCase() === lower ||
|
|
30
|
+
o.path.toLowerCase() === lower ||
|
|
31
|
+
`${o.method.toLowerCase()} ${o.path.toLowerCase()}` === lower
|
|
28
32
|
);
|
|
29
33
|
|
|
30
34
|
if (!op) {
|
|
@@ -195,8 +199,13 @@ function findRelatedTypes(op, types) {
|
|
|
195
199
|
.filter((t) => names.has(t.name) && !scalars.has(t.name))
|
|
196
200
|
.map((t) => {
|
|
197
201
|
const result = { name: t.name, kind: t.kind };
|
|
198
|
-
if (t.fields)
|
|
199
|
-
|
|
202
|
+
if (t.fields)
|
|
203
|
+
result.fields = t.fields.map((f) => ({ name: f.name, type: flattenType(f.type) }));
|
|
204
|
+
if (t.inputFields)
|
|
205
|
+
result.inputFields = t.inputFields.map((f) => ({
|
|
206
|
+
name: f.name,
|
|
207
|
+
type: flattenType(f.type),
|
|
208
|
+
}));
|
|
200
209
|
if (t.enumValues) result.enumValues = t.enumValues.map((e) => e.name);
|
|
201
210
|
return result;
|
|
202
211
|
});
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { homedir } from "os";
|
|
2
|
+
import { join, dirname } from "path";
|
|
3
|
+
import { fileURLToPath } from "url";
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import { parseArgs } from "../args.js";
|
|
6
|
+
import { out } from "../output.js";
|
|
7
|
+
|
|
8
|
+
const SKILL_NAME = "api-spec-cli";
|
|
9
|
+
const BUNDLED = join(dirname(fileURLToPath(import.meta.url)), "..", "skill", "SKILL.md");
|
|
10
|
+
|
|
11
|
+
function installDir() {
|
|
12
|
+
return join(homedir(), ".claude", "skills", SKILL_NAME);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export async function skillCmd(args) {
|
|
16
|
+
const { flags, positional } = parseArgs(args);
|
|
17
|
+
|
|
18
|
+
if (positional[0] === "path" || "path" in flags) {
|
|
19
|
+
out({ skill: SKILL_NAME, path: BUNDLED });
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
if (positional[0] === "install" || "install" in flags) {
|
|
24
|
+
if (!existsSync(BUNDLED)) throw new Error(`Bundled skill not found at ${BUNDLED}`);
|
|
25
|
+
const dest = join(installDir(), "SKILL.md");
|
|
26
|
+
mkdirSync(installDir(), { recursive: true });
|
|
27
|
+
writeFileSync(dest, readFileSync(BUNDLED, "utf-8"));
|
|
28
|
+
out({ installed: true, skill: SKILL_NAME, path: dest });
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
out({
|
|
33
|
+
skill: SKILL_NAME,
|
|
34
|
+
usage: {
|
|
35
|
+
install: "spec skill install Copy the skill into ~/.claude/skills/",
|
|
36
|
+
path: "spec skill path Print the bundled SKILL.md location",
|
|
37
|
+
},
|
|
38
|
+
bundled: BUNDLED,
|
|
39
|
+
});
|
|
40
|
+
}
|
package/src/commands/specs.js
CHANGED
|
@@ -1,19 +1,32 @@
|
|
|
1
1
|
import { parseArgs } from "../args.js";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
getRegistry,
|
|
4
|
+
saveRegistry,
|
|
5
|
+
getEntry,
|
|
6
|
+
removeCachedSpec,
|
|
7
|
+
saveCachedSpec,
|
|
8
|
+
allEntries,
|
|
9
|
+
} from "../registry.js";
|
|
3
10
|
import { fetchSpec } from "./fetch.js";
|
|
4
11
|
import { out } from "../output.js";
|
|
5
12
|
|
|
13
|
+
function findSection(registry, name) {
|
|
14
|
+
for (const section of ["mcp", "openapi", "graphql"]) {
|
|
15
|
+
if (registry[section]?.[name]) return section;
|
|
16
|
+
}
|
|
17
|
+
return null;
|
|
18
|
+
}
|
|
19
|
+
|
|
6
20
|
export async function specsCmd(args) {
|
|
7
21
|
const { flags } = parseArgs(args);
|
|
8
22
|
const compact = flags.compact !== "false";
|
|
9
23
|
const registry = getRegistry();
|
|
10
24
|
|
|
11
|
-
const specs = registry.map((e) => {
|
|
25
|
+
const specs = allEntries(registry).map((e) => {
|
|
12
26
|
if (compact) {
|
|
13
27
|
return {
|
|
14
28
|
name: e.name,
|
|
15
29
|
type: e.type,
|
|
16
|
-
transport: e.transport,
|
|
17
30
|
description: e.description || null,
|
|
18
31
|
enabled: e.enabled,
|
|
19
32
|
};
|
|
@@ -30,11 +43,11 @@ export async function registryMutate(action, args) {
|
|
|
30
43
|
if (!name) throw new Error(`Usage: spec ${action} <name>`);
|
|
31
44
|
|
|
32
45
|
const registry = getRegistry();
|
|
33
|
-
const
|
|
34
|
-
if (
|
|
46
|
+
const section = findSection(registry, name);
|
|
47
|
+
if (!section) throw new Error(`No spec named '${name}'. Run 'spec specs' to see available.`);
|
|
35
48
|
|
|
36
49
|
if (action === "remove") {
|
|
37
|
-
registry
|
|
50
|
+
delete registry[section][name];
|
|
38
51
|
saveRegistry(registry);
|
|
39
52
|
removeCachedSpec(name);
|
|
40
53
|
out({ ok: true, removed: name });
|
|
@@ -42,21 +55,21 @@ export async function registryMutate(action, args) {
|
|
|
42
55
|
}
|
|
43
56
|
|
|
44
57
|
if (action === "enable") {
|
|
45
|
-
registry[
|
|
58
|
+
registry[section][name].enabled = true;
|
|
46
59
|
saveRegistry(registry);
|
|
47
60
|
out({ ok: true, enabled: name });
|
|
48
61
|
return;
|
|
49
62
|
}
|
|
50
63
|
|
|
51
64
|
if (action === "disable") {
|
|
52
|
-
registry[
|
|
65
|
+
registry[section][name].enabled = false;
|
|
53
66
|
saveRegistry(registry);
|
|
54
67
|
out({ ok: true, disabled: name });
|
|
55
68
|
return;
|
|
56
69
|
}
|
|
57
70
|
|
|
58
71
|
if (action === "refresh") {
|
|
59
|
-
const entry = registry[
|
|
72
|
+
const entry = { ...registry[section][name], name, _section: section };
|
|
60
73
|
if (!entry.enabled) throw new Error(`Spec '${name}' is disabled. Enable it first.`);
|
|
61
74
|
const spec = await fetchSpec(entry);
|
|
62
75
|
saveCachedSpec(name, spec);
|
package/src/commands/types.js
CHANGED
|
@@ -50,7 +50,8 @@ function showOpenAPISchema(spec, target, flags) {
|
|
|
50
50
|
|
|
51
51
|
function showGraphQLType(spec, target, flags) {
|
|
52
52
|
const scalars = new Set(["String", "Int", "Float", "Boolean", "ID"]);
|
|
53
|
-
const userTypes =
|
|
53
|
+
const userTypes =
|
|
54
|
+
spec.types?.filter((t) => !t.name.startsWith("__") && !scalars.has(t.name)) || [];
|
|
54
55
|
|
|
55
56
|
if (!target) {
|
|
56
57
|
// List type names grouped by kind — compact
|
|
@@ -85,7 +86,10 @@ function showGraphQLType(spec, target, flags) {
|
|
|
85
86
|
result.fields = type.fields.map((f) => ({
|
|
86
87
|
name: f.name,
|
|
87
88
|
type: flattenType(f.type),
|
|
88
|
-
args:
|
|
89
|
+
args:
|
|
90
|
+
f.args?.length > 0
|
|
91
|
+
? f.args.map((a) => ({ name: a.name, type: flattenType(a.type) }))
|
|
92
|
+
: undefined,
|
|
89
93
|
}));
|
|
90
94
|
}
|
|
91
95
|
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { out } from "../output.js";
|
|
2
|
+
import { parseArgs } from "../args.js";
|
|
3
|
+
import { allUsage, topOperations } from "../usage.js";
|
|
4
|
+
|
|
5
|
+
export async function usageCmd(args) {
|
|
6
|
+
const { positional } = parseArgs(args);
|
|
7
|
+
const specName = positional[0];
|
|
8
|
+
|
|
9
|
+
if (specName) {
|
|
10
|
+
const operations = topOperations(specName, Infinity);
|
|
11
|
+
out({ spec: specName, operations });
|
|
12
|
+
} else {
|
|
13
|
+
out({ usage: allUsage() });
|
|
14
|
+
}
|
|
15
|
+
}
|
package/src/commands/validate.js
CHANGED
|
@@ -88,7 +88,10 @@ function validateInfo(doc, errors, warnings) {
|
|
|
88
88
|
errors.push({ path: "info.version", message: "Missing required 'info.version'" });
|
|
89
89
|
}
|
|
90
90
|
if (!doc.info.description) {
|
|
91
|
-
warnings.push({
|
|
91
|
+
warnings.push({
|
|
92
|
+
path: "info.description",
|
|
93
|
+
message: "Missing 'info.description' (recommended)",
|
|
94
|
+
});
|
|
92
95
|
}
|
|
93
96
|
}
|
|
94
97
|
|
|
@@ -121,7 +124,10 @@ function validatePaths(doc, errors, warnings, isV3) {
|
|
|
121
124
|
if (method.startsWith("x-") || method === "parameters" || method === "$ref") continue;
|
|
122
125
|
|
|
123
126
|
if (!METHODS.has(method)) {
|
|
124
|
-
warnings.push({
|
|
127
|
+
warnings.push({
|
|
128
|
+
path: `paths.${path}.${method}`,
|
|
129
|
+
message: `Unknown HTTP method '${method}'`,
|
|
130
|
+
});
|
|
125
131
|
continue;
|
|
126
132
|
}
|
|
127
133
|
|
|
@@ -161,13 +167,19 @@ function validatePaths(doc, errors, warnings, isV3) {
|
|
|
161
167
|
for (const param of pathParams) {
|
|
162
168
|
if (!declaredParams.has(param)) {
|
|
163
169
|
// Only warn — the param might be declared via $ref
|
|
164
|
-
warnings.push({
|
|
170
|
+
warnings.push({
|
|
171
|
+
path: opPath,
|
|
172
|
+
message: `Path parameter '{${param}}' may not be declared in parameters`,
|
|
173
|
+
});
|
|
165
174
|
}
|
|
166
175
|
}
|
|
167
176
|
|
|
168
177
|
// Request body on GET/DELETE/HEAD
|
|
169
178
|
if (isV3 && op.requestBody && ["get", "delete", "head"].includes(method)) {
|
|
170
|
-
warnings.push({
|
|
179
|
+
warnings.push({
|
|
180
|
+
path: opPath,
|
|
181
|
+
message: `requestBody on ${method.toUpperCase()} is unusual`,
|
|
182
|
+
});
|
|
171
183
|
}
|
|
172
184
|
}
|
|
173
185
|
}
|
|
@@ -196,7 +208,15 @@ function validateSchema(schema, path, errors, warnings) {
|
|
|
196
208
|
if (!schema || typeof schema !== "object") return;
|
|
197
209
|
if (schema.$ref) return; // reference, skip
|
|
198
210
|
|
|
199
|
-
const VALID_TYPES = new Set([
|
|
211
|
+
const VALID_TYPES = new Set([
|
|
212
|
+
"string",
|
|
213
|
+
"number",
|
|
214
|
+
"integer",
|
|
215
|
+
"boolean",
|
|
216
|
+
"array",
|
|
217
|
+
"object",
|
|
218
|
+
"null",
|
|
219
|
+
]);
|
|
200
220
|
|
|
201
221
|
if (schema.type && !VALID_TYPES.has(schema.type)) {
|
|
202
222
|
errors.push({ path, message: `Invalid type '${schema.type}'` });
|
|
@@ -220,11 +240,17 @@ function validateSchema(schema, path, errors, warnings) {
|
|
|
220
240
|
function validateServers(doc, errors, warnings, isV3) {
|
|
221
241
|
if (isV3) {
|
|
222
242
|
if (!doc.servers || doc.servers.length === 0) {
|
|
223
|
-
warnings.push({
|
|
243
|
+
warnings.push({
|
|
244
|
+
path: "servers",
|
|
245
|
+
message: "No servers defined — agent will need baseUrl configured",
|
|
246
|
+
});
|
|
224
247
|
}
|
|
225
248
|
} else {
|
|
226
249
|
if (!doc.host) {
|
|
227
|
-
warnings.push({
|
|
250
|
+
warnings.push({
|
|
251
|
+
path: "host",
|
|
252
|
+
message: "No host defined — agent will need baseUrl configured",
|
|
253
|
+
});
|
|
228
254
|
}
|
|
229
255
|
}
|
|
230
256
|
}
|
package/src/dotenv.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from "fs";
|
|
2
|
+
import { join } from "path";
|
|
3
|
+
|
|
4
|
+
function parseEnv(text) {
|
|
5
|
+
const out = {};
|
|
6
|
+
for (const rawLine of text.split(/\r?\n/)) {
|
|
7
|
+
const line = rawLine.trim();
|
|
8
|
+
if (!line || line.startsWith("#")) continue;
|
|
9
|
+
const eq = line.indexOf("=");
|
|
10
|
+
if (eq === -1) continue;
|
|
11
|
+
const key = line.slice(0, eq).trim();
|
|
12
|
+
if (!key) continue;
|
|
13
|
+
let value = line.slice(eq + 1).trim();
|
|
14
|
+
if (
|
|
15
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
16
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
17
|
+
) {
|
|
18
|
+
value = value.slice(1, -1);
|
|
19
|
+
}
|
|
20
|
+
out[key] = value;
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function loadDotenv(dir = process.cwd()) {
|
|
26
|
+
if (process.env.SPEC_NO_DOTENV) return;
|
|
27
|
+
const file = join(dir, ".env");
|
|
28
|
+
if (!existsSync(file)) return;
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = parseEnv(readFileSync(file, "utf-8"));
|
|
32
|
+
} catch {
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
for (const [key, value] of Object.entries(parsed)) {
|
|
36
|
+
if (!(key in process.env)) process.env[key] = value;
|
|
37
|
+
}
|
|
38
|
+
}
|
package/src/glob.js
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
function globToRegex(pattern) {
|
|
2
2
|
return new RegExp(
|
|
3
|
-
"^" +
|
|
3
|
+
"^" +
|
|
4
|
+
pattern
|
|
5
|
+
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
|
6
|
+
.replace(/\*/g, ".*")
|
|
7
|
+
.replace(/\?/g, ".") +
|
|
8
|
+
"$",
|
|
4
9
|
"i"
|
|
5
10
|
);
|
|
6
11
|
}
|