@askrjs/cli 0.0.2 → 0.0.4
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/README.md +46 -7
- package/dist/add.d.ts +16 -0
- package/dist/add.js +558 -1
- package/dist/cli.d.ts +5 -0
- package/dist/cli.js +43 -16
- package/dist/create.d.ts +5 -0
- package/dist/create.js +104 -26
- package/dist/directory-swap-DWoHtx7C.js +35 -0
- package/dist/discovery-Difb7Y4G.js +323 -0
- package/dist/generate.d.ts +5 -0
- package/dist/generate.js +461 -0
- package/dist/is-direct-execution-Cdlr-ZUl.js +0 -2
- package/dist/openapi.d.ts +25 -0
- package/dist/openapi.js +49 -16
- package/dist/planner-VAj7qlxr.js +285 -0
- package/dist/range-YUs9eimn.js +125 -0
- package/dist/registry-CyrRHRG5.js +209 -0
- package/dist/skills/askr-dashboard-charts/SKILL.md +61 -44
- package/dist/skills/askr-dashboard-charts/agents/openai.yaml +2 -2
- package/dist/skills/askr-ssr-ssg/SKILL.md +3 -1
- package/dist/{skills-Bh0mEhIx.js → skills-B7CbWur9.js} +89 -26
- package/dist/skills.d.ts +18 -0
- package/dist/skills.js +1 -1
- package/dist/specification-DXnDOC-0.js +49 -0
- package/dist/ssg-config.d.ts +41 -0
- package/dist/ssg-config.js +1 -0
- package/dist/ssg.d.ts +61 -0
- package/dist/ssg.js +360 -24
- package/dist/templates/full-stack/AGENTS.md +0 -1
- package/dist/templates/full-stack/README.md +0 -1
- package/dist/templates/full-stack/index.html +0 -1
- package/dist/templates/full-stack/package.json +5 -3
- package/dist/templates/full-stack/server.ts +5 -9
- package/dist/templates/full-stack/src/action-authorizations.ts +2 -2
- package/dist/templates/full-stack/src/actions/create-message.ts +5 -5
- package/dist/templates/full-stack/src/i18n.ts +4 -4
- package/dist/templates/full-stack/src/main.tsx +4 -5
- package/dist/templates/full-stack/src/pages/home.tsx +5 -5
- package/dist/templates/full-stack/src/pages/layout.tsx +15 -5
- package/dist/templates/full-stack/src/pages/not-found.tsx +5 -2
- package/dist/templates/full-stack/src/routes.tsx +11 -12
- package/dist/templates/full-stack/src/schemas.ts +1 -2
- package/dist/templates/full-stack/src/server/action-registry.ts +5 -10
- package/dist/templates/full-stack/src/server/actions/create-message.ts +4 -4
- package/dist/templates/full-stack/src/server/app.ts +67 -57
- package/dist/templates/full-stack/src/server/dependencies.ts +10 -10
- package/dist/templates/full-stack/src/server/entry-server.ts +3 -3
- package/dist/templates/full-stack/src/telemetry.ts +1 -2
- package/dist/templates/full-stack/tests/actions/create-message.test.ts +7 -8
- package/dist/templates/full-stack/tsconfig.json +1 -2
- package/dist/templates/full-stack/vite.config.ts +5 -6
- package/dist/templates/spa/AGENTS.md +1 -1
- package/dist/templates/spa/package.json +3 -4
- package/dist/templates/spa/src/adapters/operations-client.ts +7 -2
- package/dist/templates/spa/src/pages/app/_routes.tsx +2 -0
- package/dist/templates/spa/src/pages/app/admin-home.tsx +21 -3
- package/dist/templates/spa/src/pages/public/_routes.tsx +2 -0
- package/dist/templates/spa/src/styles.css +1 -1
- package/dist/templates/spa/tsconfig.json +2 -1
- package/dist/templates/spa/vitest.config.ts +0 -4
- package/dist/templates/ssg/README.md +1 -1
- package/dist/templates/ssg/package.json +1 -1
- package/dist/templates/ssg/ssg.config.ts +7 -0
- package/dist/templates/ssg/tests/ssg-config.test.ts +5 -3
- package/dist/templates/ssg/tsconfig.json +2 -1
- package/dist/templates/ssg/vitest.config.ts +0 -4
- package/dist/templates/ssr/package.json +2 -2
- package/dist/templates/ssr/server.ts +4 -10
- package/dist/templates/ssr/src/entry-server.tsx +6 -4
- package/dist/templates/startkit/package.json +2 -2
- package/dist/templates/startkit/src/lib/mock-data.ts +17 -39
- package/dist/templates/startkit/tsconfig.json +2 -1
- package/dist/update.d.ts +41 -0
- package/dist/update.js +282 -2
- package/dist/writer-D8qe_7ud.js +226 -0
- package/package.json +23 -22
- package/dist/add-DqhqRzBQ.js +0 -555
- package/dist/add-DqhqRzBQ.js.map +0 -1
- package/dist/cli.js.map +0 -1
- package/dist/create.js.map +0 -1
- package/dist/is-direct-execution-Cdlr-ZUl.js.map +0 -1
- package/dist/openapi.js.map +0 -1
- package/dist/skills-Bh0mEhIx.js.map +0 -1
- package/dist/ssg.js.map +0 -1
- package/dist/templates/full-stack/src/vite-server.d.ts +0 -6
- package/dist/templates/spa/src/styles.d.ts +0 -1
- package/dist/templates/ssg/src/vite-env.d.ts +0 -5
- package/dist/templates/ssr/src/vite-server.d.ts +0 -7
- package/dist/templates/startkit/src/vite-env.d.ts +0 -5
- package/dist/update-DyW1na5V.js +0 -1140
- package/dist/update-DyW1na5V.js.map +0 -1
- package/skills/askr-accessibility/SKILL.md +0 -68
- package/skills/askr-accessibility/agents/openai.yaml +0 -4
- package/skills/askr-agent-execution/SKILL.md +0 -85
- package/skills/askr-agent-execution/agents/openai.yaml +0 -4
- package/skills/askr-agent-workflows/SKILL.md +0 -57
- package/skills/askr-agent-workflows/agents/openai.yaml +0 -4
- package/skills/askr-api-integration/SKILL.md +0 -79
- package/skills/askr-api-integration/agents/openai.yaml +0 -4
- package/skills/askr-app-builder/SKILL.md +0 -99
- package/skills/askr-app-builder/agents/openai.yaml +0 -4
- package/skills/askr-auth-access/SKILL.md +0 -74
- package/skills/askr-auth-access/agents/openai.yaml +0 -4
- package/skills/askr-cli-vite/SKILL.md +0 -76
- package/skills/askr-cli-vite/agents/openai.yaml +0 -4
- package/skills/askr-dashboard-charts/SKILL.md +0 -82
- package/skills/askr-dashboard-charts/agents/openai.yaml +0 -4
- package/skills/askr-design-system/SKILL.md +0 -86
- package/skills/askr-design-system/agents/openai.yaml +0 -4
- package/skills/askr-env-config/SKILL.md +0 -74
- package/skills/askr-env-config/agents/openai.yaml +0 -4
- package/skills/askr-error-loading-empty/SKILL.md +0 -89
- package/skills/askr-error-loading-empty/agents/openai.yaml +0 -4
- package/skills/askr-file-upload-artifacts/SKILL.md +0 -77
- package/skills/askr-file-upload-artifacts/agents/openai.yaml +0 -4
- package/skills/askr-forms-tables-crud/SKILL.md +0 -98
- package/skills/askr-forms-tables-crud/agents/openai.yaml +0 -4
- package/skills/askr-mental-model/SKILL.md +0 -109
- package/skills/askr-mental-model/agents/openai.yaml +0 -4
- package/skills/askr-migration-react/SKILL.md +0 -81
- package/skills/askr-migration-react/agents/openai.yaml +0 -4
- package/skills/askr-observability-debugging/SKILL.md +0 -83
- package/skills/askr-observability-debugging/agents/openai.yaml +0 -4
- package/skills/askr-project-structure/SKILL.md +0 -79
- package/skills/askr-project-structure/agents/openai.yaml +0 -4
- package/skills/askr-query-mutation/SKILL.md +0 -115
- package/skills/askr-query-mutation/agents/openai.yaml +0 -4
- package/skills/askr-realtime-streaming/SKILL.md +0 -76
- package/skills/askr-realtime-streaming/agents/openai.yaml +0 -4
- package/skills/askr-resources-data/SKILL.md +0 -100
- package/skills/askr-resources-data/agents/openai.yaml +0 -4
- package/skills/askr-routing-layouts/SKILL.md +0 -107
- package/skills/askr-routing-layouts/agents/openai.yaml +0 -4
- package/skills/askr-runtime-reactivity/SKILL.md +0 -92
- package/skills/askr-runtime-reactivity/agents/openai.yaml +0 -4
- package/skills/askr-ssr-ssg/SKILL.md +0 -76
- package/skills/askr-ssr-ssg/agents/openai.yaml +0 -4
- package/skills/askr-testing-determinism/SKILL.md +0 -75
- package/skills/askr-testing-determinism/agents/openai.yaml +0 -4
- package/skills/askr-theming/SKILL.md +0 -86
- package/skills/askr-theming/agents/openai.yaml +0 -4
- package/skills/askr-ui-composition/SKILL.md +0 -92
- package/skills/askr-ui-composition/agents/openai.yaml +0 -4
- package/templates/full-stack/AGENTS.md +0 -12
- package/templates/full-stack/README.md +0 -18
- package/templates/full-stack/gitignore.template +0 -4
- package/templates/full-stack/index.html +0 -14
- package/templates/full-stack/package.json +0 -37
- package/templates/full-stack/server.ts +0 -13
- package/templates/full-stack/src/action-authorizations.ts +0 -10
- package/templates/full-stack/src/actions/create-message.ts +0 -10
- package/templates/full-stack/src/i18n.ts +0 -8
- package/templates/full-stack/src/main.tsx +0 -12
- package/templates/full-stack/src/pages/home.tsx +0 -25
- package/templates/full-stack/src/pages/layout.tsx +0 -33
- package/templates/full-stack/src/pages/not-found.tsx +0 -4
- package/templates/full-stack/src/routes.tsx +0 -20
- package/templates/full-stack/src/schemas.ts +0 -11
- package/templates/full-stack/src/server/action-registry.ts +0 -12
- package/templates/full-stack/src/server/actions/create-message.ts +0 -14
- package/templates/full-stack/src/server/app.ts +0 -66
- package/templates/full-stack/src/server/dependencies.ts +0 -55
- package/templates/full-stack/src/server/entry-server.ts +0 -8
- package/templates/full-stack/src/telemetry.ts +0 -4
- package/templates/full-stack/src/vite-server.d.ts +0 -6
- package/templates/full-stack/tests/actions/create-message.test.ts +0 -13
- package/templates/full-stack/tsconfig.json +0 -16
- package/templates/full-stack/vite.config.ts +0 -11
- package/templates/spa/AGENTS.md +0 -60
- package/templates/spa/README.md +0 -96
- package/templates/spa/gitignore.template +0 -16
- package/templates/spa/index.html +0 -13
- package/templates/spa/package.json +0 -35
- package/templates/spa/src/adapters/operations-client.ts +0 -90
- package/templates/spa/src/components/shared/metric-card.tsx +0 -27
- package/templates/spa/src/components/shared/status-badge.tsx +0 -24
- package/templates/spa/src/features/operations/operations.query.ts +0 -9
- package/templates/spa/src/main.tsx +0 -10
- package/templates/spa/src/pages/_layout.tsx +0 -9
- package/templates/spa/src/pages/_routes.tsx +0 -27
- package/templates/spa/src/pages/app/_layout.tsx +0 -94
- package/templates/spa/src/pages/app/_routes.tsx +0 -10
- package/templates/spa/src/pages/app/admin-home.tsx +0 -152
- package/templates/spa/src/pages/app/agent-runs.tsx +0 -93
- package/templates/spa/src/pages/app/settings.tsx +0 -91
- package/templates/spa/src/pages/auth/_layout.tsx +0 -38
- package/templates/spa/src/pages/auth/_routes.tsx +0 -6
- package/templates/spa/src/pages/auth/login.tsx +0 -63
- package/templates/spa/src/pages/not-found.tsx +0 -22
- package/templates/spa/src/pages/public/_layout.tsx +0 -47
- package/templates/spa/src/pages/public/_routes.tsx +0 -6
- package/templates/spa/src/pages/public/home.tsx +0 -143
- package/templates/spa/src/shared/format.ts +0 -12
- package/templates/spa/src/shared/navigation.ts +0 -15
- package/templates/spa/src/styles/components.css +0 -49
- package/templates/spa/src/styles/layout.css +0 -192
- package/templates/spa/src/styles/reset.css +0 -44
- package/templates/spa/src/styles/theme.css +0 -57
- package/templates/spa/src/styles/tokens.css +0 -20
- package/templates/spa/src/styles.css +0 -7
- package/templates/spa/src/styles.d.ts +0 -1
- package/templates/spa/tests/app.test.tsx +0 -37
- package/templates/spa/tests/components/shared.test.tsx +0 -12
- package/templates/spa/tests/resources.test.ts +0 -22
- package/templates/spa/tsconfig.json +0 -20
- package/templates/spa/tsconfig.node.json +0 -10
- package/templates/spa/vite.config.ts +0 -24
- package/templates/spa/vitest.config.ts +0 -17
- package/templates/ssg/AGENTS.md +0 -55
- package/templates/ssg/README.md +0 -39
- package/templates/ssg/gitignore.template +0 -17
- package/templates/ssg/index.html +0 -13
- package/templates/ssg/package.json +0 -35
- package/templates/ssg/src/app.tsx +0 -14
- package/templates/ssg/src/components/badge.tsx +0 -3
- package/templates/ssg/src/components/counter.tsx +0 -30
- package/templates/ssg/src/components/site-shell.tsx +0 -111
- package/templates/ssg/src/jsx.d.ts +0 -23
- package/templates/ssg/src/main.tsx +0 -8
- package/templates/ssg/src/pages/about.tsx +0 -82
- package/templates/ssg/src/pages/content.tsx +0 -63
- package/templates/ssg/src/pages/example.tsx +0 -97
- package/templates/ssg/src/pages/home.tsx +0 -51
- package/templates/ssg/src/resources/user.ts +0 -15
- package/templates/ssg/src/routes.tsx +0 -15
- package/templates/ssg/src/styles.css +0 -89
- package/templates/ssg/src/vite-env.d.ts +0 -5
- package/templates/ssg/ssg.config.ts +0 -34
- package/templates/ssg/tests/app.test.tsx +0 -19
- package/templates/ssg/tests/components/counter.test.tsx +0 -18
- package/templates/ssg/tests/resources.test.ts +0 -11
- package/templates/ssg/tests/ssg-config.test.ts +0 -11
- package/templates/ssg/tsconfig.json +0 -20
- package/templates/ssg/tsconfig.node.json +0 -10
- package/templates/ssg/vite.config.ts +0 -24
- package/templates/ssg/vitest.config.ts +0 -17
- package/templates/ssr/AGENTS.md +0 -28
- package/templates/ssr/README.md +0 -38
- package/templates/ssr/gitignore.template +0 -16
- package/templates/ssr/index.html +0 -13
- package/templates/ssr/package.json +0 -34
- package/templates/ssr/server.ts +0 -16
- package/templates/ssr/src/app.tsx +0 -21
- package/templates/ssr/src/components/counter.tsx +0 -26
- package/templates/ssr/src/entry-server.tsx +0 -10
- package/templates/ssr/src/main.tsx +0 -11
- package/templates/ssr/src/pages/about.tsx +0 -75
- package/templates/ssr/src/pages/example.tsx +0 -113
- package/templates/ssr/src/pages/home.tsx +0 -56
- package/templates/ssr/src/resources/user.ts +0 -15
- package/templates/ssr/src/routes.tsx +0 -13
- package/templates/ssr/src/styles.css +0 -148
- package/templates/ssr/src/vite-server.d.ts +0 -7
- package/templates/ssr/tests/app.test.tsx +0 -24
- package/templates/ssr/tests/components/counter.test.tsx +0 -13
- package/templates/ssr/tests/resources.test.ts +0 -11
- package/templates/ssr/tsconfig.json +0 -22
- package/templates/ssr/tsconfig.node.json +0 -10
- package/templates/ssr/tsconfig.server.json +0 -15
- package/templates/ssr/vite.config.ts +0 -25
- package/templates/ssr/vitest.config.ts +0 -13
- package/templates/startkit/AGENTS.md +0 -77
- package/templates/startkit/README.md +0 -192
- package/templates/startkit/gitignore.template +0 -16
- package/templates/startkit/index.html +0 -13
- package/templates/startkit/package.json +0 -35
- package/templates/startkit/src/components/app-header.tsx +0 -93
- package/templates/startkit/src/components/app-sidebar.tsx +0 -95
- package/templates/startkit/src/components/data-table.tsx +0 -80
- package/templates/startkit/src/components/empty-state.tsx +0 -13
- package/templates/startkit/src/components/page-header.tsx +0 -23
- package/templates/startkit/src/components/stat-card.tsx +0 -24
- package/templates/startkit/src/features/accounts/account-filters.tsx +0 -61
- package/templates/startkit/src/features/accounts/account-table.tsx +0 -79
- package/templates/startkit/src/lib/format.ts +0 -31
- package/templates/startkit/src/lib/mock-data.ts +0 -441
- package/templates/startkit/src/lib/routes.ts +0 -116
- package/templates/startkit/src/main.tsx +0 -12
- package/templates/startkit/src/pages/_layout.tsx +0 -56
- package/templates/startkit/src/pages/auth/_layout.tsx +0 -7
- package/templates/startkit/src/pages/auth/login.tsx +0 -129
- package/templates/startkit/src/pages/home.tsx +0 -83
- package/templates/startkit/src/pages/not-found.tsx +0 -24
- package/templates/startkit/src/pages/workspace/_layout.tsx +0 -17
- package/templates/startkit/src/pages/workspace/accounts/index.tsx +0 -217
- package/templates/startkit/src/pages/workspace/dashboard.tsx +0 -118
- package/templates/startkit/src/pages/workspace/settings.tsx +0 -218
- package/templates/startkit/src/router.tsx +0 -7
- package/templates/startkit/src/routes/auth-config.ts +0 -38
- package/templates/startkit/src/routes/auth.ts +0 -8
- package/templates/startkit/src/routes/index.ts +0 -28
- package/templates/startkit/src/routes/public.ts +0 -8
- package/templates/startkit/src/routes/workspace/accounts.ts +0 -8
- package/templates/startkit/src/routes/workspace/index.ts +0 -14
- package/templates/startkit/src/styles/components.css +0 -396
- package/templates/startkit/src/styles/layout.css +0 -192
- package/templates/startkit/src/styles/reset.css +0 -38
- package/templates/startkit/src/styles/theme.css +0 -76
- package/templates/startkit/src/styles/tokens.css +0 -67
- package/templates/startkit/src/styles.css +0 -7
- package/templates/startkit/src/toast.ts +0 -55
- package/templates/startkit/src/utils/join-classes.ts +0 -4
- package/templates/startkit/src/vite-env.d.ts +0 -5
- package/templates/startkit/tests/app.test.tsx +0 -32
- package/templates/startkit/tests/preferences.test.ts +0 -43
- package/templates/startkit/tests/resources.test.ts +0 -82
- package/templates/startkit/tsconfig.json +0 -20
- package/templates/startkit/tsconfig.node.json +0 -10
- package/templates/startkit/vite.config.ts +0 -32
- package/templates/startkit/vitest.config.ts +0 -13
package/dist/generate.js
ADDED
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
import { mkdir, mkdtemp, readFile, readdir, rename, rm, stat, writeFile } from "node:fs/promises";
|
|
2
|
+
import { basename, dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
4
|
+
import { load } from "js-yaml";
|
|
5
|
+
//#region src/generate/generator.ts
|
|
6
|
+
const OWNED = [
|
|
7
|
+
".askr-generated.json",
|
|
8
|
+
"schemas.ts",
|
|
9
|
+
"operations.ts",
|
|
10
|
+
"api.ts",
|
|
11
|
+
"index.ts"
|
|
12
|
+
];
|
|
13
|
+
const METHODS = [
|
|
14
|
+
"get",
|
|
15
|
+
"post",
|
|
16
|
+
"put",
|
|
17
|
+
"patch",
|
|
18
|
+
"delete",
|
|
19
|
+
"head",
|
|
20
|
+
"options"
|
|
21
|
+
];
|
|
22
|
+
const identifier = /^[A-Za-z_$][\w$]*$/;
|
|
23
|
+
const pointer = (...parts) => `#/${parts.map((p) => String(p).replace(/~/g, "~0").replace(/\//g, "~1")).join("/")}`;
|
|
24
|
+
var GenerationError = class extends Error {};
|
|
25
|
+
const fail = (message, at, operationId) => {
|
|
26
|
+
throw new GenerationError(`${operationId ? `${operationId}: ` : ""}${message} at ${at}`);
|
|
27
|
+
};
|
|
28
|
+
const pascal = (name) => {
|
|
29
|
+
const result = name.replace(/(^|[^A-Za-z0-9]+)([A-Za-z0-9])/g, (_, _s, c) => c.toUpperCase()).replace(/[^A-Za-z0-9_$]/g, "");
|
|
30
|
+
return /^\d/.test(result) ? `_${result}` : result;
|
|
31
|
+
};
|
|
32
|
+
function schemaType(schema, at, direction = "response") {
|
|
33
|
+
if (schema.$ref) {
|
|
34
|
+
const name = decodeURIComponent(String(schema.$ref).split("/").at(-1));
|
|
35
|
+
return pascal(name);
|
|
36
|
+
}
|
|
37
|
+
if (Array.isArray(schema.type)) {
|
|
38
|
+
const includesNull = schema.type.includes("null");
|
|
39
|
+
const rendered = [...new Set(schema.type.filter((value) => value !== "null"))].map((variant) => schemaType({
|
|
40
|
+
...schema,
|
|
41
|
+
type: variant,
|
|
42
|
+
nullable: false
|
|
43
|
+
}, at, direction));
|
|
44
|
+
if (includesNull) rendered.push("null");
|
|
45
|
+
if (rendered.length === 0) fail("Schema type array must not be empty", at);
|
|
46
|
+
return [...new Set(rendered)].join(" | ");
|
|
47
|
+
}
|
|
48
|
+
if (schema.const !== void 0) return JSON.stringify(schema.const);
|
|
49
|
+
if (schema.enum) return schema.enum.map((value) => JSON.stringify(value)).join(" | ") || "never";
|
|
50
|
+
if (schema.oneOf || schema.anyOf) return (schema.oneOf ?? schema.anyOf).map((item, i) => schemaType(item, `${at}/${schema.oneOf ? "oneOf" : "anyOf"}/${i}`, direction)).join(" | ");
|
|
51
|
+
if (schema.allOf) return schema.allOf.map((item, i) => schemaType(item, `${at}/allOf/${i}`, direction)).join(" & ");
|
|
52
|
+
let type = "never";
|
|
53
|
+
if (schema.type === "string") type = "string";
|
|
54
|
+
else if (schema.type === "number" || schema.type === "integer") type = "number";
|
|
55
|
+
else if (schema.type === "boolean") type = "boolean";
|
|
56
|
+
else if (schema.type === "null") type = "null";
|
|
57
|
+
else if (schema.type === "array" || schema.items) type = `Array<${schemaType(schema.items ?? {}, `${at}/items`, direction)}>`;
|
|
58
|
+
else if (schema.type === "object" || schema.properties || schema.additionalProperties !== void 0) {
|
|
59
|
+
const required = new Set(schema.required ?? []);
|
|
60
|
+
const properties = Object.entries(schema.properties ?? {}).filter(([, value]) => !(direction === "request" ? value.readOnly : value.writeOnly)).map(([key, value]) => ` ${JSON.stringify(key)}${required.has(key) ? "" : "?"}: ${schemaType(value, `${at}/properties/${key}`, direction)};`);
|
|
61
|
+
if (schema.additionalProperties && typeof schema.additionalProperties === "object") properties.push(` [key: string]: ${schemaType(schema.additionalProperties, `${at}/additionalProperties`, direction)};`);
|
|
62
|
+
type = `{\n${properties.join("\n")}\n}`;
|
|
63
|
+
} else if (!schema.type) fail("Unsupported schema without a type", at);
|
|
64
|
+
else fail(`Unsupported schema type ${schema.type}`, at);
|
|
65
|
+
return schema.nullable || Array.isArray(schema.type) && schema.type.includes("null") ? `${type} | null` : type;
|
|
66
|
+
}
|
|
67
|
+
function responseSchema(operation, at, success) {
|
|
68
|
+
return Object.entries(operation.responses ?? {}).filter(([status]) => status === "default" ? !success : success === /^2\d\d$/.test(status)).sort(([a], [b]) => a.localeCompare(b, void 0, { numeric: true }));
|
|
69
|
+
}
|
|
70
|
+
function pickContent(content, at, direction) {
|
|
71
|
+
const entries = Object.entries(content ?? {}).sort(([a], [b]) => a.localeCompare(b));
|
|
72
|
+
if (!entries.length) return {
|
|
73
|
+
type: "undefined",
|
|
74
|
+
codec: "empty()",
|
|
75
|
+
media: []
|
|
76
|
+
};
|
|
77
|
+
const types = entries.map(([mediaType, value]) => ({
|
|
78
|
+
mediaType,
|
|
79
|
+
type: schemaType(value.schema ?? {}, `${at}/content/${mediaType.replace(/~/g, "~0").replace(/\//g, "~1")}/schema`, direction)
|
|
80
|
+
}));
|
|
81
|
+
const union = [...new Set(types.map((v) => v.type))].join(" | ");
|
|
82
|
+
return {
|
|
83
|
+
type: union,
|
|
84
|
+
codec: types.length === 1 ? codecFor(types[0].mediaType, union) : `content({ ${types.map((v) => `${JSON.stringify(v.mediaType)}: ${codecFor(v.mediaType, v.type)}`).join(", ")} })`,
|
|
85
|
+
media: types.map((v) => v.mediaType)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function codecFor(mediaType, type) {
|
|
89
|
+
if (mediaType === "application/json" || mediaType.endsWith("+json")) return `json<${type}>()`;
|
|
90
|
+
if (mediaType.startsWith("text/")) return "text()";
|
|
91
|
+
if (mediaType === "application/x-www-form-urlencoded") return "urlEncoded()";
|
|
92
|
+
if (mediaType === "multipart/form-data") return "multipart()";
|
|
93
|
+
if (mediaType === "application/octet-stream") return "arrayBuffer()";
|
|
94
|
+
fail(`Unsupported media type ${mediaType}`, "#/content");
|
|
95
|
+
}
|
|
96
|
+
const pointerPart = (value) => decodeURIComponent(value).replace(/~1/g, "/").replace(/~0/g, "~");
|
|
97
|
+
const pointerValue = (document, fragment) => {
|
|
98
|
+
let value = document;
|
|
99
|
+
for (const part of fragment.replace(/^#?\/?/, "").split("/").filter(Boolean)) {
|
|
100
|
+
if (!value || typeof value !== "object") throw new GenerationError(`Reference target is missing at #/${part}`);
|
|
101
|
+
value = value[pointerPart(part)];
|
|
102
|
+
}
|
|
103
|
+
if (value === void 0) throw new GenerationError(`Reference target is missing at ${fragment || "#"}`);
|
|
104
|
+
return value;
|
|
105
|
+
};
|
|
106
|
+
function parseOpenApiDocument(contents, source) {
|
|
107
|
+
let parsed;
|
|
108
|
+
try {
|
|
109
|
+
parsed = JSON.parse(contents);
|
|
110
|
+
} catch {
|
|
111
|
+
try {
|
|
112
|
+
parsed = load(contents);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
throw new GenerationError(`Unable to parse OpenAPI document ${source}: ${error instanceof Error ? error.message : String(error)}`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new GenerationError(`OpenAPI document must be an object: ${source}`);
|
|
118
|
+
return parsed;
|
|
119
|
+
}
|
|
120
|
+
async function readSource(uri) {
|
|
121
|
+
if (uri.startsWith("http://") || uri.startsWith("https://")) {
|
|
122
|
+
const response = await fetch(uri);
|
|
123
|
+
if (!response.ok) throw new GenerationError(`Unable to fetch OpenAPI reference ${uri}: ${response.status} ${response.statusText}`);
|
|
124
|
+
return {
|
|
125
|
+
uri,
|
|
126
|
+
document: parseOpenApiDocument(await response.text(), uri)
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
const path = fileURLToPath(uri);
|
|
130
|
+
return {
|
|
131
|
+
uri,
|
|
132
|
+
document: parseOpenApiDocument(await readFile(path, "utf8"), path)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
var OpenApiBundler = class {
|
|
136
|
+
root;
|
|
137
|
+
rootUri;
|
|
138
|
+
sources = /* @__PURE__ */ new Map();
|
|
139
|
+
aliases = /* @__PURE__ */ new Map();
|
|
140
|
+
names = /* @__PURE__ */ new Map();
|
|
141
|
+
constructor(root, rootUri) {
|
|
142
|
+
this.root = root;
|
|
143
|
+
this.rootUri = rootUri;
|
|
144
|
+
}
|
|
145
|
+
async bundle() {
|
|
146
|
+
if (!this.root.components) this.root.components = {};
|
|
147
|
+
if (!this.root.components.schemas) this.root.components.schemas = {};
|
|
148
|
+
await this.expandObject(this.root, this.rootUri, this.root, /* @__PURE__ */ new Set());
|
|
149
|
+
return this.root;
|
|
150
|
+
}
|
|
151
|
+
async source(uri) {
|
|
152
|
+
let pending = this.sources.get(uri);
|
|
153
|
+
if (!pending) {
|
|
154
|
+
pending = readSource(uri);
|
|
155
|
+
this.sources.set(uri, pending);
|
|
156
|
+
}
|
|
157
|
+
return pending;
|
|
158
|
+
}
|
|
159
|
+
componentName(uri, fragment) {
|
|
160
|
+
const canonical = `${uri}#${fragment}`;
|
|
161
|
+
const existing = this.aliases.get(canonical);
|
|
162
|
+
if (existing) return existing;
|
|
163
|
+
const raw = fragment.split("/").filter(Boolean).at(-1) ?? basename(new URL(uri).pathname).replace(/\.[^.]+$/, "");
|
|
164
|
+
const base = pascal(pointerPart(raw)) || "ExternalSchema";
|
|
165
|
+
let name = base;
|
|
166
|
+
let suffix = 2;
|
|
167
|
+
while (this.names.has(name) && this.names.get(name) !== canonical) name = `${base}${suffix++}`;
|
|
168
|
+
this.aliases.set(canonical, name);
|
|
169
|
+
this.names.set(name, canonical);
|
|
170
|
+
return name;
|
|
171
|
+
}
|
|
172
|
+
async expandObject(value, uri, document, stack) {
|
|
173
|
+
for (const [key, child] of Object.entries(value)) value[key] = await this.expand(child, uri, document, stack);
|
|
174
|
+
}
|
|
175
|
+
async expand(value, uri, document, stack) {
|
|
176
|
+
if (Array.isArray(value)) return Promise.all(value.map((item) => this.expand(item, uri, document, stack)));
|
|
177
|
+
if (!value || typeof value !== "object") return value;
|
|
178
|
+
const object = value;
|
|
179
|
+
if (typeof object.$ref === "string") {
|
|
180
|
+
const resolved = await this.resolveRef(object.$ref, uri, document, stack);
|
|
181
|
+
const siblingEntries = Object.entries(object).filter(([key]) => key !== "$ref");
|
|
182
|
+
if (!siblingEntries.length) return resolved;
|
|
183
|
+
const result = resolved && typeof resolved === "object" && !Array.isArray(resolved) ? { ...resolved } : {};
|
|
184
|
+
for (const [key, child] of siblingEntries) result[key] = await this.expand(child, uri, document, stack);
|
|
185
|
+
return result;
|
|
186
|
+
}
|
|
187
|
+
const result = {};
|
|
188
|
+
for (const [key, child] of Object.entries(object)) result[key] = await this.expand(child, uri, document, stack);
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
async resolveRef(ref, baseUri, document, stack) {
|
|
192
|
+
const hash = ref.indexOf("#");
|
|
193
|
+
const targetUri = hash === -1 ? new URL(ref, baseUri).href : new URL(ref.slice(0, hash) || baseUri, baseUri).href;
|
|
194
|
+
const fragment = hash === -1 ? "" : ref.slice(hash + 1);
|
|
195
|
+
const canonical = `${targetUri}#${fragment}`;
|
|
196
|
+
const targetSource = targetUri === this.rootUri ? {
|
|
197
|
+
uri: targetUri,
|
|
198
|
+
document
|
|
199
|
+
} : await this.source(targetUri);
|
|
200
|
+
const target = pointerValue(targetSource.document, fragment);
|
|
201
|
+
if (targetUri === this.rootUri && fragment.startsWith("/components/schemas/")) return { $ref: `#${fragment}` };
|
|
202
|
+
if (fragment.startsWith("/components/schemas/")) {
|
|
203
|
+
const name = this.componentName(targetUri, fragment);
|
|
204
|
+
if (stack.has(canonical)) return { $ref: `#/components/schemas/${name}` };
|
|
205
|
+
if (!(name in this.root.components.schemas)) {
|
|
206
|
+
this.root.components.schemas[name] = {};
|
|
207
|
+
const next = new Set(stack).add(canonical);
|
|
208
|
+
this.root.components.schemas[name] = await this.expand(target, targetSource.uri, targetSource.document, next);
|
|
209
|
+
}
|
|
210
|
+
return { $ref: `#/components/schemas/${name}` };
|
|
211
|
+
}
|
|
212
|
+
if (stack.has(canonical)) throw new GenerationError(`Circular non-schema reference at ${ref}`);
|
|
213
|
+
return this.expand(target, targetSource.uri, targetSource.document, new Set(stack).add(canonical));
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
async function loadOpenApi(input) {
|
|
217
|
+
const source = await readSource(/^https?:\/\//.test(input) ? input : pathToFileURL(resolve(input)).href);
|
|
218
|
+
return new OpenApiBundler(source.document, source.uri).bundle();
|
|
219
|
+
}
|
|
220
|
+
function generateFiles(document) {
|
|
221
|
+
const version = String(document.openapi ?? "");
|
|
222
|
+
if (!/^3\.(0|1)\.\d+$/.test(version)) fail(`Unsupported OpenAPI version ${version || "missing"}`, "#/openapi");
|
|
223
|
+
if (document.webhooks) fail("Webhooks are unsupported", "#/webhooks");
|
|
224
|
+
const schemas = document.components?.schemas ?? {};
|
|
225
|
+
const names = /* @__PURE__ */ new Map();
|
|
226
|
+
for (const name of Object.keys(schemas).sort()) {
|
|
227
|
+
const safe = pascal(name);
|
|
228
|
+
if (!safe || names.has(safe)) fail(`Component name collision for ${name}`, pointer("components", "schemas", name));
|
|
229
|
+
names.set(safe, name);
|
|
230
|
+
}
|
|
231
|
+
const schemaLines = [...names].map(([safe, original]) => `export type ${safe} = ${schemaType(schemas[original], pointer("components", "schemas", original))};`);
|
|
232
|
+
const operationTypes = [];
|
|
233
|
+
const descriptors = [];
|
|
234
|
+
const seen = /* @__PURE__ */ new Set();
|
|
235
|
+
for (const path of Object.keys(document.paths ?? {}).sort()) for (const method of METHODS) {
|
|
236
|
+
const operation = document.paths[path]?.[method];
|
|
237
|
+
if (!operation) continue;
|
|
238
|
+
const at = pointer("paths", path, method);
|
|
239
|
+
const id = operation.operationId;
|
|
240
|
+
if (!id || !identifier.test(id)) fail("A unique JavaScript-identifier operationId is required", `${at}/operationId`, id);
|
|
241
|
+
if (seen.has(id)) fail("Duplicate operationId", `${at}/operationId`, id);
|
|
242
|
+
seen.add(id);
|
|
243
|
+
if (operation.callbacks) fail("Callbacks are unsupported", `${at}/callbacks`, id);
|
|
244
|
+
const parametersByIdentity = /* @__PURE__ */ new Map();
|
|
245
|
+
for (const parameter of [...document.paths[path].parameters ?? [], ...operation.parameters ?? []]) {
|
|
246
|
+
if (parameter.$ref) fail("Parameter references must be bundled", `${at}/parameters`, id);
|
|
247
|
+
if (typeof parameter.name !== "string" || typeof parameter.in !== "string") fail("Parameters require string name and in fields", `${at}/parameters`, id);
|
|
248
|
+
parametersByIdentity.set(`${parameter.in}\0${parameter.name}`, parameter);
|
|
249
|
+
}
|
|
250
|
+
const parameters = [...parametersByIdentity.values()];
|
|
251
|
+
const byLocation = {
|
|
252
|
+
path: [],
|
|
253
|
+
query: [],
|
|
254
|
+
header: []
|
|
255
|
+
};
|
|
256
|
+
for (const parameter of parameters) {
|
|
257
|
+
if (parameter.in === "cookie") fail("Cookie parameters are unsupported", `${at}/parameters`, id);
|
|
258
|
+
if (!byLocation[parameter.in]) fail(`Unsupported parameter location ${parameter.in}`, `${at}/parameters`, id);
|
|
259
|
+
byLocation[parameter.in].push(parameter);
|
|
260
|
+
}
|
|
261
|
+
const templateParameters = new Set([...path.matchAll(/\{([^{}]+)\}/g)].map((match) => match[1]));
|
|
262
|
+
const declaredPathParameters = new Set(byLocation.path.map((parameter) => parameter.name));
|
|
263
|
+
for (const name of templateParameters) {
|
|
264
|
+
const parameter = byLocation.path.find((candidate) => candidate.name === name);
|
|
265
|
+
if (!parameter) fail(`Path template parameter ${name} is not declared`, `${at}/parameters`, id);
|
|
266
|
+
if (!parameter || parameter.required !== true) fail(`Path parameter ${name} must be required`, `${at}/parameters`, id);
|
|
267
|
+
}
|
|
268
|
+
for (const name of declaredPathParameters) if (!templateParameters.has(name)) fail(`Path parameter ${name} is not present in the path template`, `${at}/parameters`, id);
|
|
269
|
+
const prefix = pascal(id);
|
|
270
|
+
const renderParameters = (location) => {
|
|
271
|
+
const list = byLocation[location].sort((a, b) => a.name.localeCompare(b.name));
|
|
272
|
+
if (!list.length) return void 0;
|
|
273
|
+
const name = `${prefix}${pascal(location)}`;
|
|
274
|
+
operationTypes.push(`export type ${name} = {\n${list.map((p) => ` ${JSON.stringify(p.name)}${p.required || location === "path" ? "" : "?"}: ${schemaType(p.schema ?? {}, `${at}/parameters`, "request")};`).join("\n")}\n};`);
|
|
275
|
+
const defaultStyle = location === "query" ? "form" : "simple";
|
|
276
|
+
return {
|
|
277
|
+
name,
|
|
278
|
+
spec: `{ ${list.map((p) => {
|
|
279
|
+
const style = p.style ?? defaultStyle;
|
|
280
|
+
const explode = p.explode ?? style === "form";
|
|
281
|
+
return `${JSON.stringify(p.name)}: { style: ${JSON.stringify(style)}, explode: ${explode} }`;
|
|
282
|
+
}).join(", ")} }`
|
|
283
|
+
};
|
|
284
|
+
};
|
|
285
|
+
const params = renderParameters("path"), query = renderParameters("query"), headers = renderParameters("header");
|
|
286
|
+
const body = operation.requestBody ? pickContent(operation.requestBody.content, `${at}/requestBody`, "request") : void 0;
|
|
287
|
+
if (body) operationTypes.push(`export type ${prefix}Body = ${body.type};`);
|
|
288
|
+
const successes = responseSchema(operation, at, true);
|
|
289
|
+
if (!successes.length) fail("At least one 2xx response is required", `${at}/responses`, id);
|
|
290
|
+
const errors = responseSchema(operation, at, false);
|
|
291
|
+
const chain = [`${method === "delete" ? "del" : method}(${JSON.stringify(path)})`];
|
|
292
|
+
if (params) chain.push(`params<${params.name}>(${params.spec})`);
|
|
293
|
+
if (query) chain.push(`query<${query.name}>(${query.spec})`);
|
|
294
|
+
if (headers) chain.push(`headers<${headers.name}>(${headers.spec})`);
|
|
295
|
+
if (body) chain.push(`body(${body.codec})`);
|
|
296
|
+
for (const [status, response] of successes) {
|
|
297
|
+
if (response.links) fail("Response links are unsupported", `${at}/responses/${status}/links`, id);
|
|
298
|
+
const value = pickContent(response.content, `${at}/responses/${status}`, "response");
|
|
299
|
+
const typeName = `${prefix}Response${status}`;
|
|
300
|
+
operationTypes.push(`export type ${typeName} = ${value.type};`);
|
|
301
|
+
chain.push(`returns(${status === "200" ? "" : `${status}, `}${value.codec})`);
|
|
302
|
+
}
|
|
303
|
+
if (errors.length) {
|
|
304
|
+
const rendered = errors.map(([status, response]) => {
|
|
305
|
+
if (response.links) fail("Response links are unsupported", `${at}/responses/${status}/links`, id);
|
|
306
|
+
const value = pickContent(response.content, `${at}/responses/${status}`, "response");
|
|
307
|
+
const typeName = `${prefix}Error${pascal(status)}`;
|
|
308
|
+
operationTypes.push(`export type ${typeName} = ${value.type};`);
|
|
309
|
+
return `${JSON.stringify(status)}: ${value.codec}`;
|
|
310
|
+
});
|
|
311
|
+
chain.push(`errors({ ${rendered.join(", ")} })`);
|
|
312
|
+
}
|
|
313
|
+
if (operation.security ?? document.security) chain.push(`security(${JSON.stringify(operation.security ?? document.security)})`);
|
|
314
|
+
descriptors.push(` ${id}: ${chain.join("\n .")},`);
|
|
315
|
+
}
|
|
316
|
+
const imports = [...new Set(descriptors.join(" ").match(/\b(get|post|put|patch|del|head|options|json|text|urlEncoded|multipart|arrayBuffer|empty|content)\b/g) ?? [])].sort();
|
|
317
|
+
const files = {
|
|
318
|
+
"schemas.ts": `${schemaLines.join("\n\n")}\n`,
|
|
319
|
+
"operations.ts": `${names.size ? `import type { ${[...names.keys()].join(", ")} } from "./schemas";\n\n` : ""}${operationTypes.join("\n\n")}\n`,
|
|
320
|
+
"api.ts": `import { ${[
|
|
321
|
+
"defineApi",
|
|
322
|
+
"createClient",
|
|
323
|
+
...imports
|
|
324
|
+
].join(", ")} } from "@askrjs/fetch";\nimport type { ClientOptions } from "@askrjs/fetch";\nimport type { ${operationTypes.map((line) => line.match(/^export type (\w+)/)?.[1]).filter(Boolean).join(", ")} } from "./operations";\n\nexport const api = defineApi({\n${descriptors.join("\n")}\n}, ${JSON.stringify({
|
|
325
|
+
servers: (document.servers ?? []).map((s) => s.url),
|
|
326
|
+
securitySchemes: document.components?.securitySchemes ?? {}
|
|
327
|
+
}, null, 2)});\n\nexport const createApiClient = (options?: ClientOptions) => createClient(api, options);\n`,
|
|
328
|
+
"index.ts": `export * from "./schemas";\nexport * from "./operations";\nexport { api, createApiClient } from "./api";\n`,
|
|
329
|
+
".askr-generated.json": ""
|
|
330
|
+
};
|
|
331
|
+
files[".askr-generated.json"] = `${JSON.stringify({
|
|
332
|
+
version: 1,
|
|
333
|
+
files: [...OWNED]
|
|
334
|
+
}, null, 2)}\n`;
|
|
335
|
+
return files;
|
|
336
|
+
}
|
|
337
|
+
async function existingFiles(directory) {
|
|
338
|
+
try {
|
|
339
|
+
return (await readdir(directory)).sort();
|
|
340
|
+
} catch {
|
|
341
|
+
return [];
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
async function writeGenerated(directory, files, check) {
|
|
345
|
+
const output = resolve(directory);
|
|
346
|
+
const outputStat = await stat(output).catch(() => null);
|
|
347
|
+
if (outputStat && !outputStat.isDirectory()) throw new GenerationError(`Generated output must be a directory: ${output}`);
|
|
348
|
+
const entries = await existingFiles(output);
|
|
349
|
+
if (check) {
|
|
350
|
+
if (!entries.length) throw new GenerationError(`Generated directory is missing: ${output}`);
|
|
351
|
+
const expected = Object.keys(files).sort();
|
|
352
|
+
if (JSON.stringify(entries) !== JSON.stringify(expected)) throw new GenerationError(`Generated file ownership differs in ${output}`);
|
|
353
|
+
for (const name of expected) if (await readFile(join(output, name), "utf8") !== files[name]) throw new GenerationError(`Generated file is stale: ${name}`);
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
if (entries.length && !entries.includes(".askr-generated.json")) throw new GenerationError(`Refusing to overwrite non-generated directory: ${output}`);
|
|
357
|
+
const stage = await mkdtemp(join(dirname(output), `.${basename(output)}-stage-`));
|
|
358
|
+
for (const [name, content] of Object.entries(files)) await writeFile(join(stage, name), content);
|
|
359
|
+
const backup = `${output}.backup-${process.pid}`;
|
|
360
|
+
let moved = false;
|
|
361
|
+
try {
|
|
362
|
+
if (entries.length) {
|
|
363
|
+
await rename(output, backup);
|
|
364
|
+
moved = true;
|
|
365
|
+
}
|
|
366
|
+
await rename(stage, output);
|
|
367
|
+
if (moved) await rm(backup, {
|
|
368
|
+
recursive: true,
|
|
369
|
+
force: true
|
|
370
|
+
});
|
|
371
|
+
} catch (error) {
|
|
372
|
+
await rm(output, {
|
|
373
|
+
recursive: true,
|
|
374
|
+
force: true
|
|
375
|
+
});
|
|
376
|
+
if (moved) await rename(backup, output);
|
|
377
|
+
throw error;
|
|
378
|
+
} finally {
|
|
379
|
+
await rm(stage, {
|
|
380
|
+
recursive: true,
|
|
381
|
+
force: true
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
async function generate(input, output, check = false) {
|
|
386
|
+
if (!/^https?:\/\//.test(input)) {
|
|
387
|
+
const resolvedInput = resolve(input);
|
|
388
|
+
const inputWithinOutput = relative(resolve(output), resolvedInput);
|
|
389
|
+
if (!inputWithinOutput.startsWith("..") && !isAbsolute(inputWithinOutput)) throw new GenerationError("Generated output must not contain its OpenAPI input document");
|
|
390
|
+
}
|
|
391
|
+
const files = generateFiles(await loadOpenApi(input));
|
|
392
|
+
await mkdir(dirname(resolve(output)), { recursive: true });
|
|
393
|
+
await writeGenerated(output, files, check);
|
|
394
|
+
}
|
|
395
|
+
//#endregion
|
|
396
|
+
//#region src/bin/generate.ts
|
|
397
|
+
async function runGenerateCli(args, io = console) {
|
|
398
|
+
const wantsJson = args.includes("--json");
|
|
399
|
+
let output;
|
|
400
|
+
let check = false;
|
|
401
|
+
let json = false;
|
|
402
|
+
let help = false;
|
|
403
|
+
const inputs = [];
|
|
404
|
+
for (let i = 0; i < args.length; i++) {
|
|
405
|
+
const arg = args[i];
|
|
406
|
+
if (arg === "--check") check = true;
|
|
407
|
+
else if (arg === "--json") json = true;
|
|
408
|
+
else if (arg === "--help" || arg === "-h") help = true;
|
|
409
|
+
else if (arg === "-o" || arg === "--output") {
|
|
410
|
+
const value = args[i + 1];
|
|
411
|
+
if (!value || value.startsWith("-")) {
|
|
412
|
+
const message = `${arg} requires a path`;
|
|
413
|
+
io.error(wantsJson ? JSON.stringify({
|
|
414
|
+
status: "error",
|
|
415
|
+
error: message
|
|
416
|
+
}) : message);
|
|
417
|
+
return 1;
|
|
418
|
+
}
|
|
419
|
+
output = value;
|
|
420
|
+
i += 1;
|
|
421
|
+
} else if (arg.startsWith("--output=")) output = arg.slice(9);
|
|
422
|
+
else if (arg.startsWith("-")) {
|
|
423
|
+
const message = `Unknown option: ${arg}`;
|
|
424
|
+
io.error(wantsJson ? JSON.stringify({
|
|
425
|
+
status: "error",
|
|
426
|
+
error: message
|
|
427
|
+
}) : message);
|
|
428
|
+
return 1;
|
|
429
|
+
} else inputs.push(arg);
|
|
430
|
+
}
|
|
431
|
+
const usage = "Usage: askr generate <input> -o <output-directory> [--check] [--json]";
|
|
432
|
+
if (help) {
|
|
433
|
+
io.log(usage);
|
|
434
|
+
return 0;
|
|
435
|
+
}
|
|
436
|
+
if (inputs.length !== 1 || !output) {
|
|
437
|
+
io.error(json ? JSON.stringify({
|
|
438
|
+
status: "error",
|
|
439
|
+
error: usage
|
|
440
|
+
}) : usage);
|
|
441
|
+
return 1;
|
|
442
|
+
}
|
|
443
|
+
try {
|
|
444
|
+
await generate(inputs[0], output, check);
|
|
445
|
+
io.log(json ? JSON.stringify({
|
|
446
|
+
status: "ok",
|
|
447
|
+
action: check ? "checked" : "generated",
|
|
448
|
+
output
|
|
449
|
+
}) : check ? "Generated client is current." : `Generated client in ${output}.`);
|
|
450
|
+
return 0;
|
|
451
|
+
} catch (error) {
|
|
452
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
453
|
+
io.error(json ? JSON.stringify({
|
|
454
|
+
status: "error",
|
|
455
|
+
error: message
|
|
456
|
+
}) : message);
|
|
457
|
+
return 1;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
//#endregion
|
|
461
|
+
export { runGenerateCli };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/bin/openapi.d.ts
|
|
2
|
+
type CliIo = Pick<Console, "error" | "log">;
|
|
3
|
+
interface ParsedArgs {
|
|
4
|
+
entry: string;
|
|
5
|
+
output: string;
|
|
6
|
+
check: boolean;
|
|
7
|
+
help: boolean;
|
|
8
|
+
json: boolean;
|
|
9
|
+
}
|
|
10
|
+
interface OpenApiDeps {
|
|
11
|
+
cwd(): string;
|
|
12
|
+
importModule(filePath: string): Promise<unknown>;
|
|
13
|
+
readFile(filePath: string): Promise<string>;
|
|
14
|
+
mkdir(directory: string): Promise<void>;
|
|
15
|
+
writeFile(filePath: string, contents: string): Promise<void>;
|
|
16
|
+
rename(from: string, to: string): Promise<void>;
|
|
17
|
+
rm(filePath: string): Promise<void>;
|
|
18
|
+
temporarySuffix(): string;
|
|
19
|
+
}
|
|
20
|
+
declare function parseOpenApiArgs(args: readonly string[]): ParsedArgs;
|
|
21
|
+
declare function serializeOpenApi(document: unknown): string;
|
|
22
|
+
declare function validateOpenApiDocument(document: unknown): asserts document is Record<string, unknown>;
|
|
23
|
+
declare function runOpenApiCli(args?: string[], io?: CliIo, overrides?: Partial<OpenApiDeps>): Promise<number>;
|
|
24
|
+
//#endregion
|
|
25
|
+
export { parseOpenApiArgs, runOpenApiCli, serializeOpenApi, validateOpenApiDocument };
|
package/dist/openapi.js
CHANGED
|
@@ -3,8 +3,8 @@ import { t as isDirectExecution } from "./is-direct-execution-Cdlr-ZUl.js";
|
|
|
3
3
|
import fs from "node:fs/promises";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { pathToFileURL } from "node:url";
|
|
6
|
+
import { dump } from "js-yaml";
|
|
6
7
|
import { tsImport } from "tsx/esm/api";
|
|
7
|
-
import { stringify } from "yaml";
|
|
8
8
|
//#region src/bin/openapi.ts
|
|
9
9
|
const helpText = `askr openapi - Generate an OpenAPI YAML artifact
|
|
10
10
|
|
|
@@ -15,6 +15,7 @@ Options:
|
|
|
15
15
|
--entry <path> TypeScript API module (default: ./src/api.ts)
|
|
16
16
|
--output <path> YAML artifact (default: ./openapi.yml)
|
|
17
17
|
--check Fail when the artifact is missing or byte-stale; never write
|
|
18
|
+
--json Print machine-readable command results
|
|
18
19
|
--help, -h Show help
|
|
19
20
|
`;
|
|
20
21
|
const defaultDeps = {
|
|
@@ -32,17 +33,19 @@ function parseOpenApiArgs(args) {
|
|
|
32
33
|
entry: "./src/api.ts",
|
|
33
34
|
output: "./openapi.yml",
|
|
34
35
|
check: false,
|
|
35
|
-
help: false
|
|
36
|
+
help: false,
|
|
37
|
+
json: false
|
|
36
38
|
};
|
|
37
39
|
for (let index = 0; index < args.length; index += 1) {
|
|
38
40
|
const value = args[index];
|
|
39
41
|
if (value === "--entry" || value === "--output") {
|
|
40
42
|
const next = args[index + 1];
|
|
41
|
-
if (!next) throw new Error(`${value} requires a path`);
|
|
43
|
+
if (!next || next.startsWith("-")) throw new Error(`${value} requires a path`);
|
|
42
44
|
if (value === "--entry") parsed.entry = next;
|
|
43
45
|
else parsed.output = next;
|
|
44
46
|
index += 1;
|
|
45
47
|
} else if (value === "--check") parsed.check = true;
|
|
48
|
+
else if (value === "--json") parsed.json = true;
|
|
46
49
|
else if (value === "--help" || value === "-h") parsed.help = true;
|
|
47
50
|
else throw new Error(`Unknown option: ${value}`);
|
|
48
51
|
}
|
|
@@ -56,14 +59,24 @@ function exporterFrom(moduleValue) {
|
|
|
56
59
|
return exported;
|
|
57
60
|
}
|
|
58
61
|
function serializeOpenApi(document) {
|
|
59
|
-
return `${
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
return `${dump(document, {
|
|
63
|
+
lineWidth: -1,
|
|
64
|
+
noCompatMode: true,
|
|
65
|
+
noRefs: true
|
|
62
66
|
}).replace(/\n+$/, "")}\n`;
|
|
63
67
|
}
|
|
64
|
-
|
|
65
|
-
const document = exporterFrom(await deps.importModule(entry)).toOpenApiDocument();
|
|
68
|
+
function validateOpenApiDocument(document) {
|
|
66
69
|
if (!document || typeof document !== "object" || Array.isArray(document)) throw new Error("toOpenApiDocument() must return an OpenAPI document object");
|
|
70
|
+
const candidate = document;
|
|
71
|
+
if (typeof candidate.openapi !== "string" || !/^3\.(?:0|1)\.\d+(?:[-+].+)?$/.test(candidate.openapi)) throw new Error("OpenAPI document must declare a supported openapi 3.0.x or 3.1.x version");
|
|
72
|
+
if (!candidate.info || typeof candidate.info !== "object" || Array.isArray(candidate.info)) throw new Error("OpenAPI document must contain an info object");
|
|
73
|
+
const info = candidate.info;
|
|
74
|
+
if (typeof info.title !== "string" || !info.title.trim() || typeof info.version !== "string" || !info.version.trim()) throw new Error("OpenAPI info must contain non-empty title and version strings");
|
|
75
|
+
if (!candidate.paths || typeof candidate.paths !== "object" || Array.isArray(candidate.paths)) throw new Error("OpenAPI document must contain a paths object");
|
|
76
|
+
}
|
|
77
|
+
async function generate(entry, deps) {
|
|
78
|
+
const document = await exporterFrom(await deps.importModule(entry)).toOpenApiDocument();
|
|
79
|
+
validateOpenApiDocument(document);
|
|
67
80
|
return serializeOpenApi(document);
|
|
68
81
|
}
|
|
69
82
|
async function atomicWrite(output, contents, deps) {
|
|
@@ -82,6 +95,7 @@ async function runOpenApiCli(args = process.argv.slice(2), io = console, overrid
|
|
|
82
95
|
...defaultDeps,
|
|
83
96
|
...overrides
|
|
84
97
|
};
|
|
98
|
+
const wantsJson = args.includes("--json");
|
|
85
99
|
try {
|
|
86
100
|
const parsed = parseOpenApiArgs(args);
|
|
87
101
|
if (parsed.help) {
|
|
@@ -90,6 +104,7 @@ async function runOpenApiCli(args = process.argv.slice(2), io = console, overrid
|
|
|
90
104
|
}
|
|
91
105
|
const entry = path.resolve(deps.cwd(), parsed.entry);
|
|
92
106
|
const output = path.resolve(deps.cwd(), parsed.output);
|
|
107
|
+
if (entry === output) throw new Error("OpenAPI output must not overwrite its source entry");
|
|
93
108
|
const expected = await generate(entry, deps);
|
|
94
109
|
if (parsed.check) {
|
|
95
110
|
let actual;
|
|
@@ -97,23 +112,43 @@ async function runOpenApiCli(args = process.argv.slice(2), io = console, overrid
|
|
|
97
112
|
actual = await deps.readFile(output);
|
|
98
113
|
} catch (error) {
|
|
99
114
|
if (error.code === "ENOENT") {
|
|
100
|
-
io.error(
|
|
115
|
+
io.error(parsed.json ? JSON.stringify({
|
|
116
|
+
status: "error",
|
|
117
|
+
reason: "missing",
|
|
118
|
+
output
|
|
119
|
+
}) : `OpenAPI artifact is missing: ${output}`);
|
|
101
120
|
return 1;
|
|
102
121
|
}
|
|
103
122
|
throw error;
|
|
104
123
|
}
|
|
105
124
|
if (actual !== expected) {
|
|
106
|
-
io.error(
|
|
125
|
+
io.error(parsed.json ? JSON.stringify({
|
|
126
|
+
status: "error",
|
|
127
|
+
reason: "stale",
|
|
128
|
+
output
|
|
129
|
+
}) : `OpenAPI artifact is stale: ${output}`);
|
|
107
130
|
return 1;
|
|
108
131
|
}
|
|
109
|
-
io.log(
|
|
132
|
+
io.log(parsed.json ? JSON.stringify({
|
|
133
|
+
status: "ok",
|
|
134
|
+
action: "checked",
|
|
135
|
+
output
|
|
136
|
+
}) : `OpenAPI artifact is current: ${output}`);
|
|
110
137
|
return 0;
|
|
111
138
|
}
|
|
112
139
|
await atomicWrite(output, expected, deps);
|
|
113
|
-
io.log(
|
|
140
|
+
io.log(parsed.json ? JSON.stringify({
|
|
141
|
+
status: "ok",
|
|
142
|
+
action: "generated",
|
|
143
|
+
output
|
|
144
|
+
}) : `Generated OpenAPI artifact: ${output}`);
|
|
114
145
|
return 0;
|
|
115
146
|
} catch (error) {
|
|
116
|
-
|
|
147
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
148
|
+
io.error(wantsJson ? JSON.stringify({
|
|
149
|
+
status: "error",
|
|
150
|
+
error: message
|
|
151
|
+
}) : message);
|
|
117
152
|
return 1;
|
|
118
153
|
}
|
|
119
154
|
}
|
|
@@ -122,6 +157,4 @@ async function main() {
|
|
|
122
157
|
}
|
|
123
158
|
if (isDirectExecution(import.meta.url)) main();
|
|
124
159
|
//#endregion
|
|
125
|
-
export { parseOpenApiArgs, runOpenApiCli, serializeOpenApi };
|
|
126
|
-
|
|
127
|
-
//# sourceMappingURL=openapi.js.map
|
|
160
|
+
export { parseOpenApiArgs, runOpenApiCli, serializeOpenApi, validateOpenApiDocument };
|