unoverse 0.1.32 → 0.1.34

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
@@ -103,26 +103,36 @@ switch (cmd) {
103
103
  break;
104
104
 
105
105
  case "update": {
106
- // Refresh the repo-local authoring skill alongside the CLI, when we are in a
107
- // universe. It is a copy by necessity (Claude Code discovers .claude/skills off
108
- // disk), so the CLI's own update is what keeps it current.
106
+ // ONE update: the CLI from npm, then everything the CLI is responsible for keeping
107
+ // current the repo-local authoring skill and, in a universe, the platform images.
108
+ // Those two steps run via `_postupdate` in the NEWLY installed binary, not this
109
+ // process: this file is being replaced mid-command, and copying the skill from the
110
+ // old install would pin it one release behind forever.
111
+ const r = spawnSync("npm", ["install", "-g", "unoverse@latest"], { stdio: "inherit" });
112
+ if (r.status === 0) {
113
+ const v = spawnSync("unoverse", ["-v"], { encoding: "utf8" }).stdout?.trim();
114
+ console.log(`\n ✓ unoverse is now ${v || "up to date"}`);
115
+ spawnSync("unoverse", ["_postupdate"], { stdio: "inherit" });
116
+ } else {
117
+ console.log(`\n ✗ global install failed (permissions?). Try:\n\n sudo npm install -g unoverse@latest\n npx unoverse@latest # or skip the global install entirely\n`);
118
+ }
119
+ process.exit(r.status ?? 0);
120
+ }
121
+
122
+ case "_postupdate": {
123
+ // Runs AS the new version, in the developer's cwd. Outside a universe there is
124
+ // nothing to refresh, and silence is the right answer.
109
125
  if (UNIVERSE) {
110
126
  const skillSrc = join(dirname(fileURLToPath(import.meta.url)), "../operator/skills/unoverse-create");
111
127
  if (existsSync(skillSrc)) {
112
128
  const { cpSync, mkdirSync } = await import("node:fs");
113
129
  mkdirSync(join(UNIVERSE.root, ".claude/skills"), { recursive: true });
114
130
  cpSync(skillSrc, join(UNIVERSE.root, ".claude/skills/unoverse-create"), { recursive: true });
115
- console.log(" ✓ authoring skill refreshed (.claude/skills/unoverse-create)");
131
+ console.log(" ✓ authoring skill refreshed");
116
132
  }
133
+ operator(UNIVERSE, ["refresh-images"]);
117
134
  }
118
- const r = spawnSync("npm", ["install", "-g", "unoverse@latest"], { stdio: "inherit" });
119
- if (r.status === 0) {
120
- const v = spawnSync("unoverse", ["-v"], { encoding: "utf8" }).stdout?.trim();
121
- console.log(`\n ✓ unoverse is now ${v || "up to date"}\n`);
122
- } else {
123
- console.log(`\n ✗ global install failed (permissions?). Try:\n\n sudo npm install -g unoverse@latest\n npx unoverse@latest # or skip the global install entirely\n`);
124
- }
125
- process.exit(r.status ?? 0);
135
+ process.exit(0);
126
136
  }
127
137
 
128
138
  // `urls` stays as an alias: it shipped in 0.1.4/0.1.5 and is in people's fingers.
@@ -5,6 +5,49 @@
5
5
  # (`npm i -g unoverse`); symlinking a per-project bash script into /usr/local/bin was the
6
6
  # second CLI we set out to delete.
7
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
+
8
51
  # First-run detection
9
52
  check_first_run() {
10
53
  if [ ! -f "$ROOT/.env" ]; then
@@ -115,6 +158,18 @@ cmd_init() {
115
158
  done
116
159
  fi
117
160
 
161
+ # THE DOWNLOAD STARTS NOW (see the block at the top of this file).
162
+ if [ "$DOCKER_OK" = "1" ]; then
163
+ if echo "$DOCR_TOKEN" | docker login "$DOCR_REGISTRY" -u "$DOCR_TOKEN" --password-stdin >/dev/null 2>&1; then
164
+ start_background_pull
165
+ echo ""
166
+ echo -e " ${CYAN}⬇${NC} ${DIM}Platform images are downloading in the background while you configure${NC}"
167
+ echo ""
168
+ else
169
+ warn "Registry login failed with this token. Images will not pull until it is fixed"
170
+ fi
171
+ fi
172
+
118
173
  # A DEFAULT, because "from your admin" is meaningless when you are the admin. The
119
174
  # platform ships no database (docker-compose has no postgres), so this points at one
120
175
  # you run. Enter takes the conventional local one.
@@ -144,6 +199,7 @@ cmd_init() {
144
199
  fi
145
200
  fi
146
201
 
202
+ pull_status_line
147
203
  # Redis, current values as defaults
148
204
  local rd
149
205
  rd=$(_env_cur REDIS_HOST); rd="${rd:-host.docker.internal}"
@@ -174,7 +230,7 @@ cmd_init() {
174
230
  # a deployed universe ("there is no auth-off deployment"), and the platform enforces it
175
231
  # rather than trusting this wizard: authConfig.ts refuses to start with auth off when
176
232
  # NODE_ENV=production. So answering "no" here cannot produce an unprotected deployment.
177
- echo ""
233
+ pull_status_line
178
234
  local cur_auth idp_prompt
179
235
  cur_auth=$(_env_cur AUTH_ENABLED)
180
236
  idp_prompt="[y/N]"
@@ -243,6 +299,7 @@ cmd_init() {
243
299
  ok "Redis namespace: ${BOLD}${REDIS_NAMESPACE}${NC}"
244
300
 
245
301
  # OpenAI (for Memory Server)
302
+ pull_status_line
246
303
  local cur_oai
247
304
  cur_oai=$(_env_cur OPENAI_API_KEY)
248
305
  # NOT just the memory server: compose hands this to the unoverse service itself —
@@ -297,21 +354,15 @@ ENVEOF
297
354
 
298
355
  ok ".env created"
299
356
 
300
- # Registry login and the image pull both need a daemon. Without one the token is
301
- # already written to .env, so `unoverse start` does this on its first run.
357
+ # Attach to the background pull started when the token was accepted: layers already
358
+ # down show as complete, the rest stream docker's progress bars. Without a daemon the
359
+ # token is in .env, and `unoverse start` pulls on its first run.
302
360
  if [ "$DOCKER_OK" = "1" ]; then
303
- echo ""
304
- echo -e " Logging in to DigitalOcean Container Registry..."
305
- if echo "$DOCR_TOKEN" | docker login "$DOCR_REGISTRY" -u "$DOCR_TOKEN" --password-stdin &>/dev/null; then
306
- ok "Logged in to DOCR"
307
- else
308
- fail "DOCR login failed. Check your token"
309
- exit 1
310
- fi
311
- cmd_pull
361
+ pull_missing_images
362
+ [ -n "$PULL_STATE" ] && rm -f "$PULL_STATE"
312
363
  else
313
364
  echo ""
314
- info "Skipped the registry login and image pull. ${BOLD}unoverse start${NC} does both once Docker is up"
365
+ info "Skipped the image download. ${BOLD}unoverse start${NC} does it once Docker is up"
315
366
  fi
316
367
 
317
368
  # Install to PATH
@@ -33,6 +33,24 @@ check_docker_file_sharing() {
33
33
  return 0
34
34
  }
35
35
 
36
+ # Pull whatever images are missing, VISIBLY. docker's own per-layer progress bars are the
37
+ # loading indication — a developer staring at a silent screen while gigabytes download
38
+ # assumes the tool is hung. Exists because init used to call cmd_pull (deleted with
39
+ # update.sh), so nothing pulled and nothing printed, and start buried compose's pull
40
+ # inside a log file.
41
+ pull_missing_images() {
42
+ local img missing=""
43
+ for img in $(docker compose -f "$ROOT/docker-compose.yml" config --images 2>/dev/null | sort -u); do
44
+ docker image inspect "$img" >/dev/null 2>&1 || missing="$missing $img"
45
+ done
46
+ [ -n "$missing" ] || return 0
47
+ echo ""
48
+ info "Pulling platform images (first run — a few minutes, progress below)..."
49
+ echo ""
50
+ docker compose -f "$ROOT/docker-compose.yml" --env-file "$ROOT/.env" pull || { fail "Image pull failed. Check the registry token (re-run unoverse create to change it)"; exit 1; }
51
+ echo ""
52
+ }
53
+
36
54
  cmd_start() {
37
55
  # Login to registry if DOCR_TOKEN is set
38
56
  local docr_token
@@ -44,6 +62,8 @@ cmd_start() {
44
62
  # `start --pull` refreshes the images first. This is where refreshing a LOCAL universe
45
63
  # lives now: `update` is the CLI's word and means only the CLI, wherever you type it.
46
64
  # A server is refreshed by `unoverse deploy` from your machine, not on the box.
65
+ pull_missing_images
66
+
47
67
  if [ "${1:-}" = "--pull" ]; then
48
68
  info "Pulling the latest images..."
49
69
  docker compose -f "$ROOT/docker-compose.yml" pull || { fail "Image pull failed"; exit 1; }
@@ -64,6 +64,22 @@ case "${1:-}" in
64
64
  check) cmd_check && cmd_db_verify && cmd_doctor ;;
65
65
  dev) cmd_dev ;;
66
66
  ground) shift; cmd_ground "$@" ;;
67
+ refresh-images)
68
+ # internal: `unoverse update` runs this after updating the CLI. Pull newer images
69
+ # if the registry has them, and recreate only what is already running — an update
70
+ # never boots a stopped universe.
71
+ if [ ! -f "$ROOT/.env" ]; then
72
+ : # unconfigured: nothing to refresh
73
+ elif ! docker info >/dev/null 2>&1; then
74
+ info "Docker is not running. Images refresh on the next unoverse start"
75
+ else
76
+ info "Checking for newer platform images..."
77
+ docker compose -f "$ROOT/docker-compose.yml" --env-file "$ROOT/.env" pull || warn "image check failed (network/registry?)"
78
+ if [ -n "$(docker compose -f "$ROOT/docker-compose.yml" ps -q 2>/dev/null)" ]; then
79
+ docker compose -f "$ROOT/docker-compose.yml" --env-file "$ROOT/.env" up -d --remove-orphans >/dev/null 2>&1 \
80
+ && ok "Running services moved to the new images"
81
+ fi
82
+ fi ;;
67
83
  deploy)
68
84
  # `deploy marketplace` is PLATFORM-OWNER only: it publishes the marketplace package
69
85
  # to npm, which a starter developer never does (they INSTALL from the marketplace).
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "unoverse",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
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",