#!/usr/bin/env node import { spawnSync } from "node:child_process"; import { copyFileSync, existsSync, mkdirSync, readdirSync, readFileSync, rmSync, writeFileSync, } from "node:fs"; import { cp } from "node:fs/promises"; import path from "node:path"; import { gzipSync } from "node:zlib"; const repoUrl = "https://github.com/KittyCAD/modeling-app.git"; const repoRef = process.env.ZOO_MODELING_APP_REF || "6f87f4e66c07c67f09d8f7db284496a3ae3332e7"; const repoTarballUrls = [ `https://ghfast.top/https://github.com/KittyCAD/modeling-app/archive/${repoRef}.tar.gz`, `https://github.com/KittyCAD/modeling-app/archive/${repoRef}.tar.gz`, ]; const root = process.cwd(); const workspaceDir = path.join(root, ".zoo-modeling-app"); const sourceDir = path.join(workspaceDir, "modeling-app"); const sourceRefFile = path.join(sourceDir, ".hicad-source-ref"); const outputDir = path.join(root, "public/vendor/zoo-modeling-app"); const customizationDir = path.join(root, "vendor/zoo-modeling-app"); const proxyToken = "hicad-session"; function run(command, args, options = {}) { const result = spawnSync(command, args, { cwd: options.cwd || root, env: { ...process.env, ...(options.env || {}) }, shell: false, stdio: "inherit", }); if (result.status !== 0) { throw new Error(`${command} ${args.join(" ")} failed`); } } function tryRun(command, args, options = {}) { const result = spawnSync(command, args, { cwd: options.cwd || root, env: { ...process.env, ...(options.env || {}) }, shell: false, stdio: "inherit", }); return result.status === 0; } function downloadOfficialSource() { mkdirSync(workspaceDir, { recursive: true }); const archivePath = path.join(workspaceDir, "modeling-app-main.tar.gz"); const extractedDir = path.join(workspaceDir, `modeling-app-${repoRef}`); rmSync(archivePath, { force: true }); rmSync(extractedDir, { force: true, recursive: true }); const downloaded = repoTarballUrls.some((url) => tryRun("curl", [ "--http1.1", "--location", "--fail", "--retry", "3", url, "--output", archivePath, ]) ); if (!downloaded) { throw new Error("Unable to download KittyCAD/modeling-app"); } run("tar", ["-xzf", archivePath, "-C", workspaceDir]); rmSync(sourceDir, { force: true, recursive: true }); run("mv", [extractedDir, sourceDir]); writeFileSync(sourceRefFile, `${repoRef}\n`); } function ensureOfficialSource() { if (existsSync(path.join(sourceDir, "package.json"))) { if (!existsSync(sourceRefFile)) { writeFileSync(sourceRefFile, `${repoRef}\n`); } const currentRef = readFileSync(sourceRefFile, "utf8").trim(); if (currentRef !== repoRef) { downloadOfficialSource(); } return; } downloadOfficialSource(); } function replaceSource(relativePath, before, after) { const filePath = path.join(sourceDir, relativePath); const source = readFileSync(filePath, "utf8"); if (after && source.includes(after)) { return; } if (!source.includes(before)) { if (!after) { return; } throw new Error(`Official source changed; patch failed for ${relativePath}`); } writeFileSync(filePath, source.replace(before, after)); } function patchOfficialSource() { copyFileSync( path.join(customizationDir, "Logo.tsx"), path.join(sourceDir, "src/components/Logo.tsx") ); copyFileSync( path.join(customizationDir, "localization.ts"), path.join(sourceDir, "src/lib/hicadLocalization.ts") ); replaceSource( "index.html", ' ', ' ' ); replaceSource( "index.html", ' \n', "" ); replaceSource( "index.html", "
To view your account, manage payment methods, or change tiers, simply{' '}`, `
To view your account, manage payment methods, or change tiers, simply{' '}` ); } function ensureDependencies() { if (existsSync(path.join(sourceDir, "node_modules/.package-lock.json"))) { return; } run("npm", ["ci", "--ignore-scripts", "--no-audit", "--no-fund"], { cwd: sourceDir, }); } function ensureWasm() { const wasmJs = path.join( sourceDir, "rust/kcl-wasm-lib/pkg/kcl_wasm_lib.js" ); const wasmBinary = path.join( sourceDir, "rust/kcl-wasm-lib/pkg/kcl_wasm_lib_bg.wasm" ); if (!(existsSync(wasmJs) && existsSync(wasmBinary))) { const cargoEnv = { CARGO_HTTP_MULTIPLEXING: "false", CARGO_REGISTRIES_CRATES_IO_INDEX: process.env.CARGO_REGISTRIES_CRATES_IO_INDEX || "sparse+https://rsproxy.cn/index/", CARGO_REGISTRIES_CRATES_IO_PROTOCOL: "sparse", }; run( "wasm-pack", [ "build", "kcl-wasm-lib", "--release", "--target", "web", "--out-dir", "pkg", "--scope", "kittycad", "--no-opt", ], { cwd: path.join(sourceDir, "rust"), env: cargoEnv } ); run( "cargo", [ "test", "-p", "kcl-lib", "--features", "artifact-graph", "export_bindings", ], { cwd: path.join(sourceDir, "rust"), env: cargoEnv } ); } copyFileSync( path.join(sourceDir, "rust/kcl-wasm-lib/README.md"), path.join(sourceDir, "rust/kcl-wasm-lib/pkg/README.md") ); copyFileSync(wasmBinary, path.join(sourceDir, "public/kcl_wasm_lib_bg.wasm")); } function precompressVersionedAssets() { const assetsDir = path.join(outputDir, "assets"); for (const entry of readdirSync(assetsDir, { withFileTypes: true })) { if (!entry.isFile() || !/\.(?:css|js)$/.test(entry.name)) { continue; } const assetPath = path.join(assetsDir, entry.name); writeFileSync(`${assetPath}.gz`, gzipSync(readFileSync(assetPath), { level: 9 })); } } async function main() { ensureOfficialSource(); ensureDependencies(); ensureWasm(); patchOfficialSource(); const buildEnv = { VITE_KITTYCAD_WEBSOCKET_URL: process.env.HICAD_ZOO_MODELING_WEBSOCKET_URL || "/api/ai-cad/zoo-ws/modeling/commands", VITE_MLEPHANT_WEBSOCKET_URL: process.env.HICAD_ZOO_AGENT_WEBSOCKET_URL || "/api/ai-cad/zoo-ws/ml/copilot", VITE_ZOO_API_BASE_URL: "/api/ai-cad/zoo", VITE_ZOO_API_TOKEN: proxyToken, VITE_ZOO_BASE_DOMAIN: "zoo.dev", }; rmSync(path.join(sourceDir, "build"), { force: true, recursive: true }); run("npx", ["vite", "build", "--base=./", "--mode=production"], { cwd: sourceDir, env: buildEnv, }); const buildDir = path.join(sourceDir, "build"); if (!existsSync(path.join(buildDir, "index.html"))) { throw new Error("Zoo Design Studio build did not produce index.html"); } rmSync(outputDir, { force: true, recursive: true }); mkdirSync(path.dirname(outputDir), { recursive: true }); await cp(buildDir, outputDir, { recursive: true }); precompressVersionedAssets(); const license = path.join(sourceDir, "LICENSE"); if (existsSync(license)) { await cp(license, path.join(outputDir, "LICENSE")); } writeFileSync( path.join(outputDir, "SOURCE.txt"), [ "HiCAD web frontend based on Zoo Design Studio", `Source: ${repoUrl}`, `Source commit: ${repoRef}`, "License: MIT", "Authentication: HiCAD session with server-side Zoo proxy", "", ].join("\n") ); console.log(`HiCAD official frontend copied to ${outputDir}`); } main().catch((error) => { console.error(error instanceof Error ? error.message : error); process.exit(1); });