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.
Files changed (42) hide show
  1. package/README.md +83 -49
  2. package/cli.ts +4 -10
  3. package/package.json +8 -2
  4. package/src/cli/compile.ts +5 -28
  5. package/src/cli/derive-name.ts +2 -2
  6. package/src/cli/exec.ts +1 -1
  7. package/src/cli/main.ts +12 -27
  8. package/src/cli/runtime/auth/resolve.ts +10 -2
  9. package/src/cli/runtime/body-flags.ts +176 -0
  10. package/src/cli/runtime/execute.ts +17 -22
  11. package/src/cli/runtime/generated.ts +23 -54
  12. package/src/cli/runtime/profile/secrets.ts +1 -1
  13. package/src/cli/runtime/profile/store.ts +1 -1
  14. package/src/cli/runtime/request.ts +48 -80
  15. package/src/cli/stable-json.ts +2 -2
  16. package/src/compiled.ts +13 -15
  17. package/src/macros/env.ts +0 -4
  18. package/CLAUDE.md +0 -111
  19. package/PLAN.md +0 -274
  20. package/biome.jsonc +0 -1
  21. package/bun.lock +0 -98
  22. package/fixtures/openapi-array-items.json +0 -22
  23. package/fixtures/openapi-auth.json +0 -34
  24. package/fixtures/openapi-body.json +0 -41
  25. package/fixtures/openapi-collision.json +0 -21
  26. package/fixtures/openapi-oauth.json +0 -54
  27. package/fixtures/openapi-servers.json +0 -35
  28. package/fixtures/openapi.json +0 -87
  29. package/scripts/smoke-specs.ts +0 -64
  30. package/src/cli/auth-requirements.test.ts +0 -27
  31. package/src/cli/auth-schemes.test.ts +0 -66
  32. package/src/cli/capabilities.test.ts +0 -94
  33. package/src/cli/command-id.test.ts +0 -32
  34. package/src/cli/command-model.test.ts +0 -44
  35. package/src/cli/naming.test.ts +0 -86
  36. package/src/cli/operations.test.ts +0 -57
  37. package/src/cli/params.test.ts +0 -70
  38. package/src/cli/positional.test.ts +0 -65
  39. package/src/cli/request-body.test.ts +0 -35
  40. package/src/cli/runtime/request.test.ts +0 -153
  41. package/src/cli/server.test.ts +0 -35
  42. 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 { buildRequest } from "./request.ts";
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 || input.globals.ocCurl) {
32
+ if (input.globals.curl) {
28
33
  process.stdout.write(`${curl}\n`);
29
34
  return;
30
35
  }
31
36
 
32
- if (input.globals.dryRun || input.globals.ocDryRun) {
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
- : input.globals.ocTimeout
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
- input.globals.headers || input.globals.ocHeaders
82
- ? Object.fromEntries(res.headers.entries())
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
- 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,
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 parser = (raw: string) => coerceValue(raw, itemType);
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
- // Some APIs have parameters like `accept`, `timeout`, etc. We always add
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
- if (action.requestBody) {
123
- cmd
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
- // Expanded JSON body flags (only for simple object bodies).
145
- const schema = action.requestBodySchema;
146
- if (schema && schema.type === "object" && schema.properties) {
147
- for (const [name, propSchema] of Object.entries(schema.properties)) {
148
- if (!name || typeof name !== "string") continue;
149
- if (!propSchema || typeof propSchema !== "object") continue;
150
- const t = (propSchema as { type?: unknown }).type;
151
- if (
152
- t !== "string" &&
153
- t !== "number" &&
154
- t !== "integer" &&
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: { ...local, __body: bodyFlags },
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
  }
@@ -6,7 +6,7 @@ export type SecretKey = {
6
6
  };
7
7
 
8
8
  export function secretServiceForSpec(specId: string): string {
9
- return `opencli:${specId}`;
9
+ return `specli:${specId}`;
10
10
  }
11
11
 
12
12
  export function tokenSecretKey(specId: string, profile: string): SecretKey {
@@ -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/opencli`;
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 (may conflict with operation 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
- const serverVars = parseKeyValuePairs(input.globals.serverVar);
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 ?? input.globals.ocAccept;
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 ?? input.globals.ocData;
285
- const file = input.globals.file ?? input.globals.ocFile;
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
- const rawBodyFlags =
291
- typeof input.flagValues.__body === "object" && input.flagValues.__body
292
- ? (input.flagValues.__body as Record<string, unknown>)
293
- : {};
294
-
295
- const bodyFlags: Record<string, unknown> = Object.fromEntries(
296
- Object.entries(rawBodyFlags).filter(
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("Use either --data/--file or --body-* flags (not both)");
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 --body-* flags.",
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
- "Expanded body flags are only supported for JSON request bodies. Use --content-type application/json or --data/--file.",
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
- // Friendly "missing required field" errors for expanded flags.
330
- if (schema?.required && Array.isArray(schema.required)) {
331
- for (const name of schema.required) {
332
- const n = String(name);
333
- const key = `body${n[0]?.toUpperCase()}${n.slice(1)}`;
334
- if (!(key in bodyFlags)) {
335
- throw new Error(
336
- `Missing required body field '${n}'. Provide --body-${n} or use --data/--file.`,
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
- const built: Record<string, unknown> = {};
343
- for (const [k, v] of Object.entries(bodyFlags)) {
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 (input.globals.data ?? input.globals.ocData) === "string" ||
417
- typeof (input.globals.file ?? input.globals.ocFile) === "string"
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
- // Profile-aware auth resolution (flags override profile).
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
 
@@ -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 { __opencli_circular: true };
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 { __opencli_circular: true };
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>;