clefbase 1.5.3 → 2.0.1

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.
@@ -4,6 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.runInit = runInit;
7
+ exports.scaffoldFunctions = scaffoldFunctions;
7
8
  exports.scaffoldLib = scaffoldLib;
8
9
  const path_1 = __importDefault(require("path"));
9
10
  const fs_1 = __importDefault(require("fs"));
@@ -48,18 +49,10 @@ async function runInit(cwd = process.cwd()) {
48
49
  mask: "●",
49
50
  validate: (v) => v.trim().length > 0 || "Required",
50
51
  }]);
51
- const { adminSecret } = await inquirer_1.default.prompt([{
52
- type: "password",
53
- name: "adminSecret",
54
- message: "Admin Secret (x-admin-secret, needed for hosting)",
55
- mask: "●",
56
- validate: (v) => v.trim().length > 0 || "Required",
57
- }]);
58
52
  const cfg = {
59
53
  serverUrl: serverUrl.replace(/\/+$/, ""),
60
54
  projectId: projectId.trim(),
61
55
  apiKey: apiKey.trim(),
62
- adminSecret: adminSecret.trim(),
63
56
  services: { database: false, auth: false, storage: false, hosting: false, functions: false },
64
57
  };
65
58
  // ── Step 2: Connection test ───────────────────────────────────────────────
@@ -96,11 +89,16 @@ async function runInit(cwd = process.cwd()) {
96
89
  if (cfg.services.hosting) {
97
90
  await setupHosting(cfg, cwd);
98
91
  }
99
- // ── Step 5: Write root config files ──────────────────────────────────────
92
+ // ── Step 5: Functions setup ───────────────────────────────────────────────
93
+ let functionsRuntime;
94
+ if (cfg.services.functions) {
95
+ functionsRuntime = await setupFunctions(cfg, cwd);
96
+ }
97
+ // ── Step 6: Write root config files ──────────────────────────────────────
100
98
  const configPath = (0, config_1.saveConfig)(cfg, cwd);
101
99
  (0, config_1.ensureGitignore)(cwd);
102
100
  (0, config_1.writeEnvExample)(cfg, cwd);
103
- // ── Step 6: Scaffold src/lib ──────────────────────────────────────────────
101
+ // ── Step 7: Scaffold src/lib ──────────────────────────────────────────────
104
102
  const libResult = scaffoldLib(cfg, cwd);
105
103
  // ── Done ──────────────────────────────────────────────────────────────────
106
104
  console.log();
@@ -113,9 +111,602 @@ async function runInit(cwd = process.cwd()) {
113
111
  console.log(chalk_1.default.dim(` Lib config: ${libResult.configCopy}`));
114
112
  console.log(chalk_1.default.dim(` Lib entry: ${libResult.libFile}`));
115
113
  }
114
+ if (cfg.services.functions) {
115
+ console.log(chalk_1.default.dim(` Functions: functions/ (${functionsRuntime ?? "node"})`));
116
+ console.log(chalk_1.default.dim(` run: node functions/deploy.mjs`));
117
+ }
116
118
  console.log();
117
119
  printUsageHint(cfg);
118
120
  }
