@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,145 @@
1
+ /**
2
+ * Database connection commands:
3
+ * supatype db connection-string — show the connection string for the linked project
4
+ * supatype db reset-password — reset the database password
5
+ */
6
+
7
+ import type { Command } from "commander"
8
+ import { loadConfig } from "../config.js"
9
+
10
+ export function registerDb(program: Command): void {
11
+ const db = program
12
+ .command("db")
13
+ .description("Database connection management")
14
+
15
+ // supatype db connection-string
16
+ db
17
+ .command("connection-string")
18
+ .description("Show the database connection string for the linked project")
19
+ .option("--transaction", "Show the transaction pool URL (for serverless/edge functions)")
20
+ .option("--env <name>", "Environment name", "production")
21
+ .action(async (opts: { transaction?: boolean; env?: string }) => {
22
+ const cwd = process.cwd()
23
+ const config = loadConfig(cwd)
24
+
25
+ // The connection string is stored in the project config or fetched from cloud
26
+ if (config.connection) {
27
+ if (opts.transaction) {
28
+ // Convert session URL to transaction pool URL (port 6432)
29
+ const txUrl = config.connection.replace(/:5432\//, ":6432/")
30
+ console.log(txUrl)
31
+ } else {
32
+ console.log(config.connection)
33
+ }
34
+ console.log()
35
+ console.log("Session mode (port 5432): for interactive tools (psql, DataGrip, TablePlus)")
36
+ console.log("Transaction mode (port 6432): for application servers and serverless functions")
37
+ return
38
+ }
39
+
40
+ // If linked to a cloud project, fetch from the API
41
+ if (config.projectRef) {
42
+ const apiUrl = config.apiUrl || "https://api.supatype.io"
43
+ const envName = opts.env || "production"
44
+
45
+ try {
46
+ const res = await fetch(
47
+ `${apiUrl}/platform/v1/projects/${config.projectRef}/environments`,
48
+ {
49
+ headers: {
50
+ Authorization: `Bearer ${config.accessToken || process.env["SUPATYPE_ACCESS_TOKEN"] || ""}`,
51
+ },
52
+ },
53
+ )
54
+
55
+ if (!res.ok) {
56
+ console.error(`Failed to fetch project info: ${res.status}`)
57
+ process.exitCode = 1
58
+ return
59
+ }
60
+
61
+ const { data } = (await res.json()) as { data: Array<{ name: string; databaseUrl?: string }> }
62
+ const env = data.find((e) => e.name === envName)
63
+
64
+ if (!env) {
65
+ console.error(`Environment "${envName}" not found`)
66
+ process.exitCode = 1
67
+ return
68
+ }
69
+
70
+ const connStr = env.databaseUrl || "Connection string not available"
71
+ if (opts.transaction) {
72
+ console.log(connStr.replace(/:5432\//, ":6432/"))
73
+ } else {
74
+ console.log(connStr)
75
+ }
76
+
77
+ console.log()
78
+ console.log("Session mode (port 5432): for interactive tools (psql, DataGrip, TablePlus)")
79
+ console.log("Transaction mode (port 6432): for application servers and serverless functions")
80
+ } catch (err) {
81
+ console.error("Failed to fetch connection string:", (err as Error).message)
82
+ process.exitCode = 1
83
+ }
84
+ return
85
+ }
86
+
87
+ console.error(
88
+ "No connection configured. Either:\n" +
89
+ " • Set 'connection' in supatype.config.ts\n" +
90
+ " • Link to a cloud project: npx supatype link --project <ref>",
91
+ )
92
+ process.exitCode = 1
93
+ })
94
+
95
+ // supatype db reset-password
96
+ db
97
+ .command("reset-password")
98
+ .description("Reset the database password for the linked project")
99
+ .option("--env <name>", "Environment name", "production")
100
+ .action(async (opts: { env?: string }) => {
101
+ const cwd = process.cwd()
102
+ const config = loadConfig(cwd)
103
+
104
+ if (!config.projectRef) {
105
+ console.error("Not linked to a cloud project. Run: npx supatype link --project <ref>")
106
+ process.exitCode = 1
107
+ return
108
+ }
109
+
110
+ const apiUrl = config.apiUrl || "https://api.supatype.io"
111
+ const envName = opts.env || "production"
112
+
113
+ try {
114
+ const res = await fetch(
115
+ `${apiUrl}/platform/v1/projects/${config.projectRef}/environments/${envName}/reset-db-password`,
116
+ {
117
+ method: "POST",
118
+ headers: {
119
+ Authorization: `Bearer ${config.accessToken || process.env["SUPATYPE_ACCESS_TOKEN"] || ""}`,
120
+ "Content-Type": "application/json",
121
+ },
122
+ },
123
+ )
124
+
125
+ if (!res.ok) {
126
+ const body = await res.text()
127
+ console.error(`Failed to reset password: ${res.status} ${body}`)
128
+ process.exitCode = 1
129
+ return
130
+ }
131
+
132
+ const { data } = (await res.json()) as { data: { password: string; connectionString: string } }
133
+ console.log("Database password reset successfully.")
134
+ console.log()
135
+ console.log(`New password: ${data.password}`)
136
+ console.log(`Connection string: ${data.connectionString}`)
137
+ console.log()
138
+ console.log("Warning: Existing database connections have been terminated.")
139
+ console.log("Update your application with the new connection string.")
140
+ } catch (err) {
141
+ console.error("Failed to reset password:", (err as Error).message)
142
+ process.exitCode = 1
143
+ }
144
+ })
145
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Deployment tier limits for static site hosting.
3
+ */
4
+
5
+ export type Tier = "free" | "pro" | "team" | "enterprise"
6
+
7
+ export interface DeploymentLimits {
8
+ maxBuildOutputMb: number
9
+ buildMinutesPerMonth: number
10
+ maxPreviewDeployments: number
11
+ deploymentRetention: number
12
+ cdnEnabled: boolean
13
+ buildMinuteOverageRate: number // £ per minute, 0 = no overage allowed
14
+ }
15
+
16
+ export const TIER_LIMITS: Record<Tier, DeploymentLimits> = {
17
+ free: {
18
+ maxBuildOutputMb: 50,
19
+ buildMinutesPerMonth: 100,
20
+ maxPreviewDeployments: 1,
21
+ deploymentRetention: 3,
22
+ cdnEnabled: false,
23
+ buildMinuteOverageRate: 0,
24
+ },
25
+ pro: {
26
+ maxBuildOutputMb: 500,
27
+ buildMinutesPerMonth: 1000,
28
+ maxPreviewDeployments: 5,
29
+ deploymentRetention: 10,
30
+ cdnEnabled: true,
31
+ buildMinuteOverageRate: 0.01,
32
+ },
33
+ team: {
34
+ maxBuildOutputMb: 2048,
35
+ buildMinutesPerMonth: 5000,
36
+ maxPreviewDeployments: 20,
37
+ deploymentRetention: 25,
38
+ cdnEnabled: true,
39
+ buildMinuteOverageRate: 0.01,
40
+ },
41
+ enterprise: {
42
+ maxBuildOutputMb: -1,
43
+ buildMinutesPerMonth: -1,
44
+ maxPreviewDeployments: -1,
45
+ deploymentRetention: -1,
46
+ cdnEnabled: true,
47
+ buildMinuteOverageRate: 0,
48
+ },
49
+ }
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Deploy commands:
3
+ * supatype deploy — push schema + build & deploy static site
4
+ * supatype deploy --app-only — only build & deploy the static site
5
+ * supatype deploy --schema-only — only push schema changes
6
+ * supatype deploy --skip-build — deploy existing build output (no build step)
7
+ * supatype deploy --preview — deploy to a preview URL
8
+ * supatype deploy rollback — roll back to previous deployment
9
+ * supatype deploy status — show current deployment info
10
+ * supatype deploy logs <version> — show build logs
11
+ */
12
+
13
+ import type { Command } from "commander"
14
+ import { existsSync, readdirSync, statSync, createReadStream } from "node:fs"
15
+ import { join } from "node:path"
16
+ import { loadConfig, loadSchemaAst } from "../config.js"
17
+ import { ensureEngine, invokeEngine } from "../engine.js"
18
+ import { resolveAppConfig, validateStaticMode, validateBuildOutput, detectPackageManager } from "../app/framework.js"
19
+ import { TIER_LIMITS, type Tier } from "./deploy-types.js"
20
+ import { spawnSync } from "node:child_process"
21
+
22
+ export function registerDeploy(program: Command): void {
23
+ const deploy = program
24
+ .command("deploy")
25
+ .description("Build and deploy your application")
26
+ .option("--app-only", "Skip schema push, only deploy the static site")
27
+ .option("--schema-only", "Skip app build, only push schema changes")
28
+ .option("--skip-build", "Deploy existing build output without building")
29
+ .option("--preview", "Deploy to a temporary preview URL")
30
+ .option("--yes", "Skip confirmation prompts")
31
+ .action(async (opts: {
32
+ appOnly?: boolean
33
+ schemaOnly?: boolean
34
+ skipBuild?: boolean
35
+ preview?: boolean
36
+ yes?: boolean
37
+ }) => {
38
+ const cwd = process.cwd()
39
+ const config = loadConfig(cwd)
40
+
41
+ // Step 1: Schema push (unless --app-only or --skip-build)
42
+ if (!opts.appOnly && !opts.skipBuild) {
43
+ console.log("=== Schema Push ===")
44
+ await ensureEngine()
45
+
46
+ const ast = loadSchemaAst(config.schema, cwd)
47
+ const diffResult = invokeEngine(
48
+ ["diff", "--connection", config.connection, "--format", "json"],
49
+ JSON.stringify(ast),
50
+ )
51
+ if (diffResult.exitCode !== 0) {
52
+ console.error("Schema diff failed:", diffResult.stderr || diffResult.stdout)
53
+ process.exit(1)
54
+ }
55
+
56
+ const diff = JSON.parse(diffResult.stdout)
57
+ const ops = diff.operations ?? []
58
+
59
+ if (ops.length > 0) {
60
+ console.log(`${ops.length} schema change(s) to apply.`)
61
+ const migrateResult = invokeEngine(
62
+ ["migrate", "--connection", config.connection],
63
+ JSON.stringify(ast),
64
+ )
65
+ if (migrateResult.exitCode !== 0) {
66
+ console.error("Migration failed:", migrateResult.stderr)
67
+ process.exit(1)
68
+ }
69
+ console.log("Schema changes applied.")
70
+ } else {
71
+ console.log("Schema is up to date.")
72
+ }
73
+ }
74
+
75
+ // Step 2: App build & deploy (unless --schema-only)
76
+ if (!opts.schemaOnly) {
77
+ if (!config.app) {
78
+ if (opts.appOnly) {
79
+ console.error("No app configuration found in supatype.config.ts")
80
+ process.exit(1)
81
+ }
82
+ // No app config — just skip app deployment silently
83
+ return
84
+ }
85
+
86
+ console.log("\n=== App Build & Deploy ===")
87
+ const appConfig = resolveAppConfig(config.app, cwd)
88
+
89
+ // Validate static mode
90
+ const staticError = validateStaticMode(appConfig.framework, appConfig.directory)
91
+ if (staticError) {
92
+ console.error(staticError)
93
+ process.exit(1)
94
+ }
95
+
96
+ // Build step
97
+ if (!opts.skipBuild && appConfig.buildCommand) {
98
+ console.log(`Framework: ${appConfig.framework}`)
99
+ console.log(`Build command: ${appConfig.buildCommand}`)
100
+ console.log(`Output directory: ${appConfig.outputDirectory}`)
101
+ console.log()
102
+
103
+ // Inject environment variables
104
+ const buildEnv: Record<string, string> = {
105
+ ...process.env as Record<string, string>,
106
+ ...appConfig.env,
107
+ }
108
+
109
+ // Inject Supatype URLs if project is linked
110
+ if (config.projectRef) {
111
+ buildEnv["NEXT_PUBLIC_SUPATYPE_URL"] = config.apiUrl || `https://${config.projectRef}.supatype.io`
112
+ buildEnv["VITE_SUPATYPE_URL"] = buildEnv["NEXT_PUBLIC_SUPATYPE_URL"]
113
+ buildEnv["PUBLIC_SUPATYPE_URL"] = buildEnv["NEXT_PUBLIC_SUPATYPE_URL"]
114
+ // NEVER inject service_role key — only anon key is safe for client-side
115
+ }
116
+
117
+ // Install dependencies
118
+ const pm = detectPackageManager(appConfig.directory)
119
+ console.log(`Installing dependencies (${pm})...`)
120
+ const installResult = spawnSync(pm, ["install"], {
121
+ cwd: appConfig.directory,
122
+ stdio: "inherit",
123
+ env: buildEnv,
124
+ timeout: 5 * 60 * 1000, // 5 minute timeout
125
+ })
126
+ if (installResult.status !== 0) {
127
+ console.error("Dependency installation failed.")
128
+ process.exit(1)
129
+ }
130
+
131
+ // Run build
132
+ console.log("\nBuilding...")
133
+ const [buildCmd, ...buildArgs] = appConfig.buildCommand.split(" ")
134
+ const buildResult = spawnSync(buildCmd!, buildArgs, {
135
+ cwd: appConfig.directory,
136
+ stdio: "inherit",
137
+ env: buildEnv,
138
+ timeout: 10 * 60 * 1000, // 10 minute timeout
139
+ })
140
+ if (buildResult.status !== 0) {
141
+ console.error("Build failed.")
142
+ process.exit(1)
143
+ }
144
+ }
145
+
146
+ // Validate build output
147
+ const maxSizeMb = 500 // Default, should be tier-aware in cloud
148
+ const validationError = validateBuildOutput(appConfig.outputDirectory, maxSizeMb)
149
+ if (validationError) {
150
+ console.error(validationError)
151
+ process.exit(1)
152
+ }
153
+
154
+ // Deploy
155
+ if (config.projectRef) {
156
+ // Cloud deployment — upload to API
157
+ await deployToCloud(config, appConfig.outputDirectory, opts.preview ?? false)
158
+ } else {
159
+ // Self-host — copy to serving directory
160
+ deploySelfHost(appConfig.outputDirectory, cwd)
161
+ }
162
+
163
+ console.log("\nDeployment complete!")
164
+ if (config.projectRef) {
165
+ const url = opts.preview
166
+ ? `https://preview-${Date.now().toString(36)}.${config.projectRef}.supatype.io`
167
+ : `https://${config.projectRef}.supatype.io`
168
+ console.log(`URL: ${url}`)
169
+ }
170
+ }
171
+ })
172
+
173
+ // supatype deploy rollback
174
+ deploy
175
+ .command("rollback")
176
+ .description("Roll back to the previous static site deployment")
177
+ .action(async () => {
178
+ const config = loadConfig(process.cwd())
179
+ if (!config.projectRef) {
180
+ console.error("Not linked to a cloud project. Rollback is only available for cloud deployments.")
181
+ process.exit(1)
182
+ }
183
+
184
+ const apiUrl = config.apiUrl || "https://api.supatype.io"
185
+ const token = config.accessToken || process.env["SUPATYPE_ACCESS_TOKEN"] || ""
186
+
187
+ const res = await fetch(`${apiUrl}/platform/v1/projects/${config.projectRef}/deployments/rollback`, {
188
+ method: "POST",
189
+ headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
190
+ })
191
+
192
+ if (!res.ok) {
193
+ const body = await res.text()
194
+ console.error(`Rollback failed: ${res.status} ${body}`)
195
+ process.exit(1)
196
+ }
197
+
198
+ const { data } = await res.json() as { data: { version: string; message: string } }
199
+ console.log(`Rolled back to deployment ${data.version}.`)
200
+ })
201
+
202
+ // supatype deploy status
203
+ deploy
204
+ .command("status")
205
+ .description("Show current deployment status")
206
+ .action(async () => {
207
+ const config = loadConfig(process.cwd())
208
+ if (!config.projectRef) {
209
+ console.error("Not linked to a cloud project.")
210
+ process.exit(1)
211
+ }
212
+
213
+ const apiUrl = config.apiUrl || "https://api.supatype.io"
214
+ const token = config.accessToken || process.env["SUPATYPE_ACCESS_TOKEN"] || ""
215
+
216
+ const res = await fetch(`${apiUrl}/platform/v1/projects/${config.projectRef}/deployments/current`, {
217
+ headers: { Authorization: `Bearer ${token}` },
218
+ })
219
+
220
+ if (!res.ok) {
221
+ console.error("No active deployment found.")
222
+ return
223
+ }
224
+
225
+ const { data } = await res.json() as { data: {
226
+ version: string
227
+ timestamp: string
228
+ size: number
229
+ buildDuration: number
230
+ url: string
231
+ status: string
232
+ }}
233
+
234
+ console.log(`Deployment: ${data.version}`)
235
+ console.log(`Status: ${data.status}`)
236
+ console.log(`Deployed: ${data.timestamp}`)
237
+ console.log(`Size: ${(data.size / (1024 * 1024)).toFixed(1)}MB`)
238
+ console.log(`Build duration: ${data.buildDuration}s`)
239
+ console.log(`URL: ${data.url}`)
240
+ })
241
+
242
+ // supatype deploy logs <version>
243
+ deploy
244
+ .command("logs [version]")
245
+ .description("Show build logs for a deployment")
246
+ .action(async (version?: string) => {
247
+ const config = loadConfig(process.cwd())
248
+ if (!config.projectRef) {
249
+ console.error("Not linked to a cloud project.")
250
+ process.exit(1)
251
+ }
252
+
253
+ const apiUrl = config.apiUrl || "https://api.supatype.io"
254
+ const token = config.accessToken || process.env["SUPATYPE_ACCESS_TOKEN"] || ""
255
+
256
+ const versionPath = version ? `/${version}` : "/current"
257
+ const res = await fetch(`${apiUrl}/platform/v1/projects/${config.projectRef}/deployments${versionPath}/logs`, {
258
+ headers: { Authorization: `Bearer ${token}` },
259
+ })
260
+
261
+ if (!res.ok) {
262
+ console.error("Logs not found.")
263
+ process.exit(1)
264
+ }
265
+
266
+ const { data } = await res.json() as { data: { logs: string } }
267
+ console.log(data.logs)
268
+ })
269
+ }
270
+
271
+ async function deployToCloud(
272
+ config: { projectRef?: string; apiUrl?: string; accessToken?: string },
273
+ outputDir: string,
274
+ isPreview: boolean,
275
+ ): Promise<void> {
276
+ const apiUrl = config.apiUrl || "https://api.supatype.io"
277
+ const token = config.accessToken || process.env["SUPATYPE_ACCESS_TOKEN"] || ""
278
+
279
+ console.log("Uploading build artifacts...")
280
+
281
+ // Collect files from output directory
282
+ const files = collectFiles(outputDir, outputDir)
283
+ console.log(`${files.length} files to upload (${formatSize(files.reduce((s, f) => s + f.size, 0))})`)
284
+
285
+ // Create deployment
286
+ const createRes = await fetch(`${apiUrl}/platform/v1/projects/${config.projectRef}/deployments`, {
287
+ method: "POST",
288
+ headers: {
289
+ Authorization: `Bearer ${token}`,
290
+ "Content-Type": "application/json",
291
+ },
292
+ body: JSON.stringify({
293
+ preview: isPreview,
294
+ fileCount: files.length,
295
+ totalSize: files.reduce((s, f) => s + f.size, 0),
296
+ files: files.map((f) => ({ path: f.relativePath, size: f.size })),
297
+ }),
298
+ })
299
+
300
+ if (!createRes.ok) {
301
+ const body = await createRes.text()
302
+ throw new Error(`Failed to create deployment: ${createRes.status} ${body}`)
303
+ }
304
+
305
+ const { data } = await createRes.json() as { data: { deploymentId: string; uploadUrl: string } }
306
+
307
+ // Upload files (simplified — in production, use multipart or presigned URLs)
308
+ for (const file of files) {
309
+ const content = require("node:fs").readFileSync(file.absolutePath)
310
+ await fetch(`${data.uploadUrl}/${file.relativePath}`, {
311
+ method: "PUT",
312
+ headers: {
313
+ Authorization: `Bearer ${token}`,
314
+ "Content-Type": "application/octet-stream",
315
+ "X-Deployment-Id": data.deploymentId,
316
+ },
317
+ body: content,
318
+ })
319
+ }
320
+
321
+ // Finalize deployment
322
+ await fetch(`${apiUrl}/platform/v1/projects/${config.projectRef}/deployments/${data.deploymentId}/finalize`, {
323
+ method: "POST",
324
+ headers: { Authorization: `Bearer ${token}` },
325
+ })
326
+ }
327
+
328
+ function deploySelfHost(outputDir: string, cwd: string): void {
329
+ const servingDir = join(cwd, ".supatype", "static")
330
+ const { mkdirSync, cpSync } = require("node:fs") as typeof import("node:fs")
331
+ mkdirSync(servingDir, { recursive: true })
332
+ cpSync(outputDir, servingDir, { recursive: true })
333
+ console.log(`Static files deployed to ${servingDir}`)
334
+ }
335
+
336
+ interface FileEntry {
337
+ absolutePath: string
338
+ relativePath: string
339
+ size: number
340
+ }
341
+
342
+ function collectFiles(dir: string, baseDir: string): FileEntry[] {
343
+ const files: FileEntry[] = []
344
+ const { readdirSync, statSync } = require("node:fs") as typeof import("node:fs")
345
+ const { relative } = require("node:path") as typeof import("node:path")
346
+
347
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
348
+ const fullPath = join(dir, entry.name)
349
+ if (entry.isFile()) {
350
+ files.push({
351
+ absolutePath: fullPath,
352
+ relativePath: relative(baseDir, fullPath).replace(/\\/g, "/"),
353
+ size: statSync(fullPath).size,
354
+ })
355
+ } else if (entry.isDirectory()) {
356
+ files.push(...collectFiles(fullPath, baseDir))
357
+ }
358
+ }
359
+ return files
360
+ }
361
+
362
+ function formatSize(bytes: number): string {
363
+ if (bytes < 1024) return `${bytes}B`
364
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)}KB`
365
+ return `${(bytes / (1024 * 1024)).toFixed(1)}MB`
366
+ }