@lumerahq/cli 0.9.4 → 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.
Files changed (31) hide show
  1. package/dist/{chunk-V2XXMMEI.js → chunk-WRAZC6SJ.js} +5 -4
  2. package/dist/chunk-WTDV3MTG.js +104 -0
  3. package/dist/index.js +12 -8
  4. package/dist/{init-EDSRR3YM.js → init-OH433IPH.js} +72 -55
  5. package/dist/{resources-2IHBFKMX.js → resources-PGBVCS2K.js} +7 -5
  6. package/dist/{run-4NDI2CN4.js → run-WIRQDYYX.js} +1 -1
  7. package/dist/templates-67O6PVFK.js +70 -0
  8. package/package.json +1 -1
  9. package/templates/default/ARCHITECTURE.md +0 -80
  10. package/templates/default/CLAUDE.md +0 -238
  11. package/templates/default/README.md +0 -59
  12. package/templates/default/biome.json +0 -38
  13. package/templates/default/gitignore +0 -9
  14. package/templates/default/index.html +0 -13
  15. package/templates/default/package.json.hbs +0 -47
  16. package/templates/default/platform/automations/.gitkeep +0 -0
  17. package/templates/default/platform/collections/example_items.json +0 -26
  18. package/templates/default/platform/hooks/.gitkeep +0 -0
  19. package/templates/default/pyproject.toml.hbs +0 -14
  20. package/templates/default/scripts/seed-demo.py +0 -35
  21. package/templates/default/src/components/Sidebar.tsx +0 -82
  22. package/templates/default/src/components/StatCard.tsx +0 -25
  23. package/templates/default/src/components/layout.tsx +0 -13
  24. package/templates/default/src/lib/queries.ts +0 -27
  25. package/templates/default/src/main.tsx +0 -131
  26. package/templates/default/src/routes/__root.tsx +0 -10
  27. package/templates/default/src/routes/index.tsx +0 -88
  28. package/templates/default/src/routes/settings.tsx +0 -21
  29. package/templates/default/src/styles.css +0 -44
  30. package/templates/default/tsconfig.json +0 -23
  31. package/templates/default/vite.config.ts +0 -28
@@ -56,10 +56,11 @@ var ApiClient = class {
56
56
  }
57
57
  // Automations
58
58
  async listAutomations(params) {
59
- let path = "/api/automations";
60
- if (params?.external_id) {
61
- path += `?external_id=${encodeURIComponent(params.external_id)}`;
62
- }
59
+ const qs = new URLSearchParams();
60
+ if (params?.external_id) qs.set("external_id", params.external_id);
61
+ if (params?.include_code) qs.set("include_code", "true");
62
+ const query = qs.toString();
63
+ const path = `/api/automations${query ? `?${query}` : ""}`;
63
64
  const result = await this.request(path);
64
65
  return result.automations || [];
65
66
  }
@@ -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
 
@@ -92,33 +93,36 @@ async function main() {
92
93
  switch (command) {
93
94
  // Resource commands
94
95
  case "plan":
95
- await import("./resources-2IHBFKMX.js").then((m) => m.plan(args.slice(1)));
96
+ await import("./resources-PGBVCS2K.js").then((m) => m.plan(args.slice(1)));
96
97
  break;
97
98
  case "apply":
98
- await import("./resources-2IHBFKMX.js").then((m) => m.apply(args.slice(1)));
99
+ await import("./resources-PGBVCS2K.js").then((m) => m.apply(args.slice(1)));
99
100
  break;
100
101
  case "pull":
101
- await import("./resources-2IHBFKMX.js").then((m) => m.pull(args.slice(1)));
102
+ await import("./resources-PGBVCS2K.js").then((m) => m.pull(args.slice(1)));
102
103
  break;
103
104
  case "destroy":
104
- await import("./resources-2IHBFKMX.js").then((m) => m.destroy(args.slice(1)));
105
+ await import("./resources-PGBVCS2K.js").then((m) => m.destroy(args.slice(1)));
105
106
  break;
106
107
  case "list":
107
- await import("./resources-2IHBFKMX.js").then((m) => m.list(args.slice(1)));
108
+ await import("./resources-PGBVCS2K.js").then((m) => m.list(args.slice(1)));
108
109
  break;
109
110
  case "show":
110
- await import("./resources-2IHBFKMX.js").then((m) => m.show(args.slice(1)));
111
+ await import("./resources-PGBVCS2K.js").then((m) => m.show(args.slice(1)));
111
112
  break;
112
113
  // Development
113
114
  case "dev":
114
115
  await import("./dev-BHBF4ECH.js").then((m) => m.dev(args.slice(1)));
115
116
  break;
116
117
  case "run":
117
- await import("./run-4NDI2CN4.js").then((m) => m.run(args.slice(1)));
118
+ await import("./run-WIRQDYYX.js").then((m) => m.run(args.slice(1)));
118
119
  break;
119
120
  // Project
120
121
  case "init":
121
- await import("./init-EDSRR3YM.js").then((m) => m.init(args.slice(1)));
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, dirname, resolve } from "path";
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
- result = result.replace(new RegExp(`\\{\\{${key}\\}\\}`, "g"), value);
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
- function copyDir(src, dest, vars) {
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
- let destName = entry.name;
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
- --yes, -y Non-interactive mode (requires project name)
158
- --dir, -d <path> Target directory (defaults to project name)
159
- --force, -f Overwrite existing directory without prompting
160
- --install, -i Install dependencies after scaffolding
161
- --help, -h Show this help
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("CI/CD example:")}
174
- lumera init my-app -y -f -i # Full non-interactive setup
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
- console.log(pc.dim(` Creating ${projectName} in ${directory}...`));
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(templatesDir, targetDir, vars);
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(" pnpm dev"));
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 {
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-CDZZ3JYU.js";
4
4
  import {
5
5
  createApiClient
6
- } from "./chunk-V2XXMMEI.js";
6
+ } from "./chunk-WRAZC6SJ.js";
7
7
  import {
8
8
  loadEnv
9
9
  } from "./chunk-2CR762KB.js";
