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.
- package/lib/create/index.mjs +19 -8
- package/operator/lib/db-verify.sh +7 -1
- package/operator/lib/setup.sh +389 -0
- package/operator/operator.sh +7 -0
- package/package.json +1 -1
package/lib/create/index.mjs
CHANGED
|
@@ -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
|
-
|
|
46
|
-
const
|
|
47
|
-
const
|
|
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
|
|
96
|
-
const operatorScript = existsSync(
|
|
97
|
-
?
|
|
98
|
-
: resolve(here, "
|
|
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, "
|
|
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
|
+
}
|
package/operator/operator.sh
CHANGED
|
@@ -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