specli 0.0.17 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli/compile.js +8 -1
- package/package.json +2 -1
- package/src/ai/tools.test.ts +83 -0
- package/src/ai/tools.ts +211 -0
- package/src/cli/auth-requirements.test.ts +27 -0
- package/src/cli/auth-requirements.ts +91 -0
- package/src/cli/auth-schemes.test.ts +66 -0
- package/src/cli/auth-schemes.ts +187 -0
- package/src/cli/capabilities.test.ts +94 -0
- package/src/cli/capabilities.ts +88 -0
- package/src/cli/command-id.test.ts +32 -0
- package/src/cli/command-id.ts +16 -0
- package/src/cli/command-index.ts +19 -0
- package/src/cli/command-model.test.ts +44 -0
- package/src/cli/command-model.ts +128 -0
- package/src/cli/compile.ts +109 -0
- package/src/cli/crypto.ts +9 -0
- package/src/cli/derive-name.ts +101 -0
- package/src/cli/exec.ts +72 -0
- package/src/cli/main.ts +255 -0
- package/src/cli/naming.test.ts +86 -0
- package/src/cli/naming.ts +224 -0
- package/src/cli/operations.test.ts +57 -0
- package/src/cli/operations.ts +152 -0
- package/src/cli/params.test.ts +70 -0
- package/src/cli/params.ts +71 -0
- package/src/cli/pluralize.ts +41 -0
- package/src/cli/positional.test.ts +65 -0
- package/src/cli/positional.ts +75 -0
- package/src/cli/request-body.test.ts +35 -0
- package/src/cli/request-body.ts +94 -0
- package/src/cli/runtime/argv.ts +14 -0
- package/src/cli/runtime/auth/resolve.ts +59 -0
- package/src/cli/runtime/body-flags.test.ts +261 -0
- package/src/cli/runtime/body-flags.ts +176 -0
- package/src/cli/runtime/body.ts +24 -0
- package/src/cli/runtime/collect.ts +6 -0
- package/src/cli/runtime/compat.ts +89 -0
- package/src/cli/runtime/context.ts +62 -0
- package/src/cli/runtime/execute.ts +147 -0
- package/src/cli/runtime/generated.ts +242 -0
- package/src/cli/runtime/headers.ts +37 -0
- package/src/cli/runtime/index.ts +3 -0
- package/src/cli/runtime/profile/secrets.ts +83 -0
- package/src/cli/runtime/profile/store.ts +100 -0
- package/src/cli/runtime/request.test.ts +375 -0
- package/src/cli/runtime/request.ts +390 -0
- package/src/cli/runtime/server-url.ts +45 -0
- package/src/cli/runtime/template.ts +26 -0
- package/src/cli/runtime/validate/ajv.ts +13 -0
- package/src/cli/runtime/validate/coerce.test.ts +98 -0
- package/src/cli/runtime/validate/coerce.ts +71 -0
- package/src/cli/runtime/validate/error.ts +29 -0
- package/src/cli/runtime/validate/index.ts +4 -0
- package/src/cli/runtime/validate/schema.ts +54 -0
- package/src/cli/schema-shape.ts +36 -0
- package/src/cli/schema.ts +76 -0
- package/src/cli/server.test.ts +55 -0
- package/src/cli/server.ts +167 -0
- package/src/cli/spec-id.ts +12 -0
- package/src/cli/spec-loader.ts +58 -0
- package/src/cli/stable-json.ts +35 -0
- package/src/cli/strings.ts +21 -0
- package/src/cli/types.ts +59 -0
- package/src/cli.ts +94 -0
- package/src/compiled.ts +24 -0
- package/src/macros/env.ts +21 -0
- package/src/macros/spec.ts +17 -0
- package/src/macros/version.ts +14 -0
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
NormalizedOperation,
|
|
3
|
+
NormalizedParameter,
|
|
4
|
+
NormalizedRequestBody,
|
|
5
|
+
OpenApiDoc,
|
|
6
|
+
} from "./types.js";
|
|
7
|
+
|
|
8
|
+
function operationKey(method: string, path: string): string {
|
|
9
|
+
return `${method.toUpperCase()} ${path}`;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const HTTP_METHODS = [
|
|
13
|
+
"get",
|
|
14
|
+
"post",
|
|
15
|
+
"put",
|
|
16
|
+
"patch",
|
|
17
|
+
"delete",
|
|
18
|
+
"options",
|
|
19
|
+
"head",
|
|
20
|
+
"trace",
|
|
21
|
+
] as const;
|
|
22
|
+
|
|
23
|
+
type RawParameter = {
|
|
24
|
+
in?: string;
|
|
25
|
+
name?: string;
|
|
26
|
+
required?: boolean;
|
|
27
|
+
description?: string;
|
|
28
|
+
schema?: unknown;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type RawRequestBody = {
|
|
32
|
+
required?: boolean;
|
|
33
|
+
content?: Record<string, { schema?: unknown } | undefined>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type RawOperation = {
|
|
37
|
+
operationId?: string;
|
|
38
|
+
tags?: string[];
|
|
39
|
+
summary?: string;
|
|
40
|
+
description?: string;
|
|
41
|
+
deprecated?: boolean;
|
|
42
|
+
security?: OpenApiDoc["security"];
|
|
43
|
+
parameters?: RawParameter[];
|
|
44
|
+
requestBody?: RawRequestBody;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
type RawPathItem = {
|
|
48
|
+
parameters?: RawParameter[];
|
|
49
|
+
} & Partial<Record<(typeof HTTP_METHODS)[number], RawOperation>>;
|
|
50
|
+
|
|
51
|
+
function normalizeParam(p: RawParameter): NormalizedParameter | undefined {
|
|
52
|
+
if (!p || typeof p !== "object") return undefined;
|
|
53
|
+
const loc = p.in;
|
|
54
|
+
const name = p.name;
|
|
55
|
+
if (
|
|
56
|
+
loc !== "path" &&
|
|
57
|
+
loc !== "query" &&
|
|
58
|
+
loc !== "header" &&
|
|
59
|
+
loc !== "cookie"
|
|
60
|
+
) {
|
|
61
|
+
return undefined;
|
|
62
|
+
}
|
|
63
|
+
if (!name) return undefined;
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
in: loc,
|
|
67
|
+
name,
|
|
68
|
+
required: Boolean(p.required || loc === "path"),
|
|
69
|
+
description: p.description,
|
|
70
|
+
schema: p.schema,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function mergeParameters(
|
|
75
|
+
pathParams: RawParameter[] | undefined,
|
|
76
|
+
opParams: RawParameter[] | undefined,
|
|
77
|
+
): NormalizedParameter[] {
|
|
78
|
+
const merged = new Map<string, NormalizedParameter>();
|
|
79
|
+
|
|
80
|
+
for (const p of pathParams ?? []) {
|
|
81
|
+
const normalized = normalizeParam(p);
|
|
82
|
+
if (!normalized) continue;
|
|
83
|
+
merged.set(`${normalized.in}:${normalized.name}`, normalized);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
for (const p of opParams ?? []) {
|
|
87
|
+
const normalized = normalizeParam(p);
|
|
88
|
+
if (!normalized) continue;
|
|
89
|
+
merged.set(`${normalized.in}:${normalized.name}`, normalized);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return [...merged.values()];
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function normalizeRequestBody(
|
|
96
|
+
rb: RawRequestBody | undefined,
|
|
97
|
+
): NormalizedRequestBody | undefined {
|
|
98
|
+
if (!rb) return undefined;
|
|
99
|
+
|
|
100
|
+
const content = rb.content ?? {};
|
|
101
|
+
const contentTypes = Object.keys(content);
|
|
102
|
+
const schemasByContentType: Record<string, unknown | undefined> = {};
|
|
103
|
+
|
|
104
|
+
for (const contentType of contentTypes) {
|
|
105
|
+
schemasByContentType[contentType] = content[contentType]?.schema;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return {
|
|
109
|
+
required: Boolean(rb.required),
|
|
110
|
+
contentTypes,
|
|
111
|
+
schemasByContentType,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export function indexOperations(doc: OpenApiDoc): NormalizedOperation[] {
|
|
116
|
+
const out: NormalizedOperation[] = [];
|
|
117
|
+
const paths = doc.paths ?? {};
|
|
118
|
+
|
|
119
|
+
for (const [path, rawPathItem] of Object.entries(paths)) {
|
|
120
|
+
if (!rawPathItem || typeof rawPathItem !== "object") continue;
|
|
121
|
+
|
|
122
|
+
const pathItem = rawPathItem as RawPathItem;
|
|
123
|
+
|
|
124
|
+
for (const method of HTTP_METHODS) {
|
|
125
|
+
const op = pathItem[method];
|
|
126
|
+
if (!op) continue;
|
|
127
|
+
|
|
128
|
+
const parameters = mergeParameters(pathItem.parameters, op.parameters);
|
|
129
|
+
const normalizedMethod = method.toUpperCase();
|
|
130
|
+
out.push({
|
|
131
|
+
key: operationKey(normalizedMethod, path),
|
|
132
|
+
method: normalizedMethod,
|
|
133
|
+
path,
|
|
134
|
+
operationId: op.operationId,
|
|
135
|
+
tags: op.tags ?? [],
|
|
136
|
+
summary: op.summary,
|
|
137
|
+
description: op.description,
|
|
138
|
+
deprecated: op.deprecated,
|
|
139
|
+
security: (op.security ?? doc.security) as OpenApiDoc["security"],
|
|
140
|
+
parameters,
|
|
141
|
+
requestBody: normalizeRequestBody(op.requestBody),
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
out.sort((a, b) => {
|
|
147
|
+
if (a.path !== b.path) return a.path.localeCompare(b.path);
|
|
148
|
+
return a.method.localeCompare(b.method);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return out;
|
|
152
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { deriveParamSpecs } from "./params.js";
|
|
4
|
+
import type { NormalizedOperation } from "./types.js";
|
|
5
|
+
|
|
6
|
+
describe("deriveParamSpecs", () => {
|
|
7
|
+
test("derives basic types + flags", () => {
|
|
8
|
+
const op: NormalizedOperation = {
|
|
9
|
+
key: "GET /contacts",
|
|
10
|
+
method: "GET",
|
|
11
|
+
path: "/contacts",
|
|
12
|
+
tags: [],
|
|
13
|
+
parameters: [
|
|
14
|
+
{
|
|
15
|
+
in: "query",
|
|
16
|
+
name: "limit",
|
|
17
|
+
required: false,
|
|
18
|
+
schema: {
|
|
19
|
+
type: "integer",
|
|
20
|
+
format: "int32",
|
|
21
|
+
enum: ["1", "2"],
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
in: "header",
|
|
26
|
+
name: "X-Request-Id",
|
|
27
|
+
required: false,
|
|
28
|
+
schema: { type: "string" },
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
const specs = deriveParamSpecs(op);
|
|
34
|
+
expect(specs).toHaveLength(2);
|
|
35
|
+
|
|
36
|
+
const limit = specs.find((p) => p.name === "limit");
|
|
37
|
+
expect(limit?.kind).toBe("flag");
|
|
38
|
+
expect(limit?.flag).toBe("--limit");
|
|
39
|
+
expect(limit?.type).toBe("integer");
|
|
40
|
+
expect(limit?.format).toBe("int32");
|
|
41
|
+
expect(limit?.enum).toEqual(["1", "2"]);
|
|
42
|
+
|
|
43
|
+
const reqId = specs.find((p) => p.name === "X-Request-Id");
|
|
44
|
+
expect(reqId?.kind).toBe("flag");
|
|
45
|
+
expect(reqId?.flag).toBe("--x-request-id");
|
|
46
|
+
expect(reqId?.type).toBe("string");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("derives array item types", () => {
|
|
50
|
+
const op: NormalizedOperation = {
|
|
51
|
+
key: "GET /things",
|
|
52
|
+
method: "GET",
|
|
53
|
+
path: "/things",
|
|
54
|
+
tags: [],
|
|
55
|
+
parameters: [
|
|
56
|
+
{
|
|
57
|
+
in: "query",
|
|
58
|
+
name: "ids",
|
|
59
|
+
required: false,
|
|
60
|
+
schema: { type: "array", items: { type: "integer" } },
|
|
61
|
+
},
|
|
62
|
+
],
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const specs = deriveParamSpecs(op);
|
|
66
|
+
expect(specs).toHaveLength(1);
|
|
67
|
+
expect(specs[0]?.type).toBe("array");
|
|
68
|
+
expect(specs[0]?.itemType).toBe("integer");
|
|
69
|
+
});
|
|
70
|
+
});
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSchemaEnumStrings,
|
|
3
|
+
getSchemaFormat,
|
|
4
|
+
getSchemaType,
|
|
5
|
+
} from "./schema-shape.js";
|
|
6
|
+
import { kebabCase } from "./strings.js";
|
|
7
|
+
import type { NormalizedOperation, NormalizedParameter } from "./types.js";
|
|
8
|
+
|
|
9
|
+
export type ParamType = import("./schema-shape.js").ParamType;
|
|
10
|
+
|
|
11
|
+
export type ParamSpec = {
|
|
12
|
+
kind: "positional" | "flag";
|
|
13
|
+
in: NormalizedParameter["in"];
|
|
14
|
+
name: string;
|
|
15
|
+
flag: string;
|
|
16
|
+
required: boolean;
|
|
17
|
+
description?: string;
|
|
18
|
+
type: ParamType;
|
|
19
|
+
format?: string;
|
|
20
|
+
enum?: string[];
|
|
21
|
+
|
|
22
|
+
// Arrays
|
|
23
|
+
itemType?: ParamType;
|
|
24
|
+
itemFormat?: string;
|
|
25
|
+
itemEnum?: string[];
|
|
26
|
+
|
|
27
|
+
// Original schema for Ajv validation and future advanced flag expansion.
|
|
28
|
+
schema?: import("./types.js").JsonSchema;
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export function deriveParamSpecs(op: NormalizedOperation): ParamSpec[] {
|
|
32
|
+
const out: ParamSpec[] = [];
|
|
33
|
+
|
|
34
|
+
for (const p of op.parameters) {
|
|
35
|
+
const flag = `--${kebabCase(p.name)}`;
|
|
36
|
+
const type = getSchemaType(p.schema);
|
|
37
|
+
const schemaObj =
|
|
38
|
+
p.schema && typeof p.schema === "object"
|
|
39
|
+
? (p.schema as import("./types.js").JsonSchema)
|
|
40
|
+
: undefined;
|
|
41
|
+
|
|
42
|
+
const itemsSchema =
|
|
43
|
+
schemaObj && type === "array" && typeof schemaObj.items === "object"
|
|
44
|
+
? (schemaObj.items as unknown)
|
|
45
|
+
: undefined;
|
|
46
|
+
|
|
47
|
+
out.push({
|
|
48
|
+
kind: p.in === "path" ? "positional" : "flag",
|
|
49
|
+
in: p.in,
|
|
50
|
+
name: p.name,
|
|
51
|
+
flag,
|
|
52
|
+
required: p.required,
|
|
53
|
+
description: p.description,
|
|
54
|
+
type,
|
|
55
|
+
format: getSchemaFormat(p.schema),
|
|
56
|
+
enum: getSchemaEnumStrings(p.schema),
|
|
57
|
+
itemType: type === "array" ? getSchemaType(itemsSchema) : undefined,
|
|
58
|
+
itemFormat: type === "array" ? getSchemaFormat(itemsSchema) : undefined,
|
|
59
|
+
itemEnum:
|
|
60
|
+
type === "array" ? getSchemaEnumStrings(itemsSchema) : undefined,
|
|
61
|
+
schema: schemaObj,
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
out.sort((a, b) => {
|
|
66
|
+
if (a.in !== b.in) return a.in.localeCompare(b.in);
|
|
67
|
+
return a.name.localeCompare(b.name);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
return out;
|
|
71
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
const IRREGULAR: Record<string, string> = {
|
|
2
|
+
person: "people",
|
|
3
|
+
man: "men",
|
|
4
|
+
woman: "women",
|
|
5
|
+
child: "children",
|
|
6
|
+
tooth: "teeth",
|
|
7
|
+
foot: "feet",
|
|
8
|
+
mouse: "mice",
|
|
9
|
+
goose: "geese",
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const UNCOUNTABLE = new Set([
|
|
13
|
+
"metadata",
|
|
14
|
+
"information",
|
|
15
|
+
"equipment",
|
|
16
|
+
"money",
|
|
17
|
+
"series",
|
|
18
|
+
"species",
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export function pluralize(word: string): string {
|
|
22
|
+
const w = word.trim();
|
|
23
|
+
if (!w) return w;
|
|
24
|
+
|
|
25
|
+
const lower = w.toLowerCase();
|
|
26
|
+
if (UNCOUNTABLE.has(lower)) return lower;
|
|
27
|
+
if (IRREGULAR[lower]) return IRREGULAR[lower];
|
|
28
|
+
|
|
29
|
+
// already plural-ish
|
|
30
|
+
if (lower.endsWith("s")) return lower;
|
|
31
|
+
|
|
32
|
+
if (/[bcdfghjklmnpqrstvwxyz]y$/.test(lower)) {
|
|
33
|
+
return lower.replace(/y$/, "ies");
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
if (/(ch|sh|x|z)$/.test(lower)) {
|
|
37
|
+
return `${lower}es`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return `${lower}s`;
|
|
41
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { ActionShapeForCli } from "./positional.js";
|
|
3
|
+
import { deriveFlags, derivePositionals } from "./positional.js";
|
|
4
|
+
|
|
5
|
+
describe("derivePositionals", () => {
|
|
6
|
+
test("returns ordered positionals from pathArgs", () => {
|
|
7
|
+
const action: ActionShapeForCli = {
|
|
8
|
+
pathArgs: ["id"],
|
|
9
|
+
params: [
|
|
10
|
+
{
|
|
11
|
+
kind: "positional",
|
|
12
|
+
in: "path",
|
|
13
|
+
name: "id",
|
|
14
|
+
flag: "--id",
|
|
15
|
+
required: true,
|
|
16
|
+
type: "string",
|
|
17
|
+
},
|
|
18
|
+
],
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const pos = derivePositionals(action);
|
|
22
|
+
expect(pos).toEqual([
|
|
23
|
+
{
|
|
24
|
+
name: "id",
|
|
25
|
+
required: true,
|
|
26
|
+
type: "string",
|
|
27
|
+
format: undefined,
|
|
28
|
+
enum: undefined,
|
|
29
|
+
description: undefined,
|
|
30
|
+
},
|
|
31
|
+
]);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
describe("deriveFlags", () => {
|
|
36
|
+
test("returns only flag params", () => {
|
|
37
|
+
const action: ActionShapeForCli = {
|
|
38
|
+
pathArgs: [],
|
|
39
|
+
params: [
|
|
40
|
+
{
|
|
41
|
+
kind: "flag",
|
|
42
|
+
in: "query",
|
|
43
|
+
name: "limit",
|
|
44
|
+
flag: "--limit",
|
|
45
|
+
required: false,
|
|
46
|
+
type: "integer",
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const flags = deriveFlags(action);
|
|
52
|
+
expect(flags.flags).toEqual([
|
|
53
|
+
{
|
|
54
|
+
in: "query",
|
|
55
|
+
name: "limit",
|
|
56
|
+
flag: "--limit",
|
|
57
|
+
required: false,
|
|
58
|
+
description: undefined,
|
|
59
|
+
type: "integer",
|
|
60
|
+
format: undefined,
|
|
61
|
+
enum: undefined,
|
|
62
|
+
},
|
|
63
|
+
]);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { ParamSpec } from "./params.js";
|
|
2
|
+
|
|
3
|
+
export type ActionShapeForCli = {
|
|
4
|
+
pathArgs: string[];
|
|
5
|
+
params: ParamSpec[];
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export type PositionalArg = {
|
|
9
|
+
name: string;
|
|
10
|
+
required: boolean;
|
|
11
|
+
description?: string;
|
|
12
|
+
type: import("./schema-shape.js").ParamType;
|
|
13
|
+
format?: string;
|
|
14
|
+
enum?: string[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type FlagsIndex = {
|
|
18
|
+
flags: Array<
|
|
19
|
+
Pick<
|
|
20
|
+
import("./params.js").ParamSpec,
|
|
21
|
+
| "in"
|
|
22
|
+
| "name"
|
|
23
|
+
| "flag"
|
|
24
|
+
| "required"
|
|
25
|
+
| "description"
|
|
26
|
+
| "type"
|
|
27
|
+
| "format"
|
|
28
|
+
| "enum"
|
|
29
|
+
| "itemType"
|
|
30
|
+
| "itemFormat"
|
|
31
|
+
| "itemEnum"
|
|
32
|
+
>
|
|
33
|
+
>;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export function derivePositionals(action: ActionShapeForCli): PositionalArg[] {
|
|
37
|
+
const byName = new Map<string, PositionalArg>();
|
|
38
|
+
|
|
39
|
+
// Use pathArgs order; match metadata from params when available.
|
|
40
|
+
for (const name of action.pathArgs) {
|
|
41
|
+
const p = action.params.find(
|
|
42
|
+
(x: ParamSpec) => x.in === "path" && x.name === name,
|
|
43
|
+
);
|
|
44
|
+
byName.set(name, {
|
|
45
|
+
name,
|
|
46
|
+
required: true,
|
|
47
|
+
description: p?.description,
|
|
48
|
+
type: p?.type ?? "unknown",
|
|
49
|
+
format: p?.format,
|
|
50
|
+
enum: p?.enum,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return [...byName.values()];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function deriveFlags(action: ActionShapeForCli): FlagsIndex {
|
|
58
|
+
return {
|
|
59
|
+
flags: action.params
|
|
60
|
+
.filter((p: ParamSpec) => p.kind === "flag")
|
|
61
|
+
.map((p: ParamSpec) => ({
|
|
62
|
+
in: p.in,
|
|
63
|
+
name: p.name,
|
|
64
|
+
flag: p.flag,
|
|
65
|
+
required: p.required,
|
|
66
|
+
description: p.description,
|
|
67
|
+
type: p.type,
|
|
68
|
+
format: p.format,
|
|
69
|
+
enum: p.enum,
|
|
70
|
+
itemType: p.itemType,
|
|
71
|
+
itemFormat: p.itemFormat,
|
|
72
|
+
itemEnum: p.itemEnum,
|
|
73
|
+
})),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { deriveRequestBodyInfo } from "./request-body.js";
|
|
4
|
+
import type { NormalizedOperation } from "./types.js";
|
|
5
|
+
|
|
6
|
+
describe("deriveRequestBodyInfo", () => {
|
|
7
|
+
test("summarizes content types and convenience flags", () => {
|
|
8
|
+
const op: NormalizedOperation = {
|
|
9
|
+
key: "POST /contacts",
|
|
10
|
+
method: "POST",
|
|
11
|
+
path: "/contacts",
|
|
12
|
+
tags: [],
|
|
13
|
+
parameters: [],
|
|
14
|
+
requestBody: {
|
|
15
|
+
required: true,
|
|
16
|
+
contentTypes: ["application/x-www-form-urlencoded", "application/json"],
|
|
17
|
+
schemasByContentType: {
|
|
18
|
+
"application/json": { type: "object" },
|
|
19
|
+
"application/x-www-form-urlencoded": { type: "object" },
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const info = deriveRequestBodyInfo(op);
|
|
25
|
+
expect(info?.required).toBe(true);
|
|
26
|
+
expect(info?.hasJson).toBe(true);
|
|
27
|
+
expect(info?.hasFormUrlEncoded).toBe(true);
|
|
28
|
+
expect(info?.hasMultipart).toBe(false);
|
|
29
|
+
expect(info?.content.map((c) => c.contentType)).toEqual([
|
|
30
|
+
"application/json",
|
|
31
|
+
"application/x-www-form-urlencoded",
|
|
32
|
+
]);
|
|
33
|
+
expect(info?.preferredSchema).toEqual({ type: "object" });
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSchemaEnumStrings,
|
|
3
|
+
getSchemaFormat,
|
|
4
|
+
getSchemaType,
|
|
5
|
+
} from "./schema-shape.js";
|
|
6
|
+
import type {
|
|
7
|
+
JsonSchema,
|
|
8
|
+
NormalizedOperation,
|
|
9
|
+
NormalizedRequestBody,
|
|
10
|
+
} from "./types.js";
|
|
11
|
+
import { isJsonSchema } from "./types.js";
|
|
12
|
+
|
|
13
|
+
export type RequestBodyContent = {
|
|
14
|
+
contentType: string;
|
|
15
|
+
required: boolean;
|
|
16
|
+
schemaType: import("./schema-shape.js").ParamType;
|
|
17
|
+
schemaFormat?: string;
|
|
18
|
+
schemaEnum?: string[];
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export type RequestBodyInfo = {
|
|
22
|
+
required: boolean;
|
|
23
|
+
content: RequestBodyContent[];
|
|
24
|
+
// Convenience flags for later arg generation.
|
|
25
|
+
hasJson: boolean;
|
|
26
|
+
hasFormUrlEncoded: boolean;
|
|
27
|
+
hasMultipart: boolean;
|
|
28
|
+
|
|
29
|
+
// Phase 1 planning: supported generic body inputs.
|
|
30
|
+
bodyFlags: string[];
|
|
31
|
+
preferredContentType?: string;
|
|
32
|
+
|
|
33
|
+
// Original JSON Schema (for expanded flags + validation)
|
|
34
|
+
preferredSchema?: JsonSchema;
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function getRequestBody(
|
|
38
|
+
op: NormalizedOperation,
|
|
39
|
+
): NormalizedRequestBody | undefined {
|
|
40
|
+
return op.requestBody;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function deriveRequestBodyInfo(
|
|
44
|
+
op: NormalizedOperation,
|
|
45
|
+
): RequestBodyInfo | undefined {
|
|
46
|
+
const rb = getRequestBody(op);
|
|
47
|
+
if (!rb) return undefined;
|
|
48
|
+
|
|
49
|
+
const content: RequestBodyContent[] = [];
|
|
50
|
+
for (const contentType of rb.contentTypes) {
|
|
51
|
+
const schema = rb.schemasByContentType[contentType];
|
|
52
|
+
content.push({
|
|
53
|
+
contentType,
|
|
54
|
+
required: rb.required,
|
|
55
|
+
schemaType: getSchemaType(schema),
|
|
56
|
+
schemaFormat: getSchemaFormat(schema),
|
|
57
|
+
schemaEnum: getSchemaEnumStrings(schema),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
content.sort((a, b) => a.contentType.localeCompare(b.contentType));
|
|
62
|
+
|
|
63
|
+
const hasJson = content.some((c) => c.contentType.includes("json"));
|
|
64
|
+
const hasFormUrlEncoded = content.some(
|
|
65
|
+
(c) => c.contentType === "application/x-www-form-urlencoded",
|
|
66
|
+
);
|
|
67
|
+
const hasMultipart = content.some((c) =>
|
|
68
|
+
c.contentType.startsWith("multipart/"),
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const bodyFlags = ["--data", "--file"]; // always available when requestBody exists
|
|
72
|
+
|
|
73
|
+
const preferredContentType =
|
|
74
|
+
content.find((c) => c.contentType === "application/json")?.contentType ??
|
|
75
|
+
content.find((c) => c.contentType.includes("json"))?.contentType ??
|
|
76
|
+
content[0]?.contentType;
|
|
77
|
+
|
|
78
|
+
const preferredSchema = preferredContentType
|
|
79
|
+
? rb.schemasByContentType[preferredContentType]
|
|
80
|
+
: undefined;
|
|
81
|
+
|
|
82
|
+
return {
|
|
83
|
+
required: rb.required,
|
|
84
|
+
content,
|
|
85
|
+
hasJson,
|
|
86
|
+
hasFormUrlEncoded,
|
|
87
|
+
hasMultipart,
|
|
88
|
+
bodyFlags,
|
|
89
|
+
preferredContentType,
|
|
90
|
+
preferredSchema: isJsonSchema(preferredSchema)
|
|
91
|
+
? preferredSchema
|
|
92
|
+
: undefined,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export function getArgValue(argv: string[], key: string): string | undefined {
|
|
2
|
+
for (let i = 0; i < argv.length; i++) {
|
|
3
|
+
const a = argv[i];
|
|
4
|
+
if (!a) continue;
|
|
5
|
+
|
|
6
|
+
if (a === key) return argv[i + 1];
|
|
7
|
+
if (a.startsWith(`${key}=`)) return a.slice(key.length + 1);
|
|
8
|
+
}
|
|
9
|
+
return undefined;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function hasAnyArg(argv: string[], names: string[]): boolean {
|
|
13
|
+
return argv.some((a) => a && names.includes(a));
|
|
14
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { AuthScheme } from "../../auth-schemes.js";
|
|
2
|
+
|
|
3
|
+
export type AuthInputs = {
|
|
4
|
+
flagAuthScheme?: string;
|
|
5
|
+
profileAuthScheme?: string;
|
|
6
|
+
embeddedAuthScheme?: string;
|
|
7
|
+
hasStoredToken?: boolean;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const BEARER_COMPATIBLE_KINDS = new Set([
|
|
11
|
+
"http-bearer",
|
|
12
|
+
"oauth2",
|
|
13
|
+
"openIdConnect",
|
|
14
|
+
]);
|
|
15
|
+
|
|
16
|
+
export function resolveAuthScheme(
|
|
17
|
+
authSchemes: AuthScheme[],
|
|
18
|
+
required: import("../../auth-requirements.js").AuthSummary,
|
|
19
|
+
inputs: AuthInputs,
|
|
20
|
+
): string | undefined {
|
|
21
|
+
// Priority: CLI flag > profile > embedded default
|
|
22
|
+
if (inputs.flagAuthScheme) return inputs.flagAuthScheme;
|
|
23
|
+
|
|
24
|
+
if (
|
|
25
|
+
inputs.profileAuthScheme &&
|
|
26
|
+
authSchemes.some((s) => s.key === inputs.profileAuthScheme)
|
|
27
|
+
) {
|
|
28
|
+
return inputs.profileAuthScheme;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (
|
|
32
|
+
inputs.embeddedAuthScheme &&
|
|
33
|
+
authSchemes.some((s) => s.key === inputs.embeddedAuthScheme)
|
|
34
|
+
) {
|
|
35
|
+
return inputs.embeddedAuthScheme;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// If operation requires exactly one scheme, choose it.
|
|
39
|
+
const alts = required.alternatives;
|
|
40
|
+
if (alts.length === 1 && alts[0]?.length === 1) return alts[0][0]?.key;
|
|
41
|
+
|
|
42
|
+
// Otherwise if there is only one scheme in spec, pick it.
|
|
43
|
+
if (authSchemes.length === 1) return authSchemes[0]?.key;
|
|
44
|
+
|
|
45
|
+
// If user has a stored token and operation accepts a bearer-compatible scheme,
|
|
46
|
+
// automatically pick the first one that matches.
|
|
47
|
+
if (inputs.hasStoredToken && alts.length > 0) {
|
|
48
|
+
for (const alt of alts) {
|
|
49
|
+
if (alt.length !== 1) continue;
|
|
50
|
+
const key = alt[0]?.key;
|
|
51
|
+
const scheme = authSchemes.find((s) => s.key === key);
|
|
52
|
+
if (scheme && BEARER_COMPATIBLE_KINDS.has(scheme.kind)) {
|
|
53
|
+
return key;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return undefined;
|
|
59
|
+
}
|