create-lumibase 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (37) hide show
  1. package/README.md +84 -0
  2. package/bin/create-lumibase.js +5 -0
  3. package/dist/index.d.ts +12 -0
  4. package/dist/index.js +406 -0
  5. package/dist/templates/cloudflare/_env.example +4 -0
  6. package/dist/templates/cloudflare/_gitignore +7 -0
  7. package/dist/templates/cloudflare/package.json.hbs +27 -0
  8. package/dist/templates/cloudflare/src/index.ts.hbs +16 -0
  9. package/dist/templates/cloudflare/tsconfig.json +13 -0
  10. package/dist/templates/cloudflare/wrangler.toml.hbs +13 -0
  11. package/dist/templates/default/_env.example +14 -0
  12. package/dist/templates/default/_gitignore +5 -0
  13. package/dist/templates/default/docker-compose.yml +21 -0
  14. package/dist/templates/default/drizzle.config.ts +18 -0
  15. package/dist/templates/default/package.json.hbs +30 -0
  16. package/dist/templates/default/src/db/client.ts +14 -0
  17. package/dist/templates/default/src/db/migrate.ts +24 -0
  18. package/dist/templates/default/src/db/schema.ts +34 -0
  19. package/dist/templates/default/src/server.ts.hbs +48 -0
  20. package/dist/templates/default/tsconfig.json +14 -0
  21. package/package.json +54 -0
  22. package/templates/cloudflare/_env.example +4 -0
  23. package/templates/cloudflare/_gitignore +7 -0
  24. package/templates/cloudflare/package.json.hbs +27 -0
  25. package/templates/cloudflare/src/index.ts.hbs +16 -0
  26. package/templates/cloudflare/tsconfig.json +13 -0
  27. package/templates/cloudflare/wrangler.toml.hbs +13 -0
  28. package/templates/default/_env.example +14 -0
  29. package/templates/default/_gitignore +5 -0
  30. package/templates/default/docker-compose.yml +21 -0
  31. package/templates/default/drizzle.config.ts +18 -0
  32. package/templates/default/package.json.hbs +30 -0
  33. package/templates/default/src/db/client.ts +14 -0
  34. package/templates/default/src/db/migrate.ts +24 -0
  35. package/templates/default/src/db/schema.ts +34 -0
  36. package/templates/default/src/server.ts.hbs +48 -0
  37. package/templates/default/tsconfig.json +14 -0