@@ -452,7 +452,7 @@ async function planCollections(api, localCollections) {
452
452
  }
453
453
  async function planAutomations(api, localAutomations) {
454
454
  const changes = [];
455
- const remoteAutomations = await api.listAutomations();
455
+ const remoteAutomations = await api.listAutomations({ include_code: true });
456
456
  const remoteByExternalId = new Map(remoteAutomations.filter((a) => a.external_id).map((a) => [a.external_id, a]));
457
457
  for (const { automation, code } of localAutomations) {
458
458
  const remote = remoteByExternalId.get(automation.external_id);
@@ -526,9 +526,11 @@ async function applyCollections(api, localCollections) {
526
526
  const hasRelations = localCollections.some((c) => c.fields.some((f) => f.type === "relation"));
527
527
  if (hasRelations) {
528
528
  for (const local of localCollections) {
529
+ const relationFieldNames = new Set(local.fields.filter((f) => f.type === "relation").map((f) => f.name));
529
530
  const withoutRelations = {
530
531
  ...local,
531
- fields: local.fields.filter((f) => f.type !== "relation")
532
+ fields: local.fields.filter((f) => f.type !== "relation"),
533
+ indexes: local.indexes?.filter((idx) => !idx.fields.some((f) => relationFieldNames.has(f)))
532
534
  };
533
535
  const apiFormat = convertCollectionToApiFormat(withoutRelations);
534
536
  try {
@@ -742,7 +744,7 @@ async function pullCollections(api, platformDir, filterName) {
742
744
  async function pullAutomations(api, platformDir, filterName) {
743
745
  const automationsDir = join(platformDir, "automations");
744
746
  mkdirSync(automationsDir, { recursive: true });
745
- const automations = await api.listAutomations();
747
+ const automations = await api.listAutomations({ include_code: true });
746
748
  for (const automation of automations) {
747
749
  if (!automation.external_id || automation.managed) continue;
748
750
  if (filterName && automation.external_id !== filterName && automation.name !== filterName) {
@@ -847,7 +849,7 @@ async function listResources(api, platformDir, filterType) {
847
849
  }
848
850
  if (!filterType || filterType === "automations") {
849
851
  const localAutomations = loadLocalAutomations(platformDir);
850
- const remoteAutomations = await api.listAutomations();
852
+ const remoteAutomations = await api.listAutomations({ include_code: true });
851
853
  const remoteByExternalId = new Map(remoteAutomations.filter((a) => a.external_id && !a.managed).map((a) => [a.external_id, a]));
852
854
  const localIds = new Set(localAutomations.map((a) => a.automation.external_id));
853
855
  for (const { automation, code } of localAutomations) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApiClient
3
- } from "./chunk-V2XXMMEI.js";
3
+ } from "./chunk-WRAZC6SJ.js";
4
4
  import {
5
5
  loadEnv
6
6
  } from "./chunk-2CR762KB.js";
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.9.4",
3
+ "version": "0.10.1",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {
@@ -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._