@unoverse-platform/studio 0.1.105 → 0.1.107

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/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # @unoverse-platform/studio
2
2
 
3
- Unoverse Studio: the standalone authoring app. It reads your own `rx/` project folder
4
- off disk and previews your components, templates, skills and nodes. No database, no
3
+ Unoverse Studio: the standalone authoring app. It reads your org's own Git repo off
4
+ disk and previews your components, templates, skills and nodes. No database, no
5
5
  Docker, no backend of its own.
6
6
 
7
7
  ```bash
@@ -9,18 +9,36 @@ npm install @unoverse-platform/studio
9
9
  npx unoverse-studio
10
10
  ```
11
11
 
12
- Studio opens on your project folder. Author in your editor, see it in Studio.
12
+ Studio opens on your org repo. Author in your editor, see it in Studio. `unoverse create`
13
+ (choose Studio) makes the org repo: the baseline only, then Studio opens it. A new org is a
14
+ new repo, so Studio refuses New project inside an org repo.
13
15
 
14
16
  ## What you author
15
17
 
18
+ One org, one repo. The org's folders sit at the repo root, and `unoverse.yaml` declares
19
+ `org: <slug>`, so the org's name never comes from the folder name.
20
+
16
21
  ```
17
- rx/components/ components: definition-backed UI, rendered by the generic SDK
18
- rx/templates/ templates: whole app surfaces, states and layouts
19
- rx/styles/ themes and tokens
20
- prompts/ skills and prompt blocks
22
+ unoverse.yaml org: <slug>
23
+ styles/ themes and tokens
24
+ identity/ the org's four identity documents
25
+ components/ components: definition-backed UI, rendered by the generic SDK
26
+ templates/ templates: components that hold components
27
+ apps/ whole app surfaces, states and layouts
28
+ atoms/ shared building blocks
29
+ skills/ Agent skills
30
+ blocks/ prompt blocks
21
31
  nodes/ declarative YAML node manifests
32
+ agents/ Agents
33
+ objects/ objects
34
+ pipelines/ pipelines
22
35
  ```
23
36
 
