@unoverse-platform/studio 0.1.90 → 0.1.92

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.
Files changed (132) hide show
  1. package/local/plugin.ts +21 -65
  2. package/package.json +13 -3
  3. package/vendor/base/src/agent-mcp/discovery.ts +203 -0
  4. package/vendor/base/src/agent-mcp/handoff.ts +56 -0
  5. package/vendor/base/src/agent-mcp/index.ts +44 -0
  6. package/vendor/base/src/agent-mcp/invoke.ts +153 -0
  7. package/vendor/base/src/agent-mcp/lean.ts +114 -0
  8. package/vendor/base/src/agent-mcp/memoryContext.ts +104 -0
  9. package/vendor/base/src/agent-mcp/memoryIngest.ts +88 -0
  10. package/vendor/base/src/agent-mcp/rowComponents.ts +234 -0
  11. package/vendor/base/src/agent-mcp/tools.ts +134 -0
  12. package/vendor/base/src/agent-mcp/types.ts +138 -0
  13. package/vendor/base/src/auth/auth.ts +307 -0
  14. package/vendor/base/src/auth/authConfig.ts +73 -0
  15. package/vendor/base/src/boot.ts +245 -0
  16. package/vendor/base/src/definitions/definitions.ts +18 -0
  17. package/vendor/base/src/definitions/dsPackage.ts +45 -0
  18. package/vendor/base/src/definitions/fsCache.ts +155 -0
  19. package/vendor/base/src/definitions/index.ts +19 -0
  20. package/vendor/base/src/definitions/inputs.ts +30 -0
  21. package/vendor/base/src/definitions/load/compile/component.ts +80 -0
  22. package/vendor/base/src/definitions/load/compile/index.ts +12 -0
  23. package/vendor/base/src/definitions/load/compile/sections.ts +132 -0
  24. package/vendor/base/src/definitions/load/compile/template.ts +153 -0
  25. package/vendor/base/src/definitions/load/expand/includes.ts +27 -0
  26. package/vendor/base/src/definitions/load/expand/index.ts +17 -0
  27. package/vendor/base/src/definitions/load/expand/inherit.ts +169 -0
  28. package/vendor/base/src/definitions/load/expand/referenced.ts +164 -0
  29. package/vendor/base/src/definitions/load/expand/refs.ts +106 -0
  30. package/vendor/base/src/definitions/load/expand/walk.ts +91 -0
  31. package/vendor/base/src/definitions/load/load.ts +487 -0
  32. package/vendor/base/src/definitions/model.ts +238 -0
  33. package/vendor/base/src/definitions/registry/catalog.ts +659 -0
  34. package/vendor/base/src/definitions/registry/homes.ts +252 -0
  35. package/vendor/base/src/definitions/theme.ts +383 -0
  36. package/vendor/base/src/executor.ts +284 -0
  37. package/vendor/base/src/http/uwsFetchAdapter.ts +397 -0
  38. package/vendor/base/src/items/baseVersion.ts +96 -0
  39. package/vendor/base/src/items/collect.ts +246 -0
  40. package/vendor/base/src/items/fingerprint.ts +34 -0
  41. package/vendor/base/src/items/publish.ts +212 -0
  42. package/vendor/base/src/lint/context.mjs +56 -0
  43. package/vendor/base/src/lint/design/defs.mjs +46 -0
  44. package/vendor/base/src/lint/design/file.mjs +737 -0
  45. package/vendor/base/src/lint/design/index.d.mts +30 -0
  46. package/vendor/base/src/lint/design/index.mjs +996 -0
  47. package/vendor/base/src/lint/design/tokens.mjs +128 -0
  48. package/vendor/base/src/lint/design/vocabulary.mjs +181 -0
  49. package/vendor/base/src/lint/design/walk.mjs +444 -0
  50. package/vendor/base/src/lint/index.mjs +28 -0
  51. package/vendor/base/src/lint/nodes/blocks.mjs +63 -0
  52. package/vendor/base/src/lint/nodes/index.d.mts +28 -0
  53. package/vendor/base/src/lint/nodes/index.mjs +82 -0
  54. package/vendor/base/src/lint/nodes/load.mjs +126 -0
  55. package/vendor/base/src/lint/nodes/node.mjs +764 -0
  56. package/vendor/base/src/lint/nodes/package.mjs +112 -0
  57. package/vendor/base/src/lint/nodes/schema.mjs +75 -0
  58. package/vendor/base/src/logger.ts +33 -0
  59. package/vendor/base/src/manifests/compose.ts +539 -0
  60. package/vendor/base/src/manifests/executor/callback.ts +107 -0
  61. package/vendor/base/src/manifests/executor/context.ts +110 -0
  62. package/vendor/base/src/manifests/executor/index.ts +47 -0
  63. package/vendor/base/src/manifests/executor/lookup.ts +21 -0
  64. package/vendor/base/src/manifests/executor/memory.ts +62 -0
  65. package/vendor/base/src/manifests/executor/promise.ts +71 -0
  66. package/vendor/base/src/manifests/executor/refire.ts +34 -0
  67. package/vendor/base/src/manifests/executor/send.ts +65 -0
  68. package/vendor/base/src/manifests/executor/session.ts +46 -0
  69. package/vendor/base/src/manifests/executor/toolBridge.ts +266 -0
  70. package/vendor/base/src/manifests/index.ts +280 -0
  71. package/vendor/base/src/manifests/integrity.ts +93 -0
  72. package/vendor/base/src/manifests/rows.ts +33 -0
  73. package/vendor/base/src/manifests/runtime/auth/aws.ts +240 -0
  74. package/vendor/base/src/manifests/runtime/auth/oauth.ts +76 -0
  75. package/vendor/base/src/manifests/runtime/authorize.ts +159 -0
  76. package/vendor/base/src/manifests/runtime/channels/final.ts +171 -0
  77. package/vendor/base/src/manifests/runtime/channels/service.ts +138 -0
  78. package/vendor/base/src/manifests/runtime/channels/workflow.ts +310 -0
  79. package/vendor/base/src/manifests/runtime/context.ts +156 -0
  80. package/vendor/base/src/manifests/runtime/docstore/handlers.ts +547 -0
  81. package/vendor/base/src/manifests/runtime/docstore/index.ts +135 -0
  82. package/vendor/base/src/manifests/runtime/docstore/sectionizer.ts +184 -0
  83. package/vendor/base/src/manifests/runtime/docstore/store.ts +172 -0
  84. package/vendor/base/src/manifests/runtime/docstore/types.ts +60 -0
  85. package/vendor/base/src/manifests/runtime/duplex/audioLane.ts +165 -0
  86. package/vendor/base/src/manifests/runtime/duplex/session.ts +511 -0
  87. package/vendor/base/src/manifests/runtime/events.ts +223 -0
  88. package/vendor/base/src/manifests/runtime/helpers.ts +53 -0
  89. package/vendor/base/src/manifests/runtime/http/allowedHosts.ts +118 -0
  90. package/vendor/base/src/manifests/runtime/http/request.ts +637 -0
  91. package/vendor/base/src/manifests/runtime/http/response.ts +162 -0
  92. package/vendor/base/src/manifests/runtime/index.ts +57 -0
  93. package/vendor/base/src/manifests/runtime/loops/chunk.ts +98 -0
  94. package/vendor/base/src/manifests/runtime/loops/loop.ts +191 -0
  95. package/vendor/base/src/manifests/runtime/loops/paginate.ts +143 -0
  96. package/vendor/base/src/manifests/runtime/loops/poll.ts +169 -0
  97. package/vendor/base/src/manifests/runtime/narrate.ts +53 -0
  98. package/vendor/base/src/manifests/runtime/state.ts +171 -0
  99. package/vendor/base/src/manifests/runtime/templating.ts +110 -0
  100. package/vendor/base/src/manifests/runtime/tools/regionSkills.ts +125 -0
  101. package/vendor/base/src/manifests/runtime/tools/toolloop.ts +550 -0
  102. package/vendor/base/src/manifests/runtime/usage.ts +87 -0
  103. package/vendor/base/src/manifests/source.ts +196 -0
  104. package/vendor/base/src/manifests/watch.ts +67 -0
  105. package/vendor/base/src/markdown/document.ts +141 -0
  106. package/vendor/base/src/paths.ts +180 -0
  107. package/vendor/base/src/platform/audioManager.ts +100 -0
  108. package/vendor/base/src/platform/clientTransport.ts +65 -0
  109. package/vendor/base/src/platform/executionContext.ts +157 -0
  110. package/vendor/base/src/platform/index.ts +10 -0
  111. package/vendor/base/src/platform/pluginAPI.ts +146 -0
  112. package/vendor/base/src/platform/redis.ts +54 -0
  113. package/vendor/base/src/platform/serviceCalls.ts +222 -0
  114. package/vendor/base/src/pluginBase.ts +248 -0
  115. package/vendor/base/src/plugins/README.md +57 -0
  116. package/vendor/base/src/plugins/discovery.ts +87 -0
  117. package/vendor/base/src/plugins/index.ts +16 -0
  118. package/vendor/base/src/plugins/loader.ts +179 -0
  119. package/vendor/base/src/plugins/redis.ts +36 -0
  120. package/vendor/base/src/plugins/state.ts +53 -0
  121. package/vendor/base/src/registry.ts +95 -0
  122. package/vendor/base/src/security/permissions.ts +93 -0
  123. package/vendor/base/src/security/rules.ts +73 -0
  124. package/vendor/base/src/template/SafeExpression.ts +556 -0
  125. package/vendor/base/src/template/StringTemplateResolver.ts +75 -0
  126. package/vendor/base/src/template/index.ts +22 -0
  127. package/vendor/base/src/types.ts +50 -0
  128. package/vendor/base/src/workflow/GetMemoryService.ts +158 -0
  129. package/vendor/base/src/workflow/PlatformMemoryCore.ts +449 -0
  130. package/vendor/base/src/workflow/routing.ts +122 -0
  131. package/vendor/sdk/lib/core/store.ts +26 -6
  132. package/local/mcp.ts +0 -265
