@supatype/cli 0.1.0-alpha.6

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 (200) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/.turbo/turbo-test.log +7 -0
  3. package/.turbo/turbo-typecheck.log +4 -0
  4. package/bin/dev-entry.ts +2 -0
  5. package/bin/supatype.js +5 -0
  6. package/dist/app/framework.d.ts +44 -0
  7. package/dist/app/framework.d.ts.map +1 -0
  8. package/dist/app/framework.js +200 -0
  9. package/dist/app/framework.js.map +1 -0
  10. package/dist/cli.d.ts +2 -0
  11. package/dist/cli.d.ts.map +1 -0
  12. package/dist/cli.js +55 -0
  13. package/dist/cli.js.map +1 -0
  14. package/dist/commands/admin.d.ts +4 -0
  15. package/dist/commands/admin.d.ts.map +1 -0
  16. package/dist/commands/admin.js +270 -0
  17. package/dist/commands/admin.js.map +1 -0
  18. package/dist/commands/app.d.ts +3 -0
  19. package/dist/commands/app.d.ts.map +1 -0
  20. package/dist/commands/app.js +235 -0
  21. package/dist/commands/app.js.map +1 -0
  22. package/dist/commands/cloud.d.ts +3 -0
  23. package/dist/commands/cloud.d.ts.map +1 -0
  24. package/dist/commands/cloud.js +256 -0
  25. package/dist/commands/cloud.js.map +1 -0
  26. package/dist/commands/db.d.ts +8 -0
  27. package/dist/commands/db.d.ts.map +1 -0
  28. package/dist/commands/db.js +123 -0
  29. package/dist/commands/db.js.map +1 -0
  30. package/dist/commands/deploy-types.d.ts +14 -0
  31. package/dist/commands/deploy-types.d.ts.map +1 -0
  32. package/dist/commands/deploy-types.js +38 -0
  33. package/dist/commands/deploy-types.js.map +1 -0
  34. package/dist/commands/deploy.d.ts +14 -0
  35. package/dist/commands/deploy.d.ts.map +1 -0
  36. package/dist/commands/deploy.js +295 -0
  37. package/dist/commands/deploy.js.map +1 -0
  38. package/dist/commands/dev.d.ts +3 -0
  39. package/dist/commands/dev.d.ts.map +1 -0
  40. package/dist/commands/dev.js +428 -0
  41. package/dist/commands/dev.js.map +1 -0
  42. package/dist/commands/diff.d.ts +3 -0
  43. package/dist/commands/diff.d.ts.map +1 -0
  44. package/dist/commands/diff.js +39 -0
  45. package/dist/commands/diff.js.map +1 -0
  46. package/dist/commands/engine.d.ts +9 -0
  47. package/dist/commands/engine.d.ts.map +1 -0
  48. package/dist/commands/engine.js +99 -0
  49. package/dist/commands/engine.js.map +1 -0
  50. package/dist/commands/functions.d.ts +3 -0
  51. package/dist/commands/functions.d.ts.map +1 -0
  52. package/dist/commands/functions.js +762 -0
  53. package/dist/commands/functions.js.map +1 -0
  54. package/dist/commands/generate.d.ts +3 -0
  55. package/dist/commands/generate.d.ts.map +1 -0
  56. package/dist/commands/generate.js +28 -0
  57. package/dist/commands/generate.js.map +1 -0
  58. package/dist/commands/init.d.ts +7 -0
  59. package/dist/commands/init.d.ts.map +1 -0
  60. package/dist/commands/init.js +515 -0
  61. package/dist/commands/init.js.map +1 -0
  62. package/dist/commands/keys.d.ts +4 -0
  63. package/dist/commands/keys.d.ts.map +1 -0
  64. package/dist/commands/keys.js +57 -0
  65. package/dist/commands/keys.js.map +1 -0
  66. package/dist/commands/logs.d.ts +6 -0
  67. package/dist/commands/logs.d.ts.map +1 -0
  68. package/dist/commands/logs.js +52 -0
  69. package/dist/commands/logs.js.map +1 -0
  70. package/dist/commands/migrate.d.ts +3 -0
  71. package/dist/commands/migrate.d.ts.map +1 -0
  72. package/dist/commands/migrate.js +71 -0
  73. package/dist/commands/migrate.js.map +1 -0
  74. package/dist/commands/plugins.d.ts +3 -0
  75. package/dist/commands/plugins.d.ts.map +1 -0
  76. package/dist/commands/plugins.js +431 -0
  77. package/dist/commands/plugins.js.map +1 -0
  78. package/dist/commands/pull.d.ts +3 -0
  79. package/dist/commands/pull.d.ts.map +1 -0
  80. package/dist/commands/pull.js +73 -0
  81. package/dist/commands/pull.js.map +1 -0
  82. package/dist/commands/push.d.ts +3 -0
  83. package/dist/commands/push.d.ts.map +1 -0
  84. package/dist/commands/push.js +87 -0
  85. package/dist/commands/push.js.map +1 -0
  86. package/dist/commands/seed.d.ts +3 -0
  87. package/dist/commands/seed.d.ts.map +1 -0
  88. package/dist/commands/seed.js +22 -0
  89. package/dist/commands/seed.js.map +1 -0
  90. package/dist/commands/self-host.d.ts +3 -0
  91. package/dist/commands/self-host.d.ts.map +1 -0
  92. package/dist/commands/self-host.js +796 -0
  93. package/dist/commands/self-host.js.map +1 -0
  94. package/dist/commands/status.d.ts +6 -0
  95. package/dist/commands/status.d.ts.map +1 -0
  96. package/dist/commands/status.js +69 -0
  97. package/dist/commands/status.js.map +1 -0
  98. package/dist/config.d.ts +106 -0
  99. package/dist/config.d.ts.map +1 -0
  100. package/dist/config.js +66 -0
  101. package/dist/config.js.map +1 -0
  102. package/dist/engine/cache.d.ts +37 -0
  103. package/dist/engine/cache.d.ts.map +1 -0
  104. package/dist/engine/cache.js +121 -0
  105. package/dist/engine/cache.js.map +1 -0
  106. package/dist/engine/download.d.ts +19 -0
  107. package/dist/engine/download.d.ts.map +1 -0
  108. package/dist/engine/download.js +108 -0
  109. package/dist/engine/download.js.map +1 -0
  110. package/dist/engine/platform.d.ts +24 -0
  111. package/dist/engine/platform.d.ts.map +1 -0
  112. package/dist/engine/platform.js +50 -0
  113. package/dist/engine/platform.js.map +1 -0
  114. package/dist/engine/resolve.d.ts +37 -0
  115. package/dist/engine/resolve.d.ts.map +1 -0
  116. package/dist/engine/resolve.js +133 -0
  117. package/dist/engine/resolve.js.map +1 -0
  118. package/dist/engine/update-notify.d.ts +11 -0
  119. package/dist/engine/update-notify.d.ts.map +1 -0
  120. package/dist/engine/update-notify.js +43 -0
  121. package/dist/engine/update-notify.js.map +1 -0
  122. package/dist/engine/verify.d.ts +50 -0
  123. package/dist/engine/verify.d.ts.map +1 -0
  124. package/dist/engine/verify.js +161 -0
  125. package/dist/engine/verify.js.map +1 -0
  126. package/dist/engine-version.d.ts +35 -0
  127. package/dist/engine-version.d.ts.map +1 -0
  128. package/dist/engine-version.js +35 -0
  129. package/dist/engine-version.js.map +1 -0
  130. package/dist/engine.d.ts +34 -0
  131. package/dist/engine.d.ts.map +1 -0
  132. package/dist/engine.js +76 -0
  133. package/dist/engine.js.map +1 -0
  134. package/dist/index.d.ts +12 -0
  135. package/dist/index.d.ts.map +1 -0
  136. package/dist/index.js +10 -0
  137. package/dist/index.js.map +1 -0
  138. package/dist/jwt.d.ts +3 -0
  139. package/dist/jwt.d.ts.map +1 -0
  140. package/dist/jwt.js +13 -0
  141. package/dist/jwt.js.map +1 -0
  142. package/dist/pull-utils.d.ts +16 -0
  143. package/dist/pull-utils.d.ts.map +1 -0
  144. package/dist/pull-utils.js +65 -0
  145. package/dist/pull-utils.js.map +1 -0
  146. package/dist/scripts/postinstall.d.ts +12 -0
  147. package/dist/scripts/postinstall.d.ts.map +1 -0
  148. package/dist/scripts/postinstall.js +31 -0
  149. package/dist/scripts/postinstall.js.map +1 -0
  150. package/dist/tsx-runner.d.ts +18 -0
  151. package/dist/tsx-runner.d.ts.map +1 -0
  152. package/dist/tsx-runner.js +62 -0
  153. package/dist/tsx-runner.js.map +1 -0
  154. package/package.json +36 -0
  155. package/src/app/framework.ts +249 -0
  156. package/src/cli.ts +58 -0
  157. package/src/commands/admin.ts +371 -0
  158. package/src/commands/app.ts +261 -0
  159. package/src/commands/cloud.ts +326 -0
  160. package/src/commands/db.ts +145 -0
  161. package/src/commands/deploy-types.ts +49 -0
  162. package/src/commands/deploy.ts +366 -0
  163. package/src/commands/dev.ts +477 -0
  164. package/src/commands/diff.ts +61 -0
  165. package/src/commands/engine.ts +133 -0
  166. package/src/commands/functions.ts +919 -0
  167. package/src/commands/generate.ts +31 -0
  168. package/src/commands/init.ts +532 -0
  169. package/src/commands/keys.ts +66 -0
  170. package/src/commands/logs.ts +58 -0
  171. package/src/commands/migrate.ts +83 -0
  172. package/src/commands/plugins.ts +508 -0
  173. package/src/commands/pull.ts +96 -0
  174. package/src/commands/push.ts +119 -0
  175. package/src/commands/seed.ts +26 -0
  176. package/src/commands/self-host.ts +932 -0
  177. package/src/commands/status.ts +83 -0
  178. package/src/config.ts +190 -0
  179. package/src/engine/cache.ts +135 -0
  180. package/src/engine/download.ts +143 -0
  181. package/src/engine/platform.ts +66 -0
  182. package/src/engine/resolve.ts +197 -0
  183. package/src/engine/update-notify.ts +50 -0
  184. package/src/engine/verify.ts +206 -0
  185. package/src/engine-version.ts +39 -0
  186. package/src/engine.ts +99 -0
  187. package/src/index.ts +19 -0
  188. package/src/jwt.ts +14 -0
  189. package/src/pull-utils.ts +57 -0
  190. package/src/scripts/postinstall.ts +40 -0
  191. package/src/tsx-runner.ts +79 -0
  192. package/tests/cli-help.test.ts +107 -0
  193. package/tests/config.test.ts +117 -0
  194. package/tests/engine-distribution.test.ts +418 -0
  195. package/tests/init.test.ts +184 -0
  196. package/tests/keys.test.ts +160 -0
  197. package/tests/pull-utils.test.ts +115 -0
  198. package/tests/tsx-runner.test.ts +66 -0
  199. package/tsconfig.json +10 -0
  200. package/tsconfig.tsbuildinfo +1 -0
