@supatype/cli 0.1.0 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (331) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/.turbo/turbo-test.log +121 -86
  3. package/.turbo/turbo-typecheck.log +2 -2
  4. package/dist/binary-cache.d.ts +2 -0
  5. package/dist/binary-cache.d.ts.map +1 -1
  6. package/dist/binary-cache.js +36 -0
  7. package/dist/binary-cache.js.map +1 -1
  8. package/dist/binary-download-lock.d.ts +12 -0
  9. package/dist/binary-download-lock.d.ts.map +1 -0
  10. package/dist/binary-download-lock.js +106 -0
  11. package/dist/binary-download-lock.js.map +1 -0
  12. package/dist/cli-package-version.d.ts +3 -0
  13. package/dist/cli-package-version.d.ts.map +1 -0
  14. package/dist/cli-package-version.js +15 -0
  15. package/dist/cli-package-version.js.map +1 -0
  16. package/dist/cli.d.ts.map +1 -1
  17. package/dist/cli.js +5 -2
  18. package/dist/cli.js.map +1 -1
  19. package/dist/commands/add.js +25 -19
  20. package/dist/commands/add.js.map +1 -1
  21. package/dist/commands/admin.d.ts +28 -1
  22. package/dist/commands/admin.d.ts.map +1 -1
  23. package/dist/commands/admin.js +290 -112
  24. package/dist/commands/admin.js.map +1 -1
  25. package/dist/commands/cache.d.ts +1 -1
  26. package/dist/commands/cache.d.ts.map +1 -1
  27. package/dist/commands/cache.js +98 -5
  28. package/dist/commands/cache.js.map +1 -1
  29. package/dist/commands/dev.d.ts.map +1 -1
  30. package/dist/commands/dev.js +28 -22
  31. package/dist/commands/dev.js.map +1 -1
  32. package/dist/commands/init.d.ts +8 -4
  33. package/dist/commands/init.d.ts.map +1 -1
  34. package/dist/commands/init.js +278 -139
  35. package/dist/commands/init.js.map +1 -1
  36. package/dist/commands/link-helpers.d.ts.map +1 -1
  37. package/dist/commands/link-helpers.js +26 -25
  38. package/dist/commands/link-helpers.js.map +1 -1
  39. package/dist/commands/push.js +3 -3
  40. package/dist/commands/push.js.map +1 -1
  41. package/dist/compose-local-server-image.d.ts +13 -0
  42. package/dist/compose-local-server-image.d.ts.map +1 -0
  43. package/dist/compose-local-server-image.js +57 -0
  44. package/dist/compose-local-server-image.js.map +1 -0
  45. package/dist/compose-rename.d.ts +10 -0
  46. package/dist/compose-rename.d.ts.map +1 -0
  47. package/dist/compose-rename.js +64 -0
  48. package/dist/compose-rename.js.map +1 -0
  49. package/dist/dev-compose.d.ts.map +1 -1
  50. package/dist/dev-compose.js +138 -97
  51. package/dist/dev-compose.js.map +1 -1
  52. package/dist/dev-log-bus.d.ts +5 -0
  53. package/dist/dev-log-bus.d.ts.map +1 -1
  54. package/dist/dev-log-bus.js +14 -0
  55. package/dist/dev-log-bus.js.map +1 -1
  56. package/dist/dev-log-filter.d.ts +4 -0
  57. package/dist/dev-log-filter.d.ts.map +1 -1
  58. package/dist/dev-log-filter.js +52 -0
  59. package/dist/dev-log-filter.js.map +1 -1
  60. package/dist/dev-ports.d.ts +27 -0
  61. package/dist/dev-ports.d.ts.map +1 -0
  62. package/dist/dev-ports.js +178 -0
  63. package/dist/dev-ports.js.map +1 -0
  64. package/dist/dev-ready-panel.d.ts +19 -0
  65. package/dist/dev-ready-panel.d.ts.map +1 -0
  66. package/dist/dev-ready-panel.js +49 -0
  67. package/dist/dev-ready-panel.js.map +1 -0
  68. package/dist/dev-session-lock.d.ts +25 -0
  69. package/dist/dev-session-lock.d.ts.map +1 -0
  70. package/dist/dev-session-lock.js +78 -0
  71. package/dist/dev-session-lock.js.map +1 -0
  72. package/dist/dev-session.d.ts +18 -5
  73. package/dist/dev-session.d.ts.map +1 -1
  74. package/dist/dev-session.js +91 -18
  75. package/dist/dev-session.js.map +1 -1
  76. package/dist/dev-shutdown.d.ts +20 -2
  77. package/dist/dev-shutdown.d.ts.map +1 -1
  78. package/dist/dev-shutdown.js +88 -10
  79. package/dist/dev-shutdown.js.map +1 -1
  80. package/dist/dev-task-colors.d.ts +3 -1
  81. package/dist/dev-task-colors.d.ts.map +1 -1
  82. package/dist/dev-task-colors.js +23 -3
  83. package/dist/dev-task-colors.js.map +1 -1
  84. package/dist/docker-runtime.js +1 -1
  85. package/dist/docker-runtime.js.map +1 -1
  86. package/dist/engine-push-output.d.ts +1 -0
  87. package/dist/engine-push-output.d.ts.map +1 -1
  88. package/dist/engine-push-output.js +4 -1
  89. package/dist/engine-push-output.js.map +1 -1
  90. package/dist/ensure-binary.d.ts.map +1 -1
  91. package/dist/ensure-binary.js +14 -2
  92. package/dist/ensure-binary.js.map +1 -1
  93. package/dist/ensure-component-binaries.d.ts +17 -0
  94. package/dist/ensure-component-binaries.d.ts.map +1 -0
  95. package/dist/ensure-component-binaries.js +45 -0
  96. package/dist/ensure-component-binaries.js.map +1 -0
  97. package/dist/ensure-dev-api-config.d.ts +3 -0
  98. package/dist/ensure-dev-api-config.d.ts.map +1 -0
  99. package/dist/ensure-dev-api-config.js +17 -0
  100. package/dist/ensure-dev-api-config.js.map +1 -0
  101. package/dist/env-file.d.ts +5 -0
  102. package/dist/env-file.d.ts.map +1 -0
  103. package/dist/env-file.js +33 -0
  104. package/dist/env-file.js.map +1 -0
  105. package/dist/init-dependency-versions.d.ts +13 -0
  106. package/dist/init-dependency-versions.d.ts.map +1 -0
  107. package/dist/init-dependency-versions.js +53 -0
  108. package/dist/init-dependency-versions.js.map +1 -0
  109. package/dist/init-package-json.d.ts +21 -0
  110. package/dist/init-package-json.d.ts.map +1 -0
  111. package/dist/init-package-json.js +46 -0
  112. package/dist/init-package-json.js.map +1 -0
  113. package/dist/postgres-ctl.d.ts.map +1 -1
  114. package/dist/postgres-ctl.js +2 -1
  115. package/dist/postgres-ctl.js.map +1 -1
  116. package/dist/required-host-components.d.ts +11 -0
  117. package/dist/required-host-components.d.ts.map +1 -0
  118. package/dist/required-host-components.js +25 -0
  119. package/dist/required-host-components.js.map +1 -0
  120. package/dist/resolve-api-url.d.ts +6 -0
  121. package/dist/resolve-api-url.d.ts.map +1 -0
  122. package/dist/resolve-api-url.js +39 -0
  123. package/dist/resolve-api-url.js.map +1 -0
  124. package/dist/rest-cache-admin.d.ts +33 -0
  125. package/dist/rest-cache-admin.d.ts.map +1 -0
  126. package/dist/rest-cache-admin.js +96 -0
  127. package/dist/rest-cache-admin.js.map +1 -0
  128. package/dist/scripts/postinstall.d.ts +4 -6
  129. package/dist/scripts/postinstall.d.ts.map +1 -1
  130. package/dist/scripts/postinstall.js +8 -41
  131. package/dist/scripts/postinstall.js.map +1 -1
  132. package/dist/self-host-compose.d.ts.map +1 -1
  133. package/dist/self-host-compose.js +24 -13
  134. package/dist/self-host-compose.js.map +1 -1
  135. package/dist/studio-dev-server.d.ts +4 -2
  136. package/dist/studio-dev-server.d.ts.map +1 -1
  137. package/dist/studio-dev-server.js +3 -2
  138. package/dist/studio-dev-server.js.map +1 -1
  139. package/dist/ui/brand.d.ts +2 -5
  140. package/dist/ui/brand.d.ts.map +1 -1
  141. package/dist/ui/brand.js +2 -7
  142. package/dist/ui/brand.js.map +1 -1
  143. package/dist/ui/clack-api.d.ts +44 -0
  144. package/dist/ui/clack-api.d.ts.map +1 -0
  145. package/dist/ui/clack-api.js +76 -0
  146. package/dist/ui/clack-api.js.map +1 -0
  147. package/dist/ui/clack.d.ts +14 -0
  148. package/dist/ui/clack.d.ts.map +1 -0
  149. package/dist/ui/clack.js +147 -0
  150. package/dist/ui/clack.js.map +1 -0
  151. package/dist/ui/components/FlowSpinner.d.ts +5 -0
  152. package/dist/ui/components/FlowSpinner.d.ts.map +1 -0
  153. package/dist/ui/components/FlowSpinner.js +8 -0
  154. package/dist/ui/components/FlowSpinner.js.map +1 -0
  155. package/dist/ui/components/LogoWordmark.d.ts +9 -0
  156. package/dist/ui/components/LogoWordmark.d.ts.map +1 -0
  157. package/dist/ui/components/LogoWordmark.js +17 -0
  158. package/dist/ui/components/LogoWordmark.js.map +1 -0
  159. package/dist/ui/components/PromptPanel.d.ts +5 -0
  160. package/dist/ui/components/PromptPanel.d.ts.map +1 -0
  161. package/dist/ui/components/PromptPanel.js +7 -0
  162. package/dist/ui/components/PromptPanel.js.map +1 -0
  163. package/dist/ui/components/SelectField.d.ts +11 -0
  164. package/dist/ui/components/SelectField.d.ts.map +1 -0
  165. package/dist/ui/components/SelectField.js +19 -0
  166. package/dist/ui/components/SelectField.js.map +1 -0
  167. package/dist/ui/confirm.d.ts +2 -1
  168. package/dist/ui/confirm.d.ts.map +1 -1
  169. package/dist/ui/confirm.js +4 -4
  170. package/dist/ui/confirm.js.map +1 -1
  171. package/dist/ui/dev/DevDashboard.d.ts +8 -0
  172. package/dist/ui/dev/DevDashboard.d.ts.map +1 -0
  173. package/dist/ui/dev/DevDashboard.js +151 -0
  174. package/dist/ui/dev/DevDashboard.js.map +1 -0
  175. package/dist/ui/dev/DevReadyPanelView.d.ts +7 -0
  176. package/dist/ui/dev/DevReadyPanelView.d.ts.map +1 -0
  177. package/dist/ui/dev/DevReadyPanelView.js +29 -0
  178. package/dist/ui/dev/DevReadyPanelView.js.map +1 -0
  179. package/dist/ui/fatal.d.ts.map +1 -1
  180. package/dist/ui/fatal.js +5 -11
  181. package/dist/ui/fatal.js.map +1 -1
  182. package/dist/ui/flows/FlowApp.d.ts +8 -0
  183. package/dist/ui/flows/FlowApp.d.ts.map +1 -0
  184. package/dist/ui/flows/FlowApp.js +42 -0
  185. package/dist/ui/flows/FlowApp.js.map +1 -0
  186. package/dist/ui/flows/prompt-fields.d.ts +48 -0
  187. package/dist/ui/flows/prompt-fields.d.ts.map +1 -0
  188. package/dist/ui/flows/prompt-fields.js +88 -0
  189. package/dist/ui/flows/prompt-fields.js.map +1 -0
  190. package/dist/ui/flows/types.d.ts +41 -0
  191. package/dist/ui/flows/types.d.ts.map +1 -0
  192. package/dist/ui/flows/types.js +2 -0
  193. package/dist/ui/flows/types.js.map +1 -0
  194. package/dist/ui/index.d.ts +1 -1
  195. package/dist/ui/index.js +1 -1
  196. package/dist/ui/messages.d.ts +1 -0
  197. package/dist/ui/messages.d.ts.map +1 -1
  198. package/dist/ui/messages.js +48 -8
  199. package/dist/ui/messages.js.map +1 -1
  200. package/dist/ui/progress.d.ts +5 -1
  201. package/dist/ui/progress.d.ts.map +1 -1
  202. package/dist/ui/progress.js +12 -3
  203. package/dist/ui/progress.js.map +1 -1
  204. package/dist/ui/prompts.d.ts +7 -5
  205. package/dist/ui/prompts.d.ts.map +1 -1
  206. package/dist/ui/prompts.js +15 -8
  207. package/dist/ui/prompts.js.map +1 -1
  208. package/dist/ui/runtime/cancel.d.ts +4 -0
  209. package/dist/ui/runtime/cancel.d.ts.map +1 -0
  210. package/dist/ui/runtime/cancel.js +6 -0
  211. package/dist/ui/runtime/cancel.js.map +1 -0
  212. package/dist/ui/runtime/command-chrome.d.ts +17 -0
  213. package/dist/ui/runtime/command-chrome.d.ts.map +1 -0
  214. package/dist/ui/runtime/command-chrome.js +123 -0
  215. package/dist/ui/runtime/command-chrome.js.map +1 -0
  216. package/dist/ui/runtime/dev-prompt-queue.d.ts +30 -0
  217. package/dist/ui/runtime/dev-prompt-queue.d.ts.map +1 -0
  218. package/dist/ui/runtime/dev-prompt-queue.js +57 -0
  219. package/dist/ui/runtime/dev-prompt-queue.js.map +1 -0
  220. package/dist/ui/runtime/flow-session.d.ts +4 -0
  221. package/dist/ui/runtime/flow-session.d.ts.map +1 -0
  222. package/dist/ui/runtime/flow-session.js +8 -0
  223. package/dist/ui/runtime/flow-session.js.map +1 -0
  224. package/dist/ui/runtime/run-clack-flow.d.ts +5 -0
  225. package/dist/ui/runtime/run-clack-flow.d.ts.map +1 -0
  226. package/dist/ui/runtime/run-clack-flow.js +34 -0
  227. package/dist/ui/runtime/run-clack-flow.js.map +1 -0
  228. package/dist/ui/theme.d.ts +24 -0
  229. package/dist/ui/theme.d.ts.map +1 -0
  230. package/dist/ui/theme.js +34 -0
  231. package/dist/ui/theme.js.map +1 -0
  232. package/dist/valkey-sidecar.d.ts +17 -0
  233. package/dist/valkey-sidecar.d.ts.map +1 -0
  234. package/dist/valkey-sidecar.js +94 -0
  235. package/dist/valkey-sidecar.js.map +1 -0
  236. package/package.json +11 -3
  237. package/src/binary-cache.ts +61 -0
  238. package/src/binary-download-lock.ts +124 -0
  239. package/src/cli-package-version.ts +15 -0
  240. package/src/cli.ts +6 -2
  241. package/src/commands/add.ts +26 -19
  242. package/src/commands/admin.ts +380 -137
  243. package/src/commands/cache.ts +105 -5
  244. package/src/commands/dev.ts +29 -21
  245. package/src/commands/init.ts +217 -46
  246. package/src/commands/link-helpers.ts +30 -29
  247. package/src/commands/push.ts +3 -3
  248. package/src/compose-local-server-image.ts +68 -0
  249. package/src/compose-rename.ts +73 -0
  250. package/src/dev-compose.ts +175 -96
  251. package/src/dev-log-bus.ts +18 -0
  252. package/src/dev-log-filter.ts +42 -0
  253. package/src/dev-ports.ts +223 -0
  254. package/src/dev-ready-panel.ts +63 -0
  255. package/src/dev-session-lock.ts +98 -0
  256. package/src/dev-session.ts +98 -18
  257. package/src/dev-shutdown.ts +114 -9
  258. package/src/dev-task-colors.ts +22 -3
  259. package/src/docker-runtime.ts +1 -1
  260. package/src/engine-push-output.ts +5 -1
  261. package/src/ensure-binary.ts +21 -1
  262. package/src/ensure-component-binaries.ts +71 -0
  263. package/src/ensure-dev-api-config.ts +18 -0
  264. package/src/env-file.ts +37 -0
  265. package/src/init-dependency-versions.ts +69 -0
  266. package/src/init-package-json.ts +63 -0
  267. package/src/postgres-ctl.ts +2 -1
  268. package/src/required-host-components.ts +37 -0
  269. package/src/resolve-api-url.ts +43 -0
  270. package/src/rest-cache-admin.ts +135 -0
  271. package/src/scripts/postinstall.ts +9 -46
  272. package/src/self-host-compose.ts +23 -12
  273. package/src/studio-dev-server.ts +5 -4
  274. package/src/ui/README.md +13 -9
  275. package/src/ui/brand.ts +2 -8
  276. package/src/ui/clack-api.ts +123 -0
  277. package/src/ui/clack.ts +153 -0
  278. package/src/ui/components/FlowSpinner.tsx +15 -0
  279. package/src/ui/components/LogoWordmark.tsx +36 -0
  280. package/src/ui/components/PromptPanel.tsx +17 -0
  281. package/src/ui/components/SelectField.tsx +58 -0
  282. package/src/ui/confirm.ts +5 -4
  283. package/src/ui/dev/DevDashboard.tsx +213 -0
  284. package/src/ui/dev/DevReadyPanelView.tsx +67 -0
  285. package/src/ui/fatal.ts +5 -10
  286. package/src/ui/flows/FlowApp.tsx +67 -0
  287. package/src/ui/flows/prompt-fields.tsx +192 -0
  288. package/src/ui/flows/types.ts +50 -0
  289. package/src/ui/index.ts +1 -1
  290. package/src/ui/messages.ts +50 -8
  291. package/src/ui/progress.ts +14 -3
  292. package/src/ui/prompts.ts +17 -9
  293. package/src/ui/runtime/cancel.ts +6 -0
  294. package/src/ui/runtime/command-chrome.ts +147 -0
  295. package/src/ui/runtime/dev-prompt-queue.ts +84 -0
  296. package/src/ui/runtime/flow-session.ts +11 -0
  297. package/src/ui/runtime/run-clack-flow.tsx +52 -0
  298. package/src/ui/theme.ts +41 -0
  299. package/src/valkey-sidecar.ts +126 -0
  300. package/tests/admin-ensure.test.ts +59 -0
  301. package/tests/binary-cache-cloud-overrides.test.ts +2 -2
  302. package/tests/binary-download-lock.test.ts +41 -0
  303. package/tests/cli-help.test.ts +7 -4
  304. package/tests/cli-package-version.test.ts +15 -0
  305. package/tests/command-chrome-coverage.test.ts +149 -0
  306. package/tests/command-chrome-wrap.test.ts +51 -0
  307. package/tests/config.test.ts +8 -4
  308. package/tests/dev-ports.test.ts +41 -0
  309. package/tests/dev-ready-panel.test.ts +20 -0
  310. package/tests/dev-session-lock.test.ts +54 -0
  311. package/tests/dev-ui.test.ts +66 -3
  312. package/tests/docker-runtime.test.ts +9 -20
  313. package/tests/ensure-dev-api-config.test.ts +22 -0
  314. package/tests/init-dependency-versions.test.ts +62 -0
  315. package/tests/init-package-json.test.ts +18 -0
  316. package/tests/init.test.ts +30 -3
  317. package/tests/required-host-components.test.ts +51 -0
  318. package/tests/resolve-api-url.test.ts +23 -0
  319. package/tests/rest-cache-admin.test.ts +28 -0
  320. package/tests/runtime-contract.test.ts +12 -3
  321. package/tests/tsconfig.json +8 -2
  322. package/tests/ui-confirm.test.ts +13 -9
  323. package/tests/ui-messages.test.ts +4 -18
  324. package/tsconfig.json +1 -0
  325. package/tsconfig.tsbuildinfo +1 -1
  326. package/vitest.config.ts +3 -0
  327. package/dist/dev-tui.d.ts +0 -24
  328. package/dist/dev-tui.d.ts.map +0 -1
  329. package/dist/dev-tui.js +0 -188
  330. package/dist/dev-tui.js.map +0 -1
  331. package/src/dev-tui.ts +0 -232
