dev-cli-watch.mjs 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  1. #!/usr/bin/env node
  2. import { execSync, spawn } from "child_process"
  3. import chokidar from "chokidar"
  4. import path from "path"
  5. import { fileURLToPath } from "url"
  6. const __filename = fileURLToPath(import.meta.url)
  7. const __dirname = path.dirname(__filename)
  8. const projectRoot = path.resolve(__dirname, "..")
  9. // ANSI color codes
  10. const colors = {
  11. reset: "\x1b[0m",
  12. bright: "\x1b[1m",
  13. dim: "\x1b[2m",
  14. green: "\x1b[32m",
  15. yellow: "\x1b[33m",
  16. blue: "\x1b[34m",
  17. red: "\x1b[31m",
  18. cyan: "\x1b[36m",
  19. }
  20. let isBuilding = false
  21. let debounceTimer = null
  22. let esbuildProcess = null
  23. let initialBuildDone = false
  24. console.log(`${colors.bright}${colors.cyan}🚀 Cline CLI Dev Watch Mode (Fast Incremental)${colors.reset}`)
  25. console.log(`${colors.dim}Starting initial build...${colors.reset}\n`)
  26. // Function to kill all CLI instances
  27. function killAllInstances() {
  28. try {
  29. execSync("./cli/bin/cline instance kill --all", {
  30. cwd: projectRoot,
  31. stdio: "pipe",
  32. })
  33. } catch (error) {
  34. // Ignore errors - instances might not be running
  35. }
  36. }
  37. // Function to start a new CLI instance
  38. function startNewInstance() {
  39. try {
  40. console.log(`${colors.blue}▶️ Starting new CLI instance...${colors.reset}`)
  41. const result = execSync("./cli/bin/cline instance new", {
  42. cwd: projectRoot,
  43. stdio: "pipe",
  44. encoding: "utf-8",
  45. })
  46. console.log(`${colors.green}✓ CLI instance started${colors.reset}`)
  47. console.log(`${colors.dim}${result.trim()}${colors.reset}\n`)
  48. } catch (error) {
  49. console.error(`${colors.red}✗ Failed to start instance: ${error.message}${colors.reset}\n`)
  50. }
  51. }
  52. // Function to rebuild Go CLI
  53. async function rebuildGo() {
  54. if (isBuilding) {
  55. return
  56. }
  57. isBuilding = true
  58. const startTime = Date.now()
  59. try {
  60. console.log(`${colors.cyan}🔨 Rebuilding Go CLI...${colors.reset}`)
  61. killAllInstances()
  62. // Just rebuild Go binaries (skip proto generation)
  63. execSync("cd cli && GO111MODULE=on go build -o bin/cline ./cmd/cline", {
  64. cwd: projectRoot,
  65. stdio: "inherit",
  66. shell: true,
  67. })
  68. execSync("cd cli && GO111MODULE=on go build -o bin/cline-host ./cmd/cline-host", {
  69. cwd: projectRoot,
  70. stdio: "inherit",
  71. shell: true,
  72. })
  73. startNewInstance()
  74. const duration = ((Date.now() - startTime) / 1000).toFixed(2)
  75. console.log(`${colors.green}✓ Go rebuild complete in ${duration}s${colors.reset}`)
  76. console.log(`${colors.dim}Watching for changes...${colors.reset}\n`)
  77. } catch (error) {
  78. console.error(`${colors.red}✗ Go build failed: ${error.message}${colors.reset}\n`)
  79. } finally {
  80. isBuilding = false
  81. }
  82. }
  83. // Function to regenerate protos and rebuild everything
  84. async function rebuildProtos() {
  85. if (isBuilding) {
  86. return
  87. }
  88. isBuilding = true
  89. const startTime = Date.now()
  90. try {
  91. console.log(`${colors.cyan}🔨 Regenerating protos...${colors.reset}`)
  92. killAllInstances()
  93. // Regenerate protos
  94. execSync("npm run protos", { cwd: projectRoot, stdio: "inherit" })
  95. execSync("npm run protos-go", { cwd: projectRoot, stdio: "inherit" })
  96. // esbuild will auto-rebuild TS due to changed generated files
  97. // Rebuild Go CLI
  98. execSync("cd cli && GO111MODULE=on go build -o bin/cline ./cmd/cline", {
  99. cwd: projectRoot,
  100. stdio: "inherit",
  101. shell: true,
  102. })
  103. execSync("cd cli && GO111MODULE=on go build -o bin/cline-host ./cmd/cline-host", {
  104. cwd: projectRoot,
  105. stdio: "inherit",
  106. shell: true,
  107. })
  108. startNewInstance()
  109. const duration = ((Date.now() - startTime) / 1000).toFixed(2)
  110. console.log(`${colors.green}✓ Proto rebuild complete in ${duration}s${colors.reset}`)
  111. console.log(`${colors.dim}Watching for changes...${colors.reset}\n`)
  112. } catch (error) {
  113. console.error(`${colors.red}✗ Proto build failed: ${error.message}${colors.reset}\n`)
  114. } finally {
  115. isBuilding = false
  116. }
  117. }
  118. // Debounced rebuild trigger
  119. function triggerGoRebuild(filepath) {
  120. if (debounceTimer) {
  121. clearTimeout(debounceTimer)
  122. }
  123. debounceTimer = setTimeout(() => {
  124. const relativePath = path.relative(projectRoot, filepath)
  125. console.log(`${colors.dim}Go file changed: ${relativePath}${colors.reset}`)
  126. rebuildGo()
  127. }, 300)
  128. }
  129. function triggerProtoRebuild(filepath) {
  130. if (debounceTimer) {
  131. clearTimeout(debounceTimer)
  132. }
  133. debounceTimer = setTimeout(() => {
  134. const relativePath = path.relative(projectRoot, filepath)
  135. console.log(`${colors.dim}Proto file changed: ${relativePath}${colors.reset}`)
  136. rebuildProtos()
  137. }, 300)
  138. }
  139. // Initial build
  140. async function initialBuild() {
  141. try {
  142. // Run protos first
  143. console.log(`${colors.blue}📦 Generating protos...${colors.reset}`)
  144. execSync("npm run protos", { cwd: projectRoot, stdio: "inherit" })
  145. execSync("npm run protos-go", { cwd: projectRoot, stdio: "inherit" })
  146. // Build standalone (skip check-types and lint for speed)
  147. console.log(`${colors.blue}📦 Building standalone...${colors.reset}`)
  148. execSync("node esbuild.mjs --standalone", { cwd: projectRoot, stdio: "inherit" })
  149. // Build Go CLI
  150. console.log(`${colors.blue}🔧 Building Go CLI...${colors.reset}`)
  151. execSync("cd cli && GO111MODULE=on go build -o bin/cline ./cmd/cline", {
  152. cwd: projectRoot,
  153. stdio: "inherit",
  154. shell: true,
  155. })
  156. execSync("cd cli && GO111MODULE=on go build -o bin/cline-host ./cmd/cline-host", {
  157. cwd: projectRoot,
  158. stdio: "inherit",
  159. shell: true,
  160. })
  161. // Start CLI instance
  162. startNewInstance()
  163. console.log(`${colors.green}${colors.bright}✓ Initial build complete!${colors.reset}`)
  164. console.log(`${colors.cyan}Now watching for changes with fast incremental rebuilds...${colors.reset}\n`)
  165. initialBuildDone = true
  166. // Start esbuild in watch mode for TypeScript (incremental rebuilds)
  167. console.log(`${colors.dim}Starting esbuild watch mode...${colors.reset}`)
  168. esbuildProcess = spawn("node", ["esbuild.mjs", "--watch", "--standalone"], {
  169. cwd: projectRoot,
  170. stdio: ["inherit", "pipe", "inherit"], // Pipe stdout to parse it
  171. })
  172. // Parse esbuild output to detect when rebuild completes
  173. esbuildProcess.stdout.on("data", (data) => {
  174. const output = data.toString()
  175. // Forward esbuild output to console
  176. process.stdout.write(output)
  177. // Detect when esbuild finishes a rebuild
  178. if (output.includes("[watch] build finished") && initialBuildDone && !isBuilding) {
  179. console.log(`${colors.cyan}📦 TypeScript rebuilt by esbuild${colors.reset}`)
  180. killAllInstances()
  181. startNewInstance()
  182. }
  183. })
  184. esbuildProcess.on("error", (error) => {
  185. console.error(`${colors.red}esbuild error: ${error.message}${colors.reset}`)
  186. })
  187. } catch (error) {
  188. console.error(`${colors.red}✗ Initial build failed: ${error.message}${colors.reset}`)
  189. process.exit(1)
  190. }
  191. }
  192. // Watch Proto files (chokidar v4 - no glob support, watch directory and filter)
  193. const protoWatcher = chokidar.watch("proto", {
  194. ignored: (filepath, stats) => {
  195. // Ignore if it's a file but not a .proto file
  196. return stats?.isFile() && !filepath.endsWith(".proto")
  197. },
  198. persistent: true,
  199. ignoreInitial: true,
  200. cwd: projectRoot,
  201. awaitWriteFinish: {
  202. stabilityThreshold: 100,
  203. pollInterval: 50,
  204. },
  205. })
  206. protoWatcher
  207. .on("change", (filepath) => {
  208. if (initialBuildDone) {
  209. console.log(`${colors.dim}[DEBUG] Proto change event: ${filepath}${colors.reset}`)
  210. triggerProtoRebuild(path.join(projectRoot, filepath))
  211. }
  212. })
  213. .on("add", (filepath) => {
  214. if (initialBuildDone) {
  215. console.log(`${colors.dim}[DEBUG] Proto add event: ${filepath}${colors.reset}`)
  216. triggerProtoRebuild(path.join(projectRoot, filepath))
  217. }
  218. })
  219. // Watch Go files (chokidar v4 - no glob support, watch directory and filter)
  220. const goWatcher = chokidar.watch("cli", {
  221. ignored: (filepath, stats) => {
  222. // Ignore node_modules and non-.go files
  223. if (filepath.includes("node_modules")) return true
  224. return stats?.isFile() && !filepath.endsWith(".go")
  225. },
  226. persistent: true,
  227. ignoreInitial: true,
  228. cwd: projectRoot,
  229. awaitWriteFinish: {
  230. stabilityThreshold: 100,
  231. pollInterval: 50,
  232. },
  233. })
  234. goWatcher
  235. .on("change", (filepath) => {
  236. if (initialBuildDone) {
  237. console.log(`${colors.dim}[DEBUG] Go change event: ${filepath}${colors.reset}`)
  238. triggerGoRebuild(path.join(projectRoot, filepath))
  239. }
  240. })
  241. .on("add", (filepath) => {
  242. if (initialBuildDone) {
  243. console.log(`${colors.dim}[DEBUG] Go add event: ${filepath}${colors.reset}`)
  244. triggerGoRebuild(path.join(projectRoot, filepath))
  245. }
  246. })
  247. // Handle shutdown gracefully
  248. process.on("SIGINT", () => {
  249. console.log(`\n${colors.yellow}Shutting down...${colors.reset}`)
  250. if (esbuildProcess) {
  251. esbuildProcess.kill()
  252. }
  253. killAllInstances()
  254. process.exit(0)
  255. })
  256. process.on("SIGTERM", () => {
  257. console.log(`\n${colors.yellow}Shutting down...${colors.reset}`)
  258. if (esbuildProcess) {
  259. esbuildProcess.kill()
  260. }
  261. killAllInstances()
  262. process.exit(0)
  263. })
  264. // Start
  265. initialBuild()