specli 0.0.21 → 0.0.23

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 (34) hide show
  1. package/README.md +58 -0
  2. package/dist/index.d.ts +106 -0
  3. package/dist/index.js +114 -0
  4. package/package.json +22 -16
  5. package/dist/ai/tools.test.d.ts +0 -1
  6. package/dist/ai/tools.test.js +0 -49
  7. package/dist/cli/model/capabilities.test.d.ts +0 -1
  8. package/dist/cli/model/capabilities.test.js +0 -84
  9. package/dist/cli/model/command-id.test.d.ts +0 -1
  10. package/dist/cli/model/command-id.test.js +0 -27
  11. package/dist/cli/model/command-model.test.d.ts +0 -1
  12. package/dist/cli/model/command-model.test.js +0 -40
  13. package/dist/cli/model/naming.test.d.ts +0 -1
  14. package/dist/cli/model/naming.test.js +0 -75
  15. package/dist/cli/parse/auth-requirements.test.d.ts +0 -1
  16. package/dist/cli/parse/auth-requirements.test.js +0 -16
  17. package/dist/cli/parse/auth-schemes.test.d.ts +0 -1
  18. package/dist/cli/parse/auth-schemes.test.js +0 -56
  19. package/dist/cli/parse/operations.test.d.ts +0 -1
  20. package/dist/cli/parse/operations.test.js +0 -51
  21. package/dist/cli/parse/params.test.d.ts +0 -1
  22. package/dist/cli/parse/params.test.js +0 -62
  23. package/dist/cli/parse/positional.test.d.ts +0 -1
  24. package/dist/cli/parse/positional.test.js +0 -60
  25. package/dist/cli/parse/request-body.test.d.ts +0 -1
  26. package/dist/cli/parse/request-body.test.js +0 -31
  27. package/dist/cli/parse/servers.test.d.ts +0 -1
  28. package/dist/cli/parse/servers.test.js +0 -49
  29. package/dist/cli/runtime/body-flags.test.d.ts +0 -1
  30. package/dist/cli/runtime/body-flags.test.js +0 -192
  31. package/dist/cli/runtime/request.test.d.ts +0 -1
  32. package/dist/cli/runtime/request.test.js +0 -332
  33. package/dist/cli/runtime/validate/coerce.test.d.ts +0 -1
  34. package/dist/cli/runtime/validate/coerce.test.js +0 -75
