@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,762 @@
1
+ import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, unlinkSync, } from "node:fs";
2
+ import { resolve, join, basename, relative } from "node:path";
3
+ import { spawnSync } from "node:child_process";
4
+ // ─── Constants ───────────────────────────────────────────────────────────────
5
+ const FUNCTIONS_DIR = "supatype/functions";
6
+ const SHARED_DIR = "_shared";
7
+ const ENV_LOCAL = ".env.local";
8
+ const ENV_PRODUCTION = ".env.production";
9
+ // ─── Registration ────────────────────────────────────────────────────────────
10
+ export function registerFunctions(program) {
11
+ const fnCmd = program
12
+ .command("functions")
13
+ .description("Manage Supatype Edge Functions (Deno-based serverless TypeScript)");
14
+ fnCmd
15
+ .command("new <name>")
16
+ .description("Scaffold a new edge function")
17
+ .action((name) => {
18
+ scaffoldFunction(process.cwd(), name);
19
+ });
20
+ fnCmd
21
+ .command("serve")
22
+ .description("Start a local Deno server that serves all functions with hot reload")
23
+ .option("--port <port>", "Port to serve on", "54321")
24
+ .option("--env-file <path>", "Path to env file", `${FUNCTIONS_DIR}/${ENV_LOCAL}`)
25
+ .action((opts) => {
26
+ serve(process.cwd(), opts);
27
+ });
28
+ fnCmd
29
+ .command("deploy")
30
+ .description("Deploy all functions (or --only <name> for one) to the linked project")
31
+ .option("--only <name>", "Deploy a single function")
32
+ .option("--dry-run", "Show what would be deployed without deploying")
33
+ .action(async (opts) => {
34
+ await deploy(process.cwd(), opts);
35
+ });
36
+ fnCmd
37
+ .command("list")
38
+ .description("List all deployed functions for the linked project")
39
+ .action(async () => {
40
+ await listFunctions(process.cwd());
41
+ });
42
+ fnCmd
43
+ .command("delete <name>")
44
+ .description("Remove a deployed function")
45
+ .action(async (name) => {
46
+ await deleteFunction(process.cwd(), name);
47
+ });
48
+ fnCmd
49
+ .command("logs <name>")
50
+ .description("Tail logs for a deployed function")
51
+ .option("--since <duration>", "Show logs since duration (e.g. 1h, 30m)", "1h")
52
+ .action(async (name, opts) => {
53
+ await functionLogs(process.cwd(), name, opts);
54
+ });
55
+ fnCmd
56
+ .command("invoke <name>")
57
+ .description("Invoke a local or deployed function")
58
+ .option("--data <json>", "JSON body to send", "{}")
59
+ .option("--auth", "Include a test JWT in the request")
60
+ .option("--local", "Invoke the local dev server (default if serve is running)")
61
+ .action(async (name, opts) => {
62
+ await invoke(process.cwd(), name, opts);
63
+ });
64
+ const envCmd = fnCmd
65
+ .command("env")
66
+ .description("Manage function environment variables");
67
+ envCmd
68
+ .command("list")
69
+ .description("List environment variables (values masked)")
70
+ .action(async () => {
71
+ await envList(process.cwd());
72
+ });
73
+ envCmd
74
+ .command("set <keyvalue>")
75
+ .description("Set an environment variable (KEY=value)")
76
+ .action(async (keyvalue) => {
77
+ await envSet(process.cwd(), keyvalue);
78
+ });
79
+ envCmd
80
+ .command("unset <key>")
81
+ .description("Remove an environment variable")
82
+ .action(async (key) => {
83
+ await envUnset(process.cwd(), key);
84
+ });
85
+ }
86
+ // ─── Scaffold ────────────────────────────────────────────────────────────────
87
+ function scaffoldFunction(cwd, name) {
88
+ const fnDir = resolve(cwd, FUNCTIONS_DIR, name);
89
+ if (existsSync(fnDir)) {
90
+ console.error(`Function "${name}" already exists at ${relative(cwd, fnDir)}`);
91
+ process.exit(1);
92
+ }
93
+ mkdirSync(fnDir, { recursive: true });
94
+ const indexContent = `// ${name} — Supatype Edge Function
95
+ // Docs: https://supatype.dev/docs/edge-functions
96
+
97
+ export default async function handler(req: Request): Promise<Response> {
98
+ const { method, url } = req
99
+
100
+ // Example: read request body for POST requests
101
+ if (method === "POST") {
102
+ const body = await req.json()
103
+ return new Response(JSON.stringify({ message: "Hello from ${name}!", received: body }), {
104
+ status: 200,
105
+ headers: { "Content-Type": "application/json" },
106
+ })
107
+ }
108
+
109
+ return new Response(JSON.stringify({ message: "Hello from ${name}!" }), {
110
+ status: 200,
111
+ headers: { "Content-Type": "application/json" },
112
+ })
113
+ }
114
+ `;
115
+ writeFileSync(join(fnDir, "index.ts"), indexContent, "utf8");
116
+ // Ensure _shared directory exists
117
+ const sharedDir = resolve(cwd, FUNCTIONS_DIR, SHARED_DIR);
118
+ if (!existsSync(sharedDir)) {
119
+ mkdirSync(sharedDir, { recursive: true });
120
+ writeFileSync(join(sharedDir, "README.md"), "# Shared Code\n\nFiles in `_shared/` are available to all functions via relative imports.\nThis directory is not deployed as a function.\n\nExample: `import { sendEmail } from '../_shared/email.ts'`\n", "utf8");
121
+ }
122
+ // Ensure .env.local exists
123
+ const envLocalPath = resolve(cwd, FUNCTIONS_DIR, ENV_LOCAL);
124
+ if (!existsSync(envLocalPath)) {
125
+ writeFileSync(envLocalPath, "# Local environment variables for edge functions\n# These are NOT committed to git\n# Set production env vars via: npx supatype functions env set KEY=value\n", "utf8");
126
+ }
127
+ console.log(`Created function: ${FUNCTIONS_DIR}/${name}/index.ts`);
128
+ console.log();
129
+ console.log(" Local dev: npx supatype functions serve");
130
+ console.log(` Invoke: npx supatype functions invoke ${name}`);
131
+ console.log(" Deploy: npx supatype functions deploy");
132
+ }
133
+ function discoverFunctions(cwd) {
134
+ const functionsDir = resolve(cwd, FUNCTIONS_DIR);
135
+ if (!existsSync(functionsDir))
136
+ return [];
137
+ const entries = readdirSync(functionsDir);
138
+ const fns = [];
139
+ for (const entry of entries) {
140
+ if (entry.startsWith("_") || entry.startsWith("."))
141
+ continue;
142
+ const fullPath = join(functionsDir, entry);
143
+ const stat = statSync(fullPath);
144
+ if (stat.isDirectory()) {
145
+ // Directory function — look for index.ts
146
+ const indexPath = join(fullPath, "index.ts");
147
+ if (existsSync(indexPath)) {
148
+ fns.push({ name: entry, entrypoint: indexPath, absPath: fullPath });
149
+ }
150
+ }
151
+ else if (entry.endsWith(".ts") && !entry.endsWith(".d.ts")) {
152
+ // Single-file function
153
+ const name = basename(entry, ".ts");
154
+ fns.push({ name, entrypoint: fullPath, absPath: fullPath });
155
+ }
156
+ }
157
+ return fns.sort((a, b) => a.name.localeCompare(b.name));
158
+ }
159
+ // ─── Serve (local dev) ──────────────────────────────────────────────────────
160
+ function serve(cwd, opts) {
161
+ const fns = discoverFunctions(cwd);
162
+ if (fns.length === 0) {
163
+ console.error(`No functions found in ${FUNCTIONS_DIR}/`);
164
+ console.error("Create one with: npx supatype functions new <name>");
165
+ process.exit(1);
166
+ }
167
+ console.log(`Discovered ${fns.length} function(s):`);
168
+ for (const fn of fns) {
169
+ console.log(` /${fn.name} → ${relative(cwd, fn.entrypoint)}`);
170
+ }
171
+ console.log();
172
+ // Generate a Deno entry script that routes requests to the correct function
173
+ const routerScript = generateLocalRouter(fns, cwd);
174
+ const routerPath = resolve(cwd, FUNCTIONS_DIR, ".serve-router.ts");
175
+ writeFileSync(routerPath, routerScript, "utf8");
176
+ const envFilePath = resolve(cwd, opts.envFile);
177
+ const envArgs = [];
178
+ if (existsSync(envFilePath)) {
179
+ envArgs.push("--env-file", envFilePath);
180
+ }
181
+ console.log(`Serving functions at http://localhost:${opts.port}/functions/v1/`);
182
+ console.log("Watching for changes...\n");
183
+ const result = spawnSync("deno", [
184
+ "run",
185
+ "--allow-net",
186
+ "--allow-env",
187
+ "--allow-read",
188
+ "--watch",
189
+ ...envArgs,
190
+ routerPath,
191
+ ], {
192
+ stdio: "inherit",
193
+ cwd,
194
+ env: {
195
+ ...process.env,
196
+ PORT: opts.port,
197
+ SUPATYPE_URL: process.env["SUPATYPE_URL"] ?? `http://localhost:8000`,
198
+ SUPATYPE_ANON_KEY: process.env["SUPATYPE_ANON_KEY"] ?? "",
199
+ SUPATYPE_SERVICE_ROLE_KEY: process.env["SUPATYPE_SERVICE_ROLE_KEY"] ?? "",
200
+ },
201
+ });
202
+ // Clean up router script
203
+ try {
204
+ unlinkSync(routerPath);
205
+ }
206
+ catch { /* ignore */ }
207
+ if (result.status !== 0) {
208
+ console.error("Function server exited with errors.");
209
+ console.error("Make sure Deno is installed: https://deno.land/manual/getting_started/installation");
210
+ process.exit(result.status ?? 1);
211
+ }
212
+ }
213
+ function generateLocalRouter(fns, cwd) {
214
+ const imports = fns.map((fn, i) => `import handler_${i} from "./${relative(resolve(cwd, FUNCTIONS_DIR), fn.entrypoint).replace(/\\/g, "/")}"`);
215
+ const routes = fns.map((fn, i) => ` "${fn.name}": handler_${i},`);
216
+ return `// Auto-generated local function router — do not edit
217
+ ${imports.join("\n")}
218
+
219
+ const handlers: Record<string, (req: Request) => Response | Promise<Response>> = {
220
+ ${routes.join("\n")}
221
+ }
222
+
223
+ const port = parseInt(Deno.env.get("PORT") ?? "54321", 10)
224
+
225
+ Deno.serve({ port }, async (req: Request): Promise<Response> => {
226
+ const url = new URL(req.url)
227
+ const pathParts = url.pathname.replace(/^\\/functions\\/v1\\//, "").split("/")
228
+ const fnName = pathParts[0] ?? ""
229
+
230
+ if (!fnName || !handlers[fnName]) {
231
+ return new Response(JSON.stringify({
232
+ error: "not_found",
233
+ message: fnName ? \`Function "\${fnName}" not found\` : "No function specified",
234
+ available: Object.keys(handlers),
235
+ }), { status: 404, headers: { "Content-Type": "application/json" } })
236
+ }
237
+
238
+ try {
239
+ const start = performance.now()
240
+ const response = await handlers[fnName]!(req)
241
+ const duration = (performance.now() - start).toFixed(1)
242
+ console.log(\`\${req.method} /functions/v1/\${fnName} → \${response.status} (\${duration}ms)\`)
243
+ return response
244
+ } catch (err) {
245
+ console.error(\`Error in function "\${fnName}":\`, err)
246
+ return new Response(JSON.stringify({
247
+ error: "function_error",
248
+ message: err instanceof Error ? err.message : "Unknown error",
249
+ }), { status: 500, headers: { "Content-Type": "application/json" } })
250
+ }
251
+ })
252
+
253
+ console.log(\`Edge function server running on http://localhost:\${port}/functions/v1/\`)
254
+ `;
255
+ }
256
+ // ─── Deploy ──────────────────────────────────────────────────────────────────
257
+ async function deploy(cwd, opts) {
258
+ const allFns = discoverFunctions(cwd);
259
+ const fns = opts.only
260
+ ? allFns.filter(f => f.name === opts.only)
261
+ : allFns;
262
+ if (fns.length === 0) {
263
+ if (opts.only) {
264
+ console.error(`Function "${opts.only}" not found in ${FUNCTIONS_DIR}/`);
265
+ }
266
+ else {
267
+ console.error(`No functions found in ${FUNCTIONS_DIR}/`);
268
+ }
269
+ process.exit(1);
270
+ }
271
+ if (opts.dryRun) {
272
+ console.log("Dry run — the following functions would be deployed:\n");
273
+ for (const fn of fns) {
274
+ console.log(` ${fn.name} → ${relative(cwd, fn.entrypoint)}`);
275
+ }
276
+ console.log(`\nTotal: ${fns.length} function(s)`);
277
+ return;
278
+ }
279
+ // Check if this is a self-hosted deployment or cloud
280
+ const isSelfHosted = detectSelfHosted(cwd);
281
+ if (isSelfHosted) {
282
+ await deploySelfHosted(cwd, fns);
283
+ }
284
+ else {
285
+ await deployCloud(cwd, fns);
286
+ }
287
+ }
288
+ function detectSelfHosted(cwd) {
289
+ return existsSync(resolve(cwd, "deploy/docker-compose.yml"));
290
+ }
291
+ async function deploySelfHosted(cwd, fns) {
292
+ console.log("Self-hosted deployment detected.\n");
293
+ console.log("Bundling functions...\n");
294
+ const bundleDir = resolve(cwd, "deploy/functions");
295
+ mkdirSync(bundleDir, { recursive: true });
296
+ for (const fn of fns) {
297
+ const start = Date.now();
298
+ const outFile = join(bundleDir, `${fn.name}.js`);
299
+ // Bundle with Deno
300
+ const result = spawnSync("deno", ["bundle", fn.entrypoint, outFile], {
301
+ stdio: "pipe",
302
+ cwd,
303
+ });
304
+ if (result.status !== 0) {
305
+ const stderr = result.stderr?.toString() ?? "";
306
+ console.error(` ${fn.name} ✗ bundle failed`);
307
+ if (stderr)
308
+ console.error(` ${stderr.trim()}`);
309
+ continue;
310
+ }
311
+ const duration = Date.now() - start;
312
+ console.log(` ${fn.name} ✓ deployed (${duration}ms)`);
313
+ }
314
+ console.log(`\nDeployed ${fns.length} function(s) to deploy/functions/`);
315
+ console.log("The supatype-functions container will pick up changes automatically.");
316
+ }
317
+ async function deployCloud(cwd, fns) {
318
+ const { getLinkedProject, getCloudToken, getCloudApiUrl } = await loadCloudHelpers();
319
+ const linked = getLinkedProject(cwd);
320
+ if (!linked) {
321
+ console.error("No linked project. Run: npx supatype cloud link");
322
+ process.exit(1);
323
+ }
324
+ const token = getCloudToken();
325
+ if (!token) {
326
+ console.error("Not logged in. Run: npx supatype cloud login");
327
+ process.exit(1);
328
+ }
329
+ const apiUrl = getCloudApiUrl();
330
+ console.log(`Deploying to project: ${linked.ref}\n`);
331
+ for (const fn of fns) {
332
+ const start = Date.now();
333
+ // Read source code
334
+ const source = readFunctionSource(fn);
335
+ try {
336
+ const res = await fetch(`${apiUrl}/api/v1/projects/${linked.ref}/functions/deploy`, {
337
+ method: "POST",
338
+ headers: {
339
+ Authorization: `Bearer ${token}`,
340
+ "Content-Type": "application/json",
341
+ "X-Org-Id": linked.orgId ?? "",
342
+ },
343
+ body: JSON.stringify({
344
+ functions: [{
345
+ name: fn.name,
346
+ source,
347
+ entrypoint: `${fn.name}/index.ts`,
348
+ }],
349
+ }),
350
+ signal: AbortSignal.timeout(60_000),
351
+ });
352
+ if (!res.ok) {
353
+ const body = await res.json().catch(() => ({}));
354
+ console.error(` ${fn.name} ✗ ${body["message"] ?? res.statusText}`);
355
+ continue;
356
+ }
357
+ const duration = Date.now() - start;
358
+ console.log(` ${fn.name} ✓ deployed (${duration}ms)`);
359
+ }
360
+ catch (err) {
361
+ console.error(` ${fn.name} ✗ ${err instanceof Error ? err.message : "unknown error"}`);
362
+ }
363
+ }
364
+ console.log(`\nDeployed ${fns.length} function(s)`);
365
+ console.log(`Invoke: https://${linked.ref}.supatype.io/functions/v1/<name>`);
366
+ }
367
+ function readFunctionSource(fn) {
368
+ const stat = statSync(fn.absPath);
369
+ if (stat.isFile()) {
370
+ return readFileSync(fn.absPath, "utf8");
371
+ }
372
+ // Directory function — read all .ts files
373
+ const files = {};
374
+ const entries = readdirSync(fn.absPath, { recursive: true });
375
+ for (const entry of entries) {
376
+ const fullPath = join(fn.absPath, entry);
377
+ if (statSync(fullPath).isFile() && (entry.endsWith(".ts") || entry.endsWith(".js"))) {
378
+ files[entry] = readFileSync(fullPath, "utf8");
379
+ }
380
+ }
381
+ return JSON.stringify(files);
382
+ }
383
+ // ─── List ────────────────────────────────────────────────────────────────────
384
+ async function listFunctions(cwd) {
385
+ const { getLinkedProject, getCloudToken, getCloudApiUrl } = await loadCloudHelpers();
386
+ const linked = getLinkedProject(cwd);
387
+ if (!linked) {
388
+ // Show local functions instead
389
+ const fns = discoverFunctions(cwd);
390
+ if (fns.length === 0) {
391
+ console.log("No functions found locally or remotely.");
392
+ return;
393
+ }
394
+ console.log("Local functions (not linked to a cloud project):\n");
395
+ for (const fn of fns) {
396
+ console.log(` ${fn.name.padEnd(30)} ${relative(cwd, fn.entrypoint)}`);
397
+ }
398
+ return;
399
+ }
400
+ const token = getCloudToken();
401
+ if (!token) {
402
+ console.error("Not logged in. Run: npx supatype cloud login");
403
+ process.exit(1);
404
+ }
405
+ try {
406
+ const res = await fetch(`${getCloudApiUrl()}/api/v1/projects/${linked.ref}/functions`, {
407
+ headers: {
408
+ Authorization: `Bearer ${token}`,
409
+ "X-Org-Id": linked.orgId ?? "",
410
+ },
411
+ signal: AbortSignal.timeout(10_000),
412
+ });
413
+ if (!res.ok) {
414
+ console.error(`Failed to list functions: ${res.statusText}`);
415
+ process.exit(1);
416
+ }
417
+ const { data } = await res.json();
418
+ if (data.length === 0) {
419
+ console.log("No deployed functions.");
420
+ return;
421
+ }
422
+ console.log("Deployed functions:\n");
423
+ console.log(` ${"Name".padEnd(28)} ${"Last Deployed".padEnd(24)} ${"Invocations (24h)".padEnd(20)} Avg Duration`);
424
+ console.log(` ${"─".repeat(28)} ${"─".repeat(24)} ${"─".repeat(20)} ${"─".repeat(12)}`);
425
+ for (const fn of data) {
426
+ const deployed = fn.deployedAt ? new Date(fn.deployedAt).toLocaleString() : "—";
427
+ console.log(` ${fn.name.padEnd(28)} ${deployed.padEnd(24)} ${String(fn.invocations24h ?? 0).padEnd(20)} ${fn.avgDurationMs ?? 0}ms`);
428
+ }
429
+ }
430
+ catch (err) {
431
+ console.error(`Error: ${err instanceof Error ? err.message : "unknown"}`);
432
+ process.exit(1);
433
+ }
434
+ }
435
+ // ─── Delete ──────────────────────────────────────────────────────────────────
436
+ async function deleteFunction(cwd, name) {
437
+ const { getLinkedProject, getCloudToken, getCloudApiUrl } = await loadCloudHelpers();
438
+ const linked = getLinkedProject(cwd);
439
+ if (!linked) {
440
+ console.error("No linked project. Run: npx supatype cloud link");
441
+ process.exit(1);
442
+ }
443
+ const token = getCloudToken();
444
+ if (!token) {
445
+ console.error("Not logged in. Run: npx supatype cloud login");
446
+ process.exit(1);
447
+ }
448
+ try {
449
+ const res = await fetch(`${getCloudApiUrl()}/api/v1/projects/${linked.ref}/functions/${name}`, {
450
+ method: "DELETE",
451
+ headers: {
452
+ Authorization: `Bearer ${token}`,
453
+ "X-Org-Id": linked.orgId ?? "",
454
+ },
455
+ signal: AbortSignal.timeout(10_000),
456
+ });
457
+ if (!res.ok) {
458
+ const body = await res.json().catch(() => ({}));
459
+ console.error(`Failed to delete "${name}": ${body["message"] ?? res.statusText}`);
460
+ process.exit(1);
461
+ }
462
+ console.log(`Function "${name}" deleted. It will return 404 immediately.`);
463
+ }
464
+ catch (err) {
465
+ console.error(`Error: ${err instanceof Error ? err.message : "unknown"}`);
466
+ process.exit(1);
467
+ }
468
+ }
469
+ // ─── Logs ────────────────────────────────────────────────────────────────────
470
+ async function functionLogs(cwd, name, opts) {
471
+ const { getLinkedProject, getCloudToken, getCloudApiUrl } = await loadCloudHelpers();
472
+ const linked = getLinkedProject(cwd);
473
+ if (!linked) {
474
+ console.error("No linked project. Run: npx supatype cloud link");
475
+ process.exit(1);
476
+ }
477
+ const token = getCloudToken();
478
+ if (!token) {
479
+ console.error("Not logged in. Run: npx supatype cloud login");
480
+ process.exit(1);
481
+ }
482
+ try {
483
+ const res = await fetch(`${getCloudApiUrl()}/api/v1/projects/${linked.ref}/functions/${name}/logs?since=${opts.since}`, {
484
+ headers: {
485
+ Authorization: `Bearer ${token}`,
486
+ "X-Org-Id": linked.orgId ?? "",
487
+ },
488
+ signal: AbortSignal.timeout(10_000),
489
+ });
490
+ if (!res.ok) {
491
+ console.error(`Failed to fetch logs: ${res.statusText}`);
492
+ process.exit(1);
493
+ }
494
+ const { data } = await res.json();
495
+ if (data.length === 0) {
496
+ console.log(`No logs for "${name}" in the last ${opts.since}.`);
497
+ return;
498
+ }
499
+ for (const entry of data) {
500
+ const ts = new Date(entry.timestamp).toISOString().slice(11, 23);
501
+ const level = entry.level.toUpperCase().padEnd(5);
502
+ console.log(`${ts} [${level}] ${entry.message}`);
503
+ }
504
+ }
505
+ catch (err) {
506
+ console.error(`Error: ${err instanceof Error ? err.message : "unknown"}`);
507
+ process.exit(1);
508
+ }
509
+ }
510
+ // ─── Invoke ──────────────────────────────────────────────────────────────────
511
+ async function invoke(cwd, name, opts) {
512
+ let url;
513
+ const headers = { "Content-Type": "application/json" };
514
+ if (opts.local) {
515
+ url = `http://localhost:54321/functions/v1/${name}`;
516
+ }
517
+ else {
518
+ const { getLinkedProject, getCloudToken } = await loadCloudHelpers();
519
+ const linked = getLinkedProject(cwd);
520
+ if (linked) {
521
+ url = `https://${linked.ref}.supatype.io/functions/v1/${name}`;
522
+ const token = getCloudToken();
523
+ if (token && opts.auth) {
524
+ headers["Authorization"] = `Bearer ${token}`;
525
+ }
526
+ }
527
+ else {
528
+ // Default to local
529
+ url = `http://localhost:54321/functions/v1/${name}`;
530
+ }
531
+ }
532
+ if (opts.auth && !headers["Authorization"]) {
533
+ // Generate a test JWT for local invocation
534
+ headers["Authorization"] = "Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJ0ZXN0LXVzZXIiLCJyb2xlIjoiYXV0aGVudGljYXRlZCIsImlhdCI6MTcwMDAwMDAwMH0.test";
535
+ }
536
+ try {
537
+ let body;
538
+ try {
539
+ JSON.parse(opts.data);
540
+ body = opts.data;
541
+ }
542
+ catch {
543
+ console.error("Invalid JSON data. Use --data '{\"key\": \"value\"}'");
544
+ process.exit(1);
545
+ }
546
+ const start = Date.now();
547
+ const res = await fetch(url, {
548
+ method: "POST",
549
+ headers,
550
+ body,
551
+ signal: AbortSignal.timeout(30_000),
552
+ });
553
+ const duration = Date.now() - start;
554
+ const responseBody = await res.text();
555
+ console.log(`Status: ${res.status} (${duration}ms)`);
556
+ console.log();
557
+ // Try to pretty-print JSON
558
+ try {
559
+ const json = JSON.parse(responseBody);
560
+ console.log(JSON.stringify(json, null, 2));
561
+ }
562
+ catch {
563
+ console.log(responseBody);
564
+ }
565
+ }
566
+ catch (err) {
567
+ if (err instanceof TypeError && err.message.includes("fetch")) {
568
+ console.error(`Cannot reach ${url}`);
569
+ console.error("Is the function server running? Start it with: npx supatype functions serve");
570
+ }
571
+ else {
572
+ console.error(`Error: ${err instanceof Error ? err.message : "unknown"}`);
573
+ }
574
+ process.exit(1);
575
+ }
576
+ }
577
+ // ─── Env management ──────────────────────────────────────────────────────────
578
+ async function envList(cwd) {
579
+ const { getLinkedProject, getCloudToken, getCloudApiUrl } = await loadCloudHelpers();
580
+ const linked = getLinkedProject(cwd);
581
+ if (!linked) {
582
+ // Show local env vars
583
+ const envPath = resolve(cwd, FUNCTIONS_DIR, ENV_LOCAL);
584
+ if (!existsSync(envPath)) {
585
+ console.log("No environment variables configured.");
586
+ return;
587
+ }
588
+ const lines = readFileSync(envPath, "utf8").split("\n");
589
+ console.log("Local environment variables:\n");
590
+ for (const line of lines) {
591
+ const trimmed = line.trim();
592
+ if (!trimmed || trimmed.startsWith("#"))
593
+ continue;
594
+ const eqIdx = trimmed.indexOf("=");
595
+ if (eqIdx > 0) {
596
+ const key = trimmed.slice(0, eqIdx);
597
+ console.log(` ${key} = ••••••••`);
598
+ }
599
+ }
600
+ return;
601
+ }
602
+ const token = getCloudToken();
603
+ if (!token) {
604
+ console.error("Not logged in. Run: npx supatype cloud login");
605
+ process.exit(1);
606
+ }
607
+ try {
608
+ const res = await fetch(`${getCloudApiUrl()}/api/v1/projects/${linked.ref}/functions/env`, {
609
+ headers: {
610
+ Authorization: `Bearer ${token}`,
611
+ "X-Org-Id": linked.orgId ?? "",
612
+ },
613
+ signal: AbortSignal.timeout(10_000),
614
+ });
615
+ if (!res.ok) {
616
+ console.error(`Failed to list env vars: ${res.statusText}`);
617
+ process.exit(1);
618
+ }
619
+ const { data } = await res.json();
620
+ if (data.length === 0) {
621
+ console.log("No environment variables set.");
622
+ return;
623
+ }
624
+ console.log("Environment variables (values masked):\n");
625
+ for (const key of data) {
626
+ console.log(` ${key} = ••••••••`);
627
+ }
628
+ }
629
+ catch (err) {
630
+ console.error(`Error: ${err instanceof Error ? err.message : "unknown"}`);
631
+ process.exit(1);
632
+ }
633
+ }
634
+ async function envSet(cwd, keyvalue) {
635
+ const eqIdx = keyvalue.indexOf("=");
636
+ if (eqIdx <= 0) {
637
+ console.error("Invalid format. Use: npx supatype functions env set KEY=value");
638
+ process.exit(1);
639
+ }
640
+ const key = keyvalue.slice(0, eqIdx);
641
+ const value = keyvalue.slice(eqIdx + 1);
642
+ const { getLinkedProject, getCloudToken, getCloudApiUrl } = await loadCloudHelpers();
643
+ const linked = getLinkedProject(cwd);
644
+ if (!linked) {
645
+ // Set in local env file
646
+ const envPath = resolve(cwd, FUNCTIONS_DIR, ENV_LOCAL);
647
+ let content = existsSync(envPath) ? readFileSync(envPath, "utf8") : "";
648
+ // Replace existing or append
649
+ const regex = new RegExp(`^${key}=.*$`, "m");
650
+ if (regex.test(content)) {
651
+ content = content.replace(regex, `${key}=${value}`);
652
+ }
653
+ else {
654
+ content = content.trimEnd() + `\n${key}=${value}\n`;
655
+ }
656
+ writeFileSync(envPath, content, "utf8");
657
+ console.log(`Set ${key} in local env file.`);
658
+ return;
659
+ }
660
+ const token = getCloudToken();
661
+ if (!token) {
662
+ console.error("Not logged in. Run: npx supatype cloud login");
663
+ process.exit(1);
664
+ }
665
+ try {
666
+ const res = await fetch(`${getCloudApiUrl()}/api/v1/projects/${linked.ref}/functions/env`, {
667
+ method: "POST",
668
+ headers: {
669
+ Authorization: `Bearer ${token}`,
670
+ "Content-Type": "application/json",
671
+ "X-Org-Id": linked.orgId ?? "",
672
+ },
673
+ body: JSON.stringify({ key, value }),
674
+ signal: AbortSignal.timeout(10_000),
675
+ });
676
+ if (!res.ok) {
677
+ const body = await res.json().catch(() => ({}));
678
+ console.error(`Failed to set env var: ${body["message"] ?? res.statusText}`);
679
+ process.exit(1);
680
+ }
681
+ console.log(`Set ${key} for project ${linked.ref}.`);
682
+ }
683
+ catch (err) {
684
+ console.error(`Error: ${err instanceof Error ? err.message : "unknown"}`);
685
+ process.exit(1);
686
+ }
687
+ }
688
+ async function envUnset(cwd, key) {
689
+ const { getLinkedProject, getCloudToken, getCloudApiUrl } = await loadCloudHelpers();
690
+ const linked = getLinkedProject(cwd);
691
+ if (!linked) {
692
+ const envPath = resolve(cwd, FUNCTIONS_DIR, ENV_LOCAL);
693
+ if (!existsSync(envPath)) {
694
+ console.error("No local env file found.");
695
+ process.exit(1);
696
+ }
697
+ let content = readFileSync(envPath, "utf8");
698
+ const regex = new RegExp(`^${key}=.*\n?`, "m");
699
+ content = content.replace(regex, "");
700
+ writeFileSync(envPath, content, "utf8");
701
+ console.log(`Removed ${key} from local env file.`);
702
+ return;
703
+ }
704
+ const token = getCloudToken();
705
+ if (!token) {
706
+ console.error("Not logged in. Run: npx supatype cloud login");
707
+ process.exit(1);
708
+ }
709
+ try {
710
+ const res = await fetch(`${getCloudApiUrl()}/api/v1/projects/${linked.ref}/functions/env/${key}`, {
711
+ method: "DELETE",
712
+ headers: {
713
+ Authorization: `Bearer ${token}`,
714
+ "X-Org-Id": linked.orgId ?? "",
715
+ },
716
+ signal: AbortSignal.timeout(10_000),
717
+ });
718
+ if (!res.ok) {
719
+ const body = await res.json().catch(() => ({}));
720
+ console.error(`Failed to unset env var: ${body["message"] ?? res.statusText}`);
721
+ process.exit(1);
722
+ }
723
+ console.log(`Removed ${key} for project ${linked.ref}.`);
724
+ }
725
+ catch (err) {
726
+ console.error(`Error: ${err instanceof Error ? err.message : "unknown"}`);
727
+ process.exit(1);
728
+ }
729
+ }
730
+ async function loadCloudHelpers() {
731
+ // These helpers read the local .supatype/linked.json and auth token
732
+ return {
733
+ getLinkedProject(cwd) {
734
+ const linkedPath = resolve(cwd, ".supatype/linked.json");
735
+ if (!existsSync(linkedPath))
736
+ return null;
737
+ try {
738
+ const data = JSON.parse(readFileSync(linkedPath, "utf8"));
739
+ const ref = data["ref"];
740
+ const orgId = data["orgId"];
741
+ return ref ? { ref, ...(orgId !== undefined ? { orgId } : {}) } : null;
742
+ }
743
+ catch {
744
+ return null;
745
+ }
746
+ },
747
+ getCloudToken() {
748
+ // Check env first, then config file
749
+ if (process.env["SUPATYPE_ACCESS_TOKEN"]) {
750
+ return process.env["SUPATYPE_ACCESS_TOKEN"];
751
+ }
752
+ const tokenPath = resolve(process.env["HOME"] ?? process.env["USERPROFILE"] ?? "~", ".supatype/token");
753
+ if (!existsSync(tokenPath))
754
+ return null;
755
+ return readFileSync(tokenPath, "utf8").trim() || null;
756
+ },
757
+ getCloudApiUrl() {
758
+ return process.env["SUPATYPE_API_URL"] ?? "https://api.supatype.io";
759
+ },
760
+ };
761
+ }
762
+ //# sourceMappingURL=functions.js.map