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.
- package/CLAUDE.md +111 -0
- package/PLAN.md +274 -0
- package/README.md +474 -0
- package/biome.jsonc +1 -0
- package/bun.lock +98 -0
- package/cli.ts +74 -0
- package/fixtures/openapi-array-items.json +22 -0
- package/fixtures/openapi-auth.json +34 -0
- package/fixtures/openapi-body.json +41 -0
- package/fixtures/openapi-collision.json +21 -0
- package/fixtures/openapi-oauth.json +54 -0
- package/fixtures/openapi-servers.json +35 -0
- package/fixtures/openapi.json +87 -0
- package/index.ts +1 -0
- package/package.json +27 -0
- package/scripts/smoke-specs.ts +64 -0
- package/src/cli/auth-requirements.test.ts +27 -0
- package/src/cli/auth-requirements.ts +91 -0
- package/src/cli/auth-schemes.test.ts +66 -0
- package/src/cli/auth-schemes.ts +187 -0
- package/src/cli/capabilities.test.ts +94 -0
- package/src/cli/capabilities.ts +88 -0
- package/src/cli/command-id.test.ts +32 -0
- package/src/cli/command-id.ts +16 -0
- package/src/cli/command-index.ts +19 -0
- package/src/cli/command-model.test.ts +44 -0
- package/src/cli/command-model.ts +128 -0
- package/src/cli/compile.ts +119 -0
- package/src/cli/crypto.ts +9 -0
- package/src/cli/derive-name.ts +101 -0
- package/src/cli/exec.ts +72 -0
- package/src/cli/main.ts +336 -0
- package/src/cli/naming.test.ts +86 -0
- package/src/cli/naming.ts +224 -0
- package/src/cli/operations.test.ts +57 -0
- package/src/cli/operations.ts +152 -0
- package/src/cli/params.test.ts +70 -0
- package/src/cli/params.ts +71 -0
- package/src/cli/pluralize.ts +41 -0
- package/src/cli/positional.test.ts +65 -0
- package/src/cli/positional.ts +75 -0
- package/src/cli/request-body.test.ts +35 -0
- package/src/cli/request-body.ts +94 -0
- package/src/cli/runtime/argv.ts +14 -0
- package/src/cli/runtime/auth/resolve.ts +31 -0
- package/src/cli/runtime/body.ts +24 -0
- package/src/cli/runtime/collect.ts +6 -0
- package/src/cli/runtime/context.ts +62 -0
- package/src/cli/runtime/execute.ts +138 -0
- package/src/cli/runtime/generated.ts +200 -0
- package/src/cli/runtime/headers.ts +37 -0
- package/src/cli/runtime/index.ts +3 -0
- package/src/cli/runtime/profile/secrets.ts +42 -0
- package/src/cli/runtime/profile/store.ts +98 -0
- package/src/cli/runtime/request.test.ts +153 -0
- package/src/cli/runtime/request.ts +487 -0
- package/src/cli/runtime/server-url.ts +44 -0
- package/src/cli/runtime/template.ts +26 -0
- package/src/cli/runtime/validate/ajv.ts +13 -0
- package/src/cli/runtime/validate/coerce.ts +71 -0
- package/src/cli/runtime/validate/error.ts +29 -0
- package/src/cli/runtime/validate/index.ts +4 -0
- package/src/cli/runtime/validate/schema.ts +54 -0
- package/src/cli/schema-shape.ts +36 -0
- package/src/cli/schema.ts +76 -0
- package/src/cli/server.test.ts +35 -0
- package/src/cli/server.ts +88 -0
- package/src/cli/spec-id.ts +12 -0
- package/src/cli/spec-loader.ts +58 -0
- package/src/cli/stable-json.ts +35 -0
- package/src/cli/strings.ts +21 -0
- package/src/cli/types.ts +59 -0
- package/src/compiled.ts +23 -0
- package/src/macros/env.ts +25 -0
- package/src/macros/spec.ts +17 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { CommandAction } from "../../command-model.ts";
|
|
2
|
+
import type { JsonSchema } from "../../types.ts";
|
|
3
|
+
|
|
4
|
+
export type ValidationSchemas = {
|
|
5
|
+
querySchema?: JsonSchema;
|
|
6
|
+
headerSchema?: JsonSchema;
|
|
7
|
+
cookieSchema?: JsonSchema;
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
type ObjectSchema = {
|
|
11
|
+
type: "object";
|
|
12
|
+
properties: Record<string, JsonSchema>;
|
|
13
|
+
required?: string[];
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export function deriveValidationSchemas(
|
|
17
|
+
action: CommandAction,
|
|
18
|
+
): ValidationSchemas {
|
|
19
|
+
// We validate only simple containers for now.
|
|
20
|
+
// Deep style/encoding differences for OpenAPI params are out of scope for v1.
|
|
21
|
+
const query: ObjectSchema = { type: "object", properties: {}, required: [] };
|
|
22
|
+
const header: ObjectSchema = { type: "object", properties: {}, required: [] };
|
|
23
|
+
const cookie: ObjectSchema = { type: "object", properties: {}, required: [] };
|
|
24
|
+
|
|
25
|
+
for (const p of action.params) {
|
|
26
|
+
if (p.kind !== "flag") continue;
|
|
27
|
+
const target =
|
|
28
|
+
p.in === "query"
|
|
29
|
+
? query
|
|
30
|
+
: p.in === "header"
|
|
31
|
+
? header
|
|
32
|
+
: p.in === "cookie"
|
|
33
|
+
? cookie
|
|
34
|
+
: undefined;
|
|
35
|
+
if (!target) continue;
|
|
36
|
+
|
|
37
|
+
const schema = p.schema ?? (p.type === "unknown" ? {} : { type: p.type });
|
|
38
|
+
target.properties[p.name] = schema;
|
|
39
|
+
if (p.required) {
|
|
40
|
+
if (!target.required) target.required = [];
|
|
41
|
+
target.required.push(p.name);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
if (!query.required?.length) delete query.required;
|
|
46
|
+
if (!header.required?.length) delete header.required;
|
|
47
|
+
if (!cookie.required?.length) delete cookie.required;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
querySchema: Object.keys(query.properties).length ? query : undefined,
|
|
51
|
+
headerSchema: Object.keys(header.properties).length ? header : undefined,
|
|
52
|
+
cookieSchema: Object.keys(cookie.properties).length ? cookie : undefined,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export type ParamType =
|
|
2
|
+
| "string"
|
|
3
|
+
| "number"
|
|
4
|
+
| "integer"
|
|
5
|
+
| "boolean"
|
|
6
|
+
| "array"
|
|
7
|
+
| "object"
|
|
8
|
+
| "unknown";
|
|
9
|
+
|
|
10
|
+
export function getSchemaType(schema: unknown): ParamType {
|
|
11
|
+
if (!schema || typeof schema !== "object") return "unknown";
|
|
12
|
+
const t = (schema as { type?: unknown }).type;
|
|
13
|
+
if (t === "string") return "string";
|
|
14
|
+
if (t === "number") return "number";
|
|
15
|
+
if (t === "integer") return "integer";
|
|
16
|
+
if (t === "boolean") return "boolean";
|
|
17
|
+
if (t === "array") return "array";
|
|
18
|
+
if (t === "object") return "object";
|
|
19
|
+
return "unknown";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getSchemaFormat(schema: unknown): string | undefined {
|
|
23
|
+
if (!schema || typeof schema !== "object") return undefined;
|
|
24
|
+
const f = (schema as { format?: unknown }).format;
|
|
25
|
+
return typeof f === "string" ? f : undefined;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function getSchemaEnumStrings(schema: unknown): string[] | undefined {
|
|
29
|
+
if (!schema || typeof schema !== "object") return undefined;
|
|
30
|
+
const e = (schema as { enum?: unknown }).enum;
|
|
31
|
+
if (!Array.isArray(e)) return undefined;
|
|
32
|
+
|
|
33
|
+
// We only surface string enums for now (enough for flag docs + completion).
|
|
34
|
+
const values = e.filter((v) => typeof v === "string") as string[];
|
|
35
|
+
return values.length ? values : undefined;
|
|
36
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import type { AuthScheme } from "./auth-schemes.ts";
|
|
2
|
+
import type { Capabilities } from "./capabilities.ts";
|
|
3
|
+
import type { CommandModel } from "./command-model.ts";
|
|
4
|
+
import type { PlannedOperation } from "./naming.ts";
|
|
5
|
+
import type { ServerInfo } from "./server.ts";
|
|
6
|
+
import type { LoadedSpec, NormalizedOperation } from "./types.ts";
|
|
7
|
+
|
|
8
|
+
export type SchemaOutput = {
|
|
9
|
+
schemaVersion: 1;
|
|
10
|
+
openapi: {
|
|
11
|
+
version: string;
|
|
12
|
+
title?: string;
|
|
13
|
+
infoVersion?: string;
|
|
14
|
+
};
|
|
15
|
+
spec: {
|
|
16
|
+
id: string;
|
|
17
|
+
fingerprint: string;
|
|
18
|
+
source: LoadedSpec["source"];
|
|
19
|
+
};
|
|
20
|
+
capabilities: Capabilities;
|
|
21
|
+
servers: ServerInfo[];
|
|
22
|
+
authSchemes: AuthScheme[];
|
|
23
|
+
operations: NormalizedOperation[];
|
|
24
|
+
planned?: PlannedOperation[];
|
|
25
|
+
commands?: CommandModel;
|
|
26
|
+
commandsIndex?: import("./command-index.ts").CommandsIndex;
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export type MinimalSchemaOutput = Pick<
|
|
30
|
+
SchemaOutput,
|
|
31
|
+
"schemaVersion" | "openapi" | "spec" | "capabilities" | "commands"
|
|
32
|
+
>;
|
|
33
|
+
|
|
34
|
+
export function buildSchemaOutput(
|
|
35
|
+
loaded: LoadedSpec,
|
|
36
|
+
operations: NormalizedOperation[],
|
|
37
|
+
planned: PlannedOperation[] | undefined,
|
|
38
|
+
servers: ServerInfo[],
|
|
39
|
+
authSchemes: AuthScheme[],
|
|
40
|
+
commands: CommandModel | undefined,
|
|
41
|
+
commandsIndex: import("./command-index.ts").CommandsIndex | undefined,
|
|
42
|
+
capabilities: Capabilities,
|
|
43
|
+
): SchemaOutput {
|
|
44
|
+
return {
|
|
45
|
+
schemaVersion: 1,
|
|
46
|
+
openapi: {
|
|
47
|
+
version: loaded.doc.openapi,
|
|
48
|
+
title: loaded.doc.info?.title,
|
|
49
|
+
infoVersion: loaded.doc.info?.version,
|
|
50
|
+
},
|
|
51
|
+
spec: {
|
|
52
|
+
id: loaded.id,
|
|
53
|
+
fingerprint: loaded.fingerprint,
|
|
54
|
+
source: loaded.source,
|
|
55
|
+
},
|
|
56
|
+
capabilities,
|
|
57
|
+
servers,
|
|
58
|
+
authSchemes,
|
|
59
|
+
operations,
|
|
60
|
+
planned,
|
|
61
|
+
commands,
|
|
62
|
+
commandsIndex,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function toMinimalSchemaOutput(
|
|
67
|
+
output: SchemaOutput,
|
|
68
|
+
): MinimalSchemaOutput {
|
|
69
|
+
return {
|
|
70
|
+
schemaVersion: output.schemaVersion,
|
|
71
|
+
openapi: output.openapi,
|
|
72
|
+
spec: output.spec,
|
|
73
|
+
capabilities: output.capabilities,
|
|
74
|
+
commands: output.commands,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, expect, test } from "bun:test";
|
|
2
|
+
|
|
3
|
+
import { listServers } from "./server.ts";
|
|
4
|
+
import type { OpenApiDoc } from "./types.ts";
|
|
5
|
+
|
|
6
|
+
describe("listServers", () => {
|
|
7
|
+
test("extracts server variables from template", () => {
|
|
8
|
+
const doc: OpenApiDoc = {
|
|
9
|
+
openapi: "3.0.3",
|
|
10
|
+
servers: [
|
|
11
|
+
{
|
|
12
|
+
url: "https://{region}.api.example.com/{basePath}",
|
|
13
|
+
variables: {
|
|
14
|
+
region: {
|
|
15
|
+
default: "us",
|
|
16
|
+
enum: ["us", "eu"],
|
|
17
|
+
},
|
|
18
|
+
basePath: {
|
|
19
|
+
default: "v1",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
} as const,
|
|
23
|
+
],
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const servers = listServers(doc);
|
|
27
|
+
expect(servers).toHaveLength(1);
|
|
28
|
+
expect(servers[0]?.variableNames).toEqual(["region", "basePath"]);
|
|
29
|
+
expect(servers[0]?.variables.map((v) => v.name)).toEqual([
|
|
30
|
+
"region",
|
|
31
|
+
"basePath",
|
|
32
|
+
]);
|
|
33
|
+
expect(servers[0]?.variables[0]?.enum).toEqual(["us", "eu"]);
|
|
34
|
+
});
|
|
35
|
+
});
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { getSchemaEnumStrings } from "./schema-shape.ts";
|
|
2
|
+
import type { OpenApiDoc } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export type ServerVariable = {
|
|
5
|
+
name: string;
|
|
6
|
+
default?: string;
|
|
7
|
+
enum?: string[];
|
|
8
|
+
description?: string;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type ServerInfo = {
|
|
12
|
+
url: string;
|
|
13
|
+
description?: string;
|
|
14
|
+
variables: ServerVariable[];
|
|
15
|
+
variableNames: string[];
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
type RawServerVariable = {
|
|
19
|
+
default?: unknown;
|
|
20
|
+
enum?: unknown;
|
|
21
|
+
description?: unknown;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
type RawServer = {
|
|
25
|
+
url?: unknown;
|
|
26
|
+
description?: unknown;
|
|
27
|
+
variables?: unknown;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
function extractVariableNames(url: string): string[] {
|
|
31
|
+
const names: string[] = [];
|
|
32
|
+
const re = /\{([^}]+)\}/g;
|
|
33
|
+
|
|
34
|
+
while (true) {
|
|
35
|
+
const match = re.exec(url);
|
|
36
|
+
if (!match) break;
|
|
37
|
+
names.push(match[1] ?? "");
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return names.map((n) => n.trim()).filter(Boolean);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export function listServers(doc: OpenApiDoc): ServerInfo[] {
|
|
44
|
+
const servers = doc.servers ?? [];
|
|
45
|
+
const out: ServerInfo[] = [];
|
|
46
|
+
|
|
47
|
+
for (const raw of servers) {
|
|
48
|
+
const s = raw as RawServer;
|
|
49
|
+
if (!s || typeof s !== "object") continue;
|
|
50
|
+
if (typeof s.url !== "string") continue;
|
|
51
|
+
|
|
52
|
+
const variableNames = extractVariableNames(s.url);
|
|
53
|
+
const variables: ServerVariable[] = [];
|
|
54
|
+
|
|
55
|
+
const rawVars =
|
|
56
|
+
s.variables &&
|
|
57
|
+
typeof s.variables === "object" &&
|
|
58
|
+
!Array.isArray(s.variables)
|
|
59
|
+
? (s.variables as Record<string, RawServerVariable>)
|
|
60
|
+
: {};
|
|
61
|
+
|
|
62
|
+
for (const name of variableNames) {
|
|
63
|
+
const v = rawVars[name];
|
|
64
|
+
const def = v?.default;
|
|
65
|
+
const desc = v?.description;
|
|
66
|
+
variables.push({
|
|
67
|
+
name,
|
|
68
|
+
default: typeof def === "string" ? def : undefined,
|
|
69
|
+
enum: getSchemaEnumStrings(v),
|
|
70
|
+
description: typeof desc === "string" ? desc : undefined,
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
out.push({
|
|
75
|
+
url: s.url,
|
|
76
|
+
description:
|
|
77
|
+
typeof s.description === "string" ? s.description : undefined,
|
|
78
|
+
variables,
|
|
79
|
+
variableNames,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return out;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function getDefaultServerUrl(doc: OpenApiDoc): string | undefined {
|
|
87
|
+
return listServers(doc)[0]?.url;
|
|
88
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { kebabCase } from "./strings.ts";
|
|
2
|
+
import type { LoadedSpec } from "./types.ts";
|
|
3
|
+
|
|
4
|
+
export function getSpecId(
|
|
5
|
+
loaded: Pick<LoadedSpec, "doc" | "fingerprint">,
|
|
6
|
+
): string {
|
|
7
|
+
const title = loaded.doc.info?.title;
|
|
8
|
+
const fromTitle = title ? kebabCase(title) : "";
|
|
9
|
+
if (fromTitle) return fromTitle;
|
|
10
|
+
|
|
11
|
+
return loaded.fingerprint.slice(0, 12);
|
|
12
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import SwaggerParser from "@apidevtools/swagger-parser";
|
|
2
|
+
import { YAML } from "bun";
|
|
3
|
+
|
|
4
|
+
import { sha256Hex } from "./crypto.ts";
|
|
5
|
+
import { getSpecId } from "./spec-id.ts";
|
|
6
|
+
import { stableStringify } from "./stable-json.ts";
|
|
7
|
+
import type { LoadedSpec, OpenApiDoc, SpecSource } from "./types.ts";
|
|
8
|
+
|
|
9
|
+
export type LoadSpecOptions = {
|
|
10
|
+
spec?: string;
|
|
11
|
+
embeddedSpecText?: string;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
function isProbablyUrl(input: string): boolean {
|
|
15
|
+
return /^https?:\/\//i.test(input);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function parseSpecText(text: string): unknown {
|
|
19
|
+
const trimmed = text.trimStart();
|
|
20
|
+
if (trimmed.startsWith("{") || trimmed.startsWith("[")) {
|
|
21
|
+
return JSON.parse(text);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
return YAML.parse(text);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function loadSpec(options: LoadSpecOptions): Promise<LoadedSpec> {
|
|
28
|
+
const { spec, embeddedSpecText } = options;
|
|
29
|
+
|
|
30
|
+
let source: SpecSource;
|
|
31
|
+
let inputForParser: unknown;
|
|
32
|
+
|
|
33
|
+
if (typeof embeddedSpecText === "string") {
|
|
34
|
+
source = "embedded";
|
|
35
|
+
inputForParser = parseSpecText(embeddedSpecText);
|
|
36
|
+
} else if (spec) {
|
|
37
|
+
source = isProbablyUrl(spec) ? "url" : "file";
|
|
38
|
+
inputForParser = spec;
|
|
39
|
+
} else {
|
|
40
|
+
throw new Error(
|
|
41
|
+
"Missing spec. Provide --spec <url|path> or build with an embedded spec.",
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const doc = (await SwaggerParser.dereference(
|
|
46
|
+
// biome-ignore lint/suspicious/noExplicitAny: unknown
|
|
47
|
+
inputForParser as any,
|
|
48
|
+
)) as OpenApiDoc;
|
|
49
|
+
|
|
50
|
+
if (!doc || typeof doc !== "object" || typeof doc.openapi !== "string") {
|
|
51
|
+
throw new Error("Loaded spec is not a valid OpenAPI document");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const fingerprint = await sha256Hex(stableStringify(doc));
|
|
55
|
+
const id = getSpecId({ doc, fingerprint });
|
|
56
|
+
|
|
57
|
+
return { source, id, fingerprint, doc };
|
|
58
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export function stableStringify(
|
|
2
|
+
value: unknown,
|
|
3
|
+
options?: { space?: number },
|
|
4
|
+
): string {
|
|
5
|
+
const visiting = new WeakSet<object>();
|
|
6
|
+
return JSON.stringify(sort(value, visiting), null, options?.space);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function sort(value: unknown, visiting: WeakSet<object>): unknown {
|
|
10
|
+
if (value === null) return null;
|
|
11
|
+
|
|
12
|
+
if (Array.isArray(value)) {
|
|
13
|
+
if (visiting.has(value)) return { __opencli_circular: true };
|
|
14
|
+
visiting.add(value);
|
|
15
|
+
const out = value.map((v) => sort(v, visiting));
|
|
16
|
+
visiting.delete(value);
|
|
17
|
+
return out;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (typeof value === "object") {
|
|
21
|
+
if (visiting.has(value)) return { __opencli_circular: true };
|
|
22
|
+
visiting.add(value);
|
|
23
|
+
|
|
24
|
+
const obj = value as Record<string, unknown>;
|
|
25
|
+
const out: Record<string, unknown> = {};
|
|
26
|
+
for (const key of Object.keys(obj).sort()) {
|
|
27
|
+
out[key] = sort(obj[key], visiting);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
visiting.delete(value);
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export function kebabCase(input: string): string {
|
|
2
|
+
const trimmed = input.trim();
|
|
3
|
+
if (!trimmed) return "";
|
|
4
|
+
|
|
5
|
+
// Convert spaces/underscores/dots to dashes, split camelCase.
|
|
6
|
+
return trimmed
|
|
7
|
+
.replace(/([a-z0-9])([A-Z])/g, "$1-$2")
|
|
8
|
+
.replace(/[\s_.:/]+/g, "-")
|
|
9
|
+
.replace(/[^a-zA-Z0-9-]/g, "-")
|
|
10
|
+
.replace(/-+/g, "-")
|
|
11
|
+
.replace(/^-|-$/g, "")
|
|
12
|
+
.toLowerCase();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export function titleCase(input: string): string {
|
|
16
|
+
return input
|
|
17
|
+
.split(/\s+/g)
|
|
18
|
+
.filter(Boolean)
|
|
19
|
+
.map((w) => w[0]?.toUpperCase() + w.slice(1))
|
|
20
|
+
.join(" ");
|
|
21
|
+
}
|
package/src/cli/types.ts
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
export type SpecSource = "embedded" | "file" | "url";
|
|
2
|
+
|
|
3
|
+
export type SecurityRequirement = Record<string, string[]>;
|
|
4
|
+
|
|
5
|
+
export type OpenApiDoc = {
|
|
6
|
+
openapi: string;
|
|
7
|
+
info?: {
|
|
8
|
+
title?: string;
|
|
9
|
+
version?: string;
|
|
10
|
+
};
|
|
11
|
+
servers?: Array<{ url: string; description?: string; variables?: unknown }>;
|
|
12
|
+
security?: SecurityRequirement[];
|
|
13
|
+
components?: {
|
|
14
|
+
securitySchemes?: Record<string, unknown>;
|
|
15
|
+
};
|
|
16
|
+
paths?: Record<string, unknown>;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export type NormalizedParameter = {
|
|
20
|
+
in: "path" | "query" | "header" | "cookie";
|
|
21
|
+
name: string;
|
|
22
|
+
required: boolean;
|
|
23
|
+
description?: string;
|
|
24
|
+
schema?: unknown;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
// Minimal JSON Schema-like shape for validation and flag expansion.
|
|
28
|
+
export type JsonSchema = Record<string, unknown>;
|
|
29
|
+
|
|
30
|
+
export function isJsonSchema(value: unknown): value is JsonSchema {
|
|
31
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export type NormalizedRequestBody = {
|
|
35
|
+
required: boolean;
|
|
36
|
+
contentTypes: string[];
|
|
37
|
+
schemasByContentType: Record<string, unknown | undefined>;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
export type NormalizedOperation = {
|
|
41
|
+
key: string;
|
|
42
|
+
method: string;
|
|
43
|
+
path: string;
|
|
44
|
+
operationId?: string;
|
|
45
|
+
tags: string[];
|
|
46
|
+
summary?: string;
|
|
47
|
+
description?: string;
|
|
48
|
+
deprecated?: boolean;
|
|
49
|
+
security?: SecurityRequirement[];
|
|
50
|
+
parameters: NormalizedParameter[];
|
|
51
|
+
requestBody?: NormalizedRequestBody;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
export type LoadedSpec = {
|
|
55
|
+
source: SpecSource;
|
|
56
|
+
id: string;
|
|
57
|
+
fingerprint: string;
|
|
58
|
+
doc: OpenApiDoc;
|
|
59
|
+
};
|
package/src/compiled.ts
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { main } from "./cli/main.ts";
|
|
4
|
+
import { env, envRequired } from "./macros/env.ts" with { type: "macro" };
|
|
5
|
+
import { loadSpec } from "./macros/spec.ts" with { type: "macro" };
|
|
6
|
+
|
|
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"));
|
|
10
|
+
|
|
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 });
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun macro: reads an environment variable at bundle-time.
|
|
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
|
+
*/
|
|
9
|
+
export function env(name: string): string | undefined {
|
|
10
|
+
if (!name) throw new Error("env macro: missing variable name");
|
|
11
|
+
return process.env[name];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Bun macro: reads a required environment variable at bundle-time.
|
|
16
|
+
* Throws if the env var is not set.
|
|
17
|
+
*/
|
|
18
|
+
export function envRequired(name: string): string {
|
|
19
|
+
if (!name) throw new Error("envRequired macro: missing variable name");
|
|
20
|
+
const value = process.env[name];
|
|
21
|
+
if (value === undefined) {
|
|
22
|
+
throw new Error(`Missing required env var: ${name}`);
|
|
23
|
+
}
|
|
24
|
+
return value;
|
|
25
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bun macro: loads an OpenAPI spec from a URL or file path at bundle-time.
|
|
3
|
+
* The spec text is inlined into the bundle.
|
|
4
|
+
*/
|
|
5
|
+
export async function loadSpec(spec: string): Promise<string> {
|
|
6
|
+
if (!spec) throw new Error("loadSpec macro: missing spec path/URL");
|
|
7
|
+
|
|
8
|
+
if (/^https?:\/\//i.test(spec)) {
|
|
9
|
+
const res = await fetch(spec);
|
|
10
|
+
if (!res.ok) {
|
|
11
|
+
throw new Error(`Failed to fetch spec: ${res.status} ${res.statusText}`);
|
|
12
|
+
}
|
|
13
|
+
return await res.text();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return await Bun.file(spec).text();
|
|
17
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
// Environment setup & latest features
|
|
4
|
+
"lib": ["ESNext"],
|
|
5
|
+
"target": "ESNext",
|
|
6
|
+
"module": "Preserve",
|
|
7
|
+
"moduleDetection": "force",
|
|
8
|
+
"jsx": "react-jsx",
|
|
9
|
+
"allowJs": true,
|
|
10
|
+
|
|
11
|
+
// Bundler mode
|
|
12
|
+
"moduleResolution": "bundler",
|
|
13
|
+
"allowImportingTsExtensions": true,
|
|
14
|
+
"verbatimModuleSyntax": true,
|
|
15
|
+
"noEmit": true,
|
|
16
|
+
|
|
17
|
+
// Best practices
|
|
18
|
+
"strict": true,
|
|
19
|
+
"skipLibCheck": true,
|
|
20
|
+
"noFallthroughCasesInSwitch": true,
|
|
21
|
+
"noUncheckedIndexedAccess": true,
|
|
22
|
+
"noImplicitOverride": true,
|
|
23
|
+
|
|
24
|
+
// Some stricter flags (disabled by default)
|
|
25
|
+
"noUnusedLocals": false,
|
|
26
|
+
"noUnusedParameters": false,
|
|
27
|
+
"noPropertyAccessFromIndexSignature": false
|
|
28
|
+
}
|
|
29
|
+
}
|