unoverse 0.1.17 → 0.1.19

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
@@ -9,7 +9,8 @@
9
9
  */
10
10
  import { existsSync } from "node:fs";
11
11
  import { spawnSync } from "node:child_process";
12
- import { join, resolve } from "node:path";
12
+ import { join, resolve, dirname } from "node:path";
13
+ import { fileURLToPath } from "node:url";
13
14
  import { create } from "../lib/create.mjs";
14
15
 
15
16
  /**
@@ -20,9 +21,17 @@ import { create } from "../lib/create.mjs";
20
21
  * root. Found means the operator commands exist; not found means they simply are not
21
22
  * offered, instead of being advertised and then refusing.
22
23
  */
24
+ // THE OPERATOR SHIPS WITH THIS PACKAGE, not with the universe. A universe is a folder
25
+ // holding a docker-compose.yml, and nothing else: it carries no tooling to keep in step.
26
+ // The scripts act on that folder (each resolves $ROOT by walking up for the compose
27
+ // file), so the code is global and the data is local.
28
+ const VENDORED = join(dirname(fileURLToPath(import.meta.url)), "../operator/operator.sh");
29
+ const IN_REPO = join(dirname(fileURLToPath(import.meta.url)), "../../../scripts/operator.sh");
30
+ const OPERATOR = existsSync(VENDORED) ? VENDORED : IN_REPO;
31
+
23
32
  function findUniverse() {
24
33
  for (let dir = process.cwd(); ; ) {
25
- if (existsSync(join(dir, "scripts/lib/common.sh"))) return dir;
34
+ if (existsSync(join(dir, "docker-compose.yml"))) return { root: dir, script: OPERATOR };
26
35
  const parent = resolve(dir, "..");
27
36
  if (parent === dir) return null;
28
37
  dir = parent;
@@ -30,11 +39,8 @@ function findUniverse() {
30
39
  }
31
40
 
32
41
  /** Run an operator command in the universe we are standing in. */
33
- function operator(root, args) {
34
- const r = spawnSync("bash", [join(root, "scripts/operator.sh"), ...args], {
35
- stdio: "inherit",
36
- cwd: root,
37
- });
42
+ function operator(u, args) {
43
+ const r = spawnSync("bash", [u.script, ...args], { stdio: "inherit", cwd: u.root });
38
44
  process.exit(r.status ?? 0);
39
45
  }
40
46
 
package/lib/create.mjs CHANGED
@@ -8,6 +8,8 @@
8
8
  * a real auth round-trip against the registry, not a format check.
9
9
  */
10
10
  import { createInterface } from "node:readline/promises";
11
+ import { dirname, resolve } from "node:path";
12
+ import { fileURLToPath } from "node:url";
11
13
  import { existsSync, mkdirSync, readdirSync } from "node:fs";
12
14
  import { spawnSync } from "node:child_process";
13
15
 
@@ -242,7 +244,13 @@ export async function create(nameArg) {
242
244
  // already in hand and already validated, so hand it over and configure now.
243
245
  rl.close();
244
246
  console.log("");
245
- const r = spawnSync("bash", [`${name}/scripts/operator.sh`, "init"], {
247
+ // This package's own operator, run against the folder just scaffolded.
248
+ const here = dirname(fileURLToPath(import.meta.url));
249
+ const vendored = resolve(here, "../operator/operator.sh");
250
+ const operatorScript = existsSync(vendored)
251
+ ? vendored
252
+ : resolve(here, "../../../scripts/operator.sh");
253
+ const r = spawnSync("bash", [operatorScript, "init"], {
246
254
  stdio: "inherit",
247
255
  cwd: name,
248
256
  env: { ...process.env, UNOVERSE_DOCR_TOKEN: token },
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bash
2
+ # unoverse check
3
+
4
+ cmd_check() {
5
+ echo ""
6
+ echo -e " ${BOLD}Unoverse Platform Health Check${NC}"
7
+ echo ""
8
+ local pass=0 total=0
9
+
10
+ # 1. Services
11
+ for svc in unoverse canvas umap memory; do
12
+ total=$((total + 1))
13
+ local status
14
+ status=$(docker compose -f "$ROOT/docker-compose.yml" ps --format '{{.Status}}' "$svc" 2>/dev/null | head -1)
15
+ if echo "$status" | grep -qi "up"; then
16
+ ok "$svc"
17
+ pass=$((pass + 1))
18
+ elif echo "$status" | grep -qi "created"; then
19
+ fail "$svc ${DIM}(stuck in Created, the container never started)${NC}"
20
+ elif [ -z "$status" ]; then
21
+ fail "$svc ${DIM}(no container found)${NC}"
22
+ else
23
+ fail "$svc ${DIM}($status)${NC}"
24
+ fi
25
+ done
26
+ echo ""
27
+
28
+ # 2. Health endpoints
29
+ for endpoint in 4105:unoverse 4101:engine 5001:umap 4104:memory; do
30
+ local port="${endpoint%%:*}" name="${endpoint##*:}"
31
+ total=$((total + 1))
32
+ local code
33
+ code=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:$port/health" 2>/dev/null)
34
+ if [ "$code" = "200" ]; then
35
+ ok "$name health ${DIM}:$port${NC}"
36
+ pass=$((pass + 1))
37
+ else
38
+ fail "$name health ${DIM}:$port → $code${NC}"
39
+ fi
40
+ done
41
+ echo ""
42
+
43
+ # 3. Packages built
44
+ local built=0 pkg_total=0
45
+ for pkg in "$ROOT"/packages/*/; do
46
+ [ -f "$pkg/package.json" ] || continue
47
+ local name
48
+ name=$(basename "$pkg")
49
+ case "$name" in marketplace|gravity-client|plugin-base|skills|prompt-blocks) continue;; esac
50
+ pkg_total=$((pkg_total + 1))
51
+ if [ -f "$pkg/dist/index.js" ]; then
52
+ built=$((built + 1))
53
+ else
54
+ fail "$name ${DIM}(missing dist/index.js)${NC}"
55
+ fi
56
+ done
57
+ total=$((total + 1))
58
+ if [ "$built" -eq "$pkg_total" ]; then
59
+ ok "$built/$pkg_total packages built"
60
+ pass=$((pass + 1))
61
+ else
62
+ fail "$built/$pkg_total packages built"
63
+ fi
64
+
65
+ # 4. Unoverse node catalog
66
+ total=$((total + 1))
67
+ local plugin_count
68
+ # Catalog lives on unoverse; :4106 is Docker-internal and :4105 /plugins is JWT-gated,
69
+ # so count nodes from inside the container (node:20-slim has no curl → use node fetch).
70
+ plugin_count=$(docker compose -f "$ROOT/docker-compose.yml" exec -T unoverse node -e "fetch('http://127.0.0.1:4106/nodes').then(r=>r.json()).then(d=>console.log((d.nodes||[]).length)).catch(()=>console.log(0))" 2>/dev/null | tr -d ' \r')
71
+ if [ "$plugin_count" -gt "0" ]; then
72
+ ok "$plugin_count nodes loaded"
73
+ pass=$((pass + 1))
74
+ else
75
+ fail "0 nodes loaded ${DIM}(check unoverse logs)${NC}"
76
+ fi
77
+
78
+ # 5. Canvas
79
+ total=$((total + 1))
80
+ local canvas_code
81
+ canvas_code=$(curl -s -o /dev/null -w '%{http_code}' "http://localhost:3001" 2>/dev/null)
82
+ if [ "$canvas_code" = "200" ]; then
83
+ ok "Canvas ${DIM}http://localhost:3001${NC}"
84
+ pass=$((pass + 1))
85
+ else
86
+ fail "Canvas ${DIM}http://localhost:3001 → $canvas_code${NC}"
87
+ fi
88
+
89
+ # Summary
90
+ echo ""
91
+ if [ "$pass" -eq "$total" ]; then
92
+ echo -e " ${GREEN}${BOLD}All $total checks passed${NC}"
93
+ else
94
+ echo -e " ${YELLOW}${BOLD}$pass/$total checks passed${NC}"
95
+ fi
96
+ echo ""
97
+ }
@@ -0,0 +1,58 @@
1
+ #!/usr/bin/env bash
2
+ # Shared colors, helpers, and constants
3
+
4
+ GRAVITY_VERSION="1.0.0"
5
+ DOCR_REGISTRY="registry.digitalocean.com"
6
+
7
+ # Colors
8
+ RED='\033[0;31m'
9
+ GREEN='\033[0;32m'
10
+ YELLOW='\033[1;33m'
11
+ CYAN='\033[0;36m'
12
+ DIM='\033[2m'
13
+ BOLD='\033[1m'
14
+ NC='\033[0m'
15
+ BLUE='\033[0;34m'
16
+ MAGENTA='\033[0;35m'
17
+ WHITE='\033[1;37m'
18
+ UNDERLINE='\033[4m'
19
+
20
+ ok() { echo -e " ${GREEN}✓${NC} $1"; }
21
+ warn() { echo -e " ${YELLOW}⚠${NC} $1"; }
22
+ fail() { echo -e " ${RED}✗${NC} $1"; }
23
+ info() { echo -e " ${DIM}$1${NC}"; }
24
+ link() { echo -e " ${CYAN}${UNDERLINE}$1${NC}"; }
25
+ banner() {
26
+ echo ""
27
+ echo -e " ${BOLD}${CYAN}⬡ $1${NC}"
28
+ echo -e " ${DIM}─────────────────────────────────${NC}"
29
+ }
30
+
31
+ # The branded access box. ONE definition so start.sh, update.sh and dashboard.sh
32
+ # never drift apart again.
33
+ print_access_urls() {
34
+ echo -e " ${WHITE}${BOLD}unoverse${NC}${DIM}, the experience layer for AI${NC}"
35
+ echo -e " ${DIM}Use the Unoverse MCP to build agents. Somewhere, Skynet is taking notes. 🤖${NC}"
36
+ echo ""
37
+ echo -e " ${CYAN}Canvas${NC} ${DIM}(build agents)${NC} ${UNDERLINE}http://localhost:3001${NC}"
38
+ echo -e " ${CYAN}Studio${NC} ${DIM}(build assets)${NC} ${UNDERLINE}http://localhost:3002${NC}"
39
+ echo -e " ${CYAN}API${NC} ${DIM}(REST + MCP)${NC} ${UNDERLINE}http://localhost:4105${NC}"
40
+ echo ""
41
+ echo -e " ${DIM}▶ Next:${NC} open this repo in ${BOLD}Claude Code${NC} and ask it to build an agent"
42
+ }
43
+
44
+ # Elapsed time helper
45
+ timer_start() { GRAVITY_START=$(date +%s); }
46
+ timer_elapsed() {
47
+ local end=$(date +%s)
48
+ local elapsed=$((end - GRAVITY_START))
49
+ if [ $elapsed -ge 60 ]; then
50
+ echo "$((elapsed / 60))m $((elapsed % 60))s"
51
+ else
52
+ echo "${elapsed}s"
53
+ fi
54
+ }
55
+
56
+ # The old "studio mode" system (mode file, is_platform_mode, require_platform_mode)
57
+ # was removed 2026-07-28: Studio is a separate app now, and this CLI has one job:
58
+ # operate a universe. See _legacy/scripts-lib/ for the retired authoring tools.
@@ -0,0 +1,85 @@
1
+ #!/usr/bin/env bash
2
+ # unoverse dashboard (default command when no args)
3
+
4
+ cmd_dashboard() {
5
+ echo ""
6
+ echo -e " ${BOLD}${CYAN}⬡ Unoverse Platform${NC} ${DIM}v${GRAVITY_VERSION}${NC}"
7
+ echo -e " ${DIM}─────────────────────────────────${NC}"
8
+
9
+ # Check if .env exists
10
+ if [ ! -f "$ROOT/.env" ]; then
11
+ echo ""
12
+ echo -e " ${YELLOW}${BOLD}First time?${NC} Run ${GREEN}${BOLD}unoverse init${NC} to get started."
13
+ echo ""
14
+ return
15
+ fi
16
+
17
+ # Quick status — use -a to see Created containers too
18
+ local ps
19
+ ps=$(docker compose -f "$ROOT/docker-compose.yml" ps -a --format "{{.Name}}\t{{.Status}}" 2>/dev/null) || true
20
+
21
+ if [ -n "$ps" ]; then
22
+ local total running created
23
+ total=$(echo "$ps" | wc -l | tr -d ' ')
24
+ running=$(echo "$ps" | grep -ci "up" || echo "0")
25
+ created=$(echo "$ps" | grep -ci "created" || echo "0")
26
+
27
+ echo ""
28
+ if [ "$running" -eq "$total" ] && [ "$total" -gt 0 ]; then
29
+ echo -e " ${GREEN}●${NC} ${BOLD}Platform running${NC} ${DIM}($running services)${NC}"
30
+ echo ""
31
+ print_access_urls
32
+ elif [ "$created" -gt 0 ] && [ "$running" -eq 0 ]; then
33
+ echo -e " ${RED}●${NC} ${BOLD}$total containers stuck in Created state${NC}"
34
+ echo ""
35
+ echo -e " Containers were created but never started."
36
+ echo -e " Run ${GREEN}${BOLD}unoverse check${NC} to diagnose"
37
+ elif [ "$running" -gt 0 ]; then
38
+ echo -e " ${YELLOW}●${NC} ${BOLD}$running/$total services up${NC}"
39
+ echo ""
40
+ print_access_urls
41
+ else
42
+ echo -e " ${RED}●${NC} ${BOLD}Platform not running${NC} ${DIM}($total containers stopped)${NC}"
43
+ echo ""
44
+ echo -e " Run ${GREEN}${BOLD}unoverse start${NC} to launch"
45
+ fi
46
+ else
47
+ echo ""
48
+ echo -e " ${DIM}●${NC} ${BOLD}Platform stopped${NC}"
49
+ echo ""
50
+ echo -e " Run ${GREEN}${BOLD}unoverse start${NC} to launch"
51
+ fi
52
+
53
+ echo ""
54
+ echo -e " ${DIM}Run ${NC}${BOLD}unoverse help${NC}${DIM} for all commands${NC}"
55
+ echo ""
56
+ }
57
+
58
+ # ── unoverse open — browser shortcut (merged from open.sh 2026-07-28) ──
59
+
60
+ cmd_open() {
61
+ local target="${1:-canvas}"
62
+ local url
63
+
64
+ case "$target" in
65
+ canvas) url="http://localhost:3001" ;;
66
+ api) url="http://localhost:4105" ;;
67
+ logs|dozzle) url="http://localhost:8080" ;;
68
+ *)
69
+ fail "Unknown target: $target"
70
+ info "Options: canvas, api, logs"
71
+ return
72
+ ;;
73
+ esac
74
+
75
+ ok "Opening $target → ${UNDERLINE}$url${NC}"
76
+
77
+ # Cross-platform open
78
+ if command -v open &>/dev/null; then
79
+ open "$url"
80
+ elif command -v xdg-open &>/dev/null; then
81
+ xdg-open "$url"
82
+ else
83
+ info "Open in your browser: $url"
84
+ fi
85
+ }
@@ -0,0 +1,145 @@
1
+ #!/usr/bin/env bash
2
+ # unoverse check — runs node-pg-migrate to apply all pending migrations
3
+
4
+ cmd_db_setup() {
5
+ banner "Database Setup"
6
+ timer_start
7
+
8
+ # Load DATABASE_URL from .env
9
+ local db_url
10
+ # MIGRATIONS CONNECT DIRECT (INFRASTRUCTURE.md, Postgres law): when the env
11
+ # carries a pooled DATABASE_URL (PgBouncer transaction mode), node-pg-migrate
12
+ # must bypass it — DDL and advisory locks don't ride transaction pooling.
13
+ # DATABASE_URL_DIRECT is rendered by the Terraform alongside the pooled URL.
14
+ db_url=$(grep "^DATABASE_URL_DIRECT=" "$ROOT/.env" 2>/dev/null | cut -d'=' -f2-)
15
+ [ -z "$db_url" ] && db_url=$(grep "^DATABASE_URL=" "$ROOT/.env" 2>/dev/null | cut -d'=' -f2-)
16
+ if [ -z "$db_url" ]; then
17
+ fail "DATABASE_URL not found in .env"
18
+ exit 1
19
+ fi
20
+ ok "DATABASE_URL configured"
21
+
22
+ # Auto-add sslmode=prefer if not specified (works for both SSL and non-SSL Postgres)
23
+ if [[ "$db_url" != *"sslmode="* ]]; then
24
+ if [[ "$db_url" == *"?"* ]]; then
25
+ db_url="${db_url}&sslmode=prefer"
26
+ else
27
+ db_url="${db_url}?sslmode=prefer"
28
+ fi
29
+ fi
30
+
31
+ # Detect environment: monorepo (local dev) vs starter (Docker)
32
+ if [ -d "$ROOT/apps/unoverse/engine" ]; then
33
+ # ── Local dev — run node-pg-migrate directly ──
34
+ echo ""
35
+ echo " Running migrations (local dev)..."
36
+
37
+ NODE_TLS_REJECT_UNAUTHORIZED=0 DATABASE_URL="$db_url" \
38
+ npx node-pg-migrate up \
39
+ --migrations-dir "$ROOT/apps/unoverse/engine/migrations" \
40
+ --migration-file-language sql \
41
+ --no-lock \
42
+ 2>&1 | sed 's/^/ /' || {
43
+ fail "Migrations failed"
44
+ exit 1
45
+ }
46
+
47
+ # Seed security corpus (idempotent)
48
+ _seed_security_corpus "$db_url" "$ROOT/apps/memory/src/security/corpus-seed.json"
49
+
50
+ else
51
+ # ── Starter/production — run via the unoverse container ──
52
+ echo ""
53
+
54
+ if ! docker compose -f "$ROOT/docker-compose.yml" ps --status running unoverse 2>/dev/null | grep -q unoverse; then
55
+ echo ""
56
+ echo -e " ${YELLOW}The unoverse service must be running to apply migrations.${NC}"
57
+ echo -e " ${DIM}(Migration files are bundled inside the unoverse Docker image)${NC}"
58
+ echo ""
59
+ echo -e " Start services first, then re-run:"
60
+ echo -e " ${GREEN}unoverse start${NC}"
61
+ echo -e " ${GREEN}unoverse check${NC}"
62
+ echo ""
63
+ exit 1
64
+ fi
65
+
66
+ echo " Running migrations (via Docker)..."
67
+
68
+ docker compose -f "$ROOT/docker-compose.yml" exec -T \
69
+ -e NODE_TLS_REJECT_UNAUTHORIZED=0 \
70
+ -e DATABASE_URL="$db_url" \
71
+ unoverse \
72
+ npx node-pg-migrate up \
73
+ --migrations-dir /app/apps/unoverse/engine/migrations \
74
+ --migration-file-language sql \
75
+ --no-lock \
76
+ 2>&1 | sed 's/^/ /' || {
77
+ fail "Migrations failed"
78
+ exit 1
79
+ }
80
+
81
+ # Seed security corpus via Docker
82
+ docker compose -f "$ROOT/docker-compose.yml" exec -T \
83
+ -e NODE_TLS_REJECT_UNAUTHORIZED=0 unoverse node --no-warnings -e "
84
+ const fs = require('fs');
85
+ const { Pool } = require('pg');
86
+ (async () => {
87
+ try {
88
+ const seedData = JSON.parse(fs.readFileSync('/app/apps/memory/src/security/corpus-seed.json', 'utf-8'));
89
+ const ssl = process.env.DATABASE_URL.includes('sslmode=') ? { rejectUnauthorized: false } : false;
90
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL, ssl });
91
+ for (const a of seedData) {
92
+ await pool.query(
93
+ 'INSERT INTO security_attack_corpus (id,category,label,attack_prompt,expected_result,severity,source,is_active) VALUES (\$1,\$2,\$3,\$4,\$5,\$6,\$7,true) ON CONFLICT (id) DO NOTHING',
94
+ [a.id, a.category, a.label, a.attack_prompt, a.expected_result, a.severity, a.source]
95
+ );
96
+ }
97
+ console.log(' ✓ Seeded ' + seedData.length + ' security corpus attacks');
98
+ await pool.end();
99
+ } catch (e) { console.warn(' ⚠ Security corpus seed skipped: ' + e.message); }
100
+ process.exit(0);
101
+ })();
102
+ " 2>&1 || true
103
+ fi
104
+
105
+ echo ""
106
+ ok "Database setup complete ${DIM}($(timer_elapsed))${NC}"
107
+ echo ""
108
+
109
+ # Auto-verify schema after setup
110
+ echo " Verifying schema..."
111
+ echo ""
112
+ cmd_db_verify
113
+ }
114
+
115
+ # Seed security attack corpus (local dev helper)
116
+ _seed_security_corpus() {
117
+ local db_url="$1"
118
+ local seed_file="$2"
119
+
120
+ if [ ! -f "$seed_file" ]; then
121
+ echo " ⚠ Security corpus seed file not found: skipping"
122
+ return 0
123
+ fi
124
+
125
+ NODE_TLS_REJECT_UNAUTHORIZED=0 DATABASE_URL="$db_url" node --no-warnings -e "
126
+ const fs = require('fs');
127
+ const { Pool } = require('pg');
128
+ (async () => {
129
+ try {
130
+ const seedData = JSON.parse(fs.readFileSync('$seed_file', 'utf-8'));
131
+ const ssl = process.env.DATABASE_URL.includes('sslmode=') ? { rejectUnauthorized: false } : false;
132
+ const pool = new Pool({ connectionString: process.env.DATABASE_URL, ssl });
133
+ for (const a of seedData) {
134
+ await pool.query(
135
+ 'INSERT INTO security_attack_corpus (id,category,label,attack_prompt,expected_result,severity,source,is_active) VALUES (\$1,\$2,\$3,\$4,\$5,\$6,\$7,true) ON CONFLICT (id) DO NOTHING',
136
+ [a.id, a.category, a.label, a.attack_prompt, a.expected_result, a.severity, a.source]
137
+ );
138
+ }
139
+ console.log(' ✓ Seeded ' + seedData.length + ' security corpus attacks');
140
+ await pool.end();
141
+ } catch (e) { console.warn(' ⚠ Security corpus seed skipped: ' + e.message); }
142
+ process.exit(0);
143
+ })();
144
+ " 2>&1 || true
145
+ }