@lumerahq/cli 0.10.0 → 0.11.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 (35) hide show
  1. package/dist/chunk-CHRKCAIZ.js +155 -0
  2. package/dist/{chunk-WRAZC6SJ.js → chunk-HIYM7EM2.js} +17 -1
  3. package/dist/dev-2HVDP3NX.js +155 -0
  4. package/dist/index.js +166 -14
  5. package/dist/init-VNJNSU4Q.js +440 -0
  6. package/dist/{resources-PGBVCS2K.js → resources-GTG3QMVV.js} +2 -4
  7. package/dist/{run-WIRQDYYX.js → run-47GF5VVS.js} +2 -4
  8. package/dist/templates-6KMZWOYH.js +194 -0
  9. package/package.json +1 -1
  10. package/dist/chunk-2CR762KB.js +0 -18
  11. package/dist/dev-BHBF4ECH.js +0 -87
  12. package/dist/init-EDSRR3YM.js +0 -360
  13. package/templates/default/ARCHITECTURE.md +0 -80
  14. package/templates/default/CLAUDE.md +0 -238
  15. package/templates/default/README.md +0 -59
  16. package/templates/default/biome.json +0 -38
  17. package/templates/default/gitignore +0 -9
  18. package/templates/default/index.html +0 -13
  19. package/templates/default/package.json.hbs +0 -47
  20. package/templates/default/platform/automations/.gitkeep +0 -0
  21. package/templates/default/platform/collections/example_items.json +0 -26
  22. package/templates/default/platform/hooks/.gitkeep +0 -0
  23. package/templates/default/pyproject.toml.hbs +0 -14
  24. package/templates/default/scripts/seed-demo.py +0 -35
  25. package/templates/default/src/components/Sidebar.tsx +0 -82
  26. package/templates/default/src/components/StatCard.tsx +0 -25
  27. package/templates/default/src/components/layout.tsx +0 -13
  28. package/templates/default/src/lib/queries.ts +0 -27
  29. package/templates/default/src/main.tsx +0 -131
  30. package/templates/default/src/routes/__root.tsx +0 -10
  31. package/templates/default/src/routes/index.tsx +0 -88
  32. package/templates/default/src/routes/settings.tsx +0 -21
  33. package/templates/default/src/styles.css +0 -44
  34. package/templates/default/tsconfig.json +0 -23
  35. package/templates/default/vite.config.ts +0 -28
