@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,19 +1,45 @@
1
1
  // ─── Admin panel CLI commands (Gap Appendices task 48) ──────────────────────
2
2
  //
3
3
  // `npx supatype admin create-user` — create an admin user in the project's
4
- // {ref}_auth.users table. Used for initial setup and ongoing admin management.
4
+ // auth.users table. First admin is ensured on `supatype dev` or `supatype push`.
5
5
 
6
6
  import type { Command } from "commander"
7
- import { randomBytes, scrypt } from "node:crypto"
8
- import { promisify } from "node:util"
7
+ import { spawnSync } from "node:child_process"
8
+ import { existsSync } from "node:fs"
9
+ import { dirname, join, resolve } from "node:path"
10
+ import bcrypt from "bcryptjs"
11
+ import type { Pool, QueryResult } from "pg"
9
12
  import { loadConfig } from "../config.js"
10
- import { connectionString } from "../project-config.js"
11
- import { signJwt } from "../jwt.js"
13
+ import {
14
+ connectionString,
15
+ resolveRuntimeProvider,
16
+ type SupatypeProjectConfig,
17
+ } from "../project-config.js"
18
+ import { readEnvValue, upsertEnvFile } from "../env-file.js"
19
+ import { hasEngineOverride } from "../binary-cache.js"
12
20
  import { confirm as uiConfirm } from "../ui/confirm.js"
13
21
  import { error, info, plain } from "../ui/messages.js"
14
- import { promptText } from "../ui/prompts.js"
22
+ import { promptPassword, promptText } from "../ui/prompts.js"
23
+ import { isInteractive } from "../ui/interactive.js"
24
+ import {
25
+ getActiveDevSession,
26
+ } from "../dev-session.js"
27
+
28
+ export const ADMIN_EMAIL_ENV = "SUPATYPE_ADMIN_EMAIL"
29
+ export const ADMIN_PASSWORD_ENV = "SUPATYPE_ADMIN_PASSWORD"
30
+
31
+ const BCRYPT_ROUNDS = 10
32
+
33
+ export interface EnsureFirstAdminOptions {
34
+ email?: string
35
+ password?: string
36
+ cwd?: string
37
+ role?: string
38
+ connection?: string
39
+ compose?: { project: string; composePath: string }
40
+ }
15
41
 
16
- const scryptAsync = promisify(scrypt)
42
+ type DbQuery = (sql: string, params?: unknown[]) => Promise<QueryResult>
17
43
 
