/ sdks / sandbox / javascript / scripts / generate-api.mjs
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