unoverse 0.1.11 → 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 CHANGED
@@ -78,7 +78,6 @@ const HELP = [
78
78
  "",
79
79
  ` ${bold("After that")}`,
80
80
  row("studio", "Design components, nodes and agent skills"),
81
- row("where", "Your universe's addresses"),
82
81
  row("update", "Update this CLI"),
83
82
  "",
84
83
  ].join("\n");
package/lib/urls.mjs CHANGED
@@ -1,13 +1,41 @@
1
1
  /**
2
- * `unoverse where` the deployed stack's addresses, PROBED, not recited.
2
+ * `unoverse where` answers one question: where is my universe, right now.
3
3
  *
4
- * Named `urls` until 0.1.5; that spelling still works as an alias.
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
- * Run it in a universe folder (or anywhere above one). It reads the deployment's
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
- const env = parseEnv(join(root, ".env.production"));
76
- const tf = terraformOutputs(root);
77
- const domain = env.DOMAIN || "";
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 (!candidates.length) {
93
- console.log(`\n ${red("✗")} .env.production has neither DOMAIN nor API_URL re-render it from the ground:`);
94
- console.log(` ${dim("cd infra/<ground> && terraform output -raw env_production > ../../.env.production")}\n`);
95
- process.exit(1);
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 results = await Promise.all(candidates.map(async (c) => ({ ...c, ...(await probe(c.url)) })));
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
- console.log(` ${upCount === results.length ? green("●") : red("●")} ${bold("universe")} ${dim(`${upCount} of ${results.length} answering${domain ? "" : " · domainless (plain HTTP) — set domain in terraform.tfvars to upgrade"}`)}`);
109
- console.log("");
110
- for (const r of results) {
111
- const status = r.up ? dim(`${r.status}`) : red("no answer");
112
- console.log(` ${mark(r)} ${dim(r.label.padEnd(labelWidth))} ${cyan(r.url.padEnd(urlWidth))} ${status}${r.note ? ` ${dim(r.note)}` : ""}`);
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 (upCount < results.length) process.exit(1);
139
+ if (!anyUp) process.exit(1);
117
140
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unoverse",
3
- "version": "0.1.11",
3
+ "version": "0.1.12",
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",