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.
- package/dist/ai.d.ts +369 -0
- package/dist/ai.d.ts.map +1 -0
- package/dist/ai.js +308 -0
- package/dist/ai.js.map +1 -0
- package/dist/app.d.ts +40 -0
- package/dist/app.d.ts.map +1 -1
- package/dist/app.js +48 -3
- package/dist/app.js.map +1 -1
- package/dist/cli-src/cli/api.js +14 -14
- package/dist/cli-src/cli/commands/deploy.js +84 -16
- package/dist/cli-src/cli/commands/init.js +616 -18
- package/dist/cli-src/cli/config.js +0 -1
- package/dist/cli-src/cli/index.js +48 -9
- package/dist/cli.js +728 -57
- package/dist/hosting/index.d.ts +8 -98
- package/dist/hosting/index.d.ts.map +1 -1
- package/dist/hosting/index.js +37 -95
- package/dist/hosting/index.js.map +1 -1
- package/dist/index.d.ts +74 -36
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +85 -37
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +0 -6
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +1 -1
|
@@ -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:
|
|
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
|
|
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:
|
|
196
|
-
` projectId:
|
|
197
|
-
` 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 =
|
|
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(`
|
|
358
|
-
console.log(chalk_1.default.cyan(` $ clefbase functions:deploy -f
|
|
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(`
|
|
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();
|