unoverse 0.1.92 → 0.1.94
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 +1 -1
- package/operator/lib/deploy.sh +64 -11
- package/operator/lib/destroy.sh +10 -7
- package/operator/lib/doctor.sh +2 -2
- package/operator/lib/help.sh +1 -0
- package/operator/lib/start.sh +15 -6
- package/operator/operator.sh +2 -4
- package/package.json +1 -1
- package/operator/lib/init.sh +0 -430
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",
|
|
63
|
+
"ground", "dev", "build", "publish",
|
|
64
64
|
]);
|
|
65
65
|
|
|
66
66
|
const [, , cmd, ...args] = process.argv;
|
package/operator/lib/deploy.sh
CHANGED
|
@@ -102,14 +102,11 @@ NODE
|
|
|
102
102
|
# password afterwards. This opens a door in the network layer; it does not open the
|
|
103
103
|
# database.
|
|
104
104
|
cmd_db_allow() {
|
|
105
|
-
local cloud
|
|
106
|
-
|
|
107
|
-
[ -
|
|
108
|
-
done
|
|
109
|
-
if [ -z "$cloud" ]; then
|
|
110
|
-
fail "No ground here. There is no database to reach"
|
|
105
|
+
local cloud
|
|
106
|
+
SELF_CMD="unoverse db-allow" cloud=$(_pick_ground "${1:-}") || {
|
|
107
|
+
[ -z "${1:-}" ] && [ ! -d "$ROOT/infra" ] && fail "No ground here. There is no database to reach"
|
|
111
108
|
return 1
|
|
112
|
-
|
|
109
|
+
}
|
|
113
110
|
|
|
114
111
|
local cluster
|
|
115
112
|
cluster=$(grep -E '^existing_pg_cluster_name[[:space:]]*=' "$ROOT/infra/$cloud/terraform.tfvars" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
|
|
@@ -188,6 +185,51 @@ NODE
|
|
|
188
185
|
# `unoverse destroy` ran with no credential at all and every API call came back 401 —
|
|
189
186
|
# the same class of bug as the Postgres question living in one branch of two. Anything
|
|
190
187
|
# that talks to a ground calls this first.
|
|
188
|
+
# WHICH GROUND THIS COMMAND MEANS.
|
|
189
|
+
# _pick_ground [name] → echoes the ground, or fails with a reason
|
|
190
|
+
#
|
|
191
|
+
# Every command used `for g in digitalocean aws; do ... break; done`: first match wins, and
|
|
192
|
+
# digitalocean is first in the list. With both grounds configured there was no way to reach
|
|
193
|
+
# the AWS one — and `unoverse destroy` would have torn down DigitalOcean while the operator
|
|
194
|
+
# meant AWS, silently, because the word "aws" had nowhere to go.
|
|
195
|
+
#
|
|
196
|
+
# So a ground can be named. With one configured, naming it is optional and the answer is
|
|
197
|
+
# obvious. With two, the name is REQUIRED rather than guessed: a wrong guess here destroys
|
|
198
|
+
# the wrong cloud.
|
|
199
|
+
_pick_ground() {
|
|
200
|
+
local want="${1:-}" g found=""
|
|
201
|
+
case "$want" in
|
|
202
|
+
do|digitalocean) want="digitalocean" ;;
|
|
203
|
+
aws|amazon) want="aws" ;;
|
|
204
|
+
"") want="" ;;
|
|
205
|
+
*) fail "Unknown ground '$want'. Use ${BOLD}digitalocean${NC} or ${BOLD}aws${NC}" >&2; return 1 ;;
|
|
206
|
+
esac
|
|
207
|
+
|
|
208
|
+
local configured=()
|
|
209
|
+
for g in digitalocean aws; do
|
|
210
|
+
[ -f "$ROOT/infra/$g/terraform.tfvars" ] && configured+=("$g")
|
|
211
|
+
done
|
|
212
|
+
|
|
213
|
+
if [ -n "$want" ]; then
|
|
214
|
+
for g in "${configured[@]}"; do
|
|
215
|
+
[ "$g" = "$want" ] && { printf '%s' "$want"; return 0; }
|
|
216
|
+
done
|
|
217
|
+
fail "No $want ground here. Run ${BOLD}unoverse deploy $want${NC} to create one" >&2
|
|
218
|
+
return 1
|
|
219
|
+
fi
|
|
220
|
+
|
|
221
|
+
case "${#configured[@]}" in
|
|
222
|
+
0) return 1 ;; # caller offers to create one
|
|
223
|
+
1) printf '%s' "${configured[0]}"; return 0 ;;
|
|
224
|
+
*)
|
|
225
|
+
# Two grounds and no name. Refuse rather than pick: this decides which cloud a
|
|
226
|
+
# destroy lands on.
|
|
227
|
+
fail "Two grounds are configured. Say which: ${BOLD}${SELF_CMD:-unoverse deploy} digitalocean${NC} or ${BOLD}${SELF_CMD:-unoverse deploy} aws${NC}" >&2
|
|
228
|
+
return 1
|
|
229
|
+
;;
|
|
230
|
+
esac
|
|
231
|
+
}
|
|
232
|
+
|
|
191
233
|
_ground_credentials() {
|
|
192
234
|
[ -n "${DIGITALOCEAN_TOKEN:-}" ] && return 0
|
|
193
235
|
local _docfg
|
|
@@ -472,6 +514,13 @@ _ground_apply() {
|
|
|
472
514
|
}
|
|
473
515
|
|
|
474
516
|
cmd_deploy() {
|
|
517
|
+
# A LEADING GROUND NAME IS NOT A SUBCOMMAND. `unoverse deploy aws` names the cloud;
|
|
518
|
+
# `unoverse deploy init` names the step; `unoverse deploy aws init` does both. Take the
|
|
519
|
+
# ground off the front when it is there, and leave everything else exactly as it was.
|
|
520
|
+
local GROUND_ARG=""
|
|
521
|
+
case "${1:-}" in
|
|
522
|
+
do|digitalocean|aws|amazon) GROUND_ARG="$1"; shift ;;
|
|
523
|
+
esac
|
|
475
524
|
|
|
476
525
|
# .env.production IS NOT A FILE IN THE UNIVERSE. It is `terraform output -raw
|
|
477
526
|
# env_production`: derived, complete, and regenerated in full on every deploy. Writing it
|
|
@@ -492,10 +541,14 @@ cmd_deploy() {
|
|
|
492
541
|
# ── ONE FLOW ────────────────────────────────────────────────────────────────
|
|
493
542
|
# Which ground, is it configured, plan it, apply it, ship it. In that order, every
|
|
494
543
|
# time, whether this is the first deploy or the fiftieth.
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
544
|
+
# A named ground wins; one configured ground is implied; two without a name is refused
|
|
545
|
+
# (see _pick_ground). Nothing configured at all falls through to creating one below,
|
|
546
|
+
# which is the first-run path and the only case where an empty answer is not a mistake.
|
|
547
|
+
local cloud="" rc have=0
|
|
548
|
+
[ -f "$ROOT/infra/digitalocean/terraform.tfvars" ] || [ -f "$ROOT/infra/aws/terraform.tfvars" ] && have=1
|
|
549
|
+
if [ "$have" = "1" ]; then
|
|
550
|
+
cloud=$(SELF_CMD="unoverse deploy" _pick_ground "${GROUND_ARG:-}") || exit 1
|
|
551
|
+
fi
|
|
499
552
|
|
|
500
553
|
if [ -z "$cloud" ]; then
|
|
501
554
|
echo ""
|
package/operator/lib/destroy.sh
CHANGED
|
@@ -19,14 +19,17 @@
|
|
|
19
19
|
# cloud resources this ground owns.
|
|
20
20
|
|
|
21
21
|
cmd_destroy() {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
22
|
+
# NAME THE GROUND. This took the first configured one, and digitalocean is first in the
|
|
23
|
+
# list — so an operator with both grounds who typed `unoverse destroy` meaning AWS would
|
|
24
|
+
# have torn down DigitalOcean instead, with the confirmation prompt happily naming the
|
|
25
|
+
# universe they thought they were destroying. _pick_ground refuses to guess when there is
|
|
26
|
+
# more than one.
|
|
27
|
+
local cloud
|
|
28
|
+
cloud=$(SELF_CMD="unoverse destroy" _pick_ground "${1:-}") || {
|
|
29
|
+
[ -f "$ROOT/infra/digitalocean/terraform.tfvars" ] || [ -f "$ROOT/infra/aws/terraform.tfvars" ] \
|
|
30
|
+
|| fail "No ground here. There is nothing deployed to take down"
|
|
28
31
|
return 1
|
|
29
|
-
|
|
32
|
+
}
|
|
30
33
|
|
|
31
34
|
local dir="$ROOT/infra/$cloud"
|
|
32
35
|
if [ ! -d "$dir/.terraform" ]; then
|
package/operator/lib/doctor.sh
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
package/operator/lib/help.sh
CHANGED
|
@@ -24,6 +24,7 @@ cmd_help() {
|
|
|
24
24
|
echo -e " ${GREEN}logs${NC} What is it doing ${DIM}(unoverse logs <service> for one)${NC}"
|
|
25
25
|
echo -e " ${GREEN}deploy${NC} Ship it to a server ${DIM}(first run asks which cloud)${NC}"
|
|
26
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}"
|
|
27
28
|
echo -e " ${GREEN}db-allow${NC} Let this machine reach the database ${DIM}(run it when you change network)${NC}"
|
|
28
29
|
echo ""
|
|
29
30
|
# Owner-only lane. Printed ONLY when publish.sh is present, so a starter kit never
|
package/operator/lib/start.sh
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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}
|
|
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
|
|
171
|
-
#
|
|
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}
|
|
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"
|
package/operator/operator.sh
CHANGED
|
@@ -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:-}" ;;
|
|
@@ -65,8 +63,8 @@ case "${1:-}" in
|
|
|
65
63
|
check) cmd_check && cmd_db_verify && cmd_doctor ;;
|
|
66
64
|
dev) cmd_dev ;;
|
|
67
65
|
ground) shift; cmd_ground "$@" ;;
|
|
68
|
-
destroy) cmd_destroy ;;
|
|
69
|
-
db-allow) cmd_db_allow ;;
|
|
66
|
+
destroy) cmd_destroy "${2:-}" ;;
|
|
67
|
+
db-allow) cmd_db_allow "${2:-}" ;;
|
|
70
68
|
refresh-images)
|
|
71
69
|
# internal: `unoverse update` runs this after updating the CLI. Pull newer images
|
|
72
70
|
# if the registry has them, and recreate only what is already running — an update
|
package/package.json
CHANGED
package/operator/lib/init.sh
DELETED
|
@@ -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
|
-
}
|