unoverse 0.1.194 → 0.1.196
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 +2 -2
- package/bin/unoverse.mjs +30 -4
- package/lib/lint.mjs +68 -0
- package/lib/publish.mjs +8 -13
- package/lib/skills.mjs +58 -35
- package/operator/ansible/playbooks/deploy-images.yml +6 -9
- package/operator/docker-compose.yml +4 -13
- package/operator/lib/check.sh +7 -16
- package/operator/lib/db-verify.sh +7 -1
- package/operator/operator.sh +4 -6
- package/package.json +2 -1
- package/vendor/base/items/collect.js +45 -0
- package/vendor/base/items/publish.js +14 -0
- package/vendor/base/lint/context.mjs +56 -0
- package/vendor/base/lint/nodes/_schema/_defs.schema.json +102 -0
- package/vendor/base/lint/nodes/_schema/api.schema.json +971 -0
- package/vendor/base/lint/nodes/_schema/audio.schema.json +94 -0
- package/vendor/base/lint/nodes/_schema/config.schema.json +174 -0
- package/vendor/base/lint/nodes/_schema/credential.schema.json +89 -0
- package/vendor/base/lint/nodes/_schema/interface.schema.json +100 -0
- package/vendor/base/lint/nodes/_schema/node.schema.json +157 -0
- package/vendor/base/lint/nodes/_schema/package.schema.json +171 -0
- package/vendor/base/lint/nodes/_schema/test.schema.json +79 -0
- package/vendor/base/lint/nodes/blocks.mjs +63 -0
- package/vendor/base/lint/nodes/index.mjs +82 -0
- package/vendor/base/lint/nodes/load.mjs +126 -0
- package/vendor/base/lint/nodes/node.mjs +764 -0
- package/vendor/base/lint/nodes/package.mjs +112 -0
- package/vendor/base/lint/nodes/schema.mjs +75 -0
- package/vendor/base/manifests/source.js +127 -0
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Creating a universe registers the `canvas` MCP (`.mcp.json`): open the folder in
|
|
|
31
31
|
Claude Code and describe the workflow you want, built live on your Canvas. The
|
|
32
32
|
authoring skills for components, apps, themes, and custom nodes come with Studio
|
|
33
33
|
projects (`unoverse studio`). `unoverse update` keeps the CLI, a universe's images,
|
|
34
|
-
and
|
|
35
|
-
|
|
34
|
+
and the installed skills current. The skills are pages of the documentation, under
|
|
35
|
+
https://docs.unoverse.ai/skills, and the CLI installs them from there.
|
|
36
36
|
|
|
37
37
|
Documentation: https://docs.unoverse.ai
|
package/bin/unoverse.mjs
CHANGED
|
@@ -96,6 +96,7 @@ const HELP = [
|
|
|
96
96
|
"",
|
|
97
97
|
` ${bold("After that")}`,
|
|
98
98
|
row("studio", "Design components, nodes and agent skills"),
|
|
99
|
+
row("lint", "Check your components, nodes and skills, the same way deploy does"),
|
|
99
100
|
row("deploy", "Ship it"),
|
|
100
101
|
` ${dim("deploy studio your components, nodes and skills → your universe")}`,
|
|
101
102
|
` ${dim("deploy aws your universe → AWS")}`,
|
|
@@ -106,9 +107,14 @@ const HELP = [
|
|
|
106
107
|
].join("\n");
|
|
107
108
|
|
|
108
109
|
switch (cmd) {
|
|
109
|
-
case "create":
|
|
110
|
+
case "create": {
|
|
110
111
|
await create(args[0]);
|
|
112
|
+
// FIRST CONTACT INSTALLS THE SKILLS. They used to arrive only on `unoverse update`,
|
|
113
|
+
// so a developer's first `claude` in a fresh workspace knew nothing about unoverse.
|
|
114
|
+
const { installSkills } = await import("../lib/skills.mjs");
|
|
115
|
+
await installSkills();
|
|
111
116
|
break;
|
|
117
|
+
}
|
|
112
118
|
|
|
113
119
|
case "update": {
|
|
114
120
|
// ONE update: the CLI from npm, then everything the CLI is responsible for keeping
|
|
@@ -151,8 +157,8 @@ switch (cmd) {
|
|
|
151
157
|
|
|
152
158
|
case "_postupdate": {
|
|
153
159
|
// Runs AS the new version, in the developer's cwd. A universe refreshes its
|
|
154
|
-
// platform images; the authoring skills install to ~/.claude/skills from the
|
|
155
|
-
//
|
|
160
|
+
// platform images; the authoring skills install to ~/.claude/skills from the docs
|
|
161
|
+
// site, so `unoverse update` is the one command that brings everything current.
|
|
156
162
|
//
|
|
157
163
|
// THE SKILLS ARE NOT CWD-SCOPED. This used to refresh `<cwd>/.claude/skills` and only
|
|
158
164
|
// when that folder already existed, which meant it fired on whichever folder update
|
|
@@ -163,7 +169,7 @@ switch (cmd) {
|
|
|
163
169
|
operator(UNIVERSE, ["refresh-images"]);
|
|
164
170
|
}
|
|
165
171
|
const { installSkills } = await import("../lib/skills.mjs");
|
|
166
|
-
installSkills();
|
|
172
|
+
await installSkills();
|
|
167
173
|
process.exit(0);
|
|
168
174
|
}
|
|
169
175
|
|
|
@@ -191,6 +197,17 @@ switch (cmd) {
|
|
|
191
197
|
// quietly keeps talking to last week's build. If the listener IS a studio, it is
|
|
192
198
|
// ours to replace: kill it and wait for the port. Anything else on the port is
|
|
193
199
|
// NOT ours: name it and stop, never kill a stranger's process.
|
|
200
|
+
// A machine that has never installed the skills gets them here too, since studio is
|
|
201
|
+
// the other first-contact command. Present means theirs to keep; update refreshes.
|
|
202
|
+
{
|
|
203
|
+
const { existsSync: has } = await import("node:fs");
|
|
204
|
+
const { join: j } = await import("node:path");
|
|
205
|
+
const { homedir } = await import("node:os");
|
|
206
|
+
if (!has(j(homedir(), ".claude", "skills", "unoverse-create"))) {
|
|
207
|
+
const { installSkills } = await import("../lib/skills.mjs");
|
|
208
|
+
await installSkills();
|
|
209
|
+
}
|
|
210
|
+
}
|
|
194
211
|
const STUDIO_PORT = 4108;
|
|
195
212
|
const lsof = spawnSync("lsof", ["-nP", `-iTCP:${STUDIO_PORT}`, "-sTCP:LISTEN", "-Fpc"], { encoding: "utf8" });
|
|
196
213
|
const holder = lsof.status === 0 ? (lsof.stdout.match(/^p(\d+)$/m) || [])[1] : undefined;
|
|
@@ -226,6 +243,15 @@ switch (cmd) {
|
|
|
226
243
|
process.exit(r.status ?? 0);
|
|
227
244
|
}
|
|
228
245
|
|
|
246
|
+
case "lint": {
|
|
247
|
+
// THE DEPLOY GATE, on its own. `deploy studio` runs exactly this before it sends, so a
|
|
248
|
+
// developer can ask the question without the answer costing them a deploy.
|
|
249
|
+
const { lint } = await import("../lib/lint.mjs");
|
|
250
|
+
const { importBase } = await import("../lib/publish.mjs");
|
|
251
|
+
await lint(args, importBase);
|
|
252
|
+
break;
|
|
253
|
+
}
|
|
254
|
+
|
|
229
255
|
case "login": {
|
|
230
256
|
// Standalone sign-in. publish calls the same flow lazily, so this exists for
|
|
231
257
|
// "set up my machine" moments and for re-authenticating after a permissions change.
|
package/lib/lint.mjs
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* unoverse lint — check a workspace before you deploy it.
|
|
3
|
+
*
|
|
4
|
+
* THE SAME GATE `deploy studio` RUNS, and nothing else: one function in base
|
|
5
|
+
* (items/publish lintForPublish) lints the project's design/ and the workspace's nodes/,
|
|
6
|
+
* and this file only prints what it found. A developer who runs this and sees a tick will
|
|
7
|
+
* deploy without a surprise, because deploy asks the same function the same question.
|
|
8
|
+
*
|
|
9
|
+
* It exists because the gate used to be reachable only by deploying (2026-09-05): the
|
|
10
|
+
* first place a developer met a lint error was halfway through shipping.
|
|
11
|
+
*/
|
|
12
|
+
import { findWorkspace, designHome } from "./workspace.mjs";
|
|
13
|
+
|
|
14
|
+
const c = { dim: "\x1b[2m", red: "\x1b[31m", green: "\x1b[32m", yellow: "\x1b[33m", bold: "\x1b[1m", off: "\x1b[0m" };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Lint one project and print the findings. Returns the findings so `deploy studio` can
|
|
18
|
+
* decide what to do with them; exits nobody.
|
|
19
|
+
*/
|
|
20
|
+
export async function lintProject({ lintForPublish, designRoot, project }) {
|
|
21
|
+
process.stdout.write(` ${c.dim}checking…${c.off}`);
|
|
22
|
+
const { problems, errors } = await lintForPublish(designRoot, project);
|
|
23
|
+
process.stdout.write("\r \r");
|
|
24
|
+
if (errors.length) {
|
|
25
|
+
console.error(` ${c.red}✗ ${errors.length} error(s) in ${project}.${c.off}\n`);
|
|
26
|
+
for (const p of errors.slice(0, 10)) console.error(` ${p.file}${p.line ? ":" + p.line : ""} ${p.msg}`);
|
|
27
|
+
if (errors.length > 10) console.error(` ${c.dim}…and ${errors.length - 10} more${c.off}`);
|
|
28
|
+
console.error("");
|
|
29
|
+
} else {
|
|
30
|
+
const warnings = problems.filter((p) => p.level === "warn").length;
|
|
31
|
+
console.log(` ${c.green}✓${c.off} checks passed${warnings ? ` ${c.dim}(${warnings} warning(s))${c.off}` : ""}`);
|
|
32
|
+
}
|
|
33
|
+
return { problems, errors };
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** `unoverse lint [project]`, from anywhere inside a workspace. Exit 1 on any error. */
|
|
37
|
+
export async function lint(args, importBase) {
|
|
38
|
+
const positional = args.find((a) => !a.startsWith("--"));
|
|
39
|
+
const ws = findWorkspace();
|
|
40
|
+
if (ws?.kind !== "assets") {
|
|
41
|
+
console.error(`\n ${c.red}✗${c.off} no design/ folder here or above. Run this inside your workspace.\n`);
|
|
42
|
+
process.exit(1);
|
|
43
|
+
}
|
|
44
|
+
const designRoot = designHome(ws.root);
|
|
45
|
+
const { listProjects } = await importBase("items/collect.js");
|
|
46
|
+
const { lintForPublish } = await importBase("items/publish.js");
|
|
47
|
+
|
|
48
|
+
const projects = listProjects(designRoot);
|
|
49
|
+
if (!projects.length) {
|
|
50
|
+
console.error(`\n ${c.red}✗${c.off} ${designRoot} holds no projects\n`);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
let project = positional;
|
|
54
|
+
if (!project) {
|
|
55
|
+
if (projects.length > 1) {
|
|
56
|
+
console.error(`\n several projects here. Say which:\n\n unoverse lint <project>\n\n Found: ${projects.join(", ")}\n`);
|
|
57
|
+
process.exit(1);
|
|
58
|
+
}
|
|
59
|
+
project = projects[0];
|
|
60
|
+
} else if (!projects.includes(project)) {
|
|
61
|
+
console.error(`\n ${c.red}✗${c.off} no project "${project}" in ${designRoot}. Found: ${projects.join(", ")}\n`);
|
|
62
|
+
process.exit(1);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
console.log(`\n ${c.bold}lint ${project}${c.off}\n`);
|
|
66
|
+
const { errors } = await lintProject({ lintForPublish, designRoot, project });
|
|
67
|
+
process.exit(errors.length ? 1 : 0);
|
|
68
|
+
}
|
package/lib/publish.mjs
CHANGED
|
@@ -22,7 +22,7 @@ const here = dirname(fileURLToPath(import.meta.url));
|
|
|
22
22
|
const IN_REPO_BASE = resolve(here, "../../base/dist");
|
|
23
23
|
const VENDORED_BASE = resolve(here, "../vendor/base");
|
|
24
24
|
|
|
25
|
-
async function importBase(subpath) {
|
|
25
|
+
export async function importBase(subpath) {
|
|
26
26
|
const root = existsSync(IN_REPO_BASE) ? IN_REPO_BASE : VENDORED_BASE;
|
|
27
27
|
const file = join(root, subpath);
|
|
28
28
|
if (!existsSync(file)) {
|
|
@@ -114,18 +114,12 @@ export async function publish(args) {
|
|
|
114
114
|
|
|
115
115
|
// ── 1. lint. Nothing is sent if this fails ──────────────────────────────────
|
|
116
116
|
console.log(`\n ${c.bold}deploy ${project}${c.off} ${c.dim}→ ${universe}${c.off}\n`);
|
|
117
|
-
|
|
118
|
-
const {
|
|
119
|
-
process.stdout.write("\r \r");
|
|
117
|
+
const { lintProject } = await import("./lint.mjs");
|
|
118
|
+
const { errors } = await lintProject({ lintForPublish, designRoot, project });
|
|
120
119
|
if (errors.length) {
|
|
121
|
-
console.error(` ${c.red}
|
|
122
|
-
for (const p of errors.slice(0, 10)) console.error(` ${p.file}${p.line ? ":" + p.line : ""} ${p.msg}`);
|
|
123
|
-
if (errors.length > 10) console.error(` ${c.dim}…and ${errors.length - 10} more${c.off}`);
|
|
124
|
-
console.error("");
|
|
120
|
+
console.error(` ${c.red}Nothing was sent.${c.off}\n`);
|
|
125
121
|
process.exit(1);
|
|
126
122
|
}
|
|
127
|
-
const warnings = problems.filter((p) => p.level === "warn").length;
|
|
128
|
-
console.log(` ${c.green}✓${c.off} checks passed${warnings ? ` ${c.dim}(${warnings} warning(s))${c.off}` : ""}`);
|
|
129
123
|
|
|
130
124
|
// ── 2. the credential, only now that there is something worth sending ───────
|
|
131
125
|
const items = collectProject(designRoot, project);
|
|
@@ -147,9 +141,10 @@ export async function publish(args) {
|
|
|
147
141
|
|
|
148
142
|
console.log("");
|
|
149
143
|
const label = (i) => `${i.kind}/${i.name}`;
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
for (const i of plan.
|
|
144
|
+
// No "(lands PENDING review)" on a node (removed 2026-09-05): the review flow in
|
|
145
|
+
// DECLARATIVE_NODES.md §9.5 is not built, and the route registers a node row at once.
|
|
146
|
+
for (const i of plan.create) console.log(` ${c.green}+${c.off} ${label(i)} ${c.dim}(new)${c.off}`);
|
|
147
|
+
for (const i of plan.update) console.log(` ${c.yellow}~${c.off} ${label(i)} ${c.dim}(changed)${c.off}`);
|
|
153
148
|
if (plan.unchanged.length) console.log(` ${c.dim}= ${plan.unchanged.length} unchanged${c.off}`);
|
|
154
149
|
for (const r of plan.refused) console.log(` ${c.red}✗ ${label(r)}${c.off} ${c.dim}${r.why}${c.off}`);
|
|
155
150
|
// Deploy is a SYNC: what left the workspace leaves the universe, said before it happens.
|
package/lib/skills.mjs
CHANGED
|
@@ -1,57 +1,80 @@
|
|
|
1
|
-
// Install the Claude Code authoring skills for this developer.
|
|
1
|
+
// Install the Claude Code authoring skills for this developer, FROM THE DOCS SITE.
|
|
2
2
|
//
|
|
3
|
-
// The
|
|
4
|
-
//
|
|
3
|
+
// The skills are pages of the documentation (packages/docs/skills/<skill>/...), published
|
|
4
|
+
// with every other page and listed in the site's llms.txt (owner ruling 2026-09-05: the
|
|
5
|
+
// docs site is the one public origin, and it already serves every page as raw markdown).
|
|
6
|
+
// The installer reads llms.txt, takes every page under /skills/, and writes each one to
|
|
7
|
+
// disk. There is no GitHub mirror and no tarball any more.
|
|
5
8
|
//
|
|
6
9
|
// PER DEVELOPER, NOT PER PROJECT (2026-08-21). These install to ~/.claude/skills, so
|
|
7
10
|
// Claude Code picks them up in every folder the developer opens and nothing lands
|
|
8
|
-
// inside a workspace where it could be committed.
|
|
9
|
-
// Studio workspace's own .claude/skills by Studio's scaffold, out of a snapshot
|
|
10
|
-
// vendored in the Studio npm package: that reproduced the platform repo's .claude
|
|
11
|
-
// folder on every remote developer's disk, pinned to whichever Studio version they
|
|
12
|
-
// installed. Studio no longer writes them (packages/studio/local/scaffold.mjs) and
|
|
13
|
-
// the mirror is the only channel.
|
|
11
|
+
// inside a workspace where it could be committed.
|
|
14
12
|
//
|
|
15
|
-
//
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
// developer's tooling current.
|
|
21
|
-
import { mkdtempSync, existsSync, mkdirSync, readdirSync, rmSync, cpSync } from "node:fs";
|
|
22
|
-
import { join } from "node:path";
|
|
13
|
+
// Runs on `unoverse create` and on a first `unoverse studio` (first contact), and on
|
|
14
|
+
// `unoverse update` (_postupdate), which is the one command that brings a developer's
|
|
15
|
+
// tooling current.
|
|
16
|
+
import { existsSync, mkdirSync, rmSync, writeFileSync, mkdtempSync, cpSync } from "node:fs";
|
|
17
|
+
import { join, dirname, basename } from "node:path";
|
|
23
18
|
import { tmpdir, homedir } from "node:os";
|
|
24
|
-
import { spawnSync } from "node:child_process";
|
|
25
19
|
|
|
26
|
-
const
|
|
20
|
+
const DOCS = "https://docs.unoverse.ai";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* The skill files a site's llms.txt names: `{ skill, rel, url }` per page under /skills/,
|
|
24
|
+
* `rel` being the path inside the skill folder. A skill's front page is SKILL.md on disk
|
|
25
|
+
* whatever case the site serves it in, because Claude Code looks for exactly that name.
|
|
26
|
+
* Pure, so it can be checked without a network.
|
|
27
|
+
*/
|
|
28
|
+
export function skillFilesFromLlms(text, origin = DOCS) {
|
|
29
|
+
const out = [];
|
|
30
|
+
const re = new RegExp(`\\((${origin.replace(/[.*+?^${}()|[\\]\\\\]/g, "\\$&")}/skills/([^/\\s)]+)/([^\\s)]+\\.md))\\)`, "g");
|
|
31
|
+
for (const m of text.matchAll(re)) {
|
|
32
|
+
const [, url, skill, path] = m;
|
|
33
|
+
const rel = basename(path).toLowerCase() === "skill.md" ? join(dirname(path), "SKILL.md") : path;
|
|
34
|
+
out.push({ skill, rel, url });
|
|
35
|
+
}
|
|
36
|
+
return out;
|
|
37
|
+
}
|
|
27
38
|
|
|
28
|
-
export function installSkills() {
|
|
39
|
+
export async function installSkills() {
|
|
29
40
|
const target = join(homedir(), ".claude", "skills");
|
|
41
|
+
let files;
|
|
42
|
+
try {
|
|
43
|
+
const res = await fetch(`${DOCS}/llms.txt`);
|
|
44
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
45
|
+
files = skillFilesFromLlms(await res.text());
|
|
46
|
+
} catch {
|
|
47
|
+
console.log(` Skills install skipped (${DOCS} unreachable). They install on the next update.`);
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
const skills = [...new Set(files.map((f) => f.skill))];
|
|
51
|
+
if (!skills.length) {
|
|
52
|
+
console.log(` Skills install skipped: ${DOCS}/llms.txt lists no skills yet.`);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
// Fetched whole into a scratch folder first, so a download that dies halfway leaves the
|
|
56
|
+
// installed copy untouched rather than half-replaced.
|
|
30
57
|
const tmp = mkdtempSync(join(tmpdir(), "unoverse-skills-"));
|
|
31
58
|
try {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
59
|
+
for (const f of files) {
|
|
60
|
+
const res = await fetch(f.url);
|
|
61
|
+
if (!res.ok) throw new Error(`${f.url}: HTTP ${res.status}`);
|
|
62
|
+
const p = join(tmp, f.skill, f.rel);
|
|
63
|
+
mkdirSync(dirname(p), { recursive: true });
|
|
64
|
+
writeFileSync(p, await res.text());
|
|
38
65
|
}
|
|
39
|
-
// A skill is a directory holding a SKILL.md. The mirror's README and dotfiles never
|
|
40
|
-
// install.
|
|
41
|
-
const names = readdirSync(tmp, { withFileTypes: true })
|
|
42
|
-
.filter((e) => e.isDirectory() && existsSync(join(tmp, e.name, "SKILL.md")))
|
|
43
|
-
.map((e) => e.name);
|
|
44
|
-
if (!names.length) return;
|
|
45
66
|
mkdirSync(target, { recursive: true });
|
|
46
|
-
// REPLACED WHOLE, ours only. Each skill folder the
|
|
67
|
+
// REPLACED WHOLE, ours only. Each skill folder the site carries is removed and
|
|
47
68
|
// re-copied, so a deleted file upstream really goes. Anything else in the
|
|
48
69
|
// developer's ~/.claude/skills is THEIRS and is never touched: this folder is
|
|
49
70
|
// shared with every skill they have installed from anywhere else.
|
|
50
|
-
for (const name of
|
|
71
|
+
for (const name of skills) {
|
|
51
72
|
rmSync(join(target, name), { recursive: true, force: true });
|
|
52
73
|
cpSync(join(tmp, name), join(target, name), { recursive: true });
|
|
53
74
|
}
|
|
54
|
-
console.log(` ✓ authoring skills installed to ~/.claude/skills (${
|
|
75
|
+
console.log(` ✓ authoring skills installed to ~/.claude/skills (${skills.join(", ")})`);
|
|
76
|
+
} catch (e) {
|
|
77
|
+
console.log(` Skills install skipped (${e.message}). They install on the next update.`);
|
|
55
78
|
} finally {
|
|
56
79
|
rmSync(tmp, { recursive: true, force: true });
|
|
57
80
|
}
|
|
@@ -76,17 +76,14 @@
|
|
|
76
76
|
debug:
|
|
77
77
|
msg: "{{ pull_result.stdout_lines[-8:] | default(['done']) }}"
|
|
78
78
|
|
|
79
|
-
#
|
|
80
|
-
#
|
|
81
|
-
#
|
|
82
|
-
#
|
|
83
|
-
|
|
84
|
-
# Scoped to this one `up`, never written into .env: a later `docker restart` must not
|
|
85
|
-
# inherit it.
|
|
86
|
-
- name: "[3/4] Restart on the new images (and take the newest base in range)"
|
|
79
|
+
# THE NODE RUNTIME CAME WITH THE IMAGE. base is baked in, not installed
|
|
80
|
+
# (MARKETPLACE.md §5a, reversed 2026-09-03), so the pull above already moved it and
|
|
81
|
+
# this step has nothing extra to ask for. It used to run with `UNOVERSE_BASE_REFRESH=1`
|
|
82
|
+
# to mean "take the newest base in range"; there is no range and no install any more.
|
|
83
|
+
- name: "[3/4] Restart on the new images"
|
|
87
84
|
shell: |
|
|
88
85
|
cd {{ gravity_dir }}
|
|
89
|
-
|
|
86
|
+
docker compose up -d 2>&1 | tail -6
|
|
90
87
|
register: up_result
|
|
91
88
|
|
|
92
89
|
# `up -d` RECREATES ON CONFIG CHANGE, NOT ON IMAGE CHANGE — not reliably, and not on
|
|
@@ -275,20 +275,12 @@ services:
|
|
|
275
275
|
# copy carrying a built dist. Without this the runtime hits MODULE_NOT_FOUND and
|
|
276
276
|
# the nodes show "Package not installed".
|
|
277
277
|
- NODE_PATH=/app/host_node_modules:/app/plugins/node_modules
|
|
278
|
-
# PASSTHROUGH, not a setting. `unoverse deploy` runs `UNOVERSE_BASE_REFRESH=1 docker
|
|
279
|
-
# compose up -d` to mean "take the newest node runtime in range"; without this line
|
|
280
|
-
# that variable would reach the compose CLI and stop there, never the container.
|
|
281
|
-
# Defaults to 0, so an ordinary restart stays offline and instant.
|
|
282
|
-
- UNOVERSE_BASE_REFRESH=${UNOVERSE_BASE_REFRESH:-0}
|
|
283
278
|
volumes:
|
|
284
279
|
- unoverse_plugins:/app/plugins
|
|
285
|
-
# THE NODE RUNTIME
|
|
286
|
-
#
|
|
287
|
-
#
|
|
288
|
-
#
|
|
289
|
-
# restart is offline and instant: the fetch happens on a cold volume, or when the
|
|
290
|
-
# declared range changes, or when a deploy sets UNOVERSE_BASE_REFRESH=1.
|
|
291
|
-
- unoverse_base:/app/base
|
|
280
|
+
# THE NODE RUNTIME IS IN THE IMAGE (MARKETPLACE.md §5a, reversed 2026-09-03). There was
|
|
281
|
+
# a `unoverse_base:/app/base` volume here between 2026-08-30 and 2026-09-03, holding a
|
|
282
|
+
# base the entrypoint installed from npm at startup. Both are gone: base is compiled in
|
|
283
|
+
# from the workspace, so the image and its runtime cannot disagree.
|
|
292
284
|
# ── Platform-dev mounts (monorepo only; sync-starter strips these three
|
|
293
285
|
# lines from the starter's copy — a customer universe serves what the DB
|
|
294
286
|
# says). Local edits are picked up on restart; design/prompts are data, no build.
|
|
@@ -407,4 +399,3 @@ services:
|
|
|
407
399
|
volumes:
|
|
408
400
|
umap_models:
|
|
409
401
|
unoverse_plugins: # Marketplace-installed node packages (persisted across restarts)
|
|
410
|
-
unoverse_base: # @unoverse-platform/base, installed at startup (MARKETPLACE.md §5a)
|
package/operator/lib/check.sh
CHANGED
|
@@ -79,27 +79,18 @@ cmd_check() {
|
|
|
79
79
|
|
|
80
80
|
# 1b. THE NODE RUNTIME THIS UNIVERSE IS ACTUALLY RUNNING.
|
|
81
81
|
#
|
|
82
|
-
# base
|
|
83
|
-
#
|
|
84
|
-
#
|
|
85
|
-
#
|
|
82
|
+
# base ships INSIDE the image (MARKETPLACE.md §5a, reversed 2026-09-03), so this number
|
|
83
|
+
# cannot disagree with the platform version above — and printing it is how you can tell.
|
|
84
|
+
# It used to be installed from npm at startup and compared against the registry here,
|
|
85
|
+
# because the two moved independently; they do not any more, and a line that still
|
|
86
|
+
# offered "npm has newer" would be advertising a lane that no longer exists.
|
|
86
87
|
# Read out of the RUNNING container rather than off this machine's disk, because the
|
|
87
88
|
# container is the only thing whose answer counts.
|
|
88
|
-
local base_installed
|
|
89
|
+
local base_installed
|
|
89
90
|
base_installed=$(docker compose -f "$ROOT/docker-compose.yml" exec -T unoverse \
|
|
90
91
|
node -p "require('/app/node_modules/@unoverse-platform/base/package.json').version" 2>/dev/null | tr -d '\r')
|
|
91
92
|
if [ -n "$base_installed" ]; then
|
|
92
|
-
|
|
93
|
-
node -p "require('/app/apps/unoverse/package.json').dependencies['@unoverse-platform/base']" 2>/dev/null | tr -d '\r')
|
|
94
|
-
base_latest=$(curl -s --max-time 5 "https://registry.npmjs.org/-/package/@unoverse-platform%2Fbase/dist-tags" 2>/dev/null \
|
|
95
|
-
| node -pe 'try{JSON.parse(require("fs").readFileSync(0,"utf8")).latest ?? ""}catch(e){""}' 2>/dev/null)
|
|
96
|
-
if [ -n "$base_latest" ] && [ "$base_latest" != "$base_installed" ]; then
|
|
97
|
-
# NOT a failure. A universe is entitled to sit on an older runtime; it is not entitled
|
|
98
|
-
# to be unable to find out. `deploy` and `update` are what take the newer one.
|
|
99
|
-
warn "node runtime ${BOLD}$base_installed${NC} ${DIM}(range $base_range · npm has $base_latest — 'unoverse deploy' takes it)${NC}"
|
|
100
|
-
else
|
|
101
|
-
ok "node runtime ${BOLD}$base_installed${NC} ${DIM}(range ${base_range:-unknown})${NC}"
|
|
102
|
-
fi
|
|
93
|
+
ok "node runtime ${BOLD}$base_installed${NC} ${DIM}(in the image)${NC}"
|
|
103
94
|
echo ""
|
|
104
95
|
fi
|
|
105
96
|
|
|
@@ -133,7 +133,13 @@ cmd_db_verify() {
|
|
|
133
133
|
goals: [
|
|
134
134
|
"goal_id", "user_id", "workflow_id", "status", "description",
|
|
135
135
|
"acceptance_criteria", "budget", "goal_state", "created_workflow_ids",
|
|
136
|
-
"created_at", "updated_at", "completed_at"
|
|
136
|
+
"created_at", "updated_at", "completed_at", "directive", "bar_locked_at"
|
|
137
|
+
],
|
|
138
|
+
goal_attempts: [
|
|
139
|
+
"id", "goal_id", "attempt", "passed", "results", "deliverables", "judged_at"
|
|
140
|
+
],
|
|
141
|
+
goal_scratch: [
|
|
142
|
+
"goal_id", "agent_id", "agent_name", "state", "updated_at"
|
|
137
143
|
],
|
|
138
144
|
knowledge_docs: [
|
|
139
145
|
"id", "workflow_id", "title", "doc_type", "sections", "version",
|
package/operator/operator.sh
CHANGED
|
@@ -155,12 +155,10 @@ case "${1:-}" in
|
|
|
155
155
|
exit 1
|
|
156
156
|
fi
|
|
157
157
|
if [ -n "$(docker compose -f "$ROOT/docker-compose.yml" ps -q 2>/dev/null)" ]; then
|
|
158
|
-
# THE NODE RUNTIME
|
|
159
|
-
#
|
|
160
|
-
#
|
|
161
|
-
#
|
|
162
|
-
# it says so. Scoped to this command; nothing is written to .env.
|
|
163
|
-
UNOVERSE_BASE_REFRESH=1 \
|
|
158
|
+
# THE NODE RUNTIME ARRIVES WITH THE IMAGE. base is baked in, not installed
|
|
159
|
+
# (MARKETPLACE.md §5a, reversed 2026-09-03), so pulling the image IS updating the
|
|
160
|
+
# runtime and there is nothing extra to ask for. `UNOVERSE_BASE_REFRESH=1` used to
|
|
161
|
+
# ride this line to mean "take the newest base in range"; the range is gone with it.
|
|
164
162
|
docker compose -f "$ROOT/docker-compose.yml" --env-file "$ROOT/.env" up -d --remove-orphans >/dev/null 2>&1 \
|
|
165
163
|
&& ok "Running services moved to the new images"
|
|
166
164
|
# MIGRATIONS RUN ON UPDATE, same as deploy and start. An update is exactly when a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unoverse",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.196",
|
|
4
4
|
"description": "The Unoverse front door — create a Studio project, a universe, or a client app, and launch Studio.",
|
|
5
5
|
"license": "SEE LICENSE IN README.md",
|
|
6
6
|
"type": "module",
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"node": ">=18"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
+
"@cfworker/json-schema": "^4.1.1",
|
|
21
22
|
"yaml": "^2.8.1"
|
|
22
23
|
},
|
|
23
24
|
"homepage": "https://github.com/unoverse-platform",
|
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
import { readdirSync, readFileSync, existsSync, statSync } from "node:fs";
|
|
21
21
|
import { join, relative, basename } from "node:path";
|
|
22
22
|
import { fingerprintOf } from "./fingerprint.js";
|
|
23
|
+
import { parse as parseYaml } from "yaml";
|
|
24
|
+
import { readDiskPackages } from "../manifests/source.js";
|
|
23
25
|
import { designSystemVersion } from "./baseVersion.js";
|
|
24
26
|
/** Folders under rx/ that are never a developer's own project. */
|
|
25
27
|
const NOT_A_PROJECT = new Set(["marketplace", "_schema", "orgs"]);
|
|
@@ -189,6 +191,49 @@ export function collectProject(designRoot, project) {
|
|
|
189
191
|
};
|
|
190
192
|
walk(blocksHome);
|
|
191
193
|
}
|
|
194
|
+
// nodes/ — THE WORKSPACE'S NODE PACKAGES, one row per node, shaped exactly as the
|
|
195
|
+
// marketplace stores a node (core/items/catalogue.ts) so the row source composes them
|
|
196
|
+
// on the same path as disk: the raw files, plus the package envelope their $refs and
|
|
197
|
+
// credential shapes resolve against. The name is the node's `type`, bare: a saved
|
|
198
|
+
// workflow stores that identity and nothing else.
|
|
199
|
+
//
|
|
200
|
+
// Until 2026-09-05 deploy never looked here, so `deploy studio` shipped a developer's
|
|
201
|
+
// components and skills and silently left every node behind.
|
|
202
|
+
//
|
|
203
|
+
// NOT FROM THE PLATFORM TREE. The monorepo's nodes ship inside the core image and are
|
|
204
|
+
// read from disk there; publishing them as rows under whichever org happened to be
|
|
205
|
+
// deployed would double every platform node. A design system on disk is the mark of
|
|
206
|
+
// the platform tree (the same test designSystemDir makes), and a developer's workspace
|
|
207
|
+
// never has one.
|
|
208
|
+
const nodesHome = join(designRoot, "..", "nodes");
|
|
209
|
+
if (existsSync(nodesHome) && !existsSync(join(designRoot, "marketplace"))) {
|
|
210
|
+
for (const pkg of collectNodePackages(nodesHome)) {
|
|
211
|
+
for (const raw of pkg.nodes) {
|
|
212
|
+
const type = nodeType(raw.files["node.yaml"]);
|
|
213
|
+
if (!type)
|
|
214
|
+
continue; // lint has already refused it; nothing to name a row by
|
|
215
|
+
add("node", type, {
|
|
216
|
+
package: { name: pkg.name, packageFile: pkg.packageFile ?? null, credentials: pkg.credentials, shared: pkg.shared },
|
|
217
|
+
dir: raw.dir,
|
|
218
|
+
files: raw.files,
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
192
223
|
return items;
|
|
193
224
|
}
|
|
225
|
+
/** The workspace's node packages, read the way the platform reads its own. */
|
|
226
|
+
function collectNodePackages(nodesHome) {
|
|
227
|
+
return readDiskPackages(nodesHome).filter((pkg) => pkg.name !== "marketplace");
|
|
228
|
+
}
|
|
229
|
+
/** `type:` off a node.yaml: the one scalar the row is named by. Unparseable means none. */
|
|
230
|
+
function nodeType(nodeYaml) {
|
|
231
|
+
try {
|
|
232
|
+
const doc = parseYaml(nodeYaml ?? "");
|
|
233
|
+
return typeof doc?.type === "string" ? doc.type : "";
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
return "";
|
|
237
|
+
}
|
|
238
|
+
}
|
|
194
239
|
//# sourceMappingURL=collect.js.map
|
|
@@ -12,6 +12,8 @@
|
|
|
12
12
|
*
|
|
13
13
|
* See docs/architecture/DECLARATIVE_NODES.md §9.
|
|
14
14
|
*/
|
|
15
|
+
import { existsSync } from "node:fs";
|
|
16
|
+
import { join } from "node:path";
|
|
15
17
|
import { collectProject } from "./collect.js";
|
|
16
18
|
/**
|
|
17
19
|
* Lint a project, returning findings. Errors mean nothing is sent.
|
|
@@ -24,6 +26,18 @@ import { collectProject } from "./collect.js";
|
|
|
24
26
|
export async function lintForPublish(designRoot, project) {
|
|
25
27
|
const { lintDefinitions } = await import("../lint/design/index.mjs");
|
|
26
28
|
const result = lintDefinitions(designRoot);
|
|
29
|
+
/**
|
|
30
|
+
* NODES TOO. A workspace keeps its node packages beside design/ (the same `nodes/` the
|
|
31
|
+
* Studio Nodes screen reads), and until 2026-09-05 this gate never looked at them: a
|
|
32
|
+
* node with an events row out of order or a host missing from allowedHosts deployed as
|
|
33
|
+
* cleanly as a correct one. The node linter is the same library the platform's own CI
|
|
34
|
+
* runs. A workspace with no nodes/ folder has nothing to lint and is not an error.
|
|
35
|
+
*/
|
|
36
|
+
const nodesHome = join(designRoot, "..", "nodes");
|
|
37
|
+
if (existsSync(nodesHome)) {
|
|
38
|
+
const { lintNodes } = await import("../lint/nodes/index.mjs");
|
|
39
|
+
result.problems.push(...lintNodes(nodesHome).problems);
|
|
40
|
+
}
|
|
27
41
|
/**
|
|
28
42
|
* SCOPED TO THE PROJECT BEING DEPLOYED, on paths that may be relative. The old
|
|
29
43
|
* check looked for "/design/" with a leading slash, which a relative path never
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The state one lint run accumulates, and the only thing every rule module shares.
|
|
3
|
+
*
|
|
4
|
+
* It is MUTABLE and RESET per run, deliberately. The linter began as a CLI that ran once
|
|
5
|
+
* and exited, so module-level arrays were harmless; Studio and the publish route lint over
|
|
6
|
+
* and over in one process, and findings from the previous run would silently accumulate
|
|
7
|
+
* into the next. `reset()` is what makes a second call return the same answer as the first.
|
|
8
|
+
*/
|
|
9
|
+
import { relative } from "node:path";
|
|
10
|
+
|
|
11
|
+
/** Every finding, in discovery order. Sorted for presentation by the caller. */
|
|
12
|
+
export const problems = [];
|
|
13
|
+
|
|
14
|
+
/** Fragment path -> the set of nodes referencing it. Powers the shared/ pruning rules. */
|
|
15
|
+
export const refCounts = new Map();
|
|
16
|
+
|
|
17
|
+
/** Node -> the executor kind derived for it, shown as a summary rather than a finding. */
|
|
18
|
+
export const derivedKinds = new Map();
|
|
19
|
+
|
|
20
|
+
/** Credential name -> where it was declared, for cross-package collision rules. */
|
|
21
|
+
export const allCredentials = new Map();
|
|
22
|
+
|
|
23
|
+
/** Where this run is reading. Set by lintNodes(), read by rel() and the loaders. */
|
|
24
|
+
export const state = { nodesHome: "", schemaDir: "", schemas: {} };
|
|
25
|
+
|
|
26
|
+
export function reset(nodesHome, schemaDir) {
|
|
27
|
+
problems.length = 0;
|
|
28
|
+
seen.clear();
|
|
29
|
+
refCounts.clear();
|
|
30
|
+
derivedKinds.clear();
|
|
31
|
+
allCredentials.clear();
|
|
32
|
+
state.nodesHome = nodesHome;
|
|
33
|
+
state.schemaDir = schemaDir;
|
|
34
|
+
state.schemas = {};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* One finding. `error` fails a build; `warn` and `hint` inform.
|
|
39
|
+
*
|
|
40
|
+
* DEDUPED. The design linter walks a definition and its expansions, so the same rule can fire
|
|
41
|
+
* on the same line twice; a caller seeing it twice would think there were two problems.
|
|
42
|
+
* `line` is optional: the design linter knows where in the file, the node linter reports per file.
|
|
43
|
+
*/
|
|
44
|
+
const seen = new Set();
|
|
45
|
+
export const report = (level, file, msg, line) => {
|
|
46
|
+
const key = `${level}|${file}|${line ?? ""}|${msg}`;
|
|
47
|
+
if (seen.has(key)) return;
|
|
48
|
+
seen.add(key);
|
|
49
|
+
problems.push(line === undefined ? { level, file, msg } : { level, file, msg, line });
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/** Shortest readable form: a walk out of the tree is worse than the absolute path. */
|
|
53
|
+
export const rel = (p) => {
|
|
54
|
+
const r = relative(process.cwd(), p);
|
|
55
|
+
return r.startsWith("..") ? p : r;
|
|
56
|
+
};
|