unoverse 0.1.77 → 0.1.78

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.
@@ -1,13 +1,14 @@
1
+ # NO DEFAULTS ON EITHER OF THESE — see the same note in infra/digitalocean/variables.tf.
2
+ # Where it runs and what it is called are the developer's answers, and a default quietly
3
+ # supplies mine instead.
1
4
  variable "region" {
2
5
  description = "AWS region. Must have Bedrock model access enabled for the models you use."
3
6
  type = string
4
- default = "us-east-1"
5
7
  }
6
8
 
7
9
  variable "name" {
8
- description = "Prefix for every resource (also the Cognito domain prefix, so keep it dns-safe)."
10
+ description = "Prefix for every resource (also the Cognito domain prefix, so keep it dns-safe), and the resource group they are tagged into."
9
11
  type = string
10
- default = "universe-poc"
11
12
  }
12
13
 
13
14
  variable "admin_cidr" {
@@ -5,16 +5,18 @@ variable "do_token" {
5
5
  sensitive = true
6
6
  }
7
7
 
8
+ # NO DEFAULTS ON EITHER OF THESE. They used to default to "lon1" and "universe-poc", which
9
+ # meant a tfvars missing them built somebody's universe in London under a placeholder name
10
+ # and reported success. Where it runs and what it is called are the developer's answers, so
11
+ # terraform should refuse to plan without them rather than quietly supply mine.
8
12
  variable "region" {
9
- description = "DigitalOcean region for everything."
13
+ description = "DigitalOcean region for everything (doctl compute region list)."
10
14
  type = string
11
- default = "lon1"
12
15
  }
13
16
 
14
17
  variable "name" {
15
- description = "Prefix for every resource."
18
+ description = "Prefix for every resource, and the name of the DigitalOcean project they are grouped under."
16
19
  type = string
17
- default = "universe-poc"
18
20
  }
19
21
 
20
22
  variable "size" {
@@ -125,6 +125,24 @@ _ensure_ground_config() {
125
125
  fi
126
126
  }
127
127
  echo ""
128
+
129
+ # YOUR IP MOVED. admin_cidr is captured once, when the ground is generated, and it is
130
+ # the ONLY address the firewall lets near port 22. Home broadband, a phone hotspot or a
131
+ # different office and the address is stale — at which point terraform applies happily,
132
+ # every resource is correct, and the ship step dies on "connect to host ... port 22:
133
+ # Operation timed out" with nothing on screen connecting the two. Check it every deploy
134
+ # and just fix it: the firewall is ours, and a one-rule update is not a decision.
135
+ local cur_ip cur_cidr have_cidr
136
+ cur_ip=$(curl -s --max-time 5 https://api.ipify.org 2>/dev/null || curl -s --max-time 5 https://ifconfig.me 2>/dev/null)
137
+ have_cidr=$(grep -E '^admin_cidr[[:space:]]*=' "$tfv" 2>/dev/null | sed -E 's/.*"([^"]+)".*/\1/')
138
+ if [ -n "$cur_ip" ] && [ -n "$have_cidr" ] && [ "$have_cidr" != "FILL_ME" ]; then
139
+ cur_cidr="$cur_ip/32"
140
+ if [ "$have_cidr" != "$cur_cidr" ]; then
141
+ node -e 'const fs=require("fs");const[f,v]=process.argv.slice(1);let s=fs.readFileSync(f,"utf8");s=s.replace(/^admin_cidr(\s*)=\s*"[^"]*"/m,"admin_cidr$1= "+JSON.stringify(v));fs.writeFileSync(f,s)' "$tfv" "$cur_cidr"
142
+ ok "Your address changed ${DIM}($have_cidr → $cur_cidr). Admin access follows you${NC}"
143
+ fi
144
+ fi
145
+
128
146
  # POSTGRES: ASK, DO NOT ASSUME. The ground discovers an existing cluster and writes
129
147
  # it as a COMMENTED line, so the default silently provisions a SECOND database next
130
148
  # to one the account already pays for. A choice with a monthly bill attached is a
@@ -244,25 +262,34 @@ _ground_apply() {
244
262
  *) fail "Terraform could not plan the change:"; tail -20 "$tmp/plan.log" | sed 's/^/ /'; rm -rf "$tmp"; return 1 ;;
245
263
  esac
246
264
 
247
- if ! terraform -chdir="$dir" show -json "$planfile" 2>/dev/null | node "$GRAVITY_LIB/tfsummary.mjs"; then
265
+ # STOPPING IS FOR LOSING SOMETHING, NOT FOR BUILDING IT. Typing `unoverse deploy` is
266
+ # already the decision; a y/N gate straight afterwards asks the same question twice and
267
+ # makes the developer's own instruction feel like a risk. The summary above still shows
268
+ # exactly what appears and what it costs — it is a briefing, not a checkpoint. tfsummary
269
+ # exits 3 when the plan destroys or replaces something, and THAT is worth interrupting.
270
+ local summary_rc=0
271
+ terraform -chdir="$dir" show -json "$planfile" 2>/dev/null | node "$GRAVITY_LIB/tfsummary.mjs"
272
+ summary_rc=${PIPESTATUS[1]}
273
+ if [ "$summary_rc" = "2" ]; then
274
+ # The plan could not be read, so nothing above is trustworthy. Show the real thing and
275
+ # fall back to asking: silence plus an unreadable plan is not consent.
248
276
  terraform -chdir="$dir" show "$planfile"
277
+ summary_rc=3
249
278
  fi
250
279
 
251
- # `read -p` does NOT interpret escapes, so ${DIM} printed as literal \033[2m. Colour
252
- # the hint with echo -e first, then take a plain prompt.
253
- # EXPLICIT YES, and it says so. Everywhere else in this CLI Enter means "go"; here it
254
- # means "stop", because this is the one prompt that starts a monthly bill. Keeping the
255
- # safety is right — leaving it as a bare [y/N] among Enter-means-go prompts is not.
256
- local REPLY
257
- echo -e " ${DIM}d shows the full technical plan${NC}"
258
- read -r -p " Type y to build this, or Enter to stop: " REPLY
259
- if [[ "$REPLY" =~ ^[Dd]$ ]]; then
260
- terraform -chdir="$dir" show "$planfile"
261
- read -r -p " Type y to build this, or Enter to stop: " REPLY
262
- fi
263
- if [[ ! "$REPLY" =~ ^[Yy]$ ]]; then
264
- info "Nothing was created. Run ${BOLD}unoverse deploy${NC} when you are ready"
265
- rm -rf "$tmp"; return 1
280
+ local docpage
281
+ [ "$cloud" = "aws" ] && docpage="aws" || docpage="digitalocean"
282
+ echo -e " ${DIM}How it fits together: https://github.com/unoverse-platform/docs/blob/main/architecture/$docpage.md${NC}"
283
+ echo ""
284
+
285
+ if [ "$summary_rc" = "3" ]; then
286
+ local REPLY
287
+ echo -e " ${RED}This removes or rebuilds things that already exist.${NC}"
288
+ read -r -p " Type y to go ahead, or Enter to stop: " REPLY
289
+ if [[ ! "$REPLY" =~ ^[Yy]$ ]]; then
290
+ info "Nothing was changed. Run ${BOLD}unoverse deploy${NC} when you are ready"
291
+ rm -rf "$tmp"; return 1
292
+ fi
266
293
  fi
267
294
 
268
295
  echo ""
@@ -13,6 +13,44 @@ _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
+ # THE UNIVERSE HAS A NAME, so ask for one. This file used to write name = "universe-poc"
17
+ # as a literal, so every universe on every account carried the same placeholder — it names
18
+ # the droplet, the cache, the load balancer and the cloud project, and the developer first
19
+ # meets it in their provider dashboard, after the bill has started. A generated file is not
20
+ # a question: nobody edits it before running deploy.
21
+ #
22
+ # Sets GROUND_NAME (and GROUND_REGION when a default is passed). Both are sanitised to what
23
+ # cloud resource names allow, so a typed "My POC" cannot fail an apply five minutes in.
24
+ _ground_identity() {
25
+ local valid="$1" suggest
26
+ suggest=$(basename "$ROOT" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]+/-/g; s/^-+//; s/-+$//')
27
+ suggest="${suggest:-universe}"
28
+ echo ""
29
+ read -r -p " Name this universe [$suggest]: " GROUND_NAME
30
+ GROUND_NAME=$(echo "${GROUND_NAME:-$suggest}" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^a-z0-9-]+/-/g; s/^-+//; s/-+$//')
31
+ GROUND_NAME="${GROUND_NAME:-$suggest}"
32
+
33
+ # NO DEFAULT REGION. A default is a choice made on the developer's behalf, and the
34
+ # previous one was mine: `lon1`, written straight into the file, which is how a universe
35
+ # meant for Singapore was built in London. There is no neutral region to fall back to, so
36
+ # this asks until it gets a real answer and checks it against the provider's own list —
37
+ # a typo here surfaces as a five-minute apply that fails on the last resource.
38
+ # "-" means the region is not this ground's to ask: AWS reads it from the developer's own
39
+ # CLI configuration, which is already their answer.
40
+ GROUND_REGION=""
41
+ [ "$valid" = "-" ] && return 0
42
+ while [ -z "$GROUND_REGION" ]; do
43
+ read -r -p " Which region? " GROUND_REGION
44
+ GROUND_REGION=$(echo "$GROUND_REGION" | tr -d '[:space:]')
45
+ # Validate only against a list we actually retrieved. An empty list means the lookup
46
+ # failed, not that every region is wrong.
47
+ if [ -n "$GROUND_REGION" ] && [ -n "$valid" ] && ! echo "$valid" | tr ' ' '\n' | grep -qx "$GROUND_REGION"; then
48
+ warn "$GROUND_REGION is not a region in your account. Pick one from the list above"
49
+ GROUND_REGION=""
50
+ fi
51
+ done
52
+ }
53
+
16
54
  _ground_do() {
17
55
  local dir="$ROOT/infra/digitalocean"
18
56
  local out="$dir/terraform.tfvars"
@@ -88,13 +126,21 @@ _ground_do() {
88
126
  info "no existing Postgres cluster. Terraform will provision one"
89
127
  fi
90
128
 
129
+ local regions
130
+ regions=$(doctl compute region list --format Slug --no-header 2>/dev/null | tr -d '\r' | xargs)
131
+ if [ -n "$regions" ]; then
132
+ echo ""
133
+ echo -e " ${DIM}Regions available to you: $regions${NC}"
134
+ fi
135
+ _ground_identity "$regions"
136
+
91
137
  cat > "$out" <<EOF
92
138
  # Generated by \`unoverse ground do\` on $(date +%Y-%m-%d). Discovered values are
93
139
  # prefilled; every FILL_ME needs YOUR value before \`terraform apply\`.
94
140
  # Token: set DIGITALOCEAN_TOKEN in the environment (preferred) or do_token here.
95
141
 
96
- region = "lon1" # doctl compute region list
97
- name = "universe-poc"
142
+ region = "$GROUND_REGION" # doctl compute region list
143
+ name = "$GROUND_NAME"
98
144
  size = "small" # small (POC) | medium | large
99
145
  admin_cidr = "${ip:-FILL_ME}${ip:+/32}" # YOUR IP — SSH + Dozzle only
100
146
  ssh_key_name = "${first_key:-FILL_ME}" # must already exist in the DO account
@@ -185,12 +231,15 @@ _ground_aws() {
185
231
  info "multiple Route53 zones. Pick one in the file (listed there)"
186
232
  fi
187
233
 
234
+ # Region already came from the AWS CLI's own configuration, so only the name is asked.
235
+ _ground_identity "-"
236
+
188
237
  cat > "$out" <<EOF
189
238
  # Generated by \`unoverse ground aws\` on $(date +%Y-%m-%d). Discovered values are
190
239
  # prefilled; every FILL_ME needs YOUR value before \`terraform apply\`.
191
240
 
192
241
  region = "$region"
193
- name = "universe-poc"
242
+ name = "$GROUND_NAME"
194
243
  admin_cidr = "${ip:-FILL_ME}${ip:+/32}" # YOUR IP — the only SSH source
195
244
  ssh_key_name = "${first_key:-FILL_ME}" # must already exist in the region
196
245
  admin_email = "FILL_ME" # initial admin (Cognito user, all roles; invite emailed)
@@ -11,6 +11,11 @@
11
11
  * their size, counts the plumbing (database users, firewall rules, generated passwords)
12
12
  * rather than listing it, and shouts about anything being destroyed.
13
13
  *
14
+ * EXIT CODE IS THE ANSWER TO "should the caller stop and ask?". 0 when the plan only adds
15
+ * or adjusts — deploy just builds, because typing `unoverse deploy` already said to. 3
16
+ * when something is destroyed or replaced, which is the only case worth interrupting a
17
+ * developer for. 2 when the JSON could not be read, so the caller shows raw terraform.
18
+ *
14
19
  * NOT A COST ORACLE. The developer sees an order of magnitude rather than a surprise. Rates
15
20
  * move, so the total is labelled approximate and the real bill is the provider's.
16
21
  *
@@ -138,7 +143,7 @@ process.stdin.on("end", () => {
138
143
  const out = [];
139
144
  if (total === 0) {
140
145
  console.log(`\n ${green("●")} Your infrastructure already matches. Nothing to change.\n`);
141
- process.exit(0);
146
+ return;
142
147
  }
143
148
 
144
149
  const width = Math.max(...Object.values(groups).flat().map((d) => d.what.length), 0) + 2;
@@ -169,4 +174,11 @@ process.stdin.on("end", () => {
169
174
  }
170
175
 
171
176
  console.log("\n" + out.join("\n"));
177
+
178
+ // Tell the caller whether this is worth stopping for. Losing something is; building
179
+ // something is not. `process.exitCode` rather than `process.exit`, which can cut off
180
+ // the output above while it is still draining down a pipe.
181
+ const destructive =
182
+ groups.delete.length + groups.replace.length + supporting.delete + supporting.replace;
183
+ if (destructive) process.exitCode = 3;
172
184
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unoverse",
3
- "version": "0.1.77",
3
+ "version": "0.1.78",
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",