@@ -0,0 +1,440 @@
1
+ import {
2
+ installAllSkills,
3
+ syncClaudeMd
4
+ } from "./chunk-UP3GV4HN.js";
5
+ import {
6
+ listAllTemplates,
7
+ resolveTemplate
8
+ } from "./chunk-CHRKCAIZ.js";
9
+ import "./chunk-D2BLSEGR.js";
10
+
11
+ // src/commands/init.ts
12
+ import pc2 from "picocolors";
13
+ import prompts from "prompts";
14
+ import { execSync } from "child_process";
15
+ import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, rmSync } from "fs";
16
+ import { join, resolve } from "path";
17
+
18
+ // src/lib/spinner.ts
19
+ import pc from "picocolors";
20
+ var frames = ["\u280B", "\u2819", "\u2839", "\u2838", "\u283C", "\u2834", "\u2826", "\u2827", "\u2807", "\u280F"];
21
+ function spinner(message) {
22
+ if (!process.stdout.isTTY) {
23
+ process.stdout.write(` ${message}
24
+ `);
25
+ return (doneMessage) => {
26
+ if (doneMessage) {
27
+ process.stdout.write(` ${doneMessage}
28
+ `);
29
+ }
30
+ };
31
+ }
32
+ let i = 0;
33
+ const interval = setInterval(() => {
34
+ const frame = frames[i % frames.length];
35
+ process.stdout.write(`\r ${pc.cyan(frame)} ${message}`);
36
+ i++;
37
+ }, 80);
38
+ return (doneMessage) => {
39
+ clearInterval(interval);
40
+ process.stdout.write(`\r${" ".repeat(message.length + 10)}\r`);
41
+ if (doneMessage) {
42
+ process.stdout.write(` ${doneMessage}
43
+ `);
44
+ }
45
+ };
46
+ }
47
+
48
+ // src/commands/init.ts
49
+ function toTitleCase(str) {
50
+ return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
51
+ }
52
+ var TEMPLATE_DEFAULTS = {
53
+ projectName: "my-lumera-app",
54
+ projectTitle: "My Lumera App"
55
+ };
56
+ function processTemplate(content, vars) {
57
+ let result = content;
58
+ for (const [key, value] of Object.entries(vars)) {
59
+ const defaultValue = TEMPLATE_DEFAULTS[key];
60
+ if (defaultValue && defaultValue !== value) {
61
+ result = result.replaceAll(defaultValue, value);
62
+ }
63
+ }
64
+ return result;
65
+ }
66
+ var TEMPLATE_EXCLUDE = /* @__PURE__ */ new Set(["template.json"]);
67
+ function copyDir(src, dest, vars, isRoot = true) {
68
+ if (!existsSync(dest)) {
69
+ mkdirSync(dest, { recursive: true });
70
+ }
71
+ for (const entry of readdirSync(src, { withFileTypes: true })) {
72
+ if (isRoot && TEMPLATE_EXCLUDE.has(entry.name)) continue;
73
+ const srcPath = join(src, entry.name);
74
+ const destPath = join(dest, entry.name);
75
+ if (entry.isDirectory()) {
76
+ copyDir(srcPath, destPath, vars, false);
77
+ } else {
78
+ const content = readFileSync(srcPath, "utf-8");
79
+ const processed = processTemplate(content, vars);
80
+ writeFileSync(destPath, processed);
81
+ }
82
+ }
83
+ }
84
+ function isGitInstalled() {
85
+ try {
86
+ execSync("git --version", { stdio: "ignore" });
87
+ return true;
88
+ } catch {
89
+ return false;
90
+ }
91
+ }
92
+ function initGitRepo(targetDir, projectName) {
93
+ try {
94
+ execSync("git init", { cwd: targetDir, stdio: "ignore" });
95
+ execSync("git add -A", { cwd: targetDir, stdio: "ignore" });
96
+ execSync(`git commit -m "Initial commit: scaffold ${projectName}"`, {
97
+ cwd: targetDir,
98
+ stdio: "ignore"
99
+ });
100
+ return true;
101
+ } catch {
102
+ return false;
103
+ }
104
+ }
105
+ function isUvInstalled() {
106
+ try {
107
+ execSync("uv --version", { stdio: "ignore" });
108
+ return true;
109
+ } catch {
110
+ return false;
111
+ }
112
+ }
113
+ function installUv() {
114
+ try {
115
+ try {
116
+ execSync("curl -LsSf https://astral.sh/uv/install.sh | sh", {
117
+ stdio: "ignore",
118
+ shell: "/bin/bash"
119
+ });
120
+ return true;
121
+ } catch {
122
+ execSync("wget -qO- https://astral.sh/uv/install.sh | sh", {
123
+ stdio: "ignore",
124
+ shell: "/bin/bash"
125
+ });
126
+ return true;
127
+ }
128
+ } catch {
129
+ return false;
130
+ }
131
+ }
132
+ function createPythonVenv(targetDir) {
133
+ try {
134
+ execSync("uv venv", { cwd: targetDir, stdio: "ignore" });
135
+ execSync("uv pip install lumera", { cwd: targetDir, stdio: "ignore" });
136
+ return true;
137
+ } catch {
138
+ return false;
139
+ }
140
+ }
141
+ function detectEditor() {
142
+ const envEditor = process.env.VISUAL || process.env.EDITOR;
143
+ if (envEditor) return envEditor;
144
+ const editors = ["cursor", "code", "zed", "subl"];
145
+ for (const editor of editors) {
146
+ try {
147
+ execSync(`which ${editor}`, { stdio: "ignore" });
148
+ return editor;
149
+ } catch {
150
+ continue;
151
+ }
152
+ }
153
+ return null;
154
+ }
155
+ function openInEditor(targetDir) {
156
+ const editor = detectEditor();
157
+ if (!editor) {
158
+ console.log(pc2.yellow(" \u26A0"), pc2.dim("No editor detected. Set VISUAL or EDITOR env var."));
159
+ return;
160
+ }
161
+ try {
162
+ execSync(`${editor} "${targetDir}"`, { stdio: "ignore" });
163
+ console.log(pc2.green(" \u2713"), pc2.dim(`Opened in ${editor}`));
164
+ } catch {
165
+ console.log(pc2.yellow(" \u26A0"), pc2.dim(`Failed to open in ${editor}`));
166
+ }
167
+ }
168
+ function parseArgs(args) {
169
+ const result = {
170
+ projectName: void 0,
171
+ directory: void 0,
172
+ template: void 0,
173
+ install: true,
174
+ open: false,
175
+ yes: false,
176
+ force: false,
177
+ help: false
178
+ };
179
+ for (let i = 0; i < args.length; i++) {
180
+ const arg = args[i];
181
+ if (arg === "--help" || arg === "-h") {
182
+ result.help = true;
183
+ } else if (arg === "--no-install") {
184
+ result.install = false;
185
+ } else if (arg === "--open" || arg === "-o") {
186
+ result.open = true;
187
+ } else if (arg === "--yes" || arg === "-y") {
188
+ result.yes = true;
189
+ } else if (arg === "--force" || arg === "-f") {
190
+ result.force = true;
191
+ } else if (arg === "--dir" || arg === "-d") {
192
+ result.directory = args[++i];
193
+ } else if (arg === "--template" || arg === "-t") {
194
+ result.template = args[++i];
195
+ } else if (!arg.startsWith("-") && !result.projectName) {
196
+ result.projectName = arg;
197
+ }
198
+ }
199
+ return result;
200
+ }
201
+ function showHelp() {
202
+ console.log(`
203
+ ${pc2.dim("Usage:")}
204
+ lumera init [name] [options]
205
+
206
+ ${pc2.dim("Description:")}
207
+ Scaffold a new Lumera project from a template.
208
+
209
+ ${pc2.dim("Options:")}
210
+ --template, -t <name> Template to use (run ${pc2.cyan("lumera templates")} to see options)
211
+ --yes, -y Non-interactive mode (requires project name)
212
+ --dir, -d <path> Target directory (defaults to project name)
213
+ --force, -f Overwrite existing directory without prompting
214
+ --no-install Skip dependency installation
215
+ --open, -o Open project in editor after scaffolding
216
+ --help, -h Show this help
217
+
218
+ ${pc2.dim("Examples:")}
219
+ lumera init my-app # Interactive mode
220
+ lumera init my-app -t invoice-processing # Use a specific template
221
+ lumera init my-app -y # Non-interactive (default template)
222
+ lumera init my-app -t invoice-processing -y -o # Full non-interactive + open editor
223
+ `);
224
+ }
225
+ async function init(args) {
226
+ const opts = parseArgs(args);
227
+ if (opts.help) {
228
+ showHelp();
229
+ return;
230
+ }
231
+ console.log();
232
+ console.log(pc2.cyan(pc2.bold(" Create Lumera App")));
233
+ console.log();
234
+ let projectName = opts.projectName;
235
+ let directory = opts.directory;
236
+ const nonInteractive = opts.yes;
237
+ if (nonInteractive && !projectName) {
238
+ console.log(pc2.red(" Error: Project name is required in non-interactive mode"));
239
+ console.log(pc2.dim(" Usage: lumera init <name> -y"));
240
+ process.exit(1);
241
+ }
242
+ let templateName = opts.template;
243
+ if (!templateName && !nonInteractive) {
244
+ try {
245
+ const stop = spinner("Fetching templates...");
246
+ const available = await listAllTemplates();
247
+ stop();
248
+ if (available.length > 1) {
249
+ const response = await prompts({
250
+ type: "select",
251
+ name: "template",
252
+ message: "Choose a template",
253
+ choices: available.map((t) => ({
254
+ title: `${t.title} ${pc2.dim(`(${t.name})`)}`,
255
+ description: t.description,
256
+ value: t.name
257
+ })),
258
+ initial: 0
259
+ });
260
+ if (!response.template) {
261
+ console.log(pc2.red("Cancelled"));
262
+ process.exit(1);
263
+ }
264
+ templateName = response.template;
265
+ }
266
+ } catch {
267
+ }
268
+ }
269
+ if (!templateName) {
270
+ templateName = "default";
271
+ }
272
+ const stopResolve = spinner("Resolving template...");
273
+ const templateDir = await resolveTemplate(templateName);
274
+ stopResolve();
275
+ if (!projectName) {
276
+ const response = await prompts({
277
+ type: "text",
278
+ name: "projectName",
279
+ message: "What is your project name?",
280
+ initial: "my-lumera-app",
281
+ validate: (value) => {
282
+ if (!value) return "Project name is required";
283
+ if (!/^[a-z0-9-]+$/.test(value)) {
284
+ return "Use lowercase letters, numbers, and hyphens only";
285
+ }
286
+ return true;
287
+ }
288
+ });
289
+ if (!response.projectName) {
290
+ console.log(pc2.red("Cancelled"));
291
+ process.exit(1);
292
+ }
293
+ projectName = response.projectName;
294
+ }
295
+ if (!/^[a-z0-9-]+$/.test(projectName)) {
296
+ console.log(pc2.red(" Error: Project name must use lowercase letters, numbers, and hyphens only"));
297
+ process.exit(1);
298
+ }
299
+ if (!directory) {
300
+ if (nonInteractive) {
301
+ directory = projectName;
302
+ } else {
303
+ const response = await prompts({
304
+ type: "text",
305
+ name: "directory",
306
+ message: "Where should we create the project?",
307
+ initial: projectName
308
+ });
309
+ if (!response.directory) {
310
+ console.log(pc2.red("Cancelled"));
311
+ process.exit(1);
312
+ }
313
+ directory = response.directory;
314
+ }
315
+ }
316
+ const projectTitle = toTitleCase(projectName);
317
+ const targetDir = resolve(process.cwd(), directory);
318
+ if (existsSync(targetDir)) {
319
+ if (nonInteractive) {
320
+ if (opts.force) {
321
+ rmSync(targetDir, { recursive: true });
322
+ } else {
323
+ console.log(pc2.red(` Error: Directory ${directory} already exists`));
324
+ console.log(pc2.dim(" Use --force (-f) to overwrite"));
325
+ process.exit(1);
326
+ }
327
+ } else {
328
+ const { overwrite } = await prompts({
329
+ type: "confirm",
330
+ name: "overwrite",
331
+ message: `Directory ${directory} already exists. Overwrite?`,
332
+ initial: false
333
+ });
334
+ if (!overwrite) {
335
+ console.log(pc2.red("Cancelled"));
336
+ process.exit(1);
337
+ }
338
+ rmSync(targetDir, { recursive: true });
339
+ }
340
+ }
341
+ mkdirSync(targetDir, { recursive: true });
342
+ console.log();
343
+ if (templateName !== "default") {
344
+ console.log(pc2.dim(` Creating ${projectName} from template ${pc2.cyan(templateName)}...`));
345
+ } else {
346
+ console.log(pc2.dim(` Creating ${projectName}...`));
347
+ }
348
+ console.log();
349
+ const vars = {
350
+ projectName,
351
+ projectTitle
352
+ };
353
+ copyDir(templateDir, targetDir, vars);
354
+ function listFiles(dir, prefix = "") {
355
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
356
+ const relativePath = prefix + entry.name;
357
+ if (entry.isDirectory()) {
358
+ listFiles(join(dir, entry.name), relativePath + "/");
359
+ } else {
360
+ console.log(pc2.green(" \u2713"), pc2.dim(relativePath));
361
+ }
362
+ }
363
+ }
364
+ listFiles(targetDir);
365
+ if (isGitInstalled()) {
366
+ const stopGit = spinner("Initializing git repository...");
367
+ if (initGitRepo(targetDir, projectName)) {
368
+ stopGit(pc2.green("\u2713") + pc2.dim(" Git repository initialized with initial commit"));
369
+ } else {
370
+ stopGit(pc2.yellow("\u26A0") + pc2.dim(" Failed to initialize git repository"));
371
+ }
372
+ } else {
373
+ console.log(pc2.yellow(" \u26A0"), pc2.dim("Git not found \u2014 skipping repository initialization"));
374
+ }
375
+ let uvAvailable = isUvInstalled();
376
+ if (!uvAvailable) {
377
+ const stopUv = spinner("Installing uv (Python package manager)...");
378
+ if (installUv()) {
379
+ stopUv(pc2.green("\u2713") + pc2.dim(" uv installed successfully"));
380
+ uvAvailable = true;
381
+ } else {
382
+ stopUv(pc2.yellow("\u26A0") + pc2.dim(" Failed to install uv \u2014 install manually: https://docs.astral.sh/uv/"));
383
+ }
384
+ }
385
+ if (uvAvailable) {
386
+ const stopVenv = spinner("Creating Python venv with Lumera SDK...");
387
+ if (createPythonVenv(targetDir)) {
388
+ stopVenv(pc2.green("\u2713") + pc2.dim(" Python venv created (.venv/) with lumera SDK"));
389
+ } else {
390
+ stopVenv(pc2.yellow("\u26A0") + pc2.dim(" Failed to create Python venv"));
391
+ }
392
+ }
393
+ if (opts.install) {
394
+ const stopInstall = spinner("Installing dependencies...");
395
+ try {
396
+ execSync("pnpm install", { cwd: targetDir, stdio: "ignore" });
397
+ stopInstall(pc2.green("\u2713") + pc2.dim(" Dependencies installed"));
398
+ } catch {
399
+ stopInstall(pc2.yellow("\u26A0") + pc2.dim(" Failed to install dependencies"));
400
+ }
401
+ }
402
+ const stopSkills = spinner("Installing Lumera skills for AI agents...");
403
+ try {
404
+ const { installed, failed } = await installAllSkills(targetDir);
405
+ if (failed > 0) {
406
+ stopSkills(pc2.yellow("\u26A0") + pc2.dim(` Installed ${installed} skills (${failed} failed)`));
407
+ } else {
408
+ stopSkills(pc2.green("\u2713") + pc2.dim(` ${installed} Lumera skills installed`));
409
+ }
410
+ syncClaudeMd(targetDir);
411
+ if (isGitInstalled()) {
412
+ try {
413
+ execSync('git add -A && git commit -m "chore: install lumera skills"', {
414
+ cwd: targetDir,
415
+ stdio: "ignore"
416
+ });
417
+ } catch {
418
+ }
419
+ }
420
+ } catch (err) {
421
+ stopSkills(pc2.yellow("\u26A0") + pc2.dim(` Failed to install skills: ${err}`));
422
+ }
423
+ console.log();
424
+ console.log(pc2.green(pc2.bold(" Done!")), "Next steps:");
425
+ console.log();
426
+ console.log(pc2.cyan(` cd ${directory}`));
427
+ if (!opts.install) {
428
+ console.log(pc2.cyan(" pnpm install"));
429
+ }
430
+ console.log(pc2.cyan(" lumera login"));
431
+ console.log(pc2.cyan(" lumera dev"));
432
+ console.log();
433
+ if (opts.open) {
434
+ openInEditor(targetDir);
435
+ console.log();
436
+ }
437
+ }
438
+ export {
439
+ init
440
+ };
@@ -2,11 +2,9 @@ import {
2
2
  deploy
3
3
  } from "./chunk-CDZZ3JYU.js";