@@ -1,28 +1,97 @@
1
1
  /**
2
- * Graceful shutdown for `supatype dev` — single path from SIGINT and TUI Ctrl+C.
2
+ * Graceful shutdown for `supatype dev` — SIGINT, TUI Ctrl+C, terminal close,
3
+ * and a synchronous compose-down fallback on process exit.
3
4
  */
4
5
 
6
+ import { existsSync } from "node:fs"
5
7
  import { endDevSession } from "./dev-session.js"
8
+ import { clearDevSessionLock } from "./dev-session-lock.js"
9
+ import { runDockerCompose } from "./self-host-compose.js"
10
+
11
+ export interface DevComposeShutdownFallback {
12
+ cwd: string
13
+ composePath: string
14
+ composeProject: string
15
+ }
6
16
 
7
17
  let shutdownWork: (() => Promise<void>) | null = null
18
+ let composeFallback: DevComposeShutdownFallback | null = null
19
+ let shutdownCwd: string | null = null
8
20
  let shuttingDown = false
9
- let signalsRegistered = false
21
+ let shutdownCompleted = false
22
+ let forceQuitRequested = false
23
+ let hooksRegistered = false
24
+ let ignoreSigintUntil = 0
25
+
26
+ /** SIGINT/SIGTERM hooks — call as soon as `supatype dev` starts (before compose is up). */
27
+ export function ensureDevShutdownHooks(): void {
28
+ if (hooksRegistered) return
29
+ hooksRegistered = true
30
+
31
+ process.on("SIGINT", onSignal)
32
+ process.on("SIGTERM", onSignal)
33
+ if (process.platform === "win32") {
34
+ process.on("SIGBREAK", onSignal)
35
+ }
36
+ process.on("exit", onProcessExit)
37
+
38
+ if (process.stdin.isTTY) {
39
+ process.stdin.on("end", onStdinClose)
40
+ process.stdin.on("close", onStdinClose)
41
+ }
42
+ }
10
43
 
