specli 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +111 -0
- package/PLAN.md +274 -0
- package/README.md +474 -0
- package/biome.jsonc +1 -0
- package/bun.lock +98 -0
- package/cli.ts +74 -0
- package/fixtures/openapi-array-items.json +22 -0
- package/fixtures/openapi-auth.json +34 -0
- package/fixtures/openapi-body.json +41 -0
- package/fixtures/openapi-collision.json +21 -0
- package/fixtures/openapi-oauth.json +54 -0
- package/fixtures/openapi-servers.json +35 -0
- package/fixtures/openapi.json +87 -0
- package/index.ts +1 -0
- package/package.json +27 -0
- package/scripts/smoke-specs.ts +64 -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 +119 -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 +336 -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 +31 -0
- package/src/cli/runtime/body.ts +24 -0
- package/src/cli/runtime/collect.ts +6 -0
- package/src/cli/runtime/context.ts +62 -0
- package/src/cli/runtime/execute.ts +138 -0
- package/src/cli/runtime/generated.ts +200 -0
- package/src/cli/runtime/headers.ts +37 -0
- package/src/cli/runtime/index.ts +3 -0
- package/src/cli/runtime/profile/secrets.ts +42 -0
- package/src/cli/runtime/profile/store.ts +98 -0
- package/src/cli/runtime/request.test.ts +153 -0
- package/src/cli/runtime/request.ts +487 -0
- package/src/cli/runtime/server-url.ts +44 -0
- package/src/cli/runtime/template.ts +26 -0
- package/src/cli/runtime/validate/ajv.ts +13 -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 +35 -0
- package/src/cli/server.ts +88 -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/compiled.ts +23 -0
- package/src/macros/env.ts +25 -0
- package/src/macros/spec.ts +17 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
import type { ActionShapeForCli } from "./positional.ts";
|
|
3
|
+
import { deriveFlags, derivePositionals } from "./positional.ts";
|
|
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.ts";
|
|
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.ts").ParamType;
|
|
13
|
+
format?: string;
|
|
14
|
+
enum?: string[];
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type FlagsIndex = {
|
|
18
|
+
flags: Array<
|
|
19
|
+
Pick<
|
|
20
|
+
import("./params.ts").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.ts";
|
|
4
|
+
import type { NormalizedOperation } from "./types.ts";
|
|
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.ts";
|
|
6
|
+
import type {
|
|
7
|
+
JsonSchema,
|
|
8
|
+
NormalizedOperation,
|
|
9
|
+
NormalizedRequestBody,
|
|
10
|
+
} from "./types.ts";
|
|
11
|
+
import { isJsonSchema } from "./types.ts";
|
|
12
|
+
|
|
13
|
+
export type RequestBodyContent = {
|
|
14
|
+
contentType: string;
|
|
15
|
+
required: boolean;
|
|
16
|
+
schemaType: import("./schema-shape.ts").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,31 @@
|
|
|
1
|
+
import type { AuthScheme } from "../../auth-schemes.ts";
|
|
2
|
+
|
|
3
|
+
export type AuthInputs = {
|
|
4
|
+
profileAuthScheme?: string;
|
|
5
|
+
flagAuthScheme?: string;
|
|
6
|
+
};
|
|
7
|
+
|
|
8
|
+
export function resolveAuthScheme(
|
|
9
|
+
authSchemes: AuthScheme[],
|
|
10
|
+
required: import("../../auth-requirements.ts").AuthSummary,
|
|
11
|
+
inputs: AuthInputs,
|
|
12
|
+
): string | undefined {
|
|
13
|
+
// Explicit flag wins (but may still be validated later when applying).
|
|
14
|
+
if (inputs.flagAuthScheme) return inputs.flagAuthScheme;
|
|
15
|
+
|
|
16
|
+
if (
|
|
17
|
+
inputs.profileAuthScheme &&
|
|
18
|
+
authSchemes.some((s) => s.key === inputs.profileAuthScheme)
|
|
19
|
+
) {
|
|
20
|
+
return inputs.profileAuthScheme;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// If operation requires exactly one scheme, choose it.
|
|
24
|
+
const alts = required.alternatives;
|
|
25
|
+
if (alts.length === 1 && alts[0]?.length === 1) return alts[0][0]?.key;
|
|
26
|
+
|
|
27
|
+
// Otherwise if there is only one scheme in spec, pick it.
|
|
28
|
+
if (authSchemes.length === 1) return authSchemes[0]?.key;
|
|
29
|
+
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { YAML } from "bun";
|
|
2
|
+
|
|
3
|
+
export type BodyInput =
|
|
4
|
+
| { kind: "none" }
|
|
5
|
+
| { kind: "data"; data: string }
|
|
6
|
+
| { kind: "file"; path: string };
|
|
7
|
+
|
|
8
|
+
export async function loadBody(
|
|
9
|
+
input: BodyInput,
|
|
10
|
+
): Promise<{ raw: string; json?: unknown } | undefined> {
|
|
11
|
+
if (input.kind === "none") return undefined;
|
|
12
|
+
if (input.kind === "data") return { raw: input.data };
|
|
13
|
+
|
|
14
|
+
const text = await Bun.file(input.path).text();
|
|
15
|
+
return { raw: text };
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function parseBodyAsJsonOrYaml(text: string): unknown {
|
|
19
|
+
const trimmed = text.trimStart();
|
|
20
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
21
|
+
return JSON.parse(text);
|
|
22
|
+
}
|
|
23
|
+
return YAML.parse(text);
|
|
24
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { listAuthSchemes } from "../auth-schemes.ts";
|
|
2
|
+
import { deriveCapabilities } from "../capabilities.ts";
|
|
3
|
+
import { buildCommandsIndex } from "../command-index.ts";
|
|
4
|
+
import { buildCommandModel } from "../command-model.ts";
|
|
5
|
+
import { planOperations } from "../naming.ts";
|
|
6
|
+
import { indexOperations } from "../operations.ts";
|
|
7
|
+
import { buildSchemaOutput } from "../schema.ts";
|
|
8
|
+
import { listServers } from "../server.ts";
|
|
9
|
+
import { loadSpec } from "../spec-loader.ts";
|
|
10
|
+
|
|
11
|
+
export type BuildRuntimeContextOptions = {
|
|
12
|
+
spec?: string;
|
|
13
|
+
embeddedSpecText?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export async function buildRuntimeContext(options: BuildRuntimeContextOptions) {
|
|
17
|
+
const loaded = await loadSpec({
|
|
18
|
+
spec: options.spec,
|
|
19
|
+
embeddedSpecText: options.embeddedSpecText,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const operations = indexOperations(loaded.doc);
|
|
23
|
+
const servers = listServers(loaded.doc);
|
|
24
|
+
const authSchemes = listAuthSchemes(loaded.doc);
|
|
25
|
+
const planned = planOperations(operations);
|
|
26
|
+
const commands = buildCommandModel(planned, {
|
|
27
|
+
specId: loaded.id,
|
|
28
|
+
globalSecurity: loaded.doc.security,
|
|
29
|
+
authSchemes,
|
|
30
|
+
});
|
|
31
|
+
const commandsIndex = buildCommandsIndex(commands);
|
|
32
|
+
const capabilities = deriveCapabilities({
|
|
33
|
+
doc: loaded.doc,
|
|
34
|
+
servers,
|
|
35
|
+
authSchemes,
|
|
36
|
+
operations,
|
|
37
|
+
commands,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const schema = buildSchemaOutput(
|
|
41
|
+
loaded,
|
|
42
|
+
operations,
|
|
43
|
+
planned,
|
|
44
|
+
servers,
|
|
45
|
+
authSchemes,
|
|
46
|
+
commands,
|
|
47
|
+
commandsIndex,
|
|
48
|
+
capabilities,
|
|
49
|
+
);
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
loaded,
|
|
53
|
+
operations,
|
|
54
|
+
servers,
|
|
55
|
+
authSchemes,
|
|
56
|
+
planned,
|
|
57
|
+
commands,
|
|
58
|
+
commandsIndex,
|
|
59
|
+
capabilities,
|
|
60
|
+
schema,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import type { CommandAction } from "../command-model.ts";
|
|
2
|
+
|
|
3
|
+
import { buildRequest } from "./request.ts";
|
|
4
|
+
|
|
5
|
+
export type ExecuteInput = {
|
|
6
|
+
action: CommandAction;
|
|
7
|
+
positionalValues: string[];
|
|
8
|
+
flagValues: Record<string, unknown>;
|
|
9
|
+
globals: import("./request.ts").RuntimeGlobals;
|
|
10
|
+
servers: import("../server.ts").ServerInfo[];
|
|
11
|
+
authSchemes: import("../auth-schemes.ts").AuthScheme[];
|
|
12
|
+
specId: string;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export async function executeAction(input: ExecuteInput): Promise<void> {
|
|
16
|
+
try {
|
|
17
|
+
const { request, curl } = await buildRequest({
|
|
18
|
+
specId: input.specId,
|
|
19
|
+
action: input.action,
|
|
20
|
+
positionalValues: input.positionalValues,
|
|
21
|
+
flagValues: input.flagValues,
|
|
22
|
+
globals: input.globals,
|
|
23
|
+
servers: input.servers,
|
|
24
|
+
authSchemes: input.authSchemes,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (input.globals.curl || input.globals.ocCurl) {
|
|
28
|
+
process.stdout.write(`${curl}\n`);
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (input.globals.dryRun || input.globals.ocDryRun) {
|
|
33
|
+
process.stdout.write(`${request.method} ${request.url}\n`);
|
|
34
|
+
for (const [k, v] of request.headers.entries()) {
|
|
35
|
+
process.stdout.write(`${k}: ${v}\n`);
|
|
36
|
+
}
|
|
37
|
+
if (request.body) {
|
|
38
|
+
const text = await request.clone().text();
|
|
39
|
+
if (text) process.stdout.write(`\n${text}\n`);
|
|
40
|
+
}
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const timeoutMs = input.globals.timeout
|
|
45
|
+
? Number(input.globals.timeout)
|
|
46
|
+
: input.globals.ocTimeout
|
|
47
|
+
? Number(input.globals.ocTimeout)
|
|
48
|
+
: undefined;
|
|
49
|
+
let timeout: Timer | undefined;
|
|
50
|
+
let controller: AbortController | undefined;
|
|
51
|
+
if (timeoutMs && Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
52
|
+
controller = new AbortController();
|
|
53
|
+
timeout = setTimeout(() => controller?.abort(), timeoutMs);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
const res = await fetch(request, { signal: controller?.signal });
|
|
58
|
+
const contentType = res.headers.get("content-type") ?? "";
|
|
59
|
+
const status = res.status;
|
|
60
|
+
|
|
61
|
+
const text = await res.text();
|
|
62
|
+
let body: unknown = text;
|
|
63
|
+
let parsedJson: unknown | undefined;
|
|
64
|
+
|
|
65
|
+
if (contentType.includes("json")) {
|
|
66
|
+
try {
|
|
67
|
+
parsedJson = text ? JSON.parse(text) : null;
|
|
68
|
+
body = parsedJson;
|
|
69
|
+
} catch {
|
|
70
|
+
// keep as text
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (!res.ok) {
|
|
75
|
+
if (input.globals.json) {
|
|
76
|
+
process.stdout.write(
|
|
77
|
+
`${JSON.stringify({
|
|
78
|
+
status,
|
|
79
|
+
body,
|
|
80
|
+
headers:
|
|
81
|
+
input.globals.headers || input.globals.ocHeaders
|
|
82
|
+
? Object.fromEntries(res.headers.entries())
|
|
83
|
+
: undefined,
|
|
84
|
+
})}\n`,
|
|
85
|
+
);
|
|
86
|
+
} else {
|
|
87
|
+
process.stderr.write(`HTTP ${status}\n`);
|
|
88
|
+
process.stderr.write(
|
|
89
|
+
`${typeof body === "string" ? body : JSON.stringify(body, null, 2)}\n`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
process.exitCode = 1;
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (input.globals.json) {
|
|
97
|
+
const payload: unknown =
|
|
98
|
+
input.globals.status ||
|
|
99
|
+
input.globals.headers ||
|
|
100
|
+
input.globals.ocStatus ||
|
|
101
|
+
input.globals.ocHeaders
|
|
102
|
+
? {
|
|
103
|
+
status:
|
|
104
|
+
input.globals.status || input.globals.ocStatus
|
|
105
|
+
? status
|
|
106
|
+
: undefined,
|
|
107
|
+
headers:
|
|
108
|
+
input.globals.headers || input.globals.ocHeaders
|
|
109
|
+
? Object.fromEntries(res.headers.entries())
|
|
110
|
+
: undefined,
|
|
111
|
+
body,
|
|
112
|
+
}
|
|
113
|
+
: body;
|
|
114
|
+
|
|
115
|
+
process.stdout.write(`${JSON.stringify(payload)}\n`);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// default (human + agent readable)
|
|
120
|
+
if (typeof parsedJson !== "undefined") {
|
|
121
|
+
process.stdout.write(`${JSON.stringify(parsedJson, null, 2)}\n`);
|
|
122
|
+
} else {
|
|
123
|
+
process.stdout.write(text);
|
|
124
|
+
if (!text.endsWith("\n")) process.stdout.write("\n");
|
|
125
|
+
}
|
|
126
|
+
} finally {
|
|
127
|
+
if (timeout) clearTimeout(timeout);
|
|
128
|
+
}
|
|
129
|
+
} catch (err) {
|
|
130
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
131
|
+
if (input.globals.json) {
|
|
132
|
+
process.stdout.write(`${JSON.stringify({ error: message })}\n`);
|
|
133
|
+
} else {
|
|
134
|
+
process.stderr.write(`error: ${message}\n`);
|
|
135
|
+
}
|
|
136
|
+
process.exitCode = 1;
|
|
137
|
+
}
|
|
138
|
+
}
|