unoverse 0.1.89 → 0.1.91

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.
@@ -381,9 +381,15 @@ resource "digitalocean_project" "universe" {
381
381
  description = "unoverse universe: ${var.name}"
382
382
  purpose = "Web Application"
383
383
  environment = var.size == "small" ? "Development" : "Production"
384
+ # EVERY resource this stack creates, or the dashboard lies. The Canvas load balancer was
385
+ # added without being listed here, so it stayed in the account's default project while
386
+ # the rest of the universe sat in this one: two views each reporting "LOAD BALANCERS (1)"
387
+ # and no single place showing what this universe actually is. Anything added below must
388
+ # be added here in the same commit.
384
389
  resources = compact([
385
390
  digitalocean_droplet.app.urn,
386
391
  digitalocean_loadbalancer.public.urn,
392
+ local.has_domain && var.canvas_public ? digitalocean_loadbalancer.canvas[0].urn : "",
387
393
  digitalocean_database_cluster.redis.urn,
388
394
  local.provision_pg ? digitalocean_database_cluster.pg[0].urn : "",
389
395
  ])
@@ -441,12 +441,26 @@ _ground_apply() {
441
441
 
442
442
  if [ "$summary_rc" = "3" ]; then
443
443
  local REPLY
444
+ # ENTER GOES AHEAD, ESC STOPS. This asked for a typed `y` and made Enter mean "stop",
445
+ # which is the opposite of every other prompt in this CLI — the developer who has just
446
+ # read the plan and decided has to break the habit of the whole tool to act on it. One
447
+ # meaning for Enter throughout, and the escape hatch is the key literally named for it.
444
448
  echo -e " ${RED}This removes or rebuilds things that already exist.${NC}"
445
- read -r -p " Type y to go ahead, or Enter to stop: " REPLY
446
- if [[ ! "$REPLY" =~ ^[Yy]$ ]]; then
449
+ echo ""
450
+ echo -e " ${DIM}Enter to go ahead · Esc to stop${NC}"
451
+ printf " "
452
+ local key=""
453
+ if [ -t 0 ]; then
454
+ IFS= read -r -s -n 1 key
455
+ fi
456
+ # \e is Escape; anything else typed (n, q, a stray letter) also stops, because only a
457
+ # deliberate empty Enter should destroy something.
458
+ if [ -n "$key" ]; then
459
+ echo ""
447
460
  info "Nothing was changed. Run ${BOLD}unoverse deploy${NC} when you are ready"
448
461
  rm -rf "$tmp"; return 1
449
462
  fi
463
+ echo ""
450
464
  fi
451
465
 
452
466
  echo ""
@@ -13,6 +13,31 @@ _ground_my_ip() {
13
13
  curl -s --max-time 5 https://api.ipify.org 2>/dev/null || curl -s --max-time 5 https://ifconfig.me 2>/dev/null
14
14
  }
15
15
 
16
+ # A GROUND WRITES SECRETS TO DISK, so the ignore rule ships with the ground itself.
17
+ #
18
+ # terraform.tfvars holds the database URL, the registry token and every service key; the
19
+ # state file holds those plus everything Terraform generated, in plain text. The platform
20
+ # repo has ignored them since the grounds landed. A universe scaffolded by the CLI did not,
21
+ # because its .gitignore comes from the starter — which only changes when the starter is
22
+ # re-synced and pushed, long after somebody's first `git init && git add .`.
23
+ #
24
+ # Fixing it here means the rule exists the moment the file it protects does, on every
25
+ # universe, without waiting for a starter release. Idempotent: it appends only what is
26
+ # missing, and never rewrites a line the developer put there.
27
+ _ground_protect_secrets() {
28
+ local gi="$ROOT/.gitignore" rule
29
+ [ -f "$gi" ] || : > "$gi"
30
+ local added=0
31
+ for rule in 'infra/**/.terraform/' 'infra/**/*.tfstate*' 'infra/**/terraform.tfvars' '.unoverse/'; do
32
+ grep -qxF "$rule" "$gi" 2>/dev/null && continue
33
+ [ "$added" = "0" ] && printf '\n# Terraform (infra/*) — state and vars carry secrets\n' >> "$gi"
34
+ printf '%s\n' "$rule" >> "$gi"
35
+ added=1
36
+ done
37
+ [ "$added" = "1" ] && ok "Added the terraform files to .gitignore ${DIM}(they hold your database URL and keys)${NC}"
38
+ return 0
39
+ }
40
+
16
41
  # THE UNIVERSE HAS A NAME, so ask for one. This file used to write name = "universe-poc"
17
42
  # as a literal, so every universe on every account carried the same placeholder — it names
18
43
  # the droplet, the cache, the load balancer and the cloud project, and the developer first
@@ -132,6 +157,7 @@ _ground_do() {
132
157
  echo ""
133
158
  echo -e " ${DIM}Regions available to you: $regions${NC}"
134
159
  fi
160
+ _ground_protect_secrets
135
161
  _ground_identity "$regions"
136
162
 
137
163
  cat > "$out" <<EOF
@@ -232,6 +258,7 @@ _ground_aws() {
232
258
  fi
233
259
 
234
260
  # Region already came from the AWS CLI's own configuration, so only the name is asked.
261
+ _ground_protect_secrets
235
262
  _ground_identity "-"
236
263
 
237
264
  cat > "$out" <<EOF
@@ -71,6 +71,17 @@ function describe(r) {
71
71
  price: PRICE[a.size],
72
72
  };
73
73
  }
74
+ // NEVER "SUPPORTING". These were counted as plumbing, which is fair when they are being
75
+ // created alongside the cluster they live on — and badly wrong on a REPLACE, where a
76
+ // dropped and recreated database takes its contents with it. "plus 1 supporting
77
+ // resource (users, rules, keys)" is not something anyone reads as "your data is about
78
+ // to be deleted". Naming them costs one line and makes that impossible to miss.
79
+ case "digitalocean_database_db":
80
+ return { what: "Database", detail: a.name ? `${a.name} — its contents live here` : "" };
81
+ case "digitalocean_database_user":
82
+ return { what: "Database user", detail: a.name ?? "" };
83
+ case "digitalocean_database_connection_pool":
84
+ return { what: "Connection pool", detail: a.name ?? "" };
74
85
  case "digitalocean_loadbalancer":
75
86
  return { what: "Load balancer", detail: "the public way in", price: PRICE.lb };
76
87
  case "digitalocean_firewall":
@@ -168,7 +179,11 @@ process.stdin.on("end", () => {
168
179
  plan.planned_values?.outputs?.monthly_estimate?.value ??
169
180
  plan.outputs?.monthly_estimate?.value;
170
181
  const monthly = ground ?? Object.values(groups).flat().reduce((sum, d) => sum + (d.price ?? 0), 0);
182
+ // THE TOTAL, NOT THE PRICE OF THIS CHANGE. `monthly_estimate` is what the whole universe
183
+ // costs to run, and printing it under a change list read as the bill for that change:
184
+ // a one-line firewall update appeared to cost $135. Say which number this is.
171
185
  if (monthly) {
186
+ out.push(` ${dim("Running total for the whole universe, not the cost of this change:")}`);
172
187
  out.push(` ${bold("Roughly")} ${bold(`$${monthly}/month`)} ${dim("at your provider's current prices, billed by them, not by unoverse")}`);
173
188
  out.push("");
174
189
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unoverse",
3
- "version": "0.1.89",
3
+ "version": "0.1.91",
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",