@lumerahq/cli 0.10.1 → 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.
@@ -36,7 +36,7 @@ async function downloadAndExtract(commitSha) {
36
36
  writeFileSync(tmpFile, buffer);
37
37
  mkdirSync(cacheDir, { recursive: true });
38
38
  for (const entry of readdirSync(cacheDir)) {
39
- if (entry === ".cache-meta.json") continue;
39
+ if (entry === ".cache-meta.json" || entry === ".refs") continue;
40
40
  rmSync(join(cacheDir, entry), { recursive: true, force: true });
41
41
  }
42
42
  const result = spawnSync("tar", ["xzf", tmpFile, "-C", cacheDir, "--strip-components=1"], { stdio: "ignore" });
@@ -56,6 +56,51 @@ async function ensureRemoteCache() {
56
56
  console.log(" Fetching latest templates...");
57
57
  return downloadAndExtract(latestSha);
58
58
  }
59
+ function parseTemplateRef(input) {
60
+ const atIndex = input.lastIndexOf("@");
61
+ if (atIndex > 0) {
62
+ return {
63
+ name: input.slice(0, atIndex),
64
+ ref: input.slice(atIndex + 1)
65
+ };
66
+ }
67
+ return { name: input, ref: null };
68
+ }
69
+ async function ensureRemoteCacheForRef(ref) {
70
+ const refCacheDir = join(getCacheDir(), ".refs", ref);
71
+ if (existsSync(refCacheDir) && existsSync(join(refCacheDir, ".cache-meta.json"))) {
72
+ return refCacheDir;
73
+ }
74
+ console.log(` Fetching templates at ref "${ref}"...`);
75
+ const urls = [
76
+ `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/archive/refs/tags/${ref}.tar.gz`,
77
+ `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/archive/refs/heads/${ref}.tar.gz`,
78
+ `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/archive/${ref}.tar.gz`
79
+ ];
80
+ let res = null;
81
+ for (const url of urls) {
82
+ const attempt = await fetch(url);
83
+ if (attempt.ok) {
84
+ res = attempt;
85
+ break;
86
+ }
87
+ }
88
+ if (!res) {
89
+ throw new Error(
90
+ `Failed to fetch templates at ref "${ref}". Ensure the tag, branch, or SHA exists in ${GITHUB_OWNER}/${GITHUB_REPO}.`
91
+ );
92
+ }
93
+ const tmpFile = join(tmpdir(), `lumera-templates-${ref}-${Date.now()}.tar.gz`);
94
+ const buffer = Buffer.from(await res.arrayBuffer());
95
+ writeFileSync(tmpFile, buffer);
96
+ mkdirSync(refCacheDir, { recursive: true });
97
+ const result = spawnSync("tar", ["xzf", tmpFile, "-C", refCacheDir, "--strip-components=1"], { stdio: "ignore" });
98
+ if (result.status !== 0) throw new Error("Failed to extract template archive");
99
+ unlinkSync(tmpFile);
100
+ const meta = { commitSha: ref, fetchedAt: (/* @__PURE__ */ new Date()).toISOString() };
101
+ writeFileSync(join(refCacheDir, ".cache-meta.json"), JSON.stringify(meta));
102
+ return refCacheDir;
103
+ }
59
104
  function listTemplates(baseDir) {
60
105
  if (!existsSync(baseDir)) return [];
61
106
  const templates = [];
@@ -80,11 +125,17 @@ function listTemplates(baseDir) {
80
125
  return a.title.localeCompare(b.title);
81
126
  });
82
127
  }
