@unoverse-platform/studio 0.1.105 → 0.1.106

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
@@ -249,7 +251,7 @@ export function localStudioHost(): Plugin {
249
251
  };
250
252
  const nodes = () => (nodesOnce ??= nodesDirs().then((dirs) => loadLocalNodes(baseSrc, ssr, dirs)));
251
253
 
252
- server.config.logger.info(`\n Studio reading: ${designHome(projectRoot)}\n`);
254
+ server.config.logger.info(`\n Studio reading: ${authoringRoot(projectRoot)}\n`);
253
255
 
254
256
  /**
255
257
  * WATCH THE PROJECT EXPLICITLY. Vite watches its own root, which is this package,
@@ -262,7 +264,7 @@ export function localStudioHost(): Plugin {
262
264
  *
263
265
  * Adding the paths puts them in the same watcher, so the existing hook fires.
264
266
  */
265
- for (const dir of [designHome(projectRoot)]) {
267
+ for (const dir of [authoringRoot(projectRoot)]) {
266
268
  const full = resolve(projectRoot, dir);
267
269
  if (existsSync(full)) server.watcher.add(full);
268
270
  }
@@ -296,7 +298,7 @@ export function localStudioHost(): Plugin {
296
298
  case path === "/local/info":
297
299
  return json(res, {
298
300
  projectRoot,
299
- rx: designHome(projectRoot),
301
+ rx: authoringRoot(projectRoot),
300
302
  nodes: await nodesDirs(),
301
303
  envFiles: [resolve(projectRoot, "..", ".env"), resolve(projectRoot, ".env")],
302
304
  });
@@ -358,7 +360,9 @@ export function localStudioHost(): Plugin {
358
360
  }
359
361
 
360
362
  case path === "/dev/orgs":
361
- return json(res, { orgs: c.themeOrgs() });
363
+ // `orgRepo` names the org when this folder is one org's repo (UNO-88): the picker
364
+ // then offers no second project, since a new org is a new repo.
365
+ return json(res, { orgs: c.themeOrgs(), orgRepo: orgRepoName(projectRoot) ?? null });
362
366
 
363
367
  /**
364
368
  * NEW PROJECT. The only thing Studio writes to disk that is not a definition.
@@ -373,6 +377,11 @@ export function localStudioHost(): Plugin {
373
377
  * project five are identical.
374
378
  */
375
379
  case path === "/dev/projects" && req.method === "POST": {
380
+ // ONE ORG, ONE REPO (UNO-88): a new org is a new repo, made with `unoverse create`
381
+ // in its own folder, never a second org inside this one.
382
+ if (orgRepoName(projectRoot)) {
383
+ return json(res, { error: "This repo is one org. A new org is a new repo: run `unoverse create` in a new folder." }, 400);
384
+ }
376
385
  const { createProject } = await import("./scaffold.mjs");
377
386
  const name = String((await body()).name ?? "");
378
387
  try {
@@ -482,9 +491,9 @@ export function localStudioHost(): Plugin {
482
491
  const p = String(b.path ?? "");
483
492
  if (!p || typeof b.text !== "string") return json(res, { error: "path and text are required" }, 400);
484
493
 
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 }[] } };
494
+ type LintModule = { lintDefinitions(designRoot: string, options?: Record<string, unknown>): { problems: { level?: string; file?: string }[] } };
495
+ type ObjectsLintModule = { lintObjects(designRoot: string, options?: Record<string, unknown>): { problems: { level?: string; file?: string }[] } };
496
+ type PromptsLintModule = { lintPrompts(designRoot: string, options?: Record<string, unknown>): { problems: { level?: string; file?: string }[] } };
488
497
  const lintMod = (await ssr(`${baseSrc}/lint/design/index.mjs`)) as unknown as LintModule;
489
498
  // OBJECTS AND PIPELINES TOO (objects README 9a, 2026-09-10): the same third linter
490
499
  // the publish gate runs, so a save here meets the sentence publish would print.
@@ -499,7 +508,7 @@ export function localStudioHost(): Plugin {
499
508
  b.text as string,
500
509
  writableRoots(projectRoot, c),
501
510
  projectRoot,
502
- (designRoot) => ({ problems: [...lintMod.lintDefinitions(designRoot).problems, ...objectsMod.lintObjects(designRoot).problems, ...promptsMod.lintPrompts(designRoot).problems] }),
511
+ (designRoot) => ({ problems: [...lintMod.lintDefinitions(designRoot, lintOptions(projectRoot)).problems, ...objectsMod.lintObjects(designRoot, lintOptions(projectRoot)).problems, ...promptsMod.lintPrompts(designRoot, lintOptions(projectRoot)).problems] }),
503
512
  );
504
513
  return json(res, result, result.saved ? 200 : result.error === "lint errors" ? 422 : 403);
505
514
  }
@@ -520,17 +529,19 @@ export function localStudioHost(): Plugin {
520
529
  type LintModule = {
521
530
  lintDefinitions(
522
531
  designRoot: string,
523
- options?: { overlay?: Record<string, string> },
532
+ options?: { overlay?: Record<string, string>; org?: { name: string; dir: string } },
524
533
  ): { problems: { level?: string; file?: string; line?: number; msg?: string }[] };
525
534
  };
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 }[] } };
535
+ type ObjectsLintModule = { lintObjects(designRoot: string, options?: { overlay?: Record<string, string>; org?: { name: string; dir: string } }): { problems: { level?: string; file?: string; line?: number; msg?: string }[] } };
536
+ 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
537
  const lintMod = (await ssr(`${baseSrc}/lint/design/index.mjs`)) as unknown as LintModule;
