@unoverse-platform/studio 0.1.45 → 0.1.47

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/bin/studio.mjs CHANGED
@@ -21,6 +21,7 @@ import { dirname, resolve, join, basename } from "path";
21
21
  import { fileURLToPath } from "url";
22
22
  import { existsSync, readdirSync } from "fs";
23
23
  import { createInterface } from "readline/promises";
24
+ import { designHome, hasDesignHome } from "../local/design-home.mjs";
24
25
  import { createFirstProject, checkProjectName } from "../local/scaffold.mjs";
25
26
 
26
27
  const here = dirname(fileURLToPath(import.meta.url));
@@ -37,11 +38,12 @@ const appRoot = resolve(here, "..");
37
38
  * instruction; the folder says where and its name says what. The only question left is the
38
39
  * one it cannot answer for you: a folder whose name no project can carry.
39
40
  */
40
- /** The same walk `catalog.ts` does: the nearest folder above us holding rx/. */
41
+ /** The same walk `catalog.ts` does: the nearest folder above us holding a design tree
42
+ * (design/, or the legacy rx/). */
41
43
  function findProjectRootFromCwd() {
42
44
  for (let dir = process.cwd(); ; ) {
43
- if (existsSync(join(dir, "rx"))) return dir;
44
- if (existsSync(join(dir, "apps/unoverse/rx"))) return join(dir, "apps/unoverse");
45
+ if (hasDesignHome(dir)) return dir;
46
+ if (hasDesignHome(join(dir, "apps/unoverse"))) return join(dir, "apps/unoverse");
45
47
  const parent = resolve(dir, "..");
46
48
  if (parent === dir) return process.cwd();
47
49
  dir = parent;
@@ -64,7 +66,7 @@ function findProjectBelowCwd() {
64
66
  const cwd = process.cwd();
65
67
  const found = readdirSync(cwd, { withFileTypes: true })
66
68
  .filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules")
67
- .filter((e) => existsSync(join(cwd, e.name, "rx")))
69
+ .filter((e) => hasDesignHome(join(cwd, e.name)))
68
70
  .map((e) => e.name);
69
71
 
70
72
  if (found.length === 1) return join(cwd, found[0]);
@@ -80,7 +82,7 @@ async function scaffoldIfEmpty() {
80
82
  // The same walk findProjectRoot does. A project ABOVE us is a project; do not offer to
81
83
  // make a second one inside it.
82
84
  for (let dir = cwd; ; ) {
83
- if (existsSync(join(dir, "rx")) || existsSync(join(dir, "apps/unoverse/rx"))) return;
85
+ if (hasDesignHome(dir) || hasDesignHome(join(dir, "apps/unoverse"))) return;
84
86
  const parent = resolve(dir, "..");
85
87
  if (parent === dir) break;
86
88
  dir = parent;
@@ -122,7 +124,7 @@ async function scaffoldIfEmpty() {
122
124
  // being named or why one was needed. The name is the folder under rx/ AND the org
123
125
  // column on everything that project publishes, which is why the tool cannot pick it.
124
126
  console.log(`\n Setting up Studio here.\n\n ${cwd}\n`);
125
- console.log(` Your components live in rx/<project>/, and that name is the org they`);
127
+ console.log(` Your components live in design/<project>/, and that name is the org they`);
126
128
  console.log(` publish under. This folder is "${derived}", which cannot be one.\n`);
127
129
  const rl = createInterface({ input: process.stdin, output: process.stdout });
128
130
  for (;;) {
@@ -140,7 +142,7 @@ async function scaffoldIfEmpty() {
140
142
  // publishes, so a flat rx/ could hold exactly one project and would read `components`
141
143
  // and `templates` as orgs.
142
144
  createFirstProject(home, name);
143
- console.log(`\n Created rx/${name}/, prompts/ and nodes/ in ${cwd}\n`);
145
+ console.log(`\n Created design/${name}/, prompts/ and nodes/ in ${cwd}\n`);
144
146
  }
145
147
 
146
148
  await scaffoldIfEmpty();
@@ -148,7 +150,7 @@ await scaffoldIfEmpty();
148
150
  /**
149
151
  * POINT `base` AT THIS DEVELOPER'S PROJECT, before anything imports it.
150
152
  *
151
- * `paths.ts` snapshots its exports at import time (`export const RX_HOME = paths.rx`), so
153
+ * `paths.ts` snapshots its exports at import time (`export const DESIGN_HOME = paths.rx`), so
152
154
  * `setPaths()` called later updates an object nobody reads any more. The only hook that
153
155
  * lands is the environment, read by `findHome()` on first import.
154
156
  *
@@ -159,7 +161,7 @@ await scaffoldIfEmpty();
159
161
  */
160
162
  const projectRoot = findProjectRootFromCwd();
161
163
  process.env.UNOVERSE_HOME = projectRoot;
162
- console.log(`\n Studio reading: ${join(projectRoot, "rx")}\n`);
164
+ console.log(`\n Studio reading: ${designHome(projectRoot)}\n`);
163
165
 
164
166
  const server = await createServer({
165
167
  // The APP is the vite root; the developer's project is found by walking up from the
@@ -2,8 +2,8 @@
2
2
  * COMPLETION VOCABULARY for the source pane.
3
3
  *
4
4
  * THERE IS ONE LINTER, and it is not here. The rules live in
5
- * `@unoverse-platform/base/src/lint/rx/`, and the editor lints unsaved text by handing that
6
- * library an overlay (`lintDefinitions(rxHome, { overlay })`), so the squiggles a developer
5
+ * `@unoverse-platform/base/src/lint/design/`, and the editor lints unsaved text by handing that
6
+ * library an overlay (`lintDefinitions(designRoot, { overlay })`), so the squiggles a developer
7
7
  * sees are the same rules publish and CI apply. An approximation of those rules in the
8
8
  * editor would be a second linter, which is exactly what the shared library exists to
9
9
  * prevent. This file adds no rule and judges nothing.
@@ -18,6 +18,7 @@
18
18
  *
19
19
  * See docs/architecture/LOCAL_STUDIO.md §Authoring in Studio.
20
20
  */
21
+ import { designHome } from "./design-home.mjs";
21
22
  import { resolve } from "path";
22
23
  import { existsSync, readdirSync } from "fs";
23
24
 
@@ -88,7 +89,7 @@ export function vocabulary(
88
89
 
89
90
  /** Atom names off disk, for completion of `Ref.ref`. */
90
91
  export function atomNames(projectRoot: string): { name: string }[] {
91
- const dirs = [resolve(projectRoot, "rx/marketplace/atoms"), resolve(projectRoot, "rx/atoms")];
92
+ const dirs = [resolve(designHome(projectRoot), "marketplace/atoms"), resolve(designHome(projectRoot), "atoms")];
92
93
  const names = new Set<string>();
93
94
  for (const d of dirs) {
94
95
  if (!existsSync(d)) continue;
package/local/catalog.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  *
11
11
  * See docs/architecture/LOCAL_STUDIO.md.
12
12
  */
13
+ import { hasDesignHome } from "./design-home.mjs";
13
14
  import { resolve } from "path";
14
15
  import { existsSync } from "fs";
15
16
 
@@ -41,10 +42,10 @@ export function findProjectRoot(from: string = process.cwd()): string {
41
42
  let dir = resolve(from);
42
43
  for (;;) {
43
44
  // A developer's project (starter kit): rx/ sits at the root.
44
- if (existsSync(resolve(dir, "rx"))) return dir;
45
+ if (hasDesignHome(dir)) return dir;
45
46
  // This monorepo, where Studio's author works: rx/ lives under apps/unoverse. Same
46
47
  // shape underneath (rx/ + prompts/), just nested, so return that as the root.
47
- if (existsSync(resolve(dir, "apps/unoverse/rx"))) return resolve(dir, "apps/unoverse");
48
+ if (hasDesignHome(resolve(dir, "apps/unoverse"))) return resolve(dir, "apps/unoverse");
48
49
  const parent = resolve(dir, "..");
49
50
  if (parent === dir) break;
50
51
  dir = parent;
@@ -0,0 +1,2 @@
1
+ export function designHome(root: string): string;
2
+ export function hasDesignHome(root: string): boolean;
@@ -0,0 +1,31 @@
1
+ // The design folder for a root, renamed `rx` → `design` (2026-08-20).
2
+ //
3
+ // MIGRATE-ONCE, not dual-read: a root still holding the legacy `rx/` is renamed to
4
+ // `design/` on first resolution, and the platform knows ONE name from then on. The
5
+ // canonical rule lives in @unoverse-platform/base `paths.ts` (`designDir`); this copy
6
+ // exists because the plain-node entrypoints (bin/studio.mjs, scaffold.mjs) cannot
7
+ // import base's TypeScript source. Keep the two in step.
8
+ import { existsSync, renameSync } from "node:fs";
9
+ import { join } from "node:path";
10
+
11
+ export function designHome(root) {
12
+ const design = join(root, "design");
13
+ const legacy = join(root, "rx");
14
+ if (!existsSync(design) && existsSync(legacy)) {
15
+ try {
16
+ renameSync(legacy, design);
17
+ console.warn(`[studio] migrated ${legacy} → ${design} (rx/ is now design/)`);
18
+ } catch (e) {
19
+ console.warn(`[studio] could not rename ${legacy} → ${design} (${e.message}). Rename it by hand; Studio reads design/ only.`);
20
+ }
21
+ }
22
+ return design;
23
+ }
24
+
25
+ /** Does this folder hold a design tree under either name? DETECTION ONLY — the walk in
26
+ * bin/studio.mjs probes arbitrary ancestor folders, and probing must never rename a
27
+ * stranger's `rx/`. Migration happens in `designHome`, called only on the root a
28
+ * caller actually adopts. */
29
+ export function hasDesignHome(root) {
30
+ return existsSync(join(root, "design")) || existsSync(join(root, "rx"));
31
+ }
package/local/mcp.ts CHANGED
@@ -63,7 +63,11 @@ export interface McpDeps {
63
63
  vocabulary: () => Promise<unknown>;
64
64
  }
65
65
 
66
- const SKILL_DIRS = ["vendor/skills/unoverse-create", "../../prompts/skills/unoverse-create"];
66
+ const SKILL_DIRS = [
67
+ "vendor/skills/unoverse-create",
68
+ "../../.claude/skills/unoverse-create", // running from source: the authoring home
69
+ "../../prompts/skills/unoverse-create",
70
+ ];
67
71
 
68
72
  /** The vendored authoring skill, which is how an agent learns the rules before writing. */
69
73
  function authoringSkill(appRoot: string): string {
package/local/plugin.ts CHANGED
@@ -15,6 +15,7 @@
15
15
  *
16
16
  * See docs/architecture/LOCAL_STUDIO.md.
17
17
  */
18
+ import { designHome } from "./design-home.mjs";
18
19
  import { createRequire } from "node:module";
19
20
  import { fileURLToPath } from "node:url";
20
21
  import { resolve } from "path";
@@ -51,13 +52,13 @@ function definitionHome(
51
52
  return null;
52
53
  }
53
54
  if (/\/atoms\//.test(uri)) {
54
- const flat = resolve(projectRoot, "rx/atoms", `${ref.toLowerCase()}.json`);
55
+ const flat = resolve(designHome(projectRoot), "atoms", `${ref.toLowerCase()}.json`);
55
56
  return existsSync(flat) ? flat : null;
56
57
  }
57
58
  const folder = c.componentFolder(ref);
58
59
  if (folder) return folder;
59
60
  // A flat, folderless component: `rx/components/<name>.json` in either tier.
60
- for (const dir of [resolve(projectRoot, "rx/marketplace/components"), resolve(projectRoot, "rx/components")]) {
61
+ for (const dir of [resolve(designHome(projectRoot), "marketplace/components"), resolve(designHome(projectRoot), "components")]) {
61
62
  const flat = resolve(dir, `${ref.toLowerCase()}.json`);
62
63
  if (existsSync(flat)) return flat;
63
64
  }
@@ -195,7 +196,7 @@ export function localStudioHost(): Plugin {
195
196
  let nodesOnce: Promise<LocalNodes | null> | null = null;
196
197
  const nodes = () => (nodesOnce ??= loadLocalNodes(baseSrc, ssr, projectRoot));
197
198
 
198
- server.config.logger.info(`\n Studio reading: ${resolve(projectRoot, "rx")}\n`);
199
+ server.config.logger.info(`\n Studio reading: ${designHome(projectRoot)}\n`);
199
200
 
200
201
  /**
201
202
  * WATCH THE PROJECT EXPLICITLY. Vite watches its own root, which is this package,
@@ -208,7 +209,7 @@ export function localStudioHost(): Plugin {
208
209
  *
209
210
  * Adding the paths puts them in the same watcher, so the existing hook fires.
210
211
  */
211
- for (const dir of ["rx", "prompts"]) {
212
+ for (const dir of [designHome(projectRoot), resolve(projectRoot, "prompts")]) {
212
213
  const full = resolve(projectRoot, dir);
213
214
  if (existsSync(full)) server.watcher.add(full);
214
215
  }
@@ -242,7 +243,7 @@ export function localStudioHost(): Plugin {
242
243
  case path === "/local/info":
243
244
  return json(res, {
244
245
  projectRoot,
245
- rx: resolve(projectRoot, "rx"),
246
+ rx: designHome(projectRoot),
246
247
  nodes: resolve(projectRoot, "nodes"),
247
248
  envFiles: [resolve(projectRoot, "..", ".env"), resolve(projectRoot, ".env")],
248
249
  });
@@ -276,20 +277,20 @@ export function localStudioHost(): Plugin {
276
277
  // mirrors the module's real exports — tsc can't follow an ssrLoadModule path.
277
278
  type PublishItem = { kind: string; name: string; why?: string; mode?: string };
278
279
  type PublishModule = {
279
- lintForPublish(rxHome: string, project: string): Promise<{ problems: unknown[]; errors: unknown[] }>;
280
- collectProject(rxHome: string, project: string): unknown;
280
+ lintForPublish(designRoot: string, project: string): Promise<{ problems: unknown[]; errors: unknown[] }>;
281
+ collectProject(designRoot: string, project: string): unknown;
281
282
  planPublish(collected: unknown, universe: string, token: string): Promise<Record<string, PublishItem[]>>;
282
283
  sendPublish(plan: unknown, universe: string, token: string): Promise<{ sent: PublishItem[]; failed?: PublishItem[] }>;
283
284
  };
284
285
  const items = (await ssr(`${baseSrc}/items/publish.ts`)) as PublishModule;
285
- const rxHome = resolve(projectRoot, "rx");
286
+ const designRoot = designHome(projectRoot);
286
287
 
287
288
  // LINT FIRST, ALWAYS. Nothing leaves the machine until the rules pass: a
288
289
  // developer should learn about a raw hex value here, not from an HTTP error.
289
- const { problems, errors } = await items.lintForPublish(rxHome, project);
290
+ const { problems, errors } = await items.lintForPublish(designRoot, project);
290
291
  if (errors.length) return json(res, { blocked: true, problems, errors });
291
292
 
292
- const collected = items.collectProject(rxHome, project);
293
+ const collected = items.collectProject(designRoot, project);
293
294
  const plan = await items.planPublish(collected, universe, token);
294
295
 
295
296
  // The screen renders kinds and names. The plan carries every file's full
@@ -424,8 +425,8 @@ export function localStudioHost(): Plugin {
424
425
  const p = String(b.path ?? "");
425
426
  if (!p || typeof b.text !== "string") return json(res, { error: "path and text are required" }, 400);
426
427
 
427
- type LintModule = { lintDefinitions(rxHome: string): { problems: { level?: string; file?: string }[] } };
428
- const lintMod = (await ssr(`${baseSrc}/lint/rx/index.mjs`)) as unknown as LintModule;
428
+ type LintModule = { lintDefinitions(designRoot: string): { problems: { level?: string; file?: string }[] } };
429
+ const lintMod = (await ssr(`${baseSrc}/lint/design/index.mjs`)) as unknown as LintModule;
429
430
 
430
431
  // Marked BEFORE the write, so the filesystem event can never beat the mark.
431
432
  recentSelfWrites.set(resolve(p), Date.now());
@@ -434,7 +435,7 @@ export function localStudioHost(): Plugin {
434
435
  b.text as string,
435
436
  writableRoots(projectRoot, c),
436
437
  projectRoot,
437
- (rxHome) => lintMod.lintDefinitions(rxHome),
438
+ (designRoot) => lintMod.lintDefinitions(designRoot),
438
439
  );
439
440
  return json(res, result, result.saved ? 200 : result.error === "lint errors" ? 422 : 403);
440
441
  }
@@ -454,13 +455,13 @@ export function localStudioHost(): Plugin {
454
455
 
455
456
  type LintModule = {
456
457
  lintDefinitions(
457
- rxHome: string,
458
+ designRoot: string,
458
459
  options?: { overlay?: Record<string, string> },
459
460
  ): { problems: { level?: string; file?: string; line?: number; msg?: string }[] };
460
461
  };
461
- const lintMod = (await ssr(`${baseSrc}/lint/rx/index.mjs`)) as unknown as LintModule;
462
+ const lintMod = (await ssr(`${baseSrc}/lint/design/index.mjs`)) as unknown as LintModule;
462
463
  const abs = resolve(p);
463
- const { problems } = lintMod.lintDefinitions(resolve(projectRoot, "rx"), { overlay: { [abs]: b.text as string } });
464
+ const { problems } = lintMod.lintDefinitions(designHome(projectRoot), { overlay: { [abs]: b.text as string } });
464
465
  return json(res, {
465
466
  findings: problems.filter((f) => f.file && resolve(f.file) === abs),
466
467
  elsewhere: problems.filter((f) => !f.file || resolve(f.file) !== abs).length,
@@ -472,7 +473,7 @@ export function localStudioHost(): Plugin {
472
473
  * linter enforces, plus this project's own token, atom and component names.
473
474
  */
474
475
  case path === "/local/vocabulary": {
475
- const vocabMod = (await ssr(`${baseSrc}/lint/rx/vocabulary.mjs`)) as unknown as Parameters<typeof vocabulary>[0];
476
+ const vocabMod = (await ssr(`${baseSrc}/lint/design/vocabulary.mjs`)) as unknown as Parameters<typeof vocabulary>[0];
476
477
  const theme = c.resolveTheme(url.searchParams.get("theme") ?? "light");
477
478
  return json(
478
479
  res,
@@ -517,21 +518,21 @@ export function localStudioHost(): Plugin {
517
518
  },
518
519
  readSource: (p) => readSource(p, writableRoots(projectRoot, c), projectRoot),
519
520
  saveSource: async (p, text) => {
520
- type LintModule = { lintDefinitions(rxHome: string): { problems: { level?: string; file?: string }[] } };
521
- const lintMod = (await ssr(`${baseSrc}/lint/rx/index.mjs`)) as unknown as LintModule;
521
+ type LintModule = { lintDefinitions(designRoot: string): { problems: { level?: string; file?: string }[] } };
522
+ const lintMod = (await ssr(`${baseSrc}/lint/design/index.mjs`)) as unknown as LintModule;
522
523
  recentSelfWrites.set(resolve(p), Date.now());
523
- return saveSource(p, text, writableRoots(projectRoot, c), projectRoot, (rxHome) =>
524
- lintMod.lintDefinitions(rxHome),
524
+ return saveSource(p, text, writableRoots(projectRoot, c), projectRoot, (designRoot) =>
525
+ lintMod.lintDefinitions(designRoot),
525
526
  );
526
527
  },
527
528
  lintProject: async () => {
528
- type LintModule = { lintDefinitions(rxHome: string): { problems: unknown[] } };
529
- const lintMod = (await ssr(`${baseSrc}/lint/rx/index.mjs`)) as unknown as LintModule;
530
- return lintMod.lintDefinitions(resolve(projectRoot, "rx"));
529
+ type LintModule = { lintDefinitions(designRoot: string): { problems: unknown[] } };
530
+ const lintMod = (await ssr(`${baseSrc}/lint/design/index.mjs`)) as unknown as LintModule;
531
+ return lintMod.lintDefinitions(designHome(projectRoot));
531
532
  },
532
533
  selection: () => selection,
533
534
  vocabulary: async () => {
534
- const vocabMod = (await ssr(`${baseSrc}/lint/rx/vocabulary.mjs`)) as unknown as Parameters<typeof vocabulary>[0];
535
+ const vocabMod = (await ssr(`${baseSrc}/lint/design/vocabulary.mjs`)) as unknown as Parameters<typeof vocabulary>[0];
535
536
  return vocabulary(vocabMod, c.resolveTheme("light"), atomNames(projectRoot), c.listDefinitions("component") as { name?: string }[]);
536
537
  },
537
538
  },
@@ -632,7 +633,7 @@ export function localStudioHost(): Plugin {
632
633
  * else, which means the developer's other editor, still reloads.
633
634
  */
634
635
  handleHotUpdate({ server, file }) {
635
- const watched = [resolve(projectRoot, "rx"), resolve(projectRoot, "prompts")];
636
+ const watched = [designHome(projectRoot), resolve(projectRoot, "prompts")];
636
637
  if (!watched.some((w) => file.startsWith(w))) return;
637
638
  const self = recentSelfWrites.get(resolve(file));
638
639
  if (self !== undefined && Date.now() - self < SELF_WRITE_WINDOW_MS) return [];
@@ -642,10 +643,10 @@ export function localStudioHost(): Plugin {
642
643
 
643
644
  configResolved() {
644
645
  // Surface a clear failure at startup rather than an empty catalog later.
645
- if (projectRoot && !existsSync(resolve(projectRoot, "rx"))) {
646
+ if (projectRoot && !existsSync(designHome(projectRoot))) {
646
647
  throw new Error(`No rx/ folder at ${projectRoot}`);
647
648
  }
648
- if (projectRoot) statSync(resolve(projectRoot, "rx"));
649
+ if (projectRoot) statSync(designHome(projectRoot));
649
650
  },
650
651
  };
651
652
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Creating a project, from either end.
3
3
  *
4
- * A project (an "org" in rx terms) is `rx/<name>/` holding components, styles and
4
+ * A project (an "org") is `design/<name>/` holding components, styles and
5
5
  * templates. It is the unit a developer authors in and the unit that publishes: every row
6
6
  * that lands in a universe carries its name in the `org` column.
7
7
  *
@@ -14,6 +14,7 @@
14
14
  * See docs/architecture/LOCAL_STUDIO.md.
15
15
  */
16
16
  import { existsSync, mkdirSync, writeFileSync, cpSync, readdirSync } from "node:fs";
17
+ import { designHome } from "./design-home.mjs";
17
18
  import { join, dirname, resolve } from "node:path";
18
19
  import { fileURLToPath } from "node:url";
19
20
 
@@ -27,7 +28,7 @@ import { fileURLToPath } from "node:url";
27
28
  export function checkProjectName(name) {
28
29
  const n = String(name ?? "").trim();
29
30
  if (!n) return "A name is required";
30
- // RESERVED FIRST, so the message is the true reason. These folders under rx/ are never a
31
+ // RESERVED FIRST, so the message is the true reason. These folders under design/ are never a
31
32
  // developer's own project (collect.ts NOT_A_PROJECT), and a project named `marketplace`
32
33
  // would shadow the design system in every loader that resolves one. Checked before the
33
34
  // format rule because `_schema` fails that too, and "use lowercase letters" would send a
@@ -56,7 +57,7 @@ const WELCOME = [
56
57
  ].join("\n");
57
58
 
58
59
  /**
59
- * Create `rx/<name>/` under `home`, with its three authoring folders and one component.
60
+ * Create `design/<name>/` under `home`, with its three authoring folders and one component.
60
61
  *
61
62
  * Refuses an existing name rather than merging into it: "create" that silently adopts a
62
63
  * folder is how a developer ends up publishing someone else's work under their own project.
@@ -65,8 +66,8 @@ export function createProject(home, name) {
65
66
  const bad = checkProjectName(name);
66
67
  if (bad) throw new Error(bad);
67
68
 
68
- const root = join(home, "rx", name);
69
- if (existsSync(root)) throw new Error(`rx/${name} already exists`);
69
+ const root = join(designHome(home), name);
70
+ if (existsSync(root)) throw new Error(`${name} already exists in this workspace`);
70
71
 
71
72
  for (const d of ["components/welcome", "templates", "styles"]) mkdirSync(join(root, d), { recursive: true });
72
73
  writeFileSync(join(root, "components/welcome/welcome.yaml"), WELCOME);
@@ -74,7 +75,7 @@ export function createProject(home, name) {
74
75
  }
75
76
 
76
77
  /**
77
- * The FIRST project, plus the folders that live beside rx/ rather than inside it.
78
+ * The FIRST project, plus the folders that live beside the design tree rather than inside it.
78
79
  *
79
80
  * Skills, prompt blocks and nodes are per-developer and not per-project, so they are made
80
81
  * once here and never by `createProject`.
@@ -85,7 +86,10 @@ export function createFirstProject(home, name) {
85
86
 
86
87
  // The Claude Code authoring skills, so "open the folder and describe what you want"
87
88
  // works in every Studio project from its first minute. Vendored into the published
88
- // app (vendor/skills, written by prepack); read from the repo when running as source.
89
+ // app (vendor/skills, written by prepack from the monorepo's .claude/skills, the one
90
+ // authoring home, mirrored publicly to github.com/unoverse-platform/skills); read
91
+ // from that home when running as source. `unoverse update` refreshes installed
92
+ // copies from the public mirror.
89
93
  //
90
94
  // ALL of them. One Studio project is where a person designs a brand AND writes the node
91
95
  // behind it, so they need the whole set: naming one skill here shipped the authoring
package/local/source.ts CHANGED
@@ -22,6 +22,7 @@
22
22
  *
23
23
  * See docs/architecture/LOCAL_STUDIO.md §Authoring in Studio.
24
24
  */
25
+ import { designHome } from "./design-home.mjs";
25
26
  import { resolve, sep, relative } from "path";
26
27
  import { existsSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "fs";
27
28
 
@@ -163,7 +164,7 @@ export async function saveSource(
163
164
  text: string,
164
165
  roots: string[],
165
166
  projectRoot: string,
166
- lint: (rxHome: string) => Promise<{ problems: LintFinding[] }> | { problems: LintFinding[] },
167
+ lint: (designRoot: string) => Promise<{ problems: LintFinding[] }> | { problems: LintFinding[] },
167
168
  ): Promise<SaveResult> {
168
169
  const abs = resolve(path);
169
170
  const w = writability(abs, roots, projectRoot);
@@ -183,10 +184,10 @@ export async function saveSource(
183
184
 
184
185
  writeFileSync(abs, text, "utf8");
185
186
 
186
- const rxHome = resolve(projectRoot, "rx");
187
+ const designRoot = designHome(projectRoot);
187
188
  let problems: LintFinding[] = [];
188
189
  try {
189
- problems = (await lint(rxHome)).problems ?? [];
190
+ problems = (await lint(designRoot)).problems ?? [];
190
191
  } catch (e) {
191
192
  // A linter that throws must not silently accept the write.
192
193
  if (previous === null) rmSync(abs, { force: true });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unoverse-platform/studio",
3
- "version": "0.1.45",
3
+ "version": "0.1.47",
4
4
  "type": "module",
5
5
  "description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
6
6
  "scripts": {
@@ -321,7 +321,7 @@ export function DesignSystemView({
321
321
  {foundation ? (
322
322
  <>Tokens with an <OverrideMark /> are <b style={{ color: ink }}>{org}</b>&rsquo;s own; the rest are inherited from the design-system foundation.</>
323
323
  ) : (
324
- <>Live tokens from the design-system foundation (<code>rx/design-system/styles</code>). Toggle the theme to restyle everything.</>
324
+ <>Live tokens from the design-system foundation (<code>design/marketplace/styles</code>). Toggle the theme to restyle everything.</>
325
325
  )}
326
326
  </p>
327
327
  {current.body}
@@ -7,7 +7,7 @@
7
7
  * Gavin first. No exceptions: not bug fixes, not one-liners, not reverts.
8
8
  * State the intended diff and wait for an explicit yes.
9
9
  *
10
- * Components and templates are DATA (rx/). If a template seems to need code
10
+ * Components and templates are DATA (design/). If a template seems to need code
11
11
  * here, the answer is almost always to express it as data — or to ask.
12
12
  * ============================================================================
13
13
  *
@@ -532,7 +532,7 @@ export function TemplatesView({
532
532
  const seedSet = (tmpls: Def[], comps: Def[], sel: string | null): Def[] => {
533
533
  const t = tmpls.find((t) => t.name === sel);
534
534
  // The declaration lives in the app manifest (app.previewComponents); the def-level
535
- // field is the legacy envelope home. Names are case-insensitive like every rx name,
535
+ // field is the legacy envelope home. Names are case-insensitive like every design name,
536
536
  // and each ENTRY seeds one instance — a repeated name previews several (a card rail).
537
537
  const pc = t?.app?.previewComponents ?? t?.previewComponents ?? [];
538
538
  return pc
@@ -657,7 +657,7 @@ export function TemplatesView({
657
657
  <nav className="flex min-h-0 min-w-0 flex-col overflow-auto border-r border-gray-700 bg-[#2c2c2e] p-2">
658
658
  <div className="px-2 py-1 text-[10px] font-semibold uppercase tracking-wider text-gray-400">Apps</div>
659
659
  {templates.length === 0 && (
660
- <div className="p-2 text-xs text-gray-400">No apps yet — add one under rx/orgs/&lt;client&gt;/templates/.</div>
660
+ <div className="p-2 text-xs text-gray-400">No apps yet — add one under design/&lt;project&gt;/templates/.</div>
661
661
  )}
662
662
  {templates.map((t) => (
663
663
  <div key={`${t.org ?? ""}/${t.name}`}>
package/src/App.tsx CHANGED
@@ -81,7 +81,7 @@ function Shell({
81
81
  const [foundation, setFoundation] = useState<ResolvedTheme | null>(null);
82
82
  const [error, setError] = useState<string | null>(null);
83
83
 
84
- // The projects in this developer's rx/ folder. Flat, per RX_ORG_MODEL. Re-read after
84
+ // The projects in this developer's design/ folder. Flat, per RX_ORG_MODEL. Re-read after
85
85
  // creating one, so a new project appears without a reload.
86
86
  const loadProjects = () =>
87
87
  fetch("/dev/orgs")
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Creating a project from inside Studio.
3
3
  *
4
- * A project is `rx/<name>/`: the folder a developer authors in, and the unit that
4
+ * A project is `design/<name>/`: the folder a developer authors in, and the unit that
5
5
  * publishes. Until this existed, Studio could create your FIRST one (bin/studio.mjs, at
6
6
  * launch, only when the folder was empty) and never your second. The dropdown listed what
7
7
  * was on disk and offered no way to add to it, so the answer to "start another project"
@@ -51,7 +51,7 @@ export function NewProject({
51
51
  >
52
52
  <h2 className="text-base font-semibold text-gray-100">New project</h2>
53
53
  <p className="mt-1 text-xs text-gray-500">
54
- Creates rx/&lt;name&gt;/ with components, styles and templates, and one component to
54
+ Creates design/&lt;name&gt;/ with components, styles and templates, and one component to
55
55
  open on.
56
56
  </p>
57
57
 
@@ -267,7 +267,7 @@ export function PublishDialog({
267
267
  .map((p, i) => (
268
268
  <li key={i} className="leading-relaxed">
269
269
  <span className="text-gray-500">
270
- {p.file?.replace(/^.*\/rx\//, "")}
270
+ {p.file?.replace(/^.*\/design\//, "")}
271
271
  {p.line ? `:${p.line}` : ""}
272
272
  </span>
273
273
  <br />
@@ -68,7 +68,7 @@ export function AppEngine(cfg: AppConfig) {
68
68
  setBoundFor(templateId);
69
69
  setBinding({});
70
70
  }
71
- // STATE-OWNED WIDTH (rx data): the layout root's `appWidth` = the core surface's
71
+ // STATE-OWNED WIDTH (design data): the layout root's `appWidth` = the core surface's
72
72
  // constant width; an occupied panel slot's `appWidth` adds to it (computeAppWidth).
73
73
  // No declared width → the host keeps its own default. There is no other path.
74
74
  // THE WHOLE DEFINITION, not a chosen four fields. This used to keep `{root, org, layouts,
@@ -163,7 +163,7 @@ export class UnoverseClient {
163
163
 
164
164
  /**
165
165
  * resources/read the RESOLVED theme by name (e.g. "light"). The server owns the
166
- * values (rx/styles); the SDK only fetches — it bundles no tokens. Cached.
166
+ * values (design/styles); the SDK only fetches — it bundles no tokens. Cached.
167
167
  */
168
168
  async readTheme(name = "light"): Promise<ResolvedTheme> {
169
169
  const cached = this.themeCache.get(name);
@@ -42,7 +42,7 @@ export interface UnoverseNode {
42
42
  * content grows (new turn or streaming text), unless the user has scrolled up. Generic
43
43
  * scroll-container behavior, opt-in via data (e.g. the conversation state's scroll box). */
44
44
  autoScroll?: boolean;
45
- /** Box: a sticky bar (its own `position: sticky` + `top` in rx) that stays HIDDEN while
45
+ /** Box: a sticky bar (its own `position: sticky` + `top` in the design definition) that stays HIDDEN while
46
46
  * the element before it (its previous sibling — e.g. a hero image) is on screen, then
47
47
  * fades in once that sibling scrolls away. Overlays the content (no flow gap). Generic
48
48
  * scroll-reveal behavior, opt-in via data. */
@@ -203,16 +203,16 @@ export type StyleVariant = Condition & { apply: Record<string, unknown> };
203
203
  // ---- The resolved theme (SHAPE only — values live on the server) ----
204
204
  //
205
205
  // The MCP server OWNS THE VISUAL: it resolves the design tokens (apps/unoverse/
206
- // rx/styles) live and serves this shape at `unoverse://theme/{name}`. This is the
206
+ // design/styles) live and serves this shape at `unoverse://theme/{name}`. This is the
207
207
  // token SHAPE, never the token VALUES — the SDK fetches the values (readTheme),
208
- // it does NOT bundle them. A brand change in rx/styles is therefore refresh-only,
208
+ // it does NOT bundle them. A brand change in design/styles is therefore refresh-only,
209
209
  // exactly like a definition change (UNOVERSE_SPEC §2d-1).
210
210
  export interface ResolvedTheme {
211
211
  color: Record<string, string>;
212
212
  /** Webfont stylesheet URLs (served `fonts.stylesheets` data) — injected into
213
213
  * document.head by the SDK (shadow roots can't register @font-face). */
214
214
  fonts?: string[];
215
- /** Dimension scale (theme.space ← rx/styles/base/spacing.json) — used for BOTH spacing
215
+ /** Dimension scale (theme.space ← design/styles/base/spacing.json) — used for BOTH spacing
216
216
  * (gap/padding/margin) AND element sizes (width/height), Tailwind-style. */
217
217
  space: Record<string, string>;
218
218
  radius: Record<string, string>;
@@ -227,17 +227,17 @@ export interface ResolvedTheme {
227
227
  text: Record<string, Record<string, string | number>>;
228
228
  skeleton: Record<string, unknown>;
229
229
  prose: Record<string, unknown>;
230
- /** STANDARD APP SIZES (theme.appSize ← rx/styles/semantic/app-sizes.json) — the named
230
+ /** STANDARD APP SIZES (theme.appSize ← design/styles/semantic/app-sizes.json) — the named
231
231
  * width blocks `appWidth` references ("chat"/"rail"/"panel" → raw host-facing CSS).
232
232
  * Resolved by the SDK like every other token — one pipeline. */
233
233
  appSize?: Record<string, string>;
234
- /** Grid behaviour (theme.grid ← rx/styles/semantic/grid) — `stackBelow`, the width
234
+ /** Grid behaviour (theme.grid ← design/styles/semantic/grid) — `stackBelow`, the width
235
235
  * at which a columns grid stacks. Served: the SDK holds no threshold of its own. */
236
236
  grid?: Record<string, string>;
237
- /** PAGE WIDTHS by name (theme.layout ← rx/styles/semantic/layout) — resolved by `dim()`
237
+ /** PAGE WIDTHS by name (theme.layout ← design/styles/semantic/layout) — resolved by `dim()`
238
238
  * alongside the space scale, so `maxWidth: "reading"` works like any dimension. */
239
239
  layout?: Record<string, string>;
240
- /** Named keyframes (theme.keyframes ← rx/styles/semantic/keyframes.json), injected
240
+ /** Named keyframes (theme.keyframes ← design/styles/semantic/keyframes.json), injected
241
241
  * once per render root. Lets any definition animate via `style.animation` — no SDK edit. */
242
242
  keyframes: Record<string, Record<string, Record<string, string>>>;
243
243
  /** Named icon glyphs as SERVED DATA. The SERVER sources these from an icon pack
@@ -245,7 +245,7 @@ export interface ResolvedTheme {
245
245
  * elements `[tag, attrs]`. The generic `Icon` leaf renders them, sized/coloured by
246
246
  * node.style. The SDK never bundles the pack — icons are served like any token. */
247
247
  icons: Record<string, { viewBox?: string; attrs?: Record<string, unknown>; children?: [string, Record<string, unknown>][] }>;
248
- /** Base render-root settings (theme.root ← rx/styles/semantic/root.json) — a CSS
248
+ /** Base render-root settings (theme.root ← design/styles/semantic/root.json) — a CSS
249
249
  * object the SDK spreads onto its outermost wrapper (e.g. font-smoothing). Served,
250
250
  * not authored: a whole-tree rendering normalization, like legacy styles/base.css. */
251
251
  root: Record<string, unknown>;
@@ -31,7 +31,7 @@ const RESET_CSS = `
31
31
  font: inherit; letter-spacing: inherit; color: inherit; margin: 0;
32
32
  }
33
33
  /* Scrollbars hidden by default across every scroll container — scrolling still works,
34
- the bar is just not painted. Generic chrome that rx can't author per-node (pseudo-
34
+ the bar is just not painted. Generic chrome that design definitions can't author per-node (pseudo-
35
35
  elements), so it lives with the structural reset. */
36
36
  .uno-isolate-root * { scrollbar-width: none; }
37
37
  .uno-isolate-root *::-webkit-scrollbar { width: 0; height: 0; display: none; }