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
@@ -1,54 +0,0 @@
1
- {
2
- "openapi": "3.0.3",
3
- "info": {
4
- "title": "OAuth API",
5
- "version": "1.0.0"
6
- },
7
- "servers": [{ "url": "https://api.example.com" }],
8
- "security": [
9
- {
10
- "oauth": ["read:ping"]
11
- },
12
- {}
13
- ],
14
- "components": {
15
- "securitySchemes": {
16
- "oauth": {
17
- "type": "oauth2",
18
- "flows": {
19
- "authorizationCode": {
20
- "authorizationUrl": "https://example.com/oauth/authorize",
21
- "tokenUrl": "https://example.com/oauth/token",
22
- "scopes": {
23
- "read:ping": "read ping"
24
- }
25
- },
26
- "clientCredentials": {
27
- "tokenUrl": "https://example.com/oauth/token",
28
- "scopes": {
29
- "read:ping": "read ping",
30
- "write:ping": "write ping"
31
- }
32
- }
33
- }
34
- }
35
- }
36
- },
37
- "paths": {
38
- "/ping": {
39
- "get": {
40
- "operationId": "Ping.Get",
41
- "security": [
42
- {
43
- "oauth": ["read:ping"]
44
- }
45
- ],
46
- "responses": {
47
- "200": {
48
- "description": "OK"
49
- }
50
- }
51
- }
52
- }
53
- }
54
- }
@@ -1,35 +0,0 @@
1
- {
2
- "openapi": "3.0.3",
3
- "info": {
4
- "title": "Servers API",
5
- "version": "1.0.0"
6
- },
7
- "servers": [
8
- {
9
- "url": "https://{region}.api.example.com/{basePath}",
10
- "description": "Regional server",
11
- "variables": {
12
- "region": {
13
- "default": "us",
14
- "enum": ["us", "eu"],
15
- "description": "Region code"
16
- },
17
- "basePath": {
18
- "default": "v1"
19
- }
20
- }
21
- }
22
- ],
23
- "paths": {
24
- "/ping": {
25
- "get": {
26
- "operationId": "Ping.Get",
27
- "responses": {
28
- "200": {
29
- "description": "OK"
30
- }
31
- }
32
- }
33
- }
34
- }
35
- }
@@ -1,87 +0,0 @@
1
- {
2
- "openapi": "3.0.3",
3
- "info": {
4
- "title": "Contacts API",
5
- "version": "1.0.0"
6
- },
7
- "servers": [
8
- {
9
- "url": "https://api.example.com"
10
- }
11
- ],
12
- "paths": {
13
- "/contacts": {
14
- "get": {
15
- "operationId": "Contacts.List",
16
- "tags": ["Contacts"],
17
- "summary": "List contacts",
18
- "parameters": [
19
- {
20
- "in": "query",
21
- "name": "limit",
22
- "schema": { "type": "integer", "minimum": 1, "maximum": 100 },
23
- "description": "Max items to return"
24
- },
25
- {
26
- "in": "query",
27
- "name": "tag",
28
- "schema": { "type": "array", "items": { "type": "string" } },
29
- "description": "Filter by tags"
30
- }
31
- ],
32
- "responses": {
33
- "200": {
34
- "description": "OK",
35
- "content": {
36
- "application/json": {
37
- "schema": {
38
- "type": "array",
39
- "items": {
40
- "type": "object",
41
- "properties": {
42
- "id": { "type": "string" },
43
- "name": { "type": "string" }
44
- },
45
- "required": ["id", "name"]
46
- }
47
- }
48
- }
49
- }
50
- }
51
- }
52
- }
53
- },
54
- "/contacts/{id}": {
55
- "get": {
56
- "operationId": "Contacts.Get",
57
- "tags": ["Contacts"],
58
- "summary": "Get contact",
59
- "parameters": [
60
- {
61
- "in": "path",
62
- "name": "id",
63
- "required": true,
64
- "schema": { "type": "string" }
65
- }
66
- ],
67
- "responses": {
68
- "200": {
69
- "description": "OK",
70
- "content": {
71
- "application/json": {
72
- "schema": {
73
- "type": "object",
74
- "properties": {
75
- "id": { "type": "string" },
76
- "name": { "type": "string" }
77
- },
78
- "required": ["id", "name"]
79
- }
80
- }
81
- }
82
- }
83
- }
84
- }
85
- }
86
- }
87
- }
@@ -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
- });