unoverse 0.1.10 → 0.1.12
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 +17 -20
- package/lib/create.mjs +9 -8
- package/lib/urls.mjs +67 -44
- package/package.json +1 -1
package/bin/unoverse.mjs
CHANGED
|
@@ -50,9 +50,12 @@ const OPERATOR_COMMANDS = new Set([
|
|
|
50
50
|
|
|
51
51
|
const [, , cmd, ...args] = process.argv;
|
|
52
52
|
|
|
53
|
-
//
|
|
54
|
-
//
|
|
55
|
-
//
|
|
53
|
+
// FIRST CONTACT. Someone who has never seen this reads it, so it answers two questions
|
|
54
|
+
// before listing anything: what is this, and what do I type first. A flat list of six
|
|
55
|
+
// commands answered neither.
|
|
56
|
+
//
|
|
57
|
+
// Visual language matches the boot readout (packages/base/src/boot.ts): dim structure,
|
|
58
|
+
// one accent, green for the words you type. No em dashes (house style).
|
|
56
59
|
const COLOR = !process.env.NO_COLOR && process.stdout.isTTY !== false;
|
|
57
60
|
const paint = (code) => (t) => (COLOR ? `\x1b[${code}m${t}\x1b[0m` : t);
|
|
58
61
|
const bold = paint("1");
|
|
@@ -60,30 +63,24 @@ const dim = paint("2");
|
|
|
60
63
|
const cyan = paint("36");
|
|
61
64
|
const green = paint("32");
|
|
62
65
|
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
// a name makes one" made the reader hold both cases in their head to understand either.
|
|
66
|
-
["create", "What are you building? Studio project · universe · client app"],
|
|
67
|
-
["create <name>", "The same, in a new folder"],
|
|
68
|
-
["studio", "Author components, nodes, agent skills"],
|
|
69
|
-
["where", "Your universe's addresses, probed live, not recited"],
|
|
70
|
-
["update", "Update this CLI to the latest version"],
|
|
71
|
-
["help", "This"],
|
|
72
|
-
];
|
|
73
|
-
const PAD = Math.max(...CMDS.map(([c]) => c.length));
|
|
66
|
+
const PAD = 16;
|
|
67
|
+
const row = (cmd, desc) => ` ${green(cmd)}${" ".repeat(Math.max(1, PAD - cmd.length))}${desc}`;
|
|
74
68
|
|
|
75
69
|
const HELP = [
|
|
76
70
|
"",
|
|
77
71
|
` ${cyan("⬡")} ${bold("unoverse")}`,
|
|
78
72
|
"",
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
73
|
+
` ${dim("Build agent-powered apps. Interfaces as data, workflows on a canvas,")}`,
|
|
74
|
+
` ${dim("agent skills in plain markdown.")}`,
|
|
75
|
+
"",
|
|
76
|
+
` ${bold("First time")}`,
|
|
77
|
+
row("create", `Asks what you're building, then sets it up`),
|
|
78
|
+
"",
|
|
79
|
+
` ${bold("After that")}`,
|
|
80
|
+
row("studio", "Design components, nodes and agent skills"),
|
|
81
|
+
row("update", "Update this CLI"),
|
|
82
82
|
"",
|
|
83
83
|
].join("\n");
|
|
84
|
-
// NO "in a universe folder, add: ..." line. It named a thing a new user has never seen
|
|
85
|
-
// and listed commands they cannot run. The CLI already knows where you are standing, so
|
|
86
|
-
// those commands appear when they apply and stay invisible when they do not.
|
|
87
84
|
|
|
88
85
|
switch (cmd) {
|
|
89
86
|
case "create":
|
package/lib/create.mjs
CHANGED
|
@@ -99,14 +99,15 @@ export async function create(nameArg) {
|
|
|
99
99
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
100
100
|
try {
|
|
101
101
|
console.log(`\n ${cyan("⬡ What are you building?")}\n`);
|
|
102
|
-
console.log(
|
|
103
|
-
console.log("
|
|
104
|
-
console.log(
|
|
102
|
+
console.log(` 1. ${"A Studio project".padEnd(20)}Components, nodes and agent skills`);
|
|
103
|
+
console.log(` ${dim(" " + "".padEnd(20) + "Most people start here")}`);
|
|
104
|
+
console.log(` 2. ${"A universe".padEnd(20)}Run the platform on your own infrastructure`);
|
|
105
|
+
console.log(` 3. ${"A client app".padEnd(20)}A website that talks to a universe\n`);
|
|
105
106
|
|
|
106
107
|
const choice = (await ask(rl, "Choose [1]: ")) || "1";
|
|
107
108
|
|
|
108
109
|
if (choice === "1") {
|
|
109
|
-
console.log(`\n ${dim("Launching Unoverse Studio
|
|
110
|
+
console.log(`\n ${dim("Launching Unoverse Studio. It creates and manages your projects.")}\n`);
|
|
110
111
|
rl.close();
|
|
111
112
|
const r = spawnSync("npx", ["-y", "@unoverse-platform/studio@latest"], { stdio: "inherit" });
|
|
112
113
|
process.exit(r.status ?? 0);
|
|
@@ -114,10 +115,10 @@ export async function create(nameArg) {
|
|
|
114
115
|
|
|
115
116
|
if (choice === "2") {
|
|
116
117
|
console.log(`\n ${cyan("A universe needs a registry access token.")}`);
|
|
117
|
-
console.log(` ${dim("It authorizes the platform's images
|
|
118
|
+
console.log(` ${dim("It authorizes the platform's images. Your Unoverse admin issues it.")}\n`);
|
|
118
119
|
const token = await ask(rl, "Registry access token: ");
|
|
119
120
|
if (!token) {
|
|
120
|
-
fail("no token
|
|
121
|
+
fail("no token. The universe kit is available to licensed operators");
|
|
121
122
|
process.exit(1);
|
|
122
123
|
}
|
|
123
124
|
process.stdout.write(" validating against the registry...");
|
|
@@ -145,7 +146,7 @@ export async function create(nameArg) {
|
|
|
145
146
|
env: { ...process.env, UNOVERSE_DOCR_TOKEN: token },
|
|
146
147
|
});
|
|
147
148
|
if (r.status !== 0) {
|
|
148
|
-
fail("setup did not finish
|
|
149
|
+
fail("setup did not finish. Run 'unoverse init' in that folder to pick it up");
|
|
149
150
|
process.exit(r.status ?? 1);
|
|
150
151
|
}
|
|
151
152
|
console.log(`
|
|
@@ -153,7 +154,7 @@ export async function create(nameArg) {
|
|
|
153
154
|
cd ${name}`}
|
|
154
155
|
unoverse start
|
|
155
156
|
|
|
156
|
-
${cyan("Build with Claude:")} open the folder in Claude Code
|
|
157
|
+
${cyan("Build with Claude:")} open the folder in Claude Code. It ships an
|
|
157
158
|
authoring skill (components, nodes, agent skills) and a builder MCP that
|
|
158
159
|
constructs workflows live on your Canvas. Ask for what you want.
|
|
159
160
|
`);
|
package/lib/urls.mjs
CHANGED
|
@@ -1,13 +1,41 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `unoverse where`
|
|
2
|
+
* `unoverse where` answers one question: where is my universe, right now.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* BOTH HALVES. It used to report only the deployed stack, and `unoverse open` handled
|
|
5
|
+
* localhost separately. Folding `open` into `where` (2026-07-31) lost the local half
|
|
6
|
+
* until this: "where is it" means the copy on this machine AND the one on your server,
|
|
7
|
+
* because on any given day it is one, the other, or both.
|
|
8
|
+
*
|
|
9
|
+
* PROBED, not recited. Every address is actually requested and reported by what
|
|
10
|
+
* answered. A URL you cannot click is a hope, not an address.
|
|
11
|
+
*
|
|
12
|
+
* Deployed facts come from .env.production first, the ground's terraform outputs as
|
|
13
|
+
* enrichment. Local needs no config: a port is either listening or it is not.
|
|
5
14
|
*
|
|
6
|
-
*
|
|
7
|
-
* own facts — .env.production first, the ground's terraform outputs as enrichment —
|
|
8
|
-
* builds every URL a person or client would use, then actually requests each one
|
|
9
|
-
* and reports what answered. A URL you cannot click is a hope, not an address.
|
|
15
|
+
* Named `urls` until 0.1.5; that spelling still works as an alias.
|
|
10
16
|
*/
|
|
17
|
+
|
|
18
|
+
// What docker-compose publishes on this machine (docker-compose.yml).
|
|
19
|
+
const LOCAL = [
|
|
20
|
+
{ label: "Canvas", url: "http://localhost:3001" },
|
|
21
|
+
{ label: "API", url: "http://localhost:4105", note: "MCP at /mcp" },
|
|
22
|
+
{ label: "Logs", url: "http://localhost:8080", note: "Dozzle" },
|
|
23
|
+
];
|
|
24
|
+
|
|
25
|
+
/** One titled block of probed addresses. */
|
|
26
|
+
function section(title, rows, subtitle) {
|
|
27
|
+
const labelWidth = Math.max(...rows.map((r) => r.label.length));
|
|
28
|
+
const urlWidth = Math.max(...rows.map((r) => r.url.length));
|
|
29
|
+
const up = rows.filter((r) => r.up).length;
|
|
30
|
+
console.log("");
|
|
31
|
+
console.log(` ${up === rows.length ? green("●") : up ? cyan("●") : red("●")} ${bold(title)} ${dim(`${up} of ${rows.length} answering${subtitle ? ` · ${subtitle}` : ""}`)}`);
|
|
32
|
+
console.log("");
|
|
33
|
+
for (const r of rows) {
|
|
34
|
+
const status = r.up ? dim(`${r.status}`) : red("no answer");
|
|
35
|
+
console.log(` ${r.up ? green("●") : red("✗")} ${dim(r.label.padEnd(labelWidth))} ${cyan(r.url.padEnd(urlWidth))} ${status}${r.note ? ` ${dim(r.note)}` : ""}`);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
11
39
|
import { existsSync, readFileSync } from "node:fs";
|
|
12
40
|
import { join, resolve, dirname } from "node:path";
|
|
13
41
|
import { spawnSync } from "node:child_process";
|
|
@@ -66,52 +94,47 @@ async function probe(url) {
|
|
|
66
94
|
|
|
67
95
|
export async function urls() {
|
|
68
96
|
const root = findUniverseRoot();
|
|
69
|
-
if (!root) {
|
|
70
|
-
console.log(`\n ${red("✗")} no .env.production found here or above — run this inside a universe folder`);
|
|
71
|
-
console.log(` ${dim("(a universe is deployed from its checkout: terraform apply renders .env.production)")}\n`);
|
|
72
|
-
process.exit(1);
|
|
73
|
-
}
|
|
74
97
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
// The same law as the ground: DOMAIN drives HTTPS URLs; without it, the
|
|
80
|
-
// explicit API_URL (rendered by terraform) carries the LB address.
|
|
81
|
-
const apiBase = domain ? `https://api.${domain}` : env.API_URL || tf.api_url || "";
|
|
82
|
-
const candidates = [];
|
|
83
|
-
if (apiBase) {
|
|
84
|
-
candidates.push({ label: "API", url: apiBase, note: "MCP at /mcp" });
|
|
85
|
-
candidates.push({ label: "Health", url: `${apiBase}/health` });
|
|
86
|
-
}
|
|
87
|
-
const canvas = tf.canvas_url && tf.canvas_url.startsWith("http") ? tf.canvas_url : null;
|
|
88
|
-
if (canvas) candidates.push({ label: "Canvas", url: canvas, note: "add to IdP origins" });
|
|
89
|
-
if (env.DEPLOY_HOST) candidates.push({ label: "Dozzle", url: `http://${env.DEPLOY_HOST}:8080`, note: "logs, admin IP only" });
|
|
90
|
-
if (tf.cognito_hosted_ui) candidates.push({ label: "Login", url: tf.cognito_hosted_ui });
|
|
98
|
+
// LOCAL first, and unconditionally. Whether something is listening on this machine is
|
|
99
|
+
// a fact, not a configuration question, so it needs no universe folder to answer.
|
|
100
|
+
const local = await Promise.all(LOCAL.map(async (c) => ({ ...c, ...(await probe(c.url)) })));
|
|
91
101
|
|
|
92
|
-
if
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
102
|
+
// DEPLOYED, only if this checkout has been deployed. No .env.production is not an
|
|
103
|
+
// error: plenty of universes only ever run locally.
|
|
104
|
+
let deployed = [];
|
|
105
|
+
let domain = "";
|
|
106
|
+
if (root) {
|
|
107
|
+
const env = parseEnv(join(root, ".env.production"));
|
|
108
|
+
const tf = terraformOutputs(root);
|
|
109
|
+
domain = env.DOMAIN || "";
|
|
110
|
+
// The same law as the ground: DOMAIN drives HTTPS URLs; without it, the
|
|
111
|
+
// explicit API_URL (rendered by terraform) carries the LB address.
|
|
112
|
+
const apiBase = domain ? `https://api.${domain}` : env.API_URL || tf.api_url || "";
|
|
113
|
+
const candidates = [];
|
|
114
|
+
if (apiBase) {
|
|
115
|
+
candidates.push({ label: "API", url: apiBase, note: "MCP at /mcp" });
|
|
116
|
+
candidates.push({ label: "Health", url: `${apiBase}/health` });
|
|
117
|
+
}
|
|
118
|
+
const canvas = tf.canvas_url && tf.canvas_url.startsWith("http") ? tf.canvas_url : null;
|
|
119
|
+
if (canvas) candidates.push({ label: "Canvas", url: canvas, note: "add to IdP origins" });
|
|
120
|
+
if (env.DEPLOY_HOST) candidates.push({ label: "Logs", url: `http://${env.DEPLOY_HOST}:8080`, note: "admin IP only" });
|
|
121
|
+
if (tf.cognito_hosted_ui) candidates.push({ label: "Login", url: tf.cognito_hosted_ui });
|
|
122
|
+
deployed = await Promise.all(candidates.map(async (c) => ({ ...c, ...(await probe(c.url)) })));
|
|
96
123
|
}
|
|
97
124
|
|
|
98
|
-
const
|
|
99
|
-
|
|
100
|
-
const labelWidth = Math.max(...results.map((r) => r.label.length));
|
|
101
|
-
const urlWidth = Math.max(...results.map((r) => r.url.length));
|
|
102
|
-
const upCount = results.filter((r) => r.up).length;
|
|
103
|
-
const rule = dim("─".repeat(Math.min(70, urlWidth + labelWidth + 12)));
|
|
104
|
-
const mark = (r) => (r.up ? green("●") : red("✗"));
|
|
125
|
+
const anyUp = [...local, ...deployed].some((r) => r.up);
|
|
126
|
+
const rule = dim("─".repeat(64));
|
|
105
127
|
|
|
106
128
|
console.log("");
|
|
107
129
|
console.log(` ${rule}`);
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
console.log(
|
|
130
|
+
section("on this machine", local, local.some((r) => r.up) ? "" : "not running · unoverse start");
|
|
131
|
+
if (deployed.length) {
|
|
132
|
+
section("deployed", deployed, domain ? "" : "domainless (plain HTTP)");
|
|
133
|
+
} else {
|
|
134
|
+
console.log("");
|
|
135
|
+
console.log(` ${dim("● deployed")} ${dim(root ? "nothing to probe yet" : "not deployed from here · unoverse deploy")}`);
|
|
113
136
|
}
|
|
114
137
|
console.log(` ${rule}`);
|
|
115
138
|
console.log("");
|
|
116
|
-
if (
|
|
139
|
+
if (!anyUp) process.exit(1);
|
|
117
140
|
}
|
package/package.json
CHANGED