generate-api.mjs
1 #!/usr/bin/env node 2 3 // Copyright 2026 Alibaba Group Holding Ltd. 4 // 5 // Licensed under the Apache License, Version 2.0 (the "License"); 6 // you may not use this file except in compliance with the License. 7 // You may obtain a copy of the License at 8 // 9 // http://www.apache.org/licenses/LICENSE-2.0 10 // 11 // Unless required by applicable law or agreed to in writing, software 12 // distributed under the License is distributed on an "AS IS" BASIS, 13 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 // See the License for the specific language governing permissions and 15 // limitations under the License. 16 17 import { spawnSync } from "node:child_process"; 18 import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; 19 import path from "node:path"; 20 import process from "node:process"; 21 import { fileURLToPath } from "node:url"; 22 23 const LICENSE_OWNER = "Alibaba Group Holding Ltd."; 24 const LICENSE_MARKER_REGEX = new RegExp(`Copyright [0-9]{4} ${LICENSE_OWNER}`); 25 26 function buildLicenseText() { 27 const year = new Date().getFullYear(); 28 return `Copyright ${year} ${LICENSE_OWNER}. 29 30 Licensed under the Apache License, Version 2.0 (the "License"); 31 you may not use this file except in compliance with the License. 32 You may obtain a copy of the License at 33 34 http://www.apache.org/licenses/LICENSE-2.0 35 36 Unless required by applicable law or agreed to in writing, software 37 distributed under the License is distributed on an "AS IS" BASIS, 38 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 39 See the License for the specific language governing permissions and 40 limitations under the License.`; 41 } 42 43 function asLineCommentHeader(text) { 44 return text 45 .split("\n") 46 .map((line) => `// ${line}`) 47 .join("\n"); 48 } 49 50 function ensureLicenseHeader(filePath) { 51 const body = readFileSync(filePath, "utf8"); 52 const head = body.split("\n").slice(0, 40).join("\n"); 53 if (LICENSE_MARKER_REGEX.test(head)) { 54 return; 55 } 56 const header = asLineCommentHeader(buildLicenseText()); 57 writeFileSync(filePath, `${header}\n\n${body}`, "utf8"); 58 } 59 60 function fail(message) { 61 console.error(`ā ${message}`); 62 process.exit(1); 63 } 64 65 function run(cmd, args, cwd) { 66 const pretty = [cmd, ...args].join(" "); 67 console.log(`\nā¶ ${pretty}`); 68 const res = spawnSync(cmd, args, { cwd, stdio: "inherit" }); 69 if (res.status !== 0) { 70 fail(`Command failed (exit=${res.status}): ${pretty}`); 71 } 72 } 73 74 const __filename = fileURLToPath(import.meta.url); 75 const __dirname = path.dirname(__filename); 76 77 // scripts/ -> package root 78 const packageRoot = path.resolve(__dirname, ".."); 79 // scripts/ -> repo root (OpenSandbox/) 80 const repoRoot = path.resolve(__dirname, "../../../../"); 81 82 const specs = { 83 execd: path.join(repoRoot, "specs", "execd-api.yaml"), 84 egress: path.join(repoRoot, "specs", "egress-api.yaml"), 85 lifecycle: path.join(repoRoot, "specs", "sandbox-lifecycle.yml"), 86 }; 87 88 for (const [name, p] of Object.entries(specs)) { 89 if (!existsSync(p)) { 90 fail(`OpenAPI spec not found for '${name}': ${p}`); 91 } 92 } 93 94 const outDir = path.join(packageRoot, "src", "api"); 95 mkdirSync(outDir, { recursive: true }); 96 97 const outFiles = { 98 execd: path.join(outDir, "execd.ts"), 99 egress: path.join(outDir, "egress.ts"), 100 lifecycle: path.join(outDir, "lifecycle.ts"), 101 }; 102 103 console.log("š OpenSandbox TypeScript SDK API Generator"); 104 console.log(`- repoRoot: ${repoRoot}`); 105 console.log(`- outDir: ${outDir}`); 106 107 // Use pnpm as requested by the project rules. 108 run("pnpm", ["exec", "openapi-typescript", specs.execd, "-o", outFiles.execd], packageRoot); 109 run("pnpm", ["exec", "openapi-typescript", specs.egress, "-o", outFiles.egress], packageRoot); 110 run( 111 "pnpm", 112 ["exec", "openapi-typescript", specs.lifecycle, "-o", outFiles.lifecycle], 113 packageRoot, 114 ); 115 116 // The generator may overwrite outputs; re-apply unified license headers after generation. 117 ensureLicenseHeader(outFiles.execd); 118 ensureLicenseHeader(outFiles.egress); 119 ensureLicenseHeader(outFiles.lifecycle); 120 121 // Clarify that the generated session API in execd.ts is not the recommended entry point. 122 const EXECD_SESSION_NOTE = `/** 123 * NOTE: The session-related path types and operations in this file (e.g. /session, runInSession) 124 * are generated from the execd OpenAPI spec. They are not the recommended runtime entry point. 125 * Use \`sandbox.commands.createSession()\`, \`sandbox.commands.runInSession()\`, and 126 * \`sandbox.commands.deleteSession()\` instead. 127 */`; 128 129 function ensureExecdSessionNote(filePath) { 130 const body = readFileSync(filePath, "utf8"); 131 if (body.includes("not the recommended runtime entry point")) { 132 return; 133 } 134 // Insert after the first "Do not make direct changes" block (after the first empty line that follows it). 135 const marker = "Do not make direct changes to the file."; 136 const idx = body.indexOf(marker); 137 if (idx === -1) return; 138 const afterBlock = body.indexOf("\n\n", idx + marker.length); 139 const insertAt = afterBlock === -1 ? idx + marker.length : afterBlock + 2; 140 const newBody = body.slice(0, insertAt) + "\n" + EXECD_SESSION_NOTE + "\n" + body.slice(insertAt); 141 writeFileSync(filePath, newBody, "utf8"); 142 } 143 144 ensureExecdSessionNote(outFiles.execd); 145 146 console.log("\nā API type generation completed:"); 147 console.log(`- ${path.relative(packageRoot, outFiles.execd)}`); 148 console.log(`- ${path.relative(packageRoot, outFiles.egress)}`); 149 console.log(`- ${path.relative(packageRoot, outFiles.lifecycle)}`); 150