@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 +30 -10
- package/bin/studio.mjs +21 -13
- package/local/authoring.ts +2 -2
- package/local/catalog.ts +10 -2
- package/local/design-home.d.mts +7 -0
- package/local/design-home.mjs +24 -1
- package/local/plugin.ts +42 -27
- package/local/sample-app/chat/components/chat-region.yaml +2 -3
- package/local/scaffold.d.mts +9 -3
- package/local/scaffold.mjs +107 -45
- package/local/source.ts +2 -2
- package/package.json +1 -1
- package/screens/EmptyLibrary.tsx +2 -2
- package/screens/IdentityView.tsx +1 -1
- package/screens/TemplatesView.tsx +1 -1
- package/src/App.tsx +10 -5
- package/vendor/base/src/definitions/registry/homes.ts +7 -1
- package/vendor/base/src/definitions/registry/orgDocuments.ts +5 -2
- package/vendor/base/src/items/collect.ts +85 -4
- package/vendor/base/src/items/publish.ts +23 -2
- package/vendor/base/src/lint/design/index.d.mts +8 -1
- package/vendor/base/src/lint/design/index.mjs +31 -8
- package/vendor/base/src/lint/objects/index.d.mts +2 -0
- package/vendor/base/src/lint/objects/index.mjs +12 -6
- package/vendor/base/src/lint/prompts/index.d.mts +2 -0
- package/vendor/base/src/lint/prompts/index.mjs +8 -3
- package/vendor/base/src/paths.ts +5 -0
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
|
|
4
|
-
|
|
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
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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**:
|
|
38
|
-
|
|
39
|
-
|
|
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 {
|
|
26
|
-
import {
|
|
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
|
-
|
|
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(`
|
|
129
|
-
console.log(`
|
|
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
|
|
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
|
-
|
|
146
|
-
|
|
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
|
-
|
|
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: ${
|
|
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.
|
package/local/authoring.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
*
|
|
19
19
|
* See docs/architecture/authoring/LOCAL_STUDIO.md §Authoring in Studio.
|
|
20
20
|
*/
|
|
21
|
-
import {
|
|
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(
|
|
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
|
-
|
|
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"));
|
package/local/design-home.d.mts
CHANGED
|
@@ -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 } };
|
package/local/design-home.mjs
CHANGED
|
@@ -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(
|
|
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(
|
|
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(
|
|
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(
|
|
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
|
-
|
|
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 =
|
|
232
|
-
|
|
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: ${
|
|
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 [
|
|
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:
|
|
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
|
-
|
|
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
|
|
527
|
-
type PromptsLintModule = { lintPrompts(designRoot: string, options?: { overlay?: Record<string, 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
|
|
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 (
|
|
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
|
|
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 = [
|
|
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(
|
|
687
|
+
if (projectRoot && !existsSync(authoringRoot(projectRoot))) {
|
|
673
688
|
throw new Error(`No rx/ folder at ${projectRoot}`);
|
|
674
689
|
}
|
|
675
|
-
if (projectRoot) statSync(
|
|
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
|
-
#
|
|
7
|
-
#
|
|
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
|
package/local/scaffold.d.mts
CHANGED
|
@@ -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
|
|
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
|
-
/**
|
|
16
|
-
export function
|
|
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;
|