unoverse 0.0.1 → 0.1.1
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 +16 -3
- package/bin/unoverse.mjs +67 -0
- package/lib/create.mjs +137 -0
- package/package.json +20 -5
package/README.md
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
# unoverse
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
The Unoverse front door.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
```bash
|
|
6
|
+
npm create unoverse@latest # what are you building?
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
- **A Studio project** — author components, templates, nodes, and agent skills.
|
|
10
|
+
Most people start (and stay) here. Launches [Unoverse Studio](https://www.npmjs.com/package/@unoverse-platform/studio).
|
|
11
|
+
- **A universe** — run the full platform on your own infrastructure. For
|
|
12
|
+
operators: requires a registry access token from your Unoverse admin.
|
|
13
|
+
- **A client app** — a website or embed talking to an existing universe.
|
|
14
|
+
|
|
15
|
+
Already have a universe? Its operator CLI lives inside it: `cd <universe> && ./unoverse`.
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
unoverse studio # launch Studio any time
|
|
19
|
+
```
|
package/bin/unoverse.mjs
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* unoverse — the front door.
|
|
4
|
+
*
|
|
5
|
+
* v0.1 owns FIRST CONTACT: `create` (what are you building?) and `studio`
|
|
6
|
+
* (launch the authoring app). Operating a universe stays with the `./unoverse`
|
|
7
|
+
* CLI inside the scaffolded kit; when this package absorbs it, that becomes
|
|
8
|
+
* transparent to the developer.
|
|
9
|
+
*/
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { spawnSync } from "node:child_process";
|
|
12
|
+
import { create } from "../lib/create.mjs";
|
|
13
|
+
|
|
14
|
+
const [, , cmd, ...args] = process.argv;
|
|
15
|
+
|
|
16
|
+
const HELP = `
|
|
17
|
+
⬡ unoverse
|
|
18
|
+
|
|
19
|
+
unoverse create [name] What are you building? Studio project · universe · client app
|
|
20
|
+
unoverse studio Launch Unoverse Studio (authoring: components, nodes, skills)
|
|
21
|
+
unoverse help This
|
|
22
|
+
|
|
23
|
+
Operating a running universe? That lives in your universe folder:
|
|
24
|
+
cd <your-universe> && ./unoverse
|
|
25
|
+
`;
|
|
26
|
+
|
|
27
|
+
switch (cmd) {
|
|
28
|
+
case "create":
|
|
29
|
+
await create(args[0]);
|
|
30
|
+
break;
|
|
31
|
+
|
|
32
|
+
case "studio": {
|
|
33
|
+
// Studio is its own npm app; this is a launcher, not a wrapper.
|
|
34
|
+
const r = spawnSync("npx", ["-y", "@unoverse-platform/studio@latest", ...args], {
|
|
35
|
+
stdio: "inherit",
|
|
36
|
+
});
|
|
37
|
+
process.exit(r.status ?? 0);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
case "help":
|
|
41
|
+
case "--help":
|
|
42
|
+
case "-h":
|
|
43
|
+
case undefined:
|
|
44
|
+
console.log(HELP);
|
|
45
|
+
break;
|
|
46
|
+
|
|
47
|
+
case "--version":
|
|
48
|
+
case "-v": {
|
|
49
|
+
const { readFileSync } = await import("node:fs");
|
|
50
|
+
const { fileURLToPath } = await import("node:url");
|
|
51
|
+
const pkg = JSON.parse(
|
|
52
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
53
|
+
);
|
|
54
|
+
console.log(pkg.version);
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
default:
|
|
59
|
+
// A universe folder has its own operator CLI — point there rather than
|
|
60
|
+
// half-implementing its commands here (v0.2 absorbs them properly).
|
|
61
|
+
if (existsSync("./unoverse")) {
|
|
62
|
+
console.log(`\n '${cmd}' is an operator command — run it in your universe:\n\n ./unoverse ${cmd} ${args.join(" ")}\n`);
|
|
63
|
+
} else {
|
|
64
|
+
console.log(`\n Unknown command: ${cmd}\n${HELP}`);
|
|
65
|
+
}
|
|
66
|
+
process.exit(1);
|
|
67
|
+
}
|
package/lib/create.mjs
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `unoverse create` — first contact, as a question instead of a repo choice.
|
|
3
|
+
*
|
|
4
|
+
* Studio is the default path: most people author, few people operate. The
|
|
5
|
+
* universe (operator kit) is the professional tier and is GATED: it demands a
|
|
6
|
+
* valid registry access token BEFORE anything downloads, because the kit is
|
|
7
|
+
* useless without one — the platform's images only pull with it. Validation is
|
|
8
|
+
* a real auth round-trip against the registry, not a format check.
|
|
9
|
+
*/
|
|
10
|
+
import { createInterface } from "node:readline/promises";
|
|
11
|
+
import { existsSync, mkdirSync } from "node:fs";
|
|
12
|
+
import { spawnSync } from "node:child_process";
|
|
13
|
+
|
|
14
|
+
const REGISTRY_HOST = "registry.digitalocean.com";
|
|
15
|
+
const STARTER_TARBALL =
|
|
16
|
+
"https://codeload.github.com/unoverse-platform/starter/tar.gz/refs/heads/main";
|
|
17
|
+
|
|
18
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
19
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
20
|
+
const ok = (s) => console.log(` \x1b[32m✓\x1b[0m ${s}`);
|
|
21
|
+
const fail = (s) => console.log(` \x1b[31m✗\x1b[0m ${s}`);
|
|
22
|
+
|
|
23
|
+
async function ask(rl, q) {
|
|
24
|
+
return (await rl.question(` ${q}`)).trim();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** The gate: prove the token opens the registry before handing over the kit.
|
|
28
|
+
* Docker registries use the bearer-realm flow: /v2/ answers 401 and names the
|
|
29
|
+
* auth realm; presenting the token there yields 200 (valid) or 401 (refused). */
|
|
30
|
+
async function validateRegistryToken(token) {
|
|
31
|
+
try {
|
|
32
|
+
const probe = await fetch(`https://${REGISTRY_HOST}/v2/`);
|
|
33
|
+
const wa = probe.headers.get("www-authenticate") ?? "";
|
|
34
|
+
const realm = /realm="([^"]+)"/.exec(wa)?.[1];
|
|
35
|
+
const service = /service="([^"]+)"/.exec(wa)?.[1] ?? REGISTRY_HOST;
|
|
36
|
+
if (!realm) return probe.status === 200;
|
|
37
|
+
const auth = Buffer.from(`${token}:${token}`).toString("base64");
|
|
38
|
+
const res = await fetch(
|
|
39
|
+
`${realm}?service=${encodeURIComponent(service)}&scope=registry:catalog:*`,
|
|
40
|
+
{ headers: { Authorization: `Basic ${auth}` } },
|
|
41
|
+
);
|
|
42
|
+
return res.ok;
|
|
43
|
+
} catch {
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function download(dir, name) {
|
|
49
|
+
mkdirSync(dir, { recursive: true });
|
|
50
|
+
const r = spawnSync(
|
|
51
|
+
"sh",
|
|
52
|
+
["-c", `curl -fsSL '${STARTER_TARBALL}' | tar -xz --strip-components=1 -C '${dir}'`],
|
|
53
|
+
{ stdio: ["ignore", "inherit", "inherit"] },
|
|
54
|
+
);
|
|
55
|
+
if (r.status !== 0) throw new Error("download failed");
|
|
56
|
+
ok(`universe scaffolded at ./${name}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export async function create(nameArg) {
|
|
60
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
61
|
+
try {
|
|
62
|
+
console.log(`\n ${cyan("⬡ What are you building?")}\n`);
|
|
63
|
+
console.log(" 1. A Studio project — author components, nodes, agent skills (most people start here)");
|
|
64
|
+
console.log(" 2. A universe — run the full platform on your own infrastructure (operators)");
|
|
65
|
+
console.log(" 3. A client app — a website/embed against an existing universe\n");
|
|
66
|
+
|
|
67
|
+
const choice = (await ask(rl, "Choose [1]: ")) || "1";
|
|
68
|
+
|
|
69
|
+
if (choice === "1") {
|
|
70
|
+
console.log(`\n ${dim("Launching Unoverse Studio — it creates and manages your projects.")}\n`);
|
|
71
|
+
rl.close();
|
|
72
|
+
const r = spawnSync("npx", ["-y", "@unoverse-platform/studio@latest"], { stdio: "inherit" });
|
|
73
|
+
process.exit(r.status ?? 0);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
if (choice === "2") {
|
|
77
|
+
console.log(`\n ${cyan("A universe needs a registry access token.")}`);
|
|
78
|
+
console.log(` ${dim("It authorizes the platform's images — issued by your Unoverse admin.")}\n`);
|
|
79
|
+
const token = await ask(rl, "Registry access token: ");
|
|
80
|
+
if (!token) {
|
|
81
|
+
fail("no token — the universe kit is available to licensed operators");
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
process.stdout.write(" validating against the registry...");
|
|
85
|
+
const valid = await validateRegistryToken(token);
|
|
86
|
+
console.log("");
|
|
87
|
+
if (!valid) {
|
|
88
|
+
fail("that token was refused by the registry");
|
|
89
|
+
console.log(` ${dim("Check it with your Unoverse admin, then try again.")}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
ok("token accepted");
|
|
93
|
+
|
|
94
|
+
const name = nameArg || (await ask(rl, "Universe folder name [my-universe]: ")) || "my-universe";
|
|
95
|
+
if (existsSync(name)) {
|
|
96
|
+
fail(`./${name} already exists`);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
download(name, name);
|
|
100
|
+
console.log(`
|
|
101
|
+
Next:
|
|
102
|
+
cd ${name}
|
|
103
|
+
./unoverse init ${dim("(setup wizard — have this token ready, it asks for it)")}
|
|
104
|
+
./unoverse start
|
|
105
|
+
|
|
106
|
+
${cyan("Build with Claude:")} open the folder in Claude Code — it ships an
|
|
107
|
+
authoring skill (components, nodes, agent skills) and a builder MCP that
|
|
108
|
+
constructs workflows live on your Canvas. Ask for what you want.
|
|
109
|
+
`);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (choice === "3") {
|
|
114
|
+
const name = nameArg || (await ask(rl, "App folder name [my-client]: ")) || "my-client";
|
|
115
|
+
if (existsSync(name)) {
|
|
116
|
+
fail(`./${name} already exists`);
|
|
117
|
+
process.exit(1);
|
|
118
|
+
}
|
|
119
|
+
// The reference client ships inside the starter — take just that folder.
|
|
120
|
+
mkdirSync(name, { recursive: true });
|
|
121
|
+
const r = spawnSync(
|
|
122
|
+
"sh",
|
|
123
|
+
["-c", `curl -fsSL '${STARTER_TARBALL}' | tar -xz --strip-components=3 -C '${name}' '*/packages/client'`],
|
|
124
|
+
{ stdio: ["ignore", "inherit", "inherit"] },
|
|
125
|
+
);
|
|
126
|
+
if (r.status !== 0 ) throw new Error("download failed");
|
|
127
|
+
ok(`client app scaffolded at ./${name}`);
|
|
128
|
+
console.log(`\n Next:\n cd ${name}\n npm install && npm run dev\n\n ${cyan("Build with Claude:")} open the folder in Claude Code and ask for what you want.\n`);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
fail(`unknown choice: ${choice}`);
|
|
133
|
+
process.exit(1);
|
|
134
|
+
} finally {
|
|
135
|
+
rl.close();
|
|
136
|
+
}
|
|
137
|
+
}
|
package/package.json
CHANGED
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "unoverse",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
5
|
-
"license": "SEE LICENSE IN README",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "The Unoverse front door \u2014 create a Studio project, a universe, or a client app, and launch Studio.",
|
|
5
|
+
"license": "SEE LICENSE IN README.md",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"unoverse": "bin/unoverse.mjs"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin",
|
|
12
|
+
"lib",
|
|
13
|
+
"README.md"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
6
18
|
"homepage": "https://github.com/unoverse-platform",
|
|
7
|
-
"repository": {
|
|
8
|
-
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "git+https://github.com/unoverse-platform/unoverse.git"
|
|
22
|
+
}
|
|
23
|
+
}
|