4
4
  import {
5
- createApiClient
6
- } from "./chunk-WRAZC6SJ.js";
7
- import {
5
+ createApiClient,
8
6
  loadEnv
9
- } from "./chunk-2CR762KB.js";
7
+ } from "./chunk-HIYM7EM2.js";
10
8
  import {
11
9
  getToken
12
10
  } from "./chunk-NDLYGKS6.js";
@@ -1,9 +1,7 @@
1
1
  import {
2
- createApiClient
3
- } from "./chunk-WRAZC6SJ.js";
4
- import {
2
+ createApiClient,
5
3
  loadEnv
6
- } from "./chunk-2CR762KB.js";
4
+ } from "./chunk-HIYM7EM2.js";
7
5
  import {
8
6
  getToken
9
7
  } from "./chunk-NDLYGKS6.js";
@@ -0,0 +1,194 @@
1
+ import {
2
+ listAllTemplates
3
+ } from "./chunk-CHRKCAIZ.js";
4
+
5
+ // src/commands/templates.ts
6
+ import pc from "picocolors";
7
+ import { existsSync, readdirSync, readFileSync } from "fs";
8
+ import { join, resolve, extname } from "path";
9
+ function showHelp() {
10
+ console.log(`
11
+ ${pc.dim("Usage:")}
12
+ lumera templates <command> [options]
13
+
14
+ ${pc.dim("Commands:")}
15
+ list, ls List available templates (default)
16
+ validate [dir] Validate a template directory
17
+
18
+ ${pc.dim("Options:")}
19
+ --verbose, -v Show full descriptions (list)
20
+ --help, -h Show this help
21
+
22
+ ${pc.dim("Examples:")}
23
+ lumera templates # List available templates
24
+ lumera templates list -v # Show with descriptions
25
+ lumera templates validate ./my-template # Validate a template dir
26
+ lumera init my-app -t invoice-processing # Use a template
27
+ `);
28
+ }
29
+ async function list(args) {
30
+ if (args.includes("--help") || args.includes("-h")) {
31
+ showHelp();
32
+ return;
33
+ }
34
+ const verbose = args.includes("--verbose") || args.includes("-v");
35
+ try {
36
+ const allTemplates = await listAllTemplates();
37
+ if (process.env.LUMERA_JSON) {
38
+ console.log(JSON.stringify(allTemplates, null, 2));
39
+ return;
40
+ }
41
+ console.log();
42
+ console.log(pc.cyan(pc.bold(" Available Templates")));
43
+ console.log();
44
+ if (allTemplates.length === 0) {
45
+ console.log(pc.dim(" No templates available."));
46
+ console.log();
47
+ return;
48
+ }
49
+ const byCategory = /* @__PURE__ */ new Map();
50
+ for (const t of allTemplates) {
51
+ const cat = t.category || "General";
52
+ if (!byCategory.has(cat)) byCategory.set(cat, []);
53
+ byCategory.get(cat).push(t);
54
+ }
55
+ for (const [category, items] of byCategory) {
56
+ console.log(pc.bold(` ${category}`));
57
+ console.log();
58
+ for (const t of items) {
59
+ console.log(` ${pc.green(t.title)} ${pc.dim(`(${t.name})`)}`);
60
+ if (verbose) {
61
+ console.log(` ${pc.dim(t.description)}`);
62
+ console.log();
63
+ }
64
+ }
65
+ if (!verbose) console.log();
66
+ }
67
+ console.log(pc.dim(` ${allTemplates.length} template${allTemplates.length === 1 ? "" : "s"} available.`));
68
+ console.log();
69
+ console.log(pc.dim(" Usage:"), pc.cyan("lumera init <name> --template <template-name>"));
70
+ console.log();
71
+ } catch (err) {
72
+ console.error(pc.red(` Error: ${err}`));
73
+ process.exit(1);
74
+ }
75
+ }
76
+ var SKIP_DIRS = /* @__PURE__ */ new Set(["node_modules", ".git", ".venv", "dist", "__pycache__", ".tanstack"]);
77
+ var TEXT_EXTS = /* @__PURE__ */ new Set([".json", ".ts", ".tsx", ".js", ".jsx", ".py", ".md", ".html", ".css", ".yaml", ".yml", ".toml"]);
78
+ function scanForPlaceholders(dir, prefix = "") {
79
+ const results = [];
80
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
81
+ if (SKIP_DIRS.has(entry.name)) continue;
82
+ const fullPath = join(dir, entry.name);
83
+ const relativePath = prefix ? `${prefix}/${entry.name}` : entry.name;
84
+ if (entry.isDirectory()) {
85
+ results.push(...scanForPlaceholders(fullPath, relativePath));
86
+ } else {
87
+ const ext = extname(entry.name);
88
+ if (!TEXT_EXTS.has(ext)) continue;
89
+ if (entry.name === "template.json") continue;
90
+ try {
91
+ const content = readFileSync(fullPath, "utf-8");
92
+ const lines = content.split("\n");
93
+ for (let i = 0; i < lines.length; i++) {
94
+ const matches = lines[i].match(/\{\{[^}]+\}\}/g);
95
+ if (matches) {
96
+ for (const match of matches) {
97
+ if (match.includes(":")) continue;
98
+ results.push({ file: relativePath, line: i + 1, text: match });
99
+ }
100
+ }
101
+ }
102
+ } catch {
103
+ }
104
+ }
105
+ }
106
+ return results;
107
+ }
108
+ async function validate(args) {
109
+ const dir = args.find((a) => !a.startsWith("-")) || ".";
110
+ const targetDir = resolve(process.cwd(), dir);
111
+ console.log();
112
+ console.log(pc.cyan(pc.bold(" Validate Template")));
113
+ console.log(pc.dim(` Directory: ${targetDir}`));
114
+ console.log();
115
+ const issues = [];
116
+ const warnings = [];
117
+ const templateJsonPath = join(targetDir, "template.json");
118
+ if (!existsSync(templateJsonPath)) {
119
+ issues.push("Missing template.json");
120
+ } else {
121
+ try {
122
+ const meta = JSON.parse(readFileSync(templateJsonPath, "utf-8"));
123
+ const required = ["name", "title", "description", "category"];
124
+ for (const field of required) {
125
+ if (!meta[field]) {
126
+ issues.push(`template.json: missing required field "${field}"`);
127
+ }
128
+ }
129
+ } catch {
130
+ issues.push("template.json: invalid JSON");
131
+ }
132
+ }
133
+ if (!existsSync(join(targetDir, "package.json"))) {
134
+ issues.push("Missing package.json");
135
+ }
136
+ if (!existsSync(join(targetDir, "platform"))) {
137
+ warnings.push("Missing platform/ directory");
138
+ } else if (!existsSync(join(targetDir, "platform", "collections"))) {
139
+ warnings.push("platform/ has no collections/ subdirectory");
140
+ }
141
+ const stalePatterns = scanForPlaceholders(targetDir);
142
+ for (const match of stalePatterns) {
143
+ warnings.push(`Stale placeholder: ${match.file}:${match.line} \u2014 ${match.text}`);
144
+ }
145
+ if (issues.length === 0 && warnings.length === 0) {
146
+ console.log(pc.green(" \u2713 Template is valid"));
147
+ console.log();
148
+ return;
149
+ }
150
+ if (issues.length > 0) {
151
+ console.log(pc.red(` ${issues.length} error${issues.length > 1 ? "s" : ""}:`));
152
+ for (const issue of issues) {
153
+ console.log(pc.red(` \u2717 ${issue}`));
154
+ }
155
+ console.log();
156
+ }
157
+ if (warnings.length > 0) {
158
+ console.log(pc.yellow(` ${warnings.length} warning${warnings.length > 1 ? "s" : ""}:`));
159
+ for (const warning of warnings) {
160
+ console.log(pc.yellow(` \u26A0 ${warning}`));
161
+ }
162
+ console.log();
163
+ }
164
+ if (issues.length > 0) {
165
+ process.exit(1);
166
+ }
167
+ }
168
+ async function templates(subcommand, args) {
169
+ if (subcommand === "--help" || subcommand === "-h") {
170
+ showHelp();
171
+ return;
172
+ }
173
+ const cmd = subcommand || "list";
174
+ switch (cmd) {
175
+ case "list":
176
+ case "ls":
177
+ await list(args);
178
+ break;
179
+ case "validate":
180
+ await validate(args);
181
+ break;
182
+ default:
183
+ if (cmd.startsWith("-")) {
184
+ await list([cmd, ...args]);
185
+ break;
186
+ }
187
+ console.error(pc.red(`Unknown templates command: ${cmd}`));
188
+ console.error(`Run ${pc.cyan("lumera templates --help")} for usage.`);
189
+ process.exit(1);
190
+ }
191
+ }
192
+ export {
193
+ templates
194
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {
@@ -1,18 +0,0 @@
1
- // src/lib/env.ts
2
- import { config } from "dotenv";
3
- import { existsSync } from "fs";
4
- import { resolve } from "path";
5
- function loadEnv(cwd = process.cwd()) {
6
- const envPath = resolve(cwd, ".env");
7
- const envLocalPath = resolve(cwd, ".env.local");
8
- if (existsSync(envPath)) {
9
- config({ path: envPath });
10
- }
11
- if (existsSync(envLocalPath)) {
12
- config({ path: envLocalPath, override: true });
13
- }
14
- }
15
-
16
- export {
17
- loadEnv
18
- };