@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,83 @@
1
+ /**
2
+ * supatype status — show current state of local dev services.
3
+ */
4
+ import type { Command } from "commander"
5
+ import { spawnSync } from "node:child_process"
6
+
7
+ interface ServiceStatus {
8
+ name: string
9
+ container: string
10
+ status: "running" | "stopped" | "error"
11
+ port?: number
12
+ uptime?: string
13
+ }
14
+
15
+ export function registerStatus(program: Command): void {
16
+ program
17
+ .command("status")
18
+ .description("Show current state of local dev services")
19
+ .action(() => {
20
+ const services: ServiceStatus[] = [
21
+ { name: "Postgres", container: "supatype-postgres", port: 5432 },
22
+ { name: "PostgREST", container: "supatype-postgrest", port: 3000 },
23
+ { name: "GoTrue", container: "supatype-gotrue", port: 9999 },
24
+ { name: "Kong", container: "supatype-kong", port: 8000 },
25
+ { name: "MinIO", container: "supatype-minio", port: 9000 },
26
+ { name: "Realtime", container: "supatype-realtime", port: 4000 },
27
+ { name: "Studio", container: "supatype-studio", port: 3100 },
28
+ ].map((svc) => {
29
+ const status = getContainerStatus(svc.container)
30
+ const uptime = getContainerUptime(svc.container)
31
+ return { ...svc, status, ...(uptime !== undefined && { uptime }) }
32
+ })
33
+
34
+ console.log("Supatype Local Development Stack\n")
35
+
36
+ const maxName = Math.max(...services.map((s) => s.name.length))
37
+ for (const svc of services) {
38
+ const icon = svc.status === "running" ? "●" : svc.status === "stopped" ? "○" : "✕"
39
+ const status = svc.status.padEnd(8)
40
+ const port = svc.port ? `:${svc.port}` : ""
41
+ const uptime = svc.uptime ? ` (${svc.uptime})` : ""
42
+ console.log(` ${icon} ${svc.name.padEnd(maxName)} ${status} ${port}${uptime}`)
43
+ }
44
+
45
+ const running = services.filter((s) => s.status === "running")
46
+ console.log(`\n${running.length}/${services.length} services running`)
47
+
48
+ if (running.length > 0) {
49
+ console.log(`\nAPI URL: http://localhost:8000`)
50
+ console.log(`Studio: http://localhost:3100`)
51
+ console.log(`Database: postgresql://postgres:postgres@localhost:5432/postgres`)
52
+ }
53
+ })
54
+ }
55
+
56
+ function getContainerStatus(name: string): "running" | "stopped" | "error" {
57
+ const result = spawnSync("docker", ["inspect", "--format", "{{.State.Status}}", name], {
58
+ timeout: 5000,
59
+ })
60
+ const status = result.stdout?.toString().trim()
61
+ if (status === "running") return "running"
62
+ if (result.status !== 0) return "stopped"
63
+ return "error"
64
+ }
65
+
66
+ function getContainerUptime(name: string): string | undefined {
67
+ const result = spawnSync("docker", ["inspect", "--format", "{{.State.StartedAt}}", name], {
68
+ timeout: 5000,
69
+ })
70
+ if (result.status !== 0) return undefined
71
+ const startedAt = result.stdout?.toString().trim()
72
+ if (!startedAt) return undefined
73
+
74
+ const started = new Date(startedAt)
75
+ const now = new Date()
76
+ const diffMs = now.getTime() - started.getTime()
77
+ if (diffMs < 0) return undefined
78
+
79
+ const hours = Math.floor(diffMs / (1000 * 60 * 60))
80
+ const minutes = Math.floor((diffMs % (1000 * 60 * 60)) / (1000 * 60))
81
+ if (hours > 0) return `${hours}h ${minutes}m`
82
+ return `${minutes}m`
83
+ }
package/src/config.ts ADDED
@@ -0,0 +1,190 @@
1
+ import { existsSync } from "node:fs"
2
+ import { resolve } from "node:path"
3
+ import { evalTsSnippet } from "./tsx-runner.js"
4
+
5
+ export interface ServiceVersionPin {
6
+ /** Docker image tag to pin this service to (e.g. "v1.2.3"). When set, `self-host upgrade` skips this service. */
7
+ version: string
8
+ }
9
+
10
+ export interface SelfHostConfig {
11
+ /** Production domain (e.g. "api.example.com"). Used by Caddy for HTTPS. */
12
+ domain: string
13
+ /** App service to include in the production stack. */
14
+ app?: {
15
+ /** Path to the app's Dockerfile, relative to the project root. */
16
+ dockerfile: string
17
+ /** Port the app listens on. */
18
+ port: number
19
+ }
20
+ ssl?: {
21
+ /** SSL provider. "caddy" = automatic Let's Encrypt. "none" = bring your own. */
22
+ provider: "caddy" | "none"
23
+ /** Email for Let's Encrypt registration (required when provider = "caddy"). */
24
+ email?: string
25
+ }
26
+ /**
27
+ * Pin specific services to fixed Docker image versions.
28
+ * When a service is pinned, `self-host upgrade` will skip it.
29
+ * Omit a service or set to `undefined` to allow automatic upgrades.
30
+ *
31
+ * @example
32
+ * services: {
33
+ * db: { version: "17-latest" },
34
+ * postgrest: { version: "v12.2.8" },
35
+ * }
36
+ */
37
+ services?: {
38
+ db?: ServiceVersionPin
39
+ gotrue?: ServiceVersionPin
40
+ postgrest?: ServiceVersionPin
41
+ kong?: ServiceVersionPin
42
+ caddy?: ServiceVersionPin
43
+ pgbouncer?: ServiceVersionPin
44
+ }
45
+ }
46
+
47
+ export type AppFramework = "nextjs" | "astro" | "vite" | "remix-spa" | "sveltekit" | "nuxt" | "static"
48
+
49
+ export interface AppConfig {
50
+ /** Framework name. Auto-detected from package.json if not specified. */
51
+ framework?: AppFramework
52
+ /** Path to the app directory (default: "./" or "./apps/web" for monorepos). */
53
+ directory?: string
54
+ /** Build command (inferred from framework if not specified). */
55
+ buildCommand?: string
56
+ /** Output directory (inferred from framework if not specified). */
57
+ outputDirectory?: string
58
+ /** Enable SPA fallback routing (default: true for Vite/CRA, false for SSG frameworks). */
59
+ spa?: boolean
60
+ /** Environment variables to inject during build. */
61
+ env?: Record<string, string>
62
+ /** Custom response headers for the static site. */
63
+ headers?: Record<string, string>
64
+ }
65
+
66
+ export interface SupatypeConfig {
67
+ /** Database connection string. */
68
+ connection: string
69
+ /**
70
+ * Path (or glob) to the schema entry point.
71
+ * Must export model definitions as named exports.
72
+ * @example "./schema/index.ts"
73
+ */
74
+ schema: string
75
+ output?: {
76
+ /** Path for generated TypeScript types. */
77
+ types?: string
78
+ /** Path for generated client helpers. */
79
+ client?: string
80
+ }
81
+ /** Self-hosted production deployment configuration. */
82
+ selfHost?: SelfHostConfig
83
+ /** Cloud project reference (set by `supatype link --project <ref>`). */
84
+ projectRef?: string
85
+ /** Cloud API URL override. */
86
+ apiUrl?: string
87
+ /** Cloud access token (prefer SUPATYPE_ACCESS_TOKEN env var). */
88
+ accessToken?: string
89
+ /** CORS configuration. */
90
+ cors?: {
91
+ /** Allowed origins. Defaults to ['*'] in development. */
92
+ allowedOrigins?: string[]
93
+ }
94
+ /** Static site hosting configuration. */
95
+ app?: AppConfig
96
+ /** Registered plugins (provider, field, composite, widget). */
97
+ plugins?: Array<unknown> | undefined
98
+ /** Admin panel configuration (see Gap Appendices tasks 47–50). */
99
+ admin?: {
100
+ /**
101
+ * Roles from {ref}_auth.users that grant admin panel access.
102
+ * Checked against `app_metadata.role` in the project JWT.
103
+ * @default ["admin"]
104
+ */
105
+ roles?: string[]
106
+ }
107
+ }
108
+
109
+ /** Identity helper — provides type inference for config files. */
110
+ export function defineConfig(config: SupatypeConfig): SupatypeConfig {
111
+ return config
112
+ }
113
+
114
+ const CONFIG_CANDIDATES = [
115
+ "supatype.config.ts",
116
+ "supatype.config.js",
117
+ "supatype.config.mjs",
118
+ ]
119
+
120
+ /** Load and evaluate supatype.config.ts from the given directory. */
121
+ export function loadConfig(cwd: string = process.cwd()): SupatypeConfig {
122
+ for (const candidate of CONFIG_CANDIDATES) {
123
+ const configPath = resolve(cwd, candidate)
124
+ if (!existsSync(configPath)) continue
125
+
126
+ const urlPath = "file:///" + configPath.replace(/\\/g, "/")
127
+
128
+ // Use dynamic import so we can always access .default —
129
+ // files without a parent package.json are treated as CJS by tsx,
130
+ // meaning export default becomes module.exports.default rather than
131
+ // the namespace default. Dynamic import + fallback handles both.
132
+ const snippet = `
133
+ const mod = await import(${JSON.stringify(urlPath)})
134
+ const config = mod.default ?? mod
135
+ process.stdout.write(JSON.stringify(config))
136
+ `
137
+ const result = evalTsSnippet(snippet, { cwd })
138
+ if (result.exitCode !== 0) {
139
+ throw new Error(
140
+ `Failed to load ${candidate}:\n${result.stderr || result.stdout}`,
141
+ )
142
+ }
143
+
144
+ const parsed = JSON.parse(result.stdout) as SupatypeConfig
145
+ if (!parsed.connection || !parsed.schema) {
146
+ throw new Error(
147
+ `${candidate} must export { connection, schema } via defineConfig()`,
148
+ )
149
+ }
150
+ return parsed
151
+ }
152
+
153
+ throw new Error(
154
+ "No supatype.config.ts found in the current directory.\n" +
155
+ "Run: supatype init",
156
+ )
157
+ }
158
+
159
+ /** Load schema AST by evaluating the user's schema entry point via tsx. */
160
+ export function loadSchemaAst(
161
+ schemaPath: string,
162
+ cwd: string = process.cwd(),
163
+ ): unknown {
164
+ const absPath = resolve(cwd, schemaPath)
165
+ if (!existsSync(absPath)) {
166
+ throw new Error(`Schema file not found: ${absPath}`)
167
+ }
168
+
169
+ const urlPath = "file:///" + absPath.replace(/\\/g, "/")
170
+
171
+ const snippet = `
172
+ import { serialiseSchema } from "@supatype/schema"
173
+ const mod = await import(${JSON.stringify(urlPath)})
174
+ const { default: _default, ...named } = mod
175
+ const models = Object.fromEntries(
176
+ Object.entries(named).filter(([, v]) =>
177
+ v != null && typeof v === "object" && "__modelMeta" in (v as object)
178
+ )
179
+ )
180
+ process.stdout.write(JSON.stringify(serialiseSchema(models)))
181
+ `
182
+ const result = evalTsSnippet(snippet, { cwd })
183
+ if (result.exitCode !== 0) {
184
+ throw new Error(
185
+ `Failed to load schema from ${absPath}:\n${result.stderr || result.stdout}`,
186
+ )
187
+ }
188
+
189
+ return JSON.parse(result.stdout)
190
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Local cache management for engine binaries.
3
+ * Binaries are stored at ~/.supatype/engine/{version}/supatype-engine[.exe]
4
+ */
5
+
6
+ import { existsSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs"
7
+ import { readFile, writeFile } from "node:fs/promises"
8
+ import { join } from "node:path"
9
+ import { homedir } from "node:os"
10
+ import type { PlatformInfo } from "./platform.js"
11
+
12
+ /**
13
+ * Root cache directory: ~/.supatype/engine/
14
+ */
15
+ export function getCacheDir(): string {
16
+ return join(homedir(), ".supatype", "engine")
17
+ }
18
+
19
+ /**
20
+ * Full path to a cached engine binary for a specific version.
21
+ */
22
+ export function getCachedBinaryPath(version: string, platform: PlatformInfo): string {
23
+ return join(getCacheDir(), version, platform.binaryName)
24
+ }
25
+
26
+ /**
27
+ * Check if a valid cached binary exists for the given version.
28
+ */
29
+ export function hasCachedBinary(version: string, platform: PlatformInfo): boolean {
30
+ const path = getCachedBinaryPath(version, platform)
31
+ return existsSync(path)
32
+ }
33
+
34
+ /**
35
+ * Ensure the cache directory for a version exists.
36
+ */
37
+ export function ensureCacheDir(version: string): string {
38
+ const dir = join(getCacheDir(), version)
39
+ mkdirSync(dir, { recursive: true })
40
+ return dir
41
+ }
42
+
43
+ /**
44
+ * List all cached engine versions.
45
+ */
46
+ export function listCachedVersions(): string[] {
47
+ const cacheDir = getCacheDir()
48
+ if (!existsSync(cacheDir)) return []
49
+
50
+ return readdirSync(cacheDir, { withFileTypes: true })
51
+ .filter((d) => d.isDirectory())
52
+ .map((d) => d.name)
53
+ .sort()
54
+ }
55
+
56
+ /**
57
+ * Remove all cached versions except the specified one.
58
+ * Returns the total bytes freed.
59
+ */
60
+ export function pruneCacheExcept(keepVersion: string): { removed: string[]; bytesFreed: number } {
61
+ const versions = listCachedVersions()
62
+ const removed: string[] = []
63
+ let bytesFreed = 0
64
+
65
+ for (const version of versions) {
66
+ if (version === keepVersion) continue
67
+ const versionDir = join(getCacheDir(), version)
68
+ bytesFreed += getDirSize(versionDir)
69
+ rmSync(versionDir, { recursive: true, force: true })
70
+ removed.push(version)
71
+ }
72
+
73
+ return { removed, bytesFreed }
74
+ }
75
+
76
+ function getDirSize(dir: string): number {
77
+ let size = 0
78
+ if (!existsSync(dir)) return size
79
+
80
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
81
+ const path = join(dir, entry.name)
82
+ if (entry.isFile()) {
83
+ size += statSync(path).size
84
+ } else if (entry.isDirectory()) {
85
+ size += getDirSize(path)
86
+ }
87
+ }
88
+ return size
89
+ }
90
+
91
+ /**
92
+ * Update check throttling.
93
+ * Stores last check timestamp in ~/.supatype/update-check.json
94
+ */
95
+ const UPDATE_CHECK_FILE = join(homedir(), ".supatype", "update-check.json")
96
+ const CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000 // 24 hours
97
+
98
+ interface UpdateCheckData {
99
+ lastCheck: number
100
+ latestVersion?: string
101
+ }
102
+
103
+ export async function shouldCheckForUpdates(): Promise<boolean> {
104
+ // Skip in CI environments
105
+ if (process.env.CI === "true" || process.env.CI === "1") return false
106
+
107
+ try {
108
+ if (!existsSync(UPDATE_CHECK_FILE)) return true
109
+ const data: UpdateCheckData = JSON.parse(await readFile(UPDATE_CHECK_FILE, "utf8"))
110
+ return Date.now() - data.lastCheck > CHECK_INTERVAL_MS
111
+ } catch {
112
+ return true
113
+ }
114
+ }
115
+
116
+ export async function saveUpdateCheck(latestVersion: string): Promise<void> {
117
+ const dir = join(homedir(), ".supatype")
118
+ mkdirSync(dir, { recursive: true })
119
+
120
+ const data: UpdateCheckData = {
121
+ lastCheck: Date.now(),
122
+ latestVersion,
123
+ }
124
+ await writeFile(UPDATE_CHECK_FILE, JSON.stringify(data, null, 2))
125
+ }
126
+
127
+ export async function getLastKnownLatestVersion(): Promise<string | undefined> {
128
+ try {
129
+ if (!existsSync(UPDATE_CHECK_FILE)) return undefined
130
+ const data: UpdateCheckData = JSON.parse(await readFile(UPDATE_CHECK_FILE, "utf8"))
131
+ return data.latestVersion
132
+ } catch {
133
+ return undefined
134
+ }
135
+ }
@@ -0,0 +1,143 @@
1
+ /**
2
+ * Engine binary download with progress bar, retry, and proxy support.
3
+ */
4
+
5
+ import { createWriteStream } from "node:fs"
6
+ import { Readable } from "node:stream"
7
+ import { pipeline } from "node:stream/promises"
8
+
9
+ export interface DownloadOptions {
10
+ url: string
11
+ dest: string
12
+ showProgress?: boolean
13
+ label?: string
14
+ }
15
+
16
+ const MAX_RETRIES = 3
17
+ const RETRY_DELAYS = [1000, 3000, 10000]
18
+
19
+ /**
20
+ * Download a file with retry and optional progress bar.
21
+ * Respects HTTP_PROXY / HTTPS_PROXY environment variables.
22
+ */
23
+ export async function downloadFile(options: DownloadOptions): Promise<void> {
24
+ const { url, dest, showProgress = false, label } = options
25
+ let lastError: Error | undefined
26
+
27
+ for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
28
+ try {
29
+ await doDownload(url, dest, showProgress, label)
30
+ return
31
+ } catch (err) {
32
+ lastError = err instanceof Error ? err : new Error(String(err))
33
+ if (attempt < MAX_RETRIES - 1) {
34
+ const delay = RETRY_DELAYS[attempt]!
35
+ process.stderr.write(
36
+ `Download failed. Retrying (${attempt + 2}/${MAX_RETRIES})...\n`,
37
+ )
38
+ await sleep(delay)
39
+ }
40
+ }
41
+ }
42
+
43
+ throw new Error(
44
+ `Failed to download after ${MAX_RETRIES} attempts: ${lastError?.message}`,
45
+ )
46
+ }
47
+
48
+ async function doDownload(
49
+ url: string,
50
+ dest: string,
51
+ showProgress: boolean,
52
+ label?: string,
53
+ ): Promise<void> {
54
+ const fetchOptions = buildFetchOptions(url)
55
+ const res = await fetch(url, fetchOptions)
56
+
57
+ if (!res.ok) {
58
+ throw new Error(`HTTP ${res.status} ${res.statusText}: ${url}`)
59
+ }
60
+
61
+ if (!res.body) {
62
+ throw new Error(`No response body: ${url}`)
63
+ }
64
+
65
+ const contentLength = Number(res.headers.get("content-length") || 0)
66
+ const out = createWriteStream(dest)
67
+
68
+ if (showProgress && contentLength > 0 && process.stderr.isTTY) {
69
+ const progressStream = createProgressStream(contentLength, label)
70
+ await pipeline(Readable.fromWeb(res.body as any), progressStream, out)
71
+ // Clear the progress line
72
+ process.stderr.write("\n")
73
+ } else {
74
+ if (showProgress && label) {
75
+ process.stderr.write(`${label}...\n`)
76
+ }
77
+ await pipeline(Readable.fromWeb(res.body as any), out)
78
+ }
79
+ }
80
+
81
+ /**
82
+ * Build fetch options respecting proxy env vars.
83
+ */
84
+ function buildFetchOptions(url: string): RequestInit {
85
+ const opts: RequestInit = {}
86
+
87
+ // Node.js 18+ fetch supports the proxy via undici dispatcher.
88
+ // For simplicity, we rely on the global-agent or undici proxy support.
89
+ // The user should set HTTPS_PROXY or HTTP_PROXY env vars.
90
+ // Node.js 22+ automatically respects these in fetch().
91
+ //
92
+ // For older Node.js, users can install global-agent or similar.
93
+
94
+ return opts
95
+ }
96
+
97
+ /**
98
+ * Creates a Transform stream that logs download progress to stderr.
99
+ */
100
+ function createProgressStream(
101
+ totalBytes: number,
102
+ label?: string,
103
+ ): import("node:stream").Transform {
104
+ const { Transform } = require("node:stream") as typeof import("node:stream")
105
+ let downloaded = 0
106
+
107
+ return new Transform({
108
+ transform(chunk: Buffer, _encoding, callback) {
109
+ downloaded += chunk.length
110
+ const percent = Math.round((downloaded / totalBytes) * 100)
111
+ const mb = (downloaded / (1024 * 1024)).toFixed(1)
112
+ const totalMb = (totalBytes / (1024 * 1024)).toFixed(1)
113
+ const barWidth = 30
114
+ const filled = Math.round((percent / 100) * barWidth)
115
+ const bar = "=".repeat(filled) + " ".repeat(barWidth - filled)
116
+
117
+ const prefix = label || "Downloading"
118
+ process.stderr.write(
119
+ `\r${prefix} ${mb}MB/${totalMb}MB [${bar}] ${percent}%`,
120
+ )
121
+
122
+ this.push(chunk)
123
+ callback()
124
+ },
125
+ })
126
+ }
127
+
128
+ function sleep(ms: number): Promise<void> {
129
+ return new Promise((resolve) => setTimeout(resolve, ms))
130
+ }
131
+
132
+ /**
133
+ * Fetch a JSON file from a URL. Returns undefined on failure.
134
+ */
135
+ export async function fetchJson<T>(url: string): Promise<T | undefined> {
136
+ try {
137
+ const res = await fetch(url)
138
+ if (!res.ok) return undefined
139
+ return (await res.json()) as T
140
+ } catch {
141
+ return undefined
142
+ }
143
+ }
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Platform detection for engine binary downloads.
3
+ * Maps Node.js platform/arch to the binary naming convention.
4
+ */
5
+
6
+ export interface PlatformInfo {
7
+ os: "linux" | "darwin" | "win"
8
+ arch: "x64" | "arm64"
9
+ binaryName: string
10
+ ext: string
11
+ }
12
+
13
+ const PLATFORM_MAP: Record<string, { os: PlatformInfo["os"]; arch: PlatformInfo["arch"] }> = {
14
+ "darwin-arm64": { os: "darwin", arch: "arm64" },
15
+ "darwin-x64": { os: "darwin", arch: "x64" },
16
+ "linux-arm64": { os: "linux", arch: "arm64" },
17
+ "linux-x64": { os: "linux", arch: "x64" },
18
+ "win32-x64": { os: "win", arch: "x64" },
19
+ }
20
+
21
+ const SUPPORTED_PLATFORMS = [
22
+ "linux-x64",
23
+ "linux-arm64",
24
+ "darwin-x64",
25
+ "darwin-arm64",
26
+ "win-x64",
27
+ ]
28
+
29
+ /**
30
+ * Detect the current platform and return the binary info.
31
+ * Throws on unsupported platforms with a helpful message.
32
+ */
33
+ export function detectPlatform(): PlatformInfo {
34
+ const key = `${process.platform}-${process.arch}`
35
+ const mapped = PLATFORM_MAP[key]
36
+
37
+ if (!mapped) {
38
+ throw new Error(
39
+ `Supatype engine is not available for ${process.platform}-${process.arch}.\n` +
40
+ `Supported platforms: ${SUPPORTED_PLATFORMS.join(", ")}`,
41
+ )
42
+ }
43
+
44
+ const ext = mapped.os === "win" ? ".exe" : ""
45
+
46
+ return {
47
+ os: mapped.os,
48
+ arch: mapped.arch,
49
+ binaryName: `supatype-engine${ext}`,
50
+ ext,
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Build the artifact filename for a given version and platform.
56
+ */
57
+ export function getArtifactName(version: string, platform: PlatformInfo): string {
58
+ return `supatype-engine-${version}-${platform.os}-${platform.arch}${platform.ext}`
59
+ }
60
+
61
+ /**
62
+ * Build the CDN download URL for a given version and artifact.
63
+ */
64
+ export function getCdnUrl(baseUrl: string, version: string, filename: string): string {
65
+ return `${baseUrl}/v${version}/${filename}`
66
+ }