@lumerahq/cli 0.19.3-dev.0 → 0.19.3

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 (42) hide show
  1. package/README.md +5 -3
  2. package/dist/chunk-H357NP7T.js +77 -0
  3. package/dist/{chunk-GKI2HQJC.js → chunk-JKXLKK5I.js} +14 -1
  4. package/dist/chunk-P5HFNAVN.js +280 -0
  5. package/dist/{chunk-53NOF33P.js → chunk-SU26C4GL.js} +9 -49
  6. package/dist/{deps-ULTIIDYK.js → deps-EC3VRNN7.js} +4 -2
  7. package/dist/{dev-5JMHMS4U.js → dev-R43VQCZD.js} +9 -2
  8. package/dist/index.js +13 -13
  9. package/dist/{init-37XOMJLU.js → init-TDIQAOG4.js} +29 -9
  10. package/dist/{register-JJUMS4FI.js → register-JFJADKAJ.js} +1 -1
  11. package/dist/{resources-4IUZYKGX.js → resources-OP7EECKZ.js} +20 -92
  12. package/dist/{run-5ZOSPBGO.js → run-EJP5WCQU.js} +1 -1
  13. package/dist/{templates-M3RDNDDY.js → templates-LNUOTNLN.js} +2 -3
  14. package/package.json +1 -1
  15. package/templates/default/.agents/skills/.gitkeep +0 -0
  16. package/templates/default/AGENTS.md +151 -0
  17. package/templates/default/README.md +18 -0
  18. package/templates/default/_gitignore +14 -0
  19. package/templates/default/architecture.md +29 -0
  20. package/templates/default/biome.json +38 -0
  21. package/templates/default/components.json +21 -0
  22. package/templates/default/icon.svg +29 -0
  23. package/templates/default/index.html +16 -0
  24. package/templates/default/package.json +53 -0
  25. package/templates/default/platform/automations/.gitkeep +0 -0
  26. package/templates/default/platform/collections/.gitkeep +0 -0
  27. package/templates/default/platform/hooks/.gitkeep +0 -0
  28. package/templates/default/pyproject.toml +14 -0
  29. package/templates/default/scripts/.gitkeep +0 -0
  30. package/templates/default/src/components/layout.tsx +11 -0
  31. package/templates/default/src/lib/auth.ts +9 -0
  32. package/templates/default/src/lib/queries.ts +17 -0
  33. package/templates/default/src/lib/utils.ts +6 -0
  34. package/templates/default/src/main.tsx +130 -0
  35. package/templates/default/src/routes/__root.tsx +10 -0
  36. package/templates/default/src/routes/index.tsx +87 -0
  37. package/templates/default/src/styles.css +128 -0
  38. package/templates/default/template.json +7 -0
  39. package/templates/default/tsconfig.json +22 -0
  40. package/templates/default/vite.config.ts +28 -0
  41. package/dist/chunk-OQW5E7UT.js +0 -159
  42. package/dist/chunk-XDTWVFPE.js +0 -120
@@ -7,17 +7,17 @@ import {
7
7
  } from "./chunk-BHYDYR75.js";
8
8
  import {
9
9
  createApiClient
10
- } from "./chunk-GKI2HQJC.js";
10
+ } from "./chunk-JKXLKK5I.js";
11
11
  import {
12
12
  getToken,
13
13
  init_auth,
14
14
  setProjectId
15
15
  } from "./chunk-ZH3NVYEQ.js";
16
+ import "./chunk-FJFIWC7G.js";
16
17
  import {
17
18
  listAllTemplates,
18
19
  resolveTemplate
19
- } from "./chunk-OQW5E7UT.js";
20
- import "./chunk-FJFIWC7G.js";
20
+ } from "./chunk-H357NP7T.js";
21
21
  import "./chunk-PNKVD2UK.js";
22
22
 
23
23
  // src/commands/init.ts
@@ -25,7 +25,7 @@ init_auth();
25
25
  import pc from "picocolors";
26
26
  import prompts from "prompts";
27
27
  import { execSync } from "child_process";
28
- import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, rmSync } from "fs";
28
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, rmSync, readlinkSync, symlinkSync } from "fs";
29
29
  import { join, resolve } from "path";
