unoverse 0.1.212 → 0.1.214
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/unoverse.mjs +3 -3
- package/lib/create/index.mjs +18 -2
- package/lib/lint.mjs +11 -0
- package/lib/login.mjs +3 -3
- package/lib/publish.mjs +46 -20
- package/lib/ui.mjs +1 -1
- package/lib/workspace.mjs +21 -5
- package/package.json +1 -1
- package/vendor/base/items/collect.js +76 -4
- package/vendor/base/items/publish.js +21 -1
- package/vendor/base/lint/design/index.mjs +39 -7
- package/vendor/base/lint/objects/index.mjs +12 -6
- package/vendor/base/lint/prompts/index.mjs +8 -3
package/bin/unoverse.mjs
CHANGED
|
@@ -261,7 +261,7 @@ switch (cmd) {
|
|
|
261
261
|
// No URL typed: the workspace's committed target is the obvious answer.
|
|
262
262
|
const { findWorkspace, readConfig } = await import("../lib/workspace.mjs");
|
|
263
263
|
const ws = findWorkspace();
|
|
264
|
-
if (ws?.kind === "assets") target = (await readConfig(ws.root)).universe;
|
|
264
|
+
if (ws?.kind === "assets" || ws?.kind === "org") target = (await readConfig(ws.root)).universe;
|
|
265
265
|
}
|
|
266
266
|
if (!target) {
|
|
267
267
|
console.error(`\n Which universe? unoverse login <url>\n ${dim("(or run it from a workspace whose unoverse.yaml names one)")}\n`);
|
|
@@ -284,8 +284,8 @@ switch (cmd) {
|
|
|
284
284
|
if (args[0] === "studio") {
|
|
285
285
|
const { findWorkspace } = await import("../lib/workspace.mjs");
|
|
286
286
|
const ws = findWorkspace();
|
|
287
|
-
if (ws?.kind !== "assets") {
|
|
288
|
-
console.log(`\n deploy studio ships
|
|
287
|
+
if (ws?.kind !== "assets" && ws?.kind !== "org") {
|
|
288
|
+
console.log(`\n deploy studio ships an org repo or design workspace, and there is none above ${process.cwd()}.\n Start one with: unoverse create\n`);
|
|
289
289
|
process.exit(1);
|
|
290
290
|
}
|
|
291
291
|
const { publish } = await import("../lib/publish.mjs");
|
package/lib/create/index.mjs
CHANGED
|
@@ -25,9 +25,25 @@ export async function create(nameArg) {
|
|
|
25
25
|
try {
|
|
26
26
|
|
|
27
27
|
if (choice === "1") {
|
|
28
|
-
|
|
28
|
+
// ONE ORG, ONE REPO (UNO-88). This folder becomes the org's own repo: its folders at
|
|
29
|
+
// the root, its name declared in unoverse.yaml, Git with a first commit. Studio makes
|
|
30
|
+
// it on first launch from the name asked here, then opens it.
|
|
31
|
+
const here = process.cwd();
|
|
32
|
+
const suggested = (nameArg || here.split(/[\\/]/).pop() || "").toLowerCase().replace(/[^a-z0-9-]+/g, "-").replace(/^[^a-z]+|-+$/g, "") || "my-org";
|
|
33
|
+
let org = "";
|
|
34
|
+
for (;;) {
|
|
35
|
+
org = (await ask(rl, `Org name ${dim(`[${suggested}]`)}: `)) || suggested;
|
|
36
|
+
if (/^[a-z][a-z0-9-]{0,63}$/.test(org)) break;
|
|
37
|
+
console.log(` ${dim("Lowercase letters, digits and hyphens, starting with a letter.")}`);
|
|
38
|
+
}
|
|
29
39
|
rl.close();
|
|
30
|
-
|
|
40
|
+
console.log(`\n ${dim(`Making ${here} the org "${org}", then opening Studio.`)}\n`);
|
|
41
|
+
// In the platform monorepo, this repo's own Studio (so unreleased work can be tried);
|
|
42
|
+
// everywhere else, the published one.
|
|
43
|
+
const local = resolve(dirname(fileURLToPath(import.meta.url)), "../../../studio/bin/studio.mjs");
|
|
44
|
+
const r = existsSync(local)
|
|
45
|
+
? spawnSync(process.execPath, [local, "--org", org], { stdio: "inherit" })
|
|
46
|
+
: spawnSync("npx", ["-y", "@unoverse-platform/studio@latest", "--org", org], { stdio: "inherit" });
|
|
31
47
|
process.exit(r.status ?? 0);
|
|
32
48
|
}
|
|
33
49
|
|
package/lib/lint.mjs
CHANGED
|
@@ -39,6 +39,17 @@ export async function lintProject({ lintForPublish, designRoot, project }) {
|
|
|
39
39
|
export async function lint(args, importBase) {
|
|
40
40
|
const positional = args.find((a) => !a.startsWith("--"));
|
|
41
41
|
const ws = findWorkspace();
|
|
42
|
+
// AN ORG REPO (UNO-88): one org, declared; lint it whole with its declared name.
|
|
43
|
+
if (ws?.kind === "org") {
|
|
44
|
+
if (positional && positional !== ws.org) {
|
|
45
|
+
console.error(`\n ${c.red}✗${c.off} this repo is the org "${ws.org}", not "${positional}"\n`);
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
const { lintOrg } = await importBase("items/publish.js");
|
|
49
|
+
console.log(`\n ${c.bold}lint ${ws.org}${c.off}\n`);
|
|
50
|
+
const { errors } = await lintProject({ lintForPublish: () => lintOrg({ org: ws.org, dir: ws.root }), designRoot: ws.root, project: ws.org });
|
|
51
|
+
process.exit(errors.length ? 1 : 0);
|
|
52
|
+
}
|
|
42
53
|
if (ws?.kind !== "assets") {
|
|
43
54
|
console.error(`\n ${c.red}✗${c.off} no design/ folder here or above. Run this inside your workspace.\n`);
|
|
44
55
|
process.exit(1);
|
package/lib/login.mjs
CHANGED
|
@@ -174,7 +174,7 @@ export async function login(universeInput, { log = console.log } = {}) {
|
|
|
174
174
|
if (!config.authEnabled) {
|
|
175
175
|
throw new Error(
|
|
176
176
|
`${origin} has authentication switched off, so there is no account to sign in with.\n` +
|
|
177
|
-
` To deploy there, set UNOVERSE_TOKEN to a
|
|
177
|
+
` To deploy there, set UNOVERSE_TOKEN to a publish key minted on the universe itself.`,
|
|
178
178
|
);
|
|
179
179
|
}
|
|
180
180
|
if (!config.issuer) throw new Error(`${origin} has no identity provider configured.`);
|
|
@@ -257,13 +257,13 @@ export async function tokenFor(origin, { interactive = true, log = console.log }
|
|
|
257
257
|
} else if (!config.authEnabled) {
|
|
258
258
|
throw new Error(
|
|
259
259
|
`${origin} has authentication switched off.\n` +
|
|
260
|
-
` To deploy there, set UNOVERSE_TOKEN to a
|
|
260
|
+
` To deploy there, set UNOVERSE_TOKEN to a publish key minted on the universe itself.`,
|
|
261
261
|
);
|
|
262
262
|
}
|
|
263
263
|
|
|
264
264
|
if (!interactive || !process.stdin.isTTY) {
|
|
265
265
|
throw new Error(
|
|
266
|
-
`No credential for ${origin}. In CI, set UNOVERSE_TOKEN to a
|
|
266
|
+
`No credential for ${origin}. In CI, set UNOVERSE_TOKEN to a publish key.\n` +
|
|
267
267
|
` At a terminal, run: unoverse login ${origin}`,
|
|
268
268
|
);
|
|
269
269
|
}
|
package/lib/publish.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* unoverse
|
|
2
|
+
* unoverse deploy studio: ship an asset workspace's items to its universe.
|
|
3
3
|
*
|
|
4
4
|
* The terminal face of @unoverse-platform/base items/publish. The order is the safety:
|
|
5
5
|
* LINT, then COMPARE (a dry-run per item against the universe), then a plan and a
|
|
@@ -49,7 +49,7 @@ const ask = async (question) => {
|
|
|
49
49
|
};
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
|
-
* `unoverse
|
|
52
|
+
* `unoverse deploy studio [project] [--to <universe> | --env <name>] [--dry-run] [--yes]`
|
|
53
53
|
* Run from anywhere inside an asset workspace. bin/unoverse.mjs routes here only when
|
|
54
54
|
* the nearest marker up from cwd is a design home.
|
|
55
55
|
*/
|
|
@@ -59,32 +59,58 @@ export async function publish(args) {
|
|
|
59
59
|
const i = args.indexOf(`--${name}`);
|
|
60
60
|
return i >= 0 ? args[i + 1] : undefined;
|
|
61
61
|
};
|
|
62
|
-
|
|
62
|
+
// An option's value is never the project: `--env uat` must not read "uat" as one.
|
|
63
|
+
const optionValues = new Set(["to", "env"].map(opt).filter(Boolean));
|
|
64
|
+
const positional = args.find((a) => !a.startsWith("--") && !optionValues.has(a));
|
|
63
65
|
|
|
64
66
|
const ws = findWorkspace();
|
|
65
|
-
if (ws?.kind !== "assets") die("no design/ folder here or above. Run this inside your workspace.");
|
|
66
|
-
const designRoot = designHome(ws.root);
|
|
67
|
+
if (ws?.kind !== "assets" && ws?.kind !== "org") die("no org repo or design/ folder here or above. Run this inside your workspace.");
|
|
67
68
|
|
|
68
|
-
const { collectProject, listProjects } = await importBase("items/collect.js");
|
|
69
|
-
const { lintForPublish, planPublish, sendPublish } = await importBase("items/publish.js");
|
|
69
|
+
const { collectProject, listProjects, collectOrg } = await importBase("items/collect.js");
|
|
70
|
+
const { lintForPublish, planPublish, sendPublish, lintOrg } = await importBase("items/publish.js");
|
|
70
71
|
|
|
71
72
|
// ── which project ───────────────────────────────────────────────────────────
|
|
72
|
-
|
|
73
|
-
|
|
73
|
+
// An org repo (UNO-88) IS one org, declared in its unoverse.yaml; the monorepo shape
|
|
74
|
+
// (design/<project>/) may hold several and asks which.
|
|
74
75
|
let project = positional;
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
76
|
+
let designRoot;
|
|
77
|
+
let collect;
|
|
78
|
+
let lintFor = lintForPublish;
|
|
79
|
+
if (ws.kind === "org") {
|
|
80
|
+
if (project && project !== ws.org) die(`this repo is the org "${ws.org}", not "${project}"`);
|
|
81
|
+
project = ws.org;
|
|
82
|
+
designRoot = ws.root;
|
|
83
|
+
collect = () => collectOrg({ org: ws.org, dir: ws.root });
|
|
84
|
+
lintFor = () => lintOrg({ org: ws.org, dir: ws.root });
|
|
85
|
+
} else {
|
|
86
|
+
designRoot = designHome(ws.root);
|
|
87
|
+
const projects = listProjects(designRoot);
|
|
88
|
+
if (!projects.length) die(`nothing to publish: ${designRoot} holds no projects`);
|
|
89
|
+
if (!project) {
|
|
90
|
+
if (projects.length > 1) {
|
|
91
|
+
die(`several projects here. Say which:\n\n unoverse deploy studio <project>\n\n Found: ${projects.join(", ")}`);
|
|
92
|
+
}
|
|
93
|
+
project = projects[0];
|
|
94
|
+
} else if (!projects.includes(project)) {
|
|
95
|
+
die(`no project "${project}" in ${designRoot}. Found: ${projects.join(", ") || "none"}`);
|
|
78
96
|
}
|
|
79
|
-
|
|
80
|
-
} else if (!projects.includes(project)) {
|
|
81
|
-
die(`no project "${project}" in ${designRoot}. Found: ${projects.join(", ") || "none"}`);
|
|
97
|
+
collect = () => collectProject(designRoot, project);
|
|
82
98
|
}
|
|
83
99
|
|
|
84
|
-
// ── which universe: --to overrides,
|
|
85
|
-
//
|
|
100
|
+
// ── which universe: --to overrides, --env names one from unoverse.yaml's
|
|
101
|
+
// `environments:`, `universe:` is the default, the login answers, and only a
|
|
102
|
+
// machine that knows nothing asks ─────────────────────────────────────────
|
|
86
103
|
const config = await readConfig(ws.root);
|
|
87
|
-
|
|
104
|
+
const envName = opt("env");
|
|
105
|
+
let envUniverse;
|
|
106
|
+
if (envName && !opt("to")) {
|
|
107
|
+
envUniverse = config.environments?.[envName];
|
|
108
|
+
if (!envUniverse) {
|
|
109
|
+
const known = Object.keys(config.environments ?? {});
|
|
110
|
+
die(`no environment "${envName}" in unoverse.yaml. ${known.length ? `Found: ${known.join(", ")}` : "Add one:\n\n environments:\n " + envName + ": https://your-universe.example"}`);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
let universe = opt("to") ?? envUniverse ?? config.universe;
|
|
88
114
|
if (!universe) {
|
|
89
115
|
// THE LOGIN ALREADY NAMED THE UNIVERSE. `unoverse login` takes an address and
|
|
90
116
|
// keeps a session for it, so a machine signed in to exactly one universe has
|
|
@@ -115,14 +141,14 @@ export async function publish(args) {
|
|
|
115
141
|
// ── 1. lint. Nothing is sent if this fails ──────────────────────────────────
|
|
116
142
|
console.log(`\n ${c.bold}deploy ${project}${c.off} ${c.dim}→ ${universe}${c.off}\n`);
|
|
117
143
|
const { lintProject } = await import("./lint.mjs");
|
|
118
|
-
const { errors } = await lintProject({ lintForPublish, designRoot, project });
|
|
144
|
+
const { errors } = await lintProject({ lintForPublish: lintFor, designRoot, project });
|
|
119
145
|
if (errors.length) {
|
|
120
146
|
console.error(` ${c.red}Nothing was sent.${c.off}\n`);
|
|
121
147
|
process.exit(1);
|
|
122
148
|
}
|
|
123
149
|
|
|
124
150
|
// ── 2. the credential, only now that there is something worth sending ───────
|
|
125
|
-
const items =
|
|
151
|
+
const items = collect();
|
|
126
152
|
if (!items.length) die(`nothing to publish in ${project}`);
|
|
127
153
|
let token;
|
|
128
154
|
try {
|
package/lib/ui.mjs
CHANGED
|
@@ -22,7 +22,7 @@ export async function ask(rl, q) {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
export const OPTIONS = [
|
|
25
|
-
["Studio", "Components, agents and
|
|
25
|
+
["Studio", "Components, apps, agents and nodes", "Most people start here"],
|
|
26
26
|
["Universe", "Run the platform yourself, on your own infrastructure"],
|
|
27
27
|
["Client", "A client accelerator that talks to unoverse"],
|
|
28
28
|
];
|
package/lib/workspace.mjs
CHANGED
|
@@ -6,10 +6,11 @@
|
|
|
6
6
|
* reason studio keeps one: this package is plain node and cannot import base's TypeScript.
|
|
7
7
|
* Keep the three in step.
|
|
8
8
|
*
|
|
9
|
-
* `unoverse.yaml` at the workspace root is the COMMITTED project config
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
9
|
+
* `unoverse.yaml` at the workspace root is the COMMITTED project config: `universe:`, the
|
|
10
|
+
* default deploy target, and `environments:`, named targets (`uat`, `production`) that CI
|
|
11
|
+
* picks with `--env`. It is config, not a secret: the whole team and CI deploy to the same
|
|
12
|
+
* places, which is exactly why it belongs in the repo. Credentials never live here (they
|
|
13
|
+
* are per machine in ~/.unoverse, see session.mjs, or a CI secret in UNOVERSE_TOKEN).
|
|
13
14
|
*/
|
|
14
15
|
import { existsSync, renameSync, readFileSync, writeFileSync } from "node:fs";
|
|
15
16
|
import { join, resolve } from "node:path";
|
|
@@ -33,7 +34,8 @@ export function designHome(root) {
|
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
/**
|
|
36
|
-
* Walk up from `from` for the nearest marker.
|
|
37
|
+
* Walk up from `from` for the nearest marker. An org repo (`unoverse.yaml` with `org:`) wins
|
|
38
|
+
* at its own level, then a design home, then a universe. The NEAREST one decides what `publish`
|
|
37
39
|
* means (LOCAL_STUDIO.md §Publish is terminal-only: one verb, the folder decides):
|
|
38
40
|
* a design home → this is an asset workspace, publish means item rows; a
|
|
39
41
|
* docker-compose.yml first → this is a universe, publish stays the operator's.
|
|
@@ -43,6 +45,8 @@ export function designHome(root) {
|
|
|
43
45
|
*/
|
|
44
46
|
export function findWorkspace(from = process.cwd()) {
|
|
45
47
|
for (let dir = resolve(from); ; ) {
|
|
48
|
+
const org = readOrgName(dir);
|
|
49
|
+
if (org) return { kind: "org", root: dir, org };
|
|
46
50
|
if (hasDesignHome(dir)) return { kind: "assets", root: dir };
|
|
47
51
|
if (existsSync(join(dir, "docker-compose.yml"))) return { kind: "universe", root: dir };
|
|
48
52
|
const parent = resolve(dir, "..");
|
|
@@ -53,6 +57,18 @@ export function findWorkspace(from = process.cwd()) {
|
|
|
53
57
|
|
|
54
58
|
const CONFIG_FILE = "unoverse.yaml";
|
|
55
59
|
|
|
60
|
+
/**
|
|
61
|
+
* AN ORG REPO (UNO-88): its `unoverse.yaml` declares `org: <slug>`, and the org's folders
|
|
62
|
+
* sit at the root with no design/. The name is declared, never read off the folder. Mirrors
|
|
63
|
+
* base items/collect.ts readOrgName; sync, because the marker walk above is.
|
|
64
|
+
*/
|
|
65
|
+
export function readOrgName(root) {
|
|
66
|
+
const path = join(root, CONFIG_FILE);
|
|
67
|
+
if (!existsSync(path)) return null;
|
|
68
|
+
const m = /^org:\s*["']?([a-z][a-z0-9-]{0,63})["']?\s*(?:#.*)?$/m.exec(readFileSync(path, "utf8"));
|
|
69
|
+
return m ? m[1] : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
56
72
|
/** Read unoverse.yaml. Absent file is `{}`, not an error — first publish writes it. */
|
|
57
73
|
export async function readConfig(root) {
|
|
58
74
|
const path = join(root, CONFIG_FILE);
|
package/package.json
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* performs correctly, and a half-published project is worse than either extreme.
|
|
19
19
|
*/
|
|
20
20
|
import { readdirSync, readFileSync, existsSync, statSync } from "node:fs";
|
|
21
|
-
import { join, relative, basename } from "node:path";
|
|
21
|
+
import { join, relative, basename, dirname, extname } from "node:path";
|
|
22
22
|
import { fingerprintOf } from "./fingerprint.js";
|
|
23
23
|
import { parse as parseYaml } from "yaml";
|
|
24
24
|
import { readDiskPackages } from "../manifests/source.js";
|
|
@@ -40,6 +40,34 @@ function designSystemDir(designRoot) {
|
|
|
40
40
|
return bundled;
|
|
41
41
|
return onDisk; // nothing found: the version will hash to an empty set, which is honest
|
|
42
42
|
}
|
|
43
|
+
const ORG_SLUG = /^[a-z][a-z0-9-]{0,63}$/;
|
|
44
|
+
/** The org a folder declares in its `unoverse.yaml`, or null when it is not an org repo. */
|
|
45
|
+
export function readOrgName(dir) {
|
|
46
|
+
const file = join(dir, "unoverse.yaml");
|
|
47
|
+
if (!existsSync(file))
|
|
48
|
+
return null;
|
|
49
|
+
try {
|
|
50
|
+
const doc = parseYaml(readFileSync(file, "utf8"));
|
|
51
|
+
const org = typeof doc?.org === "string" ? doc.org.trim() : "";
|
|
52
|
+
return ORG_SLUG.test(org) ? org : null;
|
|
53
|
+
}
|
|
54
|
+
catch {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
/** This folder as an org repo, or null. */
|
|
59
|
+
export function orgRepo(dir) {
|
|
60
|
+
const org = readOrgName(dir);
|
|
61
|
+
return org ? { org, dir } : null;
|
|
62
|
+
}
|
|
63
|
+
/** An org repo's design system: only ever the bundle installed inside that repo. */
|
|
64
|
+
function orgDesignSystemDir(dir) {
|
|
65
|
+
return join(dir, "plugins", "node_modules", "@unoverse-platform", "marketplace", "definitions");
|
|
66
|
+
}
|
|
67
|
+
/** Collect an org repo into items: the same walk as a monorepo project, rooted at the repo. */
|
|
68
|
+
export function collectOrg(home) {
|
|
69
|
+
return collectFrom(home.dir, home.org, orgDesignSystemDir(home.dir));
|
|
70
|
+
}
|
|
43
71
|
/** Every project a developer could publish, by name. */
|
|
44
72
|
export function listProjects(designRoot) {
|
|
45
73
|
if (!existsSync(designRoot))
|
|
@@ -49,6 +77,46 @@ export function listProjects(designRoot) {
|
|
|
49
77
|
.filter((e) => statSync(join(designRoot, e)).isDirectory())
|
|
50
78
|
.sort();
|
|
51
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* AN ORG'S IMAGES TRAVEL INSIDE ITS DEFINITIONS. On disk the loader turns `src: assets/x.png`
|
|
82
|
+
* into the file's data URI (definitions/load/assets.ts); a universe holds the definition and
|
|
83
|
+
* never the org's folder, so a deploy that sent the bare path shipped a broken image. The same
|
|
84
|
+
* lookup, done here before sending: the file's folder upward to its org (the one holding
|
|
85
|
+
* `styles/`), then the shared design system's. Same types, same 256 KB cap. Self-contained on
|
|
86
|
+
* purpose: this file is vendored into the published CLI with only its own imports.
|
|
87
|
+
*/
|
|
88
|
+
const ASSET_TYPES = {
|
|
89
|
+
".svg": "image/svg+xml", ".png": "image/png", ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".webp": "image/webp", ".gif": "image/gif",
|
|
90
|
+
};
|
|
91
|
+
function assetUri(fromDir, src) {
|
|
92
|
+
const rel = src.slice("assets/".length);
|
|
93
|
+
const type = ASSET_TYPES[extname(rel).toLowerCase()];
|
|
94
|
+
if (!type)
|
|
95
|
+
return null;
|
|
96
|
+
let file = null;
|
|
97
|
+
for (let d = fromDir; dirname(d) !== d; d = dirname(d)) {
|
|
98
|
+
if (existsSync(join(d, "assets", rel))) {
|
|
99
|
+
file = join(d, "assets", rel);
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
if (existsSync(join(d, "styles"))) {
|
|
103
|
+
const shared = join(dirname(d), "marketplace", "assets", rel);
|
|
104
|
+
if (existsSync(shared))
|
|
105
|
+
file = shared;
|
|
106
|
+
break;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return file && statSync(file).size <= 256 * 1024 ? `data:${type};base64,${readFileSync(file).toString("base64")}` : null;
|
|
110
|
+
}
|
|
111
|
+
const readDef = (p) => {
|
|
112
|
+
const text = readFileSync(p, "utf8");
|
|
113
|
+
if (!/\.ya?ml$/.test(p))
|
|
114
|
+
return text;
|
|
115
|
+
return text.replace(/^(\s*(?:-\s+)?[\w-]+:\s*)(["']?)(assets\/[^\s"'#]+)\2([ \t]*(?:#.*)?)$/gm, (whole, key, _q, src, rest) => {
|
|
116
|
+
const uri = assetUri(dirname(p), src);
|
|
117
|
+
return uri ? `${key}"${uri}"${rest}` : whole;
|
|
118
|
+
});
|
|
119
|
+
};
|
|
52
120
|
/** Every definition file under a folder, relative-pathed, as one object. */
|
|
53
121
|
function filesUnder(dir) {
|
|
54
122
|
const out = {};
|
|
@@ -62,7 +130,7 @@ function filesUnder(dir) {
|
|
|
62
130
|
if (e.isDirectory())
|
|
63
131
|
walk(p);
|
|
64
132
|
else if (isDef(e.name) || e.name.endsWith(".md"))
|
|
65
|
-
out[relative(dir, p)] =
|
|
133
|
+
out[relative(dir, p)] = readDef(p);
|
|
66
134
|
}
|
|
67
135
|
};
|
|
68
136
|
walk(dir);
|
|
@@ -80,10 +148,14 @@ export function collectProject(designRoot, project) {
|
|
|
80
148
|
const root = join(designRoot, project);
|
|
81
149
|
if (!existsSync(root))
|
|
82
150
|
throw new Error(`No project "${project}" in ${designRoot}`);
|
|
151
|
+
return collectFrom(root, project, designSystemDir(designRoot));
|
|
152
|
+
}
|
|
153
|
+
/** The walk itself: one org's folder, its name, and the design system it was built on. */
|
|
154
|
+
function collectFrom(root, project, designSystem) {
|
|
83
155
|
// WHAT THIS WAS BUILT ON, stamped on every item. Computed once per collection: it is the
|
|
84
156
|
// same design system for the whole project, and hashing it per item would be wasteful and
|
|
85
157
|
// could disagree with itself mid-run if a file changed.
|
|
86
|
-
const base_version = designSystemVersion(
|
|
158
|
+
const base_version = designSystemVersion(designSystem);
|
|
87
159
|
const items = [];
|
|
88
160
|
// A COMPONENT (and, since 2026-08-24, a SKILL) row's name is the qualified ref
|
|
89
161
|
// (`<org>/<name>`): the items table's identity is (kind, name) with no org in the
|
|
@@ -146,7 +218,7 @@ export function collectProject(designRoot, project) {
|
|
|
146
218
|
else if (isDef(e.name)) {
|
|
147
219
|
// The name is the definition's, without the extension: `card.yaml` is `card`,
|
|
148
220
|
// addressed the same way whether it later grows into a folder.
|
|
149
|
-
add(kind, e.name.replace(/\.(ya?ml|json)$/, ""), { files: { [e.name]:
|
|
221
|
+
add(kind, e.name.replace(/\.(ya?ml|json)$/, ""), { files: { [e.name]: readDef(join(home, e.name)) } });
|
|
150
222
|
}
|
|
151
223
|
}
|
|
152
224
|
}
|
|
@@ -75,6 +75,25 @@ export async function lintForPublish(designRoot, project) {
|
|
|
75
75
|
const mine = result.problems.filter((p) => !p.file || !inDesign(p.file) || inProject(p.file));
|
|
76
76
|
return { problems: mine, errors: mine.filter((p) => p.level === "error") };
|
|
77
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Lint an org repo (UNO-88): the same four linters, told which folder is the org and what it
|
|
80
|
+
* is called. The repo holds one org, so every finding is that org's; nothing to filter.
|
|
81
|
+
*/
|
|
82
|
+
export async function lintOrg(home) {
|
|
83
|
+
const org = { name: home.org, dir: home.dir };
|
|
84
|
+
const { lintDefinitions } = await import("../lint/design/index.mjs");
|
|
85
|
+
const result = lintDefinitions(home.dir, { org });
|
|
86
|
+
const nodesHome = join(home.dir, "nodes");
|
|
87
|
+
if (existsSync(nodesHome)) {
|
|
88
|
+
const { lintNodes } = await import("../lint/nodes/index.mjs");
|
|
89
|
+
result.problems.push(...lintNodes(nodesHome).problems);
|
|
90
|
+
}
|
|
91
|
+
const { lintObjects } = await import("../lint/objects/index.mjs");
|
|
92
|
+
result.problems.push(...lintObjects(home.dir, { org }).problems);
|
|
93
|
+
const { lintPrompts } = await import("../lint/prompts/index.mjs");
|
|
94
|
+
result.problems.push(...lintPrompts(home.dir, { org }).problems);
|
|
95
|
+
return { problems: result.problems, errors: result.problems.filter((p) => p.level === "error") };
|
|
96
|
+
}
|
|
78
97
|
/**
|
|
79
98
|
* What publishing WOULD do, without doing it.
|
|
80
99
|
*
|
|
@@ -196,7 +215,8 @@ export async function sendPublish(plan, universe, token, fetchImpl = fetch) {
|
|
|
196
215
|
const res = await fetchImpl(`${universe}/publish`, {
|
|
197
216
|
method: "POST",
|
|
198
217
|
headers: { "content-type": "application/json", authorization: `Bearer ${token}` },
|
|
199
|
-
|
|
218
|
+
// The org is part of an item's identity (UNO-89): remove THIS org's row, never another's.
|
|
219
|
+
body: JSON.stringify({ op: "remove", kind: r.kind, name: r.name, org: r.org }),
|
|
200
220
|
});
|
|
201
221
|
const body = (await res.json().catch(() => ({})));
|
|
202
222
|
if (res.ok)
|
|
@@ -75,7 +75,17 @@ export function lintDefinitions(designRoot, options = {}) {
|
|
|
75
75
|
});
|
|
76
76
|
};
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
/**
|
|
79
|
+
* AN ORG'S OWN REPO (RELEASE_LIFECYCLE.md §3, UNO-88). The repo IS the org: its folders sit
|
|
80
|
+
* at the root and its name is declared (`unoverse.yaml` org:), never read off a folder.
|
|
81
|
+
* The caller says so with `options.org = { name, dir }`, and then nothing here guesses:
|
|
82
|
+
* the org list is that one folder, and its name is the one given. Without it a flat repo
|
|
83
|
+
* would read as the legacy design-system layout (components/ at the root) and lint as
|
|
84
|
+
* nothing at all.
|
|
85
|
+
*/
|
|
86
|
+
const ORG_REPO = options.org?.dir ? resolve(options.org.dir) : null;
|
|
87
|
+
const ORG_NAME = options.org?.name ?? null;
|
|
88
|
+
const DESIGN_ROOT = ORG_REPO ? dirname(ORG_REPO) : candidates.find((p) => holdsDesignSystem(p) || holdsOrgs(p));
|
|
79
89
|
if (!DESIGN_ROOT)
|
|
80
90
|
return {
|
|
81
91
|
problems: [{ level: "error", file: candidates[0], msg: `no design/ folder here (looked in: ${candidates.join(", ")})` }],
|
|
@@ -94,6 +104,12 @@ export function lintDefinitions(designRoot, options = {}) {
|
|
|
94
104
|
// styles/base/spacing, so a fallback finding only components would build a PARTIAL scale
|
|
95
105
|
// and reject valid steps, reporting false errors on correct work. Mirrors definitions.ts.
|
|
96
106
|
const DS = (() => {
|
|
107
|
+
// In an org repo the design system is only ever the installed bundle inside that repo:
|
|
108
|
+
// never a sibling folder, which would be another org's repo.
|
|
109
|
+
if (ORG_REPO) {
|
|
110
|
+
const bundled = join(ORG_REPO, "plugins", "node_modules", "@unoverse-platform", "marketplace", "definitions");
|
|
111
|
+
return existsSync(bundled) ? bundled : join(ORG_REPO, ".unoverse", "no-design-system");
|
|
112
|
+
}
|
|
97
113
|
const onDisk = join(DESIGN_ROOT, "marketplace");
|
|
98
114
|
if (existsSync(onDisk)) return onDisk;
|
|
99
115
|
for (const c of [
|
|
@@ -102,8 +118,15 @@ export function lintDefinitions(designRoot, options = {}) {
|
|
|
102
118
|
if (existsSync(c)) return c;
|
|
103
119
|
return DESIGN_ROOT; // none anywhere: shared refs will not resolve, and the findings will say so
|
|
104
120
|
})();
|
|
121
|
+
/** Is a design system on disk at all? When not, checks that need it are skipped, not failed. */
|
|
122
|
+
const HAS_DS = DS !== DESIGN_ROOT && existsSync(DS);
|
|
123
|
+
/** The org a home folder belongs to: the declared name in an org repo, else the folder's name. */
|
|
124
|
+
const orgName = (dir) => (ORG_REPO && dir === ORG_REPO ? ORG_NAME : basename(dir));
|
|
125
|
+
/** Every home that can hold node packages, credentials or places: the design system and the orgs. */
|
|
126
|
+
const tierHomes = () => (ORG_REPO ? [DS, ...orgDirs] : readdirSync(DESIGN_ROOT).map((t) => join(DESIGN_ROOT, t)));
|
|
105
127
|
const legacyOrgsDir = join(DESIGN_ROOT, "orgs");
|
|
106
128
|
const orgDirs = (() => {
|
|
129
|
+
if (ORG_REPO) return [ORG_REPO];
|
|
107
130
|
if (existsSync(legacyOrgsDir))
|
|
108
131
|
return readdirSync(legacyOrgsDir)
|
|
109
132
|
.filter((e) => !e.startsWith("."))
|
|
@@ -448,7 +471,7 @@ for (const orgDir of orgDirs) {
|
|
|
448
471
|
const name = statSync(join(home, e)).isDirectory() ? e : isDefFile(e) ? defName(e) : null;
|
|
449
472
|
if (name) set.add(name.toLowerCase());
|
|
450
473
|
}
|
|
451
|
-
orgComponentNames.set(
|
|
474
|
+
orgComponentNames.set(orgName(orgDir), set);
|
|
452
475
|
}
|
|
453
476
|
// TEMPLATE-LOCAL PARTS are addressed `<template>/<part>` (§9b rule 7) — the same
|
|
454
477
|
// qualified shape as `<org>/<name>`, resolved against the template homes. The runtime
|
|
@@ -461,7 +484,7 @@ const orgTemplateNames = new Map(); // org (or DS basename) -> Set(lower block n
|
|
|
461
484
|
for (const root of [DS, ...orgDirs]) {
|
|
462
485
|
const home = join(root, "templates");
|
|
463
486
|
if (!existsSync(home)) continue;
|
|
464
|
-
const blocks = orgTemplateNames.get(
|
|
487
|
+
const blocks = orgTemplateNames.get(orgName(root)) ?? new Set();
|
|
465
488
|
for (const t of readdirSync(home)) {
|
|
466
489
|
if (isDefFile(t)) {
|
|
467
490
|
blocks.add(defName(t).toLowerCase());
|
|
@@ -479,11 +502,11 @@ for (const root of [DS, ...orgDirs]) {
|
|
|
479
502
|
}
|
|
480
503
|
templatePartNames.set(t.toLowerCase(), set);
|
|
481
504
|
}
|
|
482
|
-
orgTemplateNames.set(
|
|
505
|
+
orgTemplateNames.set(orgName(root), blocks);
|
|
483
506
|
}
|
|
484
507
|
const orgOfFile = (file) => {
|
|
485
508
|
const home = orgDirs.find((d) => file.startsWith(d + sep));
|
|
486
|
-
return home ?
|
|
509
|
+
return home ? orgName(home) : null;
|
|
487
510
|
};
|
|
488
511
|
// A valid Ref target if there's ANY resolvable home the FILE may reach: atoms, DS
|
|
489
512
|
// components, or (from inside an org tree) that org's own components — bare or
|
|
@@ -747,7 +770,7 @@ const credentialDefs = new Set();
|
|
|
747
770
|
// Every tier's node packages: the marketplace's and each org's, all at <tier>/nodes/.
|
|
748
771
|
const nodesHomes = [];
|
|
749
772
|
try {
|
|
750
|
-
for (const
|
|
773
|
+
for (const home of tierHomes()) nodesHomes.push(join(home, "nodes"));
|
|
751
774
|
} catch {
|
|
752
775
|
/* no design root: nothing declares a credential */
|
|
753
776
|
}
|
|
@@ -924,6 +947,15 @@ function componentNamesForFile(file) {
|
|
|
924
947
|
const org = projectSegs.join("/");
|
|
925
948
|
const cached = componentNamesCache.get(org);
|
|
926
949
|
if (cached !== undefined) return cached;
|
|
950
|
+
// A PARTIAL SET IS WORSE THAN NO SET (the app-sizes trap below, same cause). A developer's
|
|
951
|
+
// project has no design system on disk, so DS falls back to DESIGN_ROOT and only the org's
|
|
952
|
+
// own names would be left: every marketplace name a preview uses (`card`) then reads as
|
|
953
|
+
// unknown, and a fresh Studio workspace failed lint on its own sample. Unverifiable here,
|
|
954
|
+
// so not asserted.
|
|
955
|
+
if (!HAS_DS) {
|
|
956
|
+
componentNamesCache.set(org, null);
|
|
957
|
+
return null;
|
|
958
|
+
}
|
|
927
959
|
const dirs = [
|
|
928
960
|
join(DS, "components"),
|
|
929
961
|
join(DESIGN_ROOT, ...projectSegs, "components"),
|
|
@@ -955,7 +987,7 @@ function placeNames() {
|
|
|
955
987
|
if (n.type === "Place" && typeof n.name === "string") names.add(n.name);
|
|
956
988
|
for (const v of Object.values(n)) walk(v);
|
|
957
989
|
};
|
|
958
|
-
const roots = [DS, ...
|
|
990
|
+
const roots = [DS, ...tierHomes().filter((d) => !basename(d).startsWith("."))];
|
|
959
991
|
for (const root of new Set(roots))
|
|
960
992
|
for (const f of jsonFiles(join(root, "apps"))) {
|
|
961
993
|
try {
|
|
@@ -117,13 +117,19 @@ export function lintObjects(designRoot, options = {}) {
|
|
|
117
117
|
loadAllSchemas();
|
|
118
118
|
|
|
119
119
|
// ── the tiers: the marketplace, then every org folder ──
|
|
120
|
+
// An org's own repo is ONE tier with a DECLARED name (`options.org`, UNO-88): its folder may
|
|
121
|
+
// be called anything, and the org name is also the dataset table's (`dataset_<org>_<name>`).
|
|
120
122
|
const marketplace = join(ROOT, "marketplace");
|
|
121
123
|
const tiers = [];
|
|
122
|
-
if (
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
if (options.org?.dir) {
|
|
125
|
+
tiers.push({ org: options.org.name, dir: resolve(options.org.dir) });
|
|
126
|
+
} else {
|
|
127
|
+
if (isDir(marketplace)) tiers.push({ org: undefined, dir: marketplace });
|
|
128
|
+
for (const e of readdirSync(ROOT)) {
|
|
129
|
+
if (e.startsWith(".") || e === "marketplace" || e === "_schema") continue;
|
|
130
|
+
const d = join(ROOT, e);
|
|
131
|
+
if (isDir(d)) tiers.push({ org: e, dir: d });
|
|
132
|
+
}
|
|
127
133
|
}
|
|
128
134
|
|
|
129
135
|
/**
|
|
@@ -233,7 +239,7 @@ export function lintObjects(designRoot, options = {}) {
|
|
|
233
239
|
let objectCount = 0;
|
|
234
240
|
for (const [, f] of objectIndex) {
|
|
235
241
|
objectCount++;
|
|
236
|
-
const tier = { org: f.org, dir:
|
|
242
|
+
const tier = { org: f.org, dir: tiers.find((t) => t.org === f.org)?.dir ?? marketplace };
|
|
237
243
|
const at = (file) => rel(join(f.dir, file));
|
|
238
244
|
if (f.envelope === null) continue;
|
|
239
245
|
validateAgainst(SCHEMA_ID.object, f.envelope, rel(f.envFile), `object ${f.name}: the envelope`);
|
|
@@ -143,10 +143,15 @@ export function lintPrompts(designRoot, options = {}) {
|
|
|
143
143
|
// ── the tiers: the marketplace, every flat org folder, and the legacy orgs/ container ──
|
|
144
144
|
const tiers = [];
|
|
145
145
|
const marketplace = join(ROOT, "marketplace");
|
|
146
|
-
if (isDir(marketplace)) tiers.push({ org: undefined, dir: marketplace });
|
|
147
146
|
const orgDirs = (base) => readdirSync(base).filter((e) => !e.startsWith(".") && !e.startsWith("_") && e !== "marketplace" && e !== "orgs" && isDir(join(base, e)));
|
|
148
|
-
|
|
149
|
-
|
|
147
|
+
if (options.org?.dir) {
|
|
148
|
+
// An org's own repo: one tier, its name declared, never read off the folder (UNO-88).
|
|
149
|
+
tiers.push({ org: options.org.name, dir: resolve(options.org.dir) });
|
|
150
|
+
} else {
|
|
151
|
+
if (isDir(marketplace)) tiers.push({ org: undefined, dir: marketplace });
|
|
152
|
+
for (const e of orgDirs(ROOT)) tiers.push({ org: e, dir: join(ROOT, e) });
|
|
153
|
+
if (isDir(join(ROOT, "orgs"))) for (const e of orgDirs(join(ROOT, "orgs"))) tiers.push({ org: e, dir: join(ROOT, "orgs", e) });
|
|
154
|
+
}
|
|
150
155
|
|
|
151
156
|
let skillCount = 0;
|
|
152
157
|
let blockCount = 0;
|