@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.
- package/bin/studio.mjs +3 -3
- package/local/authoring.ts +2 -2
- package/local/catalog.ts +8 -4
- package/local/design-home.d.mts +2 -0
- package/local/design-home.mjs +11 -0
- package/local/plugin.ts +51 -22
- package/local/sample-identity/brand.yaml +1 -1
- package/local/sample-identity/organisation.yaml +1 -1
- package/local/sample-identity/purpose.yaml +1 -1
- package/local/sample-identity/story.yaml +1 -1
- package/local/scaffold.mjs +20 -4
- package/local/source.ts +13 -7
- package/package.json +1 -1
- package/screens/DesignSystemView.tsx +1 -1
- package/screens/EmptyLibrary.tsx +1 -1
- package/screens/PromptBlocksView.tsx +1 -1
- package/src/App.tsx +5 -5
- package/vendor/base/src/definitions/load/assets.ts +2 -2
- package/vendor/base/src/definitions/registry/catalog.ts +1 -1
- package/vendor/base/src/definitions/registry/homes.ts +15 -5
- package/vendor/base/src/definitions/registry/objects.ts +1 -1
- package/vendor/base/src/definitions/registry/orgDocuments.ts +2 -2
- package/vendor/base/src/definitions/registry/orgPack.ts +3 -2
- package/vendor/base/src/definitions/registry/pipelines.ts +1 -1
- package/vendor/base/src/definitions/theme.ts +5 -6
- package/vendor/base/src/items/baseVersion.ts +1 -1
- package/vendor/base/src/items/collect.ts +1 -1
- package/vendor/base/src/lint/design/file.mjs +2 -2
- package/vendor/base/src/lint/design/index.d.mts +6 -0
- package/vendor/base/src/lint/design/index.mjs +45 -17
- package/vendor/base/src/lint/design/vocabulary.mjs +2 -2
- package/vendor/base/src/lint/nodes/blocks.mjs +3 -3
- package/vendor/base/src/lint/nodes/node.mjs +1 -1
- package/vendor/base/src/lint/objects/index.d.mts +6 -0
- package/vendor/base/src/lint/objects/index.mjs +27 -6
- package/vendor/base/src/lint/prompts/index.d.mts +6 -0
- package/vendor/base/src/lint/prompts/index.mjs +8 -3
- package/vendor/base/src/paths.ts +65 -20
- package/vendor/base/src/plugins/discovery.ts +1 -1
- 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 (
|
|
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) ||
|
|
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;
|
package/local/authoring.ts
CHANGED
|
@@ -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(
|
|
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:
|
|
53
|
-
//
|
|
54
|
-
|
|
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
|
}
|
package/local/design-home.d.mts
CHANGED
|
@@ -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;
|
package/local/design-home.mjs
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
-
|
|
387
|
-
|
|
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,
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
package/local/scaffold.mjs
CHANGED
|
@@ -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
|
|
225
|
-
* server maps files to schemas by glob from
|
|
226
|
-
* carries this mapping gets autocomplete and
|
|
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
|
-
|
|
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
|
@@ -319,7 +319,7 @@ export function DesignSystemView({
|
|
|
319
319
|
{foundation ? (
|
|
320
320
|
<>Tokens with an <OverrideMark /> are <b className="text-gray-900">{org}</b>’s own; the rest are inherited from the design-system foundation.</>
|
|
321
321
|
) : (
|
|
322
|
-
<>Live tokens from the design-system foundation (<code>
|
|
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}
|
package/screens/EmptyLibrary.tsx
CHANGED
|
@@ -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
|
-
* `
|
|
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
|
-
* (
|
|
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
|
-
//
|
|
83
|
-
const [
|
|
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
|
-
|
|
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
|
-
{
|
|
170
|
-
{
|
|
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 {
|
|
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(
|
|
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 (`
|
|
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
|
|
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 =
|
|
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 (`
|
|
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 (`
|
|
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
|
-
* (`
|
|
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 (`
|
|
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
|