@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,261 @@
1
+ import type { Command } from "commander"
2
+ import { readFileSync, writeFileSync, existsSync } from "node:fs"
3
+ import { resolve } from "node:path"
4
+ import { APP_COMPOSE_MARKER, KONG_APP_MARKER } from "./init.js"
5
+
6
+ export function registerApp(program: Command): void {
7
+ const appCmd = program
8
+ .command("app")
9
+ .description("Manage your application container in the Supatype stack")
10
+
11
+ appCmd
12
+ .command("add")
13
+ .description("Add your application to the docker-compose stack at /")
14
+ .option("--dockerfile <path>", "Path to your Dockerfile", "./Dockerfile")
15
+ .option("--port <port>", "Port your app listens on", "3000")
16
+ .action((opts: { dockerfile: string; port: string }) => {
17
+ addApp(process.cwd(), opts.dockerfile, opts.port)
18
+ })
19
+
20
+ appCmd
21
+ .command("remove")
22
+ .description("Remove your application from the docker-compose stack")
23
+ .action(() => {
24
+ removeApp(process.cwd())
25
+ })
26
+ }
27
+
28
+ // ─── Implementation ───────────────────────────────────────────────────────────
29
+
30
+ function addApp(cwd: string, dockerfile: string, port: string): void {
31
+ const composePath = resolve(cwd, "docker-compose.yml")
32
+ if (!existsSync(composePath)) {
33
+ console.error("docker-compose.yml not found. Run: supatype init")
34
+ process.exit(1)
35
+ }
36
+
37
+ let compose = readFileSync(composePath, "utf8")
38
+ if (!compose.includes(APP_COMPOSE_MARKER)) {
39
+ console.error("App service slot not found in docker-compose.yml. Is this a supatype project?")
40
+ process.exit(1)
41
+ }
42
+
43
+ if (isAppActive(compose)) {
44
+ console.error("App service is already configured. Run: supatype app remove first.")
45
+ process.exit(1)
46
+ }
47
+
48
+ compose = uncommentServiceBlock(compose, APP_COMPOSE_MARKER, { dockerfile, port })
49
+ writeFileSync(composePath, compose, "utf8")
50
+ console.log(" updated docker-compose.yml")
51
+
52
+ const kongPath = resolve(cwd, ".supatype/kong.yml")
53
+ if (existsSync(kongPath)) {
54
+ let kong = readFileSync(kongPath, "utf8")
55
+ if (kong.includes(KONG_APP_MARKER)) {
56
+ kong = uncommentKongBlock(kong, KONG_APP_MARKER, port)
57
+ writeFileSync(kongPath, kong, "utf8")
58
+ console.log(" updated .supatype/kong.yml")
59
+ }
60
+ }
61
+
62
+ console.log(`\nApp service added (port ${port}). Your app will be available at http://localhost:8000/\n`)
63
+ console.log("Run: supatype dev")
64
+ }
65
+
66
+ function removeApp(cwd: string): void {
67
+ const composePath = resolve(cwd, "docker-compose.yml")
68
+ if (!existsSync(composePath)) {
69
+ console.error("docker-compose.yml not found.")
70
+ process.exit(1)
71
+ }
72
+
73
+ let compose = readFileSync(composePath, "utf8")
74
+ if (!isAppActive(compose)) {
75
+ console.error("No active app service found.")
76
+ process.exit(1)
77
+ }
78
+
79
+ compose = recommentServiceBlock(compose, APP_COMPOSE_MARKER)
80
+ writeFileSync(composePath, compose, "utf8")
81
+ console.log(" updated docker-compose.yml")
82
+
83
+ const kongPath = resolve(cwd, ".supatype/kong.yml")
84
+ if (existsSync(kongPath)) {
85
+ let kong = readFileSync(kongPath, "utf8")
86
+ if (!kong.includes(KONG_APP_MARKER)) {
87
+ // Active route — re-comment it
88
+ kong = recommentKongBlock(kong)
89
+ }
90
+ writeFileSync(kongPath, kong, "utf8")
91
+ console.log(" updated .supatype/kong.yml")
92
+ }
93
+
94
+ console.log("\nApp service removed.\n")
95
+ }
96
+
97
+ // ─── Block manipulation helpers ───────────────────────────────────────────────
98
+
99
+ /** Returns true if the docker-compose has an active (uncommented) app: service. */
100
+ function isAppActive(compose: string): boolean {
101
+ return /^ app:/m.test(compose)
102
+ }
103
+
104
+ /**
105
+ * Finds the commented app service block after the marker line and uncomments it,
106
+ * substituting the dockerfile path and port.
107
+ */
108
+ function uncommentServiceBlock(
109
+ compose: string,
110
+ marker: string,
111
+ opts: { dockerfile: string; port: string },
112
+ ): string {
113
+ const lines = compose.split("\n")
114
+ const markerIdx = lines.findIndex((l) => l === marker)
115
+ if (markerIdx === -1) return compose
116
+
117
+ const result: string[] = []
118
+ let i = 0
119
+
120
+ while (i < lines.length) {
121
+ if (i === markerIdx) {
122
+ // Skip the marker line itself, then uncomment the block
123
+ i++
124
+ while (i < lines.length) {
125
+ const line = lines[i]!
126
+ // End of block: empty line followed by a non-commented service-level line
127
+ if (line === "" && i + 1 < lines.length && !/^ #/.test(lines[i + 1]!)) {
128
+ result.push(line)
129
+ i++
130
+ break
131
+ }
132
+ if (/^ # /.test(line)) {
133
+ // Uncomment: replace " # " prefix with " "
134
+ let uncommented = line.replace(/^ # /, " ")
135
+ // Substitute placeholders
136
+ uncommented = uncommented.replace("./Dockerfile", opts.dockerfile)
137
+ uncommented = uncommented.replace(/- "3000:3000"/, `- "${opts.port}:${opts.port}"`)
138
+ result.push(uncommented)
139
+ } else if (line === "" || /^ #─/.test(line)) {
140
+ // Skip remaining marker-style comment lines
141
+ } else {
142
+ result.push(line)
143
+ }
144
+ i++
145
+ }
146
+ } else {
147
+ result.push(lines[i]!)
148
+ i++
149
+ }
150
+ }
151
+
152
+ return result.join("\n")
153
+ }
154
+
155
+ /**
156
+ * Finds the active app: service block and re-comments it, restoring the marker.
157
+ */
158
+ function recommentServiceBlock(compose: string, marker: string): string {
159
+ const lines = compose.split("\n")
160
+ const appIdx = lines.findIndex((l) => l === " app:")
161
+ if (appIdx === -1) return compose
162
+
163
+ const result: string[] = []
164
+ let i = 0
165
+
166
+ while (i < lines.length) {
167
+ if (i === appIdx) {
168
+ result.push(marker)
169
+ // Re-comment lines until we hit an empty line or the volumes: section
170
+ while (i < lines.length) {
171
+ const line = lines[i]!
172
+ if (line === "" || /^volumes:/.test(line) || /^ \w/.test(line)) {
173
+ result.push(line)
174
+ i++
175
+ break
176
+ }
177
+ // Re-comment: " X..." → " # X..."
178
+ result.push(line.replace(/^ /, " # "))
179
+ i++
180
+ }
181
+ } else {
182
+ result.push(lines[i]!)
183
+ i++
184
+ }
185
+ }
186
+
187
+ return result.join("\n")
188
+ }
189
+
190
+ /**
191
+ * Uncomments the Kong app route block after the marker.
192
+ */
193
+ function uncommentKongBlock(kong: string, marker: string, port: string): string {
194
+ const lines = kong.split("\n")
195
+ const markerIdx = lines.findIndex((l) => l === marker)
196
+ if (markerIdx === -1) return kong
197
+
198
+ const result: string[] = []
199
+ let i = 0
200
+
201
+ while (i < lines.length) {
202
+ if (i === markerIdx) {
203
+ i++ // skip marker
204
+ while (i < lines.length) {
205
+ const line = lines[i]!
206
+ if (line === "" && (i + 1 >= lines.length || !/^ #/.test(lines[i + 1]!))) {
207
+ result.push(line)
208
+ i++
209
+ break
210
+ }
211
+ if (/^ # /.test(line)) {
212
+ let uncommented = line.replace(/^ # /, " ")
213
+ uncommented = uncommented.replace(":3000", `:${port}`)
214
+ result.push(uncommented)
215
+ } else {
216
+ result.push(line)
217
+ }
218
+ i++
219
+ }
220
+ } else {
221
+ result.push(lines[i]!)
222
+ i++
223
+ }
224
+ }
225
+
226
+ return result.join("\n")
227
+ }
228
+
229
+ /**
230
+ * Finds the active Kong app route and re-comments it, restoring the marker.
231
+ */
232
+ function recommentKongBlock(kong: string): string {
233
+ const lines = kong.split("\n")
234
+ // Find " - name: app" (the active route)
235
+ const appIdx = lines.findIndex((l) => /^ - name: app$/.test(l))
236
+ if (appIdx === -1) return kong
237
+
238
+ const result: string[] = []
239
+ let i = 0
240
+
241
+ while (i < lines.length) {
242
+ if (i === appIdx) {
243
+ result.push(KONG_APP_MARKER)
244
+ while (i < lines.length) {
245
+ const line = lines[i]!
246
+ if (line === "" || (i > appIdx && /^ - /.test(line))) {
247
+ result.push(line)
248
+ i++
249
+ break
250
+ }
251
+ result.push(line.replace(/^ /, " # "))
252
+ i++
253
+ }
254
+ } else {
255
+ result.push(lines[i]!)
256
+ i++
257
+ }
258
+ }
259
+
260
+ return result.join("\n")
261
+ }
@@ -0,0 +1,326 @@
1
+ import type { Command } from "commander"
2
+ import { readFileSync, writeFileSync, existsSync } from "node:fs"
3
+ import { resolve } from "node:path"
4
+ import { createInterface } from "node:readline"
5
+
6
+ interface CloudConfig {
7
+ apiUrl: string
8
+ token: string
9
+ projectSlug?: string
10
+ }
11
+
12
+ function loadCloudConfig(cwd: string): CloudConfig | null {
13
+ const configPath = resolve(cwd, ".supatype/cloud.json")
14
+ if (!existsSync(configPath)) return null
15
+ return JSON.parse(readFileSync(configPath, "utf8")) as CloudConfig
16
+ }
17
+
18
+ function saveCloudConfig(cwd: string, config: CloudConfig): void {
19
+ const dir = resolve(cwd, ".supatype")
20
+ if (!existsSync(dir)) {
21
+ const { mkdirSync } = require("node:fs") as typeof import("node:fs")
22
+ mkdirSync(dir, { recursive: true })
23
+ }
24
+ writeFileSync(resolve(cwd, ".supatype/cloud.json"), JSON.stringify(config, null, 2) + "\n", "utf8")
25
+ }
26
+
27
+ async function cloudFetch<T>(config: CloudConfig, method: string, path: string, body?: unknown): Promise<T> {
28
+ const res = await fetch(`${config.apiUrl}/api/v1${path}`, {
29
+ method,
30
+ headers: {
31
+ "Content-Type": "application/json",
32
+ Authorization: `Bearer ${config.token}`,
33
+ },
34
+ ...(body !== undefined ? { body: JSON.stringify(body) } : {}),
35
+ })
36
+
37
+ const json = await res.json() as { data?: T; error?: string; message?: string }
38
+ if (!res.ok) {
39
+ throw new Error(json.message ?? json.error ?? `API error: ${res.status}`)
40
+ }
41
+ return json.data as T
42
+ }
43
+
44
+ function prompt(question: string): Promise<string> {
45
+ const rl = createInterface({ input: process.stdin, output: process.stdout })
46
+ return new Promise((resolve) => {
47
+ rl.question(question, (answer) => {
48
+ rl.close()
49
+ resolve(answer.trim())
50
+ })
51
+ })
52
+ }
53
+
54
+ // ─── Registration ──────────────────────────────────────────────────────────────
55
+
56
+ export function registerCloud(program: Command): void {
57
+ // ── Link ───────────────────────────────────────────────────────────────────
58
+ program
59
+ .command("link")
60
+ .description("Link this local project to a Supatype cloud project")
61
+ .option("--project <slug>", "Project slug to link to")
62
+ .option("--api-url <url>", "Control plane API URL", "https://api.supatype.io")
63
+ .option("--token <token>", "Authentication token")
64
+ .action(async (opts: { project?: string; apiUrl: string; token?: string }) => {
65
+ const cwd = process.cwd()
66
+ const token = opts.token ?? process.env["SUPATYPE_TOKEN"]
67
+ if (!token) {
68
+ console.error("Authentication required. Set SUPATYPE_TOKEN or pass --token.")
69
+ process.exit(1)
70
+ }
71
+
72
+ const config: CloudConfig = { apiUrl: opts.apiUrl, token }
73
+
74
+ if (opts.project) {
75
+ config.projectSlug = opts.project
76
+ } else {
77
+ // List projects and let user choose
78
+ const projects = await cloudFetch<Array<{ slug: string; name: string; status: string; tier: string }>>(
79
+ config, "GET", "/projects",
80
+ )
81
+ if (projects.length === 0) {
82
+ console.error("No projects found. Create one with: supatype projects create <name>")
83
+ process.exit(1)
84
+ }
85
+
86
+ console.log("\nAvailable projects:\n")
87
+ projects.forEach((p, i) => {
88
+ console.log(` ${i + 1}. ${p.name} (${p.slug}) [${p.tier}] — ${p.status}`)
89
+ })
90
+
91
+ const answer = await prompt(`\nSelect project (1-${projects.length}): `)
92
+ const idx = parseInt(answer, 10) - 1
93
+ if (isNaN(idx) || idx < 0 || idx >= projects.length) {
94
+ console.error("Invalid selection.")
95
+ process.exit(1)
96
+ }
97
+ config.projectSlug = projects[idx]!.slug
98
+ }
99
+
100
+ saveCloudConfig(cwd, config)
101
+ console.log(`\nLinked to project: ${config.projectSlug}`)
102
+ console.log(`Config saved to .supatype/cloud.json\n`)
103
+ })
104
+
105
+ // ── Deploy ─────────────────────────────────────────────────────────────────
106
+ program
107
+ .command("deploy")
108
+ .description("Deploy schema to the linked cloud project")
109
+ .option("--environment <name>", "Target environment", "production")
110
+ .action(async (opts: { environment: string }) => {
111
+ const cwd = process.cwd()
112
+ const config = loadCloudConfig(cwd)
113
+ if (!config?.projectSlug) {
114
+ console.error("Not linked to a cloud project. Run: supatype link")
115
+ process.exit(1)
116
+ }
117
+
118
+ console.log(`Deploying to ${config.projectSlug} (${opts.environment})...`)
119
+
120
+ // Load schema AST
121
+ const { loadConfig: loadAppConfig, loadSchemaAst } = await import("../config.js")
122
+ const { ensureEngine, invokeEngine } = await import("../engine.js")
123
+
124
+ const appConfig = loadAppConfig(cwd)
125
+ const ast = loadSchemaAst(appConfig.schema, cwd)
126
+
127
+ // Generate migration SQL locally using the engine
128
+ await ensureEngine()
129
+ const migrateResult = invokeEngine(
130
+ ["migrate", "--format", "sql"],
131
+ JSON.stringify(ast),
132
+ )
133
+ if (migrateResult.exitCode !== 0) {
134
+ console.error("Failed to generate migration:", migrateResult.stderr || migrateResult.stdout)
135
+ process.exit(1)
136
+ }
137
+
138
+ const { createHash } = await import("node:crypto")
139
+ const schemaHash = createHash("sha256").update(JSON.stringify(ast)).digest("hex").slice(0, 16)
140
+
141
+ const deployment = await cloudFetch<{
142
+ id: string; status: string; errorMessage?: string
143
+ }>(config, "POST", `/projects/${config.projectSlug}/deploy`, {
144
+ environment: opts.environment,
145
+ schemaHash,
146
+ migrationSql: migrateResult.stdout,
147
+ })
148
+
149
+ if (deployment.status === "success") {
150
+ console.log(`\nDeployment successful (${deployment.id})`)
151
+ } else if (deployment.status === "failed") {
152
+ console.error(`\nDeployment failed: ${deployment.errorMessage}`)
153
+ process.exit(1)
154
+ } else {
155
+ console.log(`\nDeployment ${deployment.status} (${deployment.id})`)
156
+ }
157
+ })
158
+
159
+ // ── Projects ───────────────────────────────────────────────────────────────
160
+ const projectsCmd = program
161
+ .command("projects")
162
+ .description("Manage cloud projects")
163
+
164
+ projectsCmd
165
+ .command("list")
166
+ .description("List all projects in your organisation")
167
+ .action(async () => {
168
+ const config = getCloudConfigOrExit()
169
+
170
+ const projects = await cloudFetch<Array<{
171
+ name: string; slug: string; tier: string; region: string; status: string
172
+ }>>(config, "GET", "/projects")
173
+
174
+ if (projects.length === 0) {
175
+ console.log("No projects. Create one with: supatype projects create <name>")
176
+ return
177
+ }
178
+
179
+ console.log("\n Name Slug Tier Region Status")
180
+ console.log(" " + "─".repeat(80))
181
+ for (const p of projects) {
182
+ console.log(
183
+ ` ${p.name.padEnd(24)}${p.slug.padEnd(25)}${p.tier.padEnd(8)}${p.region.padEnd(10)}${p.status}`,
184
+ )
185
+ }
186
+ console.log()
187
+ })
188
+
189
+ projectsCmd
190
+ .command("create <name>")
191
+ .description("Create a new project")
192
+ .option("--tier <tier>", "Project tier (free, pro, team)", "free")
193
+ .option("--region <region>", "Region (eu-fsn, eu-nbg, eu-hel)", "eu-fsn")
194
+ .action(async (name: string, opts: { tier: string; region: string }) => {
195
+ const config = getCloudConfigOrExit()
196
+
197
+ console.log(`Creating project "${name}" (${opts.tier}, ${opts.region})...`)
198
+
199
+ const project = await cloudFetch<{ slug: string; name: string; status: string }>(
200
+ config, "POST", "/projects",
201
+ { name, tier: opts.tier, region: opts.region },
202
+ )
203
+
204
+ console.log(`\nProject created: ${project.name} (${project.slug})`)
205
+ console.log(`Status: ${project.status}`)
206
+ console.log(`\nTo link this project: supatype link --project ${project.slug}\n`)
207
+ })
208
+
209
+ projectsCmd
210
+ .command("pause <slug>")
211
+ .description("Pause a project")
212
+ .action(async (slug: string) => {
213
+ const config = getCloudConfigOrExit()
214
+ await cloudFetch(config, "POST", `/projects/${slug}/pause`)
215
+ console.log(`Project ${slug} paused.`)
216
+ })
217
+
218
+ projectsCmd
219
+ .command("resume <slug>")
220
+ .description("Resume a paused project")
221
+ .action(async (slug: string) => {
222
+ const config = getCloudConfigOrExit()
223
+ await cloudFetch(config, "POST", `/projects/${slug}/resume`)
224
+ console.log(`Project ${slug} resumed.`)
225
+ })
226
+
227
+ // ── Domains ────────────────────────────────────────────────────────────────
228
+ const domainsCmd = program
229
+ .command("domains")
230
+ .description("Manage custom domains for a project")
231
+
232
+ domainsCmd
233
+ .command("list")
234
+ .description("List domains for the linked project")
235
+ .action(async () => {
236
+ const config = getCloudConfigOrExit()
237
+ if (!config.projectSlug) {
238
+ console.error("Not linked to a project. Run: supatype link")
239
+ process.exit(1)
240
+ }
241
+
242
+ const domains = await cloudFetch<Array<{
243
+ domain: string; status: string; cnameTarget: string; sslExpiresAt: string | null
244
+ }>>(config, "GET", `/projects/${config.projectSlug}/domains`)
245
+
246
+ if (domains.length === 0) {
247
+ console.log("No custom domains configured.")
248
+ return
249
+ }
250
+
251
+ console.log("\n Domain Status CNAME Target")
252
+ console.log(" " + "─".repeat(80))
253
+ for (const d of domains) {
254
+ console.log(
255
+ ` ${d.domain.padEnd(34)}${d.status.padEnd(21)}${d.cnameTarget}`,
256
+ )
257
+ }
258
+ console.log()
259
+ })
260
+
261
+ domainsCmd
262
+ .command("add <domain>")
263
+ .description("Add a custom domain")
264
+ .action(async (domain: string) => {
265
+ const config = getCloudConfigOrExit()
266
+ if (!config.projectSlug) {
267
+ console.error("Not linked to a project. Run: supatype link")
268
+ process.exit(1)
269
+ }
270
+
271
+ const result = await cloudFetch<{ domain: string; cnameTarget: string; instructions: string }>(
272
+ config, "POST", `/projects/${config.projectSlug}/domains`,
273
+ { domain },
274
+ )
275
+
276
+ console.log(`\nDomain added: ${result.domain}`)
277
+ console.log(`\n${result.instructions}`)
278
+ console.log(`\nAfter adding the CNAME record, verify with: supatype domains verify ${domain}\n`)
279
+ })
280
+
281
+ domainsCmd
282
+ .command("remove <domainId>")
283
+ .description("Remove a custom domain")
284
+ .action(async (domainId: string) => {
285
+ const config = getCloudConfigOrExit()
286
+ if (!config.projectSlug) {
287
+ console.error("Not linked to a project. Run: supatype link")
288
+ process.exit(1)
289
+ }
290
+
291
+ await cloudFetch(config, "DELETE", `/projects/${config.projectSlug}/domains/${domainId}`)
292
+ console.log("Domain removed.")
293
+ })
294
+
295
+ domainsCmd
296
+ .command("verify <domainId>")
297
+ .description("Verify CNAME and provision SSL for a domain")
298
+ .action(async (domainId: string) => {
299
+ const config = getCloudConfigOrExit()
300
+ if (!config.projectSlug) {
301
+ console.error("Not linked to a project. Run: supatype link")
302
+ process.exit(1)
303
+ }
304
+
305
+ const result = await cloudFetch<{ domain: string; status: string }>(
306
+ config, "POST", `/projects/${config.projectSlug}/domains/${domainId}/verify`,
307
+ )
308
+
309
+ console.log(`Domain ${result.domain}: ${result.status}`)
310
+ })
311
+ }
312
+
313
+ function getCloudConfigOrExit(): CloudConfig {
314
+ const cwd = process.cwd()
315
+ let config = loadCloudConfig(cwd)
316
+ if (!config) {
317
+ const token = process.env["SUPATYPE_TOKEN"]
318
+ const apiUrl = process.env["SUPATYPE_API_URL"] ?? "https://api.supatype.io"
319
+ if (!token) {
320
+ console.error("Not connected to Supatype Cloud. Run: supatype link, or set SUPATYPE_TOKEN.")
321
+ process.exit(1)
322
+ }
323
+ config = { apiUrl, token }
324
+ }
325
+ return config
326
+ }