unoverse 0.1.135 → 0.1.137

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.
@@ -45,7 +45,7 @@ export async function create(nameArg) {
45
45
  // Depths are from lib/create/, NOT lib/ — see the note at the second copy below.
46
46
  const repo2 = resolve(here2, "../../../../scripts/operator.sh");
47
47
  const op = existsSync(repo2) ? repo2 : resolve(here2, "../../operator/operator.sh");
48
- const r2 = spawnSync("bash", [op, "init"], { stdio: "inherit", cwd: target });
48
+ const r2 = spawnSync("bash", [op, "setup"], { stdio: "inherit", cwd: target });
49
49
  process.exit(r2.status ?? 0);
50
50
  }
51
51
 
@@ -111,7 +111,7 @@ export async function create(nameArg) {
111
111
  // skills is STUDIO work and its skill ships with Studio projects. What a universe
112
112
  // offers Claude Code is the canvas MCP (.mcp.json in the scaffold): building and
113
113
  // managing workflows on this universe's Canvas.
114
- const r = spawnSync("bash", [operatorScript, "init"], {
114
+ const r = spawnSync("bash", [operatorScript, "setup"], {
115
115
  stdio: "inherit",
116
116
  cwd: name,
117
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,396 @@
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
+ # BRING YOUR OWN, or take the one the deploy makes. The question used to be "do you have
204
+ # an identity provider (Auth0/OIDC) to connect?", which reads as a requirement a
205
+ # developer must go and satisfy. It is not one: AWS provisions a Cognito user pool and
206
+ # writes AUTH_ISSUER, AUTH_CLIENT_ID and AUTH_AUDIENCE into .env.production itself
207
+ # (infra/aws/outputs.tf). Only DigitalOcean needs an issuer from you, and only at deploy
208
+ # time (deploy.sh _ensure_ground_config), where it asks.
209
+ #
210
+ # So the honest question is whether you are bringing one, and the default is no.
211
+ #
212
+ # Answering either way is a LOCAL switch only. INFRASTRUCTURE.md is explicit that there
213
+ # is no auth-off deployment, and the platform enforces that rather than trusting this
214
+ # wizard: authConfig.ts refuses to start with auth off when NODE_ENV=production.
215
+ pull_status_line
216
+ local cur_auth idp_prompt
217
+ cur_auth=$(_env_cur AUTH_ENABLED)
218
+ idp_prompt="[y/N]"
219
+ [ "$cur_auth" = "true" ] && idp_prompt="[Y/n]"
220
+ echo ""
221
+ info "Sign-in: deploying provides one. ${DIM}AWS creates a Cognito user pool; DigitalOcean asks for an issuer then${NC}"
222
+ read -r -p " Are you bringing your own (Auth0/OIDC)? $idp_prompt " HAS_IDP
223
+ echo ""
224
+ if [ -z "$HAS_IDP" ] && [ "$cur_auth" = "true" ]; then HAS_IDP=y; fi
225
+
226
+ if [[ "$HAS_IDP" =~ ^[Yy]$ ]]; then
227
+ AUTH_ENABLED=true
228
+ local cur_iss cur_cid cur_aud
229
+ cur_iss=$(_env_cur AUTH_ISSUER)
230
+ cur_cid=$(_env_cur AUTH_CLIENT_ID)
231
+ cur_aud=$(_env_cur AUTH_AUDIENCE)
232
+
233
+ while true; do
234
+ if [ -n "$cur_iss" ]; then
235
+ read -p " AUTH_ISSUER [$cur_iss]: " AUTH_ISSUER || { fail "no input (end of stream). Run unoverse create interactively"; exit 1; }
236
+ AUTH_ISSUER="${AUTH_ISSUER:-$cur_iss}"
237
+ else
238
+ 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; }
239
+ fi
240
+ if [ -n "$AUTH_ISSUER" ] && [[ "$AUTH_ISSUER" == https://* ]]; then
241
+ break
242
+ fi
243
+ fail "AUTH_ISSUER must be an https:// URL from your identity provider"
244
+ done
245
+
246
+ while true; do
247
+ if [ -n "$cur_cid" ]; then
248
+ read -p " AUTH_CLIENT_ID [$cur_cid]: " AUTH_CLIENT_ID || { fail "no input (end of stream). Run unoverse create interactively"; exit 1; }
249
+ AUTH_CLIENT_ID="${AUTH_CLIENT_ID:-$cur_cid}"
250
+ else
251
+ read -p " AUTH_CLIENT_ID: " AUTH_CLIENT_ID || { fail "no input (end of stream). Run unoverse create interactively"; exit 1; }
252
+ fi
253
+ if [ -n "$AUTH_CLIENT_ID" ] && [[ "$AUTH_CLIENT_ID" != *"your-"* ]]; then
254
+ break
255
+ fi
256
+ fail "AUTH_CLIENT_ID is required"
257
+ done
258
+
259
+ cur_aud="${cur_aud:-gravity-api}"
260
+ read -p " AUTH_AUDIENCE [$cur_aud]: " AUTH_AUDIENCE
261
+ AUTH_AUDIENCE="${AUTH_AUDIENCE:-$cur_aud}"
262
+ ok "Your provider will be used, here and when you deploy"
263
+ else
264
+ AUTH_ENABLED=false
265
+ AUTH_ISSUER=""
266
+ AUTH_CLIENT_ID=""
267
+ AUTH_AUDIENCE="gravity-api"
268
+ info "No sign-in locally. The deploy makes one when you ship this universe"
269
+ fi
270
+
271
+
272
+ # NOT ASKED. 4105 is the port docker-compose publishes, so locally this is a fact the
273
+ # CLI already knows, and confirming it is not a question. A DEPLOYED universe gets its
274
+ # own API_URL rendered into .env.production by Terraform, which never comes through here.
275
+ API_URL="http://localhost:4105"
276
+
277
+ # DERIVED FROM THE FOLDER, not hardcoded. Every key the memory server writes is
278
+ # prefixed with this, so two universes sharing one Redis and the same namespace mix
279
+ # their streams and caches. It used to be written as a literal `gravity` for every
280
+ # local universe, while Terraform rendered `universe` for deployed ones: same Redis,
281
+ # same prefix, silently interleaved.
282
+ REDIS_NAMESPACE=$(basename "$ROOT" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-' | sed 's/-*$//')
283
+ REDIS_NAMESPACE="${REDIS_NAMESPACE:-universe}"
284
+ ok "Redis namespace: ${BOLD}${REDIS_NAMESPACE}${NC}"
285
+
286
+ # OpenAI (for Memory Server)
287
+ pull_status_line
288
+ local cur_oai
289
+ cur_oai=$(_env_cur OPENAI_API_KEY)
290
+ # NOT just the memory server: compose hands this to the unoverse service itself —
291
+ # agents, embeddings and memory all run on it. A universe boots without it, but its AI
292
+ # does not, so skipping gets a warning rather than silence.
293
+ if [ -n "$cur_oai" ]; then
294
+ read -p " OPENAI_API_KEY [keep current]: " OPENAI_API_KEY
295
+ OPENAI_API_KEY="${OPENAI_API_KEY:-$cur_oai}"
296
+ else
297
+ read -p " OPENAI_API_KEY (powers the platform's AI): " OPENAI_API_KEY
298
+ [ -z "$OPENAI_API_KEY" ] && warn "No OpenAI key: the universe starts, but agents, embeddings and memory will not work until one is in .env"
299
+ fi
300
+
301
+ # Node vendor keys, OPTIONAL: only the matching nodes need them, nothing platform-level
302
+ # does. Asked so compose stops warning about them and so they land in .env with names.
303
+ local cur_hb
304
+ cur_hb=$(_env_cur HYPERBROWSER_API_KEY)
305
+ if [ -n "$cur_hb" ]; then
306
+ read -p " HYPERBROWSER_API_KEY [keep current]: " HYPERBROWSER_API_KEY
307
+ HYPERBROWSER_API_KEY="${HYPERBROWSER_API_KEY:-$cur_hb}"
308
+ else
309
+ read -p " HYPERBROWSER_API_KEY (browser nodes, blank to skip): " HYPERBROWSER_API_KEY
310
+ fi
311
+
312
+
313
+ # A silent fact, not a question: the encryption key is GENERATED (a human never types
314
+ # one) and kept verbatim on re-runs — a new key orphans every stored credential.
315
+ CREDENTIAL_ENCRYPTION_KEY=$(_env_cur CREDENTIAL_ENCRYPTION_KEY)
316
+ if [ -z "$CREDENTIAL_ENCRYPTION_KEY" ]; then
317
+ CREDENTIAL_ENCRYPTION_KEY=$(openssl rand -hex 32 2>/dev/null || head -c32 /dev/urandom | xxd -p -c64)
318
+ ok "Credential encryption key generated"
319
+ fi
320
+ # Write .env
321
+ cat > "$ROOT/.env" << ENVEOF
322
+ # Written by unoverse create
323
+ DOCR_TOKEN=${DOCR_TOKEN}
324
+ DOCR_USER=${DOCR_USER:-${DOCR_TOKEN}}
325
+ DATABASE_URL=${DATABASE_URL}
326
+ REDIS_HOST=${REDIS_HOST}
327
+ REDIS_PORT=${REDIS_PORT}
328
+ REDIS_PASSWORD=${REDIS_PASSWORD}
329
+ REDIS_TLS=${REDIS_TLS}
330
+ REDIS_NAMESPACE=${REDIS_NAMESPACE}
331
+ AUTH_ENABLED=${AUTH_ENABLED}
332
+ AUTH_ISSUER=${AUTH_ISSUER}
333
+ AUTH_CLIENT_ID=${AUTH_CLIENT_ID}
334
+ AUTH_AUDIENCE=${AUTH_AUDIENCE}
335
+ API_URL=${API_URL}
336
+ OPENAI_API_KEY=${OPENAI_API_KEY}
337
+ # Generated at setup: encrypts credentials stored by nodes. Losing it orphans them —
338
+ # back it up with the database. Kept on re-runs.
339
+ CREDENTIAL_ENCRYPTION_KEY=${CREDENTIAL_ENCRYPTION_KEY}
340
+ HYPERBROWSER_API_KEY=${HYPERBROWSER_API_KEY}
341
+ DOMAIN=
342
+ ENVEOF
343
+
344
+ ok ".env created"
345
+
346
+ # Attach to the background pull started when the token was accepted: layers already
347
+ # down show as complete, the rest stream docker's progress bars. Without a daemon the
348
+ # token is in .env, and `unoverse start` pulls on its first run.
349
+ if [ "$DOCKER_OK" = "1" ]; then
350
+ pull_missing_images
351
+ [ -n "$PULL_STATE" ] && rm -f "$PULL_STATE"
352
+ else
353
+ echo ""
354
+ info "Skipped the image download. ${BOLD}unoverse start${NC} does it once Docker is up"
355
+ fi
356
+
357
+ # Install to PATH
358
+
359
+ # Done
360
+ echo ""
361
+ echo -e " ${GREEN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
362
+ echo -e " ${GREEN}${BOLD} ✓ Setup Complete!${NC} ${DIM}($(timer_elapsed))${NC}"
363
+ echo -e " ${GREEN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
364
+ # MIGRATIONS RUN HERE, so `db-setup` is not a command anyone has to know about. Deploy
365
+ # already ran them on the server (playbooks/db-setup.yml); this is the local half.
366
+ # A database that is not reachable yet is not a failed setup — .env is written and
367
+ # correct, so say so and move on rather than unwinding everything.
368
+ echo ""
369
+ # SUBSHELL, deliberately: cmd_db_setup exits on "services not up yet", and an exit in a
370
+ # sourced function would take all of init with it — setup then reported failure for a
371
+ # situation that just means "migrations run on start". Which they now do (start.sh).
372
+ if grep -q '^DATABASE_URL=' "$ROOT/.env" 2>/dev/null; then
373
+ # Quietly: db-setup narrates its own advice ("start services first, then re-run"),
374
+ # which contradicts and duplicates the one line that is true here.
375
+ if (cmd_db_setup) >/dev/null 2>&1; then
376
+ ok "Database schema is up to date"
377
+ else
378
+ info "Migrations wait for the platform: ${BOLD}unoverse start${NC} applies them once services are up"
379
+ fi
380
+ fi
381
+
382
+ echo ""
383
+ echo -e " ${BOLD}Next steps:${NC}"
384
+ echo ""
385
+ if [ "$DOCKER_OK" = "1" ]; then
386
+ echo -e " ${GREEN}unoverse start${NC} Start the platform"
387
+ else
388
+ echo -e " ${DIM}1.${NC} Start Docker Desktop"
389
+ echo -e " ${DIM}2.${NC} ${GREEN}unoverse start${NC}"
390
+ fi
391
+ echo -e " ${GREEN}unoverse where${NC} Links to your Canvas and API"
392
+ echo ""
393
+ info "Run ${BOLD}unoverse check${NC} anytime to see if it is healthy"
394
+ info "Change a setting any time: edit ${BOLD}.env${NC} directly, or re-run ${BOLD}unoverse create${NC} here (Enter keeps each current value)"
395
+ echo ""
396
+ }
@@ -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.135",
3
+ "version": "0.1.137",
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",