@@ -1,332 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { tmpdir } from "node:os";
3
- import { generateBodyFlags } from "./body-flags.js";
4
- import { buildRequest } from "./request.js";
5
- import { createAjv, formatAjvErrors } from "./validate/index.js";
6
- function makeAction(partial) {
7
- return {
8
- id: "test",
9
- key: "POST /contacts",
10
- action: "create",
11
- pathArgs: [],
12
- method: "POST",
13
- path: "/contacts",
14
- tags: [],
15
- style: "rest",
16
- positionals: [],
17
- flags: [],
18
- params: [],
19
- auth: { alternatives: [] },
20
- requestBody: {
21
- required: true,
22
- content: [
23
- {
24
- contentType: "application/json",
25
- required: true,
26
- schemaType: "object",
27
- },
28
- ],
29
- hasJson: true,
30
- hasFormUrlEncoded: false,
31
- hasMultipart: false,
32
- bodyFlags: ["--data", "--file"],
33
- preferredContentType: "application/json",
34
- preferredSchema: undefined,
35
- },
36
- requestBodySchema: {
37
- type: "object",
38
- properties: {
39
- name: { type: "string" },
40
- },
41
- required: ["name"],
42
- },
43
- ...partial,
44
- };
45
- }
46
- describe("buildRequest (requestBody)", () => {
47
- test("builds body from expanded body flags", async () => {
48
- const prevHome = process.env.HOME;
49
- const home = `${tmpdir()}/specli-test-${crypto.randomUUID()}`;
50
- process.env.HOME = home;
51
- try {
52
- const action = makeAction();
53
- const bodyFlagDefs = generateBodyFlags(action.requestBodySchema, new Set());
54
- const { request, curl } = await buildRequest({
55
- specId: "spec",
56
- action,
57
- positionalValues: [],
58
- flagValues: { name: "A" }, // --name A
59
- globals: {},
60
- servers: [
61
- { url: "https://api.example.com", variables: [], variableNames: [] },
62
- ],
63
- authSchemes: [],
64
- bodyFlagDefs,
65
- });
66
- expect(request.headers.get("Content-Type")).toBe("application/json");
67
- expect(await request.clone().text()).toBe('{"name":"A"}');
68
- expect(curl).toContain("--data");
69
- expect(curl).toContain('{"name":"A"}');
70
- }
71
- finally {
72
- process.env.HOME = prevHome;
73
- }
74
- });
75
- test("throws when requestBody is required but missing", async () => {
76
- const prevHome = process.env.HOME;
77
- const home = `${tmpdir()}/specli-test-${crypto.randomUUID()}`;
78
- process.env.HOME = home;
79
- try {
80
- const action = makeAction();
81
- const bodyFlagDefs = generateBodyFlags(action.requestBodySchema, new Set());
82
- await expect(() => buildRequest({
83
- specId: "spec",
84
- action,
85
- positionalValues: [],
86
- flagValues: {},
87
- globals: {},
88
- servers: [
89
- {
90
- url: "https://api.example.com",
91
- variables: [],
92
- variableNames: [],
93
- },
94
- ],
95
- authSchemes: [],
96
- bodyFlagDefs,
97
- })).toThrow("Required: --name");
98
- }
99
- finally {
100
- process.env.HOME = prevHome;
101
- }
102
- });
103
- test("throws friendly error for missing required expanded field", async () => {
104
- const prevHome = process.env.HOME;
105
- const home = `${tmpdir()}/specli-test-${crypto.randomUUID()}`;
106
- process.env.HOME = home;
107
- try {
108
- // Schema with two fields, one required
109
- const action = makeAction({
110
- requestBodySchema: {
111
- type: "object",
112
- properties: {
113
- name: { type: "string" },
114
- email: { type: "string" },
115
- },
116
- required: ["name"],
117
- },
118
- });
119
- const bodyFlagDefs = generateBodyFlags(action.requestBodySchema, new Set());
120
- // Provide email but not name (the required one)
121
- await expect(() => buildRequest({
122
- specId: "spec",
123
- action,
124
- positionalValues: [],
125
- flagValues: { email: "test@example.com" }, // --email (but missing --name)
126
- globals: {},
127
- servers: [
128
- {
129
- url: "https://api.example.com",
130
- variables: [],
131
- variableNames: [],
132
- },
133
- ],
134
- authSchemes: [],
135
- bodyFlagDefs,
136
- })).toThrow("Missing required fields: --name");
137
- }
138
- finally {
139
- process.env.HOME = prevHome;
140
- }
141
- });
142
- test("builds nested object from dot notation flags", async () => {
143
- const prevHome = process.env.HOME;
144
- const home = `${tmpdir()}/specli-test-${crypto.randomUUID()}`;
145
- process.env.HOME = home;
146
- try {
147
- const action = makeAction({
148
- requestBodySchema: {
149
- type: "object",
150
- properties: {
151
- name: { type: "string" },
152
- address: {
153
- type: "object",
154
- properties: {
155
- street: { type: "string" },
156
- city: { type: "string" },
157
- },
158
- },
159
- },
160
- required: ["name"],
161
- },
162
- });
163
- const bodyFlagDefs = generateBodyFlags(action.requestBodySchema, new Set());
164
- // Dot notation: --address.street and --address.city should create nested object
165
- const { request } = await buildRequest({
166
- specId: "spec",
167
- action,
168
- positionalValues: [],
169
- flagValues: {
170
- name: "Ada",
171
- "address.street": "123 Main St", // Commander keeps dots in keys
172
- "address.city": "NYC",
173
- },
174
- globals: {},
175
- servers: [
176
- { url: "https://api.example.com", variables: [], variableNames: [] },
177
- ],
178
- authSchemes: [],
179
- bodyFlagDefs,
180
- });
181
- const body = JSON.parse(await request.clone().text());
182
- expect(body).toEqual({
183
- name: "Ada",
184
- address: {
185
- street: "123 Main St",
186
- city: "NYC",
187
- },
188
- });
189
- }
190
- finally {
191
- process.env.HOME = prevHome;
192
- }
193
- });
194
- });
195
- describe("buildRequest (query parameters)", () => {
196
- test("builds query string from flag values", async () => {
197
- const prevHome = process.env.HOME;
198
- const home = `${tmpdir()}/specli-test-${crypto.randomUUID()}`;
199
- process.env.HOME = home;
200
- try {
201
- const action = {
202
- id: "test",
203
- key: "GET /contacts",
204
- action: "list",
205
- pathArgs: [],
206
- method: "GET",
207
- path: "/contacts",
208
- tags: [],
209
- style: "rest",
210
- positionals: [],
211
- flags: [
212
- {
213
- flag: "--limit",
214
- name: "limit",
215
- in: "query",
216
- type: "integer",
217
- required: false,
218
- },
219
- {
220
- flag: "--name",
221
- name: "name",
222
- in: "query",
223
- type: "string",
224
- required: false,
225
- },
226
- ],
227
- params: [
228
- {
229
- kind: "flag",
230
- flag: "--limit",
231
- name: "limit",
232
- in: "query",
233
- required: false,
234
- type: "integer",
235
- },
236
- {
237
- kind: "flag",
238
- flag: "--name",
239
- name: "name",
240
- in: "query",
241
- required: false,
242
- type: "string",
243
- },
244
- ],
245
- auth: { alternatives: [] },
246
- };
247
- const { request } = await buildRequest({
248
- specId: "spec",
249
- action,
250
- positionalValues: [],
251
- flagValues: { limit: 10, name: "andrew" },
252
- globals: {},
253
- servers: [
254
- { url: "https://api.example.com", variables: [], variableNames: [] },
255
- ],
256
- authSchemes: [],
257
- });
258
- expect(request.method).toBe("GET");
259
- expect(request.url).toBe("https://api.example.com/contacts?limit=10&name=andrew");
260
- }
261
- finally {
262
- process.env.HOME = prevHome;
263
- }
264
- });
265
- test("handles array query parameters", async () => {
266
- const prevHome = process.env.HOME;
267
- const home = `${tmpdir()}/specli-test-${crypto.randomUUID()}`;
268
- process.env.HOME = home;
269
- try {
270
- const action = {
271
- id: "test",
272
- key: "GET /contacts",
273
- action: "list",
274
- pathArgs: [],
275
- method: "GET",
276
- path: "/contacts",
277
- tags: [],
278
- style: "rest",
279
- positionals: [],
280
- flags: [
281
- {
282
- flag: "--tag",
283
- name: "tag",
284
- in: "query",
285
- type: "array",
286
- itemType: "string",
287
- required: false,
288
- },
289
- ],
290
- params: [
291
- {
292
- kind: "flag",
293
- flag: "--tag",
294
- name: "tag",
295
- in: "query",
296
- required: false,
297
- type: "array",
298
- },
299
- ],
300
- auth: { alternatives: [] },
301
- };
302
- const { request } = await buildRequest({
303
- specId: "spec",
304
- action,
305
- positionalValues: [],
306
- flagValues: { tag: ["vip", "active"] },
307
- globals: {},
308
- servers: [
309
- { url: "https://api.example.com", variables: [], variableNames: [] },
310
- ],
311
- authSchemes: [],
312
- });
313
- expect(request.url).toBe("https://api.example.com/contacts?tag=vip&tag=active");
314
- }
315
- finally {
316
- process.env.HOME = prevHome;
317
- }
318
- });
319
- });
320
- describe("formatAjvErrors", () => {
321
- test("pretty prints required errors", () => {
322
- const ajv = createAjv();
323
- const validate = ajv.compile({
324
- type: "object",
325
- properties: { name: { type: "string" } },
326
- required: ["name"],
327
- });
328
- validate({});
329
- const msg = formatAjvErrors(validate.errors);
330
- expect(msg).toBe("/ missing required property 'name'");
331
- });
332
- });
@@ -1 +0,0 @@
1
- export {};
@@ -1,75 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { coerceArrayInput, coerceValue } from "./coerce.js";
3
- describe("coerceValue", () => {
4
- test("returns string as-is for string type", () => {
5
- expect(coerceValue("hello", "string")).toBe("hello");
6
- });
7
- test("returns string as-is for unknown type", () => {
8
- expect(coerceValue("hello", "unknown")).toBe("hello");
9
- });
10
- test("parses integer type", () => {
11
- expect(coerceValue("42", "integer")).toBe(42);
12
- expect(coerceValue("-10", "integer")).toBe(-10);
13
- expect(coerceValue("0", "integer")).toBe(0);
14
- });
15
- test("throws for invalid integer", () => {
16
- expect(() => coerceValue("abc", "integer")).toThrow("Expected integer");
17
- });
18
- test("truncates decimal for integer type (parseInt behavior)", () => {
19
- // parseInt("12.5", 10) returns 12 - this is expected JS behavior
20
- expect(coerceValue("12.5", "integer")).toBe(12);
21
- });
22
- test("parses number type", () => {
23
- expect(coerceValue("42", "number")).toBe(42);
24
- expect(coerceValue("3.14", "number")).toBe(3.14);
25
- expect(coerceValue("-0.5", "number")).toBe(-0.5);
26
- });
27
- test("throws for invalid number", () => {
28
- expect(() => coerceValue("abc", "number")).toThrow("Expected number");
29
- });
30
- test("parses boolean type", () => {
31
- expect(coerceValue("true", "boolean")).toBe(true);
32
- expect(coerceValue("false", "boolean")).toBe(false);
33
- });
34
- test("throws for invalid boolean", () => {
35
- expect(() => coerceValue("yes", "boolean")).toThrow("Expected boolean");
36
- expect(() => coerceValue("1", "boolean")).toThrow("Expected boolean");
37
- });
38
- test("parses object type as JSON", () => {
39
- expect(coerceValue('{"a":1}', "object")).toEqual({ a: 1 });
40
- });
41
- test("throws for invalid object JSON", () => {
42
- expect(() => coerceValue("not json", "object")).toThrow("Expected JSON object");
43
- });
44
- });
45
- describe("coerceArrayInput", () => {
46
- test("parses comma-separated values", () => {
47
- expect(coerceArrayInput("a,b,c", "string")).toEqual(["a", "b", "c"]);
48
- });
49
- test("trims whitespace in comma-separated values", () => {
50
- expect(coerceArrayInput("a, b, c", "string")).toEqual(["a", "b", "c"]);
51
- });
52
- test("parses JSON array", () => {
53
- expect(coerceArrayInput('["a","b","c"]', "string")).toEqual([
54
- "a",
55
- "b",
56
- "c",
57
- ]);
58
- });
59
- test("coerces array items to specified type", () => {
60
- expect(coerceArrayInput("1,2,3", "integer")).toEqual([1, 2, 3]);
61
- expect(coerceArrayInput('["1","2","3"]', "integer")).toEqual([1, 2, 3]);
62
- });
63
- test("returns empty array for empty string", () => {
64
- expect(coerceArrayInput("", "string")).toEqual([]);
65
- expect(coerceArrayInput(" ", "string")).toEqual([]);
66
- });
67
- test("throws for invalid JSON array", () => {
68
- expect(() => coerceArrayInput("[invalid", "string")).toThrow("Expected JSON array");
69
- });
70
- test("treats non-array JSON as comma-separated string", () => {
71
- // If it doesn't start with '[', it's treated as comma-separated
72
- // '{"a":1}' doesn't start with '[', so it's treated as a single value
73
- expect(coerceArrayInput('{"a":1}', "string")).toEqual(['{"a":1}']);
74
- });
75
- });