@unoverse-platform/studio 0.1.109 → 0.1.110

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 (40) hide show
  1. package/bin/studio.mjs +3 -3
  2. package/local/authoring.ts +2 -2
  3. package/local/catalog.ts +8 -4
  4. package/local/design-home.d.mts +2 -0
  5. package/local/design-home.mjs +11 -0
  6. package/local/plugin.ts +51 -22
  7. package/local/sample-identity/brand.yaml +1 -1
  8. package/local/sample-identity/organisation.yaml +1 -1
  9. package/local/sample-identity/purpose.yaml +1 -1
  10. package/local/sample-identity/story.yaml +1 -1
  11. package/local/scaffold.mjs +20 -4
  12. package/local/source.ts +13 -7
  13. package/package.json +1 -1
  14. package/screens/DesignSystemView.tsx +1 -1
  15. package/screens/EmptyLibrary.tsx +1 -1
  16. package/screens/PromptBlocksView.tsx +1 -1
  17. package/src/App.tsx +5 -5
  18. package/vendor/base/src/definitions/load/assets.ts +2 -2
  19. package/vendor/base/src/definitions/registry/catalog.ts +1 -1
  20. package/vendor/base/src/definitions/registry/homes.ts +15 -5
  21. package/vendor/base/src/definitions/registry/objects.ts +1 -1
  22. package/vendor/base/src/definitions/registry/orgDocuments.ts +2 -2
  23. package/vendor/base/src/definitions/registry/orgPack.ts +3 -2
  24. package/vendor/base/src/definitions/registry/pipelines.ts +1 -1
  25. package/vendor/base/src/definitions/theme.ts +5 -6
  26. package/vendor/base/src/items/baseVersion.ts +1 -1
  27. package/vendor/base/src/items/collect.ts +1 -1
  28. package/vendor/base/src/lint/design/file.mjs +2 -2
  29. package/vendor/base/src/lint/design/index.d.mts +6 -0
  30. package/vendor/base/src/lint/design/index.mjs +45 -17
  31. package/vendor/base/src/lint/design/vocabulary.mjs +2 -2
  32. package/vendor/base/src/lint/nodes/blocks.mjs +3 -3
  33. package/vendor/base/src/lint/nodes/node.mjs +1 -1
  34. package/vendor/base/src/lint/objects/index.d.mts +6 -0
  35. package/vendor/base/src/lint/objects/index.mjs +27 -6
  36. package/vendor/base/src/lint/prompts/index.d.mts +6 -0
  37. package/vendor/base/src/lint/prompts/index.mjs +8 -3
  38. package/vendor/base/src/paths.ts +65 -20
  39. package/vendor/base/src/plugins/discovery.ts +1 -1
  40. package/vendor/base/src/plugins/loader.ts +5 -7
package/bin/studio.mjs CHANGED
@@ -22,7 +22,7 @@ import { fileURLToPath } from "url";
22
22
  import { existsSync, readdirSync } from "fs";
23
23
  import { createInterface } from "readline/promises";
24
24
  import { execSync } from "child_process";
25
- import { hasDesignHome, orgRepoName, authoringRoot } from "../local/design-home.mjs";
25
+ import { hasDesignHome, orgRepoName, authoringRoot, platformHome } from "../local/design-home.mjs";
26
26
  import { createOrgRepo, checkProjectName } from "../local/scaffold.mjs";
27
27
 
28
28
  const here = dirname(fileURLToPath(import.meta.url));