121
+ // ─── Functions scaffolding ────────────────────────────────────────────────────
122
+ /**
123
+ * Interactively scaffold the `functions/` directory.
124
+ * Returns the chosen runtime(s) so the done-summary can print it.
125
+ */
126
+ async function setupFunctions(cfg, cwd) {
127
+ console.log();
128
+ console.log(chalk_1.default.bold(" Functions"));
129
+ console.log();
130
+ const { runtime } = await inquirer_1.default.prompt([{
131
+ type: "list",
132
+ name: "runtime",
133
+ message: "Default functions runtime",
134
+ choices: [
135
+ { name: "Node.js / TypeScript (recommended)", value: "node" },
136
+ { name: "Python 3", value: "python" },
137
+ { name: "Both (create examples for each)", value: "both" },
138
+ ],
139
+ default: "node",
140
+ }]);
141
+ const scaffoldResult = scaffoldFunctions(cfg, cwd, runtime);
142
+ const sp = (0, ora_1.default)("Creating functions/ folder…").start();
143
+ sp.succeed(chalk_1.default.green(`functions/ created (${scaffoldResult.files.length} file${scaffoldResult.files.length !== 1 ? "s" : ""})`));
144
+ return runtime;
145
+ }
146
+ /**
147
+ * Write the full `functions/` directory scaffold.
148
+ * Safe to call multiple times — existing files are NOT overwritten.
149
+ */
150
+ function scaffoldFunctions(cfg, cwd = process.cwd(), runtime = "node") {
151
+ const dir = path_1.default.join(cwd, "functions");
152
+ const files = [];
153
+ fs_1.default.mkdirSync(dir, { recursive: true });
154
+ const write = (rel, content) => {
155
+ const abs = path_1.default.join(dir, rel);
156
+ fs_1.default.mkdirSync(path_1.default.dirname(abs), { recursive: true });
157
+ if (!fs_1.default.existsSync(abs)) {
158
+ fs_1.default.writeFileSync(abs, content);
159
+ files.push(path_1.default.join("functions", rel));
160
+ }
161
+ };
162
+ const wantNode = runtime === "node" || runtime === "both";
163
+ const wantPython = runtime === "python" || runtime === "both";
164
+ // ── README ────────────────────────────────────────────────────────────────
165
+ write("README.md", buildFunctionsReadme(cfg, runtime));
166
+ // ── .env ──────────────────────────────────────────────────────────────────
167
+ write(".env", buildFunctionsEnv(cfg));
168
+ write(".env.example", buildFunctionsEnvExample(cfg));
169
+ // ── .gitignore ────────────────────────────────────────────────────────────
170
+ write(".gitignore", FUNCTIONS_GITIGNORE);
171
+ // ── Node / TypeScript ─────────────────────────────────────────────────────
172
+ if (wantNode) {
173
+ write("src/hello.ts", NODE_HELLO_TS);
174
+ write("src/onUserCreate.ts", NODE_ON_USER_CREATE_TS);
175
+ write("src/scheduled.ts", NODE_SCHEDULED_TS);
176
+ write("tsconfig.json", FUNCTIONS_TSCONFIG);
177
+ }
178
+ // ── Python ────────────────────────────────────────────────────────────────
179
+ if (wantPython) {
180
+ write("python/hello.py", PYTHON_HELLO_PY);
181
+ write("python/on_user_create.py", PYTHON_ON_USER_CREATE_PY);
182
+ write("python/scheduled.py", PYTHON_SCHEDULED_PY);
183
+ write("python/requirements.txt", PYTHON_REQUIREMENTS_TXT);
184
+ }
185
+ // ── package.json (npm run deploy, npm run deploy:all) ───────────────────────
186
+ write("package.json", buildFunctionsPackageJson(cfg, runtime));
187
+ // ── deploy script (cross-platform Node) ──────────────────────────────────
188
+ write("deploy.mjs", buildDeployMjs(cfg, runtime));
189
+ return { dir, files };
190
+ }
191
+ // ─── File templates ───────────────────────────────────────────────────────────
192
+ function buildFunctionsReadme(cfg, runtime) {
193
+ const wantNode = runtime === "node" || runtime === "both";
194
+ const wantPython = runtime === "python" || runtime === "both";
195
+ const baseUrl = cfg.serverUrl.replace(/\/+$/, "");
196
+ return `# Clefbase Functions
197
+
198
+ Serverless functions for project \`${cfg.projectId}\`.
199
+
200
+ Functions run on the Clefbase server and are triggered by HTTP calls,
201
+ database events, auth events, storage events, or a cron schedule.
202
+ No infrastructure to manage — just write code and deploy.
203
+
204
+ ---
205
+
206
+ ## Quick start
207
+ ${wantNode ? `
208
+ ### Node.js / TypeScript
209
+
210
+ \`\`\`bash
211
+ # 1. Install the Clefbase CLI (if not already)
212
+ npm install -g clefbase
213
+
214
+ # 2. Deploy a function
215
+ clefbase functions:deploy -f src/hello.ts --name hello --trigger http
216
+
217
+ # 3. Call it
218
+ clefbase functions:call hello -d '{"name":"World"}'
219
+ # or via HTTP
220
+ curl -X POST ${baseUrl}/functions/call/hello \\
221
+ -H "x-cfx-key: <YOUR_API_KEY>" \\
222
+ -H "Content-Type: application/json" \\
223
+ -d '{"data":{"name":"World"}}'
224
+ \`\`\`
225
+ ` : ""}${wantPython ? `
226
+ ### Python 3
227
+
228
+ \`\`\`bash
229
+ # Deploy a Python function
230
+ clefbase functions:deploy -f python/hello.py --name hello-py --runtime python --trigger http
231
+
232
+ # Call it
233
+ clefbase functions:call hello-py -d '{"name":"World"}'
234
+ \`\`\`
235
+ ` : ""}
236
+ ---
237
+
238
+ ## Writing functions
239
+
240
+ Every function must export a single async **\`handler\`** (or the name you
241
+ pass as \`--entry\`). It receives one argument — a **\`FunctionContext\`** — and
242
+ can return any JSON-serialisable value.
243
+
244
+ ### FunctionContext shape
245
+
246
+ \`\`\`ts
247
+ interface FunctionContext {
248
+ /** Payload supplied by the caller (HTTP) or the event (triggers). */
249
+ data: unknown;
250
+
251
+ /** Populated when the caller includes a valid auth token. */
252
+ auth?: {
253
+ uid: string;
254
+ email?: string;
255
+ metadata: Record<string, unknown>;
256
+ };
257
+
258
+ /** Describes what fired the function. */
259
+ trigger: {
260
+ type: string; // "http" | "schedule" | "onDocumentCreate" | …
261
+ timestamp: string; // ISO 8601
262
+ document?: unknown; // populated for document triggers
263
+ before?: unknown; // populated for onDocumentUpdate / onDocumentWrite
264
+ after?: unknown; // populated for onDocumentUpdate / onDocumentWrite
265
+ user?: unknown; // populated for onUserCreate / onUserDelete
266
+ file?: unknown; // populated for onFileUpload / onFileDelete
267
+ };
268
+
269
+ /** Key-value env vars you passed at deploy time. */
270
+ env: Record<string, string>;
271
+ }
272
+ \`\`\`
273
+
274
+ ### Trigger types
275
+
276
+ | Type | When it fires |
277
+ |------|---------------|
278
+ | \`http\` | \`POST /functions/call/:name\` |
279
+ | \`schedule\` | Cron timer (e.g. \`0 * * * *\` = every hour) |
280
+ | \`onDocumentCreate\` | A document is created in the watched collection |
281
+ | \`onDocumentUpdate\` | A document is updated |
282
+ | \`onDocumentDelete\` | A document is deleted |
283
+ | \`onDocumentWrite\` | Any of create / update / delete |
284
+ | \`onUserCreate\` | A new user account is created |
285
+ | \`onUserDelete\` | A user account is deleted |
286
+ | \`onFileUpload\` | A file is uploaded to Storage |
287
+ | \`onFileDelete\` | A file is deleted from Storage |
288
+
289
+ ---
290
+
291
+ ## Deployment examples
292
+
293
+ \`\`\`bash
294
+ # HTTP function
295
+ clefbase functions:deploy -f src/hello.ts --trigger http
296
+
297
+ # Scheduled (every day at midnight UTC)
298
+ clefbase functions:deploy -f src/scheduled.ts --trigger schedule --cron "0 0 * * *"
299
+
300
+ # Document trigger (fires on every new "orders" document)
301
+ clefbase functions:deploy -f src/onOrder.ts --trigger onDocumentCreate --collection orders
302
+
303
+ # With env vars and a custom timeout
304
+ clefbase functions:deploy -f src/sendEmail.ts \\
305
+ --trigger http \\
306
+ --env SENDGRID_KEY=SG.xxx \\
307
+ --timeout 15000
308
+
309
+ # Use the deploy script in this folder (deploys everything at once)
310
+ node deploy.mjs
311
+ \`\`\`
312
+
313
+ ---
314
+
315
+ ## Calling HTTP functions
316
+
317
+ ### From the Clefbase SDK
318
+
319
+ \`\`\`ts
320
+ import { getFunctions, httpsCallable } from "clefbase";
321
+ // or: import { fns } from "@lib/clefBase";
322
+
323
+ const greet = httpsCallable<{ name: string }, { message: string }>(fns, "hello");
324
+ const { data, durationMs } = await greet({ name: "Alice" });
325
+ console.log(data.message); // "Hello, Alice!"
326
+ console.log(durationMs); // e.g. 42
327
+ \`\`\`
328
+
329
+ ### Auth-aware calls
330
+
331
+ \`\`\`ts
332
+ import { getAuth, setAuthToken } from "clefbase";
333
+
334
+ const { token } = await getAuth(app).signIn("alice@example.com", "password");
335
+ setAuthToken(app, token);
336
+
337
+ // ctx.auth.uid and ctx.auth.email are now available inside the function
338
+ const { data } = await callFunction(fns, "getProfile");
339
+ \`\`\`
340
+
341
+ ### Raw HTTP (no SDK)
342
+
343
+ \`\`\`bash
344
+ curl -X POST ${baseUrl}/functions/call/<functionName> \\
345
+ -H "x-cfx-key: <YOUR_API_KEY>" \\
346
+ -H "Content-Type: application/json" \\
347
+ -H "Authorization: Bearer <USER_JWT>" # optional auth
348
+ -d '{"data": { "key": "value" }}'
349
+ \`\`\`
350
+
351
+ ---
352
+
353
+ ## Viewing logs
354
+
355
+ \`\`\`bash
356
+ # Last 20 executions
357
+ clefbase functions:logs hello
358
+
359
+ # Last 50
360
+ clefbase functions:logs hello -l 50
361
+
362
+ # All functions
363
+ clefbase functions:list
364
+ \`\`\`
365
+
366
+ ---
367
+
368
+ ## Managing functions
369
+
370
+ \`\`\`bash
371
+ clefbase functions:list # see all deployed functions
372
+ clefbase functions:delete oldFunction # delete (prompts for confirmation)
373
+ clefbase functions:delete oldFunction -y # delete without confirmation
374
+ \`\`\`
375
+
376
+ ---
377
+
378
+ ## Environment variables
379
+
380
+ Secrets and config for your functions live in \`functions/.env\`.
381
+ They are **never** committed to git (already in \`.gitignore\`).
382
+
383
+ Pass them at deploy time with \`--env KEY=VALUE\` or edit \`deploy.sh\`
384
+ to read them automatically from \`.env\`.
385
+
386
+ ---
387
+
388
+ ## Project info
389
+
390
+ | Key | Value |
391
+ |-----|-------|
392
+ | Project ID | \`${cfg.projectId}\` |
393
+ | Server | \`${baseUrl}\` |
394
+ | Functions base URL | \`${baseUrl}/functions\` |
395
+ `;
396
+ }
397
+ function buildFunctionsEnv(cfg) {
398
+ return `# Clefbase Functions — secrets for this project
399
+ # This file is git-ignored. Do NOT commit it.
400
+ # Copy values from your Clefbase dashboard.
401
+
402
+ CLEFBASE_SERVER_URL=${cfg.serverUrl}
403
+ CLEFBASE_PROJECT_ID=${cfg.projectId}
404
+ CLEFBASE_API_KEY=${cfg.apiKey}
405
+
406
+ # Add your own secrets below:
407
+ # SENDGRID_API_KEY=
408
+ # STRIPE_SECRET_KEY=
409
+ # DATABASE_URL=
410
+ `;
411
+ }
412
+ function buildFunctionsEnvExample(cfg) {
413
+ return `# Clefbase Functions — environment variable template
414
+ # Copy this to .env and fill in your values.
415
+
416
+ CLEFBASE_SERVER_URL=${cfg.serverUrl}
417
+ CLEFBASE_PROJECT_ID=${cfg.projectId}
418
+ CLEFBASE_API_KEY=your_api_key_here
419
+
420
+ # Add your own secrets below:
421
+ # SENDGRID_API_KEY=
422
+ # STRIPE_SECRET_KEY=
423
+ # DATABASE_URL=
424
+ `;
425
+ }
426
+ const FUNCTIONS_GITIGNORE = `# Dependencies
427
+ node_modules/
428
+
429
+ # Python
430
+ __pycache__/
431
+ *.pyc
432
+ *.pyo
433
+ .venv/
434
+ venv/
435
+
436
+ # Secrets — never commit these
437
+ .env
438
+
439
+ # Build artefacts
440
+ dist/
441
+ *.js.map
442
+ `;
443
+ const FUNCTIONS_TSCONFIG = `{
444
+ "compilerOptions": {
445
+ "target": "ES2022",
446
+ "module": "ESNext",
447
+ "moduleResolution": "bundler",
448
+ "lib": ["ES2022"],
449
+ "strict": true,
450
+ "esModuleInterop": true,
451
+ "skipLibCheck": true,
452
+ "outDir": "dist"
453
+ },
454
+ "include": ["src/**/*"],
455
+ "exclude": ["node_modules", "dist"]
456
+ }
457
+ `;
458
+ // ─── Node / TypeScript examples ───────────────────────────────────────────────
459
+ const NODE_HELLO_TS = `/**
460
+ * hello.ts — HTTP-triggered "hello world" function.
461
+ *
462
+ * Deploy:
463
+ * clefbase functions:deploy -f src/hello.ts --name hello --trigger http
464
+ *
465
+ * Call:
466
+ * clefbase functions:call hello -d '{"name":"World"}'
467
+ */
468
+
469
+ import type { FunctionContext } from "clefbase";
470
+
471
+ interface Input { name?: string }
472
+ interface Output { message: string; timestamp: string }
473
+
474
+ export async function handler(ctx: FunctionContext): Promise<Output> {
475
+ const { name = "World" } = (ctx.data ?? {}) as Input;
476
+
477
+ return {
478
+ message: \`Hello, \${name}!\`,
479
+ timestamp: ctx.trigger.timestamp,
480
+ };
481
+ }
482
+ `;
483
+ const NODE_ON_USER_CREATE_TS = `/**
484
+ * onUserCreate.ts — fires whenever a new user signs up.
485
+ *
486
+ * Deploy:
487
+ * clefbase functions:deploy -f src/onUserCreate.ts \\
488
+ * --name onUserCreate --trigger onUserCreate
489
+ *
490
+ * ctx.trigger.user contains the new user's profile.
491
+ */
492
+
493
+ import type { FunctionContext } from "clefbase";
494
+
495
+ export async function handler(ctx: FunctionContext): Promise<void> {
496
+ const user = ctx.trigger.user as {
497
+ uid: string;
498
+ email?: string;
499
+ metadata: Record<string, unknown>;
500
+ } | undefined;
501
+
502
+ if (!user) return;
503
+
504
+ console.log(\`New user: \${user.uid} email: \${user.email ?? "—"}\`);
505
+
506
+ // Example: send a welcome email, add to a mailing list, seed default data…
507
+ // const emailKey = ctx.env["SENDGRID_API_KEY"];
508
+ }
509
+ `;
510
+ const NODE_SCHEDULED_TS = `/**
511
+ * scheduled.ts — runs on a cron schedule.
512
+ *
513
+ * Deploy (every day at midnight UTC):
514
+ * clefbase functions:deploy -f src/scheduled.ts \\
515
+ * --name dailyCleanup --trigger schedule --cron "0 0 * * *"
516
+ *
517
+ * Common cron expressions:
518
+ * "* * * * *" every minute
519
+ * "0 * * * *" every hour
520
+ * "0 9 * * 1" every Monday at 09:00 UTC
521
+ */
522
+
523
+ import type { FunctionContext } from "clefbase";
524
+
525
+ export async function handler(ctx: FunctionContext): Promise<{ cleaned: number }> {
526
+ console.log("Running scheduled cleanup at", ctx.trigger.timestamp);
527
+
528
+ // TODO: your scheduled work here
529
+ const cleaned = 0;
530
+
531
+ return { cleaned };
532
+ }
533
+ `;
534
+ // ─── Python examples ──────────────────────────────────────────────────────────
535
+ const PYTHON_HELLO_PY = `"""
536
+ hello.py — HTTP-triggered "hello world" function.
537
+
538
+ Deploy:
539
+ clefbase functions:deploy -f python/hello.py \\
540
+ --name hello-py --runtime python --trigger http
541
+
542
+ Call:
543
+ clefbase functions:call hello-py -d '{"name":"World"}'
544
+ """
545
+
546
+
547
+ async def handler(ctx):
548
+ """
549
+ ctx.data — payload from the caller
550
+ ctx.auth — populated when a valid auth token is included
551
+ ctx.trigger — describes what fired this function
552
+ ctx.env — env vars you passed at deploy time
553
+ """
554
+ data = ctx.get("data") or {}
555
+ name = data.get("name", "World")
556
+
557
+ return {
558
+ "message": f"Hello, {name}!",
559
+ "timestamp": ctx["trigger"]["timestamp"],
560
+ }
561
+ `;
562
+ const PYTHON_ON_USER_CREATE_PY = `"""
563
+ on_user_create.py — fires whenever a new user signs up.
564
+
565
+ Deploy:
566
+ clefbase functions:deploy -f python/on_user_create.py \\
567
+ --name onUserCreate-py --runtime python --trigger onUserCreate
568
+ """
569
+
570
+
571
+ async def handler(ctx):
572
+ user = (ctx.get("trigger") or {}).get("user") or {}
573
+
574
+ uid = user.get("uid", "unknown")
575
+ email = user.get("email", "—")
576
+
577
+ print(f"New user: {uid} email: {email}")
578
+
579
+ # Example: send a welcome email, seed default data, etc.
580
+ # sendgrid_key = ctx["env"].get("SENDGRID_API_KEY")
581
+ `;
582
+ const PYTHON_SCHEDULED_PY = `"""
583
+ scheduled.py — runs on a cron schedule.
584
+
585
+ Deploy (every day at midnight UTC):
586
+ clefbase functions:deploy -f python/scheduled.py \\
587
+ --name dailyCleanup-py --runtime python \\
588
+ --trigger schedule --cron "0 0 * * *"
589
+ """
590
+
591
+
592
+ async def handler(ctx):
593
+ print("Running scheduled cleanup at", ctx["trigger"]["timestamp"])
594
+
595
+ # TODO: your scheduled work here
596
+ cleaned = 0
597
+
598
+ return {"cleaned": cleaned}
599
+ `;
600
+ const PYTHON_REQUIREMENTS_TXT = `# Add your Python dependencies here.
601
+ # They are NOT automatically installed on the server — include only stdlib
602
+ # and packages already available in the Clefbase Python runtime.
603
+ #
604
+ # For heavier workloads, consider the Node.js runtime and npm packages.
605
+ #
606
+ # httpx>=0.27
607
+ # pydantic>=2.0
608
+ `;
609
+ // ─── deploy.mjs (cross-platform — runs anywhere Node.js is installed) ────────
610
+ function buildDeployMjs(cfg, runtime) {
611
+ const wantNode = runtime === "node" || runtime === "both";
612
+ const wantPython = runtime === "python" || runtime === "both";
613
+ const fns = [];
614
+ if (wantNode) {
615
+ fns.push({ name: "hello", file: "src/hello.ts", runtime: "node", trigger: "http" }, { name: "onUserCreate", file: "src/onUserCreate.ts", runtime: "node", trigger: "onUserCreate" }, { name: "dailyCleanup", file: "src/scheduled.ts", runtime: "node", trigger: "schedule", cron: "0 0 * * *" });
616
+ }
617
+ if (wantPython) {
618
+ fns.push({ name: "hello-py", file: "python/hello.py", runtime: "python", trigger: "http" }, { name: "onUserCreate-py", file: "python/on_user_create.py", runtime: "python", trigger: "onUserCreate" }, { name: "dailyCleanup-py", file: "python/scheduled.py", runtime: "python", trigger: "schedule", cron: "0 0 * * *" });
619
+ }
620
+ const fnJson = JSON.stringify(fns, null, 2);
621
+ return `#!/usr/bin/env node
622
+ // deploy.mjs — deploy all functions in this folder to Clefbase
623
+ // Generated by \`clefbase init\` • project: ${cfg.projectId}
624
+ //
625
+ // Usage (any OS): node deploy.mjs
626
+ //
627
+ // Node.js 18+ required (uses built-in fetch + fs).
628
+ // No extra dependencies — just the Clefbase CLI on your PATH.
629
+
630
+ import { execSync } from "node:child_process";
631
+ import { existsSync, readFileSync } from "node:fs";
632
+ import { fileURLToPath } from "node:url";
633
+ import { dirname, join } from "node:path";
634
+
635
+ const __dirname = dirname(fileURLToPath(import.meta.url));
636
+
637
+ // ── Load .env ─────────────────────────────────────────────────────────────────
638
+
639
+ const envPath = join(__dirname, ".env");
640
+ if (existsSync(envPath)) {
641
+ for (const line of readFileSync(envPath, "utf8").split("\\n")) {
642
+ const match = line.match(/^\\s*([^#][^=]*)=(.*)$/);
643
+ if (match) process.env[match[1].trim()] = match[2].trim();
644
+ }
645
+ }
646
+
647
+ // ── Functions to deploy ───────────────────────────────────────────────────────
648
+
649
+ const functions = ${fnJson};
650
+
651
+ // ── Deploy ────────────────────────────────────────────────────────────────────
652
+
653
+ let passed = 0;
654
+ let failed = 0;
655
+
656
+ for (const fn of functions) {
657
+ const args = [
658
+ \`--name \${fn.name}\`,
659
+ \`--file \${fn.file}\`,
660
+ \`--runtime \${fn.runtime}\`,
661
+ \`--trigger \${fn.trigger}\`,
662
+ fn.cron ? \`--cron "\${fn.cron}"\` : "",
663
+ fn.collection ? \`--collection "\${fn.collection}"\` : "",
664
+ ].filter(Boolean).join(" ");
665
+
666
+ console.log(\`\\n→ Deploying "\${fn.name}"…\`);
667
+ try {
668
+ execSync(\`clefbase functions:deploy \${args}\`, { stdio: "inherit", cwd: __dirname });
669
+ passed++;
670
+ } catch {
671
+ console.error(\` ✗ "\${fn.name}" failed\`);
672
+ failed++;
673
+ }
674
+ }
675
+
676
+ // ── Summary ───────────────────────────────────────────────────────────────────
677
+
678
+ console.log(\`\\n\${passed} deployed, \${failed} failed — project: ${cfg.projectId}\\n\`);
679
+ if (failed > 0) process.exit(1);
680
+ `;
681
+ }
682
+ // ─── functions/package.json ───────────────────────────────────────────────────
683
+ function buildFunctionsPackageJson(cfg, runtime) {
684
+ const wantNode = runtime === "node" || runtime === "both";
685
+ const scripts = {
686
+ "deploy:all": "node deploy.mjs",
687
+ };
688
+ // Individual per-function shortcuts — only Node ones get their own script
689
+ // because Python files are plain paths with no compile step either way.
690
+ if (wantNode) {
691
+ scripts["deploy:hello"] = "clefbase functions:deploy -f src/hello.ts --name hello --runtime node --trigger http";
692
+ scripts["deploy:onUserCreate"] = "clefbase functions:deploy -f src/onUserCreate.ts --name onUserCreate --runtime node --trigger onUserCreate";
693
+ scripts["deploy:scheduled"] = "clefbase functions:deploy -f src/scheduled.ts --name dailyCleanup --runtime node --trigger schedule --cron \"0 0 * * *\"";
694
+ }
695
+ if (runtime === "python" || runtime === "both") {
696
+ scripts["deploy:hello-py"] = "clefbase functions:deploy -f python/hello.py --name hello-py --runtime python --trigger http";
697
+ scripts["deploy:onUserCreate-py"] = "clefbase functions:deploy -f python/on_user_create.py --name onUserCreate-py --runtime python --trigger onUserCreate";
698
+ scripts["deploy:scheduled-py"] = "clefbase functions:deploy -f python/scheduled.py --name dailyCleanup-py --runtime python --trigger schedule --cron \"0 0 * * *\"";
699
+ }
700
+ const pkg = {
701
+ name: `${cfg.projectId}-functions`,
702
+ version: "1.0.0",
703
+ description: `Clefbase Functions for project ${cfg.projectId}`,
704
+ private: true,
705
+ type: "module",
706
+ scripts,
707
+ };
708
+ return JSON.stringify(pkg, null, 2) + "\n";
709
+ }
119
710
  /**
120
711
  * Writes two files into `<cwd>/src/lib/`:
121
712
  * • clefbase.json — a copy of the project config (adminSecret stripped)
@@ -192,10 +783,9 @@ function buildLibTs(cfg) {
192
783
  `// ─── App ─────────────────────────────────────────────────────────────────────`,
193
784
  ``,
194
785
  `const app = initClefbase({`,
195
- ` serverUrl: config.serverUrl,`,
196
- ` projectId: config.projectId,`,
197
- ` apiKey: config.apiKey,`,
198
- ` adminSecret: "",`,
786
+ ` serverUrl: config.serverUrl,`,
787
+ ` projectId: config.projectId,`,
788
+ ` apiKey: config.apiKey,`,
199
789
  `});`,
200
790
  ``,
201
791
  ];
@@ -257,6 +847,7 @@ async function setupHosting(cfg, cwd) {
257
847
  }
258
848
  let siteId = "";
259
849
  let siteName = "";
850
+ let previewUrl = "";
260
851
  if (existing.length > 0) {
261
852
  const choices = [
262
853
  ...existing.map(s => ({ name: `${s.name} ${chalk_1.default.dim(s.id)}`, value: s.id })),
@@ -269,8 +860,10 @@ async function setupHosting(cfg, cwd) {
269
860
  choices,
270
861
  }]);
271
862
  if (chosen !== "__new__") {
863
+ const found = existing.find(s => s.id === chosen);
272
864
  siteId = chosen;
273
- siteName = existing.find(s => s.id === chosen)?.name ?? chosen;
865
+ siteName = found?.name ?? chosen;
866
+ previewUrl = found?.previewUrl ?? "";
274
867
  }
275
868
  }
276
869
  if (!siteId) {
@@ -285,6 +878,7 @@ async function setupHosting(cfg, cwd) {
285
878
  const site = await (0, api_1.createSite)(cfg, newName.trim());
286
879
  siteId = site.id;
287
880
  siteName = site.name;
881
+ previewUrl = site.previewUrl ?? "";
288
882
  s.succeed(chalk_1.default.green(`Created "${siteName}" ${chalk_1.default.dim(siteId)}`));
289
883
  }
290
884
  catch (err) {
@@ -309,6 +903,7 @@ async function setupHosting(cfg, cwd) {
309
903
  cfg.hosting = {
310
904
  siteId,
311
905
  siteName,
906
+ previewUrl,
312
907
  distDir: distDir.trim(),
313
908
  entrypoint: entrypoint.trim(),
314
909
  };
@@ -354,12 +949,15 @@ function printUsageHint(cfg) {
354
949
  if (cfg.services.functions) {
355
950
  console.log();
356
951
  console.log(chalk_1.default.bold(" Functions:"));
357
- console.log(chalk_1.default.cyan(` // Deploy from a file`));
358
- console.log(chalk_1.default.cyan(` $ clefbase functions:deploy -f ./src/functions/hello.ts`));
952
+ console.log(chalk_1.default.cyan(` # Edit your functions in functions/src/`));
953
+ console.log(chalk_1.default.cyan(` $ clefbase functions:deploy -f functions/src/hello.ts`));
359
954
  console.log();
360
- console.log(chalk_1.default.cyan(` // Call from your app`));
955
+ console.log(chalk_1.default.cyan(` # Call from your app`));
361
956
  console.log(chalk_1.default.cyan(` const greet = httpsCallable(fns, "greetUser");`));
362
957
  console.log(chalk_1.default.cyan(` const { data } = await greet({ name: "Alice" });`));
958
+ console.log();
959
+ console.log(chalk_1.default.dim(` Full guide: functions/README.md`));
960
+ console.log(chalk_1.default.dim(` Deploy all: node functions/deploy.mjs`));
363
961
  }
364
962
  if (cfg.services.hosting && cfg.hosting) {
365
963
  console.log();