37
+ A new org repo holds the baseline only: `styles/`, `identity/`, `unoverse.yaml` and a
38
+ pull-request check that runs `unoverse lint`. Samples live in the public demo org repo,
39
+ [org-acme](https://github.com/unoverse-platform/org-acme), to read and copy. `design/<org>/`
40
+ is only the platform monorepo's shape.
41
+
24
42
  Everything is data. There is no codegen and no build step for your assets: the
25
43
  definition IS the artifact, and the same definition renders on web, in ChatGPT, in
26
44
  Claude, and on every other channel a universe serves.
@@ -34,9 +52,11 @@ full client:
34
52
  MCP-served app shell (`ui://unoverse/app.html`, the same one ChatGPT loads) and the
35
53
  app runs its own connections: chat, streaming, voice. Studio ships no renderer of
36
54
  its own; the renderer loads when required, from the universe that owns it.
37
- - **Publish**: your components, templates, skills and prompts publish into the
38
- universe over its API, authorized by a publish key issued on that universe
39
- (`unoverse key`). Lint runs first; nothing leaves your machine until the rules pass.
55
+ - **Publish**: every universe pulls the org's repo; nothing is pushed in. Connect the
56
+ repo on the org page's Source tab, and a push arrives by webhook within seconds, by
57
+ polling within 5 minutes, or with Sync now. Until your universe runs a release with the Git sync,
58
+ `unoverse deploy studio` still publishes to your own universe over its API. Lint runs
59
+ first; nothing leaves your machine until the rules pass.
40
60
 
41
61
  The universe is somebody's deployment of the Unoverse platform. Yours, your team's, or
42
62
  a customer's. Studio never runs one.
package/bin/studio.mjs CHANGED
@@ -22,8 +22,8 @@ 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 { designHome, hasDesignHome } from "../local/design-home.mjs";
26
- import { createFirstProject, checkProjectName } from "../local/scaffold.mjs";
25
+ import { hasDesignHome, orgRepoName, authoringRoot } from "../local/design-home.mjs";
26
+ import { createOrgRepo, checkProjectName } from "../local/scaffold.mjs";
27
27
 
28
28
  const here = dirname(fileURLToPath(import.meta.url));
29
29
  const appRoot = resolve(here, "..");
@@ -43,7 +43,7 @@ const appRoot = resolve(here, "..");
43
43
  * (design/, or the legacy rx/). */
44
44
  function findProjectRootFromCwd() {
45
45
  for (let dir = process.cwd(); ; ) {
46
- if (hasDesignHome(dir)) return dir;
46
+ if (orgRepoName(dir) || hasDesignHome(dir)) return dir;
47
47
  if (hasDesignHome(join(dir, "apps/unoverse"))) return join(dir, "apps/unoverse");
48
48
  const parent = resolve(dir, "..");
49
49
  if (parent === dir) return process.cwd();
@@ -67,7 +67,7 @@ function findProjectBelowCwd() {
67
67
  const cwd = process.cwd();
68
68
  const found = readdirSync(cwd, { withFileTypes: true })
69
69
  .filter((e) => e.isDirectory() && !e.name.startsWith(".") && e.name !== "node_modules")
70
- .filter((e) => hasDesignHome(join(cwd, e.name)))
70
+ .filter((e) => orgRepoName(join(cwd, e.name)) || hasDesignHome(join(cwd, e.name)))
71
71
  .map((e) => e.name);
72
72
 
73
73
  if (found.length === 1) return join(cwd, found[0]);
@@ -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 (hasDesignHome(dir) || hasDesignHome(join(dir, "apps/unoverse"))) return;
86
+ if (orgRepoName(dir) || hasDesignHome(dir) || hasDesignHome(join(dir, "apps/unoverse"))) return;
87
87
  const parent = resolve(dir, "..");
88
88
  if (parent === dir) break;
89
89
  dir = parent;
@@ -113,7 +113,9 @@ async function scaffoldIfEmpty() {
113
113
  const home = cwd;
114
114
  // The folder's own name, when it can be a project name. It becomes an item's `org`
115
115
  // column, so `checkProjectName` is the same gate the UI's New project uses.
116
- const derived = basename(cwd);
116
+ // `unoverse create` passes the org name it asked for (`--org <name>`); else the folder's.
117
+ const asked = process.argv.includes("--org") ? process.argv[process.argv.indexOf("--org") + 1] : undefined;
118
+ const derived = asked ?? basename(cwd);
117
119
  let name = checkProjectName(derived) ? null : derived;
118
120
 
119
121
  if (!name) {
@@ -125,11 +127,11 @@ async function scaffoldIfEmpty() {
125
127
  // being named or why one was needed. The name is the folder under rx/ AND the org
126
128
  // column on everything that project publishes, which is why the tool cannot pick it.
127
129
  console.log(`\n Setting up Studio here.\n\n ${cwd}\n`);
128
- console.log(` Your components live in design/<project>/, and that name is the org they`);
129
- console.log(` publish under. This folder is "${derived}", which cannot be one.\n`);
130
+ console.log(` This folder becomes one org's own repo, and the org needs a name: it is`);
131
+ console.log(` what everything you build is published under. "${derived}" cannot be one.\n`);
130
132
  const rl = createInterface({ input: process.stdin, output: process.stdout });
131
133
  for (;;) {
132
- name = (await rl.question(" Name your Studio project [my-project]: ")).trim() || "my-project";
134
+ name = (await rl.question(" Name your org [my-org]: ")).trim() || "my-org";
133
135
  const bad = checkProjectName(name);
134
136
  if (!bad) break;
135
137
  console.log(` ${bad}\n`);
@@ -142,8 +144,11 @@ async function scaffoldIfEmpty() {
142
144
  // project (base collect.ts listProjects) and its folder name is the `org` that
143
145
  // publishes, so a flat rx/ could hold exactly one project and would read `components`
144
146
  // and `apps` as orgs.
145
- createFirstProject(home, name);
146
- console.log(`\n Created design/${name}/ in ${cwd}\n`);
147
+ // ONE ORG, ONE REPO (UNO-88): the folder becomes the org's repo, its folders at the root
148
+ // and its name declared in unoverse.yaml. Never a design/ tree.
149
+ const repo = createOrgRepo(home, name);
150
+ console.log(`\n Created the org "${name}" in ${cwd}${repo.state === "committed" ? " (a Git repo, first commit made)" : ""}\n`);
151
+ if (repo.message) console.log(` ${repo.message}\n`);
147
152
  }
148
153
 
149
154
  await scaffoldIfEmpty();
@@ -172,9 +177,12 @@ process.env.UNOVERSE_HOME = projectRoot;
172
177
  // several — picking one here would drop a sample into an arbitrary org. Those
173
178
  // samples arrive with `createFirstProject`, which is the only moment a destination
174
179
  // is unambiguous; workspace-level samples (nodes) still top up on every launch.
175
- try { topUpSamples(projectRoot); } catch { /* a read-only folder must not block launch */ }
180
+ // An org repo knows its org, so its new samples have somewhere to land.
181
+ const org = orgRepoName(projectRoot);
182
+ // An org repo gets no samples (they live in their own repo); the monorepo shape still tops up.
183
+ try { if (!org) topUpSamples(projectRoot); } catch { /* a read-only folder must not block launch */ }
176
184
  }
177
- console.log(`\n Studio reading: ${designHome(projectRoot)}\n`);
185
+ console.log(`\n Studio reading: ${authoringRoot(projectRoot)}\n`);
178
186
 
179
187
  /**
180
188
  * LAUNCHING STUDIO IS THE DECISION; A STALE STUDIO YIELDS THE PORT.
@@ -18,7 +18,7 @@
18
18
  *
19
19
  * See docs/architecture/authoring/LOCAL_STUDIO.md §Authoring in Studio.
20
20
  */
21
- import { designHome } from "./design-home.mjs";
21
+ import { authoringRoot } from "./design-home.mjs";
22
22
  import { resolve } from "path";
23
23
  import { existsSync, readdirSync } from "fs";
24
24
 
@@ -89,7 +89,7 @@ export function vocabulary(
89
89
 
90
90
  /** Atom names off disk, for completion of `Ref.ref`. */
91
91
  export function atomNames(projectRoot: string): { name: string }[] {
92
- const dirs = [resolve(designHome(projectRoot), "marketplace/atoms"), resolve(designHome(projectRoot), "atoms")];
92
+ const dirs = [resolve(authoringRoot(projectRoot), "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 } from "./design-home.mjs";
13
+ import { hasDesignHome, orgRepoName } from "./design-home.mjs";
14
14
  import { resolve } from "path";
15
15
  import { existsSync } from "fs";
16
16
 
@@ -45,6 +45,8 @@ type Loaders = {
45
45
  export function findProjectRoot(from: string = process.cwd()): string {
46
46
  let dir = resolve(from);
47
47
  for (;;) {
48
+ // An org's own repo (UNO-88): unoverse.yaml declares the org; its folders sit at the root.
49
+ if (orgRepoName(dir)) return dir;
48
50
  // A developer's project (starter kit): rx/ sits at the root.
49
51
  if (hasDesignHome(dir)) return dir;
50
52
  // This monorepo, where Studio's author works: rx/ lives under apps/unoverse. Same
@@ -93,7 +95,13 @@ export async function loadCatalog(
93
95
  * first and every later import sees the developer's home.
94
96
  */
95
97
  const paths = await load(resolve(baseSrc, "paths.ts"));
96
- (paths.setPaths as (n: { home: string }) => void)({ home: projectRoot });
98
+ // An org repo is ONE org, declared: base's projectDir(org) is the repo root and listOrgs()
99
+ // is that org alone (base definitions/registry/homes.ts).
100
+ const org = orgRepoName(projectRoot);
101
+ (paths.setPaths as (n: { home: string; orgRepo?: { org: string; dir: string } }) => void)({
102
+ home: projectRoot,
103
+ ...(org ? { orgRepo: { org, dir: projectRoot } } : {}),
104
+ });
97
105
 
98
106
  const defs = await load(resolve(baseSrc, "definitions/definitions.ts"));
99
107
  const theme = await load(resolve(baseSrc, "definitions/theme.ts"));
@@ -1,2 +1,9 @@
1
1
  export function designHome(root: string): string;
2
2
  export function hasDesignHome(root: string): boolean;
3
+
4
+ /** The org an org repo declares in its unoverse.yaml, or null when this is not an org repo (UNO-88). */
5
+ export function orgRepoName(root: string): string | null;
6
+ /** The folder Studio authors in and watches: the repo itself for an org repo, else design/. */
7
+ export function authoringRoot(root: string): string;
8
+ /** Lint options for this root: an org repo adds `org: { name, dir }`. */
9
+ export function lintOptions<T extends object>(root: string, extra?: T): T & { org?: { name: string; dir: string } };
@@ -5,7 +5,7 @@
5
5
  // canonical rule lives in @unoverse-platform/base `paths.ts` (`designDir`); this copy
6
6
  // exists because the plain-node entrypoints (bin/studio.mjs, scaffold.mjs) cannot
7
7
  // import base's TypeScript source. Keep the two in step.
8
- import { existsSync, renameSync } from "node:fs";
8
+ import { existsSync, renameSync, readFileSync } from "node:fs";
9
9
  import { join } from "node:path";
10
10
 
11
11
  export function designHome(root) {
@@ -29,3 +29,26 @@ export function designHome(root) {
29
29
  export function hasDesignHome(root) {
30
30
  return existsSync(join(root, "design")) || existsSync(join(root, "rx"));
31
31
  }
32
+
33
+ /**
34
+ * AN ORG REPO (UNO-88): `unoverse.yaml` declares `org: <slug>` and the org's folders sit at
35
+ * the root, with no design/. The name is declared, never read off the folder. Mirrors base
36
+ * items/collect.ts readOrgName (this file cannot import base's TypeScript). Keep in step.
37
+ */
38
+ export function orgRepoName(root) {
39
+ const path = join(root, "unoverse.yaml");
40
+ if (!existsSync(path)) return null;
41
+ const m = /^org:\s*["']?([a-z][a-z0-9-]{0,63})["']?\s*(?:#.*)?$/m.exec(readFileSync(path, "utf8"));
42
+ return m ? m[1] : null;
43
+ }
44
+
45
+ /** The folder Studio authors in and watches: the repo itself for an org repo, else design/. */
46
+ export function authoringRoot(root) {
47
+ return orgRepoName(root) ? root : designHome(root);
48
+ }
49
+
50
+ /** Lint options for this root: an org repo names its org and folder (base lint options.org). */
51
+ export function lintOptions(root, extra = {}) {
52
+ const org = orgRepoName(root);
53
+ return org ? { ...extra, org: { name: org, dir: root } } : extra;
54
+ }
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 } from "./design-home.mjs";
18
+ import { designHome, authoringRoot, orgRepoName, lintOptions } 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";
@@ -53,7 +53,7 @@ function definitionHome(
53
53
  return null;
54
54
  }
55
55
  if (/\/atoms\//.test(uri)) {
56
- const flat = resolve(designHome(projectRoot), "atoms", `${ref.toLowerCase()}.json`);
56
+ const flat = resolve(authoringRoot(projectRoot), "atoms", `${ref.toLowerCase()}.json`);
57
57
  return existsSync(flat) ? flat : null;
58
58
  }
59
59
  // Templates (unoverse://templates/): two tiers, folder form, like components.
@@ -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(designHome(projectRoot), "marketplace/templates", ref.toLowerCase())];
65
+ : [resolve(authoringRoot(projectRoot), "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(designHome(projectRoot), "marketplace/components"), resolve(designHome(projectRoot), "components")]) {
72
+ for (const dir of [resolve(authoringRoot(projectRoot), "marketplace/components"), resolve(authoringRoot(projectRoot), "components")]) {
73
73
  const flat = resolve(dir, `${ref.toLowerCase()}.json`);
74
74
  if (existsSync(flat)) return flat;
75
75
  }
@@ -178,10 +178,12 @@ function readMarkdownLibrary(dir: string): MarkdownEntry[] {
178
178
  * another org's, the rule RX_ORG_MODEL.md settled for a project's assets.
179
179
  */
180
180
  function readLibrary(projectRoot: string, folder: "skills" | "blocks", orgs: string[], org?: string): MarkdownEntry[] {
181
- const out = readMarkdownLibrary(resolve(designHome(projectRoot), "marketplace", folder));
181
+ const out = readMarkdownLibrary(resolve(authoringRoot(projectRoot), "marketplace", folder));
182
182
  for (const o of orgs) {
183
183
  if (org && org !== "all" && o !== org) continue;
184
- for (const e of readMarkdownLibrary(resolve(designHome(projectRoot), o, folder))) {
184
+ // The org's own folder: the repo root in an org repo (UNO-88), else design/<org>/.
185
+ const orgHome = orgRepoName(projectRoot) ? projectRoot : resolve(designHome(projectRoot), o);
186
+ for (const e of readMarkdownLibrary(resolve(orgHome, folder))) {
185
187
  // A SKILL's lookup name carries its org, because that is the name every reader
186
188
  // addresses it by and two orgs may hold one bare name. A BLOCK's does not: the
187
189
  // server tags a block with its org and leaves the name alone, and the shape here
@@ -227,11 +229,15 @@ export function localStudioHost(): Plugin {
227
229
  * is what kept Studio from being publishable at all.
228
230
  */
229
231
  const vendored = resolve(dirname(fileURLToPath(import.meta.url)), "../vendor/base/src");
232
+ // AN INSTALLED STUDIO USES ONLY ITS OWN COPY. Node can still find a base on npm, an
233
+ // old one the marketplace package depends on (0.3.67, deprecated), and preferring it
234
+ // crashed 0.1.106 on a file that copy predates (orgDocuments.ts, 2026-10-03). Only
235
+ // the monorepo, where Studio is not under node_modules, resolves the live package.
236
+ const installed = fileURLToPath(import.meta.url).includes("/node_modules/");
230
237
  try {
231
- baseSrc = resolve(
232
- createRequire(import.meta.url).resolve("@unoverse-platform/base/package.json"),
233
- "../src",
234
- );
238
+ baseSrc = installed
239
+ ? vendored
240
+ : resolve(createRequire(import.meta.url).resolve("@unoverse-platform/base/package.json"), "../src");
235
241
  } catch {
236
242
  baseSrc = vendored;
237
243
  }
@@ -249,7 +255,7 @@ export function localStudioHost(): Plugin {
249
255
  };
250
256
  const nodes = () => (nodesOnce ??= nodesDirs().then((dirs) => loadLocalNodes(baseSrc, ssr, dirs)));
251
257
 
252
- server.config.logger.info(`\n Studio reading: ${designHome(projectRoot)}\n`);
258
+ server.config.logger.info(`\n Studio reading: ${authoringRoot(projectRoot)}\n`);
253
259
 
254
260
  /**
255
261
  * WATCH THE PROJECT EXPLICITLY. Vite watches its own root, which is this package,
@@ -262,7 +268,7 @@ export function localStudioHost(): Plugin {
262
268
  *
263
269
  * Adding the paths puts them in the same watcher, so the existing hook fires.
264
270
  */
265
- for (const dir of [designHome(projectRoot)]) {
271
+ for (const dir of [authoringRoot(projectRoot)]) {
266
272
  const full = resolve(projectRoot, dir);
267
273
  if (existsSync(full)) server.watcher.add(full);
268
274
  }
@@ -296,7 +302,7 @@ export function localStudioHost(): Plugin {
296
302
  case path === "/local/info":
297
303
  return json(res, {
298
304
  projectRoot,
299
- rx: designHome(projectRoot),
305
+ rx: authoringRoot(projectRoot),
300
306
  nodes: await nodesDirs(),
301
307
  envFiles: [resolve(projectRoot, "..", ".env"), resolve(projectRoot, ".env")],
302
308
  });
@@ -358,7 +364,9 @@ export function localStudioHost(): Plugin {
358
364
  }
359
365
 
360
366
  case path === "/dev/orgs":
361
- return json(res, { orgs: c.themeOrgs() });
367
+ // `orgRepo` names the org when this folder is one org's repo (UNO-88): the picker
368
+ // then offers no second project, since a new org is a new repo.
369
+ return json(res, { orgs: c.themeOrgs(), orgRepo: orgRepoName(projectRoot) ?? null });
362
370
 
363
371
  /**
364
372
  * NEW PROJECT. The only thing Studio writes to disk that is not a definition.
@@ -373,6 +381,11 @@ export function localStudioHost(): Plugin {
373
381
  * project five are identical.
374
382
  */
375
383
  case path === "/dev/projects" && req.method === "POST": {
384
+ // ONE ORG, ONE REPO (UNO-88): a new org is a new repo, made with `unoverse create`
385
+ // 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);
388
+ }
376
389
  const { createProject } = await import("./scaffold.mjs");
377
390
  const name = String((await body()).name ?? "");
378
391
  try {
@@ -482,9 +495,9 @@ export function localStudioHost(): Plugin {
482
495
  const p = String(b.path ?? "");
483
496
  if (!p || typeof b.text !== "string") return json(res, { error: "path and text are required" }, 400);
484
497
 
485
- type LintModule = { lintDefinitions(designRoot: string): { problems: { level?: string; file?: string }[] } };
486
- type ObjectsLintModule = { lintObjects(designRoot: string): { problems: { level?: string; file?: string }[] } };
487
- type PromptsLintModule = { lintPrompts(designRoot: string): { problems: { level?: string; file?: string }[] } };
498
+ type LintModule = { lintDefinitions(designRoot: string, options?: Record<string, unknown>): { problems: { level?: string; file?: string }[] } };
499
+ type ObjectsLintModule = { lintObjects(designRoot: string, options?: Record<string, unknown>): { problems: { level?: string; file?: string }[] } };
500
+ type PromptsLintModule = { lintPrompts(designRoot: string, options?: Record<string, unknown>): { problems: { level?: string; file?: string }[] } };
488
501
  const lintMod = (await ssr(`${baseSrc}/lint/design/index.mjs`)) as unknown as LintModule;
489
502
  // OBJECTS AND PIPELINES TOO (objects README 9a, 2026-09-10): the same third linter
490
503
  // the publish gate runs, so a save here meets the sentence publish would print.
@@ -499,7 +512,7 @@ export function localStudioHost(): Plugin {
499
512
  b.text as string,
500
513
  writableRoots(projectRoot, c),
501
514
  projectRoot,
502
- (designRoot) => ({ problems: [...lintMod.lintDefinitions(designRoot).problems, ...objectsMod.lintObjects(designRoot).problems, ...promptsMod.lintPrompts(designRoot).problems] }),
515
+ (designRoot) => ({ problems: [...lintMod.lintDefinitions(designRoot, lintOptions(projectRoot)).problems, ...objectsMod.lintObjects(designRoot, lintOptions(projectRoot)).problems, ...promptsMod.lintPrompts(designRoot, lintOptions(projectRoot)).problems] }),
503
516
  );
504
517
  return json(res, result, result.saved ? 200 : result.error === "lint errors" ? 422 : 403);
505
518
  }
@@ -520,17 +533,19 @@ export function localStudioHost(): Plugin {
520
533
  type LintModule = {
521
534
  lintDefinitions(
522
535
  designRoot: string,
523
- options?: { overlay?: Record<string, string> },
536
+ options?: { overlay?: Record<string, string>; org?: { name: string; dir: string } },
524
537
  ): { problems: { level?: string; file?: string; line?: number; msg?: string }[] };
525
538
  };
526
- type ObjectsLintModule = { lintObjects(designRoot: string, options?: { overlay?: Record<string, string> }): { problems: { level?: string; file?: string; line?: number; msg?: string }[] } };
527
- type PromptsLintModule = { lintPrompts(designRoot: string, options?: { overlay?: Record<string, string> }): { problems: { level?: string; file?: string; line?: number; msg?: string }[] } };
539
+ type ObjectsLintModule = { lintObjects(designRoot: string, options?: { overlay?: Record<string, string>; org?: { name: string; dir: string } }): { problems: { level?: string; file?: string; line?: number; msg?: string }[] } };
540
+ type PromptsLintModule = { lintPrompts(designRoot: string, options?: { overlay?: Record<string, string>; org?: { name: string; dir: string } }): { problems: { level?: string; file?: string; line?: number; msg?: string }[] } };
528
541
  const lintMod = (await ssr(`${baseSrc}/lint/design/index.mjs`)) as unknown as LintModule;
529
542
  const objectsMod = (await ssr(`${baseSrc}/lint/objects/index.mjs`)) as unknown as ObjectsLintModule;
530
543
  const promptsMod = (await ssr(`${baseSrc}/lint/prompts/index.mjs`)) as unknown as PromptsLintModule;
531
544
  const abs = resolve(p);
532
545
  const overlay = { [abs]: b.text as string };
533
- const problems = [...lintMod.lintDefinitions(designHome(projectRoot), { overlay }).problems, ...objectsMod.lintObjects(designHome(projectRoot), { overlay }).problems, ...promptsMod.lintPrompts(designHome(projectRoot), { overlay }).problems];
546
+ const root = authoringRoot(projectRoot);
547
+ const opts = lintOptions(projectRoot, { overlay });
548
+ const problems = [...lintMod.lintDefinitions(root, opts).problems, ...objectsMod.lintObjects(root, opts).problems, ...promptsMod.lintPrompts(root, opts).problems];
534
549
  return json(res, {
535
550
  findings: problems.filter((f) => f.file && resolve(f.file) === abs),
536
551
  elsewhere: problems.filter((f) => !f.file || resolve(f.file) !== abs).length,
@@ -590,11 +605,11 @@ export function localStudioHost(): Plugin {
590
605
  if (req.method === "POST") return json(res, { ok: true });
591
606
  return json(res, { entries: [] });
592
607
 
593
- // ---- The developer's OWN nodes (design/<project>/nodes/). No marketplace. ----
608
+ // ---- The developer's OWN nodes (the org's nodes/). No marketplace. ----
594
609
 
595
610
  case path === "/nodes": {
596
611
  const n = await nodes();
597
- if (!n) return json(res, { nodes: [], credentialTypes: [], reason: "no design/<project>/nodes/ folder in this workspace" });
612
+ if (!n) return json(res, { nodes: [], credentialTypes: [], reason: "no nodes/ folder in this org" });
598
613
  return json(res, { nodes: n.list(), credentialTypes: n.credentialTypes() });
599
614
  }
600
615
 
@@ -659,7 +674,7 @@ export function localStudioHost(): Plugin {
659
674
  * else, which means the developer's other editor, still reloads.
660
675
  */
661
676
  handleHotUpdate({ server, file }) {
662
- const watched = [designHome(projectRoot)];
677
+ const watched = [authoringRoot(projectRoot)];
663
678
  if (!watched.some((w) => file.startsWith(w))) return;
664
679
  const self = recentSelfWrites.get(resolve(file));
665
680
  if (self !== undefined && Date.now() - self < SELF_WRITE_WINDOW_MS) return [];
@@ -669,10 +684,10 @@ export function localStudioHost(): Plugin {
669
684
 
670
685
  configResolved() {
671
686
  // Surface a clear failure at startup rather than an empty catalog later.
672
- if (projectRoot && !existsSync(designHome(projectRoot))) {
687
+ if (projectRoot && !existsSync(authoringRoot(projectRoot))) {
673
688
  throw new Error(`No rx/ folder at ${projectRoot}`);
674
689
  }
675
- if (projectRoot) statSync(designHome(projectRoot));
690
+ if (projectRoot) statSync(authoringRoot(projectRoot));
676
691
  },
677
692
  };
678
693
  }
@@ -3,8 +3,7 @@
3
3
  # chips and the composer. The switching rides the projected conversation facts inside
4
4
  # the component; nothing is wired here. Shared by core and core-focus.
5
5
  #
6
- # A BARE ref resolves your own project's component: a ref inside a project's tree
7
- # matches that project first. Qualify it (`<project>/text-chat`) only if you ship two
8
- # projects that both carry the name.
6
+ # QUALIFIED with the org: many orgs ship a text-chat, and a bare ref to a name two orgs
7
+ # hold is an error. Studio writes the new org's name here when it copies this app.
9
8
  type: Ref
10
9
  ref: text-chat
@@ -9,8 +9,14 @@ export function checkProjectName(name: string): string | null;
9
9
  /** Create design/<name>/ under `home`. Returns the created path. Throws if unusable. */
10
10
  export function createProject(home: string, name: string): string;
11
11
 
12
- /** The first project, plus the editor settings and the CI template. Returns the project path. */
12
+ /** The first project, plus the editor settings, the pull-request check, and a Git repo with a first commit. Returns the project path. */
13
13
  export function createFirstProject(home: string, name: string): string;
14
14
 
15
- /** Write .github/workflows/unoverse.yml for `project`, once. Never replaces an existing file. */
16
- export function writeCiWorkflow(home: string, project: string): void;
15
+ /** Make `dir` one org's own repo (UNO-88): folders at the root, unoverse.yaml declaring the org, samples, editor globs, the check, Git. */
16
+ export function createOrgRepo(dir: string, org: string): { state: "committed" | "no-identity" | "inside-repo" | "no-git"; message?: string };
17
+
18
+ /** Make `home` a Git repo on main with a first commit, unless it already is one; never an author it was not given. */
19
+ export function startRepo(home: string, project: string): { state: "committed" | "no-identity" | "inside-repo" | "no-git"; message?: string };
20
+
21
+ /** Write .github/workflows/unoverse.yml for `project`, once: the pull-request check. Never replaces an existing file. */
22
+ export function writeCiWorkflow(home: string, project: string | null): void;