package/README.md ADDED
@@ -0,0 +1,84 @@
1
+ # create-lumibase
2
+
3
+ Scaffold a new [LumiBase](https://lumibase.dev) project — an Edge-native Headless CMS — in seconds.
4
+
5
+ ```bash
6
+ npm create lumibase@latest my-project
7
+ # or
8
+ npx create-lumibase@latest my-project
9
+ # or
10
+ pnpm create lumibase my-project
11
+ ```
12
+
13
+ ## What it does
14
+
15
+ `create-lumibase` bootstraps a ready-to-run LumiBase project into an empty
16
+ directory, the same way `create-next-app` or `create-vite` scaffold their
17
+ respective stacks. It is interactive by default and fully scriptable via flags.
18
+
19
+ ## Interactive flow
20
+
21
+ Running `npm create lumibase@latest` with no arguments walks you through:
22
+
23
+ 1. **Project name** — validated against npm package-name rules.
24
+ 2. **Deployment target** — `Docker` (Node.js + PostgreSQL) or `Cloudflare Workers` (Edge + D1).
25
+ 3. **Package manager** — `pnpm` / `npm` / `yarn` / `bun` (the one you invoked is auto-detected).
26
+ 4. **Install dependencies** — yes/no.
27
+ 5. **Initialize git** — yes/no.
28
+
29
+ The tool then scaffolds the files, optionally runs `git init` + a first commit,
30
+ installs dependencies, and prints the exact next steps for your chosen stack.
31
+
32
+ ## Templates
33
+
34
+ | Template | Flag | Stack |
35
+ | --- | --- | --- |
36
+ | **Docker** (default) | `--template default` | Hono + `@hono/node-server`, Drizzle ORM, PostgreSQL, Redis, `docker-compose.yml` |
37
+ | **Cloudflare Workers** | `--template cloudflare` | Hono, Drizzle ORM, D1, `wrangler.toml` |
38
+
39
+ The `default` template ships a working `posts` resource (`GET`/`POST /posts`)
40
+ that demonstrates LumiBase conventions: `nanoid()` IDs, `site_id`
41
+ multi-tenancy, the `{ data }` / `{ errors }` response format, and Zod
42
+ validation.
43
+
44
+ ## Non-interactive usage
45
+
46
+ Skip every prompt by passing flags:
47
+
48
+ ```bash
49
+ npx create-lumibase@latest my-blog \
50
+ --template default \
51
+ --pm pnpm \
52
+ --no-install \
53
+ --no-git
54
+ ```
55
+
56
+ | Flag | Description |
57
+ | --- | --- |
58
+ | `--template <default\|cloudflare>` | Choose the project template. |
59
+ | `--pm <pnpm\|npm\|yarn\|bun>` | Package manager to install with. |
60
+ | `--install` / `--no-install` | Force-enable or skip dependency install. |
61
+ | `--git` / `--no-git` | Force-enable or skip `git init`. |
62
+ | `DEBUG=1` | Print scaffolded file paths and full stack traces on error. |
63
+
64
+ ## After scaffolding (Docker template)
65
+
66
+ ```bash
67
+ cd my-blog
68
+ cp .env.example .env # fill in your secrets
69
+ pnpm install # if you skipped --install
70
+ docker compose up -d # starts Postgres + Redis
71
+ pnpm run db:generate # generate the first migration
72
+ pnpm run db:migrate # apply it
73
+ pnpm dev # http://localhost:8787
74
+ ```
75
+
76
+ ## Requirements
77
+
78
+ - Node.js `>= 20`
79
+ - For the Docker template: Docker + Docker Compose
80
+ - For the Cloudflare template: a Cloudflare account + `wrangler`
81
+
82
+ ## License
83
+
84
+ Part of the LumiBase monorepo.
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+ import('../dist/index.js').catch((err) => {
3
+ console.error(err);
4
+ process.exit(1);
5
+ });
@@ -0,0 +1,12 @@
1
+ type Template = 'default' | 'cloudflare';
2
+ type PackageManager = 'pnpm' | 'npm' | 'yarn' | 'bun';
3
+ interface ProjectConfig {
4
+ projectName: string;
5
+ targetDir: string;
6
+ template: Template;
7
+ packageManager: PackageManager;
8
+ installDeps: boolean;
9
+ initializeGit: boolean;
10
+ }
11
+
12
+ export type { PackageManager, ProjectConfig, Template };
package/dist/index.js ADDED
@@ -0,0 +1,406 @@
1
+ // src/index.ts
2
+ import { resolve as resolve2, basename as basename2 } from "path";
3
+ import { existsSync, readdirSync as readdirSync2 } from "fs";
4
+ import process3 from "process";
5
+ import pc6 from "picocolors";
6
+ import prompts from "prompts";
7
+
8
+ // src/utils/args.ts
9
+ function parseArgs(argv) {
10
+ const result = { _: [] };
11
+ for (let i = 0; i < argv.length; i++) {
12
+ const arg = argv[i];
13
+ if (!arg) continue;
14
+ if (arg.startsWith("--")) {
15
+ const raw = arg.slice(2);
16
+ if (raw.startsWith("no-")) {
17
+ result[raw] = true;
18
+ continue;
19
+ }
20
+ const eqIdx = raw.indexOf("=");
21
+ if (eqIdx !== -1) {
22
+ const key = raw.slice(0, eqIdx);
23
+ const val = raw.slice(eqIdx + 1);
24
+ result[key] = val;
25
+ continue;
26
+ }
27
+ const next = argv[i + 1];
28
+ if (next && !next.startsWith("-")) {
29
+ result[raw] = next;
30
+ i++;
31
+ } else {
32
+ result[raw] = true;
33
+ }
34
+ } else {
35
+ result._.push(arg);
36
+ }
37
+ }
38
+ return result;
39
+ }
40
+
41
+ // src/utils/validate.ts
42
+ function validateProjectName(name) {
43
+ if (!name || name.trim().length === 0) {
44
+ return "Project name cannot be empty.";
45
+ }
46
+ if (name.length > 214) {
47
+ return "Project name must be 214 characters or fewer.";
48
+ }
49
+ if (name !== name.toLowerCase()) {
50
+ return "Project name must be lowercase.";
51
+ }
52
+ if (/[^a-z0-9@._/-]/.test(name)) {
53
+ return "Project name may only contain lowercase letters, numbers, hyphens, underscores, dots, @ and /.";
54
+ }
55
+ if (/^[._]/.test(name)) {
56
+ return "Project name cannot start with a dot or underscore.";
57
+ }
58
+ return void 0;
59
+ }
60
+
61
+ // src/scaffold.ts
62
+ import { mkdirSync, readdirSync, readFileSync, writeFileSync, statSync } from "fs";
63
+ import { resolve, relative, dirname } from "path";
64
+ import { fileURLToPath } from "url";
65
+ import Handlebars from "handlebars";
66
+ import pc2 from "picocolors";
67
+
68
+ // src/utils/spinner.ts
69
+ import process2 from "process";
70
+ import pc from "picocolors";
71
+ var FRAMES = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
72
+ var INTERVAL = 80;
73
+ function createSpinner(text) {
74
+ if (!process2.stdout.isTTY) {
75
+ process2.stdout.write(` ${text}...
76
+ `);
77
+ return {
78
+ succeed(t) {
79
+ process2.stdout.write(` ${pc.green("\u2714")} ${t ?? text}
80
+ `);
81
+ },
82
+ fail(t) {
83
+ process2.stdout.write(` ${pc.red("\u2716")} ${t ?? text}
84
+ `);
85
+ },
86
+ stop() {
87
+ }
88
+ };
89
+ }
90
+ let frame = 0;
91
+ const timer = setInterval(() => {
92
+ process2.stdout.write(`\r ${pc.cyan(FRAMES[frame % FRAMES.length] ?? "\u280B")} ${text}`);
93
+ frame++;
94
+ }, INTERVAL);
95
+ const clear = () => {
96
+ clearInterval(timer);
97
+ process2.stdout.write("\r\x1B[K");
98
+ };
99
+ return {
100
+ succeed(t) {
101
+ clear();
102
+ process2.stdout.write(` ${pc.green("\u2714")} ${t ?? text}
103
+ `);
104
+ },
105
+ fail(t) {
106
+ clear();
107
+ process2.stdout.write(` ${pc.red("\u2716")} ${t ?? text}
108
+ `);
109
+ },
110
+ stop: clear
111
+ };
112
+ }
113
+
114
+ // src/scaffold.ts
115
+ var TEMPLATES_DIR = resolve(
116
+ dirname(fileURLToPath(import.meta.url)),
117
+ "templates"
118
+ );
119
+ var RENAME_MAP = {
120
+ "_gitignore": ".gitignore",
121
+ "_env.example": ".env.example",
122
+ "_npmrc": ".npmrc"
123
+ };
124
+ async function scaffold(config) {
125
+ const spinner = createSpinner(`Scaffolding ${pc2.bold(config.projectName)}`);
126
+ try {
127
+ const templateDir = resolve(TEMPLATES_DIR, config.template);
128
+ const ctx = buildTemplateContext(config);
129
+ copyDir(templateDir, config.targetDir, ctx);
130
+ spinner.succeed(`Scaffolded ${pc2.bold(config.projectName)}`);
131
+ } catch (err) {
132
+ spinner.fail("Scaffold failed");
133
+ throw err;
134
+ }
135
+ }
136
+ function buildTemplateContext(config) {
137
+ return {
138
+ projectName: config.projectName,
139
+ packageManager: config.packageManager,
140
+ isCloudflare: config.template === "cloudflare",
141
+ isDefault: config.template === "default",
142
+ year: (/* @__PURE__ */ new Date()).getFullYear()
143
+ };
144
+ }
145
+ function copyDir(src, dest, ctx) {
146
+ mkdirSync(dest, { recursive: true });
147
+ for (const entry of readdirSync(src)) {
148
+ const srcPath = resolve(src, entry);
149
+ const destName = RENAME_MAP[entry] ?? entry;
150
+ const destPath = resolve(dest, destName);
151
+ const stat = statSync(srcPath);
152
+ if (stat.isDirectory()) {
153
+ copyDir(srcPath, destPath, ctx);
154
+ } else {
155
+ copyFile(srcPath, destPath, ctx);
156
+ }
157
+ }
158
+ }
159
+ function copyFile(src, dest, ctx) {
160
+ const raw = readFileSync(src, "utf-8");
161
+ if (src.endsWith(".hbs")) {
162
+ const template = Handlebars.compile(raw, { noEscape: true });
163
+ const rendered = template(ctx);
164
+ const destWithoutHbs = dest.endsWith(".hbs") ? dest.slice(0, -4) : dest;
165
+ mkdirSync(dirname(destWithoutHbs), { recursive: true });
166
+ writeFileSync(destWithoutHbs, rendered, "utf-8");
167
+ } else {
168
+ mkdirSync(dirname(dest), { recursive: true });
169
+ writeFileSync(dest, raw);
170
+ }
171
+ if (process.env["DEBUG"]) {
172
+ console.log(pc2.dim(` + ${relative(process.cwd(), dest)}`));
173
+ }
174
+ }
175
+
176
+ // src/utils/detect-pm.ts
177
+ function detectPackageManager() {
178
+ const agent = process.env["npm_config_user_agent"] ?? "";
179
+ if (agent.startsWith("pnpm")) return "pnpm";
180
+ if (agent.startsWith("yarn")) return "yarn";
181
+ if (agent.startsWith("bun")) return "bun";
182
+ return "npm";
183
+ }
184
+
185
+ // src/install.ts
186
+ import { execa } from "execa";
187
+ import pc3 from "picocolors";
188
+ var INSTALL_CMDS = {
189
+ pnpm: ["pnpm", ["install"]],
190
+ npm: ["npm", ["install"]],
191
+ yarn: ["yarn", ["install"]],
192
+ bun: ["bun", ["install"]]
193
+ };
194
+ async function installDependencies(config) {
195
+ const { packageManager, targetDir } = config;
196
+ const spinner = createSpinner(`Installing dependencies with ${pc3.bold(packageManager)}`);
197
+ const entry = INSTALL_CMDS[packageManager];
198
+ if (!entry) {
199
+ spinner.fail(`Unknown package manager: ${packageManager}`);
200
+ return;
201
+ }
202
+ const [cmd, args] = entry;
203
+ try {
204
+ await execa(cmd, args, {
205
+ cwd: targetDir,
206
+ stdio: process.env["DEBUG"] ? "inherit" : "pipe"
207
+ });
208
+ spinner.succeed(`Dependencies installed`);
209
+ } catch (err) {
210
+ spinner.fail("Dependency install failed");
211
+ console.error(
212
+ pc3.dim(` Run ${pc3.cyan(`${packageManager} install`)} manually after fixing the error.`)
213
+ );
214
+ if (process.env["DEBUG"]) throw err;
215
+ }
216
+ }
217
+
218
+ // src/git.ts
219
+ import { execa as execa2 } from "execa";
220
+ import pc4 from "picocolors";
221
+ async function initGit(targetDir) {
222
+ const spinner = createSpinner("Initialising git repository");
223
+ try {
224
+ await execa2("git", ["init"], { cwd: targetDir, stdio: "pipe" });
225
+ await execa2("git", ["add", "-A"], { cwd: targetDir, stdio: "pipe" });
226
+ await execa2(
227
+ "git",
228
+ ["commit", "--allow-empty", "-m", "chore: initialise LumiBase project"],
229
+ { cwd: targetDir, stdio: "pipe" }
230
+ );
231
+ spinner.succeed("Git repository initialised");
232
+ } catch {
233
+ spinner.fail("Git init skipped (git may not be installed)");
234
+ console.error(pc4.dim(" Run `git init` manually when ready."));
235
+ }
236
+ }
237
+
238
+ // src/utils/print.ts
239
+ import pc5 from "picocolors";
240
+ function printNextSteps(config) {
241
+ const { projectName, packageManager, installDeps, template } = config;
242
+ const isCurrentDir = projectName === ".";
243
+ const devCmd = template === "cloudflare" ? `${packageManager} run dev` : "docker compose up -d && pnpm dev";
244
+ console.log();
245
+ console.log(pc5.bold(pc5.green("\u2714 Project created!")));
246
+ console.log();
247
+ console.log(pc5.bold(" Next steps:"));
248
+ console.log();
249
+ let step = 1;
250
+ if (!isCurrentDir) {
251
+ console.log(` ${step++}. ${pc5.cyan(`cd ${projectName}`)}`);
252
+ }
253
+ console.log(` ${step++}. ${pc5.cyan("cp .env.example .env")} ${pc5.dim("\u2190 fill in your secrets")}`);
254
+ if (!installDeps) {
255
+ console.log(` ${step++}. ${pc5.cyan(`${packageManager} install`)}`);
256
+ }
257
+ if (template === "default") {
258
+ console.log(` ${step++}. ${pc5.cyan("docker compose up -d")} ${pc5.dim("\u2190 starts Postgres + Redis")}`);
259
+ console.log(` ${step++}. ${pc5.cyan(`${packageManager} run db:migrate`)}`);
260
+ }
261
+ console.log(` ${step++}. ${pc5.cyan(devCmd)}`);
262
+ console.log();
263
+ console.log(
264
+ pc5.dim(" API ") + pc5.underline("http://localhost:8787")
265
+ );
266
+ console.log();
267
+ console.log(pc5.dim(" Docs \u2192 https://lumibase.dev/docs"));
268
+ console.log();
269
+ }
270
+
271
+ // src/index.ts
272
+ async function main() {
273
+ const argv = parseArgs(process3.argv.slice(2));
274
+ console.log();
275
+ console.log(
276
+ pc6.bold(pc6.cyan(" LumiBase")) + pc6.dim(" \u2014 Edge-native Headless CMS")
277
+ );
278
+ console.log();
279
+ let projectName = argv._[0] ?? "";
280
+ let targetDir = "";
281
+ if (!projectName) {
282
+ const res = await prompts(
283
+ {
284
+ type: "text",
285
+ name: "projectName",
286
+ message: "Project name:",
287
+ initial: "my-lumibase",
288
+ validate: (v) => validateProjectName(v) ?? true
289
+ },
290
+ { onCancel }
291
+ );
292
+ projectName = res.projectName;
293
+ }
294
+ const nameError = validateProjectName(projectName);
295
+ if (nameError) {
296
+ console.error(pc6.red(`\u2716 ${nameError}`));
297
+ process3.exit(1);
298
+ }
299
+ targetDir = resolve2(process3.cwd(), projectName);
300
+ if (existsSync(targetDir) && readdirSync2(targetDir).length > 0) {
301
+ const { overwrite } = await prompts(
302
+ {
303
+ type: "confirm",
304
+ name: "overwrite",
305
+ message: `Directory ${pc6.yellow(projectName)} is not empty. Continue and overwrite?`,
306
+ initial: false
307
+ },
308
+ { onCancel }
309
+ );
310
+ if (!overwrite) {
311
+ console.log(pc6.dim("Cancelled."));
312
+ process3.exit(0);
313
+ }
314
+ }
315
+ const template = argv.template ?? (await prompts(
316
+ {
317
+ type: "select",
318
+ name: "template",
319
+ message: "Deployment target:",
320
+ choices: [
321
+ {
322
+ title: `${pc6.bold("Docker")} ${pc6.dim("Node.js + PostgreSQL (recommended)")}`,
323
+ value: "default"
324
+ },
325
+ {
326
+ title: `${pc6.bold("Cloudflare Workers")} ${pc6.dim("Edge + D1")}`,
327
+ value: "cloudflare"
328
+ }
329
+ ],
330
+ initial: 0
331
+ },
332
+ { onCancel }
333
+ )).template;
334
+ const detectedPm = detectPackageManager();
335
+ const packageManager = argv.pm ?? (await prompts(
336
+ {
337
+ type: "select",
338
+ name: "packageManager",
339
+ message: "Package manager:",
340
+ choices: [
341
+ ["pnpm", "Recommended"],
342
+ ["npm", ""],
343
+ ["yarn", ""],
344
+ ["bun", "Fast"]
345
+ ].map(([value, hint]) => ({
346
+ title: value === detectedPm ? `${value} ${pc6.dim(`(detected${hint ? ", " + hint : ""})`)}` : hint ? `${value} ${pc6.dim(`(${hint})`)}` : value,
347
+ value
348
+ })),
349
+ initial: ["pnpm", "npm", "yarn", "bun"].indexOf(
350
+ detectedPm
351
+ )
352
+ },
353
+ { onCancel }
354
+ )).packageManager;
355
+ const installDeps = argv["no-install"] === true ? false : argv.install === true ? true : (await prompts(
356
+ {
357
+ type: "confirm",
358
+ name: "installDeps",
359
+ message: "Install dependencies?",
360
+ initial: true
361
+ },
362
+ { onCancel }
363
+ )).installDeps;
364
+ const initializeGit = argv["no-git"] === true ? false : argv.git === true ? true : (await prompts(
365
+ {
366
+ type: "confirm",
367
+ name: "initializeGit",
368
+ message: "Initialize a git repository?",
369
+ initial: true
370
+ },
371
+ { onCancel }
372
+ )).initializeGit;
373
+ const config = {
374
+ projectName: basename2(targetDir),
375
+ targetDir,
376
+ template,
377
+ packageManager,
378
+ installDeps,
379
+ initializeGit
380
+ };
381
+ console.log();
382
+ await scaffold(config);
383
+ if (initializeGit) {
384
+ await initGit(targetDir);
385
+ }
386
+ if (installDeps) {
387
+ await installDependencies(config);
388
+ }
389
+ printNextSteps(config);
390
+ }
391
+ function onCancel() {
392
+ console.log(pc6.dim("\nCancelled."));
393
+ process3.exit(0);
394
+ }
395
+ main().catch((err) => {
396
+ if (process3.env["DEBUG"]) {
397
+ console.error(err);
398
+ } else {
399
+ console.error(
400
+ pc6.red("\n\u2716 Something went wrong:"),
401
+ err instanceof Error ? err.message : String(err)
402
+ );
403
+ console.error(pc6.dim(" Run with DEBUG=1 for full stack trace."));
404
+ }
405
+ process3.exit(1);
406
+ });
@@ -0,0 +1,4 @@
1
+ # Wrangler dev secrets (not committed — use `wrangler secret put` for production)
2
+ JWT_SECRET=change-me-in-production
3
+ ADMIN_EMAIL=admin@example.com
4
+ ADMIN_PASSWORD=change-me
@@ -0,0 +1,7 @@
1
+ node_modules
2
+ dist
3
+ .env
4
+ .dev.vars
5
+ *.log
6
+ .DS_Store
7
+ .wrangler
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "wrangler dev",
8
+ "deploy": "wrangler deploy",
9
+ "db:migrate": "wrangler d1 migrations apply lumibase-db",
10
+ "db:migrate:remote": "wrangler d1 migrations apply lumibase-db --remote",
11
+ "typecheck": "tsc --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "hono": "^4.0.0",
15
+ "drizzle-orm": "^0.44.0",
16
+ "nanoid": "^5.1.5",
17
+ "uuidv7": "^1.0.2",
18
+ "zod": "^3.25.51"
19
+ },
20
+ "devDependencies": {
21
+ "@cloudflare/workers-types": "^4.0.0",
22
+ "@types/node": "^22.0.0",
23
+ "drizzle-kit": "^0.31.0",
24
+ "typescript": "^5.6.2",
25
+ "wrangler": "^4.0.0"
26
+ }
27
+ }
@@ -0,0 +1,16 @@
1
+ import { Hono } from 'hono';
2
+ import { logger } from 'hono/logger';
3
+
4
+ export interface Env {
5
+ DB: D1Database;
6
+ JWT_SECRET: string;
7
+ ADMIN_EMAIL: string;
8
+ }
9
+
10
+ const app = new Hono<{ Bindings: Env }>();
11
+
12
+ app.use('*', logger());
13
+
14
+ app.get('/', (c) => c.json({ name: '{{projectName}}', status: 'ok' }));
15
+
16
+ export default app;
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "resolveJsonModule": true,
10
+ "types": ["@cloudflare/workers-types"]
11
+ },
12
+ "include": ["src", "worker-configuration.d.ts"]
13
+ }
@@ -0,0 +1,13 @@
1
+ name = "{{projectName}}"
2
+ main = "src/index.ts"
3
+ compatibility_date = "2025-01-01"
4
+ compatibility_flags = ["nodejs_compat"]
5
+
6
+ [[d1_databases]]
7
+ binding = "DB"
8
+ database_name = "lumibase-db"
9
+ database_id = "REPLACE_WITH_YOUR_D1_ID"
10
+
11
+ [vars]
12
+ JWT_SECRET = "change-me-in-production"
13
+ ADMIN_EMAIL = "admin@example.com"
@@ -0,0 +1,14 @@
1
+ # Database
2
+ DATABASE_URL=postgresql://lumibase:lumibase@localhost:5432/lumibase
3
+
4
+ # Redis (optional — for queue/cache)
5
+ REDIS_URL=redis://localhost:6379
6
+
7
+ # Auth
8
+ JWT_SECRET=change-me-in-production
9
+ ADMIN_EMAIL=admin@example.com
10
+ ADMIN_PASSWORD=change-me
11
+
12
+ # App
13
+ PORT=8787
14
+ NODE_ENV=development
@@ -0,0 +1,5 @@
1
+ node_modules
2
+ dist
3
+ .env
4
+ *.log
5
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ services:
2
+ postgres:
3
+ image: postgres:16-alpine
4
+ restart: unless-stopped
5
+ environment:
6
+ POSTGRES_USER: lumibase
7
+ POSTGRES_PASSWORD: lumibase
8
+ POSTGRES_DB: lumibase
9
+ ports:
10
+ - "5432:5432"
11
+ volumes:
12
+ - postgres_data:/var/lib/postgresql/data
13
+
14
+ redis:
15
+ image: redis:7-alpine
16
+ restart: unless-stopped
17
+ ports:
18
+ - "6379:6379"
19
+
20
+ volumes:
21
+ postgres_data:
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from 'drizzle-kit';
2
+
3
+ const databaseUrl = process.env['DATABASE_URL'];
4
+
5
+ if (!databaseUrl) {
6
+ throw new Error('DATABASE_URL is required to run drizzle-kit commands.');
7
+ }
8
+
9
+ export default defineConfig({
10
+ schema: './src/db/schema.ts',
11
+ out: './drizzle',
12
+ dialect: 'postgresql',
13
+ dbCredentials: {
14
+ url: databaseUrl,
15
+ },
16
+ strict: true,
17
+ verbose: true,
18
+ });
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx watch --env-file=.env src/server.ts",
8
+ "build": "tsc",
9
+ "start": "node --env-file=.env dist/server.js",
10
+ "db:generate": "drizzle-kit generate",
11
+ "db:migrate": "tsx --env-file=.env src/db/migrate.ts",
12
+ "db:studio": "drizzle-kit studio",
13
+ "typecheck": "tsc --noEmit"
14
+ },
15
+ "dependencies": {
16
+ "@hono/node-server": "^1.12.0",
17
+ "hono": "^4.0.0",
18
+ "drizzle-orm": "^0.44.0",
19
+ "postgres": "^3.4.7",
20
+ "nanoid": "^5.1.5",
21
+ "uuidv7": "^1.0.2",
22
+ "zod": "^3.25.51"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^22.0.0",
26
+ "drizzle-kit": "^0.31.0",
27
+ "tsx": "^4.19.4",
28
+ "typescript": "^5.6.2"
29
+ }
30
+ }
@@ -0,0 +1,14 @@
1
+ import { drizzle } from 'drizzle-orm/postgres-js';
2
+ import postgres from 'postgres';
3
+ import * as schema from './schema.js';
4
+
5
+ const databaseUrl = process.env['DATABASE_URL'];
6
+
7
+ if (!databaseUrl) {
8
+ throw new Error('DATABASE_URL is required. Copy .env.example to .env first.');
9
+ }
10
+
11
+ const queryClient = postgres(databaseUrl);
12
+
13
+ export const db = drizzle(queryClient, { schema });
14
+ export { schema };
@@ -0,0 +1,24 @@
1
+ import { drizzle } from 'drizzle-orm/postgres-js';
2
+ import { migrate } from 'drizzle-orm/postgres-js/migrator';
3
+ import postgres from 'postgres';
4
+
5
+ const databaseUrl = process.env['DATABASE_URL'];
6
+
7
+ if (!databaseUrl) {
8
+ throw new Error('DATABASE_URL is required. Copy .env.example to .env first.');
9
+ }
10
+
11
+ // `max: 1` — migrations must run on a single connection.
12
+ const migrationClient = postgres(databaseUrl, { max: 1 });
13
+
14
+ async function main() {
15
+ console.log('Running migrations...');
16
+ await migrate(drizzle(migrationClient), { migrationsFolder: './drizzle' });
17
+ console.log('Migrations complete.');
18
+ await migrationClient.end();
19
+ }
20
+
21
+ main().catch((err) => {
22
+ console.error('Migration failed:', err);
23
+ process.exit(1);
24
+ });
@@ -0,0 +1,34 @@
1
+ import { index, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
2
+ import { nanoid } from 'nanoid';
3
+
4
+ /**
5
+ * LumiBase convention helpers.
6
+ * - IDs: nanoid() for domain tables (never serial/auto-increment).
7
+ * - Multi-tenancy: every domain table carries a `site_id`.
8
+ */
9
+ const id = () =>
10
+ text('id')
11
+ .$defaultFn(() => nanoid())
12
+ .primaryKey();
13
+ const createdAt = () => timestamp('created_at').defaultNow().notNull();
14
+ const updatedAt = () => timestamp('updated_at').defaultNow().notNull();
15
+
16
+ export const posts = pgTable(
17
+ 'posts',
18
+ {
19
+ id: id(),
20
+ siteId: text('site_id').notNull(),
21
+ title: text('title').notNull(),
22
+ slug: text('slug').notNull(),
23
+ body: text('body').notNull().default(''),
24
+ status: text('status').notNull().default('draft'),
25
+ createdAt: createdAt(),
26
+ updatedAt: updatedAt(),
27
+ },
28
+ (t) => ({
29
+ siteSlugIdx: index('posts_site_slug_idx').on(t.siteId, t.slug),
30
+ }),
31
+ );
32
+
33
+ export type Post = typeof posts.$inferSelect;
34
+ export type NewPost = typeof posts.$inferInsert;
@@ -0,0 +1,48 @@
1
+ import { serve } from '@hono/node-server';
2
+ import { Hono } from 'hono';
3
+ import { logger } from 'hono/logger';
4
+ import { eq } from 'drizzle-orm';
5
+ import { z } from 'zod';
6
+ import { db } from './db/client.js';
7
+ import { posts } from './db/schema.js';
8
+
9
+ const app = new Hono();
10
+
11
+ app.use('*', logger());
12
+
13
+ app.get('/', (c) => c.json({ name: '{{projectName}}', status: 'ok' }));
14
+
15
+ // Demo CMS resource — every query is scoped by site_id (multi-tenant rule).
16
+ const DEMO_SITE_ID = 'default';
17
+
18
+ app.get('/posts', async (c) => {
19
+ const data = await db
20
+ .select()
21
+ .from(posts)
22
+ .where(eq(posts.siteId, DEMO_SITE_ID));
23
+ return c.json({ data });
24
+ });
25
+
26
+ const createPost = z.object({
27
+ title: z.string().min(1),
28
+ slug: z.string().min(1),
29
+ body: z.string().optional(),
30
+ });
31
+
32
+ app.post('/posts', async (c) => {
33
+ const parsed = createPost.safeParse(await c.req.json());
34
+ if (!parsed.success) {
35
+ return c.json({ errors: parsed.error.issues }, 400);
36
+ }
37
+ const [created] = await db
38
+ .insert(posts)
39
+ .values({ ...parsed.data, siteId: DEMO_SITE_ID })
40
+ .returning();
41
+ return c.json({ data: created }, 201);
42
+ });
43
+
44
+ const port = Number(process.env['PORT'] ?? 8787);
45
+
46
+ serve({ fetch: app.fetch, port }, () => {
47
+ console.log(`🚀 LumiBase listening on http://localhost:${port}`);
48
+ });
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist",
10
+ "rootDir": "src",
11
+ "resolveJsonModule": true
12
+ },
13
+ "include": ["src"]
14
+ }
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "create-lumibase",
3
+ "version": "0.6.0",
4
+ "description": "Scaffold a new LumiBase project — Edge-native Headless CMS",
5
+ "type": "module",
6
+ "bin": {
7
+ "create-lumibase": "./bin/create-lumibase.js"
8
+ },
9
+ "main": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "scripts": {
15
+ "build": "tsup src/index.ts --format esm --dts --clean && node scripts/postbuild.mjs",
16
+ "dev": "tsup src/index.ts --format esm --watch",
17
+ "test": "vitest --run",
18
+ "typecheck": "tsc --noEmit",
19
+ "clean": "rm -rf .turbo dist"
20
+ },
21
+ "dependencies": {
22
+ "handlebars": "^4.7.8",
23
+ "picocolors": "^1.1.1",
24
+ "prompts": "^2.4.2",
25
+ "execa": "^9.5.2"
26
+ },
27
+ "devDependencies": {
28
+ "@types/node": "^22.0.0",
29
+ "@types/prompts": "^2.4.9",
30
+ "tsup": "^8.5.1",
31
+ "typescript": "^5.6.2",
32
+ "vitest": "^3.2.6"
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "bin",
37
+ "templates"
38
+ ],
39
+ "engines": {
40
+ "node": ">=20"
41
+ },
42
+ "keywords": [
43
+ "lumibase",
44
+ "cms",
45
+ "headless-cms",
46
+ "scaffold",
47
+ "create"
48
+ ],
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "https://github.com/lumibase/lumibase.git",
52
+ "directory": "packages/create-lumibase"
53
+ }
54
+ }
@@ -0,0 +1,4 @@
1
+ # Wrangler dev secrets (not committed — use `wrangler secret put` for production)
2
+ JWT_SECRET=change-me-in-production
3
+ ADMIN_EMAIL=admin@example.com
4
+ ADMIN_PASSWORD=change-me
@@ -0,0 +1,7 @@
1
+ node_modules
2
+ dist
3
+ .env
4
+ .dev.vars
5
+ *.log
6
+ .DS_Store
7
+ .wrangler
@@ -0,0 +1,27 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "wrangler dev",
8
+ "deploy": "wrangler deploy",
9
+ "db:migrate": "wrangler d1 migrations apply lumibase-db",
10
+ "db:migrate:remote": "wrangler d1 migrations apply lumibase-db --remote",
11
+ "typecheck": "tsc --noEmit"
12
+ },
13
+ "dependencies": {
14
+ "hono": "^4.0.0",
15
+ "drizzle-orm": "^0.44.0",
16
+ "nanoid": "^5.1.5",
17
+ "uuidv7": "^1.0.2",
18
+ "zod": "^3.25.51"
19
+ },
20
+ "devDependencies": {
21
+ "@cloudflare/workers-types": "^4.0.0",
22
+ "@types/node": "^22.0.0",
23
+ "drizzle-kit": "^0.31.0",
24
+ "typescript": "^5.6.2",
25
+ "wrangler": "^4.0.0"
26
+ }
27
+ }
@@ -0,0 +1,16 @@
1
+ import { Hono } from 'hono';
2
+ import { logger } from 'hono/logger';
3
+
4
+ export interface Env {
5
+ DB: D1Database;
6
+ JWT_SECRET: string;
7
+ ADMIN_EMAIL: string;
8
+ }
9
+
10
+ const app = new Hono<{ Bindings: Env }>();
11
+
12
+ app.use('*', logger());
13
+
14
+ app.get('/', (c) => c.json({ name: '{{projectName}}', status: 'ok' }));
15
+
16
+ export default app;
@@ -0,0 +1,13 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "resolveJsonModule": true,
10
+ "types": ["@cloudflare/workers-types"]
11
+ },
12
+ "include": ["src", "worker-configuration.d.ts"]
13
+ }
@@ -0,0 +1,13 @@
1
+ name = "{{projectName}}"
2
+ main = "src/index.ts"
3
+ compatibility_date = "2025-01-01"
4
+ compatibility_flags = ["nodejs_compat"]
5
+
6
+ [[d1_databases]]
7
+ binding = "DB"
8
+ database_name = "lumibase-db"
9
+ database_id = "REPLACE_WITH_YOUR_D1_ID"
10
+
11
+ [vars]
12
+ JWT_SECRET = "change-me-in-production"
13
+ ADMIN_EMAIL = "admin@example.com"
@@ -0,0 +1,14 @@
1
+ # Database
2
+ DATABASE_URL=postgresql://lumibase:lumibase@localhost:5432/lumibase
3
+
4
+ # Redis (optional — for queue/cache)
5
+ REDIS_URL=redis://localhost:6379
6
+
7
+ # Auth
8
+ JWT_SECRET=change-me-in-production
9
+ ADMIN_EMAIL=admin@example.com
10
+ ADMIN_PASSWORD=change-me
11
+
12
+ # App
13
+ PORT=8787
14
+ NODE_ENV=development
@@ -0,0 +1,5 @@
1
+ node_modules
2
+ dist
3
+ .env
4
+ *.log
5
+ .DS_Store
@@ -0,0 +1,21 @@
1
+ services:
2
+ postgres:
3
+ image: postgres:16-alpine
4
+ restart: unless-stopped
5
+ environment:
6
+ POSTGRES_USER: lumibase
7
+ POSTGRES_PASSWORD: lumibase
8
+ POSTGRES_DB: lumibase
9
+ ports:
10
+ - "5432:5432"
11
+ volumes:
12
+ - postgres_data:/var/lib/postgresql/data
13
+
14
+ redis:
15
+ image: redis:7-alpine
16
+ restart: unless-stopped
17
+ ports:
18
+ - "6379:6379"
19
+
20
+ volumes:
21
+ postgres_data:
@@ -0,0 +1,18 @@
1
+ import { defineConfig } from 'drizzle-kit';
2
+
3
+ const databaseUrl = process.env['DATABASE_URL'];
4
+
5
+ if (!databaseUrl) {
6
+ throw new Error('DATABASE_URL is required to run drizzle-kit commands.');
7
+ }
8
+
9
+ export default defineConfig({
10
+ schema: './src/db/schema.ts',
11
+ out: './drizzle',
12
+ dialect: 'postgresql',
13
+ dbCredentials: {
14
+ url: databaseUrl,
15
+ },
16
+ strict: true,
17
+ verbose: true,
18
+ });
@@ -0,0 +1,30 @@
1
+ {
2
+ "name": "{{projectName}}",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "type": "module",
6
+ "scripts": {
7
+ "dev": "tsx watch --env-file=.env src/server.ts",
8
+ "build": "tsc",
9
+ "start": "node --env-file=.env dist/server.js",
10
+ "db:generate": "drizzle-kit generate",
11
+ "db:migrate": "tsx --env-file=.env src/db/migrate.ts",
12
+ "db:studio": "drizzle-kit studio",
13
+ "typecheck": "tsc --noEmit"
14
+ },
15
+ "dependencies": {
16
+ "@hono/node-server": "^1.12.0",
17
+ "hono": "^4.0.0",
18
+ "drizzle-orm": "^0.44.0",
19
+ "postgres": "^3.4.7",
20
+ "nanoid": "^5.1.5",
21
+ "uuidv7": "^1.0.2",
22
+ "zod": "^3.25.51"
23
+ },
24
+ "devDependencies": {
25
+ "@types/node": "^22.0.0",
26
+ "drizzle-kit": "^0.31.0",
27
+ "tsx": "^4.19.4",
28
+ "typescript": "^5.6.2"
29
+ }
30
+ }
@@ -0,0 +1,14 @@
1
+ import { drizzle } from 'drizzle-orm/postgres-js';
2
+ import postgres from 'postgres';
3
+ import * as schema from './schema.js';
4
+
5
+ const databaseUrl = process.env['DATABASE_URL'];
6
+
7
+ if (!databaseUrl) {
8
+ throw new Error('DATABASE_URL is required. Copy .env.example to .env first.');
9
+ }
10
+
11
+ const queryClient = postgres(databaseUrl);
12
+
13
+ export const db = drizzle(queryClient, { schema });
14
+ export { schema };
@@ -0,0 +1,24 @@
1
+ import { drizzle } from 'drizzle-orm/postgres-js';
2
+ import { migrate } from 'drizzle-orm/postgres-js/migrator';
3
+ import postgres from 'postgres';
4
+
5
+ const databaseUrl = process.env['DATABASE_URL'];
6
+
7
+ if (!databaseUrl) {
8
+ throw new Error('DATABASE_URL is required. Copy .env.example to .env first.');
9
+ }
10
+
11
+ // `max: 1` — migrations must run on a single connection.
12
+ const migrationClient = postgres(databaseUrl, { max: 1 });
13
+
14
+ async function main() {
15
+ console.log('Running migrations...');
16
+ await migrate(drizzle(migrationClient), { migrationsFolder: './drizzle' });
17
+ console.log('Migrations complete.');
18
+ await migrationClient.end();
19
+ }
20
+
21
+ main().catch((err) => {
22
+ console.error('Migration failed:', err);
23
+ process.exit(1);
24
+ });
@@ -0,0 +1,34 @@
1
+ import { index, pgTable, text, timestamp } from 'drizzle-orm/pg-core';
2
+ import { nanoid } from 'nanoid';
3
+
4
+ /**
5
+ * LumiBase convention helpers.
6
+ * - IDs: nanoid() for domain tables (never serial/auto-increment).
7
+ * - Multi-tenancy: every domain table carries a `site_id`.
8
+ */
9
+ const id = () =>
10
+ text('id')
11
+ .$defaultFn(() => nanoid())
12
+ .primaryKey();
13
+ const createdAt = () => timestamp('created_at').defaultNow().notNull();
14
+ const updatedAt = () => timestamp('updated_at').defaultNow().notNull();
15
+
16
+ export const posts = pgTable(
17
+ 'posts',
18
+ {
19
+ id: id(),
20
+ siteId: text('site_id').notNull(),
21
+ title: text('title').notNull(),
22
+ slug: text('slug').notNull(),
23
+ body: text('body').notNull().default(''),
24
+ status: text('status').notNull().default('draft'),
25
+ createdAt: createdAt(),
26
+ updatedAt: updatedAt(),
27
+ },
28
+ (t) => ({
29
+ siteSlugIdx: index('posts_site_slug_idx').on(t.siteId, t.slug),
30
+ }),
31
+ );
32
+
33
+ export type Post = typeof posts.$inferSelect;
34
+ export type NewPost = typeof posts.$inferInsert;
@@ -0,0 +1,48 @@
1
+ import { serve } from '@hono/node-server';
2
+ import { Hono } from 'hono';
3
+ import { logger } from 'hono/logger';
4
+ import { eq } from 'drizzle-orm';
5
+ import { z } from 'zod';
6
+ import { db } from './db/client.js';
7
+ import { posts } from './db/schema.js';
8
+
9
+ const app = new Hono();
10
+
11
+ app.use('*', logger());
12
+
13
+ app.get('/', (c) => c.json({ name: '{{projectName}}', status: 'ok' }));
14
+
15
+ // Demo CMS resource — every query is scoped by site_id (multi-tenant rule).
16
+ const DEMO_SITE_ID = 'default';
17
+
18
+ app.get('/posts', async (c) => {
19
+ const data = await db
20
+ .select()
21
+ .from(posts)
22
+ .where(eq(posts.siteId, DEMO_SITE_ID));
23
+ return c.json({ data });
24
+ });
25
+
26
+ const createPost = z.object({
27
+ title: z.string().min(1),
28
+ slug: z.string().min(1),
29
+ body: z.string().optional(),
30
+ });
31
+
32
+ app.post('/posts', async (c) => {
33
+ const parsed = createPost.safeParse(await c.req.json());
34
+ if (!parsed.success) {
35
+ return c.json({ errors: parsed.error.issues }, 400);
36
+ }
37
+ const [created] = await db
38
+ .insert(posts)
39
+ .values({ ...parsed.data, siteId: DEMO_SITE_ID })
40
+ .returning();
41
+ return c.json({ data: created }, 201);
42
+ });
43
+
44
+ const port = Number(process.env['PORT'] ?? 8787);
45
+
46
+ serve({ fetch: app.fetch, port }, () => {
47
+ console.log(`🚀 LumiBase listening on http://localhost:${port}`);
48
+ });
@@ -0,0 +1,14 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "module": "ESNext",
5
+ "moduleResolution": "Bundler",
6
+ "strict": true,
7
+ "esModuleInterop": true,
8
+ "skipLibCheck": true,
9
+ "outDir": "dist",
10
+ "rootDir": "src",
11
+ "resolveJsonModule": true
12
+ },
13
+ "include": ["src"]
14
+ }