launch.ts 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. #!/usr/bin/env bun
  2. /**
  3. * Build the Kilo VS Code extension and launch it in a development host.
  4. *
  5. * Usage:
  6. * bun script/launch.ts [options]
  7. *
  8. * Options:
  9. * --no-build Skip the build step (reuse last build)
  10. * --workspace PATH Folder to open in VS Code (default: repo root)
  11. * --mode dev|vsix "dev" uses --extensionDevelopmentPath, "vsix" packages a VSIX (default: dev)
  12. * --app-path PATH Explicit path to the VS Code executable (auto-detected if omitted)
  13. * --insiders Prefer VS Code Insiders over stable
  14. * --wait Block until the VS Code window is closed
  15. * --clean Wipe the user-data and extensions dirs before launching
  16. *
  17. * Environment:
  18. * VSCODE_EXEC_PATH Path to VS Code executable (same as --app-path)
  19. *
  20. * Cross-platform: macOS, Linux, and Windows are all supported.
  21. *
  22. * The script uses a stable directory per repo checkout under the OS temp dir
  23. * so nothing accumulates — the same dirs are reused on every launch.
  24. */
  25. import { $ } from "bun"
  26. import { createHash } from "node:crypto"
  27. import { existsSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"
  28. import { tmpdir } from "node:os"
  29. import { delimiter, join, resolve } from "node:path"
  30. import { spawn } from "node:child_process"
  31. const win = process.platform === "win32"
  32. const root = join(import.meta.dir, "..")
  33. const repo = resolve(root, "..", "..")
  34. // Stable per-repo directory under OS temp — no accumulation
  35. const hash = createHash("sha256").update(repo).digest("hex").slice(0, 12)
  36. const base = join(tmpdir(), `kilo-vscode-dev-${hash}`)
  37. const userDir = join(base, "user-data")
  38. const extDir = join(base, "extensions")
  39. // ---------------------------------------------------------------------------
  40. // Argument parsing
  41. // ---------------------------------------------------------------------------
  42. function parse(argv: string[]) {
  43. const result: Record<string, string | boolean> = {}
  44. for (let i = 0; i < argv.length; i++) {
  45. const item = argv[i]!
  46. if (!item.startsWith("--")) continue
  47. if (item.startsWith("--no-")) {
  48. result[item.slice(5)] = false
  49. continue
  50. }
  51. const parts = item.slice(2).split("=", 2)
  52. const key = parts[0]!
  53. const raw = parts[1]
  54. if (raw !== undefined) {
  55. result[key] = raw
  56. continue
  57. }
  58. const next = argv[i + 1]
  59. if (!next || next.startsWith("--")) {
  60. result[key] = true
  61. continue
  62. }
  63. result[key] = next
  64. i++
  65. }
  66. return result
  67. }
  68. const opts = parse(process.argv.slice(2))
  69. const shouldBuild = opts["build"] !== false
  70. const mode = (opts["mode"] as string) ?? "dev"
  71. const workspace = opts["workspace"] ? resolve(opts["workspace"] as string) : repo
  72. const insiders = opts["insiders"] === true
  73. const explicit = opts["app-path"] as string | undefined
  74. const blocking = opts["wait"] === true
  75. const clean = opts["clean"] === true
  76. // ---------------------------------------------------------------------------
  77. // VS Code executable detection
  78. // ---------------------------------------------------------------------------
  79. function which(name: string): string | null {
  80. const paths = (process.env.PATH ?? "").split(delimiter).filter(Boolean)
  81. const exts = win ? [".cmd", ".exe", ".bat", ""] : [""]
  82. for (const dir of paths) {
  83. for (const ext of exts) {
  84. const full = join(dir, name.endsWith(ext) ? name : `${name}${ext}`)
  85. if (existsSync(full)) return full
  86. }
  87. }
  88. return null
  89. }
  90. function detect(): string {
  91. const env = explicit ?? process.env["VSCODE_EXEC_PATH"]
  92. if (env && existsSync(env)) return env
  93. const candidates: string[] = []
  94. const prefer = insiders ? "insiders" : "stable"
  95. if (process.platform === "darwin") {
  96. const order =
  97. prefer === "insiders"
  98. ? [
  99. "/Applications/Visual Studio Code - Insiders.app/Contents/MacOS/Code - Insiders",
  100. "/Applications/Visual Studio Code.app/Contents/MacOS/Code",
  101. ]
  102. : [
  103. "/Applications/Visual Studio Code.app/Contents/MacOS/Code",
  104. "/Applications/Visual Studio Code - Insiders.app/Contents/MacOS/Code - Insiders",
  105. ]
  106. candidates.push(...order)
  107. }
  108. if (process.platform === "linux") {
  109. const stable = [
  110. "/usr/share/code/code",
  111. "/usr/bin/code",
  112. "/snap/code/current/usr/share/code/code",
  113. "/var/lib/flatpak/exports/bin/com.visualstudio.code",
  114. ]
  115. const ins = [
  116. "/usr/share/code-insiders/code-insiders",
  117. "/usr/bin/code-insiders",
  118. "/snap/code-insiders/current/usr/share/code-insiders/code-insiders",
  119. "/var/lib/flatpak/exports/bin/com.visualstudio.code.insiders",
  120. ]
  121. candidates.push(...(prefer === "insiders" ? [...ins, ...stable] : [...stable, ...ins]))
  122. }
  123. if (win) {
  124. const local = process.env["LOCALAPPDATA"] ?? ""
  125. const program = process.env["PROGRAMFILES"] ?? "C:\\Program Files"
  126. const stable = [
  127. join(local, "Programs", "Microsoft VS Code", "Code.exe"),
  128. join(program, "Microsoft VS Code", "Code.exe"),
  129. ]
  130. const ins = [
  131. join(local, "Programs", "Microsoft VS Code Insiders", "Code - Insiders.exe"),
  132. join(program, "Microsoft VS Code Insiders", "Code - Insiders.exe"),
  133. ]
  134. candidates.push(...(prefer === "insiders" ? [...ins, ...stable] : [...stable, ...ins]))
  135. }
  136. const found = candidates.find((c) => existsSync(c))
  137. if (found) return found
  138. // Last resort: PATH lookup
  139. const path = insiders ? (which("code-insiders") ?? which("code")) : (which("code") ?? which("code-insiders"))
  140. if (path) return path
  141. console.error(
  142. `Could not find VS Code. Set VSCODE_EXEC_PATH or pass --app-path.\n` +
  143. `Searched:\n${candidates.map((c) => ` ${c}`).join("\n")}`,
  144. )
  145. process.exit(1)
  146. }
  147. // ---------------------------------------------------------------------------
  148. // CLI path detection (needed for --mode vsix)
  149. // ---------------------------------------------------------------------------
  150. function codeCli(app: string): string {
  151. const name = app.toLowerCase().includes("insiders") ? "code-insiders" : "code"
  152. const direct = which(name)
  153. if (direct) return direct
  154. if (process.platform === "darwin") {
  155. const bundled = app.includes("Insiders")
  156. ? "/Applications/Visual Studio Code - Insiders.app/Contents/Resources/app/bin/code"
  157. : "/Applications/Visual Studio Code.app/Contents/Resources/app/bin/code"
  158. if (existsSync(bundled)) return bundled
  159. }
  160. console.error(`VS Code CLI (${name}) not found on PATH. Install the shell command or use --mode dev instead.`)
  161. process.exit(1)
  162. }
  163. // ---------------------------------------------------------------------------
  164. // Helpers
  165. // ---------------------------------------------------------------------------
  166. function newest(paths: string[]) {
  167. return [...paths].sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs)
  168. }
  169. // ---------------------------------------------------------------------------
  170. // Build
  171. // ---------------------------------------------------------------------------
  172. async function compile() {
  173. if (!shouldBuild) {
  174. console.log("[launch] Skipping build (--no-build)")
  175. return
  176. }
  177. console.log("[launch] Building extension...")
  178. await $`bun run package`.cwd(root)
  179. console.log("[launch] Build complete")
  180. }
  181. // ---------------------------------------------------------------------------
  182. // VSIX packaging (only for --mode vsix)
  183. // ---------------------------------------------------------------------------
  184. async function packageVsix(out: string): Promise<string> {
  185. await $`bunx vsce package --no-dependencies --skip-license -o ${out}/`.cwd(root)
  186. const files = newest(
  187. readdirSync(out)
  188. .filter((f) => f.endsWith(".vsix"))
  189. .map((f) => join(out, f)),
  190. )
  191. const vsix = files.at(0)
  192. if (!vsix) {
  193. console.error(`No VSIX was created in ${out}`)
  194. process.exit(1)
  195. }
  196. return vsix
  197. }
  198. async function installVsix(path: string, app: string) {
  199. const cmd = codeCli(app)
  200. await $`${cmd} --extensions-dir ${extDir} --user-data-dir ${userDir} --install-extension ${path} --force`.cwd(root)
  201. }
  202. // ---------------------------------------------------------------------------
  203. // Settings for isolated instance
  204. // ---------------------------------------------------------------------------
  205. function settings() {
  206. const dir = join(userDir, "User")
  207. mkdirSync(dir, { recursive: true })
  208. writeFileSync(
  209. join(dir, "settings.json"),
  210. JSON.stringify(
  211. {
  212. "editor.accessibilitySupport": "off",
  213. "extensions.autoCheckUpdates": false,
  214. "extensions.autoUpdate": false,
  215. "extensions.ignoreRecommendations": true,
  216. "security.workspace.trust.enabled": false,
  217. "task.allowAutomaticTasks": "off",
  218. "telemetry.telemetryLevel": "off",
  219. "update.mode": "none",
  220. "workbench.startupEditor": "none",
  221. "workbench.tips.enabled": false,
  222. "window.commandCenter": false,
  223. },
  224. null,
  225. 2,
  226. ) + "\n",
  227. )
  228. }
  229. // ---------------------------------------------------------------------------
  230. // Launch
  231. // ---------------------------------------------------------------------------
  232. async function launch() {
  233. await compile()
  234. if (clean) {
  235. console.log("[launch] Cleaning previous state...")
  236. rmSync(base, { recursive: true, force: true })
  237. }
  238. mkdirSync(userDir, { recursive: true })
  239. mkdirSync(extDir, { recursive: true })
  240. const app = detect()
  241. settings()
  242. const args = [workspace, `--extensions-dir=${extDir}`, `--user-data-dir=${userDir}`, "--skip-release-notes"]
  243. if (mode === "dev") {
  244. args.push(`--extensionDevelopmentPath=${root}`)
  245. args.push("--disable-extension=kilocode.kilo-code")
  246. }
  247. if (mode === "vsix") {
  248. const out = join(base, "vsix")
  249. mkdirSync(out, { recursive: true })
  250. const vsix = await packageVsix(out)
  251. await installVsix(vsix, app)
  252. console.log(`[launch] Installed VSIX: ${vsix}`)
  253. }
  254. if (blocking) {
  255. args.push("--wait")
  256. }
  257. // Strip Electron/VS Code env vars so the spawned instance doesn't attach
  258. // to the current Electron process (e.g. when launched from a VS Code task).
  259. const env = { ...process.env }
  260. for (const key of Object.keys(env)) {
  261. if (key.startsWith("ELECTRON_") || key.startsWith("VSCODE_")) delete env[key]
  262. }
  263. console.log(`[launch] Starting VS Code (${mode} mode)`)
  264. console.log(`[launch] Executable: ${app}`)
  265. console.log(`[launch] Workspace: ${workspace}`)
  266. console.log(`[launch] State: ${base}`)
  267. if (blocking) {
  268. const result = Bun.spawnSync([app, ...args], {
  269. cwd: workspace,
  270. env,
  271. stdio: ["ignore", "inherit", "inherit"],
  272. })
  273. console.log(`[launch] VS Code exited (code ${result.exitCode})`)
  274. return
  275. }
  276. const child = spawn(app, args, {
  277. cwd: workspace,
  278. detached: !win,
  279. env,
  280. stdio: "ignore",
  281. ...(win ? { shell: true } : {}),
  282. })
  283. child.unref()
  284. console.log(`[launch] VS Code launched (pid ${child.pid})`)
  285. }
  286. try {
  287. await launch()
  288. } catch (err) {
  289. console.error(`[launch] ERROR: ${err instanceof Error ? err.message : String(err)}`)
  290. process.exit(1)
  291. }