18
44
  export function registerAdmin(program: Command): void {
19
45
  const adminCmd = program
@@ -55,72 +81,12 @@ export function registerAdmin(program: Command): void {
55
81
 
56
82
  info(`Creating admin user: ${email} (role: ${role})...`)
57
83
 
58
- // We use pg directly to insert into the auth.users table
59
84
  const pg = await importPg()
60
85
  const pool = new pg.Pool({ connectionString: connection, max: 2 })
61
86
 
62
87
  try {
63
- // Ensure the auth schema and users table exist
64
- await pool.query(`
65
- CREATE SCHEMA IF NOT EXISTS auth;
66
-
67
- CREATE TABLE IF NOT EXISTS auth.users (
68
- id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
69
- instance_id UUID,
70
- aud TEXT DEFAULT 'authenticated',
71
- role TEXT DEFAULT 'authenticated',
72
- email TEXT UNIQUE,
73
- encrypted_password TEXT,
74
- email_confirmed_at TIMESTAMPTZ DEFAULT now(),
75
- raw_app_meta_data JSONB DEFAULT '{}',
76
- raw_user_meta_data JSONB DEFAULT '{}',
77
- created_at TIMESTAMPTZ DEFAULT now(),
78
- updated_at TIMESTAMPTZ DEFAULT now(),
79
- confirmation_token TEXT DEFAULT '',
80
- recovery_token TEXT DEFAULT '',
81
- email_change_token_new TEXT DEFAULT '',
82
- email_change TEXT DEFAULT ''
83
- );
84
- `)
85
-
86
- // Check if user already exists
87
- const existing = await pool.query(
88
- `SELECT id FROM auth.users WHERE email = $1`,
89
- [email.toLowerCase()],
90
- )
91
-
92
- if (existing.rows.length > 0) {
93
- error(`User with email "${email}" already exists.`)
94
- info(`To update their role, use: supatype admin set-role --email ${email} --role ${role}`)
95
- process.exit(1)
96
- }
97
-
98
- // Hash the password (bcrypt-style for GoTrue compatibility)
99
- const passwordHash = await hashPassword(password)
100
-
101
- // Insert the admin user with the admin role in app_metadata
102
- const appMetadata = JSON.stringify({ role, provider: "email", providers: ["email"] })
103
- const userMetadata = JSON.stringify({})
104
-
105
- const result = await pool.query(
106
- `INSERT INTO auth.users (
107
- email, encrypted_password, role, aud,
108
- raw_app_meta_data, raw_user_meta_data,
109
- email_confirmed_at, created_at, updated_at
110
- ) VALUES (
111
- $1, $2, 'authenticated', 'authenticated',
112
- $3::jsonb, $4::jsonb,
113
- now(), now(), now()
114
- ) RETURNING id, email`,
115
- [email.toLowerCase(), passwordHash, appMetadata, userMetadata],
116
- )
117
-
118
- const user = result.rows[0] as { id: string; email: string }
119
-
120
- info("Admin user created successfully.")
121
- plain(` ID: ${user.id}`)
122
- plain(` Email: ${user.email}`)
123
- plain(` Role: ${role}`)
88
+ await ensureAuthUsersTable(pool)
89
+ await createAdminUser(pool, email, password, role)
124
90
  info("This user can now log in to the admin panel at /admin")
125
91
  } catch (err) {
126
92
  const message =
@@ -239,88 +205,371 @@ export function registerAdmin(program: Command): void {
239
205
  })
240
206
  }
241
207
 
242
- // ─── First admin user prompt (task 48) ──────────────────────────────────────
243
- // Called by `supatype push` on initial setup if no admin users exist.
208
+ /** @deprecated Use ensureFirstAdminUser */
209
+ export const promptFirstAdminUser = ensureFirstAdminUser
244
210
 
245
- export async function promptFirstAdminUser(
211
+ /**
212
+ * Ensure a first admin user exists (idempotent). Called from `dev` and `push`
213
+ * when auth.users is ready and no admin users exist yet.
214
+ */
215
+ export async function ensureFirstAdminUser(
246
216
  connection: string,
217
+ options: EnsureFirstAdminOptions = {},
247
218
  ): Promise<void> {
248
219
  const pg = await importPg()
249
220
  const pool = new pg.Pool({ connectionString: connection, max: 2 })
250
-
251
221
  try {
252
- // Check if auth.users table exists
253
- const tableExists = await pool.query(
254
- `SELECT EXISTS (
255
- SELECT FROM information_schema.tables
256
- WHERE table_schema = 'auth' AND table_name = 'users'
257
- ) as exists`,
258
- )
259
- if (!tableExists.rows[0]?.exists) return
260
-
261
- // Check if any admin users exist
262
- const adminCount = await pool.query(
263
- `SELECT COUNT(*) as count FROM auth.users
264
- WHERE raw_app_meta_data->>'role' IS NOT NULL
265
- AND raw_app_meta_data->>'role' != 'authenticated'`,
222
+ await ensureFirstAdminWithQuery(
223
+ (sql, params) => pool.query(sql, params),
224
+ options,
266
225
  )
226
+ } catch {
227
+ // Non-fatal — skip when DB is unreachable or auth schema is not ready
228
+ } finally {
229
+ await pool.end()
230
+ }
231
+ }
267
232
 
268
- const count = parseInt(
269
- (adminCount.rows[0] as { count: string }).count,
270
- 10,
271
- )
272
- if (count > 0) return
273
-
274
- // No admin users — prompt to create one
275
- info("No admin users found for the admin panel.")
276
- const createAdmin = await uiConfirm("Create an admin user now?")
277
- if (!createAdmin) {
278
- info("Skipped. You can create one later with: supatype admin create-user")
279
- return
233
+ /**
234
+ * Resolve DB access for the current project (host URL or compose exec when DB
235
+ * is not published to the host).
236
+ */
237
+ export async function ensureFirstAdminUserForProject(
238
+ cwd: string,
239
+ config: SupatypeProjectConfig,
240
+ options: EnsureFirstAdminOptions = {},
241
+ ): Promise<void> {
242
+ const root = resolve(cwd)
243
+ const merged: EnsureFirstAdminOptions = { cwd: root, ...options }
244
+
245
+ if (
246
+ resolveRuntimeProvider(config) === "docker" &&
247
+ merged.compose &&
248
+ !hasEngineOverride(config)
249
+ ) {
250
+ try {
251
+ await ensureFirstAdminWithQuery(
252
+ (sql, params) => composeExecQuery(root, merged.compose!, sql, params),
253
+ merged,
254
+ )
255
+ } catch {
256
+ // Non-fatal
280
257
  }
258
+ return
259
+ }
281
260
 
282
- const email = await promptText("Admin email")
283
- if (!email || !email.includes("@")) {
284
- info("Invalid email. Skipping admin user creation.")
285
- return
261
+ const connection =
262
+ merged.compose && hasEngineOverride(config)
263
+ ? hostComposeDbUrlFromEnv(root)
264
+ : options.connection ?? readEnvValue(root, "DATABASE_URL", connectionString(config))
265
+
266
+ await ensureFirstAdminUser(connection, merged)
267
+ }
268
+
269
+ async function ensureFirstAdminWithQuery(
270
+ query: DbQuery,
271
+ options: EnsureFirstAdminOptions,
272
+ ): Promise<void> {
273
+ const cwd = options.cwd ? resolve(options.cwd) : process.cwd()
274
+
275
+ if (!(await authUsersTableExists(query))) return
276
+ if (await hasAdminUsers(query)) return
277
+
278
+ const credentials = await resolveAdminCredentials(options, cwd)
279
+ if (!credentials) {
280
+ if (!isInteractive()) {
281
+ info(
282
+ "No admin users found. Set SUPATYPE_ADMIN_EMAIL / SUPATYPE_ADMIN_PASSWORD in .env, " +
283
+ "or run: supatype admin create-user",
284
+ )
286
285
  }
286
+ return
287
+ }
287
288
 
288
- const password = await promptText("Admin password (min 8 chars)")
289
- if (!password || password.length < 8) {
290
- info("Password too short. Skipping admin user creation.")
291
- return
289
+ const role = options.role ?? "admin"
290
+ try {
291
+ await createAdminUser(query, credentials.email, credentials.password, role, { quiet: true })
292
+ } catch (err) {
293
+ const message = err instanceof Error ? err.message : String(err)
294
+ error(`Failed to create admin user: ${message}`)
295
+ return
296
+ }
297
+ clearAdminSeedPassword(cwd)
298
+ logFirstAdminCreated(credentials.email, role)
299
+ }
300
+
301
+ function logFirstAdminCreated(email: string, role: string): void {
302
+ if (getActiveDevSession()?.isTui()) {
303
+ console.log(`Admin user "${email}" created (role: ${role}).`)
304
+ console.log("Log in at /admin after starting the dev server.")
305
+ return
306
+ }
307
+ info(`Admin user "${email}" created (role: ${role}).`)
308
+ info("Log in at /admin after starting the dev server.")
309
+ }
310
+
311
+ async function resolveAdminCredentials(
312
+ options: EnsureFirstAdminOptions,
313
+ cwd: string,
314
+ ): Promise<{ email: string; password: string } | null> {
315
+ const envEmail = options.email ?? readEnvValue(cwd, ADMIN_EMAIL_ENV, "").trim()
316
+ const envPassword =
317
+ options.password ?? readEnvValue(cwd, ADMIN_PASSWORD_ENV, "").trim()
318
+
319
+ if (envEmail && envPassword) {
320
+ if (!envEmail.includes("@")) {
321
+ info("Invalid admin email in .env. Skipping admin user creation.")
322
+ return null
323
+ }
324
+ if (envPassword.length < 8) {
325
+ info("Admin password in .env is too short (min 8 chars). Skipping.")
326
+ return null
292
327
  }
328
+ return { email: envEmail, password: envPassword }
329
+ }
330
+
331
+ if (!isInteractive()) return null
293
332
 
294
- const passwordHash = await hashPassword(password)
295
- const appMetadata = JSON.stringify({
296
- role: "admin",
297
- provider: "email",
298
- providers: ["email"],
333
+ info("No admin users found for the admin panel.")
334
+ const createAdmin = await uiConfirm("Create an admin user now?")
335
+ if (!createAdmin) {
336
+ info("Skipped. You can create one later with: supatype admin create-user")
337
+ return null
338
+ }
339
+
340
+ const email = await promptText("Admin email")
341
+ if (!email || !email.includes("@")) {
342
+ info("Invalid email. Skipping admin user creation.")
343
+ return null
344
+ }
345
+
346
+ const password = await promptPassword("Admin password (min 8 chars)")
347
+ if (!password || password.length < 8) {
348
+ info("Password too short. Skipping admin user creation.")
349
+ return null
350
+ }
351
+
352
+ return { email, password }
353
+ }
354
+
355
+ export function clearAdminSeedPassword(cwd: string): void {
356
+ upsertEnvFile(cwd, {}, [ADMIN_PASSWORD_ENV])
357
+ }
358
+
359
+ export async function hashPasswordForAuth(password: string): Promise<string> {
360
+ return bcrypt.hash(password, BCRYPT_ROUNDS)
361
+ }
362
+
363
+ async function createAdminUser(
364
+ db: Pool | DbQuery,
365
+ email: string,
366
+ password: string,
367
+ role: string,
368
+ opts: { quiet?: boolean } = {},
369
+ ): Promise<{ id: string; email: string }> {
370
+ const query: DbQuery =
371
+ typeof (db as Pool).query === "function"
372
+ ? (sql, params) => (db as Pool).query(sql, params)
373
+ : (db as DbQuery)
374
+
375
+ const normalized = email.toLowerCase()
376
+ const existing = await query(`SELECT id FROM auth.users WHERE email = $1`, [
377
+ normalized,
378
+ ])
379
+ if (existing.rows.length > 0) {
380
+ throw new Error(`User with email "${email}" already exists.`)
381
+ }
382
+
383
+ const passwordHash = await hashPasswordForAuth(password)
384
+ const appMetadata = JSON.stringify({
385
+ role,
386
+ provider: "email",
387
+ providers: ["email"],
388
+ })
389
+ const userMetadata = JSON.stringify({})
390
+
391
+ const result = await query(
392
+ `INSERT INTO auth.users (
393
+ id, email, encrypted_password, role, aud,
394
+ raw_app_meta_data, raw_user_meta_data,
395
+ email_confirmed_at, created_at, updated_at
396
+ ) VALUES (
397
+ gen_random_uuid(), $1, $2, 'authenticated', 'authenticated',
398
+ $3::jsonb, $4::jsonb,
399
+ now(), now(), now()
400
+ ) RETURNING id, email`,
401
+ [normalized, passwordHash, appMetadata, userMetadata],
402
+ )
403
+
404
+ const user = result.rows[0] as { id: string; email: string }
405
+ if (!opts.quiet) {
406
+ info("Admin user created successfully.")
407
+ plain(` ID: ${user.id}`)
408
+ plain(` Email: ${user.email}`)
409
+ plain(` Role: ${role}`)
410
+ }
411
+ return user
412
+ }
413
+
414
+ async function ensureAuthUsersTable(pool: Pool): Promise<void> {
415
+ await pool.query(`
416
+ CREATE SCHEMA IF NOT EXISTS auth;
417
+
418
+ CREATE TABLE IF NOT EXISTS auth.users (
419
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
420
+ instance_id UUID,
421
+ aud TEXT DEFAULT 'authenticated',
422
+ role TEXT DEFAULT 'authenticated',
423
+ email TEXT UNIQUE,
424
+ encrypted_password TEXT,
425
+ email_confirmed_at TIMESTAMPTZ DEFAULT now(),
426
+ raw_app_meta_data JSONB DEFAULT '{}',
427
+ raw_user_meta_data JSONB DEFAULT '{}',
428
+ created_at TIMESTAMPTZ DEFAULT now(),
429
+ updated_at TIMESTAMPTZ DEFAULT now(),
430
+ confirmation_token TEXT DEFAULT '',
431
+ recovery_token TEXT DEFAULT '',
432
+ email_change_token_new TEXT DEFAULT '',
433
+ email_change TEXT DEFAULT ''
434
+ );
435
+ `)
436
+ }
437
+
438
+ async function authUsersTableExists(query: DbQuery): Promise<boolean> {
439
+ const result = await query(
440
+ `SELECT EXISTS (
441
+ SELECT FROM information_schema.tables
442
+ WHERE table_schema = 'auth' AND table_name = 'users'
443
+ ) as exists`,
444
+ )
445
+ return Boolean(result.rows[0]?.exists)
446
+ }
447
+
448
+ async function hasAdminUsers(query: DbQuery): Promise<boolean> {
449
+ const adminCount = await query(
450
+ `SELECT COUNT(*)::int as count FROM auth.users
451
+ WHERE raw_app_meta_data->>'role' IS NOT NULL
452
+ AND raw_app_meta_data->>'role' != 'authenticated'`,
453
+ )
454
+ const count = (adminCount.rows[0] as { count: number } | undefined)?.count ?? 0
455
+ return count > 0
456
+ }
457
+
458
+ function composeExecQuery(
459
+ cwd: string,
460
+ compose: { project: string; composePath: string },
461
+ sql: string,
462
+ params: unknown[] = [],
463
+ ): Promise<QueryResult> {
464
+ const db = readEnvValue(cwd, "POSTGRES_DB", "supatype")
465
+ const user = readEnvValue(cwd, "POSTGRES_USER", "supatype_admin")
466
+ const envFile = join(cwd, ".env")
467
+ const composeDir = dirname(compose.composePath)
468
+ const args = [
469
+ "compose",
470
+ "-p",
471
+ compose.project,
472
+ "--project-directory",
473
+ cwd,
474
+ "-f",
475
+ compose.composePath,
476
+ ]
477
+ if (existsSync(envFile)) args.push("--env-file", envFile)
478
+
479
+ if (params.length === 0) {
480
+ args.push("exec", "-T", "db", "psql", "-U", user, "-d", db, "-tAc", sql)
481
+ const result = spawnSync("docker", args, { cwd: composeDir, encoding: "utf8" })
482
+ if (result.status !== 0) {
483
+ throw new Error((result.stderr ?? result.stdout ?? "compose psql failed").trim())
484
+ }
485
+ const text = (result.stdout ?? "").trim()
486
+ if (sql.trim().toUpperCase().startsWith("SELECT")) {
487
+ return Promise.resolve({
488
+ rows: parsePsqlScalarRows(sql, text),
489
+ rowCount: 1,
490
+ command: "SELECT",
491
+ oid: 0,
492
+ fields: [],
493
+ })
494
+ }
495
+ return Promise.resolve({
496
+ rows: [],
497
+ rowCount: 0,
498
+ command: "INSERT",
499
+ oid: 0,
500
+ fields: [],
299
501
  })
502
+ }
300
503
 
301
- await pool.query(
302
- `INSERT INTO auth.users (
303
- email, encrypted_password, role, aud,
304
- raw_app_meta_data, raw_user_meta_data,
305
- email_confirmed_at, created_at, updated_at
306
- ) VALUES (
307
- $1, $2, 'authenticated', 'authenticated',
308
- $3::jsonb, '{}'::jsonb,
309
- now(), now(), now()
310
- )`,
311
- [email.toLowerCase(), passwordHash, appMetadata],
312
- )
504
+ args.push(
505
+ "exec",
506
+ "-T",
507
+ "db",
508
+ "psql",
509
+ "-U",
510
+ user,
511
+ "-d",
512
+ db,
513
+ "-v",
514
+ "ON_ERROR_STOP=1",
515
+ "-c",
516
+ interpolateSql(sql, params),
517
+ )
518
+ const result = spawnSync("docker", args, { cwd: composeDir, encoding: "utf8" })
519
+ if (result.status !== 0) {
520
+ throw new Error((result.stderr ?? result.stdout ?? "compose psql failed").trim())
521
+ }
522
+ const stdout = (result.stdout ?? "").trim()
523
+ const idMatch = stdout.match(
524
+ /([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\s+\|\s+(.+)/i,
525
+ )
526
+ if (idMatch) {
527
+ return Promise.resolve({
528
+ rows: [{ id: idMatch[1], email: idMatch[2]?.trim() }],
529
+ rowCount: 1,
530
+ command: "INSERT",
531
+ oid: 0,
532
+ fields: [],
533
+ })
534
+ }
535
+ return Promise.resolve({
536
+ rows: [],
537
+ rowCount: 0,
538
+ command: "INSERT",
539
+ oid: 0,
540
+ fields: [],
541
+ })
542
+ }
313
543
 
314
- info(`Admin user "${email}" created (role: admin).`)
315
- info("Log in at /admin after starting the dev server.")
316
- } catch {
317
- // Non-fatal if auth schema doesn't exist yet, skip silently
318
- } finally {
319
- await pool.end()
544
+ function parsePsqlScalarRows(sql: string, text: string): Record<string, unknown>[] {
545
+ const upper = sql.toUpperCase()
546
+ if (upper.includes(" AS EXISTS")) {
547
+ return [{ exists: text === "t" }]
548
+ }
549
+ if (upper.includes(" AS COUNT")) {
550
+ return [{ count: Number.parseInt(text, 10) || 0 }]
320
551
  }
552
+ if (text === "") return []
553
+ return [{ value: text }]
321
554
  }
322
555
 
323
- // ─── Helpers ────────────────────────────────────────────────────────────────────
556
+ function interpolateSql(sql: string, params: unknown[]): string {
557
+ return sql.replace(/\$(\d+)/g, (_match, index: string) => {
558
+ const value = params[Number(index) - 1]
559
+ if (value === null || value === undefined) return "NULL"
560
+ if (typeof value === "number" || typeof value === "bigint") return String(value)
561
+ if (typeof value === "boolean") return value ? "TRUE" : "FALSE"
562
+ return `'${String(value).replace(/'/g, "''")}'`
563
+ })
564
+ }
565
+
566
+ function hostComposeDbUrlFromEnv(cwd: string): string {
567
+ const port = readEnvValue(cwd, "SUPATYPE_DEV_DB_PORT", "54329")
568
+ const user = readEnvValue(cwd, "POSTGRES_USER", "supatype_admin")
569
+ const pass = readEnvValue(cwd, "POSTGRES_PASSWORD", "postgres")
570
+ const db = readEnvValue(cwd, "POSTGRES_DB", "supatype")
571
+ return `postgresql://${user}:${pass}@127.0.0.1:${port}/${db}?sslmode=disable`
572
+ }
324
573
 
325
574
  async function importPg(): Promise<typeof import("pg")> {
326
575
  try {
@@ -330,9 +579,3 @@ async function importPg(): Promise<typeof import("pg")> {
330
579
  process.exit(1)
331
580
  }
332
581
  }
333
-
334
- async function hashPassword(password: string): Promise<string> {
335
- const salt = randomBytes(16).toString("hex")
336
- const derived = (await scryptAsync(password, salt, 64)) as Buffer
337
- return `${salt}:${derived.toString("hex")}`
338
- }