11
44
  function onSignal(): void {
45
+ if (Date.now() < ignoreSigintUntil) return
12
46
  void runDevShutdown()
13
47
  }
14
48
 
49
+ function onStdinClose(): void {
50
+ if (!process.stdin.isTTY) return
51
+ void runDevShutdown()
52
+ }
53
+
54
+ function syncComposeDownFallback(): void {
55
+ if (shutdownCompleted || !composeFallback) return
56
+ try {
57
+ runDockerCompose(
58
+ composeFallback.composePath,
59
+ ["down"],
60
+ composeFallback.cwd,
61
+ composeFallback.composeProject,
62
+ { quiet: true },
63
+ )
64
+ } catch {
65
+ // best-effort — process is exiting
66
+ }
67
+ if (shutdownCwd) clearDevSessionLock(shutdownCwd)
68
+ }
69
+
70
+ function onProcessExit(): void {
71
+ syncComposeDownFallback()
72
+ }
73
+
74
+ export interface RegisterDevShutdownOptions {
75
+ /** Sync `docker compose down` when async teardown cannot finish (terminal close, kill). */
76
+ compose?: DevComposeShutdownFallback
77
+ /** Project root — clears `.supatype/dev-session.json` after shutdown. */
78
+ cwd?: string
79
+ }
80
+
15
81
  /** Register async teardown (stop children, compose down, etc.). Call once per dev session. */
