@autoview/cli 0.1.0
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/LICENSE +661 -0
- package/README.md +407 -0
- package/lib/AutoViewAgent.d.ts +109 -0
- package/lib/AutoViewAgent.js +123 -0
- package/lib/AutoViewAgent.js.map +1 -0
- package/lib/agent/emitMcpServer.d.ts +15 -0
- package/lib/agent/emitMcpServer.js +157 -0
- package/lib/agent/emitMcpServer.js.map +1 -0
- package/lib/agent/emitReport.d.ts +14 -0
- package/lib/agent/emitReport.js +85 -0
- package/lib/agent/emitReport.js.map +1 -0
- package/lib/agent/toolSurface.d.ts +130 -0
- package/lib/agent/toolSurface.js +342 -0
- package/lib/agent/toolSurface.js.map +1 -0
- package/lib/agent/verifyAgentTasks.d.ts +87 -0
- package/lib/agent/verifyAgentTasks.js +126 -0
- package/lib/agent/verifyAgentTasks.js.map +1 -0
- package/lib/cli/main.d.ts +2 -0
- package/lib/cli/main.js +295 -0
- package/lib/cli/main.js.map +1 -0
- package/lib/compiler/AutoViewInterfaceCompiler.d.ts +27 -0
- package/lib/compiler/AutoViewInterfaceCompiler.js +68 -0
- package/lib/compiler/AutoViewInterfaceCompiler.js.map +1 -0
- package/lib/constants/AutoViewFrontendTemplate.d.ts +1 -0
- package/lib/constants/AutoViewFrontendTemplate.js +46 -0
- package/lib/constants/AutoViewFrontendTemplate.js.map +1 -0
- package/lib/constants/AutoViewSystemPromptConstant.d.ts +5 -0
- package/lib/constants/AutoViewSystemPromptConstant.js +4 -0
- package/lib/constants/AutoViewSystemPromptConstant.js.map +1 -0
- package/lib/context/IAutoViewAgentContext.d.ts +60 -0
- package/lib/context/IAutoViewAgentContext.js +3 -0
- package/lib/context/IAutoViewAgentContext.js.map +1 -0
- package/lib/fromSwagger.d.ts +53 -0
- package/lib/fromSwagger.js +513 -0
- package/lib/fromSwagger.js.map +1 -0
- package/lib/generateDeterministic.d.ts +26 -0
- package/lib/generateDeterministic.js +75 -0
- package/lib/generateDeterministic.js.map +1 -0
- package/lib/index.d.ts +15 -0
- package/lib/index.js +41 -0
- package/lib/index.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoView.d.ts +17 -0
- package/lib/orchestrate/orchestrateAutoView.js +491 -0
- package/lib/orchestrate/orchestrateAutoView.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewProductPlan.d.ts +37 -0
- package/lib/orchestrate/orchestrateAutoViewProductPlan.js +109 -0
- package/lib/orchestrate/orchestrateAutoViewProductPlan.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewRender.d.ts +133 -0
- package/lib/orchestrate/orchestrateAutoViewRender.js +943 -0
- package/lib/orchestrate/orchestrateAutoViewRender.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.d.ts +24 -0
- package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.js +92 -0
- package/lib/orchestrate/orchestrateAutoViewRenderDeterministic.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewReview.d.ts +48 -0
- package/lib/orchestrate/orchestrateAutoViewReview.js +328 -0
- package/lib/orchestrate/orchestrateAutoViewReview.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewScaffold.d.ts +45 -0
- package/lib/orchestrate/orchestrateAutoViewScaffold.js +586 -0
- package/lib/orchestrate/orchestrateAutoViewScaffold.js.map +1 -0
- package/lib/orchestrate/orchestrateAutoViewSdkStudy.d.ts +26 -0
- package/lib/orchestrate/orchestrateAutoViewSdkStudy.js +85 -0
- package/lib/orchestrate/orchestrateAutoViewSdkStudy.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewProductPlan.d.ts +96 -0
- package/lib/orchestrate/structures/IAutoViewProductPlan.js +3 -0
- package/lib/orchestrate/structures/IAutoViewProductPlan.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewProductPlanApplication.d.ts +38 -0
- package/lib/orchestrate/structures/IAutoViewProductPlanApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewProductPlanApplication.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewRenderApplication.d.ts +38 -0
- package/lib/orchestrate/structures/IAutoViewRenderApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewRenderApplication.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewReviewApplication.d.ts +40 -0
- package/lib/orchestrate/structures/IAutoViewReviewApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewReviewApplication.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewSdkMap.d.ts +63 -0
- package/lib/orchestrate/structures/IAutoViewSdkMap.js +3 -0
- package/lib/orchestrate/structures/IAutoViewSdkMap.js.map +1 -0
- package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.d.ts +37 -0
- package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.js +3 -0
- package/lib/orchestrate/structures/IAutoViewSdkStudyApplication.js.map +1 -0
- package/lib/orchestrate/utils/HistoryMessage.d.ts +10 -0
- package/lib/orchestrate/utils/HistoryMessage.js +25 -0
- package/lib/orchestrate/utils/HistoryMessage.js.map +1 -0
- package/lib/orchestrate/utils/auditFrontendRuntime.d.ts +53 -0
- package/lib/orchestrate/utils/auditFrontendRuntime.js +362 -0
- package/lib/orchestrate/utils/auditFrontendRuntime.js.map +1 -0
- package/lib/orchestrate/utils/buildDeterministicPlan.d.ts +4 -0
- package/lib/orchestrate/utils/buildDeterministicPlan.js +233 -0
- package/lib/orchestrate/utils/buildDeterministicPlan.js.map +1 -0
- package/lib/orchestrate/utils/buildDeterministicSdkMap.d.ts +22 -0
- package/lib/orchestrate/utils/buildDeterministicSdkMap.js +154 -0
- package/lib/orchestrate/utils/buildDeterministicSdkMap.js.map +1 -0
- package/lib/orchestrate/utils/cacheNodeModules.d.ts +31 -0
- package/lib/orchestrate/utils/cacheNodeModules.js +134 -0
- package/lib/orchestrate/utils/cacheNodeModules.js.map +1 -0
- package/lib/orchestrate/utils/describeEndpointPropsShape.d.ts +37 -0
- package/lib/orchestrate/utils/describeEndpointPropsShape.js +192 -0
- package/lib/orchestrate/utils/describeEndpointPropsShape.js.map +1 -0
- package/lib/orchestrate/utils/describeEndpointRequestBodyShape.d.ts +22 -0
- package/lib/orchestrate/utils/describeEndpointRequestBodyShape.js +29 -0
- package/lib/orchestrate/utils/describeEndpointRequestBodyShape.js.map +1 -0
- package/lib/orchestrate/utils/describeEndpointResponseShape.d.ts +19 -0
- package/lib/orchestrate/utils/describeEndpointResponseShape.js +30 -0
- package/lib/orchestrate/utils/describeEndpointResponseShape.js.map +1 -0
- package/lib/orchestrate/utils/executeCachedBatch.d.ts +22 -0
- package/lib/orchestrate/utils/executeCachedBatch.js +64 -0
- package/lib/orchestrate/utils/executeCachedBatch.js.map +1 -0
- package/lib/orchestrate/utils/loadShoppingFixture.d.ts +33 -0
- package/lib/orchestrate/utils/loadShoppingFixture.js +17 -0
- package/lib/orchestrate/utils/loadShoppingFixture.js.map +1 -0
- package/lib/orchestrate/utils/normalizeProductPlanPaths.d.ts +24 -0
- package/lib/orchestrate/utils/normalizeProductPlanPaths.js +77 -0
- package/lib/orchestrate/utils/normalizeProductPlanPaths.js.map +1 -0
- package/lib/orchestrate/utils/renderJsonSchema.d.ts +23 -0
- package/lib/orchestrate/utils/renderJsonSchema.js +122 -0
- package/lib/orchestrate/utils/renderJsonSchema.js.map +1 -0
- package/lib/orchestrate/utils/renderResourcePage.d.ts +36 -0
- package/lib/orchestrate/utils/renderResourcePage.js +1415 -0
- package/lib/orchestrate/utils/renderResourcePage.js.map +1 -0
- package/lib/orchestrate/utils/validateFrontendTypecheck.d.ts +109 -0
- package/lib/orchestrate/utils/validateFrontendTypecheck.js +274 -0
- package/lib/orchestrate/utils/validateFrontendTypecheck.js.map +1 -0
- package/lib/preview/renderPreview.d.ts +22 -0
- package/lib/preview/renderPreview.js +198 -0
- package/lib/preview/renderPreview.js.map +1 -0
- package/lib/typings/compiler.d.ts +39 -0
- package/lib/typings/compiler.js +3 -0
- package/lib/typings/compiler.js.map +1 -0
- package/lib/typings/events.d.ts +106 -0
- package/lib/typings/events.js +3 -0
- package/lib/typings/events.js.map +1 -0
- package/lib/typings/index.d.ts +10 -0
- package/lib/typings/index.js +27 -0
- package/lib/typings/index.js.map +1 -0
- package/lib/typings/misc.d.ts +78 -0
- package/lib/typings/misc.js +3 -0
- package/lib/typings/misc.js.map +1 -0
- package/lib/utils/ArrayUtil.d.ts +8 -0
- package/lib/utils/ArrayUtil.js +30 -0
- package/lib/utils/ArrayUtil.js.map +1 -0
- package/lib/utils/StringUtil.d.ts +11 -0
- package/lib/utils/StringUtil.js +28 -0
- package/lib/utils/StringUtil.js.map +1 -0
- package/lib/utils/classifyEndpoints.d.ts +62 -0
- package/lib/utils/classifyEndpoints.js +216 -0
- package/lib/utils/classifyEndpoints.js.map +1 -0
- package/lib/utils/endpointFilter.d.ts +26 -0
- package/lib/utils/endpointFilter.js +0 -0
- package/lib/utils/endpointFilter.js.map +1 -0
- package/lib/utils/extractFields.d.ts +85 -0
- package/lib/utils/extractFields.js +231 -0
- package/lib/utils/extractFields.js.map +1 -0
- package/lib/utils/index.d.ts +13 -0
- package/lib/utils/index.js +30 -0
- package/lib/utils/index.js.map +1 -0
- package/lib/utils/normalizeForNestia.d.ts +34 -0
- package/lib/utils/normalizeForNestia.js +133 -0
- package/lib/utils/normalizeForNestia.js.map +1 -0
- package/lib/utils/resourcePlan.d.ts +39 -0
- package/lib/utils/resourcePlan.js +95 -0
- package/lib/utils/resourcePlan.js.map +1 -0
- package/lib/utils/sliceDocument.d.ts +17 -0
- package/lib/utils/sliceDocument.js +114 -0
- package/lib/utils/sliceDocument.js.map +1 -0
- package/lib/utils/toEndpoints.d.ts +90 -0
- package/lib/utils/toEndpoints.js +227 -0
- package/lib/utils/toEndpoints.js.map +1 -0
- package/lib/verify/runWorkflows.d.ts +25 -0
- package/lib/verify/runWorkflows.js +366 -0
- package/lib/verify/runWorkflows.js.map +1 -0
- package/lib/verify/workflows.d.ts +53 -0
- package/lib/verify/workflows.js +107 -0
- package/lib/verify/workflows.js.map +1 -0
- package/package.json +82 -0
- package/prompts/AUTOVIEW_RENDER.md +398 -0
- package/prompts/AUTOVIEW_REVIEW.md +60 -0
- package/prompts/AUTOVIEW_SDK_STUDY.md +89 -0
- package/src/AutoViewAgent.ts +222 -0
- package/src/agent/emitMcpServer.integration.test.ts +168 -0
- package/src/agent/emitMcpServer.test.ts +51 -0
- package/src/agent/emitMcpServer.ts +178 -0
- package/src/agent/emitReport.ts +117 -0
- package/src/agent/toolSurface.test.ts +243 -0
- package/src/agent/toolSurface.ts +501 -0
- package/src/agent/verifyAgentTasks.test.ts +106 -0
- package/src/agent/verifyAgentTasks.ts +171 -0
- package/src/cli/main.ts +363 -0
- package/src/compiler/AutoViewInterfaceCompiler.ts +69 -0
- package/src/constants/AutoViewFrontendTemplate.ts +42 -0
- package/src/constants/AutoViewSystemPromptConstant.ts +6 -0
- package/src/context/IAutoViewAgentContext.ts +84 -0
- package/src/fromSwagger.test.ts +269 -0
- package/src/fromSwagger.ts +500 -0
- package/src/generateDeterministic.test.ts +39 -0
- package/src/generateDeterministic.ts +77 -0
- package/src/index.ts +30 -0
- package/src/orchestrate/orchestrateAutoView.ts +590 -0
- package/src/orchestrate/orchestrateAutoViewProductPlan.ts +121 -0
- package/src/orchestrate/orchestrateAutoViewRender.ts +1117 -0
- package/src/orchestrate/orchestrateAutoViewRenderDeterministic.ts +101 -0
- package/src/orchestrate/orchestrateAutoViewReview.ts +272 -0
- package/src/orchestrate/orchestrateAutoViewScaffold.ts +627 -0
- package/src/orchestrate/orchestrateAutoViewSdkStudy.ts +90 -0
- package/src/orchestrate/renderNavTs.test.ts +74 -0
- package/src/orchestrate/structures/IAutoViewProductPlan.ts +119 -0
- package/src/orchestrate/structures/IAutoViewProductPlanApplication.ts +41 -0
- package/src/orchestrate/structures/IAutoViewRenderApplication.ts +40 -0
- package/src/orchestrate/structures/IAutoViewReviewApplication.ts +42 -0
- package/src/orchestrate/structures/IAutoViewSdkMap.ts +72 -0
- package/src/orchestrate/structures/IAutoViewSdkStudyApplication.ts +40 -0
- package/src/orchestrate/utils/HistoryMessage.ts +41 -0
- package/src/orchestrate/utils/auditFrontendRuntime.test.ts +18 -0
- package/src/orchestrate/utils/auditFrontendRuntime.ts +454 -0
- package/src/orchestrate/utils/buildDeterministicPlan.test.ts +170 -0
- package/src/orchestrate/utils/buildDeterministicPlan.ts +289 -0
- package/src/orchestrate/utils/buildDeterministicSdkMap.test.ts +90 -0
- package/src/orchestrate/utils/buildDeterministicSdkMap.ts +169 -0
- package/src/orchestrate/utils/cacheNodeModules.ts +136 -0
- package/src/orchestrate/utils/describeEndpointPropsShape.test.ts +86 -0
- package/src/orchestrate/utils/describeEndpointPropsShape.ts +202 -0
- package/src/orchestrate/utils/describeEndpointRequestBodyShape.test.ts +87 -0
- package/src/orchestrate/utils/describeEndpointRequestBodyShape.ts +31 -0
- package/src/orchestrate/utils/describeEndpointResponseShape.test.ts +70 -0
- package/src/orchestrate/utils/describeEndpointResponseShape.ts +32 -0
- package/src/orchestrate/utils/executeCachedBatch.ts +59 -0
- package/src/orchestrate/utils/loadShoppingFixture.ts +52 -0
- package/src/orchestrate/utils/normalizeProductPlanPaths.ts +92 -0
- package/src/orchestrate/utils/renderJsonSchema.test.ts +162 -0
- package/src/orchestrate/utils/renderJsonSchema.ts +133 -0
- package/src/orchestrate/utils/renderResourcePage.test.ts +468 -0
- package/src/orchestrate/utils/renderResourcePage.ts +1624 -0
- package/src/orchestrate/utils/validateFrontendTypecheck.test.ts +32 -0
- package/src/orchestrate/utils/validateFrontendTypecheck.ts +335 -0
- package/src/preview/renderPreview.ts +273 -0
- package/src/typings/compiler.ts +47 -0
- package/src/typings/events.ts +155 -0
- package/src/typings/index.ts +10 -0
- package/src/typings/misc.ts +93 -0
- package/src/utils/ArrayUtil.ts +16 -0
- package/src/utils/StringUtil.ts +29 -0
- package/src/utils/classifyEndpoints.test.ts +86 -0
- package/src/utils/classifyEndpoints.ts +291 -0
- package/src/utils/endpointFilter.test.ts +50 -0
- package/src/utils/endpointFilter.ts +0 -0
- package/src/utils/extractFields.test.ts +82 -0
- package/src/utils/extractFields.ts +306 -0
- package/src/utils/index.ts +13 -0
- package/src/utils/normalizeForNestia.test.ts +93 -0
- package/src/utils/normalizeForNestia.ts +139 -0
- package/src/utils/resourcePlan.test.ts +104 -0
- package/src/utils/resourcePlan.ts +180 -0
- package/src/utils/sliceDocument.test.ts +85 -0
- package/src/utils/sliceDocument.ts +119 -0
- package/src/utils/toEndpoints.test.ts +251 -0
- package/src/utils/toEndpoints.ts +343 -0
- package/src/verify/runWorkflows.ts +403 -0
- package/src/verify/workflows.test.ts +117 -0
- package/src/verify/workflows.ts +154 -0
- package/template/CLAUDE.md +140 -0
- package/template/Dockerfile +31 -0
- package/template/PROMPT.md +80 -0
- package/template/SANDBOX.md +70 -0
- package/template/app/api/health/route.ts +10 -0
- package/template/app/globals.css +97 -0
- package/template/app/layout.tsx +30 -0
- package/template/app/page.tsx +19 -0
- package/template/components/AppShell.tsx +114 -0
- package/template/components/auto/CatalogGrid.tsx +159 -0
- package/template/components/auto/ConfirmButton.tsx +67 -0
- package/template/components/auto/EmbeddedCollection.tsx +144 -0
- package/template/components/auto/ResourceDashboard.tsx +104 -0
- package/template/components/auto/ResourceDetail.tsx +93 -0
- package/template/components/auto/ResourceForm.tsx +235 -0
- package/template/components/auto/ResourceIcon.tsx +88 -0
- package/template/components/auto/ResourceLanding.tsx +155 -0
- package/template/components/auto/ResourceTable.tsx +223 -0
- package/template/components/auto/formatValue.tsx +186 -0
- package/template/components/auto/types.ts +42 -0
- package/template/components/ui/badge.tsx +40 -0
- package/template/components/ui/button.tsx +57 -0
- package/template/components/ui/card.tsx +86 -0
- package/template/components/ui/dialog.tsx +119 -0
- package/template/components/ui/input.tsx +23 -0
- package/template/components/ui/label.tsx +24 -0
- package/template/components/ui/pagination.tsx +117 -0
- package/template/components/ui/select.tsx +92 -0
- package/template/components/ui/sheet.tsx +135 -0
- package/template/components/ui/skeleton.tsx +15 -0
- package/template/components/ui/table.tsx +120 -0
- package/template/components/ui/tabs.tsx +55 -0
- package/template/lib/utils.ts +35 -0
- package/template/next.config.mjs +52 -0
- package/template/package.json +46 -0
- package/template/postcss.config.js +6 -0
- package/template/scripts/start-shopping-backend.sh +56 -0
- package/template/tailwind.config.ts +96 -0
- package/template/tsconfig.json +29 -0
|
@@ -0,0 +1,500 @@
|
|
|
1
|
+
import { OpenApi } from "@typia/interface";
|
|
2
|
+
import { OpenApiConverter } from "@typia/utils";
|
|
3
|
+
import fs from "fs/promises";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Normalize a raw OpenAPI / Swagger payload into a standard
|
|
7
|
+
* {@link OpenApi.IDocument} — the swagger-native shape the AutoView
|
|
8
|
+
* orchestrators now consume directly (via {@link toEndpoints}).
|
|
9
|
+
*
|
|
10
|
+
* Accepts both an in-memory JSON object and a file path; the latter is
|
|
11
|
+
* convenient for the CLI binary. `OpenApiConverter.upgradeDocument` upgrades
|
|
12
|
+
* Swagger 2.0 / OpenAPI 3.0 / 3.1 to a single normalized 3.1 document, so
|
|
13
|
+
* every endpoint — query-string endpoints included — is preserved. (The old
|
|
14
|
+
* `invertOpenApiDocument` route through the AutoBE AST silently dropped
|
|
15
|
+
* query-string endpoints.)
|
|
16
|
+
*/
|
|
17
|
+
export function fromSwagger(
|
|
18
|
+
swagger: Parameters<typeof OpenApiConverter.upgradeDocument>[0],
|
|
19
|
+
): OpenApi.IDocument {
|
|
20
|
+
resolveParameterRefs(swagger);
|
|
21
|
+
resolvePathRefs(swagger);
|
|
22
|
+
stripPathRefAllOf(swagger, 0);
|
|
23
|
+
flattenAllOf(swagger);
|
|
24
|
+
promoteInlineBodies(swagger);
|
|
25
|
+
dropUnroutablePaths(swagger);
|
|
26
|
+
return OpenApiConverter.upgradeDocument(swagger);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Resolve `$ref`s that are a JSON Pointer INTO another operation's inline
|
|
31
|
+
* schema (`#/paths/~1v2~1bandwidth/get/responses/...`) by replacing the `$ref`
|
|
32
|
+
* node with a deep clone of the pointer's target. DigitalOcean DRYs whole
|
|
33
|
+
* response schemas this way — at the schema root (`/v2/monitoring/metrics/*`
|
|
34
|
+
* all alias one bandwidth schema) and inside properties
|
|
35
|
+
* (`POST /v2/apps` → `{ app: $ref → GET /v2/apps …items }`). Standard tooling
|
|
36
|
+
* only chases `#/components/...` refs; nestia emits `Response = any` for these,
|
|
37
|
+
* which turns every simulate-mode screen of the resource into a silent empty
|
|
38
|
+
* state.
|
|
39
|
+
*
|
|
40
|
+
* Cycle-safe: a pointer actively being expanded is left verbatim (the later
|
|
41
|
+
* allOf-stripping pass treats the leftover as noise). Depth-guarded against
|
|
42
|
+
* pathological nesting. Mutates `raw` in place.
|
|
43
|
+
*/
|
|
44
|
+
function resolvePathRefs(raw: unknown): void {
|
|
45
|
+
/**
|
|
46
|
+
* Follow an escaped JSON Pointer from the root. Being a URI fragment, parts
|
|
47
|
+
* are percent-decoded first (`%7B` → `{` — DO encodes `{app_id}` segments),
|
|
48
|
+
* then pointer-unescaped (`~1` → `/`, `~0` → `~`), per RFC 6901.
|
|
49
|
+
*/
|
|
50
|
+
const lookup = (pointer: string): unknown => {
|
|
51
|
+
let node: unknown = raw;
|
|
52
|
+
for (const part of pointer.slice(2).split("/")) {
|
|
53
|
+
let key = part;
|
|
54
|
+
try {
|
|
55
|
+
key = decodeURIComponent(key);
|
|
56
|
+
} catch {
|
|
57
|
+
// not percent-encoded — use the raw part
|
|
58
|
+
}
|
|
59
|
+
key = key.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
60
|
+
if (typeof node !== "object" || node === null) return undefined;
|
|
61
|
+
node = (node as Record<string, unknown>)[key];
|
|
62
|
+
}
|
|
63
|
+
return node;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const active = new Set<string>();
|
|
67
|
+
const walk = (node: unknown, depth: number): void => {
|
|
68
|
+
if (depth > 64 || typeof node !== "object" || node === null) return;
|
|
69
|
+
if (Array.isArray(node)) {
|
|
70
|
+
for (const v of node) walk(v, depth + 1);
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
const obj = node as Record<string, unknown>;
|
|
74
|
+
const ref = obj.$ref;
|
|
75
|
+
if (typeof ref === "string" && ref.startsWith("#/paths/")) {
|
|
76
|
+
if (!active.has(ref)) {
|
|
77
|
+
const target = lookup(ref);
|
|
78
|
+
if (typeof target === "object" && target !== null) {
|
|
79
|
+
active.add(ref);
|
|
80
|
+
// The target may itself hold path-pointers — resolve before cloning.
|
|
81
|
+
walk(target, depth + 1);
|
|
82
|
+
active.delete(ref);
|
|
83
|
+
delete obj.$ref;
|
|
84
|
+
Object.assign(obj, structuredClone(target));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return; // a (possibly unresolved) ref node has nothing else to walk
|
|
88
|
+
}
|
|
89
|
+
for (const v of Object.values(obj)) walk(v, depth + 1);
|
|
90
|
+
};
|
|
91
|
+
walk(raw, 0);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Promote every INLINE OBJECT request/response body into a uniquely named
|
|
96
|
+
* component schema (replacing it with a `$ref`) BEFORE nestia sees the document.
|
|
97
|
+
*
|
|
98
|
+
* nestia promotes inline bodies itself, but derives the component name from the
|
|
99
|
+
* path with `{param}` segments DROPPED — so a parent list and its child detail
|
|
100
|
+
* (`/v2/droplets` vs `/v2/droplets/{droplet_id}`) both claim
|
|
101
|
+
* `IApiV2Droplets.GetResponse`. The loser's SDK function is emitted as
|
|
102
|
+
* `Response = any`; `typia.random<any>()` then yields nothing, and the
|
|
103
|
+
* simulate-mode screen for that resource renders empty forever — silently. On
|
|
104
|
+
* DigitalOcean (every body inline) this gutted 38 of 148 SDK files.
|
|
105
|
+
*
|
|
106
|
+
* Naming here keeps the param segments (`ApiV2DropletsDropletIdGetResponse`), so
|
|
107
|
+
* names cannot collide across sibling paths; an unexpected clash falls back to a
|
|
108
|
+
* numeric suffix. Only plain OBJECT schemas are hoisted: an inline ARRAY must
|
|
109
|
+
* stay inline so the READ layer still sees `Array<$ref>` and derives the list's
|
|
110
|
+
* element columns ({@link toEndpoints}'s `resolveBody`), and primitives gain
|
|
111
|
+
* nothing. Swagger 2.0 inputs are skipped — they have no `components` section
|
|
112
|
+
* for the refs to land in (the upgrade builds it later). Mutates `raw` in place.
|
|
113
|
+
*/
|
|
114
|
+
function promoteInlineBodies(raw: unknown): void {
|
|
115
|
+
if (typeof raw !== "object" || raw === null) return;
|
|
116
|
+
const doc = raw as {
|
|
117
|
+
openapi?: unknown;
|
|
118
|
+
paths?: Record<string, Record<string, unknown>>;
|
|
119
|
+
components?: { schemas?: Record<string, unknown> };
|
|
120
|
+
};
|
|
121
|
+
if (typeof doc.openapi !== "string" || doc.paths === undefined) return;
|
|
122
|
+
doc.components ??= {};
|
|
123
|
+
doc.components.schemas ??= {};
|
|
124
|
+
const table = doc.components.schemas;
|
|
125
|
+
|
|
126
|
+
const pascal = (s: string): string =>
|
|
127
|
+
s
|
|
128
|
+
.replace(/[{}]/g, "")
|
|
129
|
+
.split(/[^A-Za-z0-9]+/)
|
|
130
|
+
.filter(Boolean)
|
|
131
|
+
.map((w) => w[0]!.toUpperCase() + w.slice(1))
|
|
132
|
+
.join("");
|
|
133
|
+
const claim = (base: string): string => {
|
|
134
|
+
let name = base;
|
|
135
|
+
for (let i = 2; table[name] !== undefined; i++) name = `${base}${i}`;
|
|
136
|
+
return name;
|
|
137
|
+
};
|
|
138
|
+
const isInlineObject = (schema: unknown): schema is Record<string, unknown> =>
|
|
139
|
+
typeof schema === "object" &&
|
|
140
|
+
schema !== null &&
|
|
141
|
+
(schema as { $ref?: unknown }).$ref === undefined &&
|
|
142
|
+
((schema as { type?: unknown }).type === "object" ||
|
|
143
|
+
typeof (schema as { properties?: unknown }).properties === "object") &&
|
|
144
|
+
Object.keys(
|
|
145
|
+
((schema as { properties?: unknown }).properties ?? {}) as object,
|
|
146
|
+
).length > 0;
|
|
147
|
+
const hoist = (holder: unknown, baseName: string): void => {
|
|
148
|
+
const h = holder as { schema?: unknown } | undefined;
|
|
149
|
+
if (h === undefined || !isInlineObject(h.schema)) return;
|
|
150
|
+
const name = claim(baseName);
|
|
151
|
+
table[name] = h.schema;
|
|
152
|
+
h.schema = { $ref: `#/components/schemas/${name}` };
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
for (const [path, item] of Object.entries(doc.paths)) {
|
|
156
|
+
if (typeof item !== "object" || item === null) continue;
|
|
157
|
+
const pathName = path.split("/").filter(Boolean).map(pascal).join("");
|
|
158
|
+
for (const method of ["get", "post", "put", "patch", "delete", "head"]) {
|
|
159
|
+
const op = item[method] as
|
|
160
|
+
| {
|
|
161
|
+
requestBody?: { content?: Record<string, unknown> };
|
|
162
|
+
responses?: Record<string, { content?: Record<string, unknown> }>;
|
|
163
|
+
}
|
|
164
|
+
| undefined;
|
|
165
|
+
if (op === undefined || typeof op !== "object") continue;
|
|
166
|
+
const opName = `Api${pathName}${pascal(method)}`;
|
|
167
|
+
hoist(op.requestBody?.content?.["application/json"], `${opName}Body`);
|
|
168
|
+
for (const [status, response] of Object.entries(op.responses ?? {})) {
|
|
169
|
+
if (typeof response !== "object" || response === null) continue;
|
|
170
|
+
hoist(
|
|
171
|
+
response.content?.["application/json"],
|
|
172
|
+
`${opName}${pascal(status)}Response`,
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Strip `allOf` members that are an unresolvable JSON-Pointer `$ref` INTO another
|
|
181
|
+
* operation's inline schema (`#/paths/~1v2~1account~1keys/.../allOf/1`) — the way
|
|
182
|
+
* DigitalOcean DRYs up shared pagination/meta wrappers. `upgradeDocument` cannot
|
|
183
|
+
* chase a `#/paths/...` pointer, and when one sits alongside the real data member
|
|
184
|
+
* (`allOf: [{ properties: { droplets } }, <links-ref>, <meta-ref>]`) it discards
|
|
185
|
+
* the WHOLE `allOf` — so the list response comes out an empty schema, its table
|
|
186
|
+
* renders no columns, and its id producer is lost. Drop only the path-pointer
|
|
187
|
+
* members (pagination noise, not domain data); when a single data member with
|
|
188
|
+
* `properties` remains, hoist it to a plain object so the upgrade keeps it.
|
|
189
|
+
* Document-wide, depth-guarded, mutates in place.
|
|
190
|
+
*/
|
|
191
|
+
function stripPathRefAllOf(node: unknown, depth: number): void {
|
|
192
|
+
if (node === null || typeof node !== "object" || depth > 64) return;
|
|
193
|
+
if (Array.isArray(node)) {
|
|
194
|
+
for (const v of node) stripPathRefAllOf(v, depth + 1);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const obj = node as Record<string, unknown>;
|
|
198
|
+
if (Array.isArray(obj.allOf)) {
|
|
199
|
+
const kept = obj.allOf.filter(
|
|
200
|
+
(m) =>
|
|
201
|
+
!(
|
|
202
|
+
m !== null &&
|
|
203
|
+
typeof m === "object" &&
|
|
204
|
+
typeof (m as { $ref?: unknown }).$ref === "string" &&
|
|
205
|
+
((m as { $ref: string }).$ref).startsWith("#/paths/")
|
|
206
|
+
),
|
|
207
|
+
);
|
|
208
|
+
if (kept.length !== obj.allOf.length) {
|
|
209
|
+
const only = kept[0] as Record<string, unknown> | undefined;
|
|
210
|
+
if (kept.length === 1 && only?.properties !== undefined && only.$ref === undefined) {
|
|
211
|
+
obj.type = "object";
|
|
212
|
+
obj.properties = { ...(obj.properties as object), ...(only.properties as object) };
|
|
213
|
+
if (only.required !== undefined) obj.required = only.required;
|
|
214
|
+
delete obj.allOf;
|
|
215
|
+
} else if (kept.length === 0) {
|
|
216
|
+
delete obj.allOf;
|
|
217
|
+
} else {
|
|
218
|
+
obj.allOf = kept;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
for (const v of Object.values(obj)) stripPathRefAllOf(v, depth + 1);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Flatten `allOf` composition into plain `properties` BEFORE upgrading —
|
|
227
|
+
* document-wide, not just component schemas. Two real-world failure shapes:
|
|
228
|
+
*
|
|
229
|
+
* - `OpenApiConverter.upgradeDocument` drops `allOf` for some component shapes
|
|
230
|
+
* (Box's `CollaborationAllowlistEntries = allOf[Pagination, { entries }]`
|
|
231
|
+
* comes out an empty object), so the type would render zero fields.
|
|
232
|
+
* - An `allOf` INLINE in an operation response (DigitalOcean's
|
|
233
|
+
* `GET /v2/droplets` → `allOf:[{properties:{droplets}}, {$ref:Pagination}]`)
|
|
234
|
+
* defeats nestia's type emitter: the route's SDK function comes out
|
|
235
|
+
* `Response = any`, `typia.random<any>()` yields nothing, and every
|
|
236
|
+
* simulate-mode screen for that resource renders empty forever — silently
|
|
237
|
+
* (38 of DigitalOcean's 148 SDK files before this pass).
|
|
238
|
+
*
|
|
239
|
+
* Merge each member's properties (resolving `$ref` members recursively,
|
|
240
|
+
* depth-guarded) into the schema's own `properties` and remove the `allOf`, so
|
|
241
|
+
* the composed fields survive both the upgrade and SDK generation. Component
|
|
242
|
+
* schemas are flattened first (so `$ref` members resolve to already-flat
|
|
243
|
+
* shapes), then every remaining `allOf` anywhere in the document. Mutates `raw`
|
|
244
|
+
* in place.
|
|
245
|
+
*/
|
|
246
|
+
function flattenAllOf(raw: unknown): void {
|
|
247
|
+
if (typeof raw !== "object" || raw === null) return;
|
|
248
|
+
const schemas = (raw as { components?: { schemas?: unknown } }).components
|
|
249
|
+
?.schemas;
|
|
250
|
+
const table = (typeof schemas === "object" && schemas !== null
|
|
251
|
+
? schemas
|
|
252
|
+
: {}) as Record<string, Record<string, unknown>>;
|
|
253
|
+
|
|
254
|
+
const merge = (
|
|
255
|
+
schema: unknown,
|
|
256
|
+
depth: number,
|
|
257
|
+
seen: ReadonlySet<string>,
|
|
258
|
+
): { properties: Record<string, unknown>; required: string[] } => {
|
|
259
|
+
if (depth > 8 || typeof schema !== "object" || schema === null) {
|
|
260
|
+
return { properties: {}, required: [] };
|
|
261
|
+
}
|
|
262
|
+
const node = schema as Record<string, unknown>;
|
|
263
|
+
if (typeof node.$ref === "string") {
|
|
264
|
+
const name = node.$ref.split("/").pop()!;
|
|
265
|
+
if (seen.has(name) || table[name] === undefined) {
|
|
266
|
+
return { properties: {}, required: [] };
|
|
267
|
+
}
|
|
268
|
+
return merge(table[name], depth + 1, new Set([...seen, name]));
|
|
269
|
+
}
|
|
270
|
+
let properties: Record<string, unknown> = {};
|
|
271
|
+
let required: string[] = [];
|
|
272
|
+
if (Array.isArray(node.allOf)) {
|
|
273
|
+
for (const member of node.allOf) {
|
|
274
|
+
const sub = merge(member, depth + 1, seen);
|
|
275
|
+
properties = { ...properties, ...sub.properties };
|
|
276
|
+
required = [...required, ...sub.required];
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (typeof node.properties === "object" && node.properties !== null) {
|
|
280
|
+
properties = { ...properties, ...(node.properties as object) };
|
|
281
|
+
}
|
|
282
|
+
if (Array.isArray(node.required)) required = [...required, ...node.required];
|
|
283
|
+
return { properties, required: [...new Set(required)] };
|
|
284
|
+
};
|
|
285
|
+
|
|
286
|
+
const flattenInPlace = (
|
|
287
|
+
schema: Record<string, unknown>,
|
|
288
|
+
seen: ReadonlySet<string>,
|
|
289
|
+
): void => {
|
|
290
|
+
const merged = merge(schema, 0, seen);
|
|
291
|
+
if (Object.keys(merged.properties).length > 0) {
|
|
292
|
+
schema.properties = merged.properties;
|
|
293
|
+
schema.required = merged.required;
|
|
294
|
+
schema.type = "object";
|
|
295
|
+
delete schema.allOf;
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
// Pass 1 — component schemas, so a `$ref` member met later resolves flat.
|
|
300
|
+
for (const name of Object.keys(table)) {
|
|
301
|
+
const schema = table[name];
|
|
302
|
+
if (schema === undefined || !Array.isArray(schema.allOf)) continue;
|
|
303
|
+
flattenInPlace(schema, new Set([name]));
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// Pass 2 — every remaining `allOf` anywhere in the document (inline response
|
|
307
|
+
// and request schemas, nested properties, array items …).
|
|
308
|
+
const walk = (node: unknown, depth: number): void => {
|
|
309
|
+
if (depth > 64 || typeof node !== "object" || node === null) return;
|
|
310
|
+
if (Array.isArray(node)) {
|
|
311
|
+
for (const v of node) walk(v, depth + 1);
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
const obj = node as Record<string, unknown>;
|
|
315
|
+
// Children first, so a nested allOf is flat before its parent merges it.
|
|
316
|
+
for (const v of Object.values(obj)) walk(v, depth + 1);
|
|
317
|
+
if (Array.isArray(obj.allOf)) flattenInPlace(obj, new Set());
|
|
318
|
+
};
|
|
319
|
+
walk(raw, 0);
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Remove path keys a typed SDK cannot be generated from:
|
|
324
|
+
*
|
|
325
|
+
* 1. `#` fragments some swaggers (Box) use to disambiguate overloaded paths
|
|
326
|
+
* (`/shared_items#folders`) — nestia turns the `#` into an illegal JS
|
|
327
|
+
* accessor segment, and the generated SDK file fails to parse.
|
|
328
|
+
* 2. A path-template parameter whose NAME is not a valid identifier
|
|
329
|
+
* (`/teams/{enterprise-team}/...`, GitHub). nestia derives the SDK accessor
|
|
330
|
+
* from the param name and crashes (`Cannot read properties of undefined`)
|
|
331
|
+
* when the hyphenated name does not round-trip — taking the WHOLE generation
|
|
332
|
+
* down, not just that route.
|
|
333
|
+
*
|
|
334
|
+
* Dropping the handful of offending paths up front lets the rest of a large real
|
|
335
|
+
* swagger (GitHub's 1190 operations) generate. Mutates `raw` in place.
|
|
336
|
+
*/
|
|
337
|
+
function dropUnroutablePaths(raw: unknown): void {
|
|
338
|
+
if (typeof raw !== "object" || raw === null) return;
|
|
339
|
+
const paths = (raw as Record<string, unknown>).paths;
|
|
340
|
+
if (typeof paths !== "object" || paths === null) return;
|
|
341
|
+
// Standard URL path characters only — letters, digits, `_-./`, and `{param}`.
|
|
342
|
+
const SAFE = /^[A-Za-z0-9_\-./{}]+$/;
|
|
343
|
+
// A `{param}` template name must be a valid JS identifier (no hyphens).
|
|
344
|
+
const IDENT_PARAM = /^[A-Za-z_$][A-Za-z0-9_$]*$/;
|
|
345
|
+
for (const key of Object.keys(paths as object)) {
|
|
346
|
+
const paramNames = [...key.matchAll(/\{([^}]+)\}/g)].map((m) => m[1]!);
|
|
347
|
+
if (!SAFE.test(key) || paramNames.some((p) => !IDENT_PARAM.test(p))) {
|
|
348
|
+
delete (paths as Record<string, unknown>)[key];
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Resolve `$ref` parameters before upgrading. Real-world swaggers (DigitalOcean,
|
|
355
|
+
* Box, …) DRY up shared query/path parameters with a `$ref` — sometimes a
|
|
356
|
+
* standard `#/components/parameters/X`, sometimes a raw JSON Pointer into another
|
|
357
|
+
* operation (`#/paths/~1v2~1account~1keys/get/parameters/0`). `upgradeDocument`
|
|
358
|
+
* does not chase those pointers; it leaves the slot `null`, which then crashes
|
|
359
|
+
* `HttpMigration` with "Cannot read properties of undefined (reading 'in')".
|
|
360
|
+
*
|
|
361
|
+
* Walk every operation's (and path item's) `parameters`, dereference each `$ref`
|
|
362
|
+
* by generic JSON Pointer against the raw document — transitively, with a depth
|
|
363
|
+
* guard — and drop anything that does not resolve to a real `{ in, name }`
|
|
364
|
+
* parameter. Mutates the freshly-parsed payload in place (we own it; cloning a
|
|
365
|
+
* multi-MB document would be wasteful).
|
|
366
|
+
*/
|
|
367
|
+
function resolveParameterRefs(raw: unknown): unknown {
|
|
368
|
+
if (typeof raw !== "object" || raw === null) return raw;
|
|
369
|
+
const doc = raw as Record<string, unknown>;
|
|
370
|
+
const paths = doc.paths;
|
|
371
|
+
if (typeof paths !== "object" || paths === null) return raw;
|
|
372
|
+
|
|
373
|
+
const resolve = (node: unknown, depth: number): unknown => {
|
|
374
|
+
if (depth > 16 || node === null || typeof node !== "object") return undefined;
|
|
375
|
+
const ref = (node as { $ref?: unknown }).$ref;
|
|
376
|
+
if (typeof ref === "string") return resolve(jsonPointer(doc, ref), depth + 1);
|
|
377
|
+
return "in" in (node as object) ? node : undefined;
|
|
378
|
+
};
|
|
379
|
+
const clean = (list: unknown): unknown[] | undefined => {
|
|
380
|
+
if (!Array.isArray(list)) return undefined;
|
|
381
|
+
return list.map((p) => resolve(p, 0)).filter((p) => p !== undefined);
|
|
382
|
+
};
|
|
383
|
+
|
|
384
|
+
const METHODS = ["get", "post", "put", "patch", "delete", "options", "head"];
|
|
385
|
+
for (const key of Object.keys(paths as object)) {
|
|
386
|
+
const item = (paths as Record<string, unknown>)[key];
|
|
387
|
+
if (typeof item !== "object" || item === null) continue;
|
|
388
|
+
const pathItem = item as Record<string, unknown>;
|
|
389
|
+
const shared = clean(pathItem.parameters);
|
|
390
|
+
if (shared !== undefined) pathItem.parameters = shared;
|
|
391
|
+
for (const method of METHODS) {
|
|
392
|
+
const op = pathItem[method];
|
|
393
|
+
if (typeof op !== "object" || op === null) continue;
|
|
394
|
+
const params = clean((op as Record<string, unknown>).parameters);
|
|
395
|
+
if (params !== undefined) {
|
|
396
|
+
(op as Record<string, unknown>).parameters = params;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
return raw;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
/** Resolve a `#/...` JSON Pointer against a document. Returns `undefined` on miss. */
|
|
404
|
+
function jsonPointer(doc: unknown, pointer: string): unknown {
|
|
405
|
+
if (!pointer.startsWith("#/")) return undefined;
|
|
406
|
+
let current: unknown = doc;
|
|
407
|
+
for (const rawPart of pointer.slice(2).split("/")) {
|
|
408
|
+
const part = rawPart.replace(/~1/g, "/").replace(/~0/g, "~");
|
|
409
|
+
if (current === null || typeof current !== "object") return undefined;
|
|
410
|
+
current = (current as Record<string, unknown>)[part];
|
|
411
|
+
}
|
|
412
|
+
return current;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/**
|
|
416
|
+
* File-path convenience: read `path` as UTF-8 JSON, parse, and run it through
|
|
417
|
+
* {@link fromSwagger}. Rejects with a helpful error message when the file is
|
|
418
|
+
* missing or not valid JSON.
|
|
419
|
+
*/
|
|
420
|
+
export async function fromSwaggerFile(
|
|
421
|
+
path: string,
|
|
422
|
+
): Promise<OpenApi.IDocument> {
|
|
423
|
+
let raw: string;
|
|
424
|
+
try {
|
|
425
|
+
raw = await fs.readFile(path, "utf-8");
|
|
426
|
+
} catch (err) {
|
|
427
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
428
|
+
throw new Error(`Failed to read swagger file at "${path}": ${message}`);
|
|
429
|
+
}
|
|
430
|
+
let parsed: unknown;
|
|
431
|
+
try {
|
|
432
|
+
parsed = JSON.parse(raw);
|
|
433
|
+
} catch (err) {
|
|
434
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
435
|
+
throw new Error(`Swagger file at "${path}" is not valid JSON: ${message}`);
|
|
436
|
+
}
|
|
437
|
+
return fromSwagger(
|
|
438
|
+
parsed as Parameters<typeof OpenApiConverter.upgradeDocument>[0],
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Pull the first non-empty `servers[].url` out of a raw OpenAPI payload, or
|
|
444
|
+
* `null` when none is present.
|
|
445
|
+
*
|
|
446
|
+
* `OpenApi.IDocument` (the shape AutoView's orchestrators consume) does
|
|
447
|
+
* not carry the OpenAPI `servers[]` field — it is dropped by
|
|
448
|
+
* `invertOpenApiDocument` because the AutoBE backend phases do not need it. The
|
|
449
|
+
* standalone CLI does need it, though: it is the difference between the
|
|
450
|
+
* generated frontend booting with `simulate: false` against the user's real
|
|
451
|
+
* backend (real data on first open) and the simulator default (typia-random
|
|
452
|
+
* gibberish). Read it directly off the raw swagger here so the CLI can pass the
|
|
453
|
+
* host into {@link AutoViewAgent} without parsing the swagger twice.
|
|
454
|
+
*
|
|
455
|
+
* Returns `null` when:
|
|
456
|
+
*
|
|
457
|
+
* - The payload is not an object, or
|
|
458
|
+
* - `servers` is missing / empty / not an array, or
|
|
459
|
+
* - No `servers[i].url` is a non-empty string.
|
|
460
|
+
*/
|
|
461
|
+
export function extractBackendFromSwagger(payload: unknown): {
|
|
462
|
+
host: string;
|
|
463
|
+
} | null {
|
|
464
|
+
if (typeof payload !== "object" || payload === null) return null;
|
|
465
|
+
const servers = (payload as { servers?: unknown }).servers;
|
|
466
|
+
if (!Array.isArray(servers)) return null;
|
|
467
|
+
for (const server of servers) {
|
|
468
|
+
if (typeof server !== "object" || server === null) continue;
|
|
469
|
+
const url = (server as { url?: unknown }).url;
|
|
470
|
+
if (typeof url === "string" && url.length > 0) {
|
|
471
|
+
return { host: url };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
return null;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/**
|
|
478
|
+
* File-path convenience for {@link extractBackendFromSwagger}: read `path` as
|
|
479
|
+
* UTF-8 JSON, parse, and run it through the extractor. Returns `null` on read
|
|
480
|
+
* or parse failure rather than throwing — the caller usually has a `--backend
|
|
481
|
+
* <url>` flag and a simulator fallback to lean on, and a missing file would
|
|
482
|
+
* already have failed louder via {@link fromSwaggerFile}.
|
|
483
|
+
*/
|
|
484
|
+
export async function extractBackendFromSwaggerFile(
|
|
485
|
+
path: string,
|
|
486
|
+
): Promise<{ host: string } | null> {
|
|
487
|
+
let raw: string;
|
|
488
|
+
try {
|
|
489
|
+
raw = await fs.readFile(path, "utf-8");
|
|
490
|
+
} catch {
|
|
491
|
+
return null;
|
|
492
|
+
}
|
|
493
|
+
let parsed: unknown;
|
|
494
|
+
try {
|
|
495
|
+
parsed = JSON.parse(raw);
|
|
496
|
+
} catch {
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
return extractBackendFromSwagger(parsed);
|
|
500
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { OpenApiConverter } from "@typia/utils";
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
|
|
4
|
+
import { generateDeterministic } from "./generateDeterministic";
|
|
5
|
+
|
|
6
|
+
function doc(paths: Record<string, unknown>, schemas: Record<string, unknown> = {}) {
|
|
7
|
+
return OpenApiConverter.upgradeDocument({
|
|
8
|
+
openapi: "3.0.0", info: { title: "Shop", version: "1.0.0" }, paths, components: { schemas },
|
|
9
|
+
} as never);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const fixture = doc(
|
|
13
|
+
{
|
|
14
|
+
"/sales": { get: { operationId: "index", responses: { 200: { description: "ok", content: { "application/json": { schema: { type: "array", items: { $ref: "#/components/schemas/Sale" } } } } } } } },
|
|
15
|
+
"/sales/{saleId}": { get: { operationId: "at", parameters: [{ name: "saleId", in: "path", required: true, schema: { type: "string" } }], responses: { 200: { description: "ok", content: { "application/json": { schema: { $ref: "#/components/schemas/Sale" } } } } } } },
|
|
16
|
+
},
|
|
17
|
+
{ Sale: { type: "object", properties: { id: { type: "string" }, title: { type: "string" } }, required: ["id"] } },
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
describe("generateDeterministic", () => {
|
|
21
|
+
it("produces a runnable project with NO LLM (scaffold + pages + connection)", async () => {
|
|
22
|
+
const files = await generateDeterministic(fixture, { backend: { host: null } });
|
|
23
|
+
// the project shell, the connection module, and at least the list page exist
|
|
24
|
+
expect(files["app/page.tsx"]).toBeDefined();
|
|
25
|
+
expect(files["src/lib/connection.ts"]).toBeDefined();
|
|
26
|
+
expect(files["app/sales/page.tsx"]).toBeDefined();
|
|
27
|
+
// the list page wires the typed SDK call — proof the deterministic render ran
|
|
28
|
+
expect(files["app/sales/page.tsx"]).toContain("api.functional.sales");
|
|
29
|
+
// no LLM path was taken (it would have thrown); reaching here proves it
|
|
30
|
+
expect(Object.keys(files).length).toBeGreaterThan(20);
|
|
31
|
+
}, 30000);
|
|
32
|
+
|
|
33
|
+
it("is deterministic — same document, same files", async () => {
|
|
34
|
+
const a = await generateDeterministic(fixture, { backend: { host: null } });
|
|
35
|
+
const b = await generateDeterministic(fixture, { backend: { host: null } });
|
|
36
|
+
expect(Object.keys(a).sort()).toEqual(Object.keys(b).sort());
|
|
37
|
+
expect(a["app/sales/page.tsx"]).toBe(b["app/sales/page.tsx"]);
|
|
38
|
+
}, 30000);
|
|
39
|
+
});
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { OpenApi } from "@typia/interface";
|
|
2
|
+
|
|
3
|
+
import { AutoViewInterfaceCompiler } from "./compiler/AutoViewInterfaceCompiler";
|
|
4
|
+
import { orchestrateAutoViewRenderDeterministic } from "./orchestrate/orchestrateAutoViewRenderDeterministic";
|
|
5
|
+
import { orchestrateAutoViewScaffold } from "./orchestrate/orchestrateAutoViewScaffold";
|
|
6
|
+
import { buildDeterministicPlan } from "./orchestrate/utils/buildDeterministicPlan";
|
|
7
|
+
import { buildDeterministicSdkMap } from "./orchestrate/utils/buildDeterministicSdkMap";
|
|
8
|
+
import { normalizeProductPlanPaths } from "./orchestrate/utils/normalizeProductPlanPaths";
|
|
9
|
+
import { IEndpointFilter } from "./utils/endpointFilter";
|
|
10
|
+
import { sliceDocument } from "./utils/sliceDocument";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Generate the frontend with NO LLM and NO API key — the whole READ → MAP →
|
|
14
|
+
* RENDER pipeline is deterministic, so for the common case (a clean swagger that
|
|
15
|
+
* compiles) you do not need a model at all. This is the path the `--no-llm` CLI
|
|
16
|
+
* flag and any programmatic caller take.
|
|
17
|
+
*
|
|
18
|
+
* It runs exactly the deterministic phases of {@link orchestrateAutoView}:
|
|
19
|
+
*
|
|
20
|
+
* 1. slice the document to `include`/`exclude` (so a large ERP spec still fits),
|
|
21
|
+
* 2. derive the SDK map + product plan structurally,
|
|
22
|
+
* 3. scaffold the project + render every page deterministically.
|
|
23
|
+
*
|
|
24
|
+
* It deliberately skips the LLM phases the full agent runs — the typecheck-
|
|
25
|
+
* recovery rerender, the runtime audit, the UI review. The render is already
|
|
26
|
+
* typecheck-clean on real swaggers; without a key those polish passes simply do
|
|
27
|
+
* not run. Returns the flat `path → content` project map.
|
|
28
|
+
*/
|
|
29
|
+
export async function generateDeterministic(
|
|
30
|
+
rawDocument: OpenApi.IDocument,
|
|
31
|
+
options: {
|
|
32
|
+
backend: { host: string | null };
|
|
33
|
+
filter?: IEndpointFilter;
|
|
34
|
+
semaphore?: number;
|
|
35
|
+
},
|
|
36
|
+
): Promise<Record<string, string>> {
|
|
37
|
+
const document = sliceDocument(rawDocument, options.filter ?? {});
|
|
38
|
+
const sdkMap = buildDeterministicSdkMap(document);
|
|
39
|
+
const actor = sdkMap.actors[0]?.name ?? "user";
|
|
40
|
+
const productPlan = normalizeProductPlanPaths(
|
|
41
|
+
buildDeterministicPlan(document, actor, options.filter ?? {}),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// A minimal context: the deterministic phases need only the interface compiler
|
|
45
|
+
// and a semaphore. `conversate` must never be reached on this path — if it is,
|
|
46
|
+
// that is a bug (an LLM call leaked into the deterministic pipeline).
|
|
47
|
+
const compiler = { interface: new AutoViewInterfaceCompiler() };
|
|
48
|
+
const ctx = {
|
|
49
|
+
state: () => ({ interface: { document, step: 0 } }),
|
|
50
|
+
compiler: async () => compiler,
|
|
51
|
+
vendor: { semaphore: options.semaphore ?? 8 },
|
|
52
|
+
dispatch: () => undefined,
|
|
53
|
+
conversate: async () => {
|
|
54
|
+
throw new Error(
|
|
55
|
+
"generateDeterministic reached an LLM call — the deterministic path must not.",
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
// biome-ignore lint: a structural stub of IAutoViewAgentContext's deterministic surface
|
|
59
|
+
} as never;
|
|
60
|
+
|
|
61
|
+
const scaffolded = await orchestrateAutoViewScaffold(ctx, {
|
|
62
|
+
document,
|
|
63
|
+
sdkMap,
|
|
64
|
+
sdkMapMarkdown: "# SDK Map\n\n_(deterministic)_\n",
|
|
65
|
+
productPlan,
|
|
66
|
+
productPlanMarkdown: "# Product Plan\n\n_(deterministic)_\n",
|
|
67
|
+
step: 0,
|
|
68
|
+
backend: options.backend,
|
|
69
|
+
});
|
|
70
|
+
const render = await orchestrateAutoViewRenderDeterministic(ctx, {
|
|
71
|
+
document,
|
|
72
|
+
productPlan,
|
|
73
|
+
step: 0,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return { ...scaffolded, ...render.files };
|
|
77
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export * from "./AutoViewAgent";
|
|
2
|
+
export * from "./context/IAutoViewAgentContext";
|
|
3
|
+
export * from "./fromSwagger";
|
|
4
|
+
export * from "./orchestrate/orchestrateAutoView";
|
|
5
|
+
export { AutoViewFrontendTemplate } from "./constants/AutoViewFrontendTemplate";
|
|
6
|
+
export {
|
|
7
|
+
describeEndpointPropsShape,
|
|
8
|
+
describeRequestBodyHint,
|
|
9
|
+
} from "./orchestrate/utils/describeEndpointPropsShape";
|
|
10
|
+
export { describeEndpointRequestBodyShape } from "./orchestrate/utils/describeEndpointRequestBodyShape";
|
|
11
|
+
export { describeEndpointResponseShape } from "./orchestrate/utils/describeEndpointResponseShape";
|
|
12
|
+
export {
|
|
13
|
+
renderJsonSchema,
|
|
14
|
+
renderNamedSchema,
|
|
15
|
+
} from "./orchestrate/utils/renderJsonSchema";
|
|
16
|
+
export {
|
|
17
|
+
hashFrontendDependencies,
|
|
18
|
+
validateFrontendTypecheck,
|
|
19
|
+
} from "./orchestrate/utils/validateFrontendTypecheck";
|
|
20
|
+
export type {
|
|
21
|
+
ITypecheckDiagnostic,
|
|
22
|
+
IValidateFrontendTypecheckResult,
|
|
23
|
+
} from "./orchestrate/utils/validateFrontendTypecheck";
|
|
24
|
+
export { auditFrontendRuntime } from "./orchestrate/utils/auditFrontendRuntime";
|
|
25
|
+
export type {
|
|
26
|
+
IAuditFrontendRuntimeResult,
|
|
27
|
+
IRuntimeAuditDiagnostic,
|
|
28
|
+
} from "./orchestrate/utils/auditFrontendRuntime";
|
|
29
|
+
export { loadShoppingFixture } from "./orchestrate/utils/loadShoppingFixture";
|
|
30
|
+
export type { IShoppingFixture } from "./orchestrate/utils/loadShoppingFixture";
|