@@ -0,0 +1,249 @@
1
+ /**
2
+ * Framework auto-detection and build configuration.
3
+ * Inspects package.json to determine the framework, then resolves
4
+ * build command, output directory, and SPA mode defaults.
5
+ */
6
+
7
+ import { existsSync, readFileSync } from "node:fs"
8
+ import { join, resolve } from "node:path"
9
+ import type { AppConfig, AppFramework } from "../config.js"
10
+
11
+ export interface ResolvedAppConfig {
12
+ framework: AppFramework
13
+ directory: string
14
+ buildCommand: string
15
+ outputDirectory: string
16
+ spa: boolean
17
+ env: Record<string, string>
18
+ headers: Record<string, string>
19
+ }
20
+
21
+ interface FrameworkDefaults {
22
+ buildCommand: string
23
+ outputDirectory: string
24
+ spa: boolean
25
+ }
26
+
27
+ const FRAMEWORK_DEFAULTS: Record<AppFramework, FrameworkDefaults> = {
28
+ nextjs: { buildCommand: "next build", outputDirectory: "out", spa: false },
29
+ astro: { buildCommand: "astro build", outputDirectory: "dist", spa: false },
30
+ vite: { buildCommand: "vite build", outputDirectory: "dist", spa: true },
31
+ "remix-spa": { buildCommand: "remix vite:build", outputDirectory: "build/client", spa: true },
32
+ sveltekit: { buildCommand: "vite build", outputDirectory: "build", spa: false },
33
+ nuxt: { buildCommand: "nuxt generate", outputDirectory: "dist", spa: false },
34
+ static: { buildCommand: "", outputDirectory: ".", spa: false },
35
+ }
36
+
37
+ // Maps package.json dependency names to framework identifiers
38
+ const FRAMEWORK_DETECTION: Array<{ dep: string; framework: AppFramework }> = [
39
+ { dep: "next", framework: "nextjs" },
40
+ { dep: "astro", framework: "astro" },
41
+ { dep: "@remix-run/react", framework: "remix-spa" },
42
+ { dep: "@sveltejs/kit", framework: "sveltekit" },
43
+ { dep: "nuxt", framework: "nuxt" },
44
+ // Vite last — many frameworks use Vite under the hood
45
+ { dep: "vite", framework: "vite" },
46
+ ]
47
+
48
+ /**
49
+ * Detect the framework from package.json dependencies.
50
+ */
51
+ export function detectFramework(appDir: string): AppFramework | undefined {
52
+ const pkgPath = join(appDir, "package.json")
53
+ if (!existsSync(pkgPath)) return undefined
54
+
55
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as {
56
+ dependencies?: Record<string, string>
57
+ devDependencies?: Record<string, string>
58
+ }
59
+
60
+ const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }
61
+
62
+ for (const { dep, framework } of FRAMEWORK_DETECTION) {
63
+ if (dep in allDeps) return framework
64
+ }
65
+
66
+ // Check for plain HTML
67
+ if (existsSync(join(appDir, "index.html"))) return "static"
68
+
69
+ return undefined
70
+ }
71
+
72
+ /**
73
+ * Detect if the project is a monorepo.
74
+ */
75
+ export function detectMonorepo(cwd: string): { isMonorepo: boolean; tool?: string } {
76
+ if (existsSync(join(cwd, "turbo.json"))) return { isMonorepo: true, tool: "turbo" }
77
+ if (existsSync(join(cwd, "pnpm-workspace.yaml"))) return { isMonorepo: true, tool: "pnpm" }
78
+ if (existsSync(join(cwd, "nx.json"))) return { isMonorepo: true, tool: "nx" }
79
+ return { isMonorepo: false }
80
+ }
81
+
82
+ /**
83
+ * Detect package manager from lockfiles.
84
+ */
85
+ export function detectPackageManager(dir: string): "npm" | "pnpm" | "yarn" {
86
+ if (existsSync(join(dir, "pnpm-lock.yaml"))) return "pnpm"
87
+ if (existsSync(join(dir, "yarn.lock"))) return "yarn"
88
+ return "npm"
89
+ }
90
+
91
+ /**
92
+ * Resolve the full app configuration from user config + auto-detection.
93
+ */
94
+ export function resolveAppConfig(
95
+ appConfig: AppConfig | undefined,
96
+ cwd: string,
97
+ ): ResolvedAppConfig {
98
+ const directory = resolve(cwd, appConfig?.directory || ".")
99
+
100
+ // Framework detection
101
+ let framework = appConfig?.framework
102
+ if (!framework) {
103
+ framework = detectFramework(directory)
104
+ if (!framework) {
105
+ throw new Error(
106
+ "Could not detect frontend framework.\n" +
107
+ "Set app.framework in supatype.config.ts, or ensure package.json is present.",
108
+ )
109
+ }
110
+ }
111
+
112
+ const defaults = FRAMEWORK_DEFAULTS[framework]
113
+
114
+ // Build command
115
+ let buildCommand = appConfig?.buildCommand || defaults.buildCommand
116
+ const mono = detectMonorepo(cwd)
117
+ if (mono.isMonorepo && mono.tool === "turbo" && !appConfig?.buildCommand) {
118
+ // In a Turborepo, run build via turbo from workspace root
119
+ const appDirRelative = appConfig?.directory || "."
120
+ if (appDirRelative !== ".") {
121
+ const pkgName = getPackageName(directory)
122
+ if (pkgName) {
123
+ buildCommand = `turbo run build --filter=${pkgName}`
124
+ }
125
+ }
126
+ }
127
+
128
+ return {
129
+ framework,
130
+ directory,
131
+ buildCommand,
132
+ outputDirectory: resolve(directory, appConfig?.outputDirectory || defaults.outputDirectory),
133
+ spa: appConfig?.spa ?? defaults.spa,
134
+ env: appConfig?.env ?? {},
135
+ headers: appConfig?.headers ?? {},
136
+ }
137
+ }
138
+
139
+ function getPackageName(dir: string): string | undefined {
140
+ const pkgPath = join(dir, "package.json")
141
+ if (!existsSync(pkgPath)) return undefined
142
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as { name?: string }
143
+ return pkg.name
144
+ }
145
+
146
+ /**
147
+ * Validate that the framework is configured for static output.
148
+ * Returns an error message if SSR mode is detected.
149
+ */
150
+ export function validateStaticMode(framework: AppFramework, appDir: string): string | null {
151
+ if (framework === "nextjs") {
152
+ // Check for output: 'export' in next.config.js/mjs/ts
153
+ for (const name of ["next.config.js", "next.config.mjs", "next.config.ts"]) {
154
+ const configPath = join(appDir, name)
155
+ if (existsSync(configPath)) {
156
+ const content = readFileSync(configPath, "utf8")
157
+ if (!content.includes("export")) {
158
+ return (
159
+ "Supatype currently supports Next.js static export only.\n" +
160
+ "Add `output: 'export'` to your next.config.js,\n" +
161
+ "or deploy your frontend to Vercel for SSR support."
162
+ )
163
+ }
164
+ }
165
+ }
166
+ }
167
+
168
+ if (framework === "astro") {
169
+ const configPath = join(appDir, "astro.config.mjs")
170
+ if (existsSync(configPath)) {
171
+ const content = readFileSync(configPath, "utf8")
172
+ if (content.includes("output: 'server'") || content.includes("output: \"server\"")) {
173
+ return (
174
+ "Supatype currently supports Astro static sites only.\n" +
175
+ "Remove `output: 'server'` from astro.config.mjs,\n" +
176
+ "or deploy your frontend separately for SSR support."
177
+ )
178
+ }
179
+ }
180
+ }
181
+
182
+ if (framework === "sveltekit") {
183
+ const pkgPath = join(appDir, "package.json")
184
+ if (existsSync(pkgPath)) {
185
+ const pkg = JSON.parse(readFileSync(pkgPath, "utf8")) as {
186
+ devDependencies?: Record<string, string>
187
+ }
188
+ if (!pkg.devDependencies?.["@sveltejs/adapter-static"]) {
189
+ return (
190
+ "Supatype requires @sveltejs/adapter-static for SvelteKit.\n" +
191
+ "Install it: npm install -D @sveltejs/adapter-static"
192
+ )
193
+ }
194
+ }
195
+ }
196
+
197
+ return null
198
+ }
199
+
200
+ /**
201
+ * Validate the build output.
202
+ */
203
+ export function validateBuildOutput(outputDir: string, maxSizeMb: number): string | null {
204
+ if (!existsSync(outputDir)) {
205
+ return `Build output directory not found: ${outputDir}`
206
+ }
207
+
208
+ // Check for at least one HTML file
209
+ const hasHtml = findHtmlFile(outputDir)
210
+ if (!hasHtml) {
211
+ return `No HTML files found in build output: ${outputDir}`
212
+ }
213
+
214
+ // Check total size
215
+ const sizeMb = getDirSizeMb(outputDir)
216
+ if (sizeMb > maxSizeMb) {
217
+ return `Build output ${sizeMb.toFixed(1)}MB exceeds limit of ${maxSizeMb}MB`
218
+ }
219
+
220
+ if (sizeMb > 500) {
221
+ console.warn(
222
+ `Warning: Build output is ${sizeMb.toFixed(1)}MB. This may include unoptimised assets or node_modules.`,
223
+ )
224
+ }
225
+
226
+ return null
227
+ }
228
+
229
+ function findHtmlFile(dir: string): boolean {
230
+ const { readdirSync, statSync } = require("node:fs") as typeof import("node:fs")
231
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
232
+ if (entry.isFile() && entry.name.endsWith(".html")) return true
233
+ if (entry.isDirectory()) {
234
+ if (findHtmlFile(join(dir, entry.name))) return true
235
+ }
236
+ }
237
+ return false
238
+ }
239
+
240
+ function getDirSizeMb(dir: string): number {
241
+ const { readdirSync, statSync } = require("node:fs") as typeof import("node:fs")
242
+ let size = 0
243
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
244
+ const path = join(dir, entry.name)
245
+ if (entry.isFile()) size += statSync(path).size
246
+ else if (entry.isDirectory()) size += getDirSizeMb(path) * 1024 * 1024
247
+ }
248
+ return size / (1024 * 1024)
249
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,58 @@
1
+ import { Command } from "commander"
2
+ import { ENGINE_VERSION } from "./engine-version.js"
3
+ import { registerInit } from "./commands/init.js"
4
+ import { registerDev } from "./commands/dev.js"
5
+ import { registerPush } from "./commands/push.js"
6
+ import { registerDiff } from "./commands/diff.js"
7
+ import { registerPull } from "./commands/pull.js"
8
+ import { registerGenerate } from "./commands/generate.js"
9
+ import { registerMigrate } from "./commands/migrate.js"
10
+ import { registerSeed } from "./commands/seed.js"
11
+ import { registerKeys } from "./commands/keys.js"
12
+ import { registerApp } from "./commands/app.js"
13
+ import { registerSelfHost } from "./commands/self-host.js"
14
+ import { registerCloud } from "./commands/cloud.js"
15
+ import { registerEngine } from "./commands/engine.js"
16
+ import { registerDb } from "./commands/db.js"
17
+ import { registerDeploy } from "./commands/deploy.js"
18
+ import { registerStatus } from "./commands/status.js"
19
+ import { registerLogs } from "./commands/logs.js"
20
+ import { registerAdmin } from "./commands/admin.js"
21
+ import { registerFunctions } from "./commands/functions.js"
22
+ import { registerPlugins } from "./commands/plugins.js"
23
+ import { showUpdateNotification } from "./engine/update-notify.js"
24
+
25
+ export function run(): void {
26
+ const program = new Command()
27
+ .name("supatype")
28
+ .description("Supatype — schema-first Postgres API")
29
+ .version(ENGINE_VERSION)
30
+
31
+ registerInit(program)
32
+ registerDev(program)
33
+ registerPush(program)
34
+ registerDiff(program)
35
+ registerPull(program)
36
+ registerGenerate(program)
37
+ registerMigrate(program)
38
+ registerSeed(program)
39
+ registerKeys(program)
40
+ registerApp(program)
41
+ registerSelfHost(program)
42
+ registerCloud(program)
43
+ registerEngine(program)
44
+ registerDb(program)
45
+ registerDeploy(program)
46
+ registerStatus(program)
47
+ registerLogs(program)
48
+ registerAdmin(program)
49
+ registerFunctions(program)
50
+ registerPlugins(program)
51
+
52
+ // After command execution, show update notification (non-blocking)
53
+ program.hook("postAction", async () => {
54
+ await showUpdateNotification()
55
+ })
56
+
57
+ program.parse()
58
+ }
@@ -0,0 +1,371 @@
1
+ // ─── Admin panel CLI commands (Gap Appendices task 48) ──────────────────────
2
+ //
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.
5
+
6
+ import type { Command } from "commander"
7
+ import { createInterface } from "node:readline"
8
+ import { randomBytes, scrypt } from "node:crypto"
9
+ import { promisify } from "node:util"
10
+ import { loadConfig } from "../config.js"
11
+ import { signJwt } from "../jwt.js"
12
+
13
+ const scryptAsync = promisify(scrypt)
14
+
15
+ export function registerAdmin(program: Command): void {
16
+ const adminCmd = program
17
+ .command("admin")
18
+ .description("Manage admin panel users and configuration")
19
+
20
+ adminCmd
21
+ .command("create-user")
22
+ .description("Create an admin user for the admin panel")
23
+ .option("--email <email>", "Admin user email address")
24
+ .option("--password <password>", "Admin user password (prompted if not provided)")
25
+ .option("--role <role>", "Admin role to assign", "admin")
26
+ .option("--connection <url>", "Database connection URL (overrides config)")
27
+ .action(
28
+ async (opts: {
29
+ email?: string
30
+ password?: string
31
+ role: string
32
+ connection?: string
33
+ }) => {
34
+ const cwd = process.cwd()
35
+ const config = loadConfig(cwd)
36
+ const connection = opts.connection ?? config.connection
37
+
38
+ const email = opts.email ?? (await prompt("Admin email: "))
39
+ if (!email || !email.includes("@")) {
40
+ console.error("A valid email address is required.")
41
+ process.exit(1)
42
+ }
43
+
44
+ const password =
45
+ opts.password ?? (await prompt("Admin password (min 8 chars): "))
46
+ if (!password || password.length < 8) {
47
+ console.error("Password must be at least 8 characters.")
48
+ process.exit(1)
49
+ }
50
+
51
+ const role = opts.role
52
+
53
+ console.log(`\nCreating admin user: ${email} (role: ${role})...`)
54
+
55
+ // We use pg directly to insert into the auth.users table
56
+ const pg = await importPg()
57
+ const pool = new pg.Pool({ connectionString: connection, max: 2 })
58
+
59
+ try {
60
+ // Ensure the auth schema and users table exist
61
+ await pool.query(`
62
+ CREATE SCHEMA IF NOT EXISTS auth;
63
+
64
+ CREATE TABLE IF NOT EXISTS auth.users (
65
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
66
+ instance_id UUID,
67
+ aud TEXT DEFAULT 'authenticated',
68
+ role TEXT DEFAULT 'authenticated',
69
+ email TEXT UNIQUE,
70
+ encrypted_password TEXT,
71
+ email_confirmed_at TIMESTAMPTZ DEFAULT now(),
72
+ raw_app_meta_data JSONB DEFAULT '{}',
73
+ raw_user_meta_data JSONB DEFAULT '{}',
74
+ created_at TIMESTAMPTZ DEFAULT now(),
75
+ updated_at TIMESTAMPTZ DEFAULT now(),
76
+ confirmation_token TEXT DEFAULT '',
77
+ recovery_token TEXT DEFAULT '',
78
+ email_change_token_new TEXT DEFAULT '',
79
+ email_change TEXT DEFAULT ''
80
+ );
81
+ `)
82
+
83
+ // Check if user already exists
84
+ const existing = await pool.query(
85
+ `SELECT id FROM auth.users WHERE email = $1`,
86
+ [email.toLowerCase()],
87
+ )
88
+
89
+ if (existing.rows.length > 0) {
90
+ console.error(
91
+ `\nUser with email "${email}" already exists.`,
92
+ )
93
+ console.log(
94
+ `To update their role, use: supatype admin set-role --email ${email} --role ${role}`,
95
+ )
96
+ process.exit(1)
97
+ }
98
+
99
+ // Hash the password (bcrypt-style for GoTrue compatibility)
100
+ const passwordHash = await hashPassword(password)
101
+
102
+ // Insert the admin user with the admin role in app_metadata
103
+ const appMetadata = JSON.stringify({ role, provider: "email", providers: ["email"] })
104
+ const userMetadata = JSON.stringify({})
105
+
106
+ const result = await pool.query(
107
+ `INSERT INTO auth.users (
108
+ email, encrypted_password, role, aud,
109
+ raw_app_meta_data, raw_user_meta_data,
110
+ email_confirmed_at, created_at, updated_at
111
+ ) VALUES (
112
+ $1, $2, 'authenticated', 'authenticated',
113
+ $3::jsonb, $4::jsonb,
114
+ now(), now(), now()
115
+ ) RETURNING id, email`,
116
+ [email.toLowerCase(), passwordHash, appMetadata, userMetadata],
117
+ )
118
+
119
+ const user = result.rows[0] as { id: string; email: string }
120
+
121
+ console.log(`\nAdmin user created successfully.`)
122
+ console.log(` ID: ${user.id}`)
123
+ console.log(` Email: ${user.email}`)
124
+ console.log(` Role: ${role}`)
125
+ console.log(
126
+ `\nThis user can now log in to the admin panel at /admin\n`,
127
+ )
128
+ } catch (err) {
129
+ const message =
130
+ err instanceof Error ? err.message : "Unknown error"
131
+ console.error(`\nFailed to create admin user: ${message}`)
132
+ process.exit(1)
133
+ } finally {
134
+ await pool.end()
135
+ }
136
+ },
137
+ )
138
+
139
+ adminCmd
140
+ .command("set-role")
141
+ .description("Change an existing user's admin role")
142
+ .requiredOption("--email <email>", "User email address")
143
+ .requiredOption("--role <role>", "New role to assign")
144
+ .option("--connection <url>", "Database connection URL (overrides config)")
145
+ .action(
146
+ async (opts: { email: string; role: string; connection?: string }) => {
147
+ const cwd = process.cwd()
148
+ const config = loadConfig(cwd)
149
+ const connection = opts.connection ?? config.connection
150
+
151
+ const pg = await importPg()
152
+ const pool = new pg.Pool({ connectionString: connection, max: 2 })
153
+
154
+ try {
155
+ const result = await pool.query(
156
+ `UPDATE auth.users
157
+ SET raw_app_meta_data = raw_app_meta_data || $1::jsonb,
158
+ updated_at = now()
159
+ WHERE email = $2
160
+ RETURNING id, email, raw_app_meta_data`,
161
+ [JSON.stringify({ role: opts.role }), opts.email.toLowerCase()],
162
+ )
163
+
164
+ if (result.rows.length === 0) {
165
+ console.error(`\nNo user found with email "${opts.email}".`)
166
+ process.exit(1)
167
+ }
168
+
169
+ const user = result.rows[0] as {
170
+ id: string
171
+ email: string
172
+ raw_app_meta_data: Record<string, unknown>
173
+ }
174
+
175
+ console.log(`\nRole updated successfully.`)
176
+ console.log(` ID: ${user.id}`)
177
+ console.log(` Email: ${user.email}`)
178
+ console.log(` Role: ${opts.role}\n`)
179
+ } catch (err) {
180
+ const message =
181
+ err instanceof Error ? err.message : "Unknown error"
182
+ console.error(`\nFailed to update role: ${message}`)
183
+ process.exit(1)
184
+ } finally {
185
+ await pool.end()
186
+ }
187
+ },
188
+ )
189
+
190
+ adminCmd
191
+ .command("list-users")
192
+ .description("List users with admin roles")
193
+ .option("--connection <url>", "Database connection URL (overrides config)")
194
+ .action(async (opts: { connection?: string }) => {
195
+ const cwd = process.cwd()
196
+ const config = loadConfig(cwd)
197
+ const connection = opts.connection ?? config.connection
198
+
199
+ const pg = await importPg()
200
+ const pool = new pg.Pool({ connectionString: connection, max: 2 })
201
+
202
+ try {
203
+ const result = await pool.query(
204
+ `SELECT id, email, raw_app_meta_data->>'role' as role, created_at
205
+ FROM auth.users
206
+ WHERE raw_app_meta_data->>'role' IS NOT NULL
207
+ AND raw_app_meta_data->>'role' != 'authenticated'
208
+ ORDER BY created_at ASC`,
209
+ )
210
+
211
+ if (result.rows.length === 0) {
212
+ console.log("\nNo admin users found.")
213
+ console.log(
214
+ "Create one with: supatype admin create-user --email admin@example.com --role admin\n",
215
+ )
216
+ return
217
+ }
218
+
219
+ console.log(
220
+ "\n ID Email Role Created",
221
+ )
222
+ console.log(" " + "-".repeat(100))
223
+ for (const row of result.rows) {
224
+ const r = row as {
225
+ id: string
226
+ email: string
227
+ role: string
228
+ created_at: string
229
+ }
230
+ const date = new Date(r.created_at).toISOString().slice(0, 10)
231
+ console.log(
232
+ ` ${r.id} ${r.email.padEnd(30)} ${r.role.padEnd(12)} ${date}`,
233
+ )
234
+ }
235
+ console.log()
236
+ } catch (err) {
237
+ const message =
238
+ err instanceof Error ? err.message : "Unknown error"
239
+ console.error(`\nFailed to list admin users: ${message}`)
240
+ process.exit(1)
241
+ } finally {
242
+ await pool.end()
243
+ }
244
+ })
245
+ }
246
+
247
+ // ─── First admin user prompt (task 48) ──────────────────────────────────────
248
+ // Called by `supatype push` on initial setup if no admin users exist.
249
+
250
+ export async function promptFirstAdminUser(
251
+ connection: string,
252
+ ): Promise<void> {
253
+ const pg = await importPg()
254
+ const pool = new pg.Pool({ connectionString: connection, max: 2 })
255
+
256
+ try {
257
+ // Check if auth.users table exists
258
+ const tableExists = await pool.query(
259
+ `SELECT EXISTS (
260
+ SELECT FROM information_schema.tables
261
+ WHERE table_schema = 'auth' AND table_name = 'users'
262
+ ) as exists`,
263
+ )
264
+ if (!tableExists.rows[0]?.exists) return
265
+
266
+ // Check if any admin users exist
267
+ const adminCount = await pool.query(
268
+ `SELECT COUNT(*) as count FROM auth.users
269
+ WHERE raw_app_meta_data->>'role' IS NOT NULL
270
+ AND raw_app_meta_data->>'role' != 'authenticated'`,
271
+ )
272
+
273
+ const count = parseInt(
274
+ (adminCount.rows[0] as { count: string }).count,
275
+ 10,
276
+ )
277
+ if (count > 0) return
278
+
279
+ // No admin users — prompt to create one
280
+ console.log("\n No admin users found for the admin panel.")
281
+ const createAdmin = await confirm(
282
+ " Create an admin user now? [y/N] ",
283
+ )
284
+ if (!createAdmin) {
285
+ console.log(
286
+ " Skipped. You can create one later with: supatype admin create-user\n",
287
+ )
288
+ return
289
+ }
290
+
291
+ const email = await prompt(" Admin email: ")
292
+ if (!email || !email.includes("@")) {
293
+ console.log(" Invalid email. Skipping admin user creation.\n")
294
+ return
295
+ }
296
+
297
+ const password = await prompt(
298
+ " Admin password (min 8 chars): ",
299
+ )
300
+ if (!password || password.length < 8) {
301
+ console.log(
302
+ " Password too short. Skipping admin user creation.\n",
303
+ )
304
+ return
305
+ }
306
+
307
+ const passwordHash = await hashPassword(password)
308
+ const appMetadata = JSON.stringify({
309
+ role: "admin",
310
+ provider: "email",
311
+ providers: ["email"],
312
+ })
313
+
314
+ await pool.query(
315
+ `INSERT INTO auth.users (
316
+ email, encrypted_password, role, aud,
317
+ raw_app_meta_data, raw_user_meta_data,
318
+ email_confirmed_at, created_at, updated_at
319
+ ) VALUES (
320
+ $1, $2, 'authenticated', 'authenticated',
321
+ $3::jsonb, '{}'::jsonb,
322
+ now(), now(), now()
323
+ )`,
324
+ [email.toLowerCase(), passwordHash, appMetadata],
325
+ )
326
+
327
+ console.log(`\n Admin user "${email}" created (role: admin).`)
328
+ console.log(` Log in at /admin after starting the dev server.\n`)
329
+ } catch {
330
+ // Non-fatal — if auth schema doesn't exist yet, skip silently
331
+ } finally {
332
+ await pool.end()
333
+ }
334
+ }
335
+
336
+ // ─── Helpers ────────────────────────────────────────────────────────────────────
337
+
338
+ async function importPg(): Promise<typeof import("pg")> {
339
+ try {
340
+ return await import("pg")
341
+ } catch {
342
+ console.error(
343
+ "pg package is required for admin commands. Install it with: pnpm add pg",
344
+ )
345
+ process.exit(1)
346
+ }
347
+ }
348
+
349
+ async function hashPassword(password: string): Promise<string> {
350
+ const salt = randomBytes(16).toString("hex")
351
+ const derived = (await scryptAsync(password, salt, 64)) as Buffer
352
+ return `${salt}:${derived.toString("hex")}`
353
+ }
354
+
355
+ function prompt(question: string): Promise<string> {
356
+ const rl = createInterface({
357
+ input: process.stdin,
358
+ output: process.stdout,
359
+ })
360
+ return new Promise((resolve) => {
361
+ rl.question(question, (answer) => {
362
+ rl.close()
363
+ resolve(answer.trim())
364
+ })
365
+ })
366
+ }
367
+
368
+ async function confirm(question: string): Promise<boolean> {
369
+ const answer = await prompt(question)
370
+ return answer.toLowerCase() === "y"
371
+ }