@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.
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test.log +7 -0
- package/.turbo/turbo-typecheck.log +4 -0
- package/bin/dev-entry.ts +2 -0
- package/bin/supatype.js +5 -0
- package/dist/app/framework.d.ts +44 -0
- package/dist/app/framework.d.ts.map +1 -0
- package/dist/app/framework.js +200 -0
- package/dist/app/framework.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +55 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/admin.d.ts +4 -0
- package/dist/commands/admin.d.ts.map +1 -0
- package/dist/commands/admin.js +270 -0
- package/dist/commands/admin.js.map +1 -0
- package/dist/commands/app.d.ts +3 -0
- package/dist/commands/app.d.ts.map +1 -0
- package/dist/commands/app.js +235 -0
- package/dist/commands/app.js.map +1 -0
- package/dist/commands/cloud.d.ts +3 -0
- package/dist/commands/cloud.d.ts.map +1 -0
- package/dist/commands/cloud.js +256 -0
- package/dist/commands/cloud.js.map +1 -0
- package/dist/commands/db.d.ts +8 -0
- package/dist/commands/db.d.ts.map +1 -0
- package/dist/commands/db.js +123 -0
- package/dist/commands/db.js.map +1 -0
- package/dist/commands/deploy-types.d.ts +14 -0
- package/dist/commands/deploy-types.d.ts.map +1 -0
- package/dist/commands/deploy-types.js +38 -0
- package/dist/commands/deploy-types.js.map +1 -0
- package/dist/commands/deploy.d.ts +14 -0
- package/dist/commands/deploy.d.ts.map +1 -0
- package/dist/commands/deploy.js +295 -0
- package/dist/commands/deploy.js.map +1 -0
- package/dist/commands/dev.d.ts +3 -0
- package/dist/commands/dev.d.ts.map +1 -0
- package/dist/commands/dev.js +428 -0
- package/dist/commands/dev.js.map +1 -0
- package/dist/commands/diff.d.ts +3 -0
- package/dist/commands/diff.d.ts.map +1 -0
- package/dist/commands/diff.js +39 -0
- package/dist/commands/diff.js.map +1 -0
- package/dist/commands/engine.d.ts +9 -0
- package/dist/commands/engine.d.ts.map +1 -0
- package/dist/commands/engine.js +99 -0
- package/dist/commands/engine.js.map +1 -0
- package/dist/commands/functions.d.ts +3 -0
- package/dist/commands/functions.d.ts.map +1 -0
- package/dist/commands/functions.js +762 -0
- package/dist/commands/functions.js.map +1 -0
- package/dist/commands/generate.d.ts +3 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +28 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/init.d.ts +7 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +515 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/keys.d.ts +4 -0
- package/dist/commands/keys.d.ts.map +1 -0
- package/dist/commands/keys.js +57 -0
- package/dist/commands/keys.js.map +1 -0
- package/dist/commands/logs.d.ts +6 -0
- package/dist/commands/logs.d.ts.map +1 -0
- package/dist/commands/logs.js +52 -0
- package/dist/commands/logs.js.map +1 -0
- package/dist/commands/migrate.d.ts +3 -0
- package/dist/commands/migrate.d.ts.map +1 -0
- package/dist/commands/migrate.js +71 -0
- package/dist/commands/migrate.js.map +1 -0
- package/dist/commands/plugins.d.ts +3 -0
- package/dist/commands/plugins.d.ts.map +1 -0
- package/dist/commands/plugins.js +431 -0
- package/dist/commands/plugins.js.map +1 -0
- package/dist/commands/pull.d.ts +3 -0
- package/dist/commands/pull.d.ts.map +1 -0
- package/dist/commands/pull.js +73 -0
- package/dist/commands/pull.js.map +1 -0
- package/dist/commands/push.d.ts +3 -0
- package/dist/commands/push.d.ts.map +1 -0
- package/dist/commands/push.js +87 -0
- package/dist/commands/push.js.map +1 -0
- package/dist/commands/seed.d.ts +3 -0
- package/dist/commands/seed.d.ts.map +1 -0
- package/dist/commands/seed.js +22 -0
- package/dist/commands/seed.js.map +1 -0
- package/dist/commands/self-host.d.ts +3 -0
- package/dist/commands/self-host.d.ts.map +1 -0
- package/dist/commands/self-host.js +796 -0
- package/dist/commands/self-host.js.map +1 -0
- package/dist/commands/status.d.ts +6 -0
- package/dist/commands/status.d.ts.map +1 -0
- package/dist/commands/status.js +69 -0
- package/dist/commands/status.js.map +1 -0
- package/dist/config.d.ts +106 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +66 -0
- package/dist/config.js.map +1 -0
- package/dist/engine/cache.d.ts +37 -0
- package/dist/engine/cache.d.ts.map +1 -0
- package/dist/engine/cache.js +121 -0
- package/dist/engine/cache.js.map +1 -0
- package/dist/engine/download.d.ts +19 -0
- package/dist/engine/download.d.ts.map +1 -0
- package/dist/engine/download.js +108 -0
- package/dist/engine/download.js.map +1 -0
- package/dist/engine/platform.d.ts +24 -0
- package/dist/engine/platform.d.ts.map +1 -0
- package/dist/engine/platform.js +50 -0
- package/dist/engine/platform.js.map +1 -0
- package/dist/engine/resolve.d.ts +37 -0
- package/dist/engine/resolve.d.ts.map +1 -0
- package/dist/engine/resolve.js +133 -0
- package/dist/engine/resolve.js.map +1 -0
- package/dist/engine/update-notify.d.ts +11 -0
- package/dist/engine/update-notify.d.ts.map +1 -0
- package/dist/engine/update-notify.js +43 -0
- package/dist/engine/update-notify.js.map +1 -0
- package/dist/engine/verify.d.ts +50 -0
- package/dist/engine/verify.d.ts.map +1 -0
- package/dist/engine/verify.js +161 -0
- package/dist/engine/verify.js.map +1 -0
- package/dist/engine-version.d.ts +35 -0
- package/dist/engine-version.d.ts.map +1 -0
- package/dist/engine-version.js +35 -0
- package/dist/engine-version.js.map +1 -0
- package/dist/engine.d.ts +34 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +76 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +12 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +10 -0
- package/dist/index.js.map +1 -0
- package/dist/jwt.d.ts +3 -0
- package/dist/jwt.d.ts.map +1 -0
- package/dist/jwt.js +13 -0
- package/dist/jwt.js.map +1 -0
- package/dist/pull-utils.d.ts +16 -0
- package/dist/pull-utils.d.ts.map +1 -0
- package/dist/pull-utils.js +65 -0
- package/dist/pull-utils.js.map +1 -0
- package/dist/scripts/postinstall.d.ts +12 -0
- package/dist/scripts/postinstall.d.ts.map +1 -0
- package/dist/scripts/postinstall.js +31 -0
- package/dist/scripts/postinstall.js.map +1 -0
- package/dist/tsx-runner.d.ts +18 -0
- package/dist/tsx-runner.d.ts.map +1 -0
- package/dist/tsx-runner.js +62 -0
- package/dist/tsx-runner.js.map +1 -0
- package/package.json +36 -0
- package/src/app/framework.ts +249 -0
- package/src/cli.ts +58 -0
- package/src/commands/admin.ts +371 -0
- package/src/commands/app.ts +261 -0
- package/src/commands/cloud.ts +326 -0
- package/src/commands/db.ts +145 -0
- package/src/commands/deploy-types.ts +49 -0
- package/src/commands/deploy.ts +366 -0
- package/src/commands/dev.ts +477 -0
- package/src/commands/diff.ts +61 -0
- package/src/commands/engine.ts +133 -0
- package/src/commands/functions.ts +919 -0
- package/src/commands/generate.ts +31 -0
- package/src/commands/init.ts +532 -0
- package/src/commands/keys.ts +66 -0
- package/src/commands/logs.ts +58 -0
- package/src/commands/migrate.ts +83 -0
- package/src/commands/plugins.ts +508 -0
- package/src/commands/pull.ts +96 -0
- package/src/commands/push.ts +119 -0
- package/src/commands/seed.ts +26 -0
- package/src/commands/self-host.ts +932 -0
- package/src/commands/status.ts +83 -0
- package/src/config.ts +190 -0
- package/src/engine/cache.ts +135 -0
- package/src/engine/download.ts +143 -0
- package/src/engine/platform.ts +66 -0
- package/src/engine/resolve.ts +197 -0
- package/src/engine/update-notify.ts +50 -0
- package/src/engine/verify.ts +206 -0
- package/src/engine-version.ts +39 -0
- package/src/engine.ts +99 -0
- package/src/index.ts +19 -0
- package/src/jwt.ts +14 -0
- package/src/pull-utils.ts +57 -0
- package/src/scripts/postinstall.ts +40 -0
- package/src/tsx-runner.ts +79 -0
- package/tests/cli-help.test.ts +107 -0
- package/tests/config.test.ts +117 -0
- package/tests/engine-distribution.test.ts +418 -0
- package/tests/init.test.ts +184 -0
- package/tests/keys.test.ts +160 -0
- package/tests/pull-utils.test.ts +115 -0
- package/tests/tsx-runner.test.ts +66 -0
- package/tsconfig.json +10 -0
- 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
|
+
}
|