specli 0.0.1 → 0.0.2
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 +83 -49
- package/cli.ts +4 -10
- package/package.json +8 -2
- package/src/cli/compile.ts +5 -28
- package/src/cli/derive-name.ts +2 -2
- package/src/cli/exec.ts +1 -1
- package/src/cli/main.ts +12 -27
- package/src/cli/runtime/auth/resolve.ts +10 -2
- package/src/cli/runtime/body-flags.ts +176 -0
- package/src/cli/runtime/execute.ts +17 -22
- package/src/cli/runtime/generated.ts +23 -54
- package/src/cli/runtime/profile/secrets.ts +1 -1
- package/src/cli/runtime/profile/store.ts +1 -1
- package/src/cli/runtime/request.ts +48 -80
- package/src/cli/stable-json.ts +2 -2
- package/src/compiled.ts +13 -15
- package/src/macros/env.ts +0 -4
- package/CLAUDE.md +0 -111
- package/PLAN.md +0 -274
- package/biome.jsonc +0 -1
- package/bun.lock +0 -98
- package/fixtures/openapi-array-items.json +0 -22
- package/fixtures/openapi-auth.json +0 -34
- package/fixtures/openapi-body.json +0 -41
- package/fixtures/openapi-collision.json +0 -21
- package/fixtures/openapi-oauth.json +0 -54
- package/fixtures/openapi-servers.json +0 -35
- package/fixtures/openapi.json +0 -87
- package/scripts/smoke-specs.ts +0 -64
- package/src/cli/auth-requirements.test.ts +0 -27
- package/src/cli/auth-schemes.test.ts +0 -66
- package/src/cli/capabilities.test.ts +0 -94
- package/src/cli/command-id.test.ts +0 -32
- package/src/cli/command-model.test.ts +0 -44
- package/src/cli/naming.test.ts +0 -86
- package/src/cli/operations.test.ts +0 -57
- package/src/cli/params.test.ts +0 -70
- package/src/cli/positional.test.ts +0 -65
- package/src/cli/request-body.test.ts +0 -35
- package/src/cli/runtime/request.test.ts +0 -153
- package/src/cli/server.test.ts +0 -35
- package/tsconfig.json +0 -29
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Body flag generation and parsing utilities.
|
|
3
|
+
*
|
|
4
|
+
* Generates CLI flags from JSON schema properties and parses
|
|
5
|
+
* dot-notation flags back into nested objects.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
type JsonSchema = {
|
|
9
|
+
type?: string;
|
|
10
|
+
properties?: Record<string, JsonSchema>;
|
|
11
|
+
items?: JsonSchema;
|
|
12
|
+
required?: string[];
|
|
13
|
+
description?: string;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export type BodyFlagDef = {
|
|
17
|
+
flag: string; // e.g. "--name" or "--address.street"
|
|
18
|
+
path: string[]; // e.g. ["name"] or ["address", "street"]
|
|
19
|
+
type: "string" | "number" | "integer" | "boolean";
|
|
20
|
+
description: string;
|
|
21
|
+
required: boolean;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Generate flag definitions from a JSON schema.
|
|
26
|
+
* Recursively handles nested objects using dot notation.
|
|
27
|
+
*/
|
|
28
|
+
export function generateBodyFlags(
|
|
29
|
+
schema: JsonSchema | undefined,
|
|
30
|
+
reservedFlags: Set<string>,
|
|
31
|
+
): BodyFlagDef[] {
|
|
32
|
+
if (!schema || schema.type !== "object" || !schema.properties) {
|
|
33
|
+
return [];
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const flags: BodyFlagDef[] = [];
|
|
37
|
+
const requiredSet = new Set(schema.required ?? []);
|
|
38
|
+
|
|
39
|
+
collectFlags(schema.properties, [], requiredSet, flags, reservedFlags);
|
|
40
|
+
|
|
41
|
+
return flags;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function collectFlags(
|
|
45
|
+
properties: Record<string, JsonSchema>,
|
|
46
|
+
pathPrefix: string[],
|
|
47
|
+
requiredAtRoot: Set<string>,
|
|
48
|
+
out: BodyFlagDef[],
|
|
49
|
+
reservedFlags: Set<string>,
|
|
50
|
+
): void {
|
|
51
|
+
for (const [name, propSchema] of Object.entries(properties)) {
|
|
52
|
+
if (!name || typeof name !== "string") continue;
|
|
53
|
+
if (!propSchema || typeof propSchema !== "object") continue;
|
|
54
|
+
|
|
55
|
+
const path = [...pathPrefix, name];
|
|
56
|
+
const flagName = `--${path.join(".")}`;
|
|
57
|
+
|
|
58
|
+
// Skip if this flag would conflict with an operation parameter
|
|
59
|
+
if (reservedFlags.has(flagName)) continue;
|
|
60
|
+
|
|
61
|
+
const t = propSchema.type;
|
|
62
|
+
|
|
63
|
+
if (t === "object" && propSchema.properties) {
|
|
64
|
+
// Recurse into nested object
|
|
65
|
+
const nestedRequired = new Set(propSchema.required ?? []);
|
|
66
|
+
collectFlags(
|
|
67
|
+
propSchema.properties,
|
|
68
|
+
path,
|
|
69
|
+
nestedRequired,
|
|
70
|
+
out,
|
|
71
|
+
reservedFlags,
|
|
72
|
+
);
|
|
73
|
+
} else if (
|
|
74
|
+
t === "string" ||
|
|
75
|
+
t === "number" ||
|
|
76
|
+
t === "integer" ||
|
|
77
|
+
t === "boolean"
|
|
78
|
+
) {
|
|
79
|
+
// Leaf property - generate a flag
|
|
80
|
+
const isRequired =
|
|
81
|
+
pathPrefix.length === 0 ? requiredAtRoot.has(name) : false;
|
|
82
|
+
|
|
83
|
+
out.push({
|
|
84
|
+
flag: flagName,
|
|
85
|
+
path,
|
|
86
|
+
type: t,
|
|
87
|
+
description: propSchema.description ?? `Body field '${path.join(".")}'`,
|
|
88
|
+
required: isRequired,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
// Skip arrays and other complex types for now
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Parse flag values with dot notation into a nested object.
|
|
97
|
+
*
|
|
98
|
+
* Example:
|
|
99
|
+
* { "address.street": "123 Main", "address.city": "NYC", "name": "Ada" }
|
|
100
|
+
* Becomes:
|
|
101
|
+
* { address: { street: "123 Main", city: "NYC" }, name: "Ada" }
|
|
102
|
+
*/
|
|
103
|
+
export function parseDotNotationFlags(
|
|
104
|
+
flagValues: Record<string, unknown>,
|
|
105
|
+
flagDefs: BodyFlagDef[],
|
|
106
|
+
): Record<string, unknown> {
|
|
107
|
+
const result: Record<string, unknown> = {};
|
|
108
|
+
|
|
109
|
+
for (const def of flagDefs) {
|
|
110
|
+
// Commander keeps dots in option names: --address.street -> "address.street"
|
|
111
|
+
const dotKey = def.path.join(".");
|
|
112
|
+
const value = flagValues[dotKey];
|
|
113
|
+
|
|
114
|
+
if (value === undefined) continue;
|
|
115
|
+
|
|
116
|
+
setNestedValue(result, def.path, value, def.type);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Set a value at a nested path, creating intermediate objects as needed.
|
|
124
|
+
*/
|
|
125
|
+
function setNestedValue(
|
|
126
|
+
obj: Record<string, unknown>,
|
|
127
|
+
path: string[],
|
|
128
|
+
value: unknown,
|
|
129
|
+
type: string,
|
|
130
|
+
): void {
|
|
131
|
+
let current = obj;
|
|
132
|
+
|
|
133
|
+
for (let i = 0; i < path.length - 1; i++) {
|
|
134
|
+
const key = path[i] as string;
|
|
135
|
+
if (!(key in current) || typeof current[key] !== "object") {
|
|
136
|
+
current[key] = {};
|
|
137
|
+
}
|
|
138
|
+
current = current[key] as Record<string, unknown>;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const finalKey = path[path.length - 1] as string;
|
|
142
|
+
|
|
143
|
+
// Coerce value based on type
|
|
144
|
+
if (type === "boolean") {
|
|
145
|
+
current[finalKey] = true;
|
|
146
|
+
} else if (type === "integer") {
|
|
147
|
+
current[finalKey] = Number.parseInt(String(value), 10);
|
|
148
|
+
} else if (type === "number") {
|
|
149
|
+
current[finalKey] = Number(String(value));
|
|
150
|
+
} else {
|
|
151
|
+
current[finalKey] = String(value);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Check if all required fields are present.
|
|
157
|
+
* Returns list of missing field paths.
|
|
158
|
+
*/
|
|
159
|
+
export function findMissingRequired(
|
|
160
|
+
flagValues: Record<string, unknown>,
|
|
161
|
+
flagDefs: BodyFlagDef[],
|
|
162
|
+
): string[] {
|
|
163
|
+
const missing: string[] = [];
|
|
164
|
+
|
|
165
|
+
for (const def of flagDefs) {
|
|
166
|
+
if (!def.required) continue;
|
|
167
|
+
|
|
168
|
+
// Commander keeps dots in option names: --address.street -> "address.street"
|
|
169
|
+
const dotKey = def.path.join(".");
|
|
170
|
+
if (flagValues[dotKey] === undefined) {
|
|
171
|
+
missing.push(dotKey);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return missing;
|
|
176
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { CommandAction } from "../command-model.ts";
|
|
2
2
|
|
|
3
|
-
import {
|
|
3
|
+
import type { BodyFlagDef } from "./body-flags.ts";
|
|
4
|
+
import { buildRequest, type EmbeddedDefaults } from "./request.ts";
|
|
4
5
|
|
|
5
6
|
export type ExecuteInput = {
|
|
6
7
|
action: CommandAction;
|
|
@@ -10,6 +11,8 @@ export type ExecuteInput = {
|
|
|
10
11
|
servers: import("../server.ts").ServerInfo[];
|
|
11
12
|
authSchemes: import("../auth-schemes.ts").AuthScheme[];
|
|
12
13
|
specId: string;
|
|
14
|
+
embeddedDefaults?: EmbeddedDefaults;
|
|
15
|
+
bodyFlagDefs?: BodyFlagDef[];
|
|
13
16
|
};
|
|
14
17
|
|
|
15
18
|
export async function executeAction(input: ExecuteInput): Promise<void> {
|
|
@@ -22,14 +25,16 @@ export async function executeAction(input: ExecuteInput): Promise<void> {
|
|
|
22
25
|
globals: input.globals,
|
|
23
26
|
servers: input.servers,
|
|
24
27
|
authSchemes: input.authSchemes,
|
|
28
|
+
embeddedDefaults: input.embeddedDefaults,
|
|
29
|
+
bodyFlagDefs: input.bodyFlagDefs,
|
|
25
30
|
});
|
|
26
31
|
|
|
27
|
-
if (input.globals.curl
|
|
32
|
+
if (input.globals.curl) {
|
|
28
33
|
process.stdout.write(`${curl}\n`);
|
|
29
34
|
return;
|
|
30
35
|
}
|
|
31
36
|
|
|
32
|
-
if (input.globals.dryRun
|
|
37
|
+
if (input.globals.dryRun) {
|
|
33
38
|
process.stdout.write(`${request.method} ${request.url}\n`);
|
|
34
39
|
for (const [k, v] of request.headers.entries()) {
|
|
35
40
|
process.stdout.write(`${k}: ${v}\n`);
|
|
@@ -43,9 +48,7 @@ export async function executeAction(input: ExecuteInput): Promise<void> {
|
|
|
43
48
|
|
|
44
49
|
const timeoutMs = input.globals.timeout
|
|
45
50
|
? Number(input.globals.timeout)
|
|
46
|
-
:
|
|
47
|
-
? Number(input.globals.ocTimeout)
|
|
48
|
-
: undefined;
|
|
51
|
+
: undefined;
|
|
49
52
|
let timeout: Timer | undefined;
|
|
50
53
|
let controller: AbortController | undefined;
|
|
51
54
|
if (timeoutMs && Number.isFinite(timeoutMs) && timeoutMs > 0) {
|
|
@@ -77,10 +80,9 @@ export async function executeAction(input: ExecuteInput): Promise<void> {
|
|
|
77
80
|
`${JSON.stringify({
|
|
78
81
|
status,
|
|
79
82
|
body,
|
|
80
|
-
headers:
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
: undefined,
|
|
83
|
+
headers: input.globals.headers
|
|
84
|
+
? Object.fromEntries(res.headers.entries())
|
|
85
|
+
: undefined,
|
|
84
86
|
})}\n`,
|
|
85
87
|
);
|
|
86
88
|
} else {
|
|
@@ -95,19 +97,12 @@ export async function executeAction(input: ExecuteInput): Promise<void> {
|
|
|
95
97
|
|
|
96
98
|
if (input.globals.json) {
|
|
97
99
|
const payload: unknown =
|
|
98
|
-
input.globals.status ||
|
|
99
|
-
input.globals.headers ||
|
|
100
|
-
input.globals.ocStatus ||
|
|
101
|
-
input.globals.ocHeaders
|
|
100
|
+
input.globals.status || input.globals.headers
|
|
102
101
|
? {
|
|
103
|
-
status:
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
headers:
|
|
108
|
-
input.globals.headers || input.globals.ocHeaders
|
|
109
|
-
? Object.fromEntries(res.headers.entries())
|
|
110
|
-
: undefined,
|
|
102
|
+
status: input.globals.status ? status : undefined,
|
|
103
|
+
headers: input.globals.headers
|
|
104
|
+
? Object.fromEntries(res.headers.entries())
|
|
105
|
+
: undefined,
|
|
111
106
|
body,
|
|
112
107
|
}
|
|
113
108
|
: body;
|
|
@@ -4,8 +4,10 @@ import type { AuthScheme } from "../auth-schemes.ts";
|
|
|
4
4
|
import type { CommandModel } from "../command-model.ts";
|
|
5
5
|
import type { ServerInfo } from "../server.ts";
|
|
6
6
|
|
|
7
|
+
import { type BodyFlagDef, generateBodyFlags } from "./body-flags.ts";
|
|
7
8
|
import { collectRepeatable } from "./collect.ts";
|
|
8
9
|
import { executeAction } from "./execute.ts";
|
|
10
|
+
import type { EmbeddedDefaults } from "./request.ts";
|
|
9
11
|
import { coerceArrayInput, coerceValue } from "./validate/index.ts";
|
|
10
12
|
|
|
11
13
|
export type GeneratedCliContext = {
|
|
@@ -13,6 +15,7 @@ export type GeneratedCliContext = {
|
|
|
13
15
|
authSchemes: AuthScheme[];
|
|
14
16
|
commands: CommandModel;
|
|
15
17
|
specId: string;
|
|
18
|
+
embeddedDefaults?: EmbeddedDefaults;
|
|
16
19
|
};
|
|
17
20
|
|
|
18
21
|
export function addGeneratedCommands(
|
|
@@ -47,7 +50,8 @@ export function addGeneratedCommands(
|
|
|
47
50
|
|
|
48
51
|
const isArray = flag.type === "array";
|
|
49
52
|
const itemType = flag.itemType ?? "string";
|
|
50
|
-
const
|
|
53
|
+
const flagType = isArray ? itemType : flag.type;
|
|
54
|
+
const parser = (raw: string) => coerceValue(raw, flagType);
|
|
51
55
|
|
|
52
56
|
if (isArray) {
|
|
53
57
|
const key = `${opt} <value>`;
|
|
@@ -77,22 +81,7 @@ export function addGeneratedCommands(
|
|
|
77
81
|
const reservedFlags = new Set(action.flags.map((f) => f.flag));
|
|
78
82
|
|
|
79
83
|
// Common curl-replacement options.
|
|
80
|
-
//
|
|
81
|
-
// namespaced variants (`--oc-*`) and only add the short versions when they
|
|
82
|
-
// do not conflict with operation flags.
|
|
83
|
-
cmd
|
|
84
|
-
.option(
|
|
85
|
-
"--oc-header <header>",
|
|
86
|
-
"Extra header (repeatable)",
|
|
87
|
-
collectRepeatable,
|
|
88
|
-
)
|
|
89
|
-
.option("--oc-accept <type>", "Override Accept header")
|
|
90
|
-
.option("--oc-status", "Include status in --json output")
|
|
91
|
-
.option("--oc-headers", "Include headers in --json output")
|
|
92
|
-
.option("--oc-dry-run", "Print request without sending")
|
|
93
|
-
.option("--oc-curl", "Print curl command without sending")
|
|
94
|
-
.option("--oc-timeout <ms>", "Request timeout in milliseconds");
|
|
95
|
-
|
|
84
|
+
// Only add flags that don't conflict with operation flags.
|
|
96
85
|
if (!reservedFlags.has("--header")) {
|
|
97
86
|
cmd.option(
|
|
98
87
|
"--header <header>",
|
|
@@ -119,15 +108,10 @@ export function addGeneratedCommands(
|
|
|
119
108
|
cmd.option("--timeout <ms>", "Request timeout in milliseconds");
|
|
120
109
|
}
|
|
121
110
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
.option("--oc-data <data>", "Inline request body")
|
|
125
|
-
.option("--oc-file <path>", "Request body from file")
|
|
126
|
-
.option(
|
|
127
|
-
"--oc-content-type <type>",
|
|
128
|
-
"Override Content-Type (defaults from OpenAPI)",
|
|
129
|
-
);
|
|
111
|
+
// Track body flag definitions for this action
|
|
112
|
+
let bodyFlagDefs: BodyFlagDef[] = [];
|
|
130
113
|
|
|
114
|
+
if (action.requestBody) {
|
|
131
115
|
if (!reservedFlags.has("--data")) {
|
|
132
116
|
cmd.option("--data <data>", "Inline request body");
|
|
133
117
|
}
|
|
@@ -141,28 +125,17 @@ export function addGeneratedCommands(
|
|
|
141
125
|
);
|
|
142
126
|
}
|
|
143
127
|
|
|
144
|
-
//
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
t !== "boolean"
|
|
156
|
-
) {
|
|
157
|
-
continue;
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const flagName = `--body-${name}`;
|
|
161
|
-
if (t === "boolean") {
|
|
162
|
-
cmd.option(flagName, `Body field '${name}'`);
|
|
163
|
-
} else {
|
|
164
|
-
cmd.option(`${flagName} <value>`, `Body field '${name}'`);
|
|
165
|
-
}
|
|
128
|
+
// Generate body flags from schema (recursive with dot notation)
|
|
129
|
+
bodyFlagDefs = generateBodyFlags(
|
|
130
|
+
action.requestBodySchema,
|
|
131
|
+
reservedFlags,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
for (const def of bodyFlagDefs) {
|
|
135
|
+
if (def.type === "boolean") {
|
|
136
|
+
cmd.option(def.flag, def.description);
|
|
137
|
+
} else {
|
|
138
|
+
cmd.option(`${def.flag} <value>`, def.description);
|
|
166
139
|
}
|
|
167
140
|
}
|
|
168
141
|
}
|
|
@@ -179,20 +152,16 @@ export function addGeneratedCommands(
|
|
|
179
152
|
const globals = command.optsWithGlobals();
|
|
180
153
|
const local = command.opts();
|
|
181
154
|
|
|
182
|
-
const bodyFlags: Record<string, unknown> = {};
|
|
183
|
-
for (const key of Object.keys(local)) {
|
|
184
|
-
if (!key.startsWith("body")) continue;
|
|
185
|
-
bodyFlags[key] = local[key];
|
|
186
|
-
}
|
|
187
|
-
|
|
188
155
|
await executeAction({
|
|
189
156
|
action,
|
|
190
157
|
positionalValues,
|
|
191
|
-
flagValues:
|
|
158
|
+
flagValues: local,
|
|
192
159
|
globals,
|
|
193
160
|
servers: context.servers,
|
|
194
161
|
authSchemes: context.authSchemes,
|
|
195
162
|
specId: context.specId,
|
|
163
|
+
embeddedDefaults: context.embeddedDefaults,
|
|
164
|
+
bodyFlagDefs,
|
|
196
165
|
});
|
|
197
166
|
});
|
|
198
167
|
}
|
|
@@ -17,7 +17,7 @@ function configDir(): string {
|
|
|
17
17
|
// Keep it simple (v1). We can move to env-paths later.
|
|
18
18
|
const home = process.env.HOME;
|
|
19
19
|
if (!home) throw new Error("Missing HOME env var");
|
|
20
|
-
return `${home}/.config/
|
|
20
|
+
return `${home}/.config/specli`;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
function configPathJson(): string {
|
|
@@ -19,7 +19,7 @@ export type RuntimeGlobals = {
|
|
|
19
19
|
server?: string;
|
|
20
20
|
serverVar?: string[];
|
|
21
21
|
|
|
22
|
-
// Common runtime flags
|
|
22
|
+
// Common runtime flags.
|
|
23
23
|
header?: string[];
|
|
24
24
|
accept?: string;
|
|
25
25
|
contentType?: string;
|
|
@@ -31,18 +31,6 @@ export type RuntimeGlobals = {
|
|
|
31
31
|
status?: boolean;
|
|
32
32
|
headers?: boolean;
|
|
33
33
|
|
|
34
|
-
// Namespaced variants for the runtime flags above.
|
|
35
|
-
ocHeader?: string[];
|
|
36
|
-
ocAccept?: string;
|
|
37
|
-
ocContentType?: string;
|
|
38
|
-
ocData?: string;
|
|
39
|
-
ocFile?: string;
|
|
40
|
-
ocTimeout?: string;
|
|
41
|
-
ocDryRun?: boolean;
|
|
42
|
-
ocCurl?: boolean;
|
|
43
|
-
ocStatus?: boolean;
|
|
44
|
-
ocHeaders?: boolean;
|
|
45
|
-
|
|
46
34
|
json?: boolean;
|
|
47
35
|
|
|
48
36
|
auth?: string;
|
|
@@ -159,6 +147,12 @@ function applyAuth(
|
|
|
159
147
|
return { headers, url };
|
|
160
148
|
}
|
|
161
149
|
|
|
150
|
+
export type EmbeddedDefaults = {
|
|
151
|
+
server?: string;
|
|
152
|
+
serverVars?: string[];
|
|
153
|
+
auth?: string;
|
|
154
|
+
};
|
|
155
|
+
|
|
162
156
|
export type BuildRequestInput = {
|
|
163
157
|
specId: string;
|
|
164
158
|
action: CommandAction;
|
|
@@ -167,6 +161,8 @@ export type BuildRequestInput = {
|
|
|
167
161
|
globals: RuntimeGlobals;
|
|
168
162
|
servers: import("../server.ts").ServerInfo[];
|
|
169
163
|
authSchemes: AuthScheme[];
|
|
164
|
+
embeddedDefaults?: EmbeddedDefaults;
|
|
165
|
+
bodyFlagDefs?: import("./body-flags.ts").BodyFlagDef[];
|
|
170
166
|
};
|
|
171
167
|
|
|
172
168
|
export async function buildRequest(
|
|
@@ -174,10 +170,16 @@ export async function buildRequest(
|
|
|
174
170
|
): Promise<{ request: Request; curl: string }> {
|
|
175
171
|
const profilesFile = await readProfiles();
|
|
176
172
|
const profile = getProfile(profilesFile, input.globals.profile);
|
|
173
|
+
const embedded = input.embeddedDefaults;
|
|
174
|
+
|
|
175
|
+
// Merge server vars: CLI flags override embedded defaults
|
|
176
|
+
const embeddedServerVars = parseKeyValuePairs(embedded?.serverVars);
|
|
177
|
+
const cliServerVars = parseKeyValuePairs(input.globals.serverVar);
|
|
178
|
+
const serverVars = { ...embeddedServerVars, ...cliServerVars };
|
|
177
179
|
|
|
178
|
-
|
|
180
|
+
// Priority: CLI flag > profile > embedded default
|
|
179
181
|
const serverUrl = resolveServerUrl({
|
|
180
|
-
serverOverride: input.globals.server ?? profile?.server,
|
|
182
|
+
serverOverride: input.globals.server ?? profile?.server ?? embedded?.server,
|
|
181
183
|
servers: input.servers,
|
|
182
184
|
serverVars,
|
|
183
185
|
});
|
|
@@ -205,7 +207,7 @@ export async function buildRequest(
|
|
|
205
207
|
);
|
|
206
208
|
|
|
207
209
|
const headers = new Headers();
|
|
208
|
-
const accept = input.globals.accept
|
|
210
|
+
const accept = input.globals.accept;
|
|
209
211
|
if (accept) headers.set("Accept", accept);
|
|
210
212
|
|
|
211
213
|
// Collect declared params for validation.
|
|
@@ -271,43 +273,36 @@ export async function buildRequest(
|
|
|
271
273
|
headers.set("Cookie", existing ? `${existing}; ${part}` : part);
|
|
272
274
|
}
|
|
273
275
|
|
|
274
|
-
const extraHeaders = [
|
|
275
|
-
...(input.globals.header ?? []),
|
|
276
|
-
...(input.globals.ocHeader ?? []),
|
|
277
|
-
].map(parseHeaderInput);
|
|
276
|
+
const extraHeaders = (input.globals.header ?? []).map(parseHeaderInput);
|
|
278
277
|
for (const { name, value } of extraHeaders) {
|
|
279
278
|
headers.set(name, value);
|
|
280
279
|
}
|
|
281
280
|
|
|
282
281
|
let body: string | undefined;
|
|
283
282
|
if (input.action.requestBody) {
|
|
284
|
-
const data = input.globals.data
|
|
285
|
-
const file = input.globals.file
|
|
283
|
+
const data = input.globals.data;
|
|
284
|
+
const file = input.globals.file;
|
|
286
285
|
|
|
287
286
|
const hasData = typeof data === "string";
|
|
288
287
|
const hasFile = typeof file === "string";
|
|
289
288
|
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
([, v]) => typeof v !== "undefined" && v !== false,
|
|
298
|
-
),
|
|
299
|
-
);
|
|
300
|
-
|
|
301
|
-
const hasExpandedBody = Object.keys(bodyFlags).length > 0;
|
|
289
|
+
// Check if any body flags were provided using the flag definitions
|
|
290
|
+
const bodyFlagDefs = input.bodyFlagDefs ?? [];
|
|
291
|
+
const hasExpandedBody = bodyFlagDefs.some((def) => {
|
|
292
|
+
// Commander keeps dots in option names: --address.street -> "address.street"
|
|
293
|
+
const dotKey = def.path.join(".");
|
|
294
|
+
return input.flagValues[dotKey] !== undefined;
|
|
295
|
+
});
|
|
302
296
|
|
|
303
297
|
if (hasData && hasFile) throw new Error("Use only one of --data or --file");
|
|
304
298
|
if (hasExpandedBody && (hasData || hasFile)) {
|
|
305
|
-
throw new Error(
|
|
299
|
+
throw new Error(
|
|
300
|
+
"Use either --data/--file or body field flags (not both)",
|
|
301
|
+
);
|
|
306
302
|
}
|
|
307
303
|
|
|
308
304
|
const contentType =
|
|
309
305
|
input.globals.contentType ??
|
|
310
|
-
input.globals.ocContentType ??
|
|
311
306
|
input.action.requestBody.preferredContentType;
|
|
312
307
|
if (contentType) headers.set("Content-Type", contentType);
|
|
313
308
|
|
|
@@ -316,57 +311,29 @@ export async function buildRequest(
|
|
|
316
311
|
if (!hasExpandedBody && !hasData && !hasFile) {
|
|
317
312
|
if (input.action.requestBody.required) {
|
|
318
313
|
throw new Error(
|
|
319
|
-
"Missing request body. Provide --data, --file, or
|
|
314
|
+
"Missing request body. Provide --data, --file, or body field flags.",
|
|
320
315
|
);
|
|
321
316
|
}
|
|
322
317
|
} else if (hasExpandedBody) {
|
|
323
318
|
if (!contentType?.includes("json")) {
|
|
324
319
|
throw new Error(
|
|
325
|
-
"
|
|
320
|
+
"Body field flags are only supported for JSON request bodies. Use --content-type application/json or --data/--file.",
|
|
326
321
|
);
|
|
327
322
|
}
|
|
328
323
|
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
}
|
|
339
|
-
}
|
|
324
|
+
// Check for missing required fields
|
|
325
|
+
const { findMissingRequired, parseDotNotationFlags } = await import(
|
|
326
|
+
"./body-flags.ts"
|
|
327
|
+
);
|
|
328
|
+
const missing = findMissingRequired(input.flagValues, bodyFlagDefs);
|
|
329
|
+
if (missing.length > 0) {
|
|
330
|
+
throw new Error(
|
|
331
|
+
`Missing required body field '${missing[0]}'. Provide --${missing[0]} or use --data/--file.`,
|
|
332
|
+
);
|
|
340
333
|
}
|
|
341
334
|
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
if (!k.startsWith("body")) continue;
|
|
345
|
-
const name = k.slice("body".length);
|
|
346
|
-
const propName = name.length
|
|
347
|
-
? name[0]?.toLowerCase() + name.slice(1)
|
|
348
|
-
: "";
|
|
349
|
-
if (!propName) continue;
|
|
350
|
-
|
|
351
|
-
const propSchema =
|
|
352
|
-
schema && schema.type === "object" && schema.properties
|
|
353
|
-
? (schema.properties as Record<string, unknown>)[propName]
|
|
354
|
-
: undefined;
|
|
355
|
-
const t =
|
|
356
|
-
propSchema && typeof propSchema === "object"
|
|
357
|
-
? (propSchema as { type?: unknown }).type
|
|
358
|
-
: "string";
|
|
359
|
-
|
|
360
|
-
if (t === "boolean") {
|
|
361
|
-
built[propName] = true;
|
|
362
|
-
} else if (t === "integer") {
|
|
363
|
-
built[propName] = Number.parseInt(String(v), 10);
|
|
364
|
-
} else if (t === "number") {
|
|
365
|
-
built[propName] = Number(String(v));
|
|
366
|
-
} else {
|
|
367
|
-
built[propName] = String(v);
|
|
368
|
-
}
|
|
369
|
-
}
|
|
335
|
+
// Build nested object from dot-notation flags
|
|
336
|
+
const built = parseDotNotationFlags(input.flagValues, bodyFlagDefs);
|
|
370
337
|
|
|
371
338
|
if (schema) {
|
|
372
339
|
const validate = ajv.compile(schema);
|
|
@@ -413,20 +380,21 @@ export async function buildRequest(
|
|
|
413
380
|
}
|
|
414
381
|
} else {
|
|
415
382
|
if (
|
|
416
|
-
typeof
|
|
417
|
-
typeof
|
|
383
|
+
typeof input.globals.data === "string" ||
|
|
384
|
+
typeof input.globals.file === "string"
|
|
418
385
|
) {
|
|
419
386
|
throw new Error("This operation does not accept a request body");
|
|
420
387
|
}
|
|
421
388
|
}
|
|
422
389
|
|
|
423
|
-
//
|
|
390
|
+
// Auth resolution priority: CLI flag > profile > embedded default
|
|
424
391
|
const resolvedAuthScheme = resolveAuthScheme(
|
|
425
392
|
input.authSchemes,
|
|
426
393
|
input.action.auth,
|
|
427
394
|
{
|
|
428
395
|
flagAuthScheme: input.globals.auth,
|
|
429
396
|
profileAuthScheme: profile?.authScheme,
|
|
397
|
+
embeddedAuthScheme: embedded?.auth,
|
|
430
398
|
},
|
|
431
399
|
);
|
|
432
400
|
|
package/src/cli/stable-json.ts
CHANGED
|
@@ -10,7 +10,7 @@ function sort(value: unknown, visiting: WeakSet<object>): unknown {
|
|
|
10
10
|
if (value === null) return null;
|
|
11
11
|
|
|
12
12
|
if (Array.isArray(value)) {
|
|
13
|
-
if (visiting.has(value)) return {
|
|
13
|
+
if (visiting.has(value)) return { __specli_circular: true };
|
|
14
14
|
visiting.add(value);
|
|
15
15
|
const out = value.map((v) => sort(v, visiting));
|
|
16
16
|
visiting.delete(value);
|
|
@@ -18,7 +18,7 @@ function sort(value: unknown, visiting: WeakSet<object>): unknown {
|
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
if (typeof value === "object") {
|
|
21
|
-
if (visiting.has(value)) return {
|
|
21
|
+
if (visiting.has(value)) return { __specli_circular: true };
|
|
22
22
|
visiting.add(value);
|
|
23
23
|
|
|
24
24
|
const obj = value as Record<string, unknown>;
|