unoverse 0.1.72 → 0.1.74
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
|
@@ -58,7 +58,7 @@ const UNIVERSE = findUniverse();
|
|
|
58
58
|
// type it to update the CLI far more often than to refresh a local universe's images,
|
|
59
59
|
// and that refresh now belongs to `start --pull`.
|
|
60
60
|
const OPERATOR_COMMANDS = new Set([
|
|
61
|
-
"start", "stop", "check", "logs", "deploy",
|
|
61
|
+
"start", "stop", "check", "logs", "deploy", "destroy",
|
|
62
62
|
// kept working, not advertised
|
|
63
63
|
"ground", "dev", "build", "publish", "init",
|
|
64
64
|
]);
|
|
@@ -270,12 +270,18 @@ resource "digitalocean_database_firewall" "pg" {
|
|
|
270
270
|
}
|
|
271
271
|
}
|
|
272
272
|
|
|
273
|
-
# ──
|
|
274
|
-
#
|
|
273
|
+
# ── Valkey: ALWAYS ours (managed, TLS, droplet-only). Never BYO — it is the
|
|
274
|
+
# shared-state backbone and the active/active future; the stack owns it.
|
|
275
|
+
#
|
|
276
|
+
# VALKEY, NOT REDIS. DigitalOcean retired the `redis` engine: /v2/databases/options now
|
|
277
|
+
# lists valkey and no redis at all. Asking for a dead engine failed with "region 'lon1'
|
|
278
|
+
# is not valid", because an unknown engine matches no region — an error that sent us
|
|
279
|
+
# looking at the region for as long as it took to ask the API what engines exist.
|
|
280
|
+
# Valkey speaks the Redis protocol, so every client here is unchanged. ──
|
|
275
281
|
resource "digitalocean_database_cluster" "redis" {
|
|
276
282
|
name = "${var.name}-redis"
|
|
277
|
-
engine = "
|
|
278
|
-
version = "
|
|
283
|
+
engine = "valkey"
|
|
284
|
+
version = "8"
|
|
279
285
|
size = local.s.redis
|
|
280
286
|
region = var.region
|
|
281
287
|
node_count = 1
|
|
@@ -295,3 +301,25 @@ resource "random_password" "credential_key" {
|
|
|
295
301
|
length = 44
|
|
296
302
|
special = false
|
|
297
303
|
}
|
|
304
|
+
|
|
305
|
+
# ── One project, so a universe looks like one thing ───────────────────────────
|
|
306
|
+
#
|
|
307
|
+
# Without this, a universe's parts scatter through the DigitalOcean default project among
|
|
308
|
+
# everything else the account runs, and "what does this universe own" has no answer you
|
|
309
|
+
# can see. The project is named after the universe, so the console shows the server, the
|
|
310
|
+
# cache and the load balancer as one group.
|
|
311
|
+
#
|
|
312
|
+
# ONLY WHAT THIS STACK OWNS. An adopted Postgres belongs to whoever created it; moving it
|
|
313
|
+
# into this project would quietly reorganise someone else's resource.
|
|
314
|
+
resource "digitalocean_project" "universe" {
|
|
315
|
+
name = var.name
|
|
316
|
+
description = "unoverse universe: ${var.name}"
|
|
317
|
+
purpose = "Web Application"
|
|
318
|
+
environment = var.size == "small" ? "Development" : "Production"
|
|
319
|
+
resources = compact([
|
|
320
|
+
digitalocean_droplet.app.urn,
|
|
321
|
+
digitalocean_loadbalancer.public.urn,
|
|
322
|
+
digitalocean_database_cluster.redis.urn,
|
|
323
|
+
local.provision_pg ? digitalocean_database_cluster.pg[0].urn : "",
|
|
324
|
+
])
|
|
325
|
+
}
|
package/operator/lib/deploy.sh
CHANGED
|
@@ -162,12 +162,15 @@ _ground_apply() {
|
|
|
162
162
|
|
|
163
163
|
# `read -p` does NOT interpret escapes, so ${DIM} printed as literal \033[2m. Colour
|
|
164
164
|
# the hint with echo -e first, then take a plain prompt.
|
|
165
|
+
# EXPLICIT YES, and it says so. Everywhere else in this CLI Enter means "go"; here it
|
|
166
|
+
# means "stop", because this is the one prompt that starts a monthly bill. Keeping the
|
|
167
|
+
# safety is right — leaving it as a bare [y/N] among Enter-means-go prompts is not.
|
|
165
168
|
local REPLY
|
|
166
|
-
echo -e " ${DIM}
|
|
167
|
-
read -r -p "
|
|
169
|
+
echo -e " ${DIM}d shows the full technical plan${NC}"
|
|
170
|
+
read -r -p " Type y to build this, or Enter to stop: " REPLY
|
|
168
171
|
if [[ "$REPLY" =~ ^[Dd]$ ]]; then
|
|
169
172
|
terraform -chdir="$dir" show "$planfile"
|
|
170
|
-
read -r -p "
|
|
173
|
+
read -r -p " Type y to build this, or Enter to stop: " REPLY
|
|
171
174
|
fi
|
|
172
175
|
if [[ ! "$REPLY" =~ ^[Yy]$ ]]; then
|
|
173
176
|
info "Nothing was created. Run ${BOLD}unoverse deploy${NC} when you are ready"
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# unoverse destroy — take the whole deployment down, in one command.
|
|
3
|
+
#
|
|
4
|
+
# The counterpart to `deploy`, and the same shape: read the ground, show in plain English
|
|
5
|
+
# what will go, take ONE answer, then do exactly what was shown. It exists because the
|
|
6
|
+
# alternative is remembering `terraform destroy` in the right folder with the right
|
|
7
|
+
# environment, and because half a teardown is worse than none — a load balancer nobody
|
|
8
|
+
# knows about bills forever.
|
|
9
|
+
#
|
|
10
|
+
# THREE THINGS MAKE THIS SAFE.
|
|
11
|
+
# 1. It shows what exists before it asks anything.
|
|
12
|
+
# 2. It names what it will NOT remove: an adopted Postgres is somebody else's cluster,
|
|
13
|
+
# read by this stack and never owned, so it survives. Saying so prevents both a
|
|
14
|
+
# nasty surprise and a false sense that the bill is now zero.
|
|
15
|
+
# 3. Confirmation is the universe's NAME, typed. y/N is muscle memory; a name is a
|
|
16
|
+
# decision, and this deletes data that no backup here can restore.
|
|
17
|
+
#
|
|
18
|
+
# It never touches the developer's terraform.tfvars, their .env, or their code. Only the
|
|
19
|
+
# cloud resources this ground owns.
|
|
20
|
+
|
|
21
|
+
cmd_destroy() {
|
|
22
|
+
local cloud="" g
|
|
23
|
+
for g in digitalocean aws; do
|
|
24
|
+
[ -f "$ROOT/infra/$g/terraform.tfvars" ] && { cloud="$g"; break; }
|
|
25
|
+
done
|
|
26
|
+
if [ -z "$cloud" ]; then
|
|
27
|
+
fail "No ground here. There is nothing deployed to take down"
|
|
28
|
+
return 1
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
local dir="$ROOT/infra/$cloud"
|
|
32
|
+
if [ ! -d "$dir/.terraform" ]; then
|
|
33
|
+
fail "That ground was never applied. Nothing exists to destroy"
|
|
34
|
+
return 1
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
_ensure_terraform || return 1
|
|
38
|
+
|
|
39
|
+
local pretty
|
|
40
|
+
[ "$cloud" = "aws" ] && pretty="AWS" || pretty="DigitalOcean"
|
|
41
|
+
echo ""
|
|
42
|
+
echo -e " ${RED}${BOLD}⬡ Taking down your $pretty universe${NC}"
|
|
43
|
+
echo ""
|
|
44
|
+
|
|
45
|
+
local tmp planfile rc
|
|
46
|
+
tmp=$(mktemp -d); planfile="$tmp/plan"
|
|
47
|
+
info "Working out what exists..."
|
|
48
|
+
terraform -chdir="$dir" plan -destroy -input=false -detailed-exitcode -out="$planfile" >"$tmp/log" 2>&1
|
|
49
|
+
rc=$?
|
|
50
|
+
case "$rc" in
|
|
51
|
+
0) ok "Nothing is deployed. There is nothing to take down"; rm -rf "$tmp"; return 0 ;;
|
|
52
|
+
2) : ;;
|
|
53
|
+
*) fail "Terraform could not read the deployment:"; tail -20 "$tmp/log" | sed 's/^/ /'; rm -rf "$tmp"; return 1 ;;
|
|
54
|
+
esac
|
|
55
|
+
|
|
56
|
+
if ! terraform -chdir="$dir" show -json "$planfile" 2>/dev/null | node "$GRAVITY_LIB/tfsummary.mjs"; then
|
|
57
|
+
terraform -chdir="$dir" show "$planfile"
|
|
58
|
+
fi
|
|
59
|
+
|
|
60
|
+
# What SURVIVES matters as much as what goes: an adopted cluster is read, never owned.
|
|
61
|
+
local kept
|
|
62
|
+
kept=$(grep -E '^existing_pg_cluster_name[[:space:]]*=' "$dir/terraform.tfvars" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
|
|
63
|
+
if [ -n "$kept" ]; then
|
|
64
|
+
info "Staying: ${BOLD}$kept${NC} ${DIM}(you reused it, so this stack never owned it. Its bill continues)${NC}"
|
|
65
|
+
fi
|
|
66
|
+
if grep -qE '^byo_postgres_url[[:space:]]*=' "$dir/terraform.tfvars" 2>/dev/null; then
|
|
67
|
+
info "Staying: ${BOLD}your own database${NC} ${DIM}(byo_postgres_url — this stack never owned it)${NC}"
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
local name
|
|
71
|
+
name=$(grep -E '^name[[:space:]]*=' "$dir/terraform.tfvars" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
|
|
72
|
+
name="${name:-universe}"
|
|
73
|
+
|
|
74
|
+
echo ""
|
|
75
|
+
echo -e " ${RED}This deletes the server and its data. It cannot be undone.${NC}"
|
|
76
|
+
echo ""
|
|
77
|
+
local typed
|
|
78
|
+
read -r -p " Type the universe name to confirm ($name): " typed
|
|
79
|
+
if [ "$typed" != "$name" ]; then
|
|
80
|
+
info "Name did not match. Nothing was destroyed"
|
|
81
|
+
rm -rf "$tmp"
|
|
82
|
+
return 1
|
|
83
|
+
fi
|
|
84
|
+
|
|
85
|
+
echo ""
|
|
86
|
+
info "Taking it down..."
|
|
87
|
+
echo ""
|
|
88
|
+
terraform -chdir="$dir" apply -input=false "$planfile" || { fail "Teardown did not finish. Re-run: unoverse destroy"; rm -rf "$tmp"; return 1; }
|
|
89
|
+
rm -rf "$tmp"
|
|
90
|
+
|
|
91
|
+
# .env.production described a server that no longer exists. Leaving it makes the next
|
|
92
|
+
# deploy think there is something to ship to.
|
|
93
|
+
rm -f "$ROOT/.env.production"
|
|
94
|
+
|
|
95
|
+
echo ""
|
|
96
|
+
ok "Your $pretty universe is gone"
|
|
97
|
+
info "Your ground, code and settings are untouched. ${BOLD}unoverse deploy${NC} builds it again"
|
|
98
|
+
echo ""
|
|
99
|
+
}
|
package/operator/lib/help.sh
CHANGED
|
@@ -23,6 +23,7 @@ cmd_help() {
|
|
|
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
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}"
|
|
26
27
|
echo ""
|
|
27
28
|
# Owner-only lane. Printed ONLY when publish.sh is present, so a starter kit never
|
|
28
29
|
# advertises a command it does not have (sync-starter.sh deletes that file).
|
|
@@ -52,10 +52,17 @@ function describe(r) {
|
|
|
52
52
|
switch (r.type) {
|
|
53
53
|
case "digitalocean_droplet":
|
|
54
54
|
return { what: "Server", detail: `${spec(a.size)} · ${a.region ?? ""}`, price: PRICE[a.size] };
|
|
55
|
-
case "digitalocean_database_cluster":
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
55
|
+
case "digitalocean_database_cluster": {
|
|
56
|
+
// MATCH THE ENGINE, don't assume two. DigitalOcean renamed redis to valkey, and a
|
|
57
|
+
// lone `=== "redis"` test quietly relabelled the cache as "PostgreSQL database" —
|
|
58
|
+
// the summary claiming one thing while the plan built another.
|
|
59
|
+
const cache = a.engine === "valkey" || a.engine === "redis";
|
|
60
|
+
return {
|
|
61
|
+
what: cache ? "Cache" : "PostgreSQL database",
|
|
62
|
+
detail: `${spec(a.size)} · ${a.engine ?? ""} v${a.version ?? ""}`,
|
|
63
|
+
price: PRICE[a.size],
|
|
64
|
+
};
|
|
65
|
+
}
|
|
59
66
|
case "digitalocean_loadbalancer":
|
|
60
67
|
return { what: "Load balancer", detail: "the public way in", price: PRICE.lb };
|
|
61
68
|
case "digitalocean_firewall":
|
package/operator/operator.sh
CHANGED
|
@@ -44,6 +44,7 @@ source "$GRAVITY_LIB/help.sh"
|
|
|
44
44
|
source "$GRAVITY_LIB/db-setup.sh"
|
|
45
45
|
source "$GRAVITY_LIB/db-verify.sh"
|
|
46
46
|
source "$GRAVITY_LIB/deploy.sh"
|
|
47
|
+
source "$GRAVITY_LIB/destroy.sh"
|
|
47
48
|
source "$GRAVITY_LIB/ground.sh"
|
|
48
49
|
# Authoring tools (lint, new, node test/hash, studio) moved to _legacy/scripts-lib
|
|
49
50
|
# 2026-07-28 — authoring lives in Studio now. The CLI is the OPERATOR tool.
|
|
@@ -64,6 +65,7 @@ case "${1:-}" in
|
|
|
64
65
|
check) cmd_check && cmd_db_verify && cmd_doctor ;;
|
|
65
66
|
dev) cmd_dev ;;
|
|
66
67
|
ground) shift; cmd_ground "$@" ;;
|
|
68
|
+
destroy) cmd_destroy ;;
|
|
67
69
|
refresh-images)
|
|
68
70
|
# internal: `unoverse update` runs this after updating the CLI. Pull newer images
|
|
69
71
|
# if the registry has them, and recreate only what is already running — an update
|
package/package.json
CHANGED