@lumerahq/cli 0.10.1 → 0.11.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.
@@ -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-ZRAW4EFI.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-ZRAW4EFI.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-ZRAW4EFI.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-ZRAW4EFI.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-ZRAW4EFI.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-ZRAW4EFI.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-4JSHTLX2.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) {