@@ -44,7 +44,7 @@ const appRoot = resolve(here, "..");
44
44
  function findProjectRootFromCwd() {
45
45
  for (let dir = process.cwd(); ; ) {
46
46
  if (orgRepoName(dir) || hasDesignHome(dir)) return dir;
47
- if (hasDesignHome(join(dir, "apps/unoverse"))) return join(dir, "apps/unoverse");
47
+ if (platformHome(dir)) return platformHome(dir);
48
48
  const parent = resolve(dir, "..");
49
49
  if (parent === dir) return process.cwd();
50
50
  dir = parent;
@@ -83,7 +83,7 @@ async function scaffoldIfEmpty() {
83
83
  // The same walk findProjectRoot does. A project ABOVE us is a project; do not offer to
84
84
  // make a second one inside it.
85
85
  for (let dir = cwd; ; ) {
86
- if (orgRepoName(dir) || hasDesignHome(dir) || hasDesignHome(join(dir, "apps/unoverse"))) return;
86
+ if (orgRepoName(dir) || hasDesignHome(dir) || platformHome(dir)) return;
87
87
  const parent = resolve(dir, "..");
88
88
  if (parent === dir) break;
89
89
  dir = parent;
@@ -88,8 +88,8 @@ export function vocabulary(
88
88
  }
89
89
 
90
90
  /** Atom names off disk, for completion of `Ref.ref`. */
91
- export function atomNames(projectRoot: string): { name: string }[] {
92
- const dirs = [resolve(authoringRoot(projectRoot), "marketplace/atoms"), resolve(authoringRoot(projectRoot), "atoms")];
91
+ export function atomNames(projectRoot: string, marketplace: string): { name: string }[] {
92
+ const dirs = [resolve(marketplace, "atoms"), resolve(authoringRoot(projectRoot), "atoms")];
93
93
  const names = new Set<string>();
94
94
  for (const d of dirs) {
95
95
  if (!existsSync(d)) continue;
package/local/catalog.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  *
11
11
  * See docs/architecture/authoring/LOCAL_STUDIO.md.
12
12
  */
13
- import { hasDesignHome, orgRepoName } from "./design-home.mjs";
13
+ import { hasDesignHome, orgRepoName, platformHome } from "./design-home.mjs";
14
14
  import { resolve } from "path";
15
15
  import { existsSync } from "fs";
16
16
 
@@ -34,6 +34,8 @@ type Loaders = {
34
34
  projectDir: (name: string) => string;
35
35
  /** A component id's on-disk folder, either tier. Null for a flat, folderless one. */
36
36
  componentFolder: (ref: string) => string | null;
37
+ /** The marketplace tier on disk (base paths.ts `marketplace`). Absent outside the monorepo. */
38
+ marketplace: string;
37
39
  };
38
40
 
39
41
  /**
@@ -49,9 +51,10 @@ export function findProjectRoot(from: string = process.cwd()): string {
49
51
  if (orgRepoName(dir)) return dir;
50
52
  // A developer's project (starter kit): rx/ sits at the root.
51
53
  if (hasDesignHome(dir)) return dir;
52
- // This monorepo, where Studio's author works: rx/ lives under apps/unoverse. Same
53
- // shape underneath (design/), just nested, so return that as the root.
54
- if (hasDesignHome(resolve(dir, "apps/unoverse"))) return resolve(dir, "apps/unoverse");
54
+ // This monorepo, where Studio's author works: the home is apps/unoverse, found by the
55
+ // marker base paths.ts uses. It holds no orgs, so Studio shows the marketplace tier.
56
+ const platform = platformHome(dir);
57
+ if (platform) return platform;
55
58
  const parent = resolve(dir, "..");
56
59
  if (parent === dir) break;
57
60
  dir = parent;
@@ -136,5 +139,6 @@ export async function loadCatalog(
136
139
  DEFAULT_ORG: theme.DEFAULT_ORG as string,
137
140
  projectDir: defs.projectDir as Loaders["projectDir"],
138
141
  componentFolder: defs.componentFolder as Loaders["componentFolder"],
142
+ marketplace: (paths.getPaths as () => { marketplace: string })().marketplace,
139
143
  };
140
144
  }
@@ -1,5 +1,7 @@
1
1
  export function designHome(root: string): string;
2
2
  export function hasDesignHome(root: string): boolean;
3
+ /** The platform repo's home (`<dir>/apps/unoverse`) when `dir` is that repo's root, else null. */
4
+ export function platformHome(dir: string): string | null;
3
5
 
4
6
  /** The org an org repo declares in its unoverse.yaml, or null when this is not an org repo (UNO-88). */
5
7
  export function orgRepoName(root: string): string | null;
@@ -30,6 +30,17 @@ export function hasDesignHome(root) {
30
30
  return existsSync(join(root, "design")) || existsSync(join(root, "rx"));
31
31
  }
32
32
 
33
+ /**
34
+ * THE PLATFORM REPO: `apps/unoverse` beside the marketplace tier it authors
35
+ * (`packages/marketplace/definitions`). The same marker base `paths.ts` findHome uses, and the
36
+ * home it returns. It holds no orgs (its design/ was retired 2026-10-04), so Studio there
37
+ * shows the marketplace tier alone. DETECTION ONLY, like hasDesignHome. Keep in step.
38
+ */
39
+ export function platformHome(dir) {
40
+ const home = join(dir, "apps", "unoverse");
41
+ return existsSync(join(dir, "packages", "marketplace", "definitions")) && existsSync(home) ? home : null;
42
+ }
43
+
33
44
  /**
34
45
  * AN ORG REPO (UNO-88): `unoverse.yaml` declares `org: <slug>` and the org's folders sit at
35
46
  * the root, with no design/. The name is declared, never read off the folder. Mirrors base
package/local/plugin.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  *
16
16
  * See docs/architecture/authoring/LOCAL_STUDIO.md.
17
17
  */
18
- import { designHome, authoringRoot, orgRepoName, lintOptions } from "./design-home.mjs";
18
+ import { designHome, authoringRoot, orgRepoName, lintOptions, platformHome } from "./design-home.mjs";
19
19
  import { createRequire } from "node:module";
20
20
  import { fileURLToPath } from "node:url";
21
21
  import { resolve, dirname } from "path";
@@ -36,7 +36,7 @@ import { atomNames, vocabulary } from "./authoring.js";
36
36
  */
37
37
  function definitionHome(
38
38
  uri: string,
39
- c: { componentFolder: (ref: string) => string | null; projectDir: (n: string) => string; listOrgs: () => string[] },
39
+ c: { componentFolder: (ref: string) => string | null; projectDir: (n: string) => string; listOrgs: () => string[]; marketplace: string },
40
40
  projectRoot: string,
41
41
  ): string | null {
42
42
  // An app definition is addressed as `unoverse://apps/<org>/<name>/definition`, so the
@@ -62,14 +62,14 @@ function definitionHome(
62
62
  const [org, name] = i === -1 ? [null, ref] : [ref.slice(0, i), ref.slice(i + 1)];
63
63
  const homes = org
64
64
  ? [resolve(c.projectDir(org), "templates", name.toLowerCase())]
65
- : [resolve(authoringRoot(projectRoot), "marketplace/templates", ref.toLowerCase())];
65
+ : [resolve(c.marketplace, "templates", ref.toLowerCase())];
66
66
  for (const dir of homes) if (existsSync(dir)) return dir;
67
67
  return null;
68
68
  }
69
69
  const folder = c.componentFolder(ref);
70
70
  if (folder) return folder;
71
71
  // A flat, folderless component: `rx/components/<name>.json` in either tier.
72
- for (const dir of [resolve(authoringRoot(projectRoot), "marketplace/components"), resolve(authoringRoot(projectRoot), "components")]) {
72
+ for (const dir of [resolve(c.marketplace, "components"), resolve(authoringRoot(projectRoot), "components")]) {
73
73
  const flat = resolve(dir, `${ref.toLowerCase()}.json`);
74
74
  if (existsSync(flat)) return flat;
75
75
  }
@@ -167,7 +167,7 @@ function readMarkdownLibrary(dir: string): MarkdownEntry[] {
167
167
  * A skill or a prompt block lives in one of two places, and the NAME's shape is what
168
168
  * carries which:
169
169
  *
170
- * UNIVERSAL `design/marketplace/<folder>/`, named bare. Published to the marketplace.
170
+ * UNIVERSAL the marketplace tier's `<folder>/` (base paths.ts `marketplace`), named bare.
171
171
  * ORG `design/<org>/<folder>/`, named `<org>/<name>`. Private to that org.
172
172
  *
173
173
  * The universe server resolves both (the items loaders). This host read the universal
@@ -177,8 +177,8 @@ function readMarkdownLibrary(dir: string): MarkdownEntry[] {
177
177
  * Scoping is the server's `?org=`: an org sees the universal tier plus its own and never
178
178
  * another org's, the rule RX_ORG_MODEL.md settled for a project's assets.
179
179
  */
180
- function readLibrary(projectRoot: string, folder: "skills" | "blocks", orgs: string[], org?: string): MarkdownEntry[] {
181
- const out = readMarkdownLibrary(resolve(authoringRoot(projectRoot), "marketplace", folder));
180
+ function readLibrary(projectRoot: string, marketplace: string, folder: "skills" | "blocks", orgs: string[], org?: string): MarkdownEntry[] {
181
+ const out = readMarkdownLibrary(resolve(marketplace, folder));
182
182
  for (const o of orgs) {
183
183
  if (org && org !== "all" && o !== org) continue;
184
184
  // The org's own folder: the repo root in an org repo (UNO-88), else design/<org>/.
@@ -202,11 +202,26 @@ function readLibrary(projectRoot: string, folder: "skills" | "blocks", orgs: str
202
202
  const recentSelfWrites = new Map<string, number>();
203
203
  const SELF_WRITE_WINDOW_MS = 2000;
204
204
 
205
+
206
+ /**
207
+ * WHY A NEW PROJECT IS REFUSED HERE, or null when it is allowed. A new org is a new repo
208
+ * (UNO-88), made with `unoverse create` in its own folder: never a second org inside an org
209
+ * repo, and never an org inside the platform repo, which holds none since 2026-10-04.
210
+ */
211
+ function newProjectRefusal(root: string | null): string | null {
212
+ if (!root) return null;
213
+ if (orgRepoName(root)) return "This repo is one org. A new org is a new repo: run `unoverse create` in a new folder.";
214
+ if (platformHome(resolve(root, "..", "..")) === root) return "The platform repo holds no orgs. A new org is a new repo: run `unoverse create` in a new folder.";
215
+ return null;
216
+ }
217
+
205
218
  export function localStudioHost(): Plugin {
206
219
  let projectRoot: string;
207
220
  let baseSrc: string;
208
221
  /** The node the developer last picked in the preview, for an agent to read. */
209
222
  let selection: Record<string, unknown> | null = null;
223
+ /** The marketplace tier on disk, watched beside the project when this checkout authors it. */
224
+ let marketplaceDir: string | undefined;
210
225
 
211
226
  return {
212
227
  name: "local-studio-host",
@@ -245,6 +260,10 @@ export function localStudioHost(): Plugin {
245
260
 
246
261
  const ssr = (id: string) => server.ssrLoadModule(id) as Promise<Record<string, unknown>>;
247
262
  const catalog = loadCatalog(baseSrc, ssr, projectRoot);
263
+ // Lint options for this project, naming the marketplace tier when this checkout authors
264
+ // it outside the design root (the monorepo's packages/marketplace/definitions).
265
+ const lintWith = (c: { marketplace: string }, extra: Record<string, unknown> = {}) =>
266
+ lintOptions(projectRoot, existsSync(c.marketplace) ? { ...extra, marketplace: c.marketplace } : extra);
248
267
  // Node packages load lazily: only when the Nodes section is first opened, so a
249
268
  // developer authoring only design assets never pays for it at startup.
250
269
  let nodesOnce: Promise<LocalNodes | null> | null = null;
@@ -272,6 +291,12 @@ export function localStudioHost(): Plugin {
272
291
  const full = resolve(projectRoot, dir);
273
292
  if (existsSync(full)) server.watcher.add(full);
274
293
  }
294
+ // The marketplace tier too, when this checkout authors it: in the monorepo it is
295
+ // packages/marketplace/definitions, outside the project folder watched above.
296
+ void catalog.then((c) => {
297
+ marketplaceDir = c.marketplace;
298
+ if (existsSync(c.marketplace)) server.watcher.add(c.marketplace);
299
+ });
275
300
 
276
301
  server.middlewares.use(async (req, res, next) => {
277
302
  const url = new URL(req.url ?? "/", "http://local");
@@ -366,7 +391,7 @@ export function localStudioHost(): Plugin {
366
391
  case path === "/dev/orgs":
367
392
  // `orgRepo` names the org when this folder is one org's repo (UNO-88): the picker
368
393
  // then offers no second project, since a new org is a new repo.
369
- return json(res, { orgs: c.themeOrgs(), orgRepo: orgRepoName(projectRoot) ?? null });
394
+ return json(res, { orgs: c.themeOrgs(), orgRepo: orgRepoName(projectRoot) ?? null, canCreate: newProjectRefusal(projectRoot) === null });
370
395
 
371
396
  /**
372
397
  * NEW PROJECT. The only thing Studio writes to disk that is not a definition.
@@ -383,8 +408,9 @@ export function localStudioHost(): Plugin {
383
408
  case path === "/dev/projects" && req.method === "POST": {
384
409
  // ONE ORG, ONE REPO (UNO-88): a new org is a new repo, made with `unoverse create`
385
410
  // in its own folder, never a second org inside this one.
386
- if (orgRepoName(projectRoot)) {
387
- return json(res, { error: "This repo is one org. A new org is a new repo: run `unoverse create` in a new folder." }, 400);
411
+ {
412
+ const refusal = newProjectRefusal(projectRoot);
413
+ if (refusal) return json(res, { error: refusal }, 400);
388
414
  }
389
415
  const { createProject } = await import("./scaffold.mjs");
390
416
  const name = String((await body()).name ?? "");
@@ -467,17 +493,17 @@ export function localStudioHost(): Plugin {
467
493
  case path === "/local/source": {
468
494
  const roots = writableRoots(projectRoot, c);
469
495
  const direct = url.searchParams.get("path");
470
- if (direct) return json(res, { files: sourceFiles(direct, roots, projectRoot) });
496
+ if (direct) return json(res, { files: sourceFiles(direct, roots, projectRoot, c.marketplace) });
471
497
 
472
498
  const uri = url.searchParams.get("uri") ?? "";
473
499
  const home = definitionHome(uri, c, projectRoot);
474
500
  if (!home) return json(res, { error: `no source for ${uri}` }, 404);
475
- return json(res, { home, files: sourceFiles(home, roots, projectRoot) });
501
+ return json(res, { home, files: sourceFiles(home, roots, projectRoot, c.marketplace) });
476
502
  }
477
503
 
478
504
  case path === "/local/source/file" && req.method !== "POST": {
479
505
  const p = url.searchParams.get("path") ?? "";
480
- const found = readSource(p, writableRoots(projectRoot, c), projectRoot);
506
+ const found = readSource(p, writableRoots(projectRoot, c), projectRoot, c.marketplace);
481
507
  return found ? json(res, found) : json(res, { error: "not readable" }, 404);
482
508
  }
483
509
 
@@ -512,7 +538,8 @@ export function localStudioHost(): Plugin {
512
538
  b.text as string,
513
539
  writableRoots(projectRoot, c),
514
540
  projectRoot,
515
- (designRoot) => ({ problems: [...lintMod.lintDefinitions(designRoot, lintOptions(projectRoot)).problems, ...objectsMod.lintObjects(designRoot, lintOptions(projectRoot)).problems, ...promptsMod.lintPrompts(designRoot, lintOptions(projectRoot)).problems] }),
541
+ (designRoot) => ({ problems: [...lintMod.lintDefinitions(designRoot, lintWith(c)).problems, ...objectsMod.lintObjects(designRoot, lintWith(c)).problems, ...promptsMod.lintPrompts(designRoot, lintWith(c)).problems] }),
542
+ c.marketplace,
516
543
  );
517
544
  return json(res, result, result.saved ? 200 : result.error === "lint errors" ? 422 : 403);
518
545
  }
@@ -544,7 +571,7 @@ export function localStudioHost(): Plugin {
544
571
  const abs = resolve(p);
545
572
  const overlay = { [abs]: b.text as string };
546
573
  const root = authoringRoot(projectRoot);
547
- const opts = lintOptions(projectRoot, { overlay });
574
+ const opts = lintWith(c, { overlay });
548
575
  const problems = [...lintMod.lintDefinitions(root, opts).problems, ...objectsMod.lintObjects(root, opts).problems, ...promptsMod.lintPrompts(root, opts).problems];
549
576
  return json(res, {
550
577
  findings: problems.filter((f) => f.file && resolve(f.file) === abs),
@@ -561,7 +588,7 @@ export function localStudioHost(): Plugin {
561
588
  const theme = c.resolveTheme(url.searchParams.get("theme") ?? "light");
562
589
  return json(
563
590
  res,
564
- vocabulary(vocabMod, theme, atomNames(projectRoot), c.listDefinitions("component") as { name?: string }[]),
591
+ vocabulary(vocabMod, theme, atomNames(projectRoot, c.marketplace), c.listDefinitions("component") as { name?: string }[]),
565
592
  );
566
593
  }
567
594
 
@@ -584,17 +611,17 @@ export function localStudioHost(): Plugin {
584
611
  return json(res, selection ?? { empty: true });
585
612
 
586
613
  case path === "/skills":
587
- return json(res, { skills: readLibrary(projectRoot, "skills", c.listOrgs(), org) });
614
+ return json(res, { skills: readLibrary(projectRoot, c.marketplace, "skills", c.listOrgs(), org) });
588
615
 
589
616
  case path.startsWith("/skills/"): {
590
617
  const name = decodeURIComponent(path.slice("/skills/".length).replace(/\/file$/, ""));
591
- const found = readLibrary(projectRoot, "skills", c.listOrgs()).find((s) => s.name === name || s.id === name);
618
+ const found = readLibrary(projectRoot, c.marketplace, "skills", c.listOrgs()).find((s) => s.name === name || s.id === name);
592
619
  // SkillsView reads `d.skill`, matching the server's shape. Not the bare object.
593
620
  return found ? json(res, { skill: found }) : json(res, { error: "skill not found" }, 404);
594
621
  }
595
622
 
596
623
  case path === "/prompt-blocks":
597
- return json(res, { blocks: readLibrary(projectRoot, "blocks", c.listOrgs(), org) });
624
+ return json(res, { blocks: readLibrary(projectRoot, c.marketplace, "blocks", c.listOrgs(), org) });
598
625
 
599
626
  // Registry toggles are a DEPLOYED concept: they persist to installed_plugins
600
627
  // and decide what a running universe exposes. Local Studio has no database and
@@ -674,7 +701,7 @@ export function localStudioHost(): Plugin {
674
701
  * else, which means the developer's other editor, still reloads.
675
702
  */
676
703
  handleHotUpdate({ server, file }) {
677
- const watched = [authoringRoot(projectRoot)];
704
+ const watched = [authoringRoot(projectRoot), ...(marketplaceDir ? [marketplaceDir] : [])];
678
705
  if (!watched.some((w) => file.startsWith(w))) return;
679
706
  const self = recentSelfWrites.get(resolve(file));
680
707
  if (self !== undefined && Date.now() - self < SELF_WRITE_WINDOW_MS) return [];
@@ -684,10 +711,12 @@ export function localStudioHost(): Plugin {
684
711
 
685
712
  configResolved() {
686
713
  // Surface a clear failure at startup rather than an empty catalog later.
687
- if (projectRoot && !existsSync(authoringRoot(projectRoot))) {
714
+ // The platform repo authors no orgs, so its design/ is absent by design: no org tier.
715
+ const platform = projectRoot ? platformHome(resolve(projectRoot, "..", "..")) === projectRoot : false;
716
+ if (projectRoot && !platform && !existsSync(authoringRoot(projectRoot))) {
688
717
  throw new Error(`No rx/ folder at ${projectRoot}`);
689
718
  }
690
- if (projectRoot) statSync(authoringRoot(projectRoot));
719
+ if (projectRoot && !platform) statSync(authoringRoot(projectRoot));
691
720
  },
692
721
  };
693
722
  }
@@ -1,5 +1,5 @@
1
1
  # The org's brand: its own words, and only those. The briefs, the limits and the shape are the
2
- # brand object's (design/marketplace/objects/brand/); this file carries the answers as defaults.
2
+ # brand object's (packages/marketplace/definitions/objects/brand/); this file carries the answers as defaults.
3
3
  unoverse: "1.0"
4
4
  type: identity
5
5
  extends: brand
@@ -1,5 +1,5 @@
1
1
  # The org's organisation: its own words, and only those. The briefs, the limits and the shape are the
2
- # organisation object's (design/marketplace/objects/organisation/); this file carries the answers as defaults.
2
+ # organisation object's (packages/marketplace/definitions/objects/organisation/); this file carries the answers as defaults.
3
3
  unoverse: "1.0"
4
4
  type: identity
5
5
  extends: organisation
@@ -1,5 +1,5 @@
1
1
  # The org's purpose: its own words, and only those. The briefs, the limits and the shape are the
2
- # purpose object's (design/marketplace/objects/purpose/); this file carries the answers as defaults.
2
+ # purpose object's (packages/marketplace/definitions/objects/purpose/); this file carries the answers as defaults.
3
3
  unoverse: "1.0"
4
4
  type: identity
5
5
  extends: purpose
@@ -1,5 +1,5 @@
1
1
  # The org's story: its own words, and only those. The briefs, the limits and the shape are the
2
- # story object's (design/marketplace/objects/story/); this file carries the answers as defaults.
2
+ # story object's (packages/marketplace/definitions/objects/story/); this file carries the answers as defaults.
3
3
  unoverse: "1.0"
4
4
  type: identity
5
5
  extends: story
@@ -221,10 +221,10 @@ export function topUpSamples(home, project) {
221
221
  */
222
222
 
223
223
  /**
224
- * EDITOR HELP FOR NODES, WITHOUT A LINE IN EVERY FILE (2026-09-05). The YAML language
225
- * server maps files to schemas by glob from `.vscode/settings.json`, so a workspace that
226
- * carries this mapping gets autocomplete and errors on every node file it will ever hold,
227
- * and no file needs a `$schema:` key. The schemas are the docs site's, so the mapping
224
+ * EDITOR HELP FOR NODES AND DESIGN, WITHOUT A LINE IN EVERY FILE (nodes 2026-09-05, design
225
+ * 2026-10-04). The YAML language server maps files to schemas by glob from
226
+ * `.vscode/settings.json`, so a workspace that carries this mapping gets autocomplete and
227
+ * errors on every node and design file it will hold, and no file needs a `$schema:` key. The schemas are the docs site's, so the mapping
228
228
  * stays true across releases. Written once, never over a developer's own settings.
229
229
  */
230
230
  const SCHEMAS = "https://docs.unoverse.ai/schemas/nodes";
@@ -257,6 +257,22 @@ const NODE_SCHEMA_MAP = {
257
257
  [`${SCHEMAS.replace(/\/nodes$/, "/pipelines")}/config.schema.json`]: ["design/*/pipelines/*/config.yaml"],
258
258
  [`${SCHEMAS.replace(/\/nodes$/, "/pipelines")}/interface.schema.json`]: ["design/*/pipelines/*/interface.yaml"],
259
259
  [`${SCHEMAS.replace(/\/nodes$/, "/pipelines")}/test.schema.json`]: ["design/*/pipelines/*/test.yaml"],
260
+ // DESIGN (UNO-90): components, templates, apps, atoms and Agents, from the same site. A
261
+ // manifest is a different shape from a definition, so the definition globs refuse the name
262
+ // with `!(manifest)` (a negated ENTRY in this list is not honoured reliably by the YAML
263
+ // extension) and the manifest schema takes it. Atoms carry no manifest.
264
+ [`${SCHEMAS.replace(/\/nodes$/, "/design")}/unoverse.schema.json`]: [
265
+ "design/*/components/**/!(manifest).yaml",
266
+ "design/*/templates/**/!(manifest).yaml",
267
+ "design/*/apps/**/!(manifest).yaml",
268
+ "design/*/atoms/**/*.yaml",
269
+ ],
270
+ [`${SCHEMAS.replace(/\/nodes$/, "/design")}/manifest.schema.json`]: [
271
+ "design/*/components/**/manifest.yaml",
272
+ "design/*/templates/**/manifest.yaml",
273
+ "design/*/apps/**/manifest.yaml",
274
+ ],
275
+ [`${SCHEMAS.replace(/\/nodes$/, "/design")}/agent.schema.json`]: ["design/*/agents/*.yaml"],
260
276
  };
261
277
  export function writeEditorSettings(home, flat = false) {
262
278
  const dir = join(home, ".vscode");
package/local/source.ts CHANGED
@@ -61,11 +61,16 @@ export function writableRoots(
61
61
  * work. Everything else is read-only, and the reason distinguishes the design system a
62
62
  * developer builds ON (normal, shown without a save button) from a path that has no
63
63
  * business being addressed at all.
64
+ *
65
+ * `designSystem` is the marketplace tier on disk when this checkout authors it (base
66
+ * paths.ts `marketplace`, the monorepo's packages/marketplace/definitions). It sits outside
67
+ * the project, and it is the design system a developer builds on, so it reads as installed.
64
68
  */
65
- export function writability(path: string, roots: string[], projectRoot: string): Writability {
69
+ export function writability(path: string, roots: string[], projectRoot: string, designSystem?: string): Writability {
66
70
  const abs = resolve(path);
67
71
  if (roots.some((r) => within(r, abs))) return { writable: true };
68
- // Inside the project but not in a writable root: the marketplace on disk, rx/_schema,
72
+ if (designSystem && within(resolve(designSystem), abs)) return { writable: false, reason: "installed" };
73
+ // Inside the project but not in a writable root: the marketplace on disk
69
74
  // or the legacy orgs/ container. Reading is the point; writing is what is refused.
70
75
  if (within(resolve(projectRoot), abs)) return { writable: false, reason: "installed" };
71
76
  // node_modules, or anywhere else entirely. The design system resolves here for a
@@ -98,14 +103,14 @@ const SOURCE_EXT = /\.(ya?ml|json|jsonc|md|mjs|js)$/;
98
103
  * `components/`), and a simple one is a single flat file. Both shapes are returned as a
99
104
  * list so the pane never has to know which it is looking at.
100
105
  */
101
- export function sourceFiles(folderOrFile: string, roots: string[], projectRoot: string): SourceFile[] {
106
+ export function sourceFiles(folderOrFile: string, roots: string[], projectRoot: string, designSystem?: string): SourceFile[] {
102
107
  const abs = resolve(folderOrFile);
103
108
  if (!existsSync(abs)) return [];
104
109
  const out: SourceFile[] = [];
105
110
  const base = statSync(abs).isDirectory() ? abs : resolve(abs, "..");
106
111
 
107
112
  const add = (p: string) => {
108
- const w = writability(p, roots, projectRoot);
113
+ const w = writability(p, roots, projectRoot, designSystem);
109
114
  out.push({ path: p, rel: relative(base, p) || p.split(sep).pop()!, writable: w.writable, reason: w.reason });
110
115
  };
111
116
 
@@ -160,9 +165,10 @@ export async function saveSource(
160
165
  roots: string[],
161
166
  projectRoot: string,
162
167
  lint: (designRoot: string) => Promise<{ problems: LintFinding[] }> | { problems: LintFinding[] },
168
+ designSystem?: string,
163
169
  ): Promise<SaveResult> {
164
170
  const abs = resolve(path);
165
- const w = writability(abs, roots, projectRoot);
171
+ const w = writability(abs, roots, projectRoot, designSystem);
166
172
  if (!w.writable) {
167
173
  const why =
168
174
  w.reason === "installed"
@@ -215,10 +221,10 @@ export interface LintFinding {
215
221
  }
216
222
 
217
223
  /** One file's text, refused unless it is a file Studio would list. */
218
- export function readSource(path: string, roots: string[], projectRoot: string): { text: string; writable: boolean; reason?: ReadOnlyReason } | null {
224
+ export function readSource(path: string, roots: string[], projectRoot: string, designSystem?: string): { text: string; writable: boolean; reason?: ReadOnlyReason } | null {
219
225
  const abs = resolve(path);
220
226
  if (!existsSync(abs) || !statSync(abs).isFile()) return null;
221
- const w = writability(abs, roots, projectRoot);
227
+ const w = writability(abs, roots, projectRoot, designSystem);
222
228
  // A path outside the project entirely is not shown. The design system is, because
223
229
  // reading what you build on is the normal case.
224
230
  if (w.reason === "outside-project" && !abs.includes(`${sep}node_modules${sep}`)) return null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@unoverse-platform/studio",
3
- "version": "0.1.109",
3
+ "version": "0.1.110",
4
4
  "type": "module",
5
5
  "description": "Unoverse Studio: the local authoring app. Reads your own rx/ folder, no backend.",
6
6
  "scripts": {
@@ -319,7 +319,7 @@ export function DesignSystemView({
319
319
  {foundation ? (
320
320
  <>Tokens with an <OverrideMark /> are <b className="text-gray-900">{org}</b>&rsquo;s own; the rest are inherited from the design-system foundation.</>
321
321
  ) : (
322
- <>Live tokens from the design-system foundation (<code>design/marketplace/styles</code>). Toggle the theme to restyle everything.</>
322
+ <>Live tokens from the design-system foundation (<code>packages/marketplace/definitions/styles</code>). Toggle the theme to restyle everything.</>
323
323
  )}
324
324
  </p>
325
325
  {current.body}
@@ -11,7 +11,7 @@
11
11
  * without needing to say so.
12
12
  *
13
13
  * AND IT MUST NAME THE RIGHT FOLDER. The old copy always said
14
- * `apps/unoverse/design/marketplace/blocks/`, which is the UNIVERSAL tier. Studio scoped to an org
14
+ * `packages/marketplace/definitions/blocks/`, which is the UNIVERSAL tier. Studio scoped to an org
15
15
  * is telling you where to author for THAT org, and sending someone to the universal
16
16
  * folder would have them write a platform-wide asset when they wanted their own.
17
17
  */
@@ -1,6 +1,6 @@
1
1
  /**
2
2
  * Prompt-blocks view — browse the reusable prompt-snippet library
3
- * (apps/unoverse/design/marketplace/blocks) served by the Unoverse server's /prompt-blocks
3
+ * (packages/marketplace/definitions/blocks) served by the Unoverse server's /prompt-blocks
4
4
  * route. Left: a navigator of blocks grouped by category. Right: the selected block's
5
5
  * metadata + full markdown content, with an enable toggle.
6
6
  *
package/src/App.tsx CHANGED
@@ -79,8 +79,8 @@ function Shell() {
79
79
  const [variant, setVariant] = usePersistentState<string>("studio:variant", "light");
80
80
 
81
81
  const [projects, setProjects] = useState<string[]>([]);
82
- // Set when this folder is one org's repo: there is no second project to create here.
83
- const [orgRepo, setOrgRepo] = useState<string | null>(null);
82
+ // False in an org repo and in the platform repo: a new org is a new repo there.
83
+ const [canCreate, setCanCreate] = useState(true);
84
84
  const [theme, setTheme] = useState<ResolvedTheme | null>(null);
85
85
  const [foundation, setFoundation] = useState<ResolvedTheme | null>(null);
86
86
  const [error, setError] = useState<string | null>(null);
@@ -92,7 +92,7 @@ function Shell() {
92
92
  .then((r) => r.json())
93
93
  .then((d) => {
94
94
  setProjects(d.orgs ?? []);
95
- setOrgRepo(d.orgRepo ?? null);
95
+ setCanCreate(d.canCreate !== false);
96
96
  })
97
97
  .catch((e) => setError(String(e)));
98
98
  useEffect(() => {
@@ -166,8 +166,8 @@ function Shell() {
166
166
  {o}
167
167
  </option>
168
168
  ))}
169
- {!orgRepo && <option disabled>──────────</option>}
170
- {!orgRepo && <option value="__new">New project…</option>}
169
+ {canCreate && <option disabled>──────────</option>}
170
+ {canCreate && <option value="__new">New project…</option>}
171
171
  </select>
172
172
  <button
173
173
  onClick={() => setVariant(variant === "light" ? "dark" : "light")}
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import { readFileSync, existsSync, statSync } from "fs";
9
9
  import { join, extname, dirname } from "path";
10
- import { projectDir } from "../registry/homes.js";
10
+ import { marketplaceHome } from "../registry/homes.js";
11
11
 
12
12
  const ASSET_PREFIX = "assets/";
13
13
  const ASSET_MAX_BYTES = 256 * 1024;
@@ -29,7 +29,7 @@ function assetFile(from: string | undefined, src: string): string | null {
29
29
  if (existsSync(f)) return f;
30
30
  if (existsSync(join(dir, "styles"))) break;
31
31
  }
32
- const shared = join(projectDir("marketplace"), "assets", rel);
32
+ const shared = join(marketplaceHome(), "assets", rel);
33
33
  return existsSync(shared) ? shared : null;
34
34
  }
35
35
 
@@ -353,7 +353,7 @@ export function loadComponentApp(id: string, org?: string): AppManifest | null {
353
353
  * about which home a definition lives in. */
354
354
  export function loadTemplateApp(id: string, org?: string): AppManifest | null {
355
355
  const lower = id.toLowerCase();
356
- // TWO TIERS, like components: the marketplace shelf (`design/marketplace/templates`) or
356
+ // TWO TIERS, like components: the marketplace shelf (`packages/marketplace/definitions/templates`) or
357
357
  // an org's own (`design/<org>/templates`). A shelf template carries no org, and its tool
358
358
  // id is the bare name.
359
359
  const home = org ? join(projectDir(org), "templates") : SHARED_DIR.template;
@@ -48,7 +48,8 @@ const INSTALLED_DESIGN = designDir(INSTALLED_HOME);
48
48
 
49
49
  /** The shared homes for the universal kinds — the marketplace.
50
50
  *
51
- * Resolve each shared dir from the on-disk design/marketplace (monorepo dev) if present,
51
+ * Resolve each shared dir from the on-disk tier (paths.ts `marketplace`: the monorepo's
52
+ * packages/marketplace/definitions, so a local universe reads it live) if present,
52
53
  * else from a bundle beside the home node, else from what this universe INSTALLED (rows,
53
54
  * hydrated). Mirrors theme.ts's styles fallback, and the getters resolve LAZILY so a tier
54
55
  * that appears after boot is picked up on first use.
@@ -62,7 +63,7 @@ const INSTALLED_DESIGN = designDir(INSTALLED_HOME);
62
63
  * server and Canvas — "they do not carry components, atoms, styles, skills, prompt blocks
63
64
  * or nodes, all of which are rows". A dead path that would silently adopt an npm tree if
64
65
  * one ever appeared is worse than no path. */
65
- const MARKETPLACE = join(DESIGN, "marketplace");
66
+ const MARKETPLACE = getPaths().marketplace;
66
67
  const DS_BUNDLE_CANDIDATES: string[] = [];
67
68
  function marketplaceDir(kind: "components" | "atoms" | "templates" | "objects" | "pipelines"): string {
68
69
  const onDisk = join(MARKETPLACE, kind);
@@ -105,6 +106,15 @@ export const SHARED_DIR = {
105
106
  },
106
107
  } as Record<"component" | "atom" | "template" | "object" | "pipeline", string>;
107
108
 
109
+ /** The marketplace tier's own folder, for what is not one of the kinds above (its shared
110
+ * `assets/`): on disk when this checkout authors it, else what this universe installed.
111
+ * The same order `projectDir` applies to an org. */
112
+ export function marketplaceHome(): string {
113
+ const installed = join(INSTALLED_DESIGN, "marketplace");
114
+ if (databaseOnly()) return installed;
115
+ return existsSync(MARKETPLACE) ? MARKETPLACE : installed;
116
+ }
117
+
108
118
  // rx-root entries that are NOT projects: the marketplace, the schema, and the legacy
109
119
  // orgs/ container itself. Everything else at the root is a flat project (the target model).
110
120
  const RESERVED_DESIGN = new Set(["marketplace", "_schema"]);
@@ -181,7 +191,7 @@ export function appDirs(refOrg: string | null): { org: string; dir: string }[] {
181
191
 
182
192
  /** The TEMPLATE homes a ref resolves against — the Template Model's kind
183
193
  * (docs/unoverse/UNOVERSE_TEMPLATE_MODEL.md). Two tiers like components: the
184
- * marketplace shelf (`design/marketplace/templates`, the BASE shapes) and the
194
+ * marketplace shelf (`packages/marketplace/definitions/templates`, the BASE shapes) and the
185
195
  * org packs (`design/<org>/templates`). The word is free in code because the app
186
196
  * kind's in-code name is `app` (rename finished in code 2026-08-28; stored words
187
197
  * unchanged until their own migration). */
@@ -194,7 +204,7 @@ export function templateDirs(refOrg: string | null): { org: string | undefined;
194
204
  }
195
205
 
196
206
  /** The OBJECT homes a ref resolves against (docs/unoverse/data/objects/README.md): the same two
197
- * tiers as components, the marketplace (`design/marketplace/objects`, the platform's kinds,
207
+ * tiers as components, the marketplace (`packages/marketplace/definitions/objects`, the platform's kinds,
198
208
  * and `need`, the atom every object extends) and the org packs (`design/<org>/objects`, an
199
209
  * org's own kinds). An org-qualified ref searches its org first, then the marketplace, so
200
210
  * `extends: need` resolves from an org's object; a bare ref searches the marketplace first. */
@@ -207,7 +217,7 @@ export function objectDirs(refOrg: string | null): { org: string | undefined; di
207
217
  }
208
218
 
209
219
  /** The PIPELINE homes a ref resolves against (docs/unoverse/data/pipelines/README.md layer 1): the marketplace
210
- * (`design/marketplace/pipelines`, the sources everyone gets: website, spreadsheet, images) and
220
+ * (`packages/marketplace/definitions/pipelines`, the sources everyone gets: website, spreadsheet, images) and
211
221
  * the org packs (`design/<org>/pipelines`, a source nobody else has). Same order as objects. */
212
222
  export function pipelineDirs(refOrg: string | null): { org: string | undefined; dir: string }[] {
213
223
  const shared = { org: undefined as string | undefined, dir: SHARED_DIR.pipeline };
@@ -13,7 +13,7 @@
13
13
  * objects/<name>/api/service.yaml the object's public functions by name (getDetail, searchDetail), as a
14
14
  * service node's tools; found by name, declared nowhere else
15
15
  *
16
- * in two tiers, the marketplace's (`design/marketplace/objects/`, shared by every org) and an
16
+ * in two tiers, the marketplace's (`packages/marketplace/definitions/objects/`, shared by every org) and an
17
17
  * org's own (`design/<org>/objects/`). `extends` names another object and is resolved here,
18
18
  * across the tiers, never as a path: the base's props compose under the object's own, later
19
19
  * keys over earlier. `need` is the atom every other positioned object extends (owner ruling