@lumerahq/cli 0.23.0-dev.3 → 0.23.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-AMXL4A64.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-AMXL4A64.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-AMXL4A64.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-AMXL4A64.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-AMXL4A64.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-AMXL4A64.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-AMXL4A64.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";
@@ -126,7 +126,6 @@ var collectionSchemaRule = {
126
126
  const fields = parsed.fields;
127
127
  const indexes = parsed.indexes;
128
128
  const search = parsed.search;
129
- const audit = parsed.audit;
130
129
  const localFieldTypes = /* @__PURE__ */ new Map();
131
130
  if (Array.isArray(fields)) {
132
131
  for (const field of fields) {
@@ -166,9 +165,6 @@ var collectionSchemaRule = {
166
165
  }
167
166
  }
168
167
  }
169
- if (audit !== void 0 && typeof audit !== "boolean") {
170
- issues.push(error(target, 'Optional field "audit" must be a boolean when present.'));
171
- }
172
168
  if (indexes !== void 0) {
173
169
  if (!Array.isArray(indexes)) {
174
170
  issues.push(error(target, 'Optional field "indexes" must be an array when present.'));
@@ -1370,10 +1366,6 @@ function loadLocalCollections(platformDir, filterName) {
1370
1366
  errors.push(`${file}: missing fields array`);
1371
1367
  continue;
1372
1368
  }
1373
- if (collection.audit !== void 0 && typeof collection.audit !== "boolean") {
1374
- errors.push(`${file}: audit must be a boolean when present`);
1375
- continue;
1376
- }
1377
1369
  for (let i = 0; i < collection.fields.length; i++) {
1378
1370
  const field = collection.fields[i];
1379
1371
  if (!field || typeof field !== "object") {
@@ -1550,14 +1542,7 @@ function convertCollectionToApiFormat(local) {
1550
1542
  const indexName = `idx_${local.id}_${idx.fields.join("_")}`;
1551
1543
  return `CREATE ${unique}INDEX ${indexName} ON ${local.name} (${fields})`;
1552
1544
  });
1553
- return {
1554
- id: local.id,
1555
- name: local.name,
1556
- schema,
1557
- indexes,
1558
- search: local.search ?? null,
1559
- ...local.audit !== void 0 ? { audit: local.audit } : {}
1560
- };
1545
+ return { id: local.id, name: local.name, schema, indexes, search: local.search ?? null };
1561
1546
  }
1562
1547
  function mapFieldType(type) {
1563
1548
  const typeMap = {
@@ -1679,10 +1664,7 @@ async function planCollections(api, localCollections) {
1679
1664
  resource: "collection",
1680
1665
  id: local.id,
1681
1666
  name: local.name,
1682
- details: [
1683
- `${local.fields.length} fields`,
1684
- ...local.audit !== void 0 ? [local.audit ? "audit enabled" : "audit disabled"] : []
1685
- ].join(", "),
1667
+ details: `${local.fields.length} fields`,
1686
1668
  fieldDetails: local.fields.map((f) => ({
1687
1669
  action: "+",
1688
1670
  name: f.name,
@@ -1708,15 +1690,13 @@ async function planCollections(api, localCollections) {
1708
1690
  }
1709
1691
  const indexesChanged = indexesDiffer(local, remote.indexes || []);
1710
1692
  const searchChanged = normalizeCollectionSearch(local.search) !== normalizeCollectionSearch(remote.search);
1711
- const auditChanged = local.audit !== void 0 && local.audit !== remote.audit;
1712
- if (added.length > 0 || removed.length > 0 || modified.length > 0 || indexesChanged || searchChanged || auditChanged) {
1693
+ if (added.length > 0 || removed.length > 0 || modified.length > 0 || indexesChanged || searchChanged) {
1713
1694
  const details = [];
1714
1695
  if (added.length > 0) details.push(`+${added.length} field${added.length > 1 ? "s" : ""}`);
1715
1696
  if (removed.length > 0) details.push(`-${removed.length} field${removed.length > 1 ? "s" : ""}`);
1716
1697
  if (modified.length > 0) details.push(`~${modified.length} field${modified.length > 1 ? "s" : ""} (${modified.join(", ")})`);
1717
1698
  if (indexesChanged) details.push("indexes changed");
1718
1699
  if (searchChanged) details.push("search changed");
1719
- if (auditChanged) details.push(local.audit ? "audit enabled" : "audit disabled");
1720
1700
  const fieldDetails = [];
1721
1701
  for (const name of added) {
1722
1702
  const f = localFieldMap.get(name);
@@ -2184,24 +2164,38 @@ async function applyApp(args) {
2184
2164
  }
2185
2165
  }
2186
2166
  const distDir = resolve(projectRoot, "dist");
2187
- await deploy({ token, appName, appTitle, distDir, apiUrl });
2167
+ const accessLevel = getFlagValue(args, "access-level");
2168
+ const validAccessLevels = [
2169
+ "org_members_with_permission",
2170
+ "org_members",
2171
+ "invited",
2172
+ "signed_in",
2173
+ "public"
2174
+ ];
2175
+ if (accessLevel && !validAccessLevels.includes(accessLevel)) {
2176
+ throw new Error(
2177
+ `Invalid --access-level "${accessLevel}". Valid values: ${validAccessLevels.join(", ")}`
2178
+ );
2179
+ }
2180
+ await deploy({ token, appName, appTitle, distDir, apiUrl, accessLevel });
2188
2181
  }
2189
- async function pullCollections(api, platformDir, filterName, projectId) {
2182
+ async function pullCollections(api, platformDir, filterName, appName) {
2190
2183
  const collectionsDir = join2(platformDir, "collections");
2191
2184
  mkdirSync(collectionsDir, { recursive: true });
2192
2185
  const collections = await api.listCollections();
2193
2186
  for (const collection of collections) {
2194
2187
  if (collection.system || collection.managed) continue;
2195
- if (projectId && collection.project_id !== projectId) continue;
2196
- if (!projectId && collection.project_id) continue;
2197
- const localName = collection.name;
2188
+ if (appName && collection.name.includes(NAMESPACE_SEPARATOR)) {
2189
+ const prefix = collection.name.split(NAMESPACE_SEPARATOR)[0];
2190
+ if (prefix !== sanitizeSlugForCollectionName(appName)) continue;
2191
+ }
2192
+ const localName = stripNamespacePrefix(collection.name, appName);
2198
2193
  if (filterName && localName !== filterName && collection.name !== filterName && collection.id !== filterName) {
2199
2194
  continue;
2200
2195
  }
2201
2196
  const localFormat = {
2202
2197
  id: collection.id,
2203
2198
  name: localName,
2204
- audit: collection.audit,
2205
2199
  fields: collection.schema.map((field) => {
2206
2200
  const localField = {
2207
2201
  name: field.name,
@@ -2605,11 +2599,7 @@ async function listResources(api, platformDir, filterType, appName, projectId) {
2605
2599
  if (!filterType || filterType === "collections") {
2606
2600
  const localCollections = loadLocalCollections(platformDir);
2607
2601
  const remoteCollections = await api.listCollections();
2608
- const projectCollections = remoteCollections.filter((collection) => {
2609
- if (collection.system || collection.managed) return false;
2610
- return projectId ? collection.project_id === projectId : !collection.project_id;
2611
- });
2612
- const remoteByName = new Map(projectCollections.map((collection) => [collection.name, collection]));
2602
+ const remoteByName = new Map(remoteCollections.filter((c) => !c.system && !c.managed).map((c) => [stripNamespacePrefix(c.name, appName), c]));
2613
2603
  const localNames = new Set(localCollections.map((c) => c.name));
2614
2604
  for (const local of localCollections) {
2615
2605
  const remote = remoteByName.get(local.name);
@@ -2630,9 +2620,15 @@ async function listResources(api, platformDir, filterType, appName, projectId) {
2630
2620
  }
2631
2621
  }
2632
2622
  }
2633
- for (const remote of projectCollections) {
2634
- if (!localNames.has(remote.name)) {
2635
- results.push({ name: remote.name, type: "collections", status: "remote-only" });
2623
+ for (const remote of remoteCollections) {
2624
+ if (remote.system || remote.managed) continue;
2625
+ if (appName && remote.name.includes(NAMESPACE_SEPARATOR)) {
2626
+ const prefix = remote.name.split(NAMESPACE_SEPARATOR)[0];
2627
+ if (prefix !== sanitizeSlugForCollectionName(appName)) continue;
2628
+ }
2629
+ const stripped = stripNamespacePrefix(remote.name, appName);
2630
+ if (!localNames.has(stripped)) {
2631
+ results.push({ name: stripped, type: "collections", status: "remote-only" });
2636
2632
  }
2637
2633
  }
2638
2634
  }
@@ -3593,7 +3589,7 @@ async function pull(args) {
3593
3589
  console.log();
3594
3590
  if (!type || type === "collections") {
3595
3591
  console.log(pc2.bold(" Collections:"));
3596
- await pullCollections(api, platformDir, name || void 0, projectId);
3592
+ await pullCollections(api, platformDir, name || void 0, appName);
3597
3593
  console.log();
3598
3594
  if (!name) {
3599
3595
  console.log(pc2.bold(" Resource shares:"));
@@ -3921,15 +3917,10 @@ async function diff(args) {
3921
3917
  }
3922
3918
  export {
3923
3919
  apply,
3924
- applyCollections,
3925
- convertCollectionToApiFormat,
3926
3920
  destroy,
3927
3921
  diff,
3928
3922
  list,
3929
- loadLocalCollections,
3930
3923
  plan,
3931
- planCollections,
3932
3924
  pull,
3933
- pullCollections,
3934
3925
  show
3935
3926
  };
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.23.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.13.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
  });