83
- async function resolveTemplate(name) {
84
- const cacheDir = await ensureRemoteCache();
128
+ async function resolveTemplate(nameWithRef) {
129
+ const { name, ref } = parseTemplateRef(nameWithRef);
130
+ const cacheDir = ref ? await ensureRemoteCacheForRef(ref) : await ensureRemoteCache();
85
131
  const dir = join(cacheDir, name);
86
132
  if (!existsSync(dir) || !existsSync(join(dir, "template.json"))) {
87
133
  const available = listTemplates(cacheDir).map((t) => t.name).join(", ");
134
+ if (ref) {
135
+ throw new Error(
136
+ `Template "${name}" not found at ref "${ref}". Available: ${available || "none"}`
137
+ );
138
+ }
88
139
  throw new Error(
89
140
  `Template "${name}" not found. Available: ${available || "none"}
90
141
  Cache location: ${cacheDir}
@@ -143,6 +143,22 @@ function createApiClient(token, baseUrl) {
143
143
  return new ApiClient(token, baseUrl);
144
144
  }
145
145
 
146
+ // src/lib/env.ts
147
+ import { config } from "dotenv";
148
+ import { existsSync } from "fs";
149
+ import { resolve } from "path";
150
+ function loadEnv(cwd = process.cwd()) {
151
+ const envPath = resolve(cwd, ".env");
152
+ const envLocalPath = resolve(cwd, ".env.local");
153
+ if (existsSync(envPath)) {
154
+ config({ path: envPath });
155
+ }
156
+ if (existsSync(envLocalPath)) {
157
+ config({ path: envLocalPath, override: true });
158
+ }
159
+ }
160
+
146
161
  export {
147
- createApiClient
162
+ createApiClient,
163
+ loadEnv
148
164
  };
@@ -0,0 +1,155 @@
1
+ import {
2
+ dev
3
+ } from "./chunk-CDZZ3JYU.js";
4
+ import {
5
+ createApiClient,
6
+ loadEnv
7
+ } from "./chunk-HIYM7EM2.js";
8
+ import {
9
+ getToken
10
+ } from "./chunk-NDLYGKS6.js";
11
+ import {
12
+ findProjectRoot,
13
+ getApiUrl,
14
+ getAppName,
15
+ getAppTitle
16
+ } from "./chunk-D2BLSEGR.js";
17
+
18
+ // src/commands/dev.ts
19
+ import pc from "picocolors";
20
+ import { execFileSync, execSync } from "child_process";
21
+ import { existsSync } from "fs";
22
+ import { join } from "path";
23
+ function parseFlags(args) {
24
+ const result = {};
25
+ for (let i = 0; i < args.length; i++) {
26
+ const arg = args[i];
27
+ if (arg.startsWith("--")) {
28
+ const key = arg.slice(2);
29
+ const next = args[i + 1];
30
+ if (next && !next.startsWith("--")) {
31
+ result[key] = next;
32
+ i++;
33
+ } else {
34
+ result[key] = true;
35
+ }
36
+ }
37
+ }
38
+ return result;
39
+ }
40
+ function showHelp() {
41
+ console.log(`
42
+ ${pc.dim("Usage:")}
43
+ lumera dev [options]
44
+
45
+ ${pc.dim("Description:")}
46
+ Start the development server with Lumera registration.
47
+ Registers your app with Lumera and starts a local dev server.
48
+ On first run, automatically applies resources and seeds demo data.
49
+
50
+ ${pc.dim("Options:")}
51
+ --port <number> Dev server port (default: 8080)
52
+ --url <url> App URL for dev mode (default: http://localhost:{port})
53
+ --skip-setup Skip auto-apply on first run
54
+ --help, -h Show this help
55
+
56
+ ${pc.dim("Environment variables:")}
57
+ LUMERA_TOKEN API token (overrides \`lumera login\`)
58
+ LUMERA_API_URL API base URL (default: https://app.lumerahq.com/api)
59
+ PORT Dev server port (default: 8080)
60
+ APP_URL App URL for dev mode
61
+
62
+ ${pc.dim("Examples:")}
63
+ lumera dev # Start dev server on port 8080
64
+ lumera dev --port 3000 # Start dev server on port 3000
65
+ lumera dev --skip-setup # Skip first-run auto-apply
66
+ `);
67
+ }
68
+ async function isFreshProject(projectRoot) {
69
+ try {
70
+ const api = createApiClient();
71
+ const remoteCollections = await api.listCollections();
72
+ const customCollections = remoteCollections.filter((c) => !c.system && !c.managed);
73
+ const platformDir = existsSync(join(projectRoot, "platform")) ? join(projectRoot, "platform") : join(projectRoot, "lumera_platform");
74
+ const collectionsDir = join(platformDir, "collections");
75
+ const hasLocalCollections = existsSync(collectionsDir);
76
+ return customCollections.length === 0 && hasLocalCollections;
77
+ } catch {
78
+ return false;
79
+ }
80
+ }
81
+ function runApply(projectRoot) {
82
+ try {
83
+ execFileSync(process.execPath, [...process.execArgv, process.argv[1], "apply"], {
84
+ cwd: projectRoot,
85
+ stdio: "inherit",
86
+ env: process.env
87
+ });
88
+ return true;
89
+ } catch {
90
+ return false;
91
+ }
92
+ }
93
+ function runSeed(projectRoot) {
94
+ const seedScript = join(projectRoot, "scripts", "seed-demo.py");
95
+ if (!existsSync(seedScript)) return false;
96
+ try {
97
+ execSync(`uv run "${seedScript}"`, {
98
+ cwd: projectRoot,
99
+ stdio: "inherit",
100
+ env: process.env
101
+ });
102
+ return true;
103
+ } catch {
104
+ return false;
105
+ }
106
+ }
107
+ async function dev2(args) {
108
+ if (args.includes("--help") || args.includes("-h")) {
109
+ showHelp();
110
+ return;
111
+ }
112
+ const flags = parseFlags(args);
113
+ const projectRoot = findProjectRoot();
114
+ loadEnv(projectRoot);
115
+ const token = getToken(projectRoot);
116
+ const appName = getAppName(projectRoot);
117
+ const appTitle = getAppTitle(projectRoot);
118
+ const apiUrl = getApiUrl();
119
+ if (!flags["skip-setup"]) {
120
+ const fresh = await isFreshProject(projectRoot);
121
+ if (fresh) {
122
+ console.log();
123
+ console.log(pc.cyan(pc.bold(" First run detected")), pc.dim("\u2014 applying resources and seeding data..."));
124
+ console.log();
125
+ const applyOk = runApply(projectRoot);
126
+ if (!applyOk) {
127
+ console.log(pc.yellow(" \u26A0 Some resources failed to apply (continuing)"));
128
+ }
129
+ const seedScript = join(projectRoot, "scripts", "seed-demo.py");
130
+ if (existsSync(seedScript)) {
131
+ console.log();
132
+ console.log(pc.dim(" Running seed script..."));
133
+ if (runSeed(projectRoot)) {
134
+ console.log(pc.green(" \u2713"), pc.dim("Seed data applied"));
135
+ } else {
136
+ console.log(pc.yellow(" \u26A0"), pc.dim("Seed script failed (continuing)"));
137
+ }
138
+ }
139
+ console.log();
140
+ }
141
+ }
142
+ const port = Number(flags.port || process.env.PORT || "8080");
143
+ const appUrl = typeof flags.url === "string" ? flags.url : process.env.APP_URL;
144
+ await dev({
145
+ token,
146
+ appName,
147
+ appTitle,
148
+ port,
149
+ appUrl,
150
+ apiUrl
151
+ });
152
+ }
153
+ export {
154
+ dev2 as dev
155
+ };
package/dist/index.js CHANGED
@@ -1,16 +1,127 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  // src/index.ts
4
- import { readFileSync } from "fs";
5
- import { dirname, join } from "path";
4
+ import { readFileSync as readFileSync2 } from "fs";
5
+ import { dirname, join as join2 } from "path";
6
6
  import { fileURLToPath } from "url";
7
7
  import pc from "picocolors";
8
+
9
+ // src/lib/update-check.ts
10
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
11
+ import { homedir } from "os";
12
+ import { join } from "path";
13
+ var CHECK_INTERVAL_MS = 24 * 60 * 60 * 1e3;
14
+ function getCachePath() {
15
+ return join(homedir(), ".lumera", "update-check.json");
16
+ }
17
+ function readCache() {
18
+ const cachePath = getCachePath();
19
+ if (!existsSync(cachePath)) return null;
20
+ try {
21
+ return JSON.parse(readFileSync(cachePath, "utf-8"));
22
+ } catch {
23
+ return null;
24
+ }
25
+ }
26
+ function writeCache(data) {
27
+ const dir = join(homedir(), ".lumera");
28
+ mkdirSync(dir, { recursive: true });
29
+ writeFileSync(getCachePath(), JSON.stringify(data));
30
+ }
31
+ function isNewer(latest, current) {
32
+ const l = latest.split(".").map(Number);
33
+ const c = current.split(".").map(Number);
34
+ for (let i = 0; i < 3; i++) {
35
+ if ((l[i] || 0) > (c[i] || 0)) return true;
36
+ if ((l[i] || 0) < (c[i] || 0)) return false;
37
+ }
38
+ return false;
39
+ }
40
+ async function checkForUpdate(currentVersion) {
41
+ try {
42
+ if (process.env.CI || process.env.LUMERA_NO_UPDATE_CHECK) return null;
43
+ const cache = readCache();
44
+ if (cache) {
45
+ const age = Date.now() - new Date(cache.lastCheck).getTime();
46
+ if (age < CHECK_INTERVAL_MS) {
47
+ if (isNewer(cache.latestVersion, currentVersion)) {
48
+ return { current: currentVersion, latest: cache.latestVersion };
49
+ }
50
+ return null;
51
+ }
52
+ }
53
+ const res = await fetch("https://registry.npmjs.org/@lumerahq/cli/latest");
54
+ if (!res.ok) return null;
55
+ const data = await res.json();
56
+ const latest = data.version;
57
+ writeCache({ lastCheck: (/* @__PURE__ */ new Date()).toISOString(), latestVersion: latest });
58
+ if (isNewer(latest, currentVersion)) {
59
+ return { current: currentVersion, latest };
60
+ }
61
+ return null;
62
+ } catch {
63
+ return null;
64
+ }
65
+ }
66
+
67
+ // src/index.ts
8
68
  var __dirname = dirname(fileURLToPath(import.meta.url));
9
- var pkg = JSON.parse(readFileSync(join(__dirname, "..", "package.json"), "utf-8"));
69
+ var pkg = JSON.parse(readFileSync2(join2(__dirname, "..", "package.json"), "utf-8"));
10
70
  var VERSION = pkg.version;
11
- var args = process.argv.slice(2);
71
+ var rawArgs = process.argv.slice(2);
72
+ var jsonMode = rawArgs.includes("--json");
73
+ if (jsonMode) process.env.LUMERA_JSON = "1";
74
+ var args = rawArgs.filter((a) => a !== "--json");
12
75
  var command = args[0];
13
76
  var subcommand = args[1];
77
+ var COMMANDS = [
78
+ "plan",
79
+ "apply",
80
+ "pull",
81
+ "destroy",
82
+ "list",
83
+ "show",
84
+ "dev",
85
+ "run",
86
+ "init",
87
+ "templates",
88
+ "status",
89
+ "migrate",
90
+ "skills",
91
+ "login",
92
+ "logout",
93
+ "whoami"
94
+ ];
95
+ function editDistance(a, b) {
96
+ const m = a.length;
97
+ const n = b.length;
98
+ const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
99
+ for (let i = 0; i <= m; i++) dp[i][0] = i;
100
+ for (let j = 0; j <= n; j++) dp[0][j] = j;
101
+ for (let i = 1; i <= m; i++) {
102
+ for (let j = 1; j <= n; j++) {
103
+ dp[i][j] = a[i - 1] === b[j - 1] ? dp[i - 1][j - 1] : 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
104
+ }
105
+ }
106
+ return dp[m][n];
107
+ }
108
+ function suggestCommand(input) {
109
+ let best = "";
110
+ let bestDist = Infinity;
111
+ for (const cmd of COMMANDS) {
112
+ const d = editDistance(input.toLowerCase(), cmd);
113
+ if (d < bestDist) {
114
+ bestDist = d;
115
+ best = cmd;
116
+ }
117
+ }
118
+ const threshold = Math.max(2, Math.ceil(input.length * 0.4));
119
+ return bestDist <= threshold ? best : null;
120
+ }
121
+ function formatElapsed(ms) {
122
+ if (ms < 1e3) return `${Math.round(ms)}ms`;
123
+ return `${(ms / 1e3).toFixed(1)}s`;
124
+ }
14
125
  function showHelp() {
15
126
  console.log(`
16
127
  ${pc.bold("Lumera CLI")} - Build and deploy Lumera apps
@@ -32,7 +143,8 @@ ${pc.dim("Development:")}
32
143
 
33
144
  ${pc.dim("Project:")}
34
145
  ${pc.cyan("init")} [name] Scaffold a new project
35
- ${pc.cyan("templates")} List available templates
146
+ ${pc.cyan("templates list")} List available templates
147
+ ${pc.cyan("templates validate")} Validate a template directory
36
148
  ${pc.cyan("status")} Show project info
37
149
  ${pc.cyan("migrate")} Upgrade legacy project
38
150
 
@@ -49,6 +161,7 @@ ${pc.dim("Auth:")}
49
161
  ${pc.dim("Options:")}
50
162
  --help, -h Show help
51
163
  --version, -v Show version
164
+ --json Output as JSON (where supported)
52
165
 
53
166
  ${pc.dim("Resource Types:")}
54
167
  collections Data collections (schemas)
@@ -89,40 +202,42 @@ async function main() {
89
202
  process.exit(1);
90
203
  }
91
204
  }
205
+ const startTime = performance.now();
206
+ const updateCheck = checkForUpdate(VERSION);
92
207
  try {
93
208
  switch (command) {
94
209
  // Resource commands
95
210
  case "plan":
96
- await import("./resources-PGBVCS2K.js").then((m) => m.plan(args.slice(1)));
211
+ await import("./resources-GTG3QMVV.js").then((m) => m.plan(args.slice(1)));
97
212
  break;
98
213
  case "apply":
99
- await import("./resources-PGBVCS2K.js").then((m) => m.apply(args.slice(1)));
214
+ await import("./resources-GTG3QMVV.js").then((m) => m.apply(args.slice(1)));
100
215
  break;
101
216
  case "pull":
102
- await import("./resources-PGBVCS2K.js").then((m) => m.pull(args.slice(1)));
217
+ await import("./resources-GTG3QMVV.js").then((m) => m.pull(args.slice(1)));
103
218
  break;
104
219
  case "destroy":
105
- await import("./resources-PGBVCS2K.js").then((m) => m.destroy(args.slice(1)));
220
+ await import("./resources-GTG3QMVV.js").then((m) => m.destroy(args.slice(1)));
106
221
  break;
107
222
  case "list":
108
- await import("./resources-PGBVCS2K.js").then((m) => m.list(args.slice(1)));
223
+ await import("./resources-GTG3QMVV.js").then((m) => m.list(args.slice(1)));
109
224
  break;
110
225
  case "show":
111
- await import("./resources-PGBVCS2K.js").then((m) => m.show(args.slice(1)));
226
+ await import("./resources-GTG3QMVV.js").then((m) => m.show(args.slice(1)));
112
227
  break;
113
228
  // Development
114
229
  case "dev":
115
- await import("./dev-BHBF4ECH.js").then((m) => m.dev(args.slice(1)));
230
+ await import("./dev-2HVDP3NX.js").then((m) => m.dev(args.slice(1)));
116
231
  break;
117
232
  case "run":
118
- await import("./run-WIRQDYYX.js").then((m) => m.run(args.slice(1)));
233
+ await import("./run-47GF5VVS.js").then((m) => m.run(args.slice(1)));
119
234
  break;
120
235
  // Project
121
236
  case "init":
122
- await import("./init-OH433IPH.js").then((m) => m.init(args.slice(1)));
237
+ await import("./init-VNJNSU4Q.js").then((m) => m.init(args.slice(1)));
123
238
  break;
124
239
  case "templates":
125
- await import("./templates-67O6PVFK.js").then((m) => m.templates(args.slice(1)));
240
+ await import("./templates-6KMZWOYH.js").then((m) => m.templates(subcommand, args.slice(2)));
126
241
  break;
127
242
  case "status":
128
243
  await import("./status-E4IHEUKO.js").then((m) => m.status(args.slice(1)));
@@ -144,10 +259,43 @@ async function main() {
144
259
  case "whoami":
145
260
  await import("./auth-7RGL7GXU.js").then((m) => m.whoami());
146
261
  break;
147
- default:
262
+ // Convenience aliases
263
+ case "help":
264
+ showHelp();
265
+ break;
266
+ case "version":
267
+ showVersion();
268
+ break;
269
+ default: {
270
+ const suggestion = suggestCommand(command);
148
271
  console.error(pc.red(`Unknown command: ${command}`));
272
+ if (suggestion) {
273
+ console.error(`Did you mean ${pc.cyan(suggestion)}?`);
274
+ }
149
275
  console.error(`Run ${pc.cyan("lumera --help")} for usage.`);
150
276
  process.exit(1);
277
+ }
278
+ }
279
+ if (!jsonMode) {
280
+ const elapsed = performance.now() - startTime;
281
+ if (elapsed >= 500) {
282
+ console.log(pc.dim(`
283
+ Done in ${formatElapsed(elapsed)}`));
284
+ }
285
+ }
286
+ if (!jsonMode) {
287
+ try {
288
+ const update = await Promise.race([
289
+ updateCheck,
290
+ new Promise((resolve) => setTimeout(() => resolve(null), 3e3))
291
+ ]);
292
+ if (update) {
293
+ console.log();
294
+ console.log(` ${pc.yellow("Update available:")} ${pc.dim(update.current)} \u2192 ${pc.green(update.latest)}`);
295
+ console.log(` Run ${pc.cyan("npm i -g @lumerahq/cli")} to update`);
296
+ }
297
+ } catch {
298
+ }
151
299
  }
152
300
  } catch (error) {
153
301
  if (error instanceof Error) {
@@ -5,15 +5,47 @@ import {
5
5
  import {
6
6
  listAllTemplates,
7
7
  resolveTemplate
8
- } from "./chunk-WTDV3MTG.js";
8
+ } from "./chunk-CHRKCAIZ.js";
9
9
  import "./chunk-D2BLSEGR.js";
10
10
 
11
11
  // src/commands/init.ts
12
- import pc from "picocolors";
12
+ import pc2 from "picocolors";
13
13
  import prompts from "prompts";
14
14
  import { execSync } from "child_process";
15
15
  import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync, rmSync } from "fs";
16
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
17
49
  function toTitleCase(str) {
18
50
  return str.split(/[-_]/).map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
19
51
  }
@@ -82,13 +114,13 @@ function installUv() {
82
114
  try {
83
115
  try {
84
116
  execSync("curl -LsSf https://astral.sh/uv/install.sh | sh", {
85
- stdio: "inherit",
117
+ stdio: "ignore",
86
118
  shell: "/bin/bash"
87
119
  });
88
120
  return true;
89
121
  } catch {
90
122
  execSync("wget -qO- https://astral.sh/uv/install.sh | sh", {
91
- stdio: "inherit",
123
+ stdio: "ignore",
92
124
  shell: "/bin/bash"
93
125
  });
94
126
  return true;
@@ -106,12 +138,40 @@ function createPythonVenv(targetDir) {
106
138
  return false;
107
139
  }
108
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
+ }
109
168
  function parseArgs(args) {
110
169
  const result = {
111
170
  projectName: void 0,
112
171
  directory: void 0,
113
172
  template: void 0,
114
- install: false,
173
+ install: true,
174
+ open: false,
115
175
  yes: false,
116
176
  force: false,
117
177
  help: false
@@ -120,8 +180,10 @@ function parseArgs(args) {
120
180
  const arg = args[i];
121
181
  if (arg === "--help" || arg === "-h") {
122
182
  result.help = true;
123
- } else if (arg === "--install" || arg === "-i") {
124
- result.install = true;
183
+ } else if (arg === "--no-install") {
184
+ result.install = false;
185
+ } else if (arg === "--open" || arg === "-o") {
186
+ result.open = true;
125
187
  } else if (arg === "--yes" || arg === "-y") {
126
188
  result.yes = true;
127
189
  } else if (arg === "--force" || arg === "-f") {
@@ -138,25 +200,26 @@ function parseArgs(args) {
138
200
  }
139
201
  function showHelp() {
140
202
  console.log(`
141
- ${pc.dim("Usage:")}
203
+ ${pc2.dim("Usage:")}
142
204
  lumera init [name] [options]
143
205
 
144
- ${pc.dim("Description:")}
206
+ ${pc2.dim("Description:")}
145
207
  Scaffold a new Lumera project from a template.
146
208
 
147
- ${pc.dim("Options:")}
148
- --template, -t <name> Template to use (run ${pc.cyan("lumera templates")} to see options)
209
+ ${pc2.dim("Options:")}
210
+ --template, -t <name> Template to use (run ${pc2.cyan("lumera templates")} to see options)
149
211
  --yes, -y Non-interactive mode (requires project name)
150
212
  --dir, -d <path> Target directory (defaults to project name)
151
213
  --force, -f Overwrite existing directory without prompting
152
- --install, -i Install dependencies after scaffolding
214
+ --no-install Skip dependency installation
215
+ --open, -o Open project in editor after scaffolding
153
216
  --help, -h Show this help
154
217
 
155
- ${pc.dim("Examples:")}
218
+ ${pc2.dim("Examples:")}
156
219
  lumera init my-app # Interactive mode
157
220
  lumera init my-app -t invoice-processing # Use a specific template
158
221
  lumera init my-app -y # Non-interactive (default template)
159
- lumera init my-app -t invoice-processing -y -i # Full non-interactive setup
222
+ lumera init my-app -t invoice-processing -y -o # Full non-interactive + open editor
160
223
  `);
161
224
  }
162
225
  async function init(args) {
@@ -166,34 +229,36 @@ async function init(args) {
166
229
  return;
167
230
  }
168
231
  console.log();
169
- console.log(pc.cyan(pc.bold(" Create Lumera App")));
232
+ console.log(pc2.cyan(pc2.bold(" Create Lumera App")));
170
233
  console.log();
171
234
  let projectName = opts.projectName;
172
235
  let directory = opts.directory;
173
236
  const nonInteractive = opts.yes;
174
237
  if (nonInteractive && !projectName) {
175
- console.log(pc.red(" Error: Project name is required in non-interactive mode"));
176
- console.log(pc.dim(" Usage: lumera init <name> -y"));
238
+ console.log(pc2.red(" Error: Project name is required in non-interactive mode"));
239
+ console.log(pc2.dim(" Usage: lumera init <name> -y"));
177
240
  process.exit(1);
178
241
  }
179
242
  let templateName = opts.template;
180
243
  if (!templateName && !nonInteractive) {
181
244
  try {
245
+ const stop = spinner("Fetching templates...");
182
246
  const available = await listAllTemplates();
247
+ stop();
183
248
  if (available.length > 1) {
184
249
  const response = await prompts({
185
250
  type: "select",
186
251
  name: "template",
187
252
  message: "Choose a template",
188
253
  choices: available.map((t) => ({
189
- title: `${t.title} ${pc.dim(`(${t.name})`)}`,
254
+ title: `${t.title} ${pc2.dim(`(${t.name})`)}`,
190
255
  description: t.description,
191
256
  value: t.name
192
257
  })),
193
258
  initial: 0
194
259
  });
195
260
  if (!response.template) {
196
- console.log(pc.red("Cancelled"));
261
+ console.log(pc2.red("Cancelled"));
197
262
  process.exit(1);
198
263
  }
199
264
  templateName = response.template;
@@ -204,7 +269,9 @@ async function init(args) {
204
269
  if (!templateName) {
205
270
  templateName = "default";
206
271
  }
272
+ const stopResolve = spinner("Resolving template...");
207
273
  const templateDir = await resolveTemplate(templateName);
274
+ stopResolve();
208
275
  if (!projectName) {
209
276
  const response = await prompts({
210
277
  type: "text",
@@ -220,13 +287,13 @@ async function init(args) {
220
287
  }
221
288
  });
222
289
  if (!response.projectName) {
223
- console.log(pc.red("Cancelled"));
290
+ console.log(pc2.red("Cancelled"));
224
291
  process.exit(1);
225
292
  }
226
293
  projectName = response.projectName;
227
294
  }
228
295
  if (!/^[a-z0-9-]+$/.test(projectName)) {
229
- console.log(pc.red(" Error: Project name must use lowercase letters, numbers, and hyphens only"));
296
+ console.log(pc2.red(" Error: Project name must use lowercase letters, numbers, and hyphens only"));
230
297
  process.exit(1);
231
298
  }
232
299
  if (!directory) {
@@ -240,7 +307,7 @@ async function init(args) {
240
307
  initial: projectName
241
308
  });
242
309
  if (!response.directory) {
243
- console.log(pc.red("Cancelled"));
310
+ console.log(pc2.red("Cancelled"));
244
311
  process.exit(1);
245
312
  }
246
313
  directory = response.directory;
@@ -253,8 +320,8 @@ async function init(args) {
253
320
  if (opts.force) {
254
321
  rmSync(targetDir, { recursive: true });
255
322
  } else {
256
- console.log(pc.red(` Error: Directory ${directory} already exists`));
257
- console.log(pc.dim(" Use --force (-f) to overwrite"));
323
+ console.log(pc2.red(` Error: Directory ${directory} already exists`));
324
+ console.log(pc2.dim(" Use --force (-f) to overwrite"));
258
325
  process.exit(1);
259
326
  }
260
327
  } else {
@@ -265,7 +332,7 @@ async function init(args) {
265
332
  initial: false
266
333
  });
267
334
  if (!overwrite) {
268
- console.log(pc.red("Cancelled"));
335
+ console.log(pc2.red("Cancelled"));
269
336
  process.exit(1);
270
337
  }
271
338
  rmSync(targetDir, { recursive: true });
@@ -274,9 +341,9 @@ async function init(args) {
274
341
  mkdirSync(targetDir, { recursive: true });
275
342
  console.log();
276
343
  if (templateName !== "default") {
277
- console.log(pc.dim(` Creating ${projectName} from template ${pc.cyan(templateName)}...`));
344
+ console.log(pc2.dim(` Creating ${projectName} from template ${pc2.cyan(templateName)}...`));
278
345
  } else {
279
- console.log(pc.dim(` Creating ${projectName}...`));
346
+ console.log(pc2.dim(` Creating ${projectName}...`));
280
347
  }
281
348
  console.log();
282
349
  const vars = {
@@ -290,61 +357,55 @@ async function init(args) {
290
357
  if (entry.isDirectory()) {
291
358
  listFiles(join(dir, entry.name), relativePath + "/");
292
359
  } else {
293
- console.log(pc.green(" \u2713"), pc.dim(relativePath));
360
+ console.log(pc2.green(" \u2713"), pc2.dim(relativePath));
294
361
  }
295
362
  }
296
363
  }
297
364
  listFiles(targetDir);
298
365
  if (isGitInstalled()) {
299
- console.log();
300
- console.log(pc.dim(" Initializing git repository..."));
366
+ const stopGit = spinner("Initializing git repository...");
301
367
  if (initGitRepo(targetDir, projectName)) {
302
- console.log(pc.green(" \u2713"), pc.dim("Git repository initialized with initial commit"));
368
+ stopGit(pc2.green("\u2713") + pc2.dim(" Git repository initialized with initial commit"));
303
369
  } else {
304
- console.log(pc.yellow(" \u26A0"), pc.dim("Failed to initialize git repository"));
370
+ stopGit(pc2.yellow("\u26A0") + pc2.dim(" Failed to initialize git repository"));
305
371
  }
306
372
  } else {
307
- console.log();
308
- console.log(pc.yellow(" \u26A0"), pc.dim("Git not found - skipping repository initialization"));
373
+ console.log(pc2.yellow(" \u26A0"), pc2.dim("Git not found \u2014 skipping repository initialization"));
309
374
  }
310
375
  let uvAvailable = isUvInstalled();
311
376
  if (!uvAvailable) {
312
- console.log();
313
- console.log(pc.dim(" Installing uv (Python package manager)..."));
377
+ const stopUv = spinner("Installing uv (Python package manager)...");
314
378
  if (installUv()) {
315
- console.log(pc.green(" \u2713"), pc.dim("uv installed successfully"));
379
+ stopUv(pc2.green("\u2713") + pc2.dim(" uv installed successfully"));
316
380
  uvAvailable = true;
317
381
  } else {
318
- console.log(pc.yellow(" \u26A0"), pc.dim("Failed to install uv - install manually: https://docs.astral.sh/uv/"));
382
+ stopUv(pc2.yellow("\u26A0") + pc2.dim(" Failed to install uv \u2014 install manually: https://docs.astral.sh/uv/"));
319
383
  }
320
384
  }
321
385
  if (uvAvailable) {
322
- console.log();
323
- console.log(pc.dim(" Creating Python venv with Lumera SDK..."));
386
+ const stopVenv = spinner("Creating Python venv with Lumera SDK...");
324
387
  if (createPythonVenv(targetDir)) {
325
- console.log(pc.green(" \u2713"), pc.dim("Python venv created (.venv/) with lumera SDK"));
388
+ stopVenv(pc2.green("\u2713") + pc2.dim(" Python venv created (.venv/) with lumera SDK"));
326
389
  } else {
327
- console.log(pc.yellow(" \u26A0"), pc.dim("Failed to create Python venv"));
390
+ stopVenv(pc2.yellow("\u26A0") + pc2.dim(" Failed to create Python venv"));
328
391
  }
329
392
  }
330
393
  if (opts.install) {
331
- console.log();
332
- console.log(pc.dim(" Installing dependencies..."));
394
+ const stopInstall = spinner("Installing dependencies...");
333
395
  try {
334
- execSync("pnpm install", { cwd: targetDir, stdio: "inherit" });
335
- console.log(pc.green(" \u2713"), pc.dim("Dependencies installed"));
396
+ execSync("pnpm install", { cwd: targetDir, stdio: "ignore" });
397
+ stopInstall(pc2.green("\u2713") + pc2.dim(" Dependencies installed"));
336
398
  } catch {
337
- console.log(pc.yellow(" \u26A0"), pc.dim("Failed to install dependencies"));
399
+ stopInstall(pc2.yellow("\u26A0") + pc2.dim(" Failed to install dependencies"));
338
400
  }
339
401
  }
340
- console.log();
341
- console.log(pc.dim(" Installing Lumera skills for AI agents..."));
402
+ const stopSkills = spinner("Installing Lumera skills for AI agents...");
342
403
  try {
343
404
  const { installed, failed } = await installAllSkills(targetDir);
344
405
  if (failed > 0) {
345
- console.log(pc.yellow(" \u26A0"), pc.dim(`Installed ${installed} skills (${failed} failed)`));
406
+ stopSkills(pc2.yellow("\u26A0") + pc2.dim(` Installed ${installed} skills (${failed} failed)`));
346
407
  } else {
347
- console.log(pc.green(" \u2713"), pc.dim(`${installed} Lumera skills installed`));
408
+ stopSkills(pc2.green("\u2713") + pc2.dim(` ${installed} Lumera skills installed`));
348
409
  }
349
410
  syncClaudeMd(targetDir);
350
411
  if (isGitInstalled()) {
@@ -357,20 +418,22 @@ async function init(args) {
357
418
  }
358
419
  }
359
420
  } catch (err) {
360
- console.log(pc.yellow(" \u26A0"), pc.dim(`Failed to install skills: ${err}`));
421
+ stopSkills(pc2.yellow("\u26A0") + pc2.dim(` Failed to install skills: ${err}`));
361
422
  }
362
423
  console.log();
363
- console.log(pc.green(pc.bold(" Done!")), "Next steps:");
424
+ console.log(pc2.green(pc2.bold(" Done!")), "Next steps:");
364
425
  console.log();
365
- console.log(pc.cyan(` cd ${directory}`));
426
+ console.log(pc2.cyan(` cd ${directory}`));
366
427
  if (!opts.install) {
367
- console.log(pc.cyan(" pnpm install"));
428
+ console.log(pc2.cyan(" pnpm install"));
368
429
  }
369
- console.log(pc.cyan(" lumera login"));
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"));
430
+ console.log(pc2.cyan(" lumera login"));
431
+ console.log(pc2.cyan(" lumera dev"));
373
432
  console.log();
433
+ if (opts.open) {
434
+ openInEditor(targetDir);
435
+ console.log();
436
+ }
374
437
  }
375
438
  export {
376
439
  init
@@ -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.1",
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
- };
@@ -1,87 +0,0 @@
1
- import {
2
- dev
3
- } from "./chunk-CDZZ3JYU.js";
4
- import {
5
- loadEnv
6
- } from "./chunk-2CR762KB.js";
7
- import {
8
- getToken
9
- } from "./chunk-NDLYGKS6.js";
10
- import {
11
- findProjectRoot,
12
- getApiUrl,
13
- getAppName,
14
- getAppTitle
15
- } from "./chunk-D2BLSEGR.js";
16
-
17
- // src/commands/dev.ts
18
- import pc from "picocolors";
19
- function parseFlags(args) {
20
- const result = {};
21
- for (let i = 0; i < args.length; i++) {
22
- const arg = args[i];
23
- if (arg.startsWith("--")) {
24
- const key = arg.slice(2);
25
- const next = args[i + 1];
26
- if (next && !next.startsWith("--")) {
27
- result[key] = next;
28
- i++;
29
- } else {
30
- result[key] = true;
31
- }
32
- }
33
- }
34
- return result;
35
- }
36
- function showHelp() {
37
- console.log(`
38
- ${pc.dim("Usage:")}
39
- lumera dev [options]
40
-
41
- ${pc.dim("Description:")}
42
- Start the development server with Lumera registration.
43
- Registers your app with Lumera and starts a local dev server.
44
-
45
- ${pc.dim("Options:")}
46
- --port <number> Dev server port (default: 8080)
47
- --url <url> App URL for dev mode (default: http://localhost:{port})
48
- --help, -h Show this help
49
-
50
- ${pc.dim("Environment variables:")}
51
- LUMERA_TOKEN API token (overrides \`lumera login\`)
52
- LUMERA_API_URL API base URL (default: https://app.lumerahq.com/api)
53
- PORT Dev server port (default: 8080)
54
- APP_URL App URL for dev mode
55
-
56
- ${pc.dim("Examples:")}
57
- lumera dev # Start dev server on port 8080
58
- lumera dev --port 3000 # Start dev server on port 3000
59
- lumera dev --url http://192.168.1.100:8080 # Custom URL for mobile testing
60
- `);
61
- }
62
- async function dev2(args) {
63
- if (args.includes("--help") || args.includes("-h")) {
64
- showHelp();
65
- return;
66
- }
67
- const flags = parseFlags(args);
68
- const projectRoot = findProjectRoot();
69
- loadEnv(projectRoot);
70
- const token = getToken(projectRoot);
71
- const appName = getAppName(projectRoot);
72
- const appTitle = getAppTitle(projectRoot);
73
- const apiUrl = getApiUrl();
74
- const port = Number(flags.port || process.env.PORT || "8080");
75
- const appUrl = typeof flags.url === "string" ? flags.url : process.env.APP_URL;
76
- await dev({
77
- token,
78
- appName,
79
- appTitle,
80
- port,
81
- appUrl,
82
- apiUrl
83
- });
84
- }
85
- export {
86
- dev2 as dev
87
- };
@@ -1,70 +0,0 @@
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
- };