unoverse 0.1.134 → 0.1.136

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.
@@ -42,9 +42,10 @@ export async function create(nameArg) {
42
42
  console.log(`\n ${dim(`${target === "." ? "This folder" : `./${target}`} is already a universe. Picking setup back up.`)}\n`);
43
43
  rl.close();
44
44
  const here2 = dirname(fileURLToPath(import.meta.url));
45
- const vend = resolve(here2, "../operator/operator.sh");
46
- const op = existsSync(vend) ? vend : resolve(here2, "../../../scripts/operator.sh");
47
- const r2 = spawnSync("bash", [op, "init"], { stdio: "inherit", cwd: target });
45
+ // Depths are from lib/create/, NOT lib/ — see the note at the second copy below.
46
+ const repo2 = resolve(here2, "../../../../scripts/operator.sh");
47
+ const op = existsSync(repo2) ? repo2 : resolve(here2, "../../operator/operator.sh");
48
+ const r2 = spawnSync("bash", [op, "setup"], { stdio: "inherit", cwd: target });
48
49
  process.exit(r2.status ?? 0);
49
50
  }
50
51
 
@@ -91,16 +92,26 @@ export async function create(nameArg) {
91
92
  rl.close();
92
93
  console.log("");
93
94
  // This package's own operator, run against the folder just scaffolded.
95
+ // COUNT FROM lib/create/, not lib/. These four segments moved down a directory in
96
+ // the create.mjs split and kept the depths they had at lib/, so the vendored copy
97
+ // was looked for at lib/operator/ (never there) and the fallback landed on
98
+ // <node_modules>/scripts/operator.sh. Every universe scaffolded, then died on
99
+ // "setup did not finish" at the last step.
100
+ //
101
+ // IN-REPO WINS, as in bin/unoverse.mjs: the vendored copy is a prepack artifact, so
102
+ // preferring it makes every edit to scripts/lib invisible while developing here. A
103
+ // published package has no ../../../../scripts, so there the vendored copy is the
104
+ // only one and still wins.
94
105
  const here = dirname(fileURLToPath(import.meta.url));
95
- const vendored = resolve(here, "../operator/operator.sh");
96
- const operatorScript = existsSync(vendored)
97
- ? vendored
98
- : resolve(here, "../../../scripts/operator.sh");
106
+ const inRepo = resolve(here, "../../../../scripts/operator.sh");
107
+ const operatorScript = existsSync(inRepo)
108
+ ? inRepo
109
+ : resolve(here, "../../operator/operator.sh");
99
110
  // NO authoring skill here, deliberately. Building components, nodes and agent
100
111
  // skills is STUDIO work and its skill ships with Studio projects. What a universe
101
112
  // offers Claude Code is the canvas MCP (.mcp.json in the scaffold): building and
102
113
  // managing workflows on this universe's Canvas.
103
- const r = spawnSync("bash", [operatorScript, "init"], {
114
+ const r = spawnSync("bash", [operatorScript, "setup"], {
104
115
  stdio: "inherit",
105
116
  cwd: name,
106
117
  env: { ...process.env, UNOVERSE_DOCR_TOKEN: token.pass, UNOVERSE_DOCR_USER: token.user },
@@ -90,7 +90,13 @@ cmd_db_verify() {
90
90
  "universal_id", "content_hash", "title", "description", "object_type",
91
91
  "needs", "source_url", "source_id", "umap_x", "umap_y", "umap_z",
92
92
  "umap_cluster_id", "color_hex", "needs_umap_update", "created_at",
93
- "updated_at", "workflow_id", "key_need", "metadata", "embedding_original"
93
+ "updated_at", "workflow_id", "key_need", "metadata", "embedding_original",
94
+ "cluster_distance"
95
+ ],
96
+ dictionary_clusters: [
97
+ "cluster_id", "workflow_id", "parent_id", "depth", "name", "description",
98
+ "name_embedding", "medoid_id", "terms", "member_count", "umap_x", "umap_y",
99
+ "umap_z", "radius", "created_at", "updated_at"
94
100
  ],
95
101
  dictionary_content_chunks: [
96
102
  "chunk_id", "text", "source_url", "source_type", "metadata",
@@ -0,0 +1,389 @@
1
+ #!/usr/bin/env bash
2
+ # The .env a local universe needs, written once by `unoverse create`.
3
+
4
+ # install_to_path REMOVED 2026-07-31. The global command is the npm package `unoverse`
5
+ # (`npm i -g unoverse`); symlinking a per-project bash script into /usr/local/bin was the
6
+ # second CLI we set out to delete.
7
+
8
+ # ── The image download, as an experience ─────────────────────────────────────
9
+ #
10
+ # The pull starts the moment the token is accepted, so the minute spent answering
11
+ # questions is also the minute the images arrive. NOTHING writes to the terminal
12
+ # asynchronously — a progress bar racing a readline prompt corrupts what the developer
13
+ # is typing. The background half writes one line of STATE to a temp file; the wizard
14
+ # prints a status line synchronously between question groups, where output composes
15
+ # with input; the end of setup attaches with docker's own bars for whatever remains.
16
+ PULL_STATE=""
17
+ start_background_pull() {
18
+ PULL_STATE=$(mktemp)
19
+ (
20
+ imgs=$(docker compose -f "$ROOT/docker-compose.yml" config --images 2>/dev/null | sort -u)
21
+ total=$(printf '%s\n' "$imgs" | grep -c .)
22
+ n=0
23
+ for img in $imgs; do
24
+ short="${img##*/}"; short="${short%%:*}"
25
+ if docker image inspect "$img" >/dev/null 2>&1; then
26
+ n=$((n+1)); echo "$n $total ready $short" > "$PULL_STATE"; continue
27
+ fi
28
+ echo "$n $total pulling $short" > "$PULL_STATE"
29
+ docker pull "$img" >/dev/null 2>&1
30
+ n=$((n+1)); echo "$n $total pulled $short" > "$PULL_STATE"
31
+ done
32
+ echo "$total $total done -" > "$PULL_STATE"
33
+ ) &
34
+ }
35
+
36
+ # One dim line, printed only when the wizard is printing anyway. Never a redraw.
37
+ pull_status_line() {
38
+ [ -n "$PULL_STATE" ] && [ -s "$PULL_STATE" ] || return 0
39
+ local n total verb what
40
+ read -r n total verb what < "$PULL_STATE" 2>/dev/null || return 0
41
+ if [ "$verb" = "done" ]; then
42
+ echo -e " ${GREEN}⬇${NC} ${DIM}platform images: all $total downloaded${NC}"
43
+ elif [ "$verb" = "pulling" ]; then
44
+ echo -e " ${CYAN}⬇${NC} ${DIM}platform images: $n of $total · pulling $what…${NC}"
45
+ else
46
+ echo -e " ${CYAN}⬇${NC} ${DIM}platform images: $n of $total${NC}"
47
+ fi
48
+ echo ""
49
+ }
50
+
51
+ cmd_setup() {
52
+ echo ""
53
+ echo -e " ${BOLD}${CYAN}⬡ Unoverse Setup${NC}"
54
+ echo -e " ${DIM}─────────────────────────────────${NC}"
55
+ echo ""
56
+
57
+ # (The studio/platform mode interview was removed 2026-07-28 — Studio is a
58
+ # separate app, and this CLI only sets up the platform.)
59
+ timer_start
60
+
61
+ # DOCKER IS NEEDED TO START, NOT TO CONFIGURE. This used to exit here, which threw
62
+ # away a scaffold and a validated token because Docker Desktop happened to be closed.
63
+ # Writing .env needs nothing running, so record the state and carry on; the steps that
64
+ # genuinely need a daemon skip themselves, and `start` is where it becomes an error.
65
+ DOCKER_OK=0
66
+ if ! command -v docker &>/dev/null; then
67
+ warn "Docker is not installed. Configuration will finish; install it before ${BOLD}unoverse start${NC}"
68
+ info "Install: https://docs.docker.com/get-docker/"
69
+ elif ! docker info &>/dev/null; then
70
+ warn "Docker is not running. Configuration will finish; start Docker Desktop before ${BOLD}unoverse start${NC}"
71
+ else
72
+ DOCKER_OK=1
73
+ ok "Docker is installed and running"
74
+ fi
75
+
76
+ # Apple Silicon check
77
+ if [ "$(uname -m)" = "arm64" ]; then
78
+ ok "Apple Silicon detected: multi-arch images will run natively"
79
+ echo ""
80
+ fi
81
+
82
+ # RE-RUNNING EDITS. There is no "overwrite? [y/N]" gate any more: every existing value
83
+ # becomes its question's default, so Enter keeps a setting and typing replaces it.
84
+ # Walking through changes only what you change — which makes this the way to change
85
+ # one env var, not a destructive restart.
86
+ _env_cur() { grep "^$1=" "$ROOT/.env" 2>/dev/null | head -1 | cut -d= -f2-; }
87
+
88
+ echo ""
89
+ echo -e " ${BOLD}Configure your environment:${NC}"
90
+ if [ -f "$ROOT/.env" ]; then
91
+ echo -e " ${DIM}(Existing .env found. Enter keeps each current value)${NC}"
92
+ else
93
+ echo -e " ${DIM}(Press Enter to use defaults)${NC}"
94
+ fi
95
+ echo ""
96
+
97
+ # DOCR Token. `unoverse create` has already asked for this and VALIDATED it against
98
+ # the registry, so it hands it over rather than making you type the same credential
99
+ # twice minutes apart. Typed here only when init is run on its own.
100
+ if [ -n "${UNOVERSE_DOCR_TOKEN:-}" ]; then
101
+ DOCR_TOKEN="$UNOVERSE_DOCR_TOKEN"
102
+ DOCR_USER="${UNOVERSE_DOCR_USER:-$UNOVERSE_DOCR_TOKEN}"
103
+ ok "Registry token carried over from create"
104
+ else
105
+ local cur_token
106
+ cur_token=$(_env_cur DOCR_TOKEN)
107
+ DOCR_USER=$(_env_cur DOCR_USER)
108
+ while true; do
109
+ if [ -n "$cur_token" ]; then
110
+ read -p " DOCR Token [keep current]: " DOCR_TOKEN || { fail "no input (end of stream). Run unoverse create interactively"; exit 1; }
111
+ DOCR_TOKEN="${DOCR_TOKEN:-$cur_token}"
112
+ else
113
+ read -p " DOCR Token (from your Unoverse admin): " DOCR_TOKEN || { fail "no input (end of stream). Run unoverse create interactively"; exit 1; }
114
+ fi
115
+ # Accept the credential in any shape, AS A PAIR. The downloaded Docker
116
+ # credentials wrap base64("email:token") — the username is the email, and
117
+ # logging in token-as-username gets "unauthorized" for exactly that shape.
118
+ DOCR_USER=""
119
+ if [[ "$DOCR_TOKEN" != dop_v1_* ]]; then
120
+ local decoded
121
+ decoded=$(printf '%s' "$DOCR_TOKEN" | sed -E 's/.*"auth"[^"]*"([A-Za-z0-9+\/=]+)".*/\1/' | base64 -d 2>/dev/null | tr -d '\0')
122
+ [ -n "$decoded" ] || decoded=$(printf '%s' "$DOCR_TOKEN" | base64 -d 2>/dev/null | tr -d '\0')
123
+ case "$decoded" in
124
+ *:dop_v1_*) DOCR_USER="${decoded%%:*}"; DOCR_TOKEN="dop_v1_${decoded##*dop_v1_}";;
125
+ esac
126
+ fi
127
+ DOCR_USER="${DOCR_USER:-$DOCR_TOKEN}"
128
+ if [[ "$DOCR_TOKEN" == dop_v1_* ]]; then
129
+ break
130
+ fi
131
+ fail "That does not look like a registry credential. Paste it exactly as it was sent"
132
+ done
133
+ fi
134
+
135
+ # THE DOWNLOAD STARTS NOW (see the block at the top of this file).
136
+ if [ "$DOCKER_OK" = "1" ]; then
137
+ local login_err
138
+ if login_err=$(echo "$DOCR_TOKEN" | docker login "$DOCR_REGISTRY" -u "${DOCR_USER:-$DOCR_TOKEN}" --password-stdin 2>&1 >/dev/null); then
139
+ start_background_pull
140
+ echo ""
141
+ echo -e " ${CYAN}⬇${NC} ${DIM}Platform images are downloading in the background while you configure${NC}"
142
+ echo ""
143
+ else
144
+ # The REASON, not just the fact: a swallowed docker error left "login failed"
145
+ # undiagnosable when create had just validated the same credential.
146
+ warn "Registry login failed. Images will not pull until it is fixed"
147
+ echo "$login_err" | grep -vi "warning" | head -3 | sed 's/^/ /'
148
+ fi
149
+ fi
150
+
151
+ # A DEFAULT, because "from your admin" is meaningless when you are the admin. The
152
+ # platform ships no database (docker-compose has no postgres), so this points at one
153
+ # you run. Enter takes the conventional local one.
154
+ DB_DEFAULT=$(_env_cur DATABASE_URL)
155
+ DB_DEFAULT="${DB_DEFAULT:-postgres://postgres:postgres@localhost:5432/unoverse}"
156
+ while true; do
157
+ read -p " DATABASE_URL [${DB_DEFAULT}]: " DATABASE_URL || { fail "no input (end of stream). Run unoverse create interactively"; exit 1; }
158
+ DATABASE_URL="${DATABASE_URL:-$DB_DEFAULT}"
159
+ if [ -n "$DATABASE_URL" ] && [[ "$DATABASE_URL" != *"user:password"* ]]; then
160
+ break
161
+ fi
162
+ fail "DATABASE_URL is required. Get it from your Unoverse admin"
163
+ done
164
+
165
+ # Auto-add SSL params if missing
166
+ if [[ "$DATABASE_URL" != *"sslmode="* ]] && [[ "$DATABASE_URL" != *"ssl="* ]]; then
167
+ local sep="?"
168
+ [[ "$DATABASE_URL" == *"?"* ]] && sep="&"
169
+ if [[ "$DATABASE_URL" == *"localhost"* ]] || \
170
+ [[ "$DATABASE_URL" == *"127.0.0.1"* ]] || \
171
+ [[ "$DATABASE_URL" == *"host.docker.internal"* ]]; then
172
+ DATABASE_URL="${DATABASE_URL}${sep}sslmode=disable"
173
+ ok "Local database detected. Added sslmode=disable"
174
+ else
175
+ DATABASE_URL="${DATABASE_URL}${sep}sslmode=require"
176
+ ok "Managed database detected. Added sslmode=require"
177
+ fi
178
+ fi
179
+
180
+ pull_status_line
181
+ # Redis, current values as defaults
182
+ local rd
183
+ rd=$(_env_cur REDIS_HOST); rd="${rd:-host.docker.internal}"
184
+ read -p " REDIS_HOST [$rd]: " REDIS_HOST
185
+ REDIS_HOST="${REDIS_HOST:-$rd}"
186
+
187
+ rd=$(_env_cur REDIS_PORT); rd="${rd:-6379}"
188
+ read -p " REDIS_PORT [$rd]: " REDIS_PORT
189
+ REDIS_PORT="${REDIS_PORT:-$rd}"
190
+
191
+ rd=$(_env_cur REDIS_PASSWORD)
192
+ if [ -n "$rd" ]; then
193
+ read -p " REDIS_PASSWORD [keep current]: " REDIS_PASSWORD
194
+ REDIS_PASSWORD="${REDIS_PASSWORD:-$rd}"
195
+ else
196
+ read -p " REDIS_PASSWORD (blank for none): " REDIS_PASSWORD
197
+ fi
198
+
199
+ rd=$(_env_cur REDIS_TLS); rd="${rd:-false}"
200
+ read -p " REDIS_TLS [$rd]: " REDIS_TLS
201
+ REDIS_TLS="${REDIS_TLS:-$rd}"
202
+
203
+ # Auth (required — from admin)
204
+ # ASK BEFORE DEMANDING. A developer trying the platform locally has no identity
205
+ # provider yet, and AUTH_ISSUER was required, so init could not be completed at all.
206
+ #
207
+ # This is a LOCAL switch only. INFRASTRUCTURE.md is explicit that auth is always on for
208
+ # a deployed universe ("there is no auth-off deployment"), and the platform enforces it
209
+ # rather than trusting this wizard: authConfig.ts refuses to start with auth off when
210
+ # NODE_ENV=production. So answering "no" here cannot produce an unprotected deployment.
211
+ pull_status_line
212
+ local cur_auth idp_prompt
213
+ cur_auth=$(_env_cur AUTH_ENABLED)
214
+ idp_prompt="[y/N]"
215
+ [ "$cur_auth" = "true" ] && idp_prompt="[Y/n]"
216
+ read -r -p " Do you have an identity provider (Auth0/OIDC) to connect? $idp_prompt " HAS_IDP
217
+ echo ""
218
+ if [ -z "$HAS_IDP" ] && [ "$cur_auth" = "true" ]; then HAS_IDP=y; fi
219
+
220
+ if [[ "$HAS_IDP" =~ ^[Yy]$ ]]; then
221
+ AUTH_ENABLED=true
222
+ local cur_iss cur_cid cur_aud
223
+ cur_iss=$(_env_cur AUTH_ISSUER)
224
+ cur_cid=$(_env_cur AUTH_CLIENT_ID)
225
+ cur_aud=$(_env_cur AUTH_AUDIENCE)
226
+
227
+ while true; do
228
+ if [ -n "$cur_iss" ]; then
229
+ read -p " AUTH_ISSUER [$cur_iss]: " AUTH_ISSUER || { fail "no input (end of stream). Run unoverse create interactively"; exit 1; }
230
+ AUTH_ISSUER="${AUTH_ISSUER:-$cur_iss}"
231
+ else
232
+ read -p " AUTH_ISSUER (e.g. https://your-tenant.auth0.com): " AUTH_ISSUER || { fail "no input (end of stream). Run unoverse create interactively"; exit 1; }
233
+ fi
234
+ if [ -n "$AUTH_ISSUER" ] && [[ "$AUTH_ISSUER" == https://* ]]; then
235
+ break
236
+ fi
237
+ fail "AUTH_ISSUER must be an https:// URL from your identity provider"
238
+ done
239
+
240
+ while true; do
241
+ if [ -n "$cur_cid" ]; then
242
+ read -p " AUTH_CLIENT_ID [$cur_cid]: " AUTH_CLIENT_ID || { fail "no input (end of stream). Run unoverse create interactively"; exit 1; }
243
+ AUTH_CLIENT_ID="${AUTH_CLIENT_ID:-$cur_cid}"
244
+ else
245
+ read -p " AUTH_CLIENT_ID: " AUTH_CLIENT_ID || { fail "no input (end of stream). Run unoverse create interactively"; exit 1; }
246
+ fi
247
+ if [ -n "$AUTH_CLIENT_ID" ] && [[ "$AUTH_CLIENT_ID" != *"your-"* ]]; then
248
+ break
249
+ fi
250
+ fail "AUTH_CLIENT_ID is required"
251
+ done
252
+
253
+ cur_aud="${cur_aud:-gravity-api}"
254
+ read -p " AUTH_AUDIENCE [$cur_aud]: " AUTH_AUDIENCE
255
+ AUTH_AUDIENCE="${AUTH_AUDIENCE:-$cur_aud}"
256
+ else
257
+ AUTH_ENABLED=false
258
+ AUTH_ISSUER=""
259
+ AUTH_CLIENT_ID=""
260
+ AUTH_AUDIENCE="gravity-api"
261
+ warn "Auth is OFF for local development"
262
+ info "Deploying needs it: ${BOLD}unoverse deploy${NC} runs with auth on, and the server refuses to start without it in production"
263
+ fi
264
+
265
+ # NOT ASKED. 4105 is the port docker-compose publishes, so locally this is a fact the
266
+ # CLI already knows, and confirming it is not a question. A DEPLOYED universe gets its
267
+ # own API_URL rendered into .env.production by Terraform, which never comes through here.
268
+ API_URL="http://localhost:4105"
269
+
270
+ # DERIVED FROM THE FOLDER, not hardcoded. Every key the memory server writes is
271
+ # prefixed with this, so two universes sharing one Redis and the same namespace mix
272
+ # their streams and caches. It used to be written as a literal `gravity` for every
273
+ # local universe, while Terraform rendered `universe` for deployed ones: same Redis,
274
+ # same prefix, silently interleaved.
275
+ REDIS_NAMESPACE=$(basename "$ROOT" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-' | sed 's/-*$//')
276
+ REDIS_NAMESPACE="${REDIS_NAMESPACE:-universe}"
277
+ ok "Redis namespace: ${BOLD}${REDIS_NAMESPACE}${NC}"
278
+
279
+ # OpenAI (for Memory Server)
280
+ pull_status_line
281
+ local cur_oai
282
+ cur_oai=$(_env_cur OPENAI_API_KEY)
283
+ # NOT just the memory server: compose hands this to the unoverse service itself —
284
+ # agents, embeddings and memory all run on it. A universe boots without it, but its AI
285
+ # does not, so skipping gets a warning rather than silence.
286
+ if [ -n "$cur_oai" ]; then
287
+ read -p " OPENAI_API_KEY [keep current]: " OPENAI_API_KEY
288
+ OPENAI_API_KEY="${OPENAI_API_KEY:-$cur_oai}"
289
+ else
290
+ read -p " OPENAI_API_KEY (powers the platform's AI): " OPENAI_API_KEY
291
+ [ -z "$OPENAI_API_KEY" ] && warn "No OpenAI key: the universe starts, but agents, embeddings and memory will not work until one is in .env"
292
+ fi
293
+
294
+ # Node vendor keys, OPTIONAL: only the matching nodes need them, nothing platform-level
295
+ # does. Asked so compose stops warning about them and so they land in .env with names.
296
+ local cur_hb
297
+ cur_hb=$(_env_cur HYPERBROWSER_API_KEY)
298
+ if [ -n "$cur_hb" ]; then
299
+ read -p " HYPERBROWSER_API_KEY [keep current]: " HYPERBROWSER_API_KEY
300
+ HYPERBROWSER_API_KEY="${HYPERBROWSER_API_KEY:-$cur_hb}"
301
+ else
302
+ read -p " HYPERBROWSER_API_KEY (browser nodes, blank to skip): " HYPERBROWSER_API_KEY
303
+ fi
304
+
305
+
306
+ # A silent fact, not a question: the encryption key is GENERATED (a human never types
307
+ # one) and kept verbatim on re-runs — a new key orphans every stored credential.
308
+ CREDENTIAL_ENCRYPTION_KEY=$(_env_cur CREDENTIAL_ENCRYPTION_KEY)
309
+ if [ -z "$CREDENTIAL_ENCRYPTION_KEY" ]; then
310
+ CREDENTIAL_ENCRYPTION_KEY=$(openssl rand -hex 32 2>/dev/null || head -c32 /dev/urandom | xxd -p -c64)
311
+ ok "Credential encryption key generated"
312
+ fi
313
+ # Write .env
314
+ cat > "$ROOT/.env" << ENVEOF
315
+ # Written by unoverse create
316
+ DOCR_TOKEN=${DOCR_TOKEN}
317
+ DOCR_USER=${DOCR_USER:-${DOCR_TOKEN}}
318
+ DATABASE_URL=${DATABASE_URL}
319
+ REDIS_HOST=${REDIS_HOST}
320
+ REDIS_PORT=${REDIS_PORT}
321
+ REDIS_PASSWORD=${REDIS_PASSWORD}
322
+ REDIS_TLS=${REDIS_TLS}
323
+ REDIS_NAMESPACE=${REDIS_NAMESPACE}
324
+ AUTH_ENABLED=${AUTH_ENABLED}
325
+ AUTH_ISSUER=${AUTH_ISSUER}
326
+ AUTH_CLIENT_ID=${AUTH_CLIENT_ID}
327
+ AUTH_AUDIENCE=${AUTH_AUDIENCE}
328
+ API_URL=${API_URL}
329
+ OPENAI_API_KEY=${OPENAI_API_KEY}
330
+ # Generated at setup: encrypts credentials stored by nodes. Losing it orphans them —
331
+ # back it up with the database. Kept on re-runs.
332
+ CREDENTIAL_ENCRYPTION_KEY=${CREDENTIAL_ENCRYPTION_KEY}
333
+ HYPERBROWSER_API_KEY=${HYPERBROWSER_API_KEY}
334
+ DOMAIN=
335
+ ENVEOF
336
+
337
+ ok ".env created"
338
+
339
+ # Attach to the background pull started when the token was accepted: layers already
340
+ # down show as complete, the rest stream docker's progress bars. Without a daemon the
341
+ # token is in .env, and `unoverse start` pulls on its first run.
342
+ if [ "$DOCKER_OK" = "1" ]; then
343
+ pull_missing_images
344
+ [ -n "$PULL_STATE" ] && rm -f "$PULL_STATE"
345
+ else
346
+ echo ""
347
+ info "Skipped the image download. ${BOLD}unoverse start${NC} does it once Docker is up"
348
+ fi
349
+
350
+ # Install to PATH
351
+
352
+ # Done
353
+ echo ""
354
+ echo -e " ${GREEN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
355
+ echo -e " ${GREEN}${BOLD} ✓ Setup Complete!${NC} ${DIM}($(timer_elapsed))${NC}"
356
+ echo -e " ${GREEN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
357
+ # MIGRATIONS RUN HERE, so `db-setup` is not a command anyone has to know about. Deploy
358
+ # already ran them on the server (playbooks/db-setup.yml); this is the local half.
359
+ # A database that is not reachable yet is not a failed setup — .env is written and
360
+ # correct, so say so and move on rather than unwinding everything.
361
+ echo ""
362
+ # SUBSHELL, deliberately: cmd_db_setup exits on "services not up yet", and an exit in a
363
+ # sourced function would take all of init with it — setup then reported failure for a
364
+ # situation that just means "migrations run on start". Which they now do (start.sh).
365
+ if grep -q '^DATABASE_URL=' "$ROOT/.env" 2>/dev/null; then
366
+ # Quietly: db-setup narrates its own advice ("start services first, then re-run"),
367
+ # which contradicts and duplicates the one line that is true here.
368
+ if (cmd_db_setup) >/dev/null 2>&1; then
369
+ ok "Database schema is up to date"
370
+ else
371
+ info "Migrations wait for the platform: ${BOLD}unoverse start${NC} applies them once services are up"
372
+ fi
373
+ fi
374
+
375
+ echo ""
376
+ echo -e " ${BOLD}Next steps:${NC}"
377
+ echo ""
378
+ if [ "$DOCKER_OK" = "1" ]; then
379
+ echo -e " ${GREEN}unoverse start${NC} Start the platform"
380
+ else
381
+ echo -e " ${DIM}1.${NC} Start Docker Desktop"
382
+ echo -e " ${DIM}2.${NC} ${GREEN}unoverse start${NC}"
383
+ fi
384
+ echo -e " ${GREEN}unoverse where${NC} Links to your Canvas and API"
385
+ echo ""
386
+ info "Run ${BOLD}unoverse check${NC} anytime to see if it is healthy"
387
+ info "Change a setting any time: edit ${BOLD}.env${NC} directly, or re-run ${BOLD}unoverse create${NC} here (Enter keeps each current value)"
388
+ echo ""
389
+ }
@@ -58,6 +58,7 @@ source "$GRAVITY_LIB/check.sh"
58
58
  source "$GRAVITY_LIB/help.sh"
59
59
  source "$GRAVITY_LIB/db-setup.sh"
60
60
  source "$GRAVITY_LIB/db-verify.sh"
61
+ source "$GRAVITY_LIB/setup.sh"
61
62
  source "$GRAVITY_LIB/deploy.sh"
62
63
  source "$GRAVITY_LIB/destroy.sh"
63
64
  source "$GRAVITY_LIB/ground.sh"
@@ -70,6 +71,12 @@ source "$GRAVITY_LIB/ground.sh"
70
71
  # (The old "studio mode" gate is gone — 2026-07-28. This CLI has ONE job:
71
72
  # operate a universe. Studio is a separate app; authoring happens there.)
72
73
  case "${1:-}" in
74
+ # NOT A COMMAND ANYONE TYPES. `unoverse create` calls it with the registry token it has
75
+ # already validated, and it writes the .env a local universe needs. It was deleted with
76
+ # `unoverse init` (2026-08-02) on the reading that create wrote the .env; create never
77
+ # did, it called this. What the deletion was RIGHT about is kept: it is not advertised,
78
+ # and `start` no longer launches it, so starting the platform is never an interview.
79
+ setup) cmd_setup ;;
73
80
  start) shift; cmd_start "$@" ;;
74
81
  stop) cmd_stop ;;
75
82
  logs) cmd_logs "${2:-}" ;;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unoverse",
3
- "version": "0.1.134",
3
+ "version": "0.1.136",
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",