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.
Files changed (76) hide show
  1. package/CLAUDE.md +111 -0
  2. package/PLAN.md +274 -0
  3. package/README.md +474 -0
  4. package/biome.jsonc +1 -0
  5. package/bun.lock +98 -0
  6. package/cli.ts +74 -0
  7. package/fixtures/openapi-array-items.json +22 -0
  8. package/fixtures/openapi-auth.json +34 -0
  9. package/fixtures/openapi-body.json +41 -0
  10. package/fixtures/openapi-collision.json +21 -0
  11. package/fixtures/openapi-oauth.json +54 -0
  12. package/fixtures/openapi-servers.json +35 -0
  13. package/fixtures/openapi.json +87 -0
  14. package/index.ts +1 -0
  15. package/package.json +27 -0
  16. package/scripts/smoke-specs.ts +64 -0
  17. package/src/cli/auth-requirements.test.ts +27 -0
  18. package/src/cli/auth-requirements.ts +91 -0
  19. package/src/cli/auth-schemes.test.ts +66 -0
  20. package/src/cli/auth-schemes.ts +187 -0
  21. package/src/cli/capabilities.test.ts +94 -0
  22. package/src/cli/capabilities.ts +88 -0
  23. package/src/cli/command-id.test.ts +32 -0
  24. package/src/cli/command-id.ts +16 -0
  25. package/src/cli/command-index.ts +19 -0
  26. package/src/cli/command-model.test.ts +44 -0
  27. package/src/cli/command-model.ts +128 -0
  28. package/src/cli/compile.ts +119 -0
  29. package/src/cli/crypto.ts +9 -0
  30. package/src/cli/derive-name.ts +101 -0
  31. package/src/cli/exec.ts +72 -0
  32. package/src/cli/main.ts +336 -0
  33. package/src/cli/naming.test.ts +86 -0
  34. package/src/cli/naming.ts +224 -0
  35. package/src/cli/operations.test.ts +57 -0
  36. package/src/cli/operations.ts +152 -0
  37. package/src/cli/params.test.ts +70 -0
  38. package/src/cli/params.ts +71 -0
  39. package/src/cli/pluralize.ts +41 -0
  40. package/src/cli/positional.test.ts +65 -0
  41. package/src/cli/positional.ts +75 -0
  42. package/src/cli/request-body.test.ts +35 -0
  43. package/src/cli/request-body.ts +94 -0
  44. package/src/cli/runtime/argv.ts +14 -0
  45. package/src/cli/runtime/auth/resolve.ts +31 -0
  46. package/src/cli/runtime/body.ts +24 -0
  47. package/src/cli/runtime/collect.ts +6 -0
  48. package/src/cli/runtime/context.ts +62 -0
  49. package/src/cli/runtime/execute.ts +138 -0
  50. package/src/cli/runtime/generated.ts +200 -0
  51. package/src/cli/runtime/headers.ts +37 -0
  52. package/src/cli/runtime/index.ts +3 -0
  53. package/src/cli/runtime/profile/secrets.ts +42 -0
  54. package/src/cli/runtime/profile/store.ts +98 -0
  55. package/src/cli/runtime/request.test.ts +153 -0
  56. package/src/cli/runtime/request.ts +487 -0
  57. package/src/cli/runtime/server-url.ts +44 -0
  58. package/src/cli/runtime/template.ts +26 -0
  59. package/src/cli/runtime/validate/ajv.ts +13 -0
  60. package/src/cli/runtime/validate/coerce.ts +71 -0
  61. package/src/cli/runtime/validate/error.ts +29 -0
  62. package/src/cli/runtime/validate/index.ts +4 -0
  63. package/src/cli/runtime/validate/schema.ts +54 -0
  64. package/src/cli/schema-shape.ts +36 -0
  65. package/src/cli/schema.ts +76 -0
  66. package/src/cli/server.test.ts +35 -0
  67. package/src/cli/server.ts +88 -0
  68. package/src/cli/spec-id.ts +12 -0
  69. package/src/cli/spec-loader.ts +58 -0
  70. package/src/cli/stable-json.ts +35 -0
  71. package/src/cli/strings.ts +21 -0
  72. package/src/cli/types.ts +59 -0
  73. package/src/compiled.ts +23 -0
  74. package/src/macros/env.ts +25 -0
  75. package/src/macros/spec.ts +17 -0
  76. 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,6 @@
1
+ export function collectRepeatable(
2
+ value: string,
3
+ previous: string[] | undefined,
4
+ ): string[] {
5
+ return [...(previous ?? []), value];
6
+ }
@@ -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
+ }