16
- export function registerDevShutdown(work: () => Promise<void>): void {
82
+ export function registerDevShutdown(
83
+ work: () => Promise<void>,
84
+ opts?: RegisterDevShutdownOptions,
85
+ ): void {
17
86
  shutdownWork = work
18
- if (signalsRegistered) return
19
- signalsRegistered = true
20
- process.on("SIGINT", onSignal)
21
- process.on("SIGTERM", onSignal)
87
+ composeFallback = opts?.compose ?? null
88
+ shutdownCwd = opts?.cwd ?? opts?.compose?.cwd ?? null
89
+ ensureDevShutdownHooks()
22
90
  }
23
91
 
24
92
  /** TUI Ctrl+C — do not re-emit SIGINT (avoids double-fire on Windows raw mode). */
25
93
  export function requestDevShutdown(): void {
94
+ ignoreSigintUntil = Date.now() + 400
26
95
  void runDevShutdown()
27
96
  }
28
97
 
@@ -30,13 +99,33 @@ export function isDevShuttingDown(): boolean {
30
99
  return shuttingDown
31
100
  }
32
101
 
102
+ /** @internal Tests — reset module state between cases. */
103
+ export function resetDevShutdownForTests(): void {
104
+ shutdownWork = null
105
+ composeFallback = null
106
+ shutdownCwd = null
107
+ shuttingDown = false
108
+ shutdownCompleted = false
109
+ forceQuitRequested = false
110
+ hooksRegistered = false
111
+ ignoreSigintUntil = 0
112
+ }
113
+
33
114
  async function runDevShutdown(): Promise<void> {
34
115
  if (shuttingDown) {
116
+ if (!forceQuitRequested) {
117
+ forceQuitRequested = true
118
+ process.stderr.write(
119
+ "\n[supatype] Still shutting down (stopping Docker)… press Ctrl+C again to force quit.\n",
120
+ )
121
+ return
122
+ }
35
123
  try {
36
124
  endDevSession()
37
125
  } catch {
38
126
  // best-effort terminal restore
39
127
  }
128
+ process.stderr.write("\n[supatype] Forced quit — Docker containers may still be running.\n")
40
129
  process.stdout.write("\n")
41
130
  process.exit(130)
42
131
  }
@@ -44,11 +133,27 @@ async function runDevShutdown(): Promise<void> {
44
133
 
45
134
  try {
46
135
  endDevSession()
47
- process.stdout.write("\n")
48
- await shutdownWork?.()
136
+ process.stderr.write("\n[supatype] Shutting down…\n")
137
+ if (!shutdownWork) {
138
+ if (shutdownCwd) clearDevSessionLock(shutdownCwd)
139
+ process.exit(130)
140
+ }
141
+ await shutdownWork()
142
+ shutdownCompleted = true
143
+ if (shutdownCwd) clearDevSessionLock(shutdownCwd)
49
144
  process.exit(0)
50
145
  } catch (err) {
51
146
  process.stderr.write(`[supatype] Shutdown failed: ${(err as Error).message}\n`)
147
+ process.stderr.write(
148
+ "[supatype] Docker containers may still be running — try: supatype self-host compose down\n",
149
+ )
150
+ syncComposeDownFallback()
151
+ shutdownCompleted = true
52
152
  process.exit(1)
53
153
  }
54
154
  }
155
+
156
+ /** Whether a compose fallback was registered (tests). */
157
+ export function hasComposeShutdownFallback(): boolean {
158
+ return composeFallback !== null && existsSync(composeFallback.composePath)
159
+ }
@@ -41,7 +41,26 @@ export function colorizeLogLine(taskId: string, line: string): string {
41
41
  return `${base}${line}${RESET}`
42
42
  }
43
43
 
44
- export function colorizeTaskLabel(taskId: string, label: string, focused: boolean): string {
45
- const style = focused ? BOLD + taskColor(taskId) : taskColor(taskId)
46
- return `${style}${label}${RESET}`
44
+ /** Ink colour names for React components (parallel to ANSI taskColor). */
45
+ export function taskInkColor(taskId: string): string {
46
+ switch (taskId) {
47
+ case "stack":
48
+ return "magenta"
49
+ case "app":
50
+ return "green"
51
+ case "studio":
52
+ return "cyan"
53
+ case "server":
54
+ return "green"
55
+ case "postgrest":
56
+ return "blue"
57
+ default:
58
+ return "white"
59
+ }
60
+ }
61
+
62
+ export function logInkColor(taskId: string, line: string): string {
63
+ if (line.startsWith("✗ ")) return "red"
64
+ if (line.startsWith("⚠ ")) return "yellow"
65
+ return taskInkColor(taskId)
47
66
  }
@@ -7,7 +7,7 @@ import { platform } from "node:os"
7
7
  import { endDevSession, getActiveDevSession } from "./dev-session.js"
8
8
  import { isInteractive } from "./ui/interactive.js"
9
9
  import { error, plain } from "./ui/messages.js"
10
- import { clack as p, printLogo } from "./ui/prompts.js"
10
+ import { p, printLogo } from "./ui/prompts.js"
11
11
 
12
12
  export type DockerDaemonProbe =
13
13
  | { ok: true }
@@ -59,6 +59,10 @@ export function formatEnginePushMessage(result: EnginePushResult): string {
59
59
  const COMPOSE_PROGRESS_LINE =
60
60
  /^Container\s+.+\s+(Running|Waiting|Healthy|Created|Starting|Started|Exited|Stopped)\s*$/i
61
61
 
62
+ export function isComposeProgressLine(line: string): boolean {
63
+ return COMPOSE_PROGRESS_LINE.test(line.trim())
64
+ }
65
+
62
66
  /** Drop docker compose progress noise; keep engine JSON and real errors. */
63
67
  export function filterComposeNoise(output: string): string {
64
68
  return output
@@ -67,7 +71,7 @@ export function filterComposeNoise(output: string): string {
67
71
  .filter((line) => {
68
72
  const t = line.trim()
69
73
  if (!t) return false
70
- if (COMPOSE_PROGRESS_LINE.test(t)) return false
74
+ if (isComposeProgressLine(t)) return false
71
75
  return true
72
76
  })
73
77
  .join("\n")
@@ -7,8 +7,10 @@ import {
7
7
  download,
8
8
  currentPlatform,
9
9
  resolveVersionFor,
10
+ isCachedBinaryReady,
10
11
  type Component,
11
12
  } from "./binary-cache.js"
13
+ import { isDownloadInProgress, waitForComponentDownload } from "./binary-download-lock.js"
12
14
  import type { SupatypeProjectConfig } from "./project-config.js"
13
15
 
14
16
  export async function ensureBinary(
@@ -24,5 +26,23 @@ export async function ensureBinary(
24
26
  }
25
27
  }
26
28
 
27
- return download(component, await resolveVersionFor(component, config), currentPlatform())
29
+ const version = await resolveVersionFor(component, config)
30
+ const platform = currentPlatform()
31
+
32
+ if (isDownloadInProgress(component, version)) {
33
+ console.log(`[supatype] Waiting for in-progress ${component} download...`)
34
+ const outcome = await waitForComponentDownload(
35
+ component,
36
+ version,
37
+ () => isCachedBinaryReady(component, version, platform),
38
+ (c) => {
39
+ console.log(`[supatype] Still waiting for ${c}...`)
40
+ },
41
+ )
42
+ if (outcome === "ready") {
43
+ return resolveBinary(component, config)
44
+ }
45
+ }
46
+
47
+ return download(component, version, platform)
28
48
  }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Ensure CDN component binaries are cached when the host CLI needs them.
3
+ */
4
+
5
+ import { ensureBinary } from "./ensure-binary.js"
6
+ import type { Component } from "./components.js"
7
+ import type { SupatypeProjectConfig } from "./project-config.js"
8
+ import {
9
+ describeRequiredHostComponents,
10
+ requiredHostComponents,
11
+ } from "./required-host-components.js"
12
+ import { resolveRuntimeProvider } from "./project-config.js"
13
+ import { error, info, warn } from "./ui/messages.js"
14
+
15
+ export interface ComponentBinaryFailure {
16
+ component: Component
17
+ message: string
18
+ }
19
+
20
+ export interface EnsureComponentBinariesResult {
21
+ ok: boolean
22
+ failures: ComponentBinaryFailure[]
23
+ skipped: boolean
24
+ }
25
+
26
+ export async function ensureComponentBinaries(
27
+ config: SupatypeProjectConfig,
28
+ cwd: string = process.cwd(),
29
+ ): Promise<EnsureComponentBinariesResult> {
30
+ const components = requiredHostComponents(config, cwd)
31
+ const provider = resolveRuntimeProvider(config)
32
+
33
+ if (components.length === 0) {
34
+ return { ok: true, failures: [], skipped: true }
35
+ }
36
+
37
+ const list = describeRequiredHostComponents(components)
38
+ if (provider === "docker") {
39
+ info(
40
+ `Preparing host CLI tools (${list}) — Compose still runs Postgres and the API stack.`,
41
+ )
42
+ } else {
43
+ info(`Preparing local runtime components (${list})…`)
44
+ }
45
+
46
+ const failures: ComponentBinaryFailure[] = []
47
+
48
+ for (const component of components) {
49
+ try {
50
+ await ensureBinary(component, config)
51
+ info(`${component} ready.`)
52
+ } catch (err) {
53
+ const message = err instanceof Error ? err.message : String(err)
54
+ failures.push({ component, message })
55
+ error(`Could not prepare ${component}: ${message}`)
56
+ }
57
+ }
58
+
59
+ return { ok: failures.length === 0, failures, skipped: false }
60
+ }
61
+
62
+ export function reportComponentBinaryFailures(failures: ComponentBinaryFailure[]): void {
63
+ if (failures.length === 0) return
64
+
65
+ warn("Some component binaries are still missing.")
66
+ for (const { component, message } of failures) {
67
+ warn(` ${component}: ${message}`)
68
+ }
69
+ warn("Run manually from your project directory: supatype update")
70
+ warn("If downloads keep failing, check your network or try again in a few minutes.")
71
+ }
@@ -0,0 +1,18 @@
1
+ import { copyFileSync, existsSync, mkdirSync } from "node:fs"
2
+ import { dirname, join } from "node:path"
3
+
4
+ const TEMPLATE_REL = join("config", "api-config.json")
5
+ const TARGET_REL = join(".supatype", "api-config.json")
6
+
7
+ /** Copy committed `config/api-config.json` when `.supatype/api-config.json` is missing. */
8
+ export function ensureDevApiConfig(cwd: string): boolean {
9
+ const target = join(cwd, TARGET_REL)
10
+ if (existsSync(target)) return false
11
+
12
+ const template = join(cwd, TEMPLATE_REL)
13
+ if (!existsSync(template)) return false
14
+
15
+ mkdirSync(dirname(target), { recursive: true })
16
+ copyFileSync(template, target)
17
+ return true
18
+ }
@@ -0,0 +1,37 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs"
2
+ import { join } from "node:path"
3
+
4
+ /** Merge key/value updates into a project `.env` without dropping unrelated lines. */
5
+ export function upsertEnvFile(
6
+ cwd: string,
7
+ updates: Record<string, string>,
8
+ removeKeys: readonly string[] = [],
9
+ ): void {
10
+ const envPath = join(cwd, ".env")
11
+ const existing = existsSync(envPath) ? readFileSync(envPath, "utf8") : ""
12
+ const keys = new Set([...Object.keys(updates), ...removeKeys])
13
+ const kept = existing
14
+ .split("\n")
15
+ .filter((line) => {
16
+ const key = line.split("=")[0]?.trim()
17
+ return key && line.includes("=") && !keys.has(key)
18
+ })
19
+ const merged = [...kept, ...Object.entries(updates).map(([key, value]) => `${key}=${value}`)]
20
+ writeFileSync(envPath, `${merged.join("\n").trimEnd()}\n`, "utf8")
21
+ }
22
+
23
+ export function readEnvValue(cwd: string, key: string, fallback: string): string {
24
+ const envPath = join(cwd, ".env")
25
+ if (existsSync(envPath)) {
26
+ const m = readFileSync(envPath, "utf8").match(new RegExp(`^${key}=(.+)$`, "m"))
27
+ if (m?.[1]) return m[1].trim()
28
+ }
29
+ return fallback
30
+ }
31
+
32
+ export function readEnvInt(cwd: string, key: string): number | null {
33
+ const raw = readEnvValue(cwd, key, "")
34
+ if (!raw) return null
35
+ const port = Number(raw)
36
+ return Number.isInteger(port) && port > 0 && port <= 65535 ? port : null
37
+ }
@@ -0,0 +1,69 @@
1
+ import { cliPackageVersion } from "./cli-package-version.js"
2
+
3
+ export interface InitDependencyVersions {
4
+ cli: string
5
+ types: string
6
+ }
7
+
8
+ type NpmRegistryPackage = {
9
+ versions?: Record<string, unknown>
10
+ "dist-tags"?: { latest?: string }
11
+ }
12
+
13
+ /** Fallback when the registry is unreachable (offline / tests). */
14
+ export function initDependencyVersionsFallback(): InitDependencyVersions {
15
+ const version = cliPackageVersion()
16
+ return { cli: version, types: version }
17
+ }
18
+
19
+ async function fetchNpmPackage(name: string): Promise<NpmRegistryPackage | null> {
20
+ const url = `https://registry.npmjs.org/${encodeURIComponent(name)}`
21
+ try {
22
+ const resp = await fetch(url, { signal: AbortSignal.timeout(10_000) })
23
+ if (!resp.ok) return null
24
+ return (await resp.json()) as NpmRegistryPackage
25
+ } catch {
26
+ return null
27
+ }
28
+ }
29
+
30
+ function versionPublishedOnNpm(meta: NpmRegistryPackage | null, version: string): boolean {
31
+ return Boolean(meta?.versions?.[version])
32
+ }
33
+
34
+ function latestPublishedOnNpm(meta: NpmRegistryPackage | null): string | null {
35
+ const latest = meta?.["dist-tags"]?.latest
36
+ return typeof latest === "string" && latest.trim() !== "" ? latest.trim() : null
37
+ }
38
+
39
+ function resolvePackageVersion(
40
+ meta: NpmRegistryPackage | null,
41
+ preferred: string,
42
+ ): string {
43
+ if (versionPublishedOnNpm(meta, preferred)) return preferred
44
+ const latest = latestPublishedOnNpm(meta)
45
+ if (latest) return latest
46
+ return preferred
47
+ }
48
+
49
+ /**
50
+ * Pick @supatype/cli and @supatype/types versions that exist on npm.
51
+ * The running CLI may be a local dev build ahead of the registry (e.g. 0.1.3
52
+ * while types is only published at 0.1.1).
53
+ */
54
+ export async function resolveInitDependencyVersions(): Promise<InitDependencyVersions> {
55
+ const runningCli = cliPackageVersion()
56
+ const [cliMeta, typesMeta] = await Promise.all([
57
+ fetchNpmPackage("@supatype/cli"),
58
+ fetchNpmPackage("@supatype/types"),
59
+ ])
60
+
61
+ if (!cliMeta && !typesMeta) {
62
+ return initDependencyVersionsFallback()
63
+ }
64
+
65
+ return {
66
+ cli: resolvePackageVersion(cliMeta, runningCli),
67
+ types: resolvePackageVersion(typesMeta, runningCli),
68
+ }
69
+ }
@@ -0,0 +1,63 @@
1
+ import type { InitDependencyVersions } from "./init-dependency-versions.js"
2
+
3
+ export interface MergePackageJsonOptions {
4
+ projectName: string
5
+ app: { viteDevUrl?: string; mode: string }
6
+ helloFunction: boolean
7
+ }
8
+
9
+ type PackageJson = {
10
+ name?: string
11
+ private?: boolean
12
+ type?: string
13
+ scripts?: Record<string, string>
14
+ dependencies?: Record<string, string>
15
+ devDependencies?: Record<string, string>
16
+ }
17
+
18
+ const CORE_SCRIPTS = {
19
+ dev: "supatype dev",
20
+ push: "supatype push",
21
+ seed: "tsx seed.ts",
22
+ } as const
23
+
24
+ /** Merge Supatype deps/scripts into an existing package.json object. */
25
+ export function mergeSupatypePackageJson(
26
+ pkg: PackageJson,
27
+ opts: MergePackageJsonOptions,
28
+ deps: InitDependencyVersions,
29
+ ): PackageJson {
30
+ const next: PackageJson = { ...pkg }
31
+
32
+ if (!next.name) next.name = opts.projectName
33
+ if (next.private === undefined) next.private = true
34
+ if (!next.type) next.type = "module"
35
+
36
+ const scripts = { ...(next.scripts ?? {}) }
37
+ for (const [key, value] of Object.entries(CORE_SCRIPTS)) {
38
+ if (scripts[key] === undefined) scripts[key] = value
39
+ }
40
+ if (scripts.dev !== CORE_SCRIPTS.dev && scripts["supatype:dev"] === undefined) {
41
+ scripts["supatype:dev"] = CORE_SCRIPTS.dev
42
+ }
43
+ if (opts.app.viteDevUrl && scripts.vite === undefined) {
44
+ scripts.vite = "vite"
45
+ }
46
+ if (opts.helloFunction && scripts.functions === undefined) {
47
+ scripts.functions = "supatype functions serve"
48
+ }
49
+ next.scripts = scripts
50
+
51
+ const dependencies = { ...(next.dependencies ?? {}) }
52
+ if (dependencies["@supatype/cli"] === undefined) dependencies["@supatype/cli"] = `^${deps.cli}`
53
+ if (dependencies["@supatype/types"] === undefined) dependencies["@supatype/types"] = `^${deps.types}`
54
+ next.dependencies = dependencies
55
+
56
+ const devDependencies = { ...(next.devDependencies ?? {}) }
57
+ if (devDependencies.tsx === undefined) devDependencies.tsx = "^4.19.2"
58
+ if (devDependencies.typescript === undefined) devDependencies.typescript = "^5"
59
+ if (opts.app.viteDevUrl && devDependencies.vite === undefined) devDependencies.vite = "^6"
60
+ next.devDependencies = devDependencies
61
+
62
+ return next
63
+ }
@@ -148,7 +148,8 @@ export async function isPortInUse(port: number): Promise<boolean> {
148
148
  return new Promise((resolve) => {
149
149
  const server = createServer()
150
150
  server.once("error", (err: NodeJS.ErrnoException) => {
151
- resolve(err.code === "EADDRINUSE")
151
+ // EADDRINUSE — something is listening. EACCES — Windows excluded/reserved port range.
152
+ resolve(err.code === "EADDRINUSE" || err.code === "EACCES")
152
153
  })
153
154
  server.once("listening", () => {
154
155
  server.close(() => resolve(false))
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Which CDN-cached host binaries a project needs (native vs docker).
3
+ *
4
+ * Docker runs Postgres, API, and schema push in Compose, but the CLI still uses a
5
+ * host engine binary for type generation and admin-config refresh after push.
6
+ */
7
+
8
+ import { existsSync } from "node:fs"
9
+ import type { Component } from "./components.js"
10
+ import {
11
+ functionsPathCandidatesFromProject,
12
+ resolveRuntimeProvider,
13
+ type SupatypeProjectConfig,
14
+ } from "./project-config.js"
15
+
16
+ export function requiredHostComponents(
17
+ config: SupatypeProjectConfig,
18
+ cwd: string = process.cwd(),
19
+ ): Component[] {
20
+ const provider = resolveRuntimeProvider(config)
21
+
22
+ if (provider === "docker") {
23
+ return ["engine"]
24
+ }
25
+
26
+ const components: Component[] = ["engine", "server", "postgres"]
27
+ const hasFunctions = functionsPathCandidatesFromProject(config, cwd).some((dir) =>
28
+ existsSync(dir),
29
+ )
30
+ if (hasFunctions) components.push("deno")
31
+ return components
32
+ }
33
+
34
+ export function describeRequiredHostComponents(components: readonly Component[]): string {
35
+ if (components.length === 0) return ""
36
+ return components.join(", ")
37
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Resolve the project API base URL (Kong gateway or direct server) for CLI HTTP calls.
3
+ */
4
+
5
+ import { loadConfig } from "./config.js"
6
+ import { readEnvValue } from "./env-file.js"
7
+ import { serverBaseUrl } from "./project-config.js"
8
+
9
+ const API_URL_ENV_KEYS = [
10
+ "SUPATYPE_URL",
11
+ "SUPATYPE_API_URL",
12
+ "PUBLIC_SUPATYPE_URL",
13
+ "NEXT_PUBLIC_SUPATYPE_URL",
14
+ "SITE_URL",
15
+ "API_EXTERNAL_URL",
16
+ ] as const
17
+
18
+ /** Kong or server origin without a trailing slash. */
19
+ export function resolveProjectApiUrl(cwd: string): string {
20
+ for (const key of API_URL_ENV_KEYS) {
21
+ const value = readEnvValue(cwd, key, "").trim()
22
+ if (value) {
23
+ return value.replace(/\/+$/, "")
24
+ }
25
+ }
26
+
27
+ const kongPort = readEnvValue(cwd, "SUPATYPE_KONG_PORT", "").trim()
28
+ if (kongPort) {
29
+ return `http://localhost:${kongPort}`
30
+ }
31
+
32
+ try {
33
+ const fromConfig = serverBaseUrl(loadConfig(cwd))
34
+ if (fromConfig) {
35
+ return fromConfig.replace(/\/+$/, "")
36
+ }
37
+ } catch {
38
+ // No supatype.config — fall through to PORT default.
39
+ }
40
+
41
+ const port = readEnvValue(cwd, "PORT", "54321").trim() || "54321"
42
+ return `http://localhost:${port}`
43
+ }