30
30
  function toTitleCase(str) {
31
31
  return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
@@ -40,6 +40,7 @@ function processTemplate(content, replacements) {
40
40
  return result;
41
41
  }
42
42
  var TEMPLATE_EXCLUDE = /* @__PURE__ */ new Set(["template.json"]);
43
+ var TEMPLATE_RENAMES = /* @__PURE__ */ new Map([["_gitignore", ".gitignore"]]);
43
44
  function copyDir(src, dest, replacements, isRoot = true) {
44
45
  if (!existsSync(dest)) {
45
46
  mkdirSync(dest, { recursive: true });
@@ -47,8 +48,16 @@ function copyDir(src, dest, replacements, isRoot = true) {
47
48
  for (const entry of readdirSync(src, { withFileTypes: true })) {
48
49
  if (isRoot && TEMPLATE_EXCLUDE.has(entry.name)) continue;
49
50
  const srcPath = join(src, entry.name);
50
- const destPath = join(dest, entry.name);
51
- if (entry.isDirectory()) {
51
+ const destName = TEMPLATE_RENAMES.get(entry.name) ?? entry.name;
52
+ const destPath = join(dest, destName);
53
+ if (entry.isSymbolicLink()) {
54
+ try {
55
+ symlinkSync(readlinkSync(srcPath), destPath);
56
+ } catch {
57
+ const content = readFileSync(srcPath, "utf-8");
58
+ writeFileSync(destPath, processTemplate(content, replacements));
59
+ }
60
+ } else if (entry.isDirectory()) {
52
61
  copyDir(srcPath, destPath, replacements, false);
53
62
  } else {
54
63
  const content = readFileSync(srcPath, "utf-8");
@@ -57,6 +66,16 @@ function copyDir(src, dest, replacements, isRoot = true) {
57
66
  }
58
67
  }
59
68
  }
69
+ function ensureClaudeInstructionsLink(projectRoot) {
70
+ const agentsMdPath = join(projectRoot, "AGENTS.md");
71
+ const claudeMdPath = join(projectRoot, "CLAUDE.md");
72
+ if (!existsSync(agentsMdPath) || existsSync(claudeMdPath)) return;
73
+ try {
74
+ symlinkSync("AGENTS.md", claudeMdPath);
75
+ } catch {
76
+ writeFileSync(claudeMdPath, readFileSync(agentsMdPath, "utf-8"));
77
+ }
78
+ }
60
79
  function isGitInstalled() {
61
80
  try {
62
81
  execSync("git --version", { stdio: "ignore" });
@@ -193,9 +212,9 @@ ${pc.dim("Options:")}
193
212
 
194
213
  ${pc.dim("Examples:")}
195
214
  lumera init my-app # Interactive mode
196
- lumera init my-app -t invoice-processing # Use a specific template
215
+ lumera init my-app -t default # Use a specific template
197
216
  lumera init my-app -y # Non-interactive (default template)
198
- lumera init my-app -t invoice-processing -y -o # Full non-interactive + open editor
217
+ lumera init my-app -t default -y -o # Full non-interactive + open editor
199
218
  `);
200
219
  }
201
220
  async function init(args) {
@@ -218,7 +237,7 @@ async function init(args) {
218
237
  let templateName = opts.template;
219
238
  if (!templateName && !nonInteractive) {
220
239
  try {
221
- const stop = spinner("Fetching templates...");
240
+ const stop = spinner("Loading templates...");
222
241
  const available = await listAllTemplates();
223
242
  stop();
224
243
  if (available.length > 1) {
@@ -333,6 +352,7 @@ async function init(args) {
333
352
  [sourceTitle, projectTitle]
334
353
  ];
335
354
  copyDir(templateDir, targetDir, replacements);
355
+ ensureClaudeInstructionsLink(targetDir);
336
356
  function listFiles(dir, prefix = "") {
337
357
  for (const entry of readdirSync(dir, { withFileTypes: true })) {
338
358
  const relativePath = prefix + entry.name;
@@ -6,7 +6,7 @@ import {
6
6
  } from "./chunk-BHYDYR75.js";
7
7
  import {
8
8
  createApiClient
9
- } from "./chunk-GKI2HQJC.js";
9
+ } from "./chunk-JKXLKK5I.js";
10
10
  import {
11
11
  findProjectRoot,
12
12
  getAppName,
@@ -1,15 +1,16 @@
1
- import {
2
- syncDeps
3
- } from "./chunk-XDTWVFPE.js";
4
1
  import {
5
2
  deploy
6
- } from "./chunk-53NOF33P.js";
3
+ } from "./chunk-SU26C4GL.js";
4
+ import {
5
+ projectResourceDepsEnabled,
6
+ syncDeps
7
+ } from "./chunk-P5HFNAVN.js";
7
8
  import {
8
9
  loadEnv
9
10
  } from "./chunk-2CR762KB.js";
10
11
  import {
11
12
  createApiClient
12
- } from "./chunk-GKI2HQJC.js";
13
+ } from "./chunk-JKXLKK5I.js";
13
14
  import {
14
15
  findProjectRoot,
15
16
  getApiUrl,
@@ -148,82 +149,13 @@ function safePrintLint(warnings, projectRoot) {
148
149
  if (process.env.LUMERA_DEBUG) console.error("[lint] print failed:", err);
149
150
  }
150
151
  }
151
- var PACKAGE_MANAGERS = ["bun", "pnpm", "yarn", "npm"];
152
- var PACKAGE_MANAGER_VALUE_FLAGS = /* @__PURE__ */ new Set(["--package-manager"]);
153
- function isPackageManager(value) {
154
- return PACKAGE_MANAGERS.includes(value);
155
- }
156
- function commandAvailable(command) {
157
- try {
158
- execFileSync(command, ["--version"], { stdio: "ignore" });
159
- return true;
160
- } catch {
161
- return false;
162
- }
163
- }
164
- function parsePackageManagerSpecifier(value) {
165
- const raw = (value || "").trim();
166
- if (!raw) return void 0;
167
- const name = raw.split("@")[0];
168
- return isPackageManager(name) ? name : void 0;
169
- }
170
- function packageManagerFromPackageJson(projectRoot) {
171
- try {
172
- const pkg = JSON.parse(readFileSync(join(projectRoot, "package.json"), "utf-8"));
173
- return parsePackageManagerSpecifier(pkg.packageManager);
174
- } catch {
175
- return void 0;
176
- }
177
- }
178
- function getFlagValue(args, name) {
179
- const long = `--${name}`;
180
- for (let i = 0; i < args.length; i++) {
181
- const arg = args[i];
182
- if (arg === long) {
183
- const next = args[i + 1];
184
- return next && !next.startsWith("-") ? next : void 0;
185
- }
186
- if (arg.startsWith(`${long}=`)) {
187
- return arg.slice(long.length + 1);
188
- }
189
- }
190
- return void 0;
191
- }
192
- function getPositionalArgs(args) {
193
- const out = [];
194
- for (let i = 0; i < args.length; i++) {
195
- const arg = args[i];
196
- if (PACKAGE_MANAGER_VALUE_FLAGS.has(arg)) {
197
- i++;
198
- continue;
152
+ function detectPackageManager() {
153
+ for (const pm of ["bun", "pnpm", "yarn", "npm"]) {
154
+ try {
155
+ execFileSync(pm, ["--version"], { stdio: "ignore" });
156
+ return pm;
157
+ } catch {
199
158
  }
200
- if (arg.startsWith("-")) continue;
201
- out.push(arg);
202
- }
203
- return out;
204
- }
205
- function detectPackageManager(projectRoot, override) {
206
- const requested = parsePackageManagerSpecifier(override || process.env.LUMERA_PACKAGE_MANAGER);
207
- if (requested) {
208
- if (!commandAvailable(requested)) {
209
- throw new Error(`Package manager '${requested}' was requested but is not available on PATH`);
210
- }
211
- return requested;
212
- }
213
- const declared = packageManagerFromPackageJson(projectRoot);
214
- if (declared && commandAvailable(declared)) return declared;
215
- const lockfileManagers = [
216
- { pm: "pnpm", file: "pnpm-lock.yaml" },
217
- { pm: "bun", file: "bun.lockb" },
218
- { pm: "bun", file: "bun.lock" },
219
- { pm: "yarn", file: "yarn.lock" },
220
- { pm: "npm", file: "package-lock.json" }
221
- ];
222
- for (const { pm, file } of lockfileManagers) {
223
- if (existsSync(join(projectRoot, file)) && commandAvailable(pm)) return pm;
224
- }
225
- for (const pm of ["pnpm", "bun", "yarn", "npm"]) {
226
- if (commandAvailable(pm)) return pm;
227
159
  }
228
160
  return "npm";
229
161
  }
@@ -503,9 +435,6 @@ ${pc2.dim("Resources:")}
503
435
  ${pc2.dim("Options:")}
504
436
  --yes, -y Skip confirmation prompt (for CI/CD)
505
437
  --skip-build Skip build step when applying app
506
- --no-app Apply resources only; do not build/deploy the app
507
- --resources-only Alias for --no-app
508
- --package-manager Package manager for app builds (pnpm, bun, yarn, npm)
509
438
 
510
439
  ${pc2.dim("Examples:")}
511
440
  lumera apply # Apply everything (shows plan, asks to confirm)
@@ -514,8 +443,6 @@ ${pc2.dim("Examples:")}
514
443
  lumera apply agents -y # Apply agents without confirmation
515
444
  lumera apply app # Deploy frontend
516
445
  lumera apply app --skip-build # Deploy without rebuilding
517
- lumera apply --no-app -y # Apply resources without app build/deploy
518
- lumera apply app --package-manager pnpm
519
446
  `);
520
447
  }
521
448
  function showPullHelp() {
@@ -1464,7 +1391,7 @@ async function applyApp(args) {
1464
1391
  if (!skipBuild) {
1465
1392
  console.log(pc2.dim(" Building..."));
1466
1393
  try {
1467
- const pm = detectPackageManager(projectRoot, getFlagValue(args, "package-manager"));
1394
+ const pm = detectPackageManager();
1468
1395
  execSync(`${pm} run build`, { cwd: projectRoot, stdio: "inherit" });
1469
1396
  } catch {
1470
1397
  throw new Error("Build failed");
@@ -2568,13 +2495,9 @@ async function apply(args) {
2568
2495
  const appName = getAppName(projectRoot);
2569
2496
  const api = createApiClient(void 0, void 0, appName);
2570
2497
  const projectId = getProjectId(projectRoot);
2571
- const positionalArgs = getPositionalArgs(args);
2498
+ const positionalArgs = args.filter((a) => !a.startsWith("-"));
2572
2499
  const { type, name } = parseResource(positionalArgs[0]);
2573
2500
  const autoConfirm = args.includes("--yes") || args.includes("-y") || !!process.env.CI;
2574
- const skipApp = args.includes("--no-app") || args.includes("--resources-only");
2575
- if (type === "app" && skipApp) {
2576
- throw new Error("Cannot combine app resource with --no-app / --resources-only");
2577
- }
2578
2501
  if (type === "app") {
2579
2502
  console.log();
2580
2503
  console.log(pc2.cyan(pc2.bold(" Apply")));
@@ -2615,7 +2538,7 @@ async function apply(args) {
2615
2538
  }
2616
2539
  }
2617
2540
  let willDeployApp = false;
2618
- if (!type && !skipApp) {
2541
+ if (!type) {
2619
2542
  try {
2620
2543
  if (existsSync(join(projectRoot, "dist")) || existsSync(join(projectRoot, "src"))) {
2621
2544
  willDeployApp = true;
@@ -2795,6 +2718,11 @@ async function pull(args) {
2795
2718
  console.log(pc2.bold(" Collections:"));
2796
2719
  await pullCollections(api, platformDir, name || void 0, appName);
2797
2720
  console.log();
2721
+ if (!name && await projectResourceDepsEnabled(projectRoot)) {
2722
+ console.log(pc2.bold(" Resource shares:"));
2723
+ await syncDeps(projectRoot, { write: true, legacyWhenDisabled: false });
2724
+ console.log();
2725
+ }
2798
2726
  }
2799
2727
  if (!type || type === "automations") {
2800
2728
  console.log(pc2.bold(" Automations:"));
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-2CR762KB.js";
4
4
  import {
5
5
  createApiClient
6
- } from "./chunk-GKI2HQJC.js";
6
+ } from "./chunk-JKXLKK5I.js";
7
7
  import {
8
8
  findProjectRoot,
9
9
  getApiUrl,
@@ -1,7 +1,6 @@
1
1
  import {
2
2
  listAllTemplates
3
- } from "./chunk-OQW5E7UT.js";
4
- import "./chunk-FJFIWC7G.js";
3
+ } from "./chunk-H357NP7T.js";
5
4
  import "./chunk-PNKVD2UK.js";
6
5
 
7
6
  // src/commands/templates.ts
@@ -25,7 +24,7 @@ ${pc.dim("Examples:")}
25
24
  lumera templates # List available templates
26
25
  lumera templates list -v # Show with descriptions
27
26
  lumera templates validate ./my-template # Validate a template dir
28
- lumera init my-app -t invoice-processing # Use a template
27
+ lumera init my-app -t default # Use a template
29
28
  `);
30
29
  }
31
30
  async function list(args) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.19.3-dev.0",
3
+ "version": "0.19.3",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {
File without changes
@@ -0,0 +1,151 @@
1
+ # {{projectTitle}}
2
+
3
+ A Lumera app built with collections, automations, hooks, and a React frontend.
4
+
5
+ ## Project Structure
6
+
7
+ ```
8
+ platform/
9
+ ├── collections/ # Collection schemas (JSON) — deployed via lumera apply
10
+ ├── automations/ # Python automations (config.json + run.py per automation)
11
+ ├── hooks/ # JavaScript hooks on collection lifecycle events
12
+ src/
13
+ ├── routes/index.tsx # Home page
14
+ ├── lib/queries.ts # Data fetching helpers
15
+ ├── components/ # Shared UI components
16
+ ├── components/ui/ # shadcn UI primitives (installed via MCP)
17
+ scripts/ # Utility scripts (seed data, migrations, etc.)
18
+ ```
19
+
20
+ ## Lumera Concepts
21
+
22
+ A Lumera app is built from these primitives — all defined as code in `platform/`:
23
+
24
+ - **Collections** (`platform/collections/*.json`) — Data tables with typed fields. Deployed via `lumera apply`.
25
+ - **Automations** (`platform/automations/*/`) — Python scripts that run on Lumera's servers. Each has a `config.json` and `run.py`.
26
+ - **Hooks** (`platform/hooks/*.js`) — JavaScript on collection lifecycle events (`before_create`, `after_update`, etc.).
27
+ - **Webhooks** — Receive events from external services (Stripe, GitHub, etc.). Events land in `lm_event_log`; process them with hooks or automations.
28
+ - **Mailbox** — Each tenant gets an email address. Inbound emails are persisted to `lm_mailbox_messages` — use hooks to trigger automations on new mail.
29
+ - **Email** — Send transactional emails from automations via `from lumera import email`. Logged to `lm_email_logs`.
30
+
31
+ All resources use **external IDs** in the format `<app-name>:<resource-name>` (auto-derived from `package.json` name + directory/file name).
32
+
33
+ ## Project Namespacing
34
+
35
+ **All collection names are automatically namespaced by project.** Always use bare names (e.g. `orders`, not `myproject__orders`) everywhere — in collection JSON files, Python SDK calls, JavaScript hooks, and frontend queries. The platform resolves bare names to the project-scoped version transparently.
36
+
37
+ - `pb.ensure_collection("orders", ...)` → stored as `{project}__orders`
38
+ - `pb.search("orders", ...)` → resolves to `{project}__orders`
39
+ - `ctx.dao.find("orders", ...)` → resolves to `{project}__orders`
40
+ - `pbList("orders", ...)` in frontend → resolves via `X-Lumera-Project` header
41
+
42
+ **Never manually prefix collection names with `{project}__`.** Two projects can each have an `orders` collection — they are fully isolated.
43
+
44
+ For detailed technical reference (data models, relationships, design decisions), see [architecture.md](architecture.md).
45
+
46
+ ## UI Components
47
+
48
+ **Always use shadcn components for UI.** Never hand-code primitives like buttons, dialogs, cards, inputs, tables, etc.
49
+
50
+ ### Installing components
51
+
52
+ Use `bunx` to install shadcn components (the sandbox has `bun`, not `npm`/`npx`):
53
+
54
+ ```bash
55
+ bunx shadcn@latest add button card dialog # Install specific components
56
+ bunx shadcn@latest add table input label select # Install more as needed
57
+ ```
58
+
59
+ Components install into `src/components/ui/` and are fully editable.
60
+
61
+ ### shadcn MCP server
62
+
63
+ A `shadcn` MCP server is available to browse and search the registry. Use `mcp({ server: "shadcn" })` to list available tools, then:
64
+
65
+ ```
66
+ mcp({ search: "button" }) # Search across all MCP servers
67
+ mcp({ server: "shadcn" }) # List shadcn tools
68
+ ```
69
+
70
+ Useful MCP tools:
71
+ - **search_items_in_registries** — fuzzy search components by name
72
+ - **view_items_in_registries** — view component source code
73
+ - **get_item_examples_from_registries** — find usage examples with full code
74
+ - **get_add_command_for_items** — get the install command
75
+
76
+ All tools use the `@shadcn` registry (e.g. `@shadcn/button`, `@shadcn/card`).
77
+
78
+ ### Project setup
79
+
80
+ - `components.json` — configures shadcn paths, aliases, and theme
81
+ - `src/styles.css` — shadcn neutral theme CSS variables
82
+ - `src/lib/utils.ts` — `cn()` utility, import from `@/lib/utils`
83
+ - `src/components/ui/` — installed shadcn components (editable)
84
+
85
+ **Do not hand-code UI primitives.** If you need a button, card, dialog, table, form, select, tabs, tooltip, or any standard UI element — install it from shadcn first.
86
+
87
+ ## Frontend API Calls
88
+
89
+ Custom apps run in an **iframe**. **Always use the `@lumerahq/ui` bridge for API calls.** Never use raw `fetch()` against Lumera API endpoints.
90
+
91
+ ```ts
92
+ // ✅ Correct — uses postMessage bridge, no CORS issues
93
+ import { pbSql, pbList, pbCreate, pbUpdate, pbDelete } from '@lumerahq/ui/lib';
94
+
95
+ // CRUD
96
+ const records = await pbList('orders', { filter: JSON.stringify({ status: "pending" }) });
97
+ await pbCreate('orders', { amount: 100 });
98
+ await pbUpdate('orders', recordId, { status: 'done' });
99
+ await pbDelete('orders', recordId);
100
+
101
+ // ❌ WRONG — direct fetch will fail with CORS errors
102
+ fetch('/api/pb/sql', { method: 'POST', body: JSON.stringify({ sql: '...' }) });
103
+ fetch('https://app.lumerahq.com/api/pb/sql', ...);
104
+ ```
105
+
106
+ **Why:** The bridge sends requests to the parent window via `postMessage`. The parent makes the actual API call on its own origin — no CORS, and auth is handled automatically.
107
+
108
+ **Rules:**
109
+ - Always import from `@lumerahq/ui/lib` — never write a custom `apiFetch` with raw `fetch()`
110
+ - Token is not needed — the bridge handles auth via the parent session
111
+ - `X-Lumera-Project` header is not needed — the parent adds it automatically
112
+
113
+
114
+ ## Workflow
115
+
116
+ Follow the user's lead. If they tell you exactly what to build, build it. The workflow below is the default when they describe a goal and leave the approach to you.
117
+
118
+ ### Step 1: Plan
119
+ 1. **Read skills first** — Read the matching skill files for API details and patterns.
120
+ 2. **Discuss the plan** — Start from the **simplest thing that works** — one collection, one screen, one feature. Propose incremental steps that layer on complexity but if the decisions are obvious, you can execute multiple steps in one go. Each step should be a complete horizontal slice (collection + backend logic + UI).
121
+ 3. **Stop and ask the user to approve.** Iterate until they're happy with the plan. They may reorder steps, drop features, or add ones you didn't think of.
122
+
123
+ ### Step 2: Build (one slice at a time)
124
+ 4. **Build horizontally** — Pick the first step. Build the full slice: collection schema → `lumera apply` → seed data → UI route/components → commit. Each slice should be deployable and usable on its own.
125
+ 5. **Stop and ask for feedback** — Tell the user to open the **Preview tab** to see the app. The dev server starts automatically — you do NOT need to run `pnpm dev` or start any server. Iterate on the slice until they're happy.
126
+ 6. **Repeat** — Move to the next step. Build, deploy, get feedback.
127
+
128
+ ### Rules
129
+ 7. **Code is source of truth** — Edit files in `platform/`, then deploy with `lumera apply`. Don't edit in the Lumera UI.
130
+ 8. **Keep docs current** — After each slice, update `architecture.md` with what was built (data models, relationships, hook logic, design decisions). Also update the project description at the top of this file (`AGENTS.md`) so it reflects what the project actually does now — not the original template description.
131
+ 9. **Commit and push** — After each slice or significant change: `git add -A && git commit -m "descriptive message" && git push`. The sandbox is ephemeral — uncommitted work is lost if recycled.
132
+ 10. **Deploy marker** — When your changes need `lumera apply`, include at the end of your response: `<!-- DEPLOY: short commit message -->`. Skip for frontend-only changes.
133
+
134
+ ## File Artifacts
135
+
136
+ When you create a file the user should **see** in chat (HTML pages, SVG graphics, CSV exports, PDFs, images, charts, etc.), call `create_artifact` with the file path after writing it. This uploads the file and renders an inline preview or download card in the chat. Without this step, the file exists in the sandbox but the user cannot see it.
137
+
138
+ Always call `create_artifact` for: `.html`, `.svg`, `.csv`, `.json`, `.xml`, `.md`, `.txt`, `.pdf`, `.png`, `.jpg`, `.gif`, `.xlsx`, `.docx`, `.pptx`, `.zip` — any file the user asked to see or download.
139
+
140
+ ## Python
141
+
142
+ Python 3.14 is pre-installed with common data packages (pandas, numpy, matplotlib, pdfplumber, openpyxl, etc.). To install additional packages use `uv add <package>` (preferred) or `pip install <package>`.
143
+
144
+ ## Key Commands
145
+
146
+ ```bash
147
+ lumera plan # Preview changes (dry run)
148
+ lumera apply # Deploy everything
149
+ lumera run scripts/seed.py # Run a script remotely
150
+ lumera status # Show sync status
151
+ ```
@@ -0,0 +1,18 @@
1
+ # {{projectTitle}}
2
+
3
+ Blank Lumera starter — a minimal React app with platform scaffolding. No pre-built collections, automations, or agents. Start building from scratch.
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ lumera plan # Preview changes
9
+ lumera apply # Deploy resources
10
+ ```
11
+
12
+ ## Structure
13
+
14
+ - `platform/collections/` — Collection schemas (JSON)
15
+ - `platform/automations/` — Python automations
16
+ - `platform/hooks/` — JavaScript hooks
17
+ - `src/` — React frontend (TanStack Router + Query)
18
+ - `scripts/` — Utility scripts
@@ -0,0 +1,14 @@
1
+ node_modules
2
+ dist
3
+ pnpm-lock.yaml
4
+ .env
5
+ .env.local
6
+ *.local
7
+ __pycache__
8
+ *.pyc
9
+ .venv/
10
+ .lumera/
11
+ uv.lock
12
+ src/routeTree.gen.ts
13
+ .tanstack/
14
+ .pi/sessions/
@@ -0,0 +1,29 @@
1
+ # Architecture
2
+
3
+ > Update this file as you build — it should always reflect the current state of the project so any agent or developer can understand the system from this file alone.
4
+
5
+ ## Overview
6
+
7
+ This is a blank starter. No collections, automations, or hooks are defined yet. Build your app by adding resources to `platform/` and deploying with `lumera apply`.
8
+
9
+ ## Collections
10
+
11
+ _None yet — add collection schemas to `platform/collections/` as you build._
12
+
13
+ ## Automations
14
+
15
+ _None yet — add automation configs to `platform/automations/` as you build._
16
+
17
+ ## Hooks
18
+
19
+ _None yet — add hook scripts to `platform/hooks/` as you build._
20
+
21
+ ## Frontend Routes
22
+
23
+ | Route | Purpose |
24
+ |-------|---------|
25
+ | `/` | Home page — blank canvas, ready to build |
26
+
27
+ ## Design Decisions
28
+
29
+ _Document key decisions here as you make them._
@@ -0,0 +1,38 @@
1
+ {
2
+ "$schema": "https://biomejs.dev/schemas/2.0.0/schema.json",
3
+ "vcs": {
4
+ "enabled": true,
5
+ "clientKind": "git",
6
+ "useIgnoreFile": true
7
+ },
8
+ "files": {
9
+ "ignoreUnknown": false,
10
+ "includes": ["**", "!!**/node_modules", "!!**/dist", "!!**/*routeTree.gen.ts"]
11
+ },
12
+ "formatter": {
13
+ "enabled": true,
14
+ "indentStyle": "space",
15
+ "indentWidth": 2,
16
+ "lineWidth": 100
17
+ },
18
+ "linter": {
19
+ "enabled": true,
20
+ "rules": {
21
+ "recommended": true,
22
+ "suspicious": { "noExplicitAny": "off" },
23
+ "style": { "noNonNullAssertion": "off" }
24
+ }
25
+ },
26
+ "javascript": {
27
+ "formatter": {
28
+ "quoteStyle": "single",
29
+ "semicolons": "always",
30
+ "trailingCommas": "es5"
31
+ }
32
+ },
33
+ "css": {
34
+ "parser": {
35
+ "tailwindDirectives": true
36
+ }
37
+ }
38
+ }
@@ -0,0 +1,21 @@
1
+ {
2
+ "$schema": "https://ui.shadcn.com/schema.json",
3
+ "style": "radix-luma",
4
+ "rsc": false,
5
+ "tsx": true,
6
+ "tailwind": {
7
+ "config": "",
8
+ "css": "src/styles.css",
9
+ "baseColor": "neutral",
10
+ "cssVariables": true,
11
+ "prefix": ""
12
+ },
13
+ "iconLibrary": "lucide",
14
+ "aliases": {
15
+ "components": "@/components",
16
+ "utils": "@/lib/utils",
17
+ "ui": "@/components/ui",
18
+ "lib": "@/lib",
19
+ "hooks": "@/hooks"
20
+ }
21
+ }
@@ -0,0 +1,29 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80" fill="none">
2
+ <defs>
3
+ <linearGradient id="bg" x1="0" y1="0" x2="80" y2="80" gradientUnits="userSpaceOnUse">
4
+ <stop offset="0%" stop-color="#ecfdf5"/>
5
+ <stop offset="100%" stop-color="#d1fae5"/>
6
+ </linearGradient>
7
+ <linearGradient id="g1" x1="0" y1="0" x2="1" y2="1">
8
+ <stop offset="0%" stop-color="#34d399"/>
9
+ <stop offset="100%" stop-color="#059669"/>
10
+ </linearGradient>
11
+ </defs>
12
+ <rect width="80" height="80" rx="18" fill="url(#bg)"/>
13
+ <!-- Rocket body -->
14
+ <path d="M40 12 C40 12 28 28 28 44 C28 52 33 58 40 58 C47 58 52 52 52 44 C52 28 40 12 40 12Z" fill="url(#g1)" opacity="0.9"/>
15
+ <!-- Window -->
16
+ <circle cx="40" cy="34" r="5" fill="#ecfdf5"/>
17
+ <circle cx="40" cy="34" r="3" fill="#059669" opacity="0.3"/>
18
+ <!-- Fins -->
19
+ <path d="M28 44 L20 54 L28 50Z" fill="#34d399" opacity="0.7"/>
20
+ <path d="M52 44 L60 54 L52 50Z" fill="#34d399" opacity="0.7"/>
21
+ <!-- Flame -->
22
+ <path d="M36 58 Q38 68 40 72 Q42 68 44 58" fill="#fbbf24"/>
23
+ <path d="M38 58 Q39 65 40 68 Q41 65 42 58" fill="#f97316"/>
24
+ <!-- Stars -->
25
+ <circle cx="18" cy="18" r="1.5" fill="#059669" opacity="0.5"/>
26
+ <circle cx="62" cy="14" r="1" fill="#059669" opacity="0.4"/>
27
+ <circle cx="14" cy="38" r="1" fill="#059669" opacity="0.3"/>
28
+ <circle cx="66" cy="32" r="1.5" fill="#059669" opacity="0.4"/>
29
+ </svg>
@@ -0,0 +1,16 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
6
+ <meta name="theme-color" content="#000000" />
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
10
+ <title>{{projectTitle}}</title>
11
+ </head>
12
+ <body>
13
+ <div id="app"></div>
14
+ <script type="module" src="/src/main.tsx"></script>
15
+ </body>
16
+ </html>