| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345 |
- #!/usr/bin/env bun
- /**
- * Build the Kilo VS Code extension and launch it in a development host.
- *
- * Usage:
- * bun script/launch.ts [options]
- *
- * Options:
- * --no-build Skip the build step (reuse last build)
- * --workspace PATH Folder to open in VS Code (default: repo root)
- * --mode dev|vsix "dev" uses --extensionDevelopmentPath, "vsix" packages a VSIX (default: dev)
- * --app-path PATH Explicit path to the VS Code executable (auto-detected if omitted)
- * --insiders Prefer VS Code Insiders over stable
- * --wait Block until the VS Code window is closed
- * --clean Wipe the user-data and extensions dirs before launching
- *
- * Environment:
- * VSCODE_EXEC_PATH Path to VS Code executable (same as --app-path)
- *
- * Cross-platform: macOS, Linux, and Windows are all supported.
- *
- * The script uses a stable directory per repo checkout under the OS temp dir
- * so nothing accumulates — the same dirs are reused on every launch.
- */
- import { $ } from "bun"
- import { createHash } from "node:crypto"
- import { existsSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"
- import { tmpdir } from "node:os"
- import { delimiter, join, resolve } from "node:path"
- import { spawn } from "node:child_process"
- const win = process.platform === "win32"
- const root = join(import.meta.dir, "..")
- const repo = resolve(root, "..", "..")
- // Stable per-repo directory under OS temp — no accumulation
- const hash = createHash("sha256").update(repo).digest("hex").slice(0, 12)
- const base = join(tmpdir(), `kilo-vscode-dev-${hash}`)
- const userDir = join(base, "user-data")
- const extDir = join(base, "extensions")
- // ---------------------------------------------------------------------------
- // Argument parsing
- // ---------------------------------------------------------------------------
- function parse(argv: string[]) {
- const result: Record<string, string | boolean> = {}
- for (let i = 0; i < argv.length; i++) {
- const item = argv[i]!
- if (!item.startsWith("--")) continue
- if (item.startsWith("--no-")) {
- result[item.slice(5)] = false
- continue
- }
- const parts = item.slice(2).split("=", 2)
- const key = parts[0]!
- const raw = parts[1]
- if (raw !== undefined) {
- result[key] = raw
- continue
- }
- const next = argv[i + 1]
- if (!next || next.startsWith("--")) {
- result[key] = true
- continue
- }
- result[key] = next
- i++
- }
- return result
- }
- const opts = parse(process.argv.slice(2))
- const shouldBuild = opts["build"] !== false
- const mode = (opts["mode"] as string) ?? "dev"
- const workspace = opts["workspace"] ? resolve(opts["workspace"] as string) : repo
- const insiders = opts["insiders"] === true
- const explicit = opts["app-path"] as string | undefined
- const blocking = opts["wait"] === true
- const clean = opts["clean"] === true
- // ---------------------------------------------------------------------------
- // VS Code executable detection
- // ---------------------------------------------------------------------------
- function which(name: string): string | null {
- const paths = (process.env.PATH ?? "").split(delimiter).filter(Boolean)
- const exts = win ? [".cmd", ".exe", ".bat", ""] : [""]
- for (const dir of paths) {
- for (const ext of exts) {
- const full = join(dir, name.endsWith(ext) ? name : `${name}${ext}`)
- if (existsSync(full)) return full
- }
- }
- return null
- }
- function detect(): string {
- const env = explicit ?? process.env["VSCODE_EXEC_PATH"]
- if (env && existsSync(env)) return env
- const candidates: string[] = []
- const prefer = insiders ? "insiders" : "stable"
- if (process.platform === "darwin") {
- const order =
- prefer === "insiders"
- ? [
- "/Applications/Visual Studio Code - Insiders.app/Contents/MacOS/Code - Insiders",
- "/Applications/Visual Studio Code.app/Contents/MacOS/Code",
- ]
- : [
- "/Applications/Visual Studio Code.app/Contents/MacOS/Code",
- "/Applications/Visual Studio Code - Insiders.app/Contents/MacOS/Code - Insiders",
- ]
- candidates.push(...order)
- }
- if (process.platform === "linux") {
- const stable = [
- "/usr/share/code/code",
- "/usr/bin/code",
- "/snap/code/current/usr/share/code/code",
- "/var/lib/flatpak/exports/bin/com.visualstudio.code",
- ]
- const ins = [
- "/usr/share/code-insiders/code-insiders",
- "/usr/bin/code-insiders",
- "/snap/code-insiders/current/usr/share/code-insiders/code-insiders",
- "/var/lib/flatpak/exports/bin/com.visualstudio.code.insiders",
- ]
- candidates.push(...(prefer === "insiders" ? [...ins, ...stable] : [...stable, ...ins]))
- }
- if (win) {
- const local = process.env["LOCALAPPDATA"] ?? ""
- const program = process.env["PROGRAMFILES"] ?? "C:\\Program Files"
- const stable = [
- join(local, "Programs", "Microsoft VS Code", "Code.exe"),
- join(program, "Microsoft VS Code", "Code.exe"),
- ]
- const ins = [
- join(local, "Programs", "Microsoft VS Code Insiders", "Code - Insiders.exe"),
- join(program, "Microsoft VS Code Insiders", "Code - Insiders.exe"),
- ]
- candidates.push(...(prefer === "insiders" ? [...ins, ...stable] : [...stable, ...ins]))
- }
- const found = candidates.find((c) => existsSync(c))
- if (found) return found
- // Last resort: PATH lookup
- const path = insiders ? (which("code-insiders") ?? which("code")) : (which("code") ?? which("code-insiders"))
- if (path) return path
- console.error(
- `Could not find VS Code. Set VSCODE_EXEC_PATH or pass --app-path.\n` +
- `Searched:\n${candidates.map((c) => ` ${c}`).join("\n")}`,
- )
- process.exit(1)
- }
- // ---------------------------------------------------------------------------
- // CLI path detection (needed for --mode vsix)
- // ---------------------------------------------------------------------------
- function codeCli(app: string): string {
- const name = app.toLowerCase().includes("insiders") ? "code-insiders" : "code"
- const direct = which(name)
- if (direct) return direct
- if (process.platform === "darwin") {
- const bundled = app.includes("Insiders")
- ? "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code"
- : "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code"
- if (existsSync(bundled)) return bundled
- }
- console.error(`VS Code CLI (${name}) not found on PATH. Install the shell command or use --mode dev instead.`)
- process.exit(1)
- }
- // ---------------------------------------------------------------------------
- // Helpers
- // ---------------------------------------------------------------------------
- function newest(paths: string[]) {
- return [...paths].sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)
- }
- // ---------------------------------------------------------------------------
- // Build
- // ---------------------------------------------------------------------------
- async function compile() {
- if (!shouldBuild) {
- console.log("[launch] Skipping build (--no-build)")
- return
- }
- console.log("[launch] Building extension...")
- await $`bun run package`.cwd(root)
- console.log("[launch] Build complete")
- }
- // ---------------------------------------------------------------------------
- // VSIX packaging (only for --mode vsix)
- // ---------------------------------------------------------------------------
- async function packageVsix(out: string): Promise<string> {
- await $`bunx vsce package --no-dependencies --skip-license -o ${out}/`.cwd(root)
- const files = newest(
- readdirSync(out)
- .filter((f) => f.endsWith(".vsix"))
- .map((f) => join(out, f)),
- )
- const vsix = files.at(0)
- if (!vsix) {
- console.error(`No VSIX was created in ${out}`)
- process.exit(1)
- }
- return vsix
- }
- async function installVsix(path: string, app: string) {
- const cmd = codeCli(app)
- await $`${cmd} --extensions-dir ${extDir} --user-data-dir ${userDir} --install-extension ${path} --force`.cwd(root)
- }
- // ---------------------------------------------------------------------------
- // Settings for isolated instance
- // ---------------------------------------------------------------------------
- function settings() {
- const dir = join(userDir, "User")
- mkdirSync(dir, { recursive: true })
- writeFileSync(
- join(dir, "settings.json"),
- JSON.stringify(
- {
- "editor.accessibilitySupport": "off",
- "extensions.autoCheckUpdates": false,
- "extensions.autoUpdate": false,
- "extensions.ignoreRecommendations": true,
- "security.workspace.trust.enabled": false,
- "task.allowAutomaticTasks": "off",
- "telemetry.telemetryLevel": "off",
- "update.mode": "none",
- "workbench.startupEditor": "none",
- "workbench.tips.enabled": false,
- "window.commandCenter": false,
- },
- null,
- 2,
- ) + "\n",
- )
- }
- // ---------------------------------------------------------------------------
- // Launch
- // ---------------------------------------------------------------------------
- async function launch() {
- await compile()
- if (clean) {
- console.log("[launch] Cleaning previous state...")
- rmSync(base, { recursive: true, force: true })
- }
- mkdirSync(userDir, { recursive: true })
- mkdirSync(extDir, { recursive: true })
- const app = detect()
- settings()
- const args = [workspace, `--extensions-dir=${extDir}`, `--user-data-dir=${userDir}`, "--skip-release-notes"]
- if (mode === "dev") {
- args.push(`--extensionDevelopmentPath=${root}`)
- args.push("--disable-extension=kilocode.kilo-code")
- }
- if (mode === "vsix") {
- const out = join(base, "vsix")
- mkdirSync(out, { recursive: true })
- const vsix = await packageVsix(out)
- await installVsix(vsix, app)
- console.log(`[launch] Installed VSIX: ${vsix}`)
- }
- if (blocking) {
- args.push("--wait")
- }
- // Strip Electron/VS Code env vars so the spawned instance doesn't attach
- // to the current Electron process (e.g. when launched from a VS Code task).
- const env = { ...process.env }
- for (const key of Object.keys(env)) {
- if (key.startsWith("ELECTRON_") || key.startsWith("VSCODE_")) delete env[key]
- }
- console.log(`[launch] Starting VS Code (${mode} mode)`)
- console.log(`[launch] Executable: ${app}`)
- console.log(`[launch] Workspace: ${workspace}`)
- console.log(`[launch] State: ${base}`)
- if (blocking) {
- const result = Bun.spawnSync([app, ...args], {
- cwd: workspace,
- env,
- stdio: ["ignore", "inherit", "inherit"],
- })
- console.log(`[launch] VS Code exited (code ${result.exitCode})`)
- return
- }
- const child = spawn(app, args, {
- cwd: workspace,
- detached: !win,
- env,
- stdio: "ignore",
- ...(win ? { shell: true } : {}),
- })
- child.unref()
- console.log(`[launch] VS Code launched (pid ${child.pid})`)
- }
- try {
- await launch()
- } catch (err) {
- console.error(`[launch] ERROR: ${err instanceof Error ? err.message : String(err)}`)
- process.exit(1)
- }
|