@@ -0,0 +1,252 @@
1
+ /**
2
+ * The HOMES: where every kind of definition lives on disk, and how a ref resolves
3
+ * to a folder — the filesystem IS the registry. Split from definitions.ts
4
+ * 2026-08-29; the layout rules and addressing doctrine are unchanged.
5
+ */
6
+
7
+ import { readdirSync, existsSync, statSync } from "fs";
8
+ import { join, dirname } from "path";
9
+ import { fileURLToPath } from "url";
10
+ import { packagedDesignSystem } from "../dsPackage.js";
11
+ import { defPath } from "../fsCache.js";
12
+ import { NODES_HOME, DESIGN_HOME, INSTALLED_HOME, databaseOnly, designDir } from "../../paths.js";
13
+
14
+ const __dirname = dirname(fileURLToPath(import.meta.url));
15
+
16
+ // ---- Shared parts + per-org parts (the one rule of the layout) ----
17
+ //
18
+ // UNIVERSAL parts live ONCE at the top of `rx/`: atoms and components (a card is a
19
+ // card — the theme carries the brand) plus `rx/orgs/default/styles`, the DEFAULT token set that is
20
+ // also the starter you copy for a new client. CLIENT parts live in that client's
21
+ // folder `rx/orgs/<org>/`: its templates (apps) and its complete, self-contained
22
+ // styles. No fallback, no overlay — each thing has exactly one home.
23
+ //
24
+ // Addressing follows the homes: components/atoms are bare names
25
+ // (unoverse://components/card — unchanged for existing consumers); templates and
26
+ // themes are org-scoped `<org>/<name>` (unoverse://apps/sab/banktransfer,
27
+ // unoverse://theme/bpp/light). A bare template/app ref is the legacy compat path:
28
+ // it scans orgs alphabetically (deterministic; dies with the pre-org consumers).
29
+ // The filesystem stays the registry: a new client = a new folder under rx/orgs/.
30
+ // One home for the definitions root (paths.ts), so this loader and theme.ts
31
+ // cannot drift, and the standalone local Studio can point both at a dev folder.
32
+ const DESIGN = DESIGN_HOME;
33
+ const ORGS_ROOT = join(DESIGN, "orgs");
34
+
35
+ /**
36
+ * The rx tree HYDRATED FROM INSTALLED ROWS (items/hydrate.ts), laid out identically.
37
+ *
38
+ * SEARCHED LAST, ALWAYS. What the platform ships on disk wins over anything installed:
39
+ * a developer editing a component in the monorepo must see their own file, never a
40
+ * database's copy of an older one. That is the same precedence the node loader applies
41
+ * with [disk, rows], and putting the installed tree in its own root is what makes the
42
+ * rule structural rather than a comparison somebody has to remember to write.
43
+ *
44
+ * On a deployed universe the disk tiers are empty by design, so this is the only tier
45
+ * with anything in it and the ordering never comes up.
46
+ */
47
+ const INSTALLED_DESIGN = designDir(INSTALLED_HOME);
48
+
49
+ /** The shared homes for the universal kinds — the marketplace.
50
+ *
51
+ * Resolve each shared dir from the on-disk design/marketplace (monorepo dev) if present,
52
+ * else from a bundle beside the home node, else from what this universe INSTALLED (rows,
53
+ * hydrated). Mirrors theme.ts's styles fallback, and the getters resolve LAZILY so a tier
54
+ * that appears after boot is picked up on first use.
55
+ *
56
+ * NO npm TIER. This used to list `PLUGINS_DIR/node_modules/@unoverse-platform/marketplace/
57
+ * definitions` as a candidate, from when a universe acquired the design system by
58
+ * installing the marketplace package at boot (`plugins/startup.ts`, `CORE_PACKAGES`). That
59
+ * acquisition was deleted; the candidate outlived it, pointing at a directory nothing
60
+ * writes. MARKETPLACE.md §5 is the rule it contradicted: a universe takes content as ROWS,
61
+ * fetched from the marketplace URL at install time, and the image carries the engine, the
62
+ * server and Canvas — "they do not carry components, atoms, styles, skills, prompt blocks
63
+ * or nodes, all of which are rows". A dead path that would silently adopt an npm tree if
64
+ * one ever appeared is worse than no path. */
65
+ const MARKETPLACE = join(DESIGN, "marketplace");
66
+ const DS_BUNDLE_CANDIDATES = [join(NODES_HOME, "marketplace", "definitions")];
67
+ function marketplaceDir(kind: "components" | "atoms" | "templates"): string {
68
+ const onDisk = join(MARKETPLACE, kind);
69
+ // Under the switch the authored tiers are skipped, so what a deployed universe would
70
+ // resolve is what a developer resolves.
71
+ if (databaseOnly()) {
72
+ const installed = join(INSTALLED_DESIGN, "marketplace", kind);
73
+ return existsSync(installed) ? installed : onDisk;
74
+ }
75
+ if (existsSync(onDisk)) return onDisk;
76
+ for (const base of DS_BUNDLE_CANDIDATES) {
77
+ const p = join(base, kind);
78
+ if (existsSync(p)) return p;
79
+ }
80
+ // Last: what this universe was PUBLISHED. A deployed universe holds no rx and no
81
+ // bundle, so this is where its design system actually lives.
82
+ const installed = join(INSTALLED_DESIGN, "marketplace", kind);
83
+ if (existsSync(installed)) return installed;
84
+ const packaged = packagedDesignSystem();
85
+ if (packaged && existsSync(join(packaged, kind))) return join(packaged, kind);
86
+ return onDisk; // none present → the disk path (may be empty); nothing to resolve
87
+ }
88
+ export const SHARED_DIR = {
89
+ get component() {
90
+ return marketplaceDir("components");
91
+ },
92
+ get atom() {
93
+ return marketplaceDir("atoms");
94
+ },
95
+ get template() {
96
+ return marketplaceDir("templates");
97
+ },
98
+ } as Record<"component" | "atom" | "template", string>;
99
+
100
+ // rx-root entries that are NOT projects: the marketplace, the schema, and the legacy
101
+ // orgs/ container itself. Everything else at the root is a flat project (the target model).
102
+ const RESERVED_DESIGN = new Set(["marketplace", "_schema"]);
103
+
104
+ /** A project's on-disk home. During the flatten migration a project can live either FLAT
105
+ * at the rx root (`rx/<name>` — the target) or under the legacy `rx/orgs/<name>`. Flat
106
+ * wins. A project folder carries at least one of styles/apps/components. */
107
+ export function projectDir(name: string): string {
108
+ const installedProject = join(INSTALLED_DESIGN, name);
109
+ if (databaseOnly()) return installedProject;
110
+ const flat = join(DESIGN, name);
111
+ if (existsSync(flat)) return flat;
112
+ const legacy = join(ORGS_ROOT, name);
113
+ if (existsSync(legacy)) return legacy;
114
+ // Published, not authored here. Checked after both on-disk homes so a project being
115
+ // edited in the monorepo is never shadowed by the database's copy of it.
116
+ const installed = join(INSTALLED_DESIGN, name);
117
+ return existsSync(installed) ? installed : legacy;
118
+ }
119
+
120
+ /** The client projects, sorted: flat at the rx root (the target) PLUS any still under the
121
+ * legacy `rx/orgs/` (mid-migration). Excludes the marketplace, schema, and orgs/ folder.
122
+ * A flat candidate must look like a project (styles/apps/components) so a stray dir
123
+ * isn't mistaken for one. */
124
+ export function listOrgs(): string[] {
125
+ const isProject = (dir: string) =>
126
+ existsSync(join(dir, "styles")) || existsSync(join(dir, "apps")) || existsSync(join(dir, "components")) ||
127
+ existsSync(join(dir, "templates"));
128
+ const flat = existsSync(DESIGN) && !databaseOnly()
129
+ ? readdirSync(DESIGN, { withFileTypes: true })
130
+ .filter((e) => e.isDirectory() && !RESERVED_DESIGN.has(e.name) && isProject(join(DESIGN, e.name)))
131
+ .map((e) => e.name)
132
+ : [];
133
+ const legacy = existsSync(ORGS_ROOT) && !databaseOnly()
134
+ ? readdirSync(ORGS_ROOT, { withFileTypes: true }).filter((e) => e.isDirectory()).map((e) => e.name)
135
+ : [];
136
+ // Projects this universe was PUBLISHED but does not author. On a deployed universe
137
+ // these are the only ones there are. A name in both is listed once, and `projectDir`
138
+ // decides which home wins — on disk, every time.
139
+ const installed = existsSync(INSTALLED_DESIGN)
140
+ ? readdirSync(INSTALLED_DESIGN, { withFileTypes: true })
141
+ .filter((e) => e.isDirectory() && !RESERVED_DESIGN.has(e.name) && isProject(join(INSTALLED_DESIGN, e.name)))
142
+ .map((e) => e.name)
143
+ : [];
144
+ return [...new Set([...flat, ...legacy, ...installed])].sort();
145
+ }
146
+
147
+ /** Definition roots for guards/tooling sweeps: the shared tree (components/atoms in
148
+ * the marketplace, org: null) plus each org tree (its apps/). */
149
+ export function rxRoots(): { org: string | null; root: string }[] {
150
+ return [{ org: null, root: MARKETPLACE }, ...listOrgs().map((org) => ({ org, root: projectDir(org) }))];
151
+ }
152
+
153
+ /** Split an `<org>/<name>` ref; a bare name has no org. */
154
+ export function parseRef(ref: string): { org: string | null; name: string } {
155
+ const i = ref.indexOf("/");
156
+ return i === -1 ? { org: null, name: ref } : { org: ref.slice(0, i), name: ref.slice(i + 1) };
157
+ }
158
+
159
+ /** The APP homes a ref resolves against: its org's, or (bare ref) every org's. */
160
+ export function appDirs(refOrg: string | null): { org: string; dir: string }[] {
161
+ const orgs = refOrg === null ? listOrgs() : [refOrg];
162
+ return orgs
163
+ .filter((o) => existsSync(projectDir(o)))
164
+ .map((o) => ({ org: o, dir: join(projectDir(o), "apps") }));
165
+ }
166
+
167
+ /** The TEMPLATE homes a ref resolves against — the Template Model's kind
168
+ * (docs/unoverse/UNOVERSE_TEMPLATE_MODEL.md). Two tiers like components: the
169
+ * marketplace shelf (`design/marketplace/templates`, the BASE shapes) and the
170
+ * org packs (`design/<org>/templates`). The word is free in code because the app
171
+ * kind's in-code name is `app` (rename finished in code 2026-08-28; stored words
172
+ * unchanged until their own migration). */
173
+ export function templateDirs(refOrg: string | null): { org: string | undefined; dir: string }[] {
174
+ const shared = { org: undefined as string | undefined, dir: SHARED_DIR.template };
175
+ const orgDirs = (refOrg === null ? listOrgs() : [refOrg])
176
+ .filter((o) => existsSync(join(projectDir(o), "templates")))
177
+ .map((o) => ({ org: o as string | undefined, dir: join(projectDir(o), "templates") }));
178
+ return refOrg === null ? [shared, ...orgDirs] : [...orgDirs, shared];
179
+ }
180
+
181
+ /** The component homes a ref resolves against. Components live in TWO tiers: the
182
+ * marketplace (`rx/components`, generic, every org may use) and the org packs
183
+ * (`rx/orgs/<org>/components`, the client's own microapps — org-private). Names are
184
+ * unique WITHIN a tier, and an org may never shadow a marketplace name, so a bare
185
+ * ref resolves the shared home first, then the orgs; TWO orgs may carry the same
186
+ * name (each addressed `<org>/<name>`), so a bare ref matching more than one org is
187
+ * AMBIGUOUS — resolveComponentDirs turns that into a loud error, never first-wins.
188
+ * An org-prefixed ref searches its org first (shared last, as compat for prefixed
189
+ * refs to universal names). */
190
+ export function componentDirs(refOrg: string | null): { org: string | undefined; dir: string }[] {
191
+ const shared = { org: undefined as string | undefined, dir: SHARED_DIR.component };
192
+ const orgDirs = (refOrg === null ? listOrgs() : [refOrg])
193
+ .filter((o) => existsSync(join(projectDir(o), "components")))
194
+ .map((o) => ({ org: o as string | undefined, dir: join(projectDir(o), "components") }));
195
+ // TEMPLATE-LOCAL COMPONENTS (§9b.7, ruled 2026-08-29): parts that exist only for
196
+ // one template live in the template's own components/ folder, addressed
197
+ // `<template>/<part>` (email-digest/masthead). When the qualifier names no org
198
+ // pack, the template homes answer instead — the folder is the namespace.
199
+ if (refOrg !== null && orgDirs.length === 0) {
200
+ for (const { dir } of templateDirs(null)) {
201
+ const tdir = join(dir, refOrg, "components");
202
+ if (existsSync(tdir)) return [{ org: refOrg, dir: tdir }, shared];
203
+ }
204
+ }
205
+ return refOrg === null ? [shared, ...orgDirs] : [...orgDirs, shared];
206
+ }
207
+
208
+ /** True when `dir` holds the component `<lower>` in either authored form. */
209
+ export function componentHit(dir: string, lower: string): boolean {
210
+ return !!defPath(dir, lower) || !!defPath(join(dir, lower), lower);
211
+ }
212
+
213
+ /** The dirs a component ref may actually load from. Qualified refs pass through
214
+ * untouched. A bare ref narrows to the ONE home that carries the name: the CONTEXT
215
+ * org first when given (the org of the definition/app doing the asking — a cloned
216
+ * pack's own manifests and layouts say `course-card` and mean their own), then the
217
+ * marketplace (an org may never shadow it, so these two never conflict); a single
218
+ * org match resolves for compat with pre-org refs (saved workflows, COMPONENT_INIT
219
+ * `type`); a name in two or more orgs WITH no context throws, naming the qualified
220
+ * candidates — the caller must say which org it means. */
221
+ export function resolveComponentDirs(
222
+ dirs: { org: string | undefined; dir: string }[],
223
+ refOrg: string | null,
224
+ lower: string,
225
+ contextOrg?: string,
226
+ ): { org: string | undefined; dir: string }[] {
227
+ if (refOrg !== null) return dirs;
228
+ const hits = dirs.filter(({ dir }) => componentHit(dir, lower));
229
+ const contextHit = contextOrg ? hits.find((h) => h.org === contextOrg) : undefined;
230
+ if (contextHit) return [contextHit];
231
+ const sharedHit = hits.find((h) => h.org === undefined);
232
+ if (sharedHit) return [sharedHit];
233
+ if (hits.length > 1)
234
+ throw new Error(
235
+ `component ref "${lower}" is ambiguous: it exists in ${hits.map((h) => `${h.org}/${lower}`).join(", ")}. Use the org-qualified ref.`,
236
+ );
237
+ return hits.length === 1 ? hits : dirs;
238
+ }
239
+
240
+ /** Resolve a component ref (bare or `<org>/<name>`, either tier) to its ON-DISK
241
+ * FOLDER — the home of its manifest and any lifecycle handler files (onstart.js …).
242
+ * Returns null for a flat/marketplace component with no folder. Used by the MCP
243
+ * render path to run component lifecycle handlers. */
244
+ export function componentFolder(ref: string): string | null {
245
+ const { org: refOrg, name } = parseRef(ref);
246
+ const lower = name.toLowerCase();
247
+ for (const { dir } of resolveComponentDirs(componentDirs(refOrg), refOrg, lower)) {
248
+ const folder = join(dir, lower);
249
+ if (defPath(folder, "manifest") || defPath(folder, lower)) return folder;
250
+ }
251
+ return null;
252
+ }
@@ -0,0 +1,383 @@
1
+ /**
2
+ * Runtime theme resolver — the MCP server’s live, compiled view of the org styles sets (rx/orgs/<org>/styles).
3
+ *
4
+ * The server OWNS THE VISUAL. It reads the design tokens (rx/orgs/<org>/styles, the design
5
+ * system source) at request time and serves the RESOLVED theme to channels at
6
+ * `unoverse://theme/{name}`. The SDK owns ZERO token values — it FETCHES this
7
+ * (UnoverseClient.readTheme). So a brand/spacing/type change in an org’s styles is
8
+ * refresh-only, exactly like a definition change — never a SDK rebuild
9
+ * (UNOVERSE_SPEC §2d-1).
10
+ *
11
+ * This is the same resolution `stylegen` performs — but SERVED, not baked into
12
+ * the SDK bundle. stylegen now imports this so there is ONE resolver, used only
13
+ * to EXPORT tokens for the native SDKs (web fetches live).
14
+ */
15
+ import { readdirSync, existsSync, statSync } from "fs";
16
+ import { join, dirname } from "path";
17
+ import { fileURLToPath } from "url";
18
+ import { readDefCached, defPath, isDefFile, defName, dirSignature, cachedBySignature } from "./fsCache.js";
19
+ import { packagedDesignSystem } from "./dsPackage.js";
20
+ // Icon glyphs are sourced from a pack HERE (control plane), never in the SDK bundle:
21
+ // rx/ lists semantic→lucide names; we resolve each to served `theme.icons` data.
22
+ import { icons as lucideIcons } from "lucide";
23
+
24
+ // lucide renders stroke glyphs with these standard svg attrs (it ships no runtime
25
+ // `defaultAttributes`). Served as DATA so the SDK applies them verbatim — it authors none.
26
+ const LUCIDE_SVG_ATTRS = { fill: "none", stroke: "currentColor", strokeWidth: 2, strokeLinecap: "round", strokeLinejoin: "round" };
27
+
28
+ const __dirname = dirname(fileURLToPath(import.meta.url));
29
+ // The marketplace FOUNDATION (base + semantic + default themes) lives at
30
+ // `rx/marketplace/styles` — the shared token contract every org inherits. An org
31
+ // (`rx/orgs/<org>/styles`) supplies ONLY what it customises (typically just its brand
32
+ // themes); base/semantic tokens it omits are INHERITED from the foundation via
33
+ // buildTheme's per-token cascade. A theme is addressed `<org>/<name>`
34
+ // (unoverse://theme/sab/light); a bare name (or the `default` org) = the foundation's
35
+ // own default set. Token NAMES are the shared contract (components reference them);
36
+ // VALUES are each org's own — that's how two clients get different fonts/colors/spacing
37
+ // from the same components, without re-copying the foundation.
38
+ import { listOrgs, projectDir } from "./definitions.js";
39
+ import { NODES_HOME, PLUGINS_DIR, DESIGN_HOME, INSTALLED_HOME, databaseOnly, designDir } from "../paths.js";
40
+
41
+ const DESIGN = DESIGN_HOME;
42
+ export const DEFAULT_ORG = "default";
43
+
44
+ /** The styles dir for a project, or null if it has none. `projectDir` resolves the
45
+ * project whether it's flat at the rx root (the target) or under legacy `rx/orgs/`. */
46
+ function orgStyles(org: string): string | null {
47
+ const dir = join(projectDir(org), "styles");
48
+ return existsSync(dir) ? dir : null;
49
+ }
50
+
51
+ /**
52
+ * The marketplace FOUNDATION — base + semantic + default themes, the shared token
53
+ * contract every org inherits (buildTheme's fallback layer) AND the `default` theme set
54
+ * itself. Its home is now `rx/marketplace/styles` (the honest marketplace folder, no
55
+ * longer an org costume under `orgs/default`). Resolution mirrors `findDesignComponentsDir`:
56
+ * the on-disk marketplace dir wins (dev/local), else the bundled package — the monorepo
57
+ * home node, then the installed marketplace package. This lets the foundation ship WITH
58
+ * the package (no `rx/marketplace` on disk) once purged from the image (Phase 4).
59
+ */
60
+ const DS_STYLES_CANDIDATES = [
61
+ join(NODES_HOME, "marketplace", "definitions", "styles"),
62
+ join(PLUGINS_DIR, "node_modules", "@unoverse-platform", "marketplace", "definitions", "styles"),
63
+ ];
64
+ /**
65
+ * WHAT THIS UNIVERSE WAS INSTALLED — the tier this function was missing.
66
+ *
67
+ * The design system is CHOSEN now, not shipped (MARKETPLACE.md §3), so on a deployed
68
+ * universe it is neither on disk nor in a bundle: it is rows, unpacked here. `definitions.ts`
69
+ * already ends its search on this tier for components and atoms, and its comment says it
70
+ * "mirrors theme.ts's styles fallback" — which was true when written and stopped being
71
+ * true when that tier was added there and not here.
72
+ *
73
+ * The cost was total and silent. A universe would install the design system, hold all 49
74
+ * rows, render components from them, and still resolve NO themes: `/dev/themes` answered
75
+ * `{"themes":[]}` and Studio sat on "loading theme from mcp…" for ever, because a screen
76
+ * with no tokens cannot draw. Installed, present, and unreachable.
77
+ */
78
+ const INSTALLED_STYLES = join(designDir(INSTALLED_HOME), "marketplace", "styles");
79
+
80
+ function foundationStyles(): string | null {
81
+ // Under the switch the authored tiers are skipped, so what a deployed universe would
82
+ // resolve is what a developer resolves. Same shape as `marketplaceDir`.
83
+ if (databaseOnly()) return existsSync(INSTALLED_STYLES) ? INSTALLED_STYLES : null;
84
+ const designDs = join(DESIGN, "marketplace", "styles");
85
+ if (existsSync(designDs)) return designDs; // on-disk marketplace wins (dev/local)
86
+ for (const c of DS_STYLES_CANDIDATES) if (existsSync(c)) return c;
87
+ // SEARCHED LAST, so anything on disk still wins — the same precedence the node loader
88
+ // applies with [disk, rows]. On a deployed universe the tiers above are empty by design
89
+ // and this is the only one with anything in it.
90
+ if (existsSync(INSTALLED_STYLES)) return INSTALLED_STYLES;
91
+ // Last: the copy the running HOST carries. Studio has the design system in its own
92
+ // node_modules and no project-local tier will ever hold one (dsPackage.ts).
93
+ const packaged = packagedDesignSystem();
94
+ if (packaged && existsSync(join(packaged, "styles"))) return join(packaged, "styles");
95
+ return null;
96
+ }
97
+
98
+ /** Styles home for an org REF — the marketplace foundation for the `default` ref,
99
+ * else the org's own folder. (`default` is no longer a folder under `orgs/`; it IS the
100
+ * marketplace, so its themes resolve from the foundation.) */
101
+ function stylesForOrg(org: string): string | null {
102
+ return org === DEFAULT_ORG ? foundationStyles() : orgStyles(org);
103
+ }
104
+
105
+ /** Every org that has a styles set: the default first, then the clients. */
106
+ export function themeOrgs(): string[] {
107
+ return [DEFAULT_ORG, ...listOrgs().filter((o) => o !== DEFAULT_ORG && orgStyles(o) !== null)];
108
+ }
109
+
110
+ export interface ResolvedTheme {
111
+ /** Webfont stylesheet URLs (`fonts.stylesheets` — e.g. Google Fonts css2). Served
112
+ * DATA like everything else; the SDK injects a <link> per URL into document.head
113
+ * (fonts must register at document level — shadow roots can't). */
114
+ fonts: string[];
115
+ color: Record<string, string>;
116
+ space: Record<string, string>;
117
+ radius: Record<string, string>;
118
+ shadow: Record<string, string>;
119
+ borderWidth: Record<string, string>;
120
+ /** Font weights (`font.weight.*`) — SERVED, like every other token.
121
+ *
122
+ * These were the one scale the SDK held as a literal map of four names, so the six the
123
+ * foundation actually defines were never all reachable and an org that added its own
124
+ * (yasisland's `extrabold`) got silence: the SDK passed the unknown word to CSS, CSS
125
+ * dropped the declaration, and the text rendered at whatever it inherited. */
126
+ weight: Record<string, string | number>;
127
+ lineHeight: Record<string, string | number>;
128
+ text: Record<string, Record<string, string | number>>;
129
+ skeleton: Record<string, unknown>;
130
+ prose: Record<string, unknown>;
131
+ /** STANDARD APP SIZES (semantic/app-sizes.json) — the named width blocks `appWidth`
132
+ * references ("chat"/"rail"/"panel" → raw host-facing CSS). Resolved by the SDK
133
+ * like every other token: ONE pipeline, no serve-time rewriting of definitions. */
134
+ appSize: Record<string, string>;
135
+ /** Grid behaviour (`grid.*`) — currently `stackBelow`, the width at which a columns
136
+ * grid stacks. Served, so the SDK holds no threshold of its own. */
137
+ grid: Record<string, string>;
138
+ /** PAGE WIDTHS by name (`layout.*`) — aliases onto the space scale so a page-level cap
139
+ * reads as what it is. Element sizes stay scale steps; see semantic/layout. */
140
+ layout: Record<string, string>;
141
+ /** BREAKPOINTS (`breakpoint.*`) — the named width bands responsive style maps key
142
+ * on (semantic/breakpoints). Container widths, served; the SDK holds none. */
143
+ breakpoint: Record<string, string>;
144
+ keyframes: Record<string, Record<string, Record<string, string>>>;
145
+ icons: Record<string, { viewBox?: string; attrs?: Record<string, unknown>; children?: [string, Record<string, unknown>][] }>;
146
+ root: Record<string, unknown>;
147
+ }
148
+
149
+ type Json = any;
150
+
151
+ /** Flatten a token file into { "dotted.path": rawValue } (rawValue = $value). */
152
+ function collect(obj: Json, prefix: string, out: Record<string, unknown>): void {
153
+ for (const [k, v] of Object.entries(obj)) {
154
+ if (k.startsWith("$")) continue;
155
+ if (v && typeof v === "object" && "$value" in (v as Json)) {
156
+ out[prefix ? `${prefix}.${k}` : k] = (v as Json).$value;
157
+ } else if (v && typeof v === "object") {
158
+ collect(v, prefix ? `${prefix}.${k}` : k, out);
159
+ }
160
+ }
161
+ }
162
+
163
+ function readTokenDir(dir: string, into: Record<string, unknown>): void {
164
+ if (!existsSync(dir)) return;
165
+ for (const f of readdirSync(dir).filter(isDefFile)) {
166
+ collect(readDefCached(join(dir, f)), "", into);
167
+ }
168
+ }
169
+
170
+ /** Resolve one raw value: alias → looked-up value (recursive), composite → per-field, array → join. */
171
+ function resolveValue(raw: unknown, reg: Record<string, unknown>, seen = new Set<string>()): unknown {
172
+ if (typeof raw === "string") {
173
+ // Replace EVERY {token.path} occurrence — supports composite values like
174
+ // "{space.3} {space.6}" or "{border.width.thick} solid {color.action.primary}".
175
+ if (!raw.includes("{")) return raw;
176
+ return raw.replace(/\{([^}]+)\}/g, (_, path) => {
177
+ if (seen.has(path)) throw new Error(`Cyclic token alias: ${path}`);
178
+ if (!(path in reg)) throw new Error(`Unknown token alias: {${path}}`);
179
+ return String(resolveValue(reg[path], reg, new Set(seen).add(path)));
180
+ });
181
+ }
182
+ if (Array.isArray(raw)) return raw.map((x) => resolveValue(x, reg, new Set(seen)));
183
+ if (raw && typeof raw === "object") {
184
+ const o: Record<string, unknown> = {};
185
+ for (const [k, v] of Object.entries(raw)) o[k] = resolveValue(v, reg, new Set(seen));
186
+ return o;
187
+ }
188
+ return raw;
189
+ }
190
+
191
+ const flat = (v: unknown) => (Array.isArray(v) ? v.join(", ") : v);
192
+
193
+ /** Build one resolved theme from an org tree, with the DEFAULT set as the foundation.
194
+ *
195
+ * Phase 3 cascade ("org = just a theme"): the default set's `base`/`semantic` (the shared
196
+ * marketplace contract) is the foundation. An org supplies only what differs — typically
197
+ * just its `themes/<name>.json` brand file — and inherits the rest.
198
+ *
199
+ * The fallback is PER TOKEN, not per layer. This comment used to say the opposite ("an org
200
+ * that ships its own `base`/`semantic` fully replaces the default's for that layer"), which
201
+ * describes an earlier design and would mean an org adding one `base/color.yaml` silently
202
+ * lost every other base token. The body below reads foundation → org into ONE flat registry
203
+ * keyed by dotted path, so an org token overrides the foundation's and an org OMITS a token
204
+ * to inherit it. An org may therefore name a single token in a file whose siblings it wants
205
+ * to keep. Full forks (every default token present in the org) still resolve byte-identically,
206
+ * which is why both readings looked correct against the existing orgs.
207
+ */
208
+ function buildTheme(styles: string, themeFile: string): ResolvedTheme {
209
+ const reg: Record<string, unknown> = {};
210
+ // PER-TOKEN cascade: read the foundation's base+semantic FIRST, then the org's ON TOP —
211
+ // the flat token registry is keyed by dotted path, so an org token overrides the
212
+ // foundation's and an org OMITS a token to inherit it. An org therefore keeps only the
213
+ // files/tokens it customises; everything else comes from the foundation. Existing full
214
+ // forks (every default token present in the org) resolve byte-identically (verified).
215
+ const defaultStyles = foundationStyles();
216
+ if (defaultStyles && defaultStyles !== styles) {
217
+ readTokenDir(join(defaultStyles, "base"), reg);
218
+ readTokenDir(join(defaultStyles, "semantic"), reg);
219
+ }
220
+ readTokenDir(join(styles, "base"), reg);
221
+ readTokenDir(join(styles, "semantic"), reg);
222
+ collect(readDefCached(themeFile), "", reg);
223
+
224
+ const bucket = (prefix: string, mapVal: (v: unknown) => unknown) => {
225
+ const out: Record<string, string> = {};
226
+ for (const path of Object.keys(reg)) {
227
+ if (!path.startsWith(prefix)) continue;
228
+ out[path.slice(prefix.length)] = mapVal(resolveValue(reg[path], reg)) as string;
229
+ }
230
+ return out;
231
+ };
232
+
233
+ return {
234
+ fonts: ((resolveValue(reg["fonts.stylesheets"], reg) as string[] | undefined) ?? []).filter((u) => typeof u === "string"),
235
+ color: bucket("color.", (v) => v),
236
+ space: bucket("space.", (v) => v),
237
+ radius: bucket("radius.", (v) => v),
238
+ appSize: bucket("appSize.", (v) => v),
239
+ grid: bucket("grid.", (v) => v),
240
+ breakpoint: bucket("breakpoint.", (v) => v),
241
+ layout: bucket("layout.", (v) => v),
242
+ shadow: bucket("shadow.", (v) => v),
243
+ borderWidth: bucket("border.width.", (v) => v),
244
+ weight: bucket("font.weight.", (v) => v) as Record<string, string | number>,
245
+ lineHeight: bucket("font.lineHeight.", (v) => v) as Record<string, string | number>,
246
+ text: (() => {
247
+ const out: Record<string, Record<string, string | number>> = {};
248
+ for (const path of Object.keys(reg)) {
249
+ if (!path.startsWith("text.")) continue;
250
+ const v = resolveValue(reg[path], reg) as Record<string, unknown>;
251
+ out[path.slice("text.".length)] =
252
+ v && typeof v === "object"
253
+ ? (Object.fromEntries(Object.entries(v).map(([k, x]) => [k, flat(x)])) as Record<string, string | number>)
254
+ : (v as never);
255
+ }
256
+ return out;
257
+ })(),
258
+ // Skeleton recipe — dimensions as DATA (gap/fill/radius + per-variant `bars`).
259
+ // Arrays are kept as-is (NOT flattened); the SDK reads them verbatim.
260
+ skeleton: (() => {
261
+ const out: Record<string, unknown> = {};
262
+ for (const path of Object.keys(reg)) {
263
+ if (!path.startsWith("skeleton.")) continue;
264
+ out[path.slice("skeleton.".length)] = resolveValue(reg[path], reg);
265
+ }
266
+ return out;
267
+ })(),
268
+ // Prose recipe — the Markdown primitive's element styling as DATA (link/list/
269
+ // table/image/heading-map). The SDK reads these verbatim; it authors nothing.
270
+ prose: (() => {
271
+ const out: Record<string, unknown> = {};
272
+ for (const path of Object.keys(reg)) {
273
+ if (!path.startsWith("prose.")) continue;
274
+ out[path.slice("prose.".length)] = resolveValue(reg[path], reg);
275
+ }
276
+ return out;
277
+ })(),
278
+ // Keyframes — named animation recipes as DATA (each a stop→decls map). The SDK
279
+ // serializes these into @keyframes once per root (keyframesCss); it authors none.
280
+ keyframes: (() => {
281
+ const out: Record<string, unknown> = {};
282
+ for (const path of Object.keys(reg)) {
283
+ if (!path.startsWith("keyframes.")) continue;
284
+ out[path.slice("keyframes.".length)] = resolveValue(reg[path], reg);
285
+ }
286
+ return out;
287
+ })() as Record<string, Record<string, Record<string, string>>>,
288
+ // Icons — semantic name (rx/) → pack glyph name → SERVED element DATA. The pack
289
+ // (lucide) lives ONLY here on the control plane; the SDK gets `{viewBox, attrs,
290
+ // children}` and renders it generically. To add a pack, adapt its data to this same
291
+ // shape below — the SDK and definitions never change (see iconFromPack).
292
+ icons: (() => {
293
+ const out: Record<string, unknown> = {};
294
+ for (const path of Object.keys(reg)) {
295
+ if (!path.startsWith("icons.")) continue;
296
+ const glyph = iconFromPack(String(resolveValue(reg[path], reg)));
297
+ if (glyph) out[path.slice("icons.".length)] = glyph;
298
+ }
299
+ return out;
300
+ })() as Record<string, { viewBox?: string; attrs?: Record<string, unknown>; children?: [string, Record<string, unknown>][] }>,
301
+ // Base render-root CSS (font-smoothing etc.) — served; the SDK spreads it at the root.
302
+ root: (resolveValue(reg["root"], reg) as Record<string, unknown>) ?? {},
303
+ };
304
+ }
305
+
306
+ /**
307
+ * Adapt a pack's icon → the SERVED shape `{ viewBox, attrs, children }` (pack-agnostic;
308
+ * the SDK only ever sees this shape). lucide gives raw `[tag, attrs][]` elements + a
309
+ * 24×24 stroke convention. Another pack = another branch here that normalizes to the
310
+ * same shape — nothing in the SDK or in rx/ definitions changes.
311
+ */
312
+ function iconFromPack(name: string): { viewBox: string; attrs: Record<string, unknown>; children: [string, Record<string, unknown>][] } | null {
313
+ const node = (lucideIcons as Record<string, [string, Record<string, unknown>][]>)[name];
314
+ if (!node) return null;
315
+ return { viewBox: "0 0 24 24", attrs: LUCIDE_SVG_ATTRS, children: node };
316
+ }
317
+
318
+ /**
319
+ * THE ICON VOCABULARY a content record may name, derived from the SERVED set.
320
+ *
321
+ * A promoted row carries `features: [{ title, description, icon }]`, where the icon is
322
+ * chosen by the extractor and rendered by a card from `theme.icons`. Those are two ends of
323
+ * one contract, and when the extraction prompt carried its own hand-written list they
324
+ * drifted: the model named icons the renderer could not resolve, and every feature drew a
325
+ * blank chip. Silently, because a missing glyph is not an error anywhere.
326
+ *
327
+ * So the writer asks the renderer. Add a name to `rx/styles/semantic/icons.yaml` and the
328
+ * extractor may use it on the next call, with no prompt to edit and nothing to keep in step.
329
+ * Empty (a deployment with no rx tree) leaves the schema to describe the field in words.
330
+ */
331
+ export function iconVocabulary(org = "default"): string[] {
332
+ const theme = resolveTheme(`${org}/light`);
333
+ return theme ? Object.keys(theme.icons).sort() : [];
334
+ }
335
+
336
+ /** The theme names available — `<org>/<name>` across every styles set, or one org's
337
+ * bare names when `org` is given (each set typically ships light + dark). */
338
+ export function listThemeNames(org?: string): string[] {
339
+ const orgs = org ? [org] : themeOrgs();
340
+ const out: string[] = [];
341
+ for (const o of orgs) {
342
+ const styles = stylesForOrg(o);
343
+ if (!styles) continue;
344
+ const themes = join(styles, "themes");
345
+ if (!existsSync(themes)) continue;
346
+ for (const f of readdirSync(themes).filter(isDefFile)) {
347
+ const name = defName(f);
348
+ out.push(org ? name : `${o}/${name}`);
349
+ }
350
+ }
351
+ return out;
352
+ }
353
+
354
+ /** Resolve a theme by `<org>/<name>` ref (bare name = the default set), or null if it
355
+ * doesn't exist. Resolution (token expansion, icon glyphs) is the expensive part and
356
+ * is pure in the styles files — memoized on their mtimes, so a token edit still
357
+ * hot-reloads (mtime moves → rebuild) while steady-state reads pay a stat sweep
358
+ * instead of a full build. */
359
+ export function resolveTheme(ref: string): ResolvedTheme | null {
360
+ const i = ref.indexOf("/");
361
+ const [org, name] = i === -1 ? [DEFAULT_ORG, ref] : [ref.slice(0, i), ref.slice(i + 1)];
362
+ const styles = stylesForOrg(org);
363
+ if (!styles) return null;
364
+ const file = defPath(join(styles, "themes"), name);
365
+ if (!file) return null;
366
+ // Signature includes the DEFAULT foundation dirs too — a theme-only org inherits them
367
+ // (buildTheme), so an edit to default's base/semantic must invalidate this org's cache.
368
+ const defaultStyles = foundationStyles();
369
+ const sigDirs = [join(styles, "base"), join(styles, "semantic")];
370
+ if (defaultStyles && defaultStyles !== styles) sigDirs.push(join(defaultStyles, "base"), join(defaultStyles, "semantic"));
371
+ const sig = `${dirSignature(sigDirs)};${file}:${statSync(file).mtimeMs}`;
372
+ return cachedBySignature(`theme:${org}/${name}`, sig, () => buildTheme(styles, file));
373
+ }
374
+
375
+ /** Resolve every theme (for stylegen's native-SDK export). */
376
+ export function resolveThemes(): Record<string, ResolvedTheme> {
377
+ const out: Record<string, ResolvedTheme> = {};
378
+ for (const name of listThemeNames()) {
379
+ const t = resolveTheme(name);
380
+ if (t) out[name] = t;
381
+ }
382
+ return out;
383
+ }