@unoverse-platform/studio 0.1.109 → 0.1.111
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/datasetSql.ts +7 -12
- 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/vendor/base/src/security/rules.ts +6 -0
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;
|
|
@@ -2,13 +2,9 @@
|
|
|
2
2
|
* A DATASET'S TABLE, AS SQL (docs/unoverse/data/objects/dataset.md, PROPOSED 2026-09-18).
|
|
3
3
|
*
|
|
4
4
|
* The declaration in an object's folder is the schema, so nobody writes SQL and nobody edits a
|
|
5
|
-
* schema by hand. This turns that declaration into
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* WHERE IT GOES. Beside the declaration, as `dataset.sql`, and never into the migrations folder:
|
|
9
|
-
* a dataset's table is made at publish by the platform's one door, and a file kept where the
|
|
10
|
-
* migration runner finds it would be a step someone has to run. This is what ran, written down,
|
|
11
|
-
* so a schema change can be read and compared and is never invisible.
|
|
5
|
+
* schema by hand. This turns that declaration into the statements the platform's one door runs
|
|
6
|
+
* (services/dataset/ensure.ts). Nothing here runs anything, and nothing here is ever written to
|
|
7
|
+
* a file: an org holds no SQL.
|
|
12
8
|
*
|
|
13
9
|
* THE POLICIES ARE PART OF THE TABLE, not a later step. They are written in the same statement
|
|
14
10
|
* block, so a table cannot exist without them. A table created today and secured on Thursday is
|
|
@@ -100,11 +96,10 @@ export function datasetMigration(object: string, d: DatasetDefinition, org?: str
|
|
|
100
96
|
const indexed = d.columns.filter((c) => c.indexed);
|
|
101
97
|
const lines: string[] = [];
|
|
102
98
|
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
if (d.description) lines.push(
|
|
107
|
-
lines.push("");
|
|
99
|
+
// NO CALLER TEXT IN A COMMENT (2026-10-04): the org's and the object's names once went into a
|
|
100
|
+
// header here, and a line break in either ended the comment and ran what followed as SQL. What
|
|
101
|
+
// this returns is run, so it holds only names the generator squeezed or the lint checked.
|
|
102
|
+
if (d.description) lines.push(`-- ${d.description.replace(/\s+/g, " ")}`);
|
|
108
103
|
lines.push(`CREATE TABLE IF NOT EXISTS ${t} (`);
|
|
109
104
|
lines.push(` id UUID PRIMARY KEY DEFAULT gen_random_uuid(),`);
|
|
110
105
|
lines.push(` -- WHOSE IT IS, and what it belongs to. Not the author's to name, and not optional:`);
|