529
538
  const objectsMod = (await ssr(`${baseSrc}/lint/objects/index.mjs`)) as unknown as ObjectsLintModule;
530
539
  const promptsMod = (await ssr(`${baseSrc}/lint/prompts/index.mjs`)) as unknown as PromptsLintModule;
531
540
  const abs = resolve(p);
532
541
  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];
542
+ const root = authoringRoot(projectRoot);
543
+ const opts = lintOptions(projectRoot, { overlay });
544
+ const problems = [...lintMod.lintDefinitions(root, opts).problems, ...objectsMod.lintObjects(root, opts).problems, ...promptsMod.lintPrompts(root, opts).problems];
534
545
  return json(res, {
535
546
  findings: problems.filter((f) => f.file && resolve(f.file) === abs),
536
547
  elsewhere: problems.filter((f) => !f.file || resolve(f.file) !== abs).length,
@@ -590,11 +601,11 @@ export function localStudioHost(): Plugin {
590
601
  if (req.method === "POST") return json(res, { ok: true });
591
602
  return json(res, { entries: [] });
592
603
 
593
- // ---- The developer's OWN nodes (design/<project>/nodes/). No marketplace. ----
604
+ // ---- The developer's OWN nodes (the org's nodes/). No marketplace. ----
594
605
 
595
606
  case path === "/nodes": {
596
607
  const n = await nodes();
597
- if (!n) return json(res, { nodes: [], credentialTypes: [], reason: "no design/<project>/nodes/ folder in this workspace" });
608
+ if (!n) return json(res, { nodes: [], credentialTypes: [], reason: "no nodes/ folder in this org" });
598
609
  return json(res, { nodes: n.list(), credentialTypes: n.credentialTypes() });
599
610
  }
600
611
 
@@ -659,7 +670,7 @@ export function localStudioHost(): Plugin {
659
670
  * else, which means the developer's other editor, still reloads.
660
671
  */
661
672
  handleHotUpdate({ server, file }) {
662
- const watched = [designHome(projectRoot)];
673
+ const watched = [authoringRoot(projectRoot)];
663
674
  if (!watched.some((w) => file.startsWith(w))) return;
664
675
  const self = recentSelfWrites.get(resolve(file));
665
676
  if (self !== undefined && Date.now() - self < SELF_WRITE_WINDOW_MS) return [];
@@ -669,10 +680,10 @@ export function localStudioHost(): Plugin {
669
680
 
670
681
  configResolved() {
671
682
  // Surface a clear failure at startup rather than an empty catalog later.
672
- if (projectRoot && !existsSync(designHome(projectRoot))) {
683
+ if (projectRoot && !existsSync(authoringRoot(projectRoot))) {
673
684
  throw new Error(`No rx/ folder at ${projectRoot}`);
674
685
  }
675
- if (projectRoot) statSync(designHome(projectRoot));
686
+ if (projectRoot) statSync(authoringRoot(projectRoot));
676
687
  },
677
688
  };
678
689
  }
@@ -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;