unoverse 0.1.93 → 0.1.95

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
@@ -60,7 +60,7 @@ const UNIVERSE = findUniverse();
60
60
  const OPERATOR_COMMANDS = new Set([
61
61
  "start", "stop", "check", "logs", "deploy", "destroy", "db-allow",
62
62
  // kept working, not advertised
63
- "ground", "dev", "build", "publish", "init",
63
+ "ground", "dev", "build", "publish",
64
64
  ]);
65
65
 
66
66
  const [, , cmd, ...args] = process.argv;
@@ -40,7 +40,7 @@ cmd_doctor() {
40
40
  if [ -f "$ROOT/.env" ]; then
41
41
  ok ".env file exists"
42
42
  else
43
- fail ".env file missing. Run ${BOLD}unoverse init${NC}"
43
+ fail ".env file missing. Run ${BOLD}unoverse create${NC} in an empty folder"
44
44
  issues=$((issues + 1))
45
45
  print_summary $issues
46
46
  return
@@ -82,7 +82,7 @@ cmd_doctor() {
82
82
  if [ -f "$docker_config" ] && grep -q "$DOCR_REGISTRY" "$docker_config" 2>/dev/null; then
83
83
  ok "Logged in to DigitalOcean Container Registry"
84
84
  else
85
- fail "Not logged in to DOCR. Run ${BOLD}unoverse init${NC}"
85
+ fail "Not logged in to the registry. Run ${BOLD}unoverse start${NC}, which logs in from your .env"
86
86
  issues=$((issues + 1))
87
87
  fi
88
88
 
@@ -22,9 +22,11 @@ cmd_help() {
22
22
  echo -e " ${GREEN}stop${NC} Stop it"
23
23
  echo -e " ${GREEN}check${NC} Is it healthy ${DIM}(services, schema, environment)${NC}"
24
24
  echo -e " ${GREEN}logs${NC} What is it doing ${DIM}(unoverse logs <service> for one)${NC}"
25
- echo -e " ${GREEN}deploy${NC} Ship it to a server ${DIM}(first run asks which cloud)${NC}"
26
- echo -e " ${GREEN}destroy${NC} Take the deployment down ${DIM}(shows what goes, and what stays)${NC}"
27
- echo -e " ${DIM}With two grounds, name one: deploy aws · destroy digitalocean${NC}"
25
+ # THE ARGUMENT IS ON THE LINE, not in a footnote. It read as a special case for people
26
+ # with two clouds; it is simply how the command is shaped. A cloud is optional when only
27
+ # one ground exists and required when two do.
28
+ echo -e " ${GREEN}deploy${NC} Ship it to a server ${DIM}· aws or digitalocean${NC}"
29
+ echo -e " ${GREEN}destroy${NC} Take it down ${DIM}· aws or digitalocean, shows what goes and what stays${NC}"
28
30
  echo -e " ${GREEN}db-allow${NC} Let this machine reach the database ${DIM}(run it when you change network)${NC}"
29
31
  echo ""
30
32
  # Owner-only lane. Printed ONLY when publish.sh is present, so a starter kit never
@@ -25,7 +25,7 @@ check_docker_file_sharing() {
25
25
  echo -e " This will cause services to stay in ${BOLD}Created${NC} state and not start."
26
26
  echo ""
27
27
  echo -e " ${BOLD}Fix (choose one):${NC}"
28
- echo -e " 1. Move project to ${BOLD}~/dev/${NC} or ${BOLD}~/projects/${NC} and re-run ${BOLD}unoverse init${NC}"
28
+ echo -e " 1. Move project to ${BOLD}~/dev/${NC} or ${BOLD}~/projects/${NC} and re-run ${BOLD}unoverse start${NC}"
29
29
  echo -e " 2. Docker Desktop → Settings → Resources → File Sharing → add ${BOLD}$HOME/$location${NC}"
30
30
  echo ""
31
31
  return 1
@@ -79,7 +79,16 @@ cmd_start() {
79
79
  fi
80
80
 
81
81
  check_docker_file_sharing || exit 1
82
- check_first_run
82
+ # NO SETUP WIZARD. `unoverse create` writes the .env; there is nothing for start to
83
+ # interview about, and an interactive wizard on the way to "start the platform" was a
84
+ # command inside a command. Say what is missing and name the one that fixes it.
85
+ if [ ! -f "$ROOT/.env" ]; then
86
+ echo ""
87
+ fail "No .env here. This folder is not a universe"
88
+ info "Run ${BOLD}unoverse create${NC} in an empty folder to make one"
89
+ echo ""
90
+ exit 1
91
+ fi
83
92
  banner "Starting Unoverse Platform"
84
93
  timer_start
85
94
 
@@ -126,7 +135,7 @@ cmd_start() {
126
135
  echo -e " ${DIM}───────────────────────────────${NC}"
127
136
  echo ""
128
137
  echo -e " ${YELLOW}Common fixes:${NC}"
129
- echo -e " • Not logged into registry? ${GREEN}unoverse init${NC}"
138
+ echo -e " • Not logged into registry? check DOCR_TOKEN in ${GREEN}.env${NC}"
130
139
  echo -e " • Missing .env? ${GREEN}cp .env.example .env${NC} and fill in values"
131
140
  echo -e " • Images not pulled? ${GREEN}docker compose pull${NC}"
132
141
  echo ""
@@ -167,8 +176,8 @@ cmd_start() {
167
176
  build_component_nodes
168
177
  # Migrations, now that the unoverse service is up (they run from inside its
169
178
  # container). This is what makes start the resume point after any interrupted
170
- # setup: pull, boot, migrate, no third command to know. Subshell for the same
171
- # reason init uses one: cmd_db_setup exits rather than returns on failure.
179
+ # setup: pull, boot, migrate, no third command to know. Subshell because cmd_db_setup exits
180
+ # rather than returns on failure.
172
181
  if ! (cmd_db_setup) >/dev/null 2>&1; then
173
182
  warn "Migrations did not apply. Run ${BOLD}unoverse check${NC} to see why"
174
183
  fi
@@ -180,7 +189,7 @@ cmd_start() {
180
189
  echo ""
181
190
  echo -e " ${YELLOW}No containers were created. Possible causes:${NC}"
182
191
  echo -e " • Images not pulled yet? ${GREEN}docker compose pull${NC}"
183
- echo -e " • Not logged into registry? ${GREEN}unoverse init${NC}"
192
+ echo -e " • Not logged into registry? check DOCR_TOKEN in ${GREEN}.env${NC}"
184
193
  echo ""
185
194
  else
186
195
  printf "\r ${YELLOW}!${NC} ${DIM}$running/$total services running${NC}\n"
@@ -33,7 +33,6 @@ fi
33
33
  # ── Source all modules ──────────────────────────────────────────────
34
34
  source "$GRAVITY_LIB/find-root.sh"
35
35
  source "$GRAVITY_LIB/common.sh"
36
- source "$GRAVITY_LIB/init.sh"
37
36
  source "$GRAVITY_LIB/start.sh"
38
37
  source "$GRAVITY_LIB/stop.sh"
39
38
  source "$GRAVITY_LIB/logs.sh"
@@ -55,7 +54,6 @@ source "$GRAVITY_LIB/ground.sh"
55
54
  # (The old "studio mode" gate is gone — 2026-07-28. This CLI has ONE job:
56
55
  # operate a universe. Studio is a separate app; authoring happens there.)
57
56
  case "${1:-}" in
58
- init) cmd_init ;; # not advertised: create configures, start resumes
59
57
  start) shift; cmd_start "$@" ;;
60
58
  stop) cmd_stop ;;
61
59
  logs) cmd_logs "${2:-}" ;;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unoverse",
3
- "version": "0.1.93",
3
+ "version": "0.1.95",
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",
@@ -1,430 +0,0 @@
1
- #!/usr/bin/env bash
2
- # unoverse init, login, install-to-path, first-run check
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
- # First-run detection
52
- check_first_run() {
53
- if [ ! -f "$ROOT/.env" ]; then
54
- echo ""
55
- echo -e " ${YELLOW}${BOLD}Welcome to Unoverse!${NC}"
56
- echo ""
57
- echo -e " Looks like this is your first time. Let's get you set up."
58
- echo -e " You'll need credentials from your Unoverse admin."
59
- echo ""
60
- read -r -p " Ready to start setup? [Y/n] " REPLY
61
- echo ""
62
- if [[ ! "$REPLY" =~ ^[Nn]$ ]]; then
63
- cmd_init
64
- else
65
- echo ""
66
- info "Run ${BOLD}unoverse start${NC} when you're ready. It picks setup back up."
67
- echo ""
68
- fi
69
- exit 0
70
- fi
71
- }
72
-
73
- cmd_login() {
74
- # Check if already logged in
75
- local docker_config="$HOME/.docker/config.json"
76
- if [ -f "$docker_config" ]; then
77
- if grep -q "$DOCR_REGISTRY" "$docker_config" 2>/dev/null; then
78
- ok "Already logged in to DOCR"
79
- return
80
- fi
81
- fi
82
-
83
- read -p " DOCR Token: " TOKEN
84
- if echo "$TOKEN" | docker login "$DOCR_REGISTRY" -u "$TOKEN" --password-stdin &>/dev/null; then
85
- ok "Logged in to DOCR"
86
- else
87
- fail "Login failed"
88
- exit 1
89
- fi
90
- }
91
-
92
- cmd_init() {
93
- echo ""
94
- echo -e " ${BOLD}${CYAN}⬡ Unoverse Setup${NC}"
95
- echo -e " ${DIM}─────────────────────────────────${NC}"
96
- echo ""
97
-
98
- # (The studio/platform mode interview was removed 2026-07-28 — Studio is a
99
- # separate app, and this CLI only sets up the platform.)
100
- timer_start
101
-
102
- # DOCKER IS NEEDED TO START, NOT TO CONFIGURE. This used to exit here, which threw
103
- # away a scaffold and a validated token because Docker Desktop happened to be closed.
104
- # Writing .env needs nothing running, so record the state and carry on; the steps that
105
- # genuinely need a daemon skip themselves, and `start` is where it becomes an error.
106
- DOCKER_OK=0
107
- if ! command -v docker &>/dev/null; then
108
- warn "Docker is not installed. Configuration will finish; install it before ${BOLD}unoverse start${NC}"
109
- info "Install: https://docs.docker.com/get-docker/"
110
- elif ! docker info &>/dev/null; then
111
- warn "Docker is not running. Configuration will finish; start Docker Desktop before ${BOLD}unoverse start${NC}"
112
- else
113
- DOCKER_OK=1
114
- ok "Docker is installed and running"
115
- fi
116
-
117
- # Apple Silicon check
118
- if [ "$(uname -m)" = "arm64" ]; then
119
- ok "Apple Silicon detected: multi-arch images will run natively"
120
- echo ""
121
- fi
122
-
123
- # RE-RUNNING EDITS. There is no "overwrite? [y/N]" gate any more: every existing value
124
- # becomes its question's default, so Enter keeps a setting and typing replaces it.
125
- # Walking through changes only what you change — which makes this the way to change
126
- # one env var, not a destructive restart.
127
- _env_cur() { grep "^$1=" "$ROOT/.env" 2>/dev/null | head -1 | cut -d= -f2-; }
128
-
129
- echo ""
130
- echo -e " ${BOLD}Configure your environment:${NC}"
131
- if [ -f "$ROOT/.env" ]; then
132
- echo -e " ${DIM}(Existing .env found. Enter keeps each current value)${NC}"
133
- else
134
- echo -e " ${DIM}(Press Enter to use defaults)${NC}"
135
- fi
136
- echo ""
137
-
138
- # DOCR Token. `unoverse create` has already asked for this and VALIDATED it against
139
- # the registry, so it hands it over rather than making you type the same credential
140
- # twice minutes apart. Typed here only when init is run on its own.
141
- if [ -n "${UNOVERSE_DOCR_TOKEN:-}" ]; then
142
- DOCR_TOKEN="$UNOVERSE_DOCR_TOKEN"
143
- DOCR_USER="${UNOVERSE_DOCR_USER:-$UNOVERSE_DOCR_TOKEN}"
144
- ok "Registry token carried over from create"
145
- else
146
- local cur_token
147
- cur_token=$(_env_cur DOCR_TOKEN)
148
- DOCR_USER=$(_env_cur DOCR_USER)
149
- while true; do
150
- if [ -n "$cur_token" ]; then
151
- read -p " DOCR Token [keep current]: " DOCR_TOKEN || { fail "no input (end of stream). Run unoverse init interactively"; exit 1; }
152
- DOCR_TOKEN="${DOCR_TOKEN:-$cur_token}"
153
- else
154
- read -p " DOCR Token (from your Unoverse admin): " DOCR_TOKEN || { fail "no input (end of stream). Run unoverse init interactively"; exit 1; }
155
- fi
156
- # Accept the credential in any shape, AS A PAIR. The downloaded Docker
157
- # credentials wrap base64("email:token") — the username is the email, and
158
- # logging in token-as-username gets "unauthorized" for exactly that shape.
159
- DOCR_USER=""
160
- if [[ "$DOCR_TOKEN" != dop_v1_* ]]; then
161
- local decoded
162
- decoded=$(printf '%s' "$DOCR_TOKEN" | sed -E 's/.*"auth"[^"]*"([A-Za-z0-9+\/=]+)".*/\1/' | base64 -d 2>/dev/null | tr -d '\0')
163
- [ -n "$decoded" ] || decoded=$(printf '%s' "$DOCR_TOKEN" | base64 -d 2>/dev/null | tr -d '\0')
164
- case "$decoded" in
165
- *:dop_v1_*) DOCR_USER="${decoded%%:*}"; DOCR_TOKEN="dop_v1_${decoded##*dop_v1_}";;
166
- esac
167
- fi
168
- DOCR_USER="${DOCR_USER:-$DOCR_TOKEN}"
169
- if [[ "$DOCR_TOKEN" == dop_v1_* ]]; then
170
- break
171
- fi
172
- fail "That does not look like a registry credential. Paste it exactly as it was sent"
173
- done
174
- fi
175
-
176
- # THE DOWNLOAD STARTS NOW (see the block at the top of this file).
177
- if [ "$DOCKER_OK" = "1" ]; then
178
- local login_err
179
- if login_err=$(echo "$DOCR_TOKEN" | docker login "$DOCR_REGISTRY" -u "${DOCR_USER:-$DOCR_TOKEN}" --password-stdin 2>&1 >/dev/null); then
180
- start_background_pull
181
- echo ""
182
- echo -e " ${CYAN}⬇${NC} ${DIM}Platform images are downloading in the background while you configure${NC}"
183
- echo ""
184
- else
185
- # The REASON, not just the fact: a swallowed docker error left "login failed"
186
- # undiagnosable when create had just validated the same credential.
187
- warn "Registry login failed. Images will not pull until it is fixed"
188
- echo "$login_err" | grep -vi "warning" | head -3 | sed 's/^/ /'
189
- fi
190
- fi
191
-
192
- # A DEFAULT, because "from your admin" is meaningless when you are the admin. The
193
- # platform ships no database (docker-compose has no postgres), so this points at one
194
- # you run. Enter takes the conventional local one.
195
- DB_DEFAULT=$(_env_cur DATABASE_URL)
196
- DB_DEFAULT="${DB_DEFAULT:-postgres://postgres:postgres@localhost:5432/unoverse}"
197
- while true; do
198
- read -p " DATABASE_URL [${DB_DEFAULT}]: " DATABASE_URL || { fail "no input (end of stream). Run unoverse init interactively"; exit 1; }
199
- DATABASE_URL="${DATABASE_URL:-$DB_DEFAULT}"
200
- if [ -n "$DATABASE_URL" ] && [[ "$DATABASE_URL" != *"user:password"* ]]; then
201
- break
202
- fi
203
- fail "DATABASE_URL is required. Get it from your Unoverse admin"
204
- done
205
-
206
- # Auto-add SSL params if missing
207
- if [[ "$DATABASE_URL" != *"sslmode="* ]] && [[ "$DATABASE_URL" != *"ssl="* ]]; then
208
- local sep="?"
209
- [[ "$DATABASE_URL" == *"?"* ]] && sep="&"
210
- if [[ "$DATABASE_URL" == *"localhost"* ]] || \
211
- [[ "$DATABASE_URL" == *"127.0.0.1"* ]] || \
212
- [[ "$DATABASE_URL" == *"host.docker.internal"* ]]; then
213
- DATABASE_URL="${DATABASE_URL}${sep}sslmode=disable"
214
- ok "Local database detected. Added sslmode=disable"
215
- else
216
- DATABASE_URL="${DATABASE_URL}${sep}sslmode=require"
217
- ok "Managed database detected. Added sslmode=require"
218
- fi
219
- fi
220
-
221
- pull_status_line
222
- # Redis, current values as defaults
223
- local rd
224
- rd=$(_env_cur REDIS_HOST); rd="${rd:-host.docker.internal}"
225
- read -p " REDIS_HOST [$rd]: " REDIS_HOST
226
- REDIS_HOST="${REDIS_HOST:-$rd}"
227
-
228
- rd=$(_env_cur REDIS_PORT); rd="${rd:-6379}"
229
- read -p " REDIS_PORT [$rd]: " REDIS_PORT
230
- REDIS_PORT="${REDIS_PORT:-$rd}"
231
-
232
- rd=$(_env_cur REDIS_PASSWORD)
233
- if [ -n "$rd" ]; then
234
- read -p " REDIS_PASSWORD [keep current]: " REDIS_PASSWORD
235
- REDIS_PASSWORD="${REDIS_PASSWORD:-$rd}"
236
- else
237
- read -p " REDIS_PASSWORD (blank for none): " REDIS_PASSWORD
238
- fi
239
-
240
- rd=$(_env_cur REDIS_TLS); rd="${rd:-false}"
241
- read -p " REDIS_TLS [$rd]: " REDIS_TLS
242
- REDIS_TLS="${REDIS_TLS:-$rd}"
243
-
244
- # Auth (required — from admin)
245
- # ASK BEFORE DEMANDING. A developer trying the platform locally has no identity
246
- # provider yet, and AUTH_ISSUER was required, so init could not be completed at all.
247
- #
248
- # This is a LOCAL switch only. INFRASTRUCTURE.md is explicit that auth is always on for
249
- # a deployed universe ("there is no auth-off deployment"), and the platform enforces it
250
- # rather than trusting this wizard: authConfig.ts refuses to start with auth off when
251
- # NODE_ENV=production. So answering "no" here cannot produce an unprotected deployment.
252
- pull_status_line
253
- local cur_auth idp_prompt
254
- cur_auth=$(_env_cur AUTH_ENABLED)
255
- idp_prompt="[y/N]"
256
- [ "$cur_auth" = "true" ] && idp_prompt="[Y/n]"
257
- read -r -p " Do you have an identity provider (Auth0/OIDC) to connect? $idp_prompt " HAS_IDP
258
- echo ""
259
- if [ -z "$HAS_IDP" ] && [ "$cur_auth" = "true" ]; then HAS_IDP=y; fi
260
-
261
- if [[ "$HAS_IDP" =~ ^[Yy]$ ]]; then
262
- AUTH_ENABLED=true
263
- local cur_iss cur_cid cur_aud
264
- cur_iss=$(_env_cur AUTH_ISSUER)
265
- cur_cid=$(_env_cur AUTH_CLIENT_ID)
266
- cur_aud=$(_env_cur AUTH_AUDIENCE)
267
-
268
- while true; do
269
- if [ -n "$cur_iss" ]; then
270
- read -p " AUTH_ISSUER [$cur_iss]: " AUTH_ISSUER || { fail "no input (end of stream). Run unoverse init interactively"; exit 1; }
271
- AUTH_ISSUER="${AUTH_ISSUER:-$cur_iss}"
272
- else
273
- read -p " AUTH_ISSUER (e.g. https://your-tenant.auth0.com): " AUTH_ISSUER || { fail "no input (end of stream). Run unoverse init interactively"; exit 1; }
274
- fi
275
- if [ -n "$AUTH_ISSUER" ] && [[ "$AUTH_ISSUER" == https://* ]]; then
276
- break
277
- fi
278
- fail "AUTH_ISSUER must be an https:// URL from your identity provider"
279
- done
280
-
281
- while true; do
282
- if [ -n "$cur_cid" ]; then
283
- read -p " AUTH_CLIENT_ID [$cur_cid]: " AUTH_CLIENT_ID || { fail "no input (end of stream). Run unoverse init interactively"; exit 1; }
284
- AUTH_CLIENT_ID="${AUTH_CLIENT_ID:-$cur_cid}"
285
- else
286
- read -p " AUTH_CLIENT_ID: " AUTH_CLIENT_ID || { fail "no input (end of stream). Run unoverse init interactively"; exit 1; }
287
- fi
288
- if [ -n "$AUTH_CLIENT_ID" ] && [[ "$AUTH_CLIENT_ID" != *"your-"* ]]; then
289
- break
290
- fi
291
- fail "AUTH_CLIENT_ID is required"
292
- done
293
-
294
- cur_aud="${cur_aud:-gravity-api}"
295
- read -p " AUTH_AUDIENCE [$cur_aud]: " AUTH_AUDIENCE
296
- AUTH_AUDIENCE="${AUTH_AUDIENCE:-$cur_aud}"
297
- else
298
- AUTH_ENABLED=false
299
- AUTH_ISSUER=""
300
- AUTH_CLIENT_ID=""
301
- AUTH_AUDIENCE="gravity-api"
302
- warn "Auth is OFF for local development"
303
- info "Deploying needs it: ${BOLD}unoverse deploy${NC} runs with auth on, and the server refuses to start without it in production"
304
- fi
305
-
306
- # NOT ASKED. 4105 is the port docker-compose publishes, so locally this is a fact the
307
- # CLI already knows, and confirming it is not a question. A DEPLOYED universe gets its
308
- # own API_URL rendered into .env.production by Terraform, which never comes through here.
309
- API_URL="http://localhost:4105"
310
-
311
- # DERIVED FROM THE FOLDER, not hardcoded. Every key the memory server writes is
312
- # prefixed with this, so two universes sharing one Redis and the same namespace mix
313
- # their streams and caches. It used to be written as a literal `gravity` for every
314
- # local universe, while Terraform rendered `universe` for deployed ones: same Redis,
315
- # same prefix, silently interleaved.
316
- REDIS_NAMESPACE=$(basename "$ROOT" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9-' '-' | sed 's/-*$//')
317
- REDIS_NAMESPACE="${REDIS_NAMESPACE:-universe}"
318
- ok "Redis namespace: ${BOLD}${REDIS_NAMESPACE}${NC}"
319
-
320
- # OpenAI (for Memory Server)
321
- pull_status_line
322
- local cur_oai
323
- cur_oai=$(_env_cur OPENAI_API_KEY)
324
- # NOT just the memory server: compose hands this to the unoverse service itself —
325
- # agents, embeddings and memory all run on it. A universe boots without it, but its AI
326
- # does not, so skipping gets a warning rather than silence.
327
- if [ -n "$cur_oai" ]; then
328
- read -p " OPENAI_API_KEY [keep current]: " OPENAI_API_KEY
329
- OPENAI_API_KEY="${OPENAI_API_KEY:-$cur_oai}"
330
- else
331
- read -p " OPENAI_API_KEY (powers the platform's AI): " OPENAI_API_KEY
332
- [ -z "$OPENAI_API_KEY" ] && warn "No OpenAI key: the universe starts, but agents, embeddings and memory will not work until one is in .env"
333
- fi
334
-
335
- # Node vendor keys, OPTIONAL: only the matching nodes need them, nothing platform-level
336
- # does. Asked so compose stops warning about them and so they land in .env with names.
337
- local cur_hb
338
- cur_hb=$(_env_cur HYPERBROWSER_API_KEY)
339
- if [ -n "$cur_hb" ]; then
340
- read -p " HYPERBROWSER_API_KEY [keep current]: " HYPERBROWSER_API_KEY
341
- HYPERBROWSER_API_KEY="${HYPERBROWSER_API_KEY:-$cur_hb}"
342
- else
343
- read -p " HYPERBROWSER_API_KEY (browser nodes, blank to skip): " HYPERBROWSER_API_KEY
344
- fi
345
-
346
-
347
- # A silent fact, not a question: the encryption key is GENERATED (a human never types
348
- # one) and kept verbatim on re-runs — a new key orphans every stored credential.
349
- CREDENTIAL_ENCRYPTION_KEY=$(_env_cur CREDENTIAL_ENCRYPTION_KEY)
350
- if [ -z "$CREDENTIAL_ENCRYPTION_KEY" ]; then
351
- CREDENTIAL_ENCRYPTION_KEY=$(openssl rand -hex 32 2>/dev/null || head -c32 /dev/urandom | xxd -p -c64)
352
- ok "Credential encryption key generated"
353
- fi
354
- # Write .env
355
- cat > "$ROOT/.env" << ENVEOF
356
- # Generated by unoverse init
357
- DOCR_TOKEN=${DOCR_TOKEN}
358
- DOCR_USER=${DOCR_USER:-${DOCR_TOKEN}}
359
- DATABASE_URL=${DATABASE_URL}
360
- REDIS_HOST=${REDIS_HOST}
361
- REDIS_PORT=${REDIS_PORT}
362
- REDIS_PASSWORD=${REDIS_PASSWORD}
363
- REDIS_TLS=${REDIS_TLS}
364
- REDIS_NAMESPACE=${REDIS_NAMESPACE}
365
- AUTH_ENABLED=${AUTH_ENABLED}
366
- AUTH_ISSUER=${AUTH_ISSUER}
367
- AUTH_CLIENT_ID=${AUTH_CLIENT_ID}
368
- AUTH_AUDIENCE=${AUTH_AUDIENCE}
369
- API_URL=${API_URL}
370
- OPENAI_API_KEY=${OPENAI_API_KEY}
371
- # Generated at setup: encrypts credentials stored by nodes. Losing it orphans them —
372
- # back it up with the database. Kept on re-runs.
373
- CREDENTIAL_ENCRYPTION_KEY=${CREDENTIAL_ENCRYPTION_KEY}
374
- HYPERBROWSER_API_KEY=${HYPERBROWSER_API_KEY}
375
- DOMAIN=
376
- ENVEOF
377
-
378
- ok ".env created"
379
-
380
- # Attach to the background pull started when the token was accepted: layers already
381
- # down show as complete, the rest stream docker's progress bars. Without a daemon the
382
- # token is in .env, and `unoverse start` pulls on its first run.
383
- if [ "$DOCKER_OK" = "1" ]; then
384
- pull_missing_images
385
- [ -n "$PULL_STATE" ] && rm -f "$PULL_STATE"
386
- else
387
- echo ""
388
- info "Skipped the image download. ${BOLD}unoverse start${NC} does it once Docker is up"
389
- fi
390
-
391
- # Install to PATH
392
-
393
- # Done
394
- echo ""
395
- echo -e " ${GREEN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
396
- echo -e " ${GREEN}${BOLD} ✓ Setup Complete!${NC} ${DIM}($(timer_elapsed))${NC}"
397
- echo -e " ${GREEN}${BOLD}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}"
398
- # MIGRATIONS RUN HERE, so `db-setup` is not a command anyone has to know about. Deploy
399
- # already ran them on the server (playbooks/db-setup.yml); this is the local half.
400
- # A database that is not reachable yet is not a failed setup — .env is written and
401
- # correct, so say so and move on rather than unwinding everything.
402
- echo ""
403
- # SUBSHELL, deliberately: cmd_db_setup exits on "services not up yet", and an exit in a
404
- # sourced function would take all of init with it — setup then reported failure for a
405
- # situation that just means "migrations run on start". Which they now do (start.sh).
406
- if grep -q '^DATABASE_URL=' "$ROOT/.env" 2>/dev/null; then
407
- # Quietly: db-setup narrates its own advice ("start services first, then re-run"),
408
- # which contradicts and duplicates the one line that is true here.
409
- if (cmd_db_setup) >/dev/null 2>&1; then
410
- ok "Database schema is up to date"
411
- else
412
- info "Migrations wait for the platform: ${BOLD}unoverse start${NC} applies them once services are up"
413
- fi
414
- fi
415
-
416
- echo ""
417
- echo -e " ${BOLD}Next steps:${NC}"
418
- echo ""
419
- if [ "$DOCKER_OK" = "1" ]; then
420
- echo -e " ${GREEN}unoverse start${NC} Start the platform"
421
- else
422
- echo -e " ${DIM}1.${NC} Start Docker Desktop"
423
- echo -e " ${DIM}2.${NC} ${GREEN}unoverse start${NC}"
424
- fi
425
- echo -e " ${GREEN}unoverse where${NC} Links to your Canvas and API"
426
- echo ""
427
- info "Run ${BOLD}unoverse check${NC} anytime to see if it is healthy"
428
- info "Change a setting any time: edit ${BOLD}.env${NC} directly, or re-run ${BOLD}unoverse create${NC} (Enter keeps each current value)"
429
- echo ""
430
- }