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
@@ -2,8 +2,6 @@ import type { AuthScheme } from "../auth-schemes.ts";
2
2
  import type { CommandAction } from "../command-model.ts";
3
3
 
4
4
  import { resolveAuthScheme } from "./auth/resolve.ts";
5
- import { loadBody, parseBodyAsJsonOrYaml } from "./body.ts";
6
- import { parseHeaderInput } from "./headers.ts";
7
5
  import { getToken } from "./profile/secrets.ts";
8
6
  import { getProfile, readProfiles } from "./profile/store.ts";
9
7
  import { resolveServerUrl } from "./server-url.ts";
@@ -19,30 +17,7 @@ export type RuntimeGlobals = {
19
17
  server?: string;
20
18
  serverVar?: string[];
21
19
 
22
- // Common runtime flags (may conflict with operation flags).
23
- header?: string[];
24
- accept?: string;
25
- contentType?: string;
26
- data?: string;
27
- file?: string;
28
- timeout?: string;
29
- dryRun?: boolean;
30
20
  curl?: boolean;
31
- status?: boolean;
32
- headers?: boolean;
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
21
  json?: boolean;
47
22
 
48
23
  auth?: string;
@@ -159,6 +134,12 @@ function applyAuth(
159
134
  return { headers, url };
160
135
  }
161
136
 
137
+ export type EmbeddedDefaults = {
138
+ server?: string;
139
+ serverVars?: string[];
140
+ auth?: string;
141
+ };
142
+
162
143
  export type BuildRequestInput = {
163
144
  specId: string;
164
145
  action: CommandAction;
@@ -167,6 +148,8 @@ export type BuildRequestInput = {
167
148
  globals: RuntimeGlobals;
168
149
  servers: import("../server.ts").ServerInfo[];
169
150
  authSchemes: AuthScheme[];
151
+ embeddedDefaults?: EmbeddedDefaults;
152
+ bodyFlagDefs?: import("./body-flags.ts").BodyFlagDef[];
170
153
  };
171
154
 
172
155
  export async function buildRequest(
@@ -174,10 +157,16 @@ export async function buildRequest(
174
157
  ): Promise<{ request: Request; curl: string }> {
175
158
  const profilesFile = await readProfiles();
176
159
  const profile = getProfile(profilesFile, input.globals.profile);
160
+ const embedded = input.embeddedDefaults;
161
+
162
+ // Merge server vars: CLI flags override embedded defaults
163
+ const embeddedServerVars = parseKeyValuePairs(embedded?.serverVars);
164
+ const cliServerVars = parseKeyValuePairs(input.globals.serverVar);
165
+ const serverVars = { ...embeddedServerVars, ...cliServerVars };
177
166
 
178
- const serverVars = parseKeyValuePairs(input.globals.serverVar);
167
+ // Priority: CLI flag > profile > embedded default
179
168
  const serverUrl = resolveServerUrl({
180
- serverOverride: input.globals.server ?? profile?.server,
169
+ serverOverride: input.globals.server ?? profile?.server ?? embedded?.server,
181
170
  servers: input.servers,
182
171
  serverVars,
183
172
  });
@@ -205,8 +194,6 @@ export async function buildRequest(
205
194
  );
206
195
 
207
196
  const headers = new Headers();
208
- const accept = input.globals.accept ?? input.globals.ocAccept;
209
- if (accept) headers.set("Accept", accept);
210
197
 
211
198
  // Collect declared params for validation.
212
199
  const queryValues: Record<string, unknown> = {};
@@ -271,102 +258,45 @@ export async function buildRequest(
271
258
  headers.set("Cookie", existing ? `${existing}; ${part}` : part);
272
259
  }
273
260
 
274
- const extraHeaders = [
275
- ...(input.globals.header ?? []),
276
- ...(input.globals.ocHeader ?? []),
277
- ].map(parseHeaderInput);
278
- for (const { name, value } of extraHeaders) {
279
- headers.set(name, value);
280
- }
281
-
282
261
  let body: string | undefined;
283
262
  if (input.action.requestBody) {
284
- const data = input.globals.data ?? input.globals.ocData;
285
- const file = input.globals.file ?? input.globals.ocFile;
286
-
287
- const hasData = typeof data === "string";
288
- const hasFile = typeof file === "string";
289
-
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;
302
-
303
- if (hasData && hasFile) throw new Error("Use only one of --data or --file");
304
- if (hasExpandedBody && (hasData || hasFile)) {
305
- throw new Error("Use either --data/--file or --body-* flags (not both)");
306
- }
307
-
308
- const contentType =
309
- input.globals.contentType ??
310
- input.globals.ocContentType ??
311
- input.action.requestBody.preferredContentType;
263
+ // Check if any body flags were provided using the flag definitions
264
+ const bodyFlagDefs = input.bodyFlagDefs ?? [];
265
+ const hasBodyFlags = bodyFlagDefs.some((def) => {
266
+ // Commander keeps dots in option names: --address.street -> "address.street"
267
+ const dotKey = def.path.join(".");
268
+ return input.flagValues[dotKey] !== undefined;
269
+ });
270
+
271
+ const contentType = input.action.requestBody.preferredContentType;
312
272
  if (contentType) headers.set("Content-Type", contentType);
313
273
 
314
274
  const schema = input.action.requestBodySchema;
315
275
 
316
- if (!hasExpandedBody && !hasData && !hasFile) {
276
+ if (!hasBodyFlags) {
317
277
  if (input.action.requestBody.required) {
318
- throw new Error(
319
- "Missing request body. Provide --data, --file, or --body-* flags.",
320
- );
278
+ throw new Error("Missing required request body fields.");
321
279
  }
322
- } else if (hasExpandedBody) {
280
+ } else {
323
281
  if (!contentType?.includes("json")) {
324
282
  throw new Error(
325
- "Expanded body flags are only supported for JSON request bodies. Use --content-type application/json or --data/--file.",
283
+ "Body field flags are only supported for JSON request bodies.",
326
284
  );
327
285
  }
328
286
 
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
- }
287
+ // Check for missing required fields
288
+ const { findMissingRequired, parseDotNotationFlags } = await import(
289
+ "./body-flags.ts"
290
+ );
291
+ const missing = findMissingRequired(input.flagValues, bodyFlagDefs);
292
+ if (missing.length > 0) {
293
+ throw new Error(
294
+ `Missing required body field '${missing[0]}'. Provide --${missing[0]}.`,
295
+ );
340
296
  }
341
297
 
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
- }
298
+ // Build nested object from dot-notation flags
299
+ const built = parseDotNotationFlags(input.flagValues, bodyFlagDefs);
370
300
 
371
301
  if (schema) {
372
302
  const validate = ajv.compile(schema);
@@ -376,57 +306,17 @@ export async function buildRequest(
376
306
  }
377
307
 
378
308
  body = JSON.stringify(built);
379
- } else if (hasData) {
380
- if (contentType?.includes("json")) {
381
- const parsed = parseBodyAsJsonOrYaml(data as string);
382
-
383
- if (schema) {
384
- const validate = ajv.compile(schema);
385
- if (!validate(parsed)) {
386
- throw new Error(formatAjvErrors(validate.errors));
387
- }
388
- }
389
-
390
- body = JSON.stringify(parsed);
391
- } else {
392
- body = data as string;
393
- }
394
- } else if (hasFile) {
395
- const loaded = await loadBody({
396
- kind: "file",
397
- path: file as string,
398
- });
399
- if (contentType?.includes("json")) {
400
- const parsed = parseBodyAsJsonOrYaml(loaded?.raw ?? "");
401
-
402
- if (schema) {
403
- const validate = ajv.compile(schema);
404
- if (!validate(parsed)) {
405
- throw new Error(formatAjvErrors(validate.errors));
406
- }
407
- }
408
-
409
- body = JSON.stringify(parsed);
410
- } else {
411
- body = loaded?.raw;
412
- }
413
- }
414
- } else {
415
- if (
416
- typeof (input.globals.data ?? input.globals.ocData) === "string" ||
417
- typeof (input.globals.file ?? input.globals.ocFile) === "string"
418
- ) {
419
- throw new Error("This operation does not accept a request body");
420
309
  }
421
310
  }
422
311
 
423
- // Profile-aware auth resolution (flags override profile).
312
+ // Auth resolution priority: CLI flag > profile > embedded default
424
313
  const resolvedAuthScheme = resolveAuthScheme(
425
314
  input.authSchemes,
426
315
  input.action.auth,
427
316
  {
428
317
  flagAuthScheme: input.globals.auth,
429
318
  profileAuthScheme: profile?.authScheme,
319
+ embeddedAuthScheme: embedded?.auth,
430
320
  },
431
321
  );
432
322
 
@@ -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>;
package/src/compiled.ts CHANGED
@@ -5,19 +5,17 @@ import { env, envRequired } from "./macros/env.ts" with { type: "macro" };
5
5
  import { loadSpec } from "./macros/spec.ts" with { type: "macro" };
6
6
 
7
7
  // This entrypoint is intended to be compiled.
8
- // The spec is embedded via Bun macro at bundle-time.
9
- const embeddedSpecText = await loadSpec(envRequired("OPENCLI_EMBED_SPEC"));
8
+ // All values are embedded via Bun macros at bundle-time.
9
+ const embeddedSpecText = await loadSpec(envRequired("SPECLI_SPEC"));
10
+ const cliName = env("SPECLI_NAME");
11
+ const server = env("SPECLI_SERVER");
12
+ const serverVars = env("SPECLI_SERVER_VARS");
13
+ const auth = env("SPECLI_AUTH");
10
14
 
11
- // CLI name is also embedded at bundle-time.
12
- const cliName = env("OPENCLI_CLI_NAME");
13
-
14
- // Use embedded `execArgv` as default CLI args.
15
- // We insert them before user-provided args so user flags win.
16
- const argv = [
17
- process.argv[0] ?? cliName ?? "opencli",
18
- process.argv[1] ?? cliName ?? "opencli",
19
- ...(process.execArgv ?? []),
20
- ...process.argv.slice(2),
21
- ];
22
-
23
- await main(argv, { embeddedSpecText, cliName });
15
+ await main(process.argv, {
16
+ embeddedSpecText,
17
+ cliName,
18
+ server,
19
+ serverVars: serverVars ? serverVars.split(",") : undefined,
20
+ auth,
21
+ });
package/src/macros/env.ts CHANGED
@@ -1,10 +1,6 @@
1
1
  /**
2
2
  * Bun macro: reads an environment variable at bundle-time.
3
3
  * Returns undefined if the env var is not set.
4
- *
5
- * Usage:
6
- * import { env } from "./macros/env.ts" with { type: "macro" };
7
- * const value = env("MY_VAR");
8
4
  */
9
5
  export function env(name: string): string | undefined {
10
6
  if (!name) throw new Error("env macro: missing variable name");
package/CLAUDE.md DELETED
@@ -1,111 +0,0 @@
1
- ---
2
- description: Use Bun instead of Node.js, npm, pnpm, or vite.
3
- globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
4
- alwaysApply: false
5
- ---
6
-
7
- Default to using Bun instead of Node.js.
8
-
9
- - Use `bun <file>` instead of `node <file>` or `ts-node <file>`
10
- - Use `bun test` instead of `jest` or `vitest`
11
- - Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
12
- - Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
13
- - Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
14
- - Use `bunx <package> <command>` instead of `npx <package> <command>`
15
- - Bun automatically loads .env, so don't use dotenv.
16
-
17
- ## APIs
18
-
19
- - `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
20
- - `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
21
- - `Bun.redis` for Redis. Don't use `ioredis`.
22
- - `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
23
- - `WebSocket` is built-in. Don't use `ws`.
24
- - Prefer `Bun.file` over `node:fs`'s readFile/writeFile
25
- - Bun.$`ls` instead of execa.
26
-
27
- ## Testing
28
-
29
- Use `bun test` to run tests.
30
-
31
- ```ts#index.test.ts
32
- import { test, expect } from "bun:test";
33
-
34
- test("hello world", () => {
35
- expect(1).toBe(1);
36
- });
37
- ```
38
-
39
- ## Frontend
40
-
41
- Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
42
-
43
- Server:
44
-
45
- ```ts#index.ts
46
- import index from "./index.html"
47
-
48
- Bun.serve({
49
- routes: {
50
- "/": index,
51
- "/api/users/:id": {
52
- GET: (req) => {
53
- return new Response(JSON.stringify({ id: req.params.id }));
54
- },
55
- },
56
- },
57
- // optional websocket support
58
- websocket: {
59
- open: (ws) => {
60
- ws.send("Hello, world!");
61
- },
62
- message: (ws, message) => {
63
- ws.send(message);
64
- },
65
- close: (ws) => {
66
- // handle close
67
- }
68
- },
69
- development: {
70
- hmr: true,
71
- console: true,
72
- }
73
- })
74
- ```
75
-
76
- HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
77
-
78
- ```html#index.html
79
- <html>
80
- <body>
81
- <h1>Hello, world!</h1>
82
- <script type="module" src="./frontend.tsx"></script>
83
- </body>
84
- </html>
85
- ```
86
-
87
- With the following `frontend.tsx`:
88
-
89
- ```tsx#frontend.tsx
90
- import React from "react";
91
- import { createRoot } from "react-dom/client";
92
-
93
- // import .css files directly and it works
94
- import './index.css';
95
-
96
- const root = createRoot(document.body);
97
-
98
- export default function Frontend() {
99
- return <h1>Hello, world!</h1>;
100
- }
101
-
102
- root.render(<Frontend />);
103
- ```
104
-
105
- Then, run index.ts
106
-
107
- ```sh
108
- bun --hot ./index.ts
109
- ```
110
-
111
- For more information, read the Bun API docs in `node_modules/bun-types/docs/**.mdx`.