@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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unoverse-platform/studio",
3
- "version": "0.1.59",
3
+ "version": "0.1.61",
4
4
  "type": "module",
5
5
  "description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
6
6
  "scripts": {
@@ -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
- if (!cancelled) setError(e instanceof Error ? e.message : String(e));
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
- // Token source: prefer the real OIDC access token; fall back to a pasted JWT.
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 Studio: its publish step
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 publish from Studio: the publish lint gates it |
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 | publish from Studio, same gate |
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 Studio's publish lint; it
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