@unoverse-platform/studio 0.1.59 → 0.1.61
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/local/plugin.ts
CHANGED
|
@@ -253,6 +253,32 @@ export function localStudioHost(): Plugin {
|
|
|
253
253
|
// runs the same base items/publish functions directly, and no token ever
|
|
254
254
|
// nears the browser because the browser no longer takes part.
|
|
255
255
|
|
|
256
|
+
/**
|
|
257
|
+
* THE CLI'S SESSION, SERVED TO THE PREVIEW. `unoverse login` keeps sessions
|
|
258
|
+
* in ~/.unoverse/sessions.json keyed by universe origin (cli lib/session.mjs
|
|
259
|
+
* — that file IS the contract). Studio used to know nothing of it, so a
|
|
260
|
+
* developer who had just logged in still saw "Offline" and a blank Live
|
|
261
|
+
* preview with no hint why (observed live 2026-08-21). The preview already
|
|
262
|
+
* carries a bearer in the browser by design (the pasted-JWT field), so
|
|
263
|
+
* serving the session token here changes what the browser holds not at all —
|
|
264
|
+
* it only spares the developer pasting what the CLI already has. Expired or
|
|
265
|
+
* absent answers {}: the UI then says to run unoverse login, by name.
|
|
266
|
+
*/
|
|
267
|
+
case path === "/local/session": {
|
|
268
|
+
const origin = new URL(req.url ?? "/", "http://x").searchParams.get("origin") ?? "";
|
|
269
|
+
try {
|
|
270
|
+
const { readFileSync } = await import("node:fs");
|
|
271
|
+
const { homedir } = await import("node:os");
|
|
272
|
+
const { join } = await import("node:path");
|
|
273
|
+
const all = JSON.parse(readFileSync(join(homedir(), ".unoverse", "sessions.json"), "utf8"));
|
|
274
|
+
const sess = all[origin.replace(/\/$/, "")];
|
|
275
|
+
if (sess?.access_token && (!sess.expires_at || sess.expires_at > Date.now() + 60_000)) {
|
|
276
|
+
return json(res, { token: sess.access_token, email: sess.email ?? "" });
|
|
277
|
+
}
|
|
278
|
+
} catch { /* no file, unreadable, malformed: same answer as signed out */ }
|
|
279
|
+
return json(res, {});
|
|
280
|
+
}
|
|
281
|
+
|
|
256
282
|
case path === "/dev/orgs":
|
|
257
283
|
return json(res, { orgs: c.themeOrgs() });
|
|
258
284
|
|
package/package.json
CHANGED
|
@@ -80,7 +80,21 @@ export function NativeAppHost({
|
|
|
80
80
|
if (!text) throw new Error(`${APP_UI_URI} returned no content — is the webSDK built on the universe?`);
|
|
81
81
|
if (!cancelled) setHtml(text);
|
|
82
82
|
} catch (e) {
|
|
83
|
-
|
|
83
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
84
|
+
// AUTH FAILURES SAY SO, BY NAME. A 401 here rendered as a bare transport error
|
|
85
|
+
// (or worse, an eternal loading ghost), and "Offline" told the developer
|
|
86
|
+
// nothing. The fix is one terminal command; the error is the place to say it.
|
|
87
|
+
const authShaped = /401|unauthori[sz]ed|jwt|token/i.test(msg);
|
|
88
|
+
if (!cancelled)
|
|
89
|
+
setError(
|
|
90
|
+
authShaped
|
|
91
|
+
? `This universe requires sign-in.
|
|
92
|
+
|
|
93
|
+
Run: unoverse login ${server}
|
|
94
|
+
|
|
95
|
+
then come back — Studio picks the session up by itself.`
|
|
96
|
+
: msg,
|
|
97
|
+
);
|
|
84
98
|
} finally {
|
|
85
99
|
client.close().catch(() => {});
|
|
86
100
|
}
|
|
@@ -26,7 +26,7 @@ import { ComponentStore } from "@unoverse/sdk";
|
|
|
26
26
|
import { StreamedUnoverseTemplate, useUnoverseConnection, computeAppWidth, resolveActiveLayout, visible, type ResolvedTheme, type UseVoiceServiceConfig } from "@unoverse/sdk";
|
|
27
27
|
import { NativeAppHost } from "./NativeAppHost";
|
|
28
28
|
import { inArrangement, viewOfSlice } from "./states";
|
|
29
|
-
import { client, authedFetch, hasAuth, UNOVERSE_BASE, liveUniverseUrl } from "studio-host";
|
|
29
|
+
import { client, authedFetch, hasAuth, hasSourceAccess, UNOVERSE_BASE, liveUniverseUrl } from "studio-host";
|
|
30
30
|
import { NAV, NAV_SECTION, SCREEN, navItem, navSubItem } from "./chrome";
|
|
31
31
|
|
|
32
32
|
// Per-APP test conversation: each app chats in its own conversation id, so testing
|
|
@@ -243,9 +243,18 @@ export function TemplatesView({
|
|
|
243
243
|
|
|
244
244
|
// OIDC session (undefined when no AuthProvider is mounted — env unset).
|
|
245
245
|
const auth = useAuth();
|
|
246
|
-
|
|
246
|
+
/**
|
|
247
|
+
* THE TERMINAL LOGIN COUNTS HERE. `unoverse login` keeps the session; the local host
|
|
248
|
+
* serves its token per origin (/local/session). Without this a developer who had just
|
|
249
|
+
* logged in still saw Offline and a blank preview with no hint why. Refetched when the
|
|
250
|
+
* server changes and when the tab regains focus, so a login run in the terminal is
|
|
251
|
+
* picked up on the next glance at Studio. Empty when signed out or expired — then the
|
|
252
|
+
* UI says to run unoverse login rather than showing a mute Offline.
|
|
253
|
+
*/
|
|
254
|
+
const [cliSession, setCliSession] = useState<{ token?: string; email?: string }>({});
|
|
255
|
+
// Token source: the real OIDC access token, else the terminal login, else a pasted JWT.
|
|
247
256
|
const authToken = auth?.user?.access_token;
|
|
248
|
-
const token = authToken || connCfg.token;
|
|
257
|
+
const token = authToken || cliSession.token || connCfg.token;
|
|
249
258
|
|
|
250
259
|
// The live server: the signed-in universe, else the local dev platform (see the
|
|
251
260
|
// CONN_DEFAULTS note). A universe URL is already an https origin; the bare local
|
|
@@ -253,6 +262,21 @@ export function TemplatesView({
|
|
|
253
262
|
const universeUrl = liveUniverseUrl();
|
|
254
263
|
const liveServer = universeUrl ?? LOCAL_DEV_SERVER;
|
|
255
264
|
const liveBase = liveServer.includes("://") ? liveServer : `http://${liveServer}`;
|
|
265
|
+
useEffect(() => {
|
|
266
|
+
if (!hasSourceAccess) return;
|
|
267
|
+
let live = true;
|
|
268
|
+
const read = () =>
|
|
269
|
+
authedFetch(`/local/session?origin=${encodeURIComponent(liveBase.replace(/\/$/, ""))}`)
|
|
270
|
+
.then((r) => r.json())
|
|
271
|
+
.then((d) => live && setCliSession(d ?? {}))
|
|
272
|
+
.catch(() => live && setCliSession({}));
|
|
273
|
+
read();
|
|
274
|
+
window.addEventListener("focus", read);
|
|
275
|
+
return () => {
|
|
276
|
+
live = false;
|
|
277
|
+
window.removeEventListener("focus", read);
|
|
278
|
+
};
|
|
279
|
+
}, [liveBase]);
|
|
256
280
|
|
|
257
281
|
const config = useMemo(
|
|
258
282
|
() => ({
|
|
@@ -102,16 +102,16 @@ fetch it as raw markdown.
|
|
|
102
102
|
## Step 3: Deploy loop (after the artifact is written)
|
|
103
103
|
|
|
104
104
|
Components and templates have NO scaffold command. Author them from the references
|
|
105
|
-
(mirror the closest existing artifact). Validation lives in
|
|
105
|
+
(mirror the closest existing artifact). Validation lives in the deploy lint (`unoverse deploy studio`): it
|
|
106
106
|
lints every definition before anything leaves the machine (0 errors required, doc-cited
|
|
107
107
|
messages; justify any warning), and the conformance checklist is
|
|
108
108
|
[UNOVERSE_CONFORMANCE](https://docs.unoverse.ai/unoverse/UNOVERSE_CONFORMANCE.md).
|
|
109
109
|
|
|
110
110
|
| Artifact | To see it live |
|
|
111
111
|
|---|---|
|
|
112
|
-
| Component / atom / template / style | Preview in Studio (`unoverse studio`), then
|
|
112
|
+
| Component / atom / template / style | Preview in Studio (`unoverse studio`), then `unoverse deploy studio`: the deploy lint gates it |
|
|
113
113
|
| Existing component restyle/edit only | takes effect live (the SDK reads `design/` directly) |
|
|
114
|
-
| Agent skill / prompt block |
|
|
114
|
+
| Agent skill / prompt block | `unoverse deploy studio`, same gate |
|
|
115
115
|
| Node | Studio's Nodes screen: Load sample → Run. A node is YAML; running it is the only proof (no build) |
|
|
116
116
|
|
|
117
117
|
**Close the loop with the developer.** After creating something visual, tell them how
|
|
@@ -32,7 +32,7 @@ It prints every part the agent can fill, its description, and its limits. Read t
|
|
|
32
32
|
output, not the YAML. Then run the design lint
|
|
33
33
|
(`node -e "import('./packages/base/src/lint/design/index.mjs').then(m=>{const r=m.lintDefinitions();console.log(r.problems.length);r.problems.forEach(p=>console.log(p.level,p.file,p.msg))})"`).
|
|
34
34
|
|
|
35
|
-
In a **Studio developer workspace**, the same linter runs as
|
|
35
|
+
In a **Studio developer workspace**, the same linter runs as the deploy lint (`unoverse deploy studio`); it
|
|
36
36
|
catches two of the five traps below by name. Read what it says rather than silencing it, and always do the final proof in
|
|
37
37
|
Step 6: one real fill.
|
|
38
38
|
|