@lumerahq/cli 0.10.0 → 0.10.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/chunk-WTDV3MTG.js +104 -0
- package/dist/index.js +5 -1
- package/dist/{init-EDSRR3YM.js → init-OH433IPH.js} +72 -55
- package/dist/templates-67O6PVFK.js +70 -0
- package/package.json +1 -1
- package/templates/default/ARCHITECTURE.md +0 -80
- package/templates/default/CLAUDE.md +0 -238
- package/templates/default/README.md +0 -59
- package/templates/default/biome.json +0 -38
- package/templates/default/gitignore +0 -9
- package/templates/default/index.html +0 -13
- package/templates/default/package.json.hbs +0 -47
- package/templates/default/platform/automations/.gitkeep +0 -0
- package/templates/default/platform/collections/example_items.json +0 -26
- package/templates/default/platform/hooks/.gitkeep +0 -0
- package/templates/default/pyproject.toml.hbs +0 -14
- package/templates/default/scripts/seed-demo.py +0 -35
- package/templates/default/src/components/Sidebar.tsx +0 -82
- package/templates/default/src/components/StatCard.tsx +0 -25
- package/templates/default/src/components/layout.tsx +0 -13
- package/templates/default/src/lib/queries.ts +0 -27
- package/templates/default/src/main.tsx +0 -131
- package/templates/default/src/routes/__root.tsx +0 -10
- package/templates/default/src/routes/index.tsx +0 -88
- package/templates/default/src/routes/settings.tsx +0 -21
- package/templates/default/src/styles.css +0 -44
- package/templates/default/tsconfig.json +0 -23
- package/templates/default/vite.config.ts +0 -28
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
// src/lib/templates.ts
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, unlinkSync, writeFileSync } from "fs";
|
|
3
|
+
import { homedir, tmpdir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
import { spawnSync } from "child_process";
|
|
6
|
+
var GITHUB_OWNER = "lumerahq";
|
|
7
|
+
var GITHUB_REPO = "app-templates";
|
|
8
|
+
var GITHUB_REF = "main";
|
|
9
|
+
function getCacheDir() {
|
|
10
|
+
return join(homedir(), ".lumera", "templates");
|
|
11
|
+
}
|
|
12
|
+
function readCacheMeta() {
|
|
13
|
+
const metaPath = join(getCacheDir(), ".cache-meta.json");
|
|
14
|
+
if (!existsSync(metaPath)) return null;
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
17
|
+
} catch {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async function fetchLatestSha() {
|
|
22
|
+
const url = `https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/commits/${GITHUB_REF}`;
|
|
23
|
+
const res = await fetch(url, {
|
|
24
|
+
headers: { Accept: "application/vnd.github.sha" }
|
|
25
|
+
});
|
|
26
|
+
if (!res.ok) throw new Error(`GitHub API error: ${res.status}`);
|
|
27
|
+
return (await res.text()).trim();
|
|
28
|
+
}
|
|
29
|
+
async function downloadAndExtract(commitSha) {
|
|
30
|
+
const cacheDir = getCacheDir();
|
|
31
|
+
const tarballUrl = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/archive/refs/heads/${GITHUB_REF}.tar.gz`;
|
|
32
|
+
const res = await fetch(tarballUrl);
|
|
33
|
+
if (!res.ok) throw new Error(`Failed to download templates: ${res.status}`);
|
|
34
|
+
const tmpFile = join(tmpdir(), `lumera-templates-${Date.now()}.tar.gz`);
|
|
35
|
+
const buffer = Buffer.from(await res.arrayBuffer());
|
|
36
|
+
writeFileSync(tmpFile, buffer);
|
|
37
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
38
|
+
for (const entry of readdirSync(cacheDir)) {
|
|
39
|
+
if (entry === ".cache-meta.json") continue;
|
|
40
|
+
rmSync(join(cacheDir, entry), { recursive: true, force: true });
|
|
41
|
+
}
|
|
42
|
+
const result = spawnSync("tar", ["xzf", tmpFile, "-C", cacheDir, "--strip-components=1"], { stdio: "ignore" });
|
|
43
|
+
if (result.status !== 0) throw new Error("Failed to extract template archive");
|
|
44
|
+
unlinkSync(tmpFile);
|
|
45
|
+
const meta = { commitSha, fetchedAt: (/* @__PURE__ */ new Date()).toISOString() };
|
|
46
|
+
writeFileSync(join(cacheDir, ".cache-meta.json"), JSON.stringify(meta));
|
|
47
|
+
return cacheDir;
|
|
48
|
+
}
|
|
49
|
+
async function ensureRemoteCache() {
|
|
50
|
+
const cacheDir = getCacheDir();
|
|
51
|
+
const cached = readCacheMeta();
|
|
52
|
+
const latestSha = await fetchLatestSha();
|
|
53
|
+
if (cached?.commitSha === latestSha && existsSync(cacheDir)) {
|
|
54
|
+
return cacheDir;
|
|
55
|
+
}
|
|
56
|
+
console.log(" Fetching latest templates...");
|
|
57
|
+
return downloadAndExtract(latestSha);
|
|
58
|
+
}
|
|
59
|
+
function listTemplates(baseDir) {
|
|
60
|
+
if (!existsSync(baseDir)) return [];
|
|
61
|
+
const templates = [];
|
|
62
|
+
for (const entry of readdirSync(baseDir, { withFileTypes: true })) {
|
|
63
|
+
if (!entry.isDirectory()) continue;
|
|
64
|
+
const metaPath = join(baseDir, entry.name, "template.json");
|
|
65
|
+
if (!existsSync(metaPath)) continue;
|
|
66
|
+
try {
|
|
67
|
+
const raw = JSON.parse(readFileSync(metaPath, "utf-8"));
|
|
68
|
+
if (!raw.name || !raw.title || !raw.description || !raw.category) {
|
|
69
|
+
console.warn(`Warning: Skipping template "${entry.name}" \u2014 template.json is missing required fields (name, title, description, category)`);
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
templates.push(raw);
|
|
73
|
+
} catch {
|
|
74
|
+
console.warn(`Warning: Skipping template "${entry.name}" \u2014 invalid template.json`);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return templates.sort((a, b) => {
|
|
78
|
+
if (a.name === "default") return -1;
|
|
79
|
+
if (b.name === "default") return 1;
|
|
80
|
+
return a.title.localeCompare(b.title);
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
async function resolveTemplate(name) {
|
|
84
|
+
const cacheDir = await ensureRemoteCache();
|
|
85
|
+
const dir = join(cacheDir, name);
|
|
86
|
+
if (!existsSync(dir) || !existsSync(join(dir, "template.json"))) {
|
|
87
|
+
const available = listTemplates(cacheDir).map((t) => t.name).join(", ");
|
|
88
|
+
throw new Error(
|
|
89
|
+
`Template "${name}" not found. Available: ${available || "none"}
|
|
90
|
+
Cache location: ${cacheDir}
|
|
91
|
+
Try deleting the cache and retrying: rm -rf ${cacheDir}`
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
return dir;
|
|
95
|
+
}
|
|
96
|
+
async function listAllTemplates() {
|
|
97
|
+
const cacheDir = await ensureRemoteCache();
|
|
98
|
+
return listTemplates(cacheDir);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export {
|
|
102
|
+
resolveTemplate,
|
|
103
|
+
listAllTemplates
|
|
104
|
+
};
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,7 @@ ${pc.dim("Development:")}
|
|
|
32
32
|
|
|
33
33
|
${pc.dim("Project:")}
|
|
34
34
|
${pc.cyan("init")} [name] Scaffold a new project
|
|
35
|
+
${pc.cyan("templates")} List available templates
|
|
35
36
|
${pc.cyan("status")} Show project info
|
|
36
37
|
${pc.cyan("migrate")} Upgrade legacy project
|
|
37
38
|
|
|
@@ -118,7 +119,10 @@ async function main() {
|
|
|
118
119
|
break;
|
|
119
120
|
// Project
|
|
120
121
|
case "init":
|
|
121
|
-
await import("./init-
|
|
122
|
+
await import("./init-OH433IPH.js").then((m) => m.init(args.slice(1)));
|
|
123
|
+
break;
|
|
124
|
+
case "templates":
|
|
125
|
+
await import("./templates-67O6PVFK.js").then((m) => m.templates(args.slice(1)));
|
|
122
126
|
break;
|
|
123
127
|
case "status":
|
|
124
128
|
await import("./status-E4IHEUKO.js").then((m) => m.status(args.slice(1)));
|
|
@@ -2,6 +2,10 @@ import {
|
|
|
2
2
|
installAllSkills,
|
|
3
3
|
syncClaudeMd
|
|
4
4
|
} from "./chunk-UP3GV4HN.js";
|
|
5
|
+
import {
|
|
6
|
+
listAllTemplates,
|
|
7
|
+
resolveTemplate
|
|
8
|
+
} from "./chunk-WTDV3MTG.js";
|
|
5
9
|
import "./chunk-D2BLSEGR.js";
|
|
6
10
|
|
|
7
11
|
// src/commands/init.ts
|
|
@@ -9,51 +13,35 @@ import pc from "picocolors";
|
|
|
9
13
|
import prompts from "prompts";
|
|
10
14
|
import { execSync } from "child_process";
|
|
11
15
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, rmSync } from "fs";
|
|
12
|
-
import { join,
|
|
13
|
-
import { fileURLToPath } from "url";
|
|
14
|
-
var __filename = fileURLToPath(import.meta.url);
|
|
15
|
-
var __dirname = dirname(__filename);
|
|
16
|
-
function getTemplatesDir() {
|
|
17
|
-
const candidates = [
|
|
18
|
-
// From dist/index.js or dist/commands/init.js
|
|
19
|
-
resolve(__dirname, "../templates/default"),
|
|
20
|
-
resolve(__dirname, "../../templates/default"),
|
|
21
|
-
// From src during development
|
|
22
|
-
resolve(__dirname, "../../../templates/default")
|
|
23
|
-
];
|
|
24
|
-
for (const candidate of candidates) {
|
|
25
|
-
if (existsSync(candidate)) {
|
|
26
|
-
return candidate;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
throw new Error(`Templates directory not found. Searched: ${candidates.join(", ")}`);
|
|
30
|
-
}
|
|
16
|
+
import { join, resolve } from "path";
|
|
31
17
|
function toTitleCase(str) {
|
|
32
18
|
return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
|
|
33
19
|
}
|
|
20
|
+
var TEMPLATE_DEFAULTS = {
|
|
21
|
+
projectName: "my-lumera-app",
|
|
22
|
+
projectTitle: "My Lumera App"
|
|
23
|
+
};
|
|
34
24
|
function processTemplate(content, vars) {
|
|
35
25
|
let result = content;
|
|
36
26
|
for (const [key, value] of Object.entries(vars)) {
|
|
37
|
-
|
|
27
|
+
const defaultValue = TEMPLATE_DEFAULTS[key];
|
|
28
|
+
if (defaultValue && defaultValue !== value) {
|
|
29
|
+
result = result.replaceAll(defaultValue, value);
|
|
30
|
+
}
|
|
38
31
|
}
|
|
39
32
|
return result;
|
|
40
33
|
}
|
|
41
|
-
|
|
34
|
+
var TEMPLATE_EXCLUDE = /* @__PURE__ */ new Set(["template.json"]);
|
|
35
|
+
function copyDir(src, dest, vars, isRoot = true) {
|
|
42
36
|
if (!existsSync(dest)) {
|
|
43
37
|
mkdirSync(dest, { recursive: true });
|
|
44
38
|
}
|
|
45
39
|
for (const entry of readdirSync(src, { withFileTypes: true })) {
|
|
40
|
+
if (isRoot && TEMPLATE_EXCLUDE.has(entry.name)) continue;
|
|
46
41
|
const srcPath = join(src, entry.name);
|
|
47
|
-
|
|
48
|
-
if (destName.endsWith(".hbs")) {
|
|
49
|
-
destName = destName.slice(0, -4);
|
|
50
|
-
}
|
|
51
|
-
if (destName === "gitignore") {
|
|
52
|
-
destName = ".gitignore";
|
|
53
|
-
}
|
|
54
|
-
const destPath = join(dest, destName);
|
|
42
|
+
const destPath = join(dest, entry.name);
|
|
55
43
|
if (entry.isDirectory()) {
|
|
56
|
-
copyDir(srcPath, destPath, vars);
|
|
44
|
+
copyDir(srcPath, destPath, vars, false);
|
|
57
45
|
} else {
|
|
58
46
|
const content = readFileSync(srcPath, "utf-8");
|
|
59
47
|
const processed = processTemplate(content, vars);
|
|
@@ -122,6 +110,7 @@ function parseArgs(args) {
|
|
|
122
110
|
const result = {
|
|
123
111
|
projectName: void 0,
|
|
124
112
|
directory: void 0,
|
|
113
|
+
template: void 0,
|
|
125
114
|
install: false,
|
|
126
115
|
yes: false,
|
|
127
116
|
force: false,
|
|
@@ -139,6 +128,8 @@ function parseArgs(args) {
|
|
|
139
128
|
result.force = true;
|
|
140
129
|
} else if (arg === "--dir" || arg === "-d") {
|
|
141
130
|
result.directory = args[++i];
|
|
131
|
+
} else if (arg === "--template" || arg === "-t") {
|
|
132
|
+
result.template = args[++i];
|
|
142
133
|
} else if (!arg.startsWith("-") && !result.projectName) {
|
|
143
134
|
result.projectName = arg;
|
|
144
135
|
}
|
|
@@ -151,27 +142,21 @@ ${pc.dim("Usage:")}
|
|
|
151
142
|
lumera init [name] [options]
|
|
152
143
|
|
|
153
144
|
${pc.dim("Description:")}
|
|
154
|
-
Scaffold a new Lumera project.
|
|
145
|
+
Scaffold a new Lumera project from a template.
|
|
155
146
|
|
|
156
147
|
${pc.dim("Options:")}
|
|
157
|
-
--
|
|
158
|
-
--
|
|
159
|
-
--
|
|
160
|
-
--
|
|
161
|
-
--
|
|
162
|
-
|
|
163
|
-
${pc.dim("Interactive mode:")}
|
|
164
|
-
lumera init # Prompt for project name and directory
|
|
165
|
-
lumera init my-project # Prompt for directory only
|
|
166
|
-
|
|
167
|
-
${pc.dim("Non-interactive mode:")}
|
|
168
|
-
lumera init my-project -y # Create ./my-project
|
|
169
|
-
lumera init my-project -y -d ./apps # Create ./apps
|
|
170
|
-
lumera init my-project -y -f # Overwrite if exists
|
|
171
|
-
lumera init my-project -y -i # Create and install deps
|
|
148
|
+
--template, -t <name> Template to use (run ${pc.cyan("lumera templates")} to see options)
|
|
149
|
+
--yes, -y Non-interactive mode (requires project name)
|
|
150
|
+
--dir, -d <path> Target directory (defaults to project name)
|
|
151
|
+
--force, -f Overwrite existing directory without prompting
|
|
152
|
+
--install, -i Install dependencies after scaffolding
|
|
153
|
+
--help, -h Show this help
|
|
172
154
|
|
|
173
|
-
${pc.dim("
|
|
174
|
-
lumera init my-app
|
|
155
|
+
${pc.dim("Examples:")}
|
|
156
|
+
lumera init my-app # Interactive mode
|
|
157
|
+
lumera init my-app -t invoice-processing # Use a specific template
|
|
158
|
+
lumera init my-app -y # Non-interactive (default template)
|
|
159
|
+
lumera init my-app -t invoice-processing -y -i # Full non-interactive setup
|
|
175
160
|
`);
|
|
176
161
|
}
|
|
177
162
|
async function init(args) {
|
|
@@ -191,6 +176,35 @@ async function init(args) {
|
|
|
191
176
|
console.log(pc.dim(" Usage: lumera init <name> -y"));
|
|
192
177
|
process.exit(1);
|
|
193
178
|
}
|
|
179
|
+
let templateName = opts.template;
|
|
180
|
+
if (!templateName && !nonInteractive) {
|
|
181
|
+
try {
|
|
182
|
+
const available = await listAllTemplates();
|
|
183
|
+
if (available.length > 1) {
|
|
184
|
+
const response = await prompts({
|
|
185
|
+
type: "select",
|
|
186
|
+
name: "template",
|
|
187
|
+
message: "Choose a template",
|
|
188
|
+
choices: available.map((t) => ({
|
|
189
|
+
title: `${t.title} ${pc.dim(`(${t.name})`)}`,
|
|
190
|
+
description: t.description,
|
|
191
|
+
value: t.name
|
|
192
|
+
})),
|
|
193
|
+
initial: 0
|
|
194
|
+
});
|
|
195
|
+
if (!response.template) {
|
|
196
|
+
console.log(pc.red("Cancelled"));
|
|
197
|
+
process.exit(1);
|
|
198
|
+
}
|
|
199
|
+
templateName = response.template;
|
|
200
|
+
}
|
|
201
|
+
} catch {
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!templateName) {
|
|
205
|
+
templateName = "default";
|
|
206
|
+
}
|
|
207
|
+
const templateDir = await resolveTemplate(templateName);
|
|
194
208
|
if (!projectName) {
|
|
195
209
|
const response = await prompts({
|
|
196
210
|
type: "text",
|
|
@@ -233,7 +247,6 @@ async function init(args) {
|
|
|
233
247
|
}
|
|
234
248
|
}
|
|
235
249
|
const projectTitle = toTitleCase(projectName);
|
|
236
|
-
const projectInitial = projectTitle.charAt(0).toUpperCase();
|
|
237
250
|
const targetDir = resolve(process.cwd(), directory);
|
|
238
251
|
if (existsSync(targetDir)) {
|
|
239
252
|
if (nonInteractive) {
|
|
@@ -260,15 +273,17 @@ async function init(args) {
|
|
|
260
273
|
}
|
|
261
274
|
mkdirSync(targetDir, { recursive: true });
|
|
262
275
|
console.log();
|
|
263
|
-
|
|
276
|
+
if (templateName !== "default") {
|
|
277
|
+
console.log(pc.dim(` Creating ${projectName} from template ${pc.cyan(templateName)}...`));
|
|
278
|
+
} else {
|
|
279
|
+
console.log(pc.dim(` Creating ${projectName}...`));
|
|
280
|
+
}
|
|
264
281
|
console.log();
|
|
265
|
-
const templatesDir = getTemplatesDir();
|
|
266
282
|
const vars = {
|
|
267
283
|
projectName,
|
|
268
|
-
projectTitle
|
|
269
|
-
projectInitial
|
|
284
|
+
projectTitle
|
|
270
285
|
};
|
|
271
|
-
copyDir(
|
|
286
|
+
copyDir(templateDir, targetDir, vars);
|
|
272
287
|
function listFiles(dir, prefix = "") {
|
|
273
288
|
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
274
289
|
const relativePath = prefix + entry.name;
|
|
@@ -352,7 +367,9 @@ async function init(args) {
|
|
|
352
367
|
console.log(pc.cyan(" pnpm install"));
|
|
353
368
|
}
|
|
354
369
|
console.log(pc.cyan(" lumera login"));
|
|
355
|
-
console.log(pc.cyan("
|
|
370
|
+
console.log(pc.cyan(" lumera apply"));
|
|
371
|
+
console.log(pc.cyan(" lumera run scripts/seed-demo.py"));
|
|
372
|
+
console.log(pc.cyan(" lumera dev"));
|
|
356
373
|
console.log();
|
|
357
374
|
}
|
|
358
375
|
export {
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import {
|
|
2
|
+
listAllTemplates
|
|
3
|
+
} from "./chunk-WTDV3MTG.js";
|
|
4
|
+
|
|
5
|
+
// src/commands/templates.ts
|
|
6
|
+
import pc from "picocolors";
|
|
7
|
+
function showHelp() {
|
|
8
|
+
console.log(`
|
|
9
|
+
${pc.dim("Usage:")}
|
|
10
|
+
lumera templates [options]
|
|
11
|
+
|
|
12
|
+
${pc.dim("Description:")}
|
|
13
|
+
List available project templates.
|
|
14
|
+
|
|
15
|
+
${pc.dim("Options:")}
|
|
16
|
+
--verbose, -v Show full descriptions
|
|
17
|
+
--help, -h Show this help
|
|
18
|
+
|
|
19
|
+
${pc.dim("Examples:")}
|
|
20
|
+
lumera templates # List available templates
|
|
21
|
+
lumera templates -v # Show with descriptions
|
|
22
|
+
lumera init my-app -t invoice-processing # Use a template
|
|
23
|
+
`);
|
|
24
|
+
}
|
|
25
|
+
async function templates(args) {
|
|
26
|
+
if (args.includes("--help") || args.includes("-h")) {
|
|
27
|
+
showHelp();
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const verbose = args.includes("--verbose") || args.includes("-v");
|
|
31
|
+
console.log();
|
|
32
|
+
console.log(pc.cyan(pc.bold(" Available Templates")));
|
|
33
|
+
console.log();
|
|
34
|
+
try {
|
|
35
|
+
const allTemplates = await listAllTemplates();
|
|
36
|
+
if (allTemplates.length === 0) {
|
|
37
|
+
console.log(pc.dim(" No templates available."));
|
|
38
|
+
console.log();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const byCategory = /* @__PURE__ */ new Map();
|
|
42
|
+
for (const t of allTemplates) {
|
|
43
|
+
const cat = t.category || "General";
|
|
44
|
+
if (!byCategory.has(cat)) byCategory.set(cat, []);
|
|
45
|
+
byCategory.get(cat).push(t);
|
|
46
|
+
}
|
|
47
|
+
for (const [category, items] of byCategory) {
|
|
48
|
+
console.log(pc.bold(` ${category}`));
|
|
49
|
+
console.log();
|
|
50
|
+
for (const t of items) {
|
|
51
|
+
console.log(` ${pc.green(t.title)} ${pc.dim(`(${t.name})`)}`);
|
|
52
|
+
if (verbose) {
|
|
53
|
+
console.log(` ${pc.dim(t.description)}`);
|
|
54
|
+
console.log();
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!verbose) console.log();
|
|
58
|
+
}
|
|
59
|
+
console.log(pc.dim(` ${allTemplates.length} template${allTemplates.length === 1 ? "" : "s"} available.`));
|
|
60
|
+
console.log();
|
|
61
|
+
console.log(pc.dim(" Usage:"), pc.cyan("lumera init <name> --template <template-name>"));
|
|
62
|
+
console.log();
|
|
63
|
+
} catch (err) {
|
|
64
|
+
console.error(pc.red(` Error: ${err}`));
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
export {
|
|
69
|
+
templates
|
|
70
|
+
};
|
package/package.json
CHANGED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
# {{projectTitle}} — Architecture
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
{{projectTitle}} is a Lumera embedded app — a React frontend served inside the Lumera platform iframe, backed by collections, hooks, and automations managed through the Lumera CLI.
|
|
6
|
-
|
|
7
|
-
## System Diagram
|
|
8
|
-
|
|
9
|
-
```
|
|
10
|
-
┌─────────────────────────────────────────────────┐
|
|
11
|
-
│ Lumera Platform │
|
|
12
|
-
│ │
|
|
13
|
-
│ ┌───────────┐ postMessage ┌────────────┐ │
|
|
14
|
-
│ │ Host UI │ ◄──────────────► │ App │ │
|
|
15
|
-
│ │ │ (auth, init) │ (iframe) │ │
|
|
16
|
-
│ └─────┬─────┘ └─────┬──────┘ │
|
|
17
|
-
│ │ │ │
|
|
18
|
-
│ │ REST API │ │
|
|
19
|
-
│ ▼ ▼ │
|
|
20
|
-
│ ┌──────────────────────────────────────────┐ │
|
|
21
|
-
│ │ Lumera API │ │
|
|
22
|
-
│ │ - Collections (CRUD, SQL, search) │ │
|
|
23
|
-
│ │ - Automations (run, poll, cancel) │ │
|
|
24
|
-
│ │ - File storage (upload, download) │ │
|
|
25
|
-
│ └──────────────┬───────────────────────────┘ │
|
|
26
|
-
│ │ │
|
|
27
|
-
│ ▼ │
|
|
28
|
-
│ ┌──────────────────────────────────────────┐ │
|
|
29
|
-
│ │ Tenant Database (PocketBase/SQLite) │ │
|
|
30
|
-
│ │ - example_items │ │
|
|
31
|
-
│ │ - (add your collections here) │ │
|
|
32
|
-
│ └──────────────────────────────────────────┘ │
|
|
33
|
-
└─────────────────────────────────────────────────┘
|
|
34
|
-
```
|
|
35
|
-
|
|
36
|
-
## Frontend (`src/`)
|
|
37
|
-
|
|
38
|
-
React app using TanStack Router (file-based routing) and TanStack Query for data fetching. Embedded in Lumera via iframe with postMessage bridge for authentication.
|
|
39
|
-
|
|
40
|
-
| Directory | Purpose |
|
|
41
|
-
|------------------|--------------------------------------|
|
|
42
|
-
| `src/routes/` | Pages — file names map to URL paths |
|
|
43
|
-
| `src/components/`| Shared React components |
|
|
44
|
-
| `src/lib/` | API helpers, query functions |
|
|
45
|
-
| `src/main.tsx` | App entry — auth bridge, router init |
|
|
46
|
-
|
|
47
|
-
**Key patterns:**
|
|
48
|
-
- Auth context flows from `main.tsx` via `AuthContext`
|
|
49
|
-
- Data fetching uses `pbList`, `pbSql` from `@lumerahq/ui/lib`
|
|
50
|
-
- Styling via Tailwind CSS 4 with theme tokens in `styles.css`
|
|
51
|
-
|
|
52
|
-
## Platform Resources (`platform/`)
|
|
53
|
-
|
|
54
|
-
Declarative definitions deployed via `lumera apply`.
|
|
55
|
-
|
|
56
|
-
| Directory | Purpose |
|
|
57
|
-
|--------------------------|----------------------------------|
|
|
58
|
-
| `platform/collections/` | Collection schemas (JSON) |
|
|
59
|
-
| `platform/automations/` | Background Python scripts |
|
|
60
|
-
| `platform/hooks/` | Server-side JS on collection events |
|
|
61
|
-
|
|
62
|
-
## Scripts (`scripts/`)
|
|
63
|
-
|
|
64
|
-
Local Python scripts run via `lumera run`. Used for seeding data, migrations, and ad-hoc operations. All scripts should be idempotent.
|
|
65
|
-
|
|
66
|
-
## Data Flow
|
|
67
|
-
|
|
68
|
-
1. **User opens app** → Lumera host sends auth payload via postMessage
|
|
69
|
-
2. **App authenticates** → Stores session token in `AuthContext`
|
|
70
|
-
3. **App fetches data** → Calls Lumera API via `@lumerahq/ui/lib` helpers
|
|
71
|
-
4. **Data mutations** → API calls trigger collection hooks if configured
|
|
72
|
-
5. **Background work** → Automations run async via `createRun` / `pollRun`
|
|
73
|
-
|
|
74
|
-
## Collections
|
|
75
|
-
|
|
76
|
-
| Collection | Purpose |
|
|
77
|
-
|------------------|----------------------------|
|
|
78
|
-
| `example_items` | Starter collection (replace with your own) |
|
|
79
|
-
|
|
80
|
-
_Update this table as you add collections._
|