clefbase 2.0.0 → 2.0.2

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)
@@ -173,7 +764,18 @@ function buildLibTs(cfg) {
173
764
  ` *`,
174
765
  ` * Usage:`,
175
766
  ...(database ? [` * import { db } from "@lib/clefBase";`] : []),
176
- ...(auth ? [` * import { auth } from "@lib/clefBase";`] : []),
767
+ ...(auth ? [
768
+ ` * import { auth } from "@lib/clefBase";`,
769
+ ` *`,
770
+ ` * // Email / password`,
771
+ ` * const { user } = await auth.signIn("alice@example.com", "pass");`,
772
+ ` *`,
773
+ ` * // Google sign-in — add this to your root component / entry point:`,
774
+ ` * await auth.handleGatewayCallback(); // handles ?cfx_token= on return`,
775
+ ` *`,
776
+ ` * // On sign-in button click:`,
777
+ ` * await auth.signInWithGateway("google");`,
778
+ ] : []),
177
779
  ...(storage ? [` * import { storage } from "@lib/clefBase";`] : []),
178
780
  ...(fns ? [
179
781
  ` * import { fns, httpsCallable } from "@lib/clefBase";`,
@@ -192,10 +794,9 @@ function buildLibTs(cfg) {
192
794
  `// ─── App ─────────────────────────────────────────────────────────────────────`,
193
795
  ``,
194
796
  `const app = initClefbase({`,
195
- ` serverUrl: config.serverUrl,`,
196
- ` projectId: config.projectId,`,
197
- ` apiKey: config.apiKey,`,
198
- ` adminSecret: "",`,
797
+ ` serverUrl: config.serverUrl,`,
798
+ ` projectId: config.projectId,`,
799
+ ` apiKey: config.apiKey,`,
199
800
  `});`,
200
801
  ``,
201
802
  ];
@@ -214,6 +815,20 @@ function buildLibTs(cfg) {
214
815
  lines.push(`/** Clefbase Auth — sign up, sign in, manage sessions. */`);
215
816
  lines.push(`export const auth: Auth = getAuth(app);`);
216
817
  lines.push("");
818
+ lines.push(`/**`);
819
+ lines.push(` * Call this once on every page load (before rendering your UI).`);
820
+ lines.push(` * Detects the ?cfx_token= param the gateway appends after OAuth,`);
821
+ lines.push(` * saves the session, and cleans the URL.`);
822
+ lines.push(` *`);
823
+ lines.push(` * @example`);
824
+ lines.push(` * // React / Next.js — in your root layout or _app.tsx:`);
825
+ lines.push(` * useEffect(() => { auth.handleGatewayCallback(); }, []);`);
826
+ lines.push(` *`);
827
+ lines.push(` * // Vanilla JS — at the top of your entry point:`);
828
+ lines.push(` * await auth.handleGatewayCallback();`);
829
+ lines.push(` */`);
830
+ lines.push(`export { auth };`);
831
+ lines.push("");
217
832
  }
218
833
  if (storage) {
219
834
  lines.push(`/** Clefbase Storage — upload and manage files. */`);
@@ -257,6 +872,7 @@ async function setupHosting(cfg, cwd) {
257
872
  }
258
873
  let siteId = "";
259
874
  let siteName = "";
875
+ let previewUrl = "";
260
876
  if (existing.length > 0) {
261
877
  const choices = [
262
878
  ...existing.map(s => ({ name: `${s.name} ${chalk_1.default.dim(s.id)}`, value: s.id })),
@@ -269,8 +885,10 @@ async function setupHosting(cfg, cwd) {
269
885
  choices,
270
886
  }]);
271
887
  if (chosen !== "__new__") {
888
+ const found = existing.find(s => s.id === chosen);
272
889
  siteId = chosen;
273
- siteName = existing.find(s => s.id === chosen)?.name ?? chosen;
890
+ siteName = found?.name ?? chosen;
891
+ previewUrl = found?.previewUrl ?? "";
274
892
  }
275
893
  }
276
894
  if (!siteId) {
@@ -285,6 +903,7 @@ async function setupHosting(cfg, cwd) {
285
903
  const site = await (0, api_1.createSite)(cfg, newName.trim());
286
904
  siteId = site.id;
287
905
  siteName = site.name;
906
+ previewUrl = site.previewUrl ?? "";
288
907
  s.succeed(chalk_1.default.green(`Created "${siteName}" ${chalk_1.default.dim(siteId)}`));
289
908
  }
290
909
  catch (err) {
@@ -309,6 +928,7 @@ async function setupHosting(cfg, cwd) {
309
928
  cfg.hosting = {
310
929
  siteId,
311
930
  siteName,
931
+ previewUrl,
312
932
  distDir: distDir.trim(),
313
933
  entrypoint: entrypoint.trim(),
314
934
  };
@@ -346,6 +966,8 @@ function printUsageHint(cfg) {
346
966
  if (cfg.services.auth) {
347
967
  console.log();
348
968
  console.log(chalk_1.default.cyan(` const { user } = await auth.signIn("email", "pass");`));
969
+ console.log(chalk_1.default.cyan(` await auth.signInWithGateway("google"); // redirects to auth.cleforyx.com`));
970
+ console.log(chalk_1.default.cyan(` await auth.handleGatewayCallback(); // call on every page load`));
349
971
  }
350
972
  if (cfg.services.storage) {
351
973
  console.log();
@@ -354,12 +976,15 @@ function printUsageHint(cfg) {
354
976
  if (cfg.services.functions) {
355
977
  console.log();
356
978
  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`));
979
+ console.log(chalk_1.default.cyan(` # Edit your functions in functions/src/`));
980
+ console.log(chalk_1.default.cyan(` $ clefbase functions:deploy -f functions/src/hello.ts`));
359
981
  console.log();
360
- console.log(chalk_1.default.cyan(` // Call from your app`));
982
+ console.log(chalk_1.default.cyan(` # Call from your app`));
361
983
  console.log(chalk_1.default.cyan(` const greet = httpsCallable(fns, "greetUser");`));
362
984
  console.log(chalk_1.default.cyan(` const { data } = await greet({ name: "Alice" });`));
985
+ console.log();
986
+ console.log(chalk_1.default.dim(` Full guide: functions/README.md`));
987
+ console.log(chalk_1.default.dim(` Deploy all: node functions/deploy.mjs`));
363
988
  }
364
989
  if (cfg.services.hosting && cfg.hosting) {
365
990
  console.log();