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