specli 0.0.1 → 0.0.3

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 +76 -86
  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 +41 -91
  11. package/src/cli/runtime/generated.ts +28 -93
  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 +42 -152
  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
@@ -1,64 +0,0 @@
1
- #!/usr/bin/env bun
2
-
3
- const specs = [
4
- {
5
- name: "vercel",
6
- url: "https://api.vercel.com/copper/_openapi.json",
7
- },
8
- {
9
- name: "digitalocean",
10
- url: "https://raw.githubusercontent.com/digitalocean/openapi/main/specification/DigitalOcean-public.v2.yaml",
11
- },
12
- // {
13
- // name: "stripe",
14
- // url: "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.yaml",
15
- // },
16
- // NOTE: OpenAI spec is not reliably fetchable via raw.githubusercontent.com
17
- // in some environments (intermittent 404). Keep it out of the default smoke
18
- // list to avoid flaky CI.
19
- // {
20
- // name: "openai",
21
- // url: "https://raw.githubusercontent.com/openai/openai-openapi/master/openapi.yaml",
22
- // },
23
- ] as const;
24
-
25
- type Result = {
26
- spec: string;
27
- ok: boolean;
28
- ms: number;
29
- error?: string;
30
- };
31
-
32
- const results: Result[] = [];
33
-
34
- for (const spec of specs) {
35
- const start = performance.now();
36
- try {
37
- console.log(spec.url);
38
- await Bun.$`bun ./cli.ts exec ${spec.url} __schema --json --min > /dev/null`;
39
- results.push({
40
- spec: spec.name,
41
- ok: true,
42
- ms: Math.round(performance.now() - start),
43
- });
44
- } catch (err) {
45
- const message = err instanceof Error ? err.message : String(err);
46
- results.push({
47
- spec: spec.name,
48
- ok: false,
49
- ms: Math.round(performance.now() - start),
50
- error: message,
51
- });
52
- }
53
- }
54
-
55
- const allOk = results.every((r) => r.ok);
56
- for (const r of results) {
57
- if (r.ok) {
58
- process.stdout.write(`ok ${r.spec} (${r.ms}ms)\n`);
59
- } else {
60
- process.stdout.write(`fail ${r.spec} (${r.ms}ms)\n${r.error}\n`);
61
- }
62
- }
63
-
64
- process.exitCode = allOk ? 0 : 1;
@@ -1,27 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
-
3
- import { summarizeAuth } from "./auth-requirements.ts";
4
- import type { AuthScheme } from "./auth-schemes.ts";
5
-
6
- describe("summarizeAuth", () => {
7
- test("uses operation-level security when present", () => {
8
- const schemes: AuthScheme[] = [{ key: "oauth", kind: "oauth2" }];
9
-
10
- const summary = summarizeAuth(
11
- [{ oauth: ["read:ping"] }],
12
- [{ oauth: ["read:other"] }],
13
- schemes,
14
- );
15
-
16
- expect(summary.alternatives).toEqual([
17
- [{ key: "oauth", scopes: ["read:ping"] }],
18
- ]);
19
- });
20
-
21
- test("empty operation security disables auth", () => {
22
- const schemes: AuthScheme[] = [{ key: "oauth", kind: "oauth2" }];
23
-
24
- const summary = summarizeAuth([], [{ oauth: ["read:other"] }], schemes);
25
- expect(summary.alternatives).toEqual([]);
26
- });
27
- });
@@ -1,66 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
-
3
- import { listAuthSchemes } from "./auth-schemes.ts";
4
- import type { OpenApiDoc } from "./types.ts";
5
-
6
- describe("listAuthSchemes", () => {
7
- test("parses bearer + apiKey", () => {
8
- const doc: OpenApiDoc = {
9
- openapi: "3.0.3",
10
- components: {
11
- securitySchemes: {
12
- bearerAuth: {
13
- type: "http",
14
- scheme: "bearer",
15
- bearerFormat: "JWT",
16
- },
17
- apiKeyAuth: {
18
- type: "apiKey",
19
- in: "header",
20
- name: "X-API-Key",
21
- },
22
- },
23
- },
24
- };
25
-
26
- const schemes = listAuthSchemes(doc);
27
- expect(schemes).toHaveLength(2);
28
-
29
- const bearer = schemes.find((s) => s.key === "bearerAuth");
30
- expect(bearer?.kind).toBe("http-bearer");
31
-
32
- const apiKey = schemes.find((s) => s.key === "apiKeyAuth");
33
- expect(apiKey?.kind).toBe("api-key");
34
- expect(apiKey?.in).toBe("header");
35
- expect(apiKey?.name).toBe("X-API-Key");
36
- });
37
-
38
- test("parses oauth2 flows", () => {
39
- const doc = {
40
- openapi: "3.0.3",
41
- components: {
42
- securitySchemes: {
43
- oauth: {
44
- type: "oauth2",
45
- flows: {
46
- clientCredentials: {
47
- tokenUrl: "https://example.com/oauth/token",
48
- scopes: {
49
- "read:ping": "read ping",
50
- },
51
- },
52
- },
53
- },
54
- },
55
- },
56
- } as const;
57
-
58
- const schemes = listAuthSchemes(doc);
59
- const oauth = schemes.find((s) => s.key === "oauth");
60
- expect(oauth?.kind).toBe("oauth2");
61
- expect(oauth?.oauthFlows?.clientCredentials?.tokenUrl).toBe(
62
- "https://example.com/oauth/token",
63
- );
64
- expect(oauth?.oauthFlows?.clientCredentials?.scopes).toEqual(["read:ping"]);
65
- });
66
- });
@@ -1,94 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import type { AuthScheme } from "./auth-schemes.ts";
3
- import { deriveCapabilities } from "./capabilities.ts";
4
- import type { CommandModel } from "./command-model.ts";
5
- import type { ServerInfo } from "./server.ts";
6
- import type { NormalizedOperation, OpenApiDoc } from "./types.ts";
7
-
8
- describe("deriveCapabilities", () => {
9
- test("reports requestBody + server vars", () => {
10
- const doc: OpenApiDoc = {
11
- openapi: "3.0.3",
12
- security: [{ bearerAuth: [] }],
13
- };
14
-
15
- const servers: ServerInfo[] = [
16
- {
17
- url: "https://{region}.api.example.com",
18
- variables: [],
19
- variableNames: ["region"],
20
- },
21
- ];
22
-
23
- const authSchemes: AuthScheme[] = [
24
- { key: "bearerAuth", kind: "http-bearer" },
25
- ];
26
-
27
- const operations: NormalizedOperation[] = [
28
- {
29
- key: "POST /contacts",
30
- method: "POST",
31
- path: "/contacts",
32
- tags: [],
33
- parameters: [],
34
- requestBody: {
35
- required: true,
36
- contentTypes: ["application/json"],
37
- schemasByContentType: { "application/json": { type: "object" } },
38
- },
39
- },
40
- ];
41
-
42
- const commands: CommandModel = {
43
- resources: [
44
- {
45
- resource: "contacts",
46
- actions: [
47
- {
48
- id: "x",
49
- key: "POST /contacts",
50
- action: "create",
51
- pathArgs: [],
52
- method: "POST",
53
- path: "/contacts",
54
- tags: [],
55
- style: "rest",
56
- positionals: [],
57
- flags: [],
58
- params: [],
59
- auth: { alternatives: [] },
60
- requestBody: {
61
- required: true,
62
- content: [
63
- {
64
- contentType: "application/json",
65
- required: true,
66
- schemaType: "object",
67
- },
68
- ],
69
- hasJson: true,
70
- hasFormUrlEncoded: false,
71
- hasMultipart: false,
72
- bodyFlags: ["--data", "--file"],
73
- preferredContentType: "application/json",
74
- },
75
- },
76
- ],
77
- },
78
- ],
79
- };
80
-
81
- const caps = deriveCapabilities({
82
- doc,
83
- servers,
84
- authSchemes,
85
- operations,
86
- commands,
87
- });
88
- expect(caps.servers.hasVariables).toBe(true);
89
- expect(caps.operations.hasRequestBodies).toBe(true);
90
- expect(caps.commands.hasRequestBodies).toBe(true);
91
- expect(caps.auth.hasSecurityRequirements).toBe(true);
92
- expect(caps.auth.kinds).toEqual(["http-bearer"]);
93
- });
94
- });
@@ -1,32 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
-
3
- import { buildCommandId } from "./command-id.ts";
4
-
5
- describe("buildCommandId", () => {
6
- test("includes spec/resource/action/op", () => {
7
- expect(
8
- buildCommandId({
9
- specId: "contacts-api",
10
- resource: "contacts",
11
- action: "get",
12
- operationKey: "GET /contacts/{id}",
13
- }),
14
- ).toBe("contacts-api:contacts:get:get-contacts-id");
15
- });
16
-
17
- test("disambiguates by operationKey", () => {
18
- const a = buildCommandId({
19
- specId: "x",
20
- resource: "contacts",
21
- action: "list",
22
- operationKey: "GET /contacts",
23
- });
24
- const b = buildCommandId({
25
- specId: "x",
26
- resource: "contacts",
27
- action: "list",
28
- operationKey: "GET /contacts/search",
29
- });
30
- expect(a).not.toBe(b);
31
- });
32
- });
@@ -1,44 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
-
3
- import { buildCommandModel } from "./command-model.ts";
4
- import type { PlannedOperation } from "./naming.ts";
5
-
6
- describe("buildCommandModel", () => {
7
- test("groups operations by resource", () => {
8
- const planned: PlannedOperation[] = [
9
- {
10
- key: "GET /contacts",
11
- method: "GET",
12
- path: "/contacts",
13
- tags: ["Contacts"],
14
- parameters: [],
15
- resource: "contacts",
16
- action: "list",
17
- canonicalAction: "list",
18
- pathArgs: [],
19
- style: "rest",
20
- },
21
- {
22
- key: "GET /contacts/{id}",
23
- method: "GET",
24
- path: "/contacts/{id}",
25
- tags: ["Contacts"],
26
- parameters: [],
27
- resource: "contacts",
28
- action: "get",
29
- canonicalAction: "get",
30
- pathArgs: ["id"],
31
- style: "rest",
32
- },
33
- ];
34
-
35
- const model = buildCommandModel(planned, { specId: "contacts-api" });
36
- expect(model.resources).toHaveLength(1);
37
- expect(model.resources[0]?.resource).toBe("contacts");
38
- expect(model.resources[0]?.actions).toHaveLength(2);
39
- expect(model.resources[0]?.actions.map((a) => a.action)).toEqual([
40
- "get",
41
- "list",
42
- ]);
43
- });
44
- });
@@ -1,86 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { planOperation } from "./naming.ts";
3
- import type { NormalizedOperation } from "./types.ts";
4
-
5
- describe("planOperation", () => {
6
- test("REST: GET /contacts -> contacts list", () => {
7
- const op: NormalizedOperation = {
8
- key: "GET /contacts",
9
- method: "GET",
10
- path: "/contacts",
11
- operationId: "Contacts.List",
12
- tags: ["Contacts"],
13
- parameters: [],
14
- };
15
-
16
- const planned = planOperation(op);
17
- expect(planned.style).toBe("rest");
18
- expect(planned.resource).toBe("contacts");
19
- expect(planned.action).toBe("list");
20
- expect(planned.pathArgs).toEqual([]);
21
- });
22
-
23
- test("REST: singleton /ping stays ping and prefers operationId action", () => {
24
- const op: NormalizedOperation = {
25
- key: "GET /ping",
26
- method: "GET",
27
- path: "/ping",
28
- operationId: "Ping.Get",
29
- tags: [],
30
- parameters: [],
31
- };
32
-
33
- const planned = planOperation(op);
34
- expect(planned.style).toBe("rest");
35
- expect(planned.resource).toBe("ping");
36
- expect(planned.action).toBe("get");
37
- });
38
-
39
- test("REST: singular path pluralizes to contacts", () => {
40
- const op: NormalizedOperation = {
41
- key: "GET /contact/{id}",
42
- method: "GET",
43
- path: "/contact/{id}",
44
- tags: [],
45
- parameters: [],
46
- };
47
-
48
- const planned = planOperation(op);
49
- expect(planned.style).toBe("rest");
50
- expect(planned.resource).toBe("contacts");
51
- expect(planned.action).toBe("get");
52
- expect(planned.pathArgs).toEqual(["id"]);
53
- });
54
-
55
- test("RPC: POST /Contacts.List -> contacts list", () => {
56
- const op: NormalizedOperation = {
57
- key: "POST /Contacts.List",
58
- method: "POST",
59
- path: "/Contacts.List",
60
- operationId: "Contacts.List",
61
- tags: [],
62
- parameters: [],
63
- };
64
-
65
- const planned = planOperation(op);
66
- expect(planned.style).toBe("rpc");
67
- expect(planned.resource).toBe("contacts");
68
- expect(planned.action).toBe("list");
69
- });
70
-
71
- test("RPC: Retrieve canonicalizes to get", () => {
72
- const op: NormalizedOperation = {
73
- key: "POST /Contacts.Retrieve",
74
- method: "POST",
75
- path: "/Contacts.Retrieve",
76
- operationId: "Contacts.Retrieve",
77
- tags: [],
78
- parameters: [],
79
- };
80
-
81
- const planned = planOperation(op);
82
- expect(planned.style).toBe("rpc");
83
- expect(planned.resource).toBe("contacts");
84
- expect(planned.action).toBe("get");
85
- });
86
- });
@@ -1,57 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
-
3
- import { indexOperations } from "./operations.ts";
4
- import type { OpenApiDoc } from "./types.ts";
5
-
6
- describe("indexOperations", () => {
7
- test("indexes basic operations", () => {
8
- const doc: OpenApiDoc = {
9
- openapi: "3.0.3",
10
- paths: {
11
- "/contacts": {
12
- get: {
13
- operationId: "Contacts.List",
14
- tags: ["Contacts"],
15
- parameters: [
16
- {
17
- in: "query",
18
- name: "limit",
19
- schema: { type: "integer" },
20
- },
21
- ],
22
- },
23
- },
24
- "/contacts/{id}": {
25
- get: {
26
- operationId: "Contacts.Get",
27
- tags: ["Contacts"],
28
- parameters: [
29
- {
30
- in: "path",
31
- name: "id",
32
- required: true,
33
- schema: { type: "string" },
34
- },
35
- ],
36
- },
37
- },
38
- },
39
- };
40
-
41
- const ops = indexOperations(doc);
42
- expect(ops).toHaveLength(2);
43
-
44
- expect(ops[0]?.key).toBe("GET /contacts");
45
- expect(ops[0]?.path).toBe("/contacts");
46
- expect(ops[0]?.method).toBe("GET");
47
- expect(ops[0]?.parameters).toHaveLength(1);
48
- expect(ops[0]?.parameters[0]?.in).toBe("query");
49
-
50
- expect(ops[1]?.key).toBe("GET /contacts/{id}");
51
- expect(ops[1]?.path).toBe("/contacts/{id}");
52
- expect(ops[1]?.method).toBe("GET");
53
- expect(ops[1]?.parameters).toHaveLength(1);
54
- expect(ops[1]?.parameters[0]?.in).toBe("path");
55
- expect(ops[1]?.parameters[0]?.required).toBe(true);
56
- });
57
- });
@@ -1,70 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
-
3
- import { deriveParamSpecs } from "./params.ts";
4
- import type { NormalizedOperation } from "./types.ts";
5
-
6
- describe("deriveParamSpecs", () => {
7
- test("derives basic types + flags", () => {
8
- const op: NormalizedOperation = {
9
- key: "GET /contacts",
10
- method: "GET",
11
- path: "/contacts",
12
- tags: [],
13
- parameters: [
14
- {
15
- in: "query",
16
- name: "limit",
17
- required: false,
18
- schema: {
19
- type: "integer",
20
- format: "int32",
21
- enum: ["1", "2"],
22
- },
23
- },
24
- {
25
- in: "header",
26
- name: "X-Request-Id",
27
- required: false,
28
- schema: { type: "string" },
29
- },
30
- ],
31
- };
32
-
33
- const specs = deriveParamSpecs(op);
34
- expect(specs).toHaveLength(2);
35
-
36
- const limit = specs.find((p) => p.name === "limit");
37
- expect(limit?.kind).toBe("flag");
38
- expect(limit?.flag).toBe("--limit");
39
- expect(limit?.type).toBe("integer");
40
- expect(limit?.format).toBe("int32");
41
- expect(limit?.enum).toEqual(["1", "2"]);
42
-
43
- const reqId = specs.find((p) => p.name === "X-Request-Id");
44
- expect(reqId?.kind).toBe("flag");
45
- expect(reqId?.flag).toBe("--x-request-id");
46
- expect(reqId?.type).toBe("string");
47
- });
48
-
49
- test("derives array item types", () => {
50
- const op: NormalizedOperation = {
51
- key: "GET /things",
52
- method: "GET",
53
- path: "/things",
54
- tags: [],
55
- parameters: [
56
- {
57
- in: "query",
58
- name: "ids",
59
- required: false,
60
- schema: { type: "array", items: { type: "integer" } },
61
- },
62
- ],
63
- };
64
-
65
- const specs = deriveParamSpecs(op);
66
- expect(specs).toHaveLength(1);
67
- expect(specs[0]?.type).toBe("array");
68
- expect(specs[0]?.itemType).toBe("integer");
69
- });
70
- });
@@ -1,65 +0,0 @@
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
- });
@@ -1,35 +0,0 @@
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
- });