@lumerahq/cli 0.23.0-dev.3 → 0.24.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.
@@ -41,6 +41,24 @@ function resolveVersion(cwd) {
41
41
  }
42
42
  return formatTimestampVersion();
43
43
  }
44
+ function resolveUIVersion(cwd) {
45
+ try {
46
+ const installed = JSON.parse(
47
+ readFileSync(resolve(cwd, "node_modules/@lumerahq/ui/package.json"), "utf-8")
48
+ );
49
+ if (installed?.version) return String(installed.version);
50
+ } catch {
51
+ }
52
+ try {
53
+ const pkg = JSON.parse(readFileSync(resolve(cwd, "package.json"), "utf-8"));
54
+ const spec = pkg?.dependencies?.["@lumerahq/ui"] ?? pkg?.devDependencies?.["@lumerahq/ui"];
55
+ if (!spec) return "none";
56
+ const cleaned = String(spec).replace(/^[\^~>=<\s]+/, "");
57
+ return /^\d+\.\d+/.test(cleaned) ? cleaned : "";
58
+ } catch {
59
+ return "";
60
+ }
61
+ }
44
62
  function resolveGitSha(cwd) {
45
63
  try {
46
64
  return execSync("git rev-parse HEAD", {
@@ -62,11 +80,14 @@ async function createTarball(distDir) {
62
80
  archive.finalize();
63
81
  });
64
82
  }
65
- async function ensureAppRecord(apiBase, token, appName, appTitle) {
83
+ async function ensureAppRecord(apiBase, token, appName, appTitle, accessLevel) {
66
84
  const appData = {
67
85
  name: appTitle,
68
86
  hosting_type: "s3"
69
87
  };
88
+ if (accessLevel) {
89
+ appData.access_level = accessLevel;
90
+ }
70
91
  const res = await fetchWithRetry(`${apiBase}/custom-apps/${encodeURIComponent(appName)}/registration`, {
71
92
  method: "PUT",
72
93
  headers: {
@@ -87,13 +108,14 @@ async function deploy(options) {
87
108
  appName,
88
109
  appTitle,
89
110
  distDir,
90
- apiUrl
111
+ apiUrl,
112
+ accessLevel
91
113
  } = options;
92
114
  if (!existsSync(distDir)) {
93
115
  throw new Error(`dist directory not found: ${distDir}`);
94
116
  }
95
117
  console.log(pc.dim("Ensuring app record..."));
96
- await ensureAppRecord(apiUrl, token, appName, appTitle);
118
+ await ensureAppRecord(apiUrl, token, appName, appTitle, accessLevel);
97
119
  console.log(pc.cyan(`Deploying ${appTitle}...`));
98
120
  const version = options.version || process.env.LUMERA_APP_VERSION?.trim() || resolveVersion(dirname(distDir));
99
121
  console.log(pc.dim(`Version: ${version}`));
@@ -104,6 +126,8 @@ async function deploy(options) {
104
126
  formData.append("version", version);
105
127
  const gitSha = resolveGitSha(dirname(distDir));
106
128
  if (gitSha) formData.append("git_sha", gitSha);
129
+ const uiVersion = resolveUIVersion(dirname(distDir));
130
+ if (uiVersion) formData.append("ui_version", uiVersion);
107
131
  const res = await fetchWithRetry(`${apiUrl}/custom-apps/${appName}/deploy`, {
108
132
  method: "POST",
109
133
  headers: {
@@ -118,9 +142,13 @@ async function deploy(options) {
118
142
  }
119
143
  const result = await res.json();
120
144
  const launchUrl = result.launch_url;
145
+ const appUrl = result.app_url;
121
146
  console.log(pc.green(`
122
147
  Deployed! ${launchUrl}`));
123
- return { launchUrl, version };
148
+ if (appUrl) {
149
+ console.log(pc.green(`App URL: ${appUrl}`));
150
+ }
151
+ return { launchUrl, appUrl, version };
124
152
  }
125
153
  async function registerDevApp(apiBase, token, appName, appTitle, appUrl) {
126
154
  const appData = {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  dev
3
- } from "./chunk-WV5HOQNX.js";
3
+ } from "./chunk-I7QEYKS7.js";
4
4
  import {
5
5
  syncDeps
6
6
  } from "./chunk-3ESHIPZH.js";
package/dist/index.js CHANGED
@@ -226,29 +226,29 @@ async function main() {
226
226
  switch (command) {
227
227
  // Resource commands
228
228
  case "plan":
229
- await import("./resources-QCCLRDOL.js").then((m) => m.plan(args.slice(1)));
229
+ await import("./resources-CGMBV3T6.js").then((m) => m.plan(args.slice(1)));
230
230
  break;
231
231
  case "apply":
232
- await import("./resources-QCCLRDOL.js").then((m) => m.apply(args.slice(1)));
232
+ await import("./resources-CGMBV3T6.js").then((m) => m.apply(args.slice(1)));
233
233
  break;
234
234
  case "pull":
235
- await import("./resources-QCCLRDOL.js").then((m) => m.pull(args.slice(1)));
235
+ await import("./resources-CGMBV3T6.js").then((m) => m.pull(args.slice(1)));
236
236
  break;
237
237
  case "destroy":
238
- await import("./resources-QCCLRDOL.js").then((m) => m.destroy(args.slice(1)));
238
+ await import("./resources-CGMBV3T6.js").then((m) => m.destroy(args.slice(1)));
239
239
  break;
240
240
  case "list":
241
- await import("./resources-QCCLRDOL.js").then((m) => m.list(args.slice(1)));
241
+ await import("./resources-CGMBV3T6.js").then((m) => m.list(args.slice(1)));
242
242
  break;
243
243
  case "show":
244
- await import("./resources-QCCLRDOL.js").then((m) => m.show(args.slice(1)));
244
+ await import("./resources-CGMBV3T6.js").then((m) => m.show(args.slice(1)));
245
245
  break;
246
246
  case "diff":
247
- await import("./resources-QCCLRDOL.js").then((m) => m.diff(args.slice(1)));
247
+ await import("./resources-CGMBV3T6.js").then((m) => m.diff(args.slice(1)));
248
248
  break;
249
249
  // Development
250
250
  case "dev":
251
- await import("./dev-UTPD4MN5.js").then((m) => m.dev(args.slice(1)));
251
+ await import("./dev-VWHFYKGC.js").then((m) => m.dev(args.slice(1)));
252
252
  break;
253
253
  case "run":
254
254
  await import("./run-DJXAXDD2.js").then((m) => m.run(args.slice(1)));
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  deploy
3
- } from "./chunk-WV5HOQNX.js";
3
+ } from "./chunk-I7QEYKS7.js";
4
4
  import {
5
5
  syncDeps
6
6
  } from "./chunk-3ESHIPZH.js";
@@ -2184,7 +2184,20 @@ async function applyApp(args) {
2184
2184
  }
2185
2185
  }
2186
2186
  const distDir = resolve(projectRoot, "dist");
2187
- await deploy({ token, appName, appTitle, distDir, apiUrl });
2187
+ const accessLevel = getFlagValue(args, "access-level");
2188
+ const validAccessLevels = [
2189
+ "org_members_with_permission",
2190
+ "org_members",
2191
+ "invited",
2192
+ "signed_in",
2193
+ "public"
2194
+ ];
2195
+ if (accessLevel && !validAccessLevels.includes(accessLevel)) {
2196
+ throw new Error(
2197
+ `Invalid --access-level "${accessLevel}". Valid values: ${validAccessLevels.join(", ")}`
2198
+ );
2199
+ }
2200
+ await deploy({ token, appName, appTitle, distDir, apiUrl, accessLevel });
2188
2201
  }
2189
2202
  async function pullCollections(api, platformDir, filterName, projectId) {
2190
2203
  const collectionsDir = join2(platformDir, "collections");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumerahq/cli",
3
- "version": "0.23.0-dev.3",
3
+ "version": "0.24.0",
4
4
  "description": "CLI for building and deploying Lumera apps",
5
5
  "type": "module",
6
6
  "engines": {
@@ -24,7 +24,7 @@
24
24
  "check:ci": "biome check . && tsr generate && tsc --noEmit"
25
25
  },
26
26
  "dependencies": {
27
- "@lumerahq/ui": "^0.13.0-dev.2",
27
+ "@lumerahq/ui": "^0.14.0",
28
28
  "@tanstack/react-query": "^5.90.11",
29
29
  "@tanstack/react-router": "1.155.0",
30
30
  "clsx": "^2.1.1",
@@ -1,4 +1,10 @@
1
- import { type HostPayload, isEmbedded, onInitMessage, postReadyMessage } from '@lumerahq/ui/lib';
1
+ import {
2
+ type HostPayload,
3
+ isEmbedded,
4
+ onInitMessage,
5
+ postReadyMessage,
6
+ redirectToLogin,
7
+ } from '@lumerahq/ui/lib';
2
8
  import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
3
9
  import { createHashHistory, createRouter, RouterProvider } from '@tanstack/react-router';
4
10
  import { StrictMode, useEffect, useRef, useState } from 'react';
@@ -60,24 +66,27 @@ function RouteRestorer() {
60
66
 
61
67
  const App = () => {
62
68
  const [hostContext, setHostContext] = useState<AuthContextValue | null>(null);
63
- const [status, setStatus] = useState<'pending' | 'ready' | 'denied'>('pending');
69
+ const [status, setStatus] = useState<'pending' | 'ready' | 'unauthenticated'>('pending');
64
70
 
65
71
  useEffect(() => {
66
72
  if (typeof window === 'undefined') return;
67
73
 
68
- const standaloneDevMode =
69
- import.meta.env.DEV && !isEmbedded() && !!import.meta.env.VITE_DEV_API_BASE_URL;
70
-
71
- if (!isEmbedded() && !standaloneDevMode) {
72
- setStatus('denied');
73
- return;
74
- }
75
-
74
+ // The bridge picks the transport automatically: postMessage when embedded
75
+ // in the Lumera shell, same-origin /api with the app-session cookie when
76
+ // served standalone on the app's own hostname, and direct dev fetch when
77
+ // VITE_DEV_API_BASE_URL is set.
76
78
  const cleanup = onInitMessage((payload?: HostPayload) => {
79
+ if (!payload) {
80
+ // No identity and no app context: authentication is required
81
+ // (payload.user alone being undefined means an anonymous visitor on
82
+ // a public app — that still counts as ready).
83
+ setStatus('unauthenticated');
84
+ return;
85
+ }
77
86
  setHostContext({
78
- company: payload?.company,
79
- user: payload?.user,
80
- sessionToken: payload?.session?.token,
87
+ company: payload.company,
88
+ user: payload.user,
89
+ sessionToken: payload.session?.token,
81
90
  });
82
91
  setStatus('ready');
83
92
  });
@@ -99,16 +108,23 @@ const App = () => {
99
108
  );
100
109
  }
101
110
 
102
- if (status === 'denied' || !hostContext) {
111
+ if (status === 'unauthenticated' || !hostContext) {
103
112
  return (
104
113
  <main className="flex min-h-screen items-center justify-center bg-background">
105
114
  <div className="text-center space-y-3">
106
115
  <div className="inline-flex size-12 rounded-xl bg-muted items-center justify-center mb-2">
107
- <span className="text-2xl font-semibold text-muted-foreground">403</span>
116
+ <span className="text-2xl font-semibold text-muted-foreground">401</span>
108
117
  </div>
109
- <p className="text-sm text-muted-foreground">
110
- Access denied, this app can only be accessed from within Lumera.
111
- </p>
118
+ <p className="text-sm text-muted-foreground">Sign in to access this app.</p>
119
+ {!isEmbedded() && (
120
+ <button
121
+ type="button"
122
+ className="text-sm font-medium text-primary underline underline-offset-4"
123
+ onClick={() => redirectToLogin()}
124
+ >
125
+ Sign in with Lumera
126
+ </button>
127
+ )}
112
128
  </div>
113
129
  </main>
114
130
  );
@@ -20,6 +20,8 @@ export default defineConfig({
20
20
  dedupe: ['react', 'react-dom'],
21
21
  },
22
22
  server: {
23
- allowedHosts: ['mac.lumerahq.com'],
23
+ // Local platform host plus standalone app hostnames
24
+ // (<project>--<company>.<env-suffix> on lumerahq.app).
25
+ allowedHosts: ['mac.lumerahq.com', '.lumerahq.app'],
24
26
  },
25
27
  });