@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.
- package/dist/{chunk-WV5HOQNX.js → chunk-I7QEYKS7.js} +32 -4
- package/dist/{dev-UTPD4MN5.js → dev-VWHFYKGC.js} +1 -1
- package/dist/index.js +8 -8
- package/dist/{resources-QCCLRDOL.js → resources-AMXL4A64.js} +35 -44
- package/package.json +1 -1
- package/templates/default/package.json +1 -1
- package/templates/default/src/main.tsx +34 -18
- package/templates/default/vite.config.ts +3 -1
|
@@ -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
|
-
|
|
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 = {
|
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-
|
|
229
|
+
await import("./resources-AMXL4A64.js").then((m) => m.plan(args.slice(1)));
|
|
230
230
|
break;
|
|
231
231
|
case "apply":
|
|
232
|
-
await import("./resources-
|
|
232
|
+
await import("./resources-AMXL4A64.js").then((m) => m.apply(args.slice(1)));
|
|
233
233
|
break;
|
|
234
234
|
case "pull":
|
|
235
|
-
await import("./resources-
|
|
235
|
+
await import("./resources-AMXL4A64.js").then((m) => m.pull(args.slice(1)));
|
|
236
236
|
break;
|
|
237
237
|
case "destroy":
|
|
238
|
-
await import("./resources-
|
|
238
|
+
await import("./resources-AMXL4A64.js").then((m) => m.destroy(args.slice(1)));
|
|
239
239
|
break;
|
|
240
240
|
case "list":
|
|
241
|
-
await import("./resources-
|
|
241
|
+
await import("./resources-AMXL4A64.js").then((m) => m.list(args.slice(1)));
|
|
242
242
|
break;
|
|
243
243
|
case "show":
|
|
244
|
-
await import("./resources-
|
|
244
|
+
await import("./resources-AMXL4A64.js").then((m) => m.show(args.slice(1)));
|
|
245
245
|
break;
|
|
246
246
|
case "diff":
|
|
247
|
-
await import("./resources-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 (
|
|
2196
|
-
|
|
2197
|
-
|
|
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
|
|
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
|
|
2634
|
-
if (
|
|
2635
|
-
|
|
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,
|
|
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,4 +1,10 @@
|
|
|
1
|
-
import {
|
|
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' | '
|
|
69
|
+
const [status, setStatus] = useState<'pending' | 'ready' | 'unauthenticated'>('pending');
|
|
64
70
|
|
|
65
71
|
useEffect(() => {
|
|
66
72
|
if (typeof window === 'undefined') return;
|
|
67
73
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
|
79
|
-
user: payload
|
|
80
|
-
sessionToken: payload
|
|
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 === '
|
|
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">
|
|
116
|
+
<span className="text-2xl font-semibold text-muted-foreground">401</span>
|
|
108
117
|
</div>
|
|
109
|
-
<p className="text-sm text-muted-foreground">
|
|
110
|
-
|
|
111
|
-
|
|
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
|
-
|
|
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
|
});
|