feat: unify install.sh — single installer for framework + npm CLI
Some checks failed
ci/woodpecker/push/ci Pipeline failed
ci/woodpecker/pr/ci Pipeline failed

- tools/install.sh now installs both components:
  1. Framework (bash launcher, guides, runtime configs) → ~/.config/mosaic/
  2. @mosaic/cli (TUI, gateway client, wizard) → ~/.npm-global/
- Downloads framework from monorepo archive (no bootstrap repo dependency)
- Supports --framework, --cli, --check, --ref flags
- Delete remote-install.sh and remote-install.ps1 (redundant redirectors)
- Update all stale mosaic/bootstrap references → mosaic/mosaic-stack
- Update README.md with monorepo install instructions

Deprecates: mosaic/bootstrap repo
This commit is contained in:
Jarvis
2026-04-01 21:32:19 -05:00
parent 1d731fd3f3
commit de12221d47
8 changed files with 300 additions and 419 deletions

View File

@@ -80,7 +80,7 @@ USAGE
check_mosaic_home() {
if [[ ! -d "$MOSAIC_HOME" ]]; then
echo "[mosaic] ERROR: ~/.config/mosaic not found." >&2
echo "[mosaic] Install with: curl -sL https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.sh | sh" >&2
echo "[mosaic] Install with: bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh)" >&2
exit 1
fi
}
@@ -88,7 +88,7 @@ check_mosaic_home() {
check_agents_md() {
if [[ ! -f "$MOSAIC_HOME/AGENTS.md" ]]; then
echo "[mosaic] ERROR: ~/.config/mosaic/AGENTS.md not found." >&2
echo "[mosaic] Re-run the installer: cd ~/src/mosaic-bootstrap && bash install.sh" >&2
echo "[mosaic] Re-run the installer: bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh)" >&2
exit 1
fi
}

View File

@@ -12,7 +12,7 @@ set -euo pipefail
# mosaic-release-upgrade --ref v0.2.0 --overwrite --yes
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
REMOTE_SCRIPT_URL="${MOSAIC_REMOTE_INSTALL_URL:-https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.sh}"
REMOTE_SCRIPT_URL="${MOSAIC_REMOTE_INSTALL_URL:-https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh}"
BOOTSTRAP_REF="${MOSAIC_BOOTSTRAP_REF:-main}"
INSTALL_MODE="${MOSAIC_INSTALL_MODE:-keep}" # keep|overwrite
YES=false

View File

@@ -19,7 +19,7 @@ $MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:U
$RemoteInstallerUrl = if ($env:MOSAIC_REMOTE_INSTALL_URL) {
$env:MOSAIC_REMOTE_INSTALL_URL
} else {
"https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.ps1"
"https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh"
}
$installMode = if ($Overwrite) { "overwrite" } elseif ($Keep) { "keep" } elseif ($env:MOSAIC_INSTALL_MODE) { $env:MOSAIC_INSTALL_MODE } else { "keep" }

View File

@@ -49,7 +49,7 @@ Options:
function Assert-MosaicHome {
if (-not (Test-Path $MosaicHome)) {
Write-Host "[mosaic] ERROR: ~/.config/mosaic not found." -ForegroundColor Red
Write-Host "[mosaic] Install with: irm https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.ps1 | iex"
Write-Host "[mosaic] Install with: bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh)"
exit 1
}
}

View File

@@ -1,43 +1,41 @@
# Mosaic Agent Framework
Universal agent standards layer for Claude Code, Codex, and OpenCode.
Universal agent standards layer for Claude Code, Codex, OpenCode, and Pi.
One config, every runtime, same standards.
> **This repository is a generic framework baseline.** No personal data, credentials, user-specific preferences, or machine-specific paths should be committed. All personalization happens at install time via `mosaic init` or by editing files in `~/.config/mosaic/` after installation.
> **This is the framework component of [mosaic-stack](https://git.mosaicstack.dev/mosaic/mosaic-stack).** No personal data, credentials, user-specific preferences, or machine-specific paths should be committed. All personalization happens at install time via `mosaic init` or by editing files in `~/.config/mosaic/` after installation.
## Quick Install
### Mac / Linux
```bash
curl -sL https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.sh | sh
bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh)
```
### Windows (PowerShell)
```powershell
irm https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.ps1 | iex
# PowerShell installer coming soon — use WSL + the bash installer above.
```
### From Source (any platform)
```bash
git clone https://git.mosaicstack.dev/mosaic/bootstrap.git ~/src/mosaic-bootstrap
cd ~/src/mosaic-bootstrap && bash install.sh
git clone git@git.mosaicstack.dev:mosaic/mosaic-stack.git ~/src/mosaic-stack
cd ~/src/mosaic-stack && bash tools/install.sh
```
If Node.js 18+ is available, the remote installer automatically uses the TypeScript wizard instead of the bash installer for a richer setup experience.
The installer:
The installer will:
- Install the framework to `~/.config/mosaic/`
- Add `~/.config/mosaic/bin` to your PATH
- Sync runtime adapters and skills
- Install and configure sequential-thinking MCP (hard requirement)
- Run a health audit
- Detect existing installs and prompt to keep or overwrite local files
- Prompt you to run `mosaic init` to set up your agent identity
- Downloads the framework from the monorepo archive
- Installs it to `~/.config/mosaic/`
- Installs `@mosaic/cli` globally via npm (TUI, gateway client, wizard)
- Adds `~/.config/mosaic/bin` to your PATH
- Syncs runtime adapters and skills
- Runs a health audit
- Detects existing installs and preserves local files (SOUL.md, USER.md, etc.)
## First Run
@@ -58,7 +56,7 @@ The wizard configures three files loaded into every agent session:
- `USER.md` — your user profile (name, timezone, accessibility, preferences)
- `TOOLS.md` — machine-level tool reference (git providers, credentials, CLI patterns)
It also detects installed runtimes (Claude, Codex, OpenCode), configures sequential-thinking MCP, and offers curated skill selection from 8 categories.
It also detects installed runtimes (Claude, Codex, OpenCode, Pi), configures sequential-thinking MCP, and offers curated skill selection from 8 categories.
### Non-Interactive Mode
@@ -77,9 +75,12 @@ If Node.js is unavailable, `mosaic init` falls back to the bash-based `mosaic-in
## Launching Agent Sessions
```bash
mosaic pi # Launch Pi with full Mosaic injection (recommended)
mosaic claude # Launch Claude Code with full Mosaic injection
mosaic codex # Launch Codex with full Mosaic injection
mosaic opencode # Launch OpenCode with full Mosaic injection
mosaic yolo claude # Launch Claude in dangerous-permissions mode
mosaic yolo pi # Launch Pi in yolo mode
```
The launcher:
@@ -100,23 +101,18 @@ You can still launch runtimes directly (`claude`, `codex`, etc.) — thin runtim
├── USER.md ← User profile and accessibility (generated by mosaic init)
├── TOOLS.md ← Machine-level tool reference (generated by mosaic init)
├── STANDARDS.md ← Machine-wide standards
├── guides/E2E-DELIVERY.md ← Mandatory E2E software delivery procedure
├── guides/PRD.md ← Mandatory PRD requirements gate before coding
├── guides/DOCUMENTATION.md ← Mandatory documentation standard and gates
├── bin/ ← CLI tools (mosaic, mosaic-init, mosaic-doctor, etc.)
├── dist/ ← Bundled wizard (mosaic-wizard.mjs)
├── guides/ ← Operational guides
├── tools/ ← Tool suites: git, portainer, authentik, coolify, codex, etc.
├── guides/ ← Operational guides (E2E delivery, PRD, docs, etc.)
├── bin/ ← CLI tools (mosaic launcher, mosaic-init, mosaic-doctor, etc.)
├── tools/ ← Tool suites: git, orchestrator, prdy, quality, etc.
├── runtime/ ← Runtime adapters + runtime-specific references
│ ├── claude/CLAUDE.md
│ ├── claude/RUNTIME.md
│ ├── opencode/AGENTS.md
│ ├── opencode/RUNTIME.md
── codex/instructions.md
│ ├── codex/RUNTIME.md
│ └── mcp/SEQUENTIAL-THINKING.json
│ ├── claude/ ← CLAUDE.md, RUNTIME.md, settings.json, hooks
│ ├── codex/ ← instructions.md, RUNTIME.md
│ ├── opencode/ ← AGENTS.md, RUNTIME.md
│ ├── pi/ ← RUNTIME.md, mosaic-extension.ts
── mcp/ ← MCP server configs
├── skills/ ← Universal skills (synced from mosaic/agent-skills)
├── skills-local/ ← Local cross-runtime skills
├── memory/ ← Persistent agent memory (preserved across upgrades)
└── templates/ ← SOUL.md template, project templates
```
@@ -124,6 +120,7 @@ You can still launch runtimes directly (`claude`, `codex`, etc.) — thin runtim
| Launch method | Injection mechanism |
| ------------------- | ----------------------------------------------------------------------------------------- |
| `mosaic pi` | `--append-system-prompt` with composed runtime contract + skills + extension |
| `mosaic claude` | `--append-system-prompt` with composed runtime contract (`AGENTS.md` + runtime reference) |
| `mosaic codex` | Writes composed runtime contract to `~/.codex/instructions.md` before launch |
| `mosaic opencode` | Writes composed runtime contract to `~/.config/opencode/AGENTS.md` before launch |
@@ -131,9 +128,6 @@ You can still launch runtimes directly (`claude`, `codex`, etc.) — thin runtim
| `codex` (direct) | `~/.codex/instructions.md` thin pointer → load AGENTS + runtime reference |
| `opencode` (direct) | `~/.config/opencode/AGENTS.md` thin pointer → load AGENTS + runtime reference |
Mosaic `AGENTS.md` enforces loading `guides/E2E-DELIVERY.md` before execution and
requires `guides/PRD.md` before coding and `guides/DOCUMENTATION.md` for code/API/auth/infra documentation gates.
## Management Commands
```bash
@@ -142,126 +136,53 @@ mosaic init # Interactive wizard (or legacy init)
mosaic doctor # Health audit — detect drift and missing files
mosaic sync # Sync skills from canonical source
mosaic bootstrap <path> # Bootstrap a repo with Mosaic standards
mosaic upgrade check # Check release upgrade status (no changes)
mosaic upgrade # Upgrade installed Mosaic release (keeps SOUL.md by default)
mosaic upgrade --dry-run # Preview release upgrade without changes
mosaic upgrade --ref main # Upgrade from a specific branch/tag/commit ref
mosaic upgrade --overwrite # Upgrade release and overwrite local files
mosaic upgrade project ... # Project file cleanup mode (see below)
mosaic upgrade # Upgrade installed Mosaic release
mosaic upgrade check # Check upgrade status (no changes)
```
## Upgrading Mosaic Release
## Upgrading
Upgrade the installed framework in place:
Run the installer again — it handles upgrades automatically:
```bash
# Default (safe): keep local SOUL.md, USER.md, TOOLS.md + memory
mosaic upgrade
# Check current/target release info without changing files
mosaic upgrade check
# Non-interactive
mosaic upgrade --yes
# Pull a specific ref
mosaic upgrade --ref main
# Force full overwrite (fresh install semantics)
mosaic upgrade --overwrite --yes
bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh)
```
`mosaic upgrade` re-runs the remote installer and passes install mode controls (`keep`/`overwrite`).
This is the manual upgrade path today and is suitable for future app-driven update checks.
## Upgrading Projects
After centralizing AGENTS.md and SOUL.md, existing projects may have stale files:
Or from a local checkout:
```bash
# Preview what would change across all projects
mosaic upgrade project --all --dry-run
# Apply to all projects
mosaic upgrade project --all
# Apply to a specific project
mosaic upgrade project ~/src/my-project
cd ~/src/mosaic-stack && git pull && bash tools/install.sh
```
Backward compatibility is preserved for historical usage:
The installer preserves local `SOUL.md`, `USER.md`, `TOOLS.md`, and `memory/` by default.
### Flags
```bash
mosaic upgrade --all # still routes to project-upgrade
mosaic upgrade ~/src/my-repo # still routes to project-upgrade
bash tools/install.sh --check # Version check only
bash tools/install.sh --framework # Framework only (skip npm CLI)
bash tools/install.sh --cli # npm CLI only (skip framework)
bash tools/install.sh --ref v1.0 # Install from a specific git ref
```
What it does per project:
| File | Action |
| ----------- | ------------------------------------------------------------- |
| `SOUL.md` | Removed — now global at `~/.config/mosaic/SOUL.md` |
| `CLAUDE.md` | Replaced with thin pointer to global AGENTS.md |
| `AGENTS.md` | Stale load-order sections stripped; project content preserved |
Backups (`.mosaic-bak`) are created before any modification.
## Universal Skills
The installer syncs skills from `mosaic/agent-skills` into `~/.config/mosaic/skills/`, then links each skill into runtime directories (`~/.claude/skills`, `~/.codex/skills`, `~/.config/opencode/skills`).
The installer syncs skills from `mosaic/agent-skills` into `~/.config/mosaic/skills/`, then links each skill into runtime directories.
```bash
mosaic sync # Full sync (clone + link)
~/.config/mosaic/bin/mosaic-sync-skills --link-only # Re-link only
```
## Runtime Compatibility
The installer pushes thin runtime adapters as regular files (not symlinks):
- `~/.claude/CLAUDE.md` — pointer to `~/.config/mosaic/AGENTS.md`
- `~/.claude/settings.json`, `hooks-config.json`, `context7-integration.md`
- `~/.config/opencode/AGENTS.md` — pointer to `~/.config/mosaic/AGENTS.md`
- `~/.codex/instructions.md` — pointer to `~/.config/mosaic/AGENTS.md`
- `~/.claude/settings.json`, `~/.codex/config.toml`, and `~/.config/opencode/config.json` include sequential-thinking MCP config
Re-sync manually:
## Health Audit
```bash
~/.config/mosaic/bin/mosaic-link-runtime-assets
mosaic doctor # Standard audit
~/.config/mosaic/bin/mosaic-doctor --fail-on-warn # Strict mode
```
## MCP Registration
### How MCPs Are Configured in Claude Code
**MCPs must be registered via `claude mcp add` — not by hand-editing `~/.claude/settings.json`.**
`settings.json` controls hooks, model, plugins, and allowed commands. The `mcpServers` key in
`settings.json` is silently ignored by Claude Code's MCP loader. The correct file is `~/.claude.json`,
which is managed by the `claude mcp` CLI.
```bash
# Register a stdio MCP (user scope = all projects, persists across sessions)
claude mcp add --scope user <name> -- npx -y <package>
# Register an HTTP MCP (e.g. OpenBrain)
claude mcp add --scope user --transport http <name> <url> \
--header "Authorization: Bearer <token>"
# List registered MCPs
claude mcp list
```
**Scope options:**
- `--scope user` — writes to `~/.claude.json`, available in all projects (recommended for shared tools)
- `--scope project` — writes to `.claude/settings.json` in the project root, committed to the repo
- `--scope local` — default, machine-local only, not committed
**Transport for HTTP MCPs must be `http`** — not `sse`. `type: "sse"` is a deprecated protocol
that silently fails to connect against FastMCP streamable HTTP servers.
### sequential-thinking MCP (Hard Requirement)
sequential-thinking MCP is required for Mosaic Stack. The installer registers it automatically.
@@ -272,74 +193,12 @@ To verify or re-register manually:
~/.config/mosaic/bin/mosaic-ensure-sequential-thinking --check
```
### OpenBrain Semantic Memory (Recommended)
### Claude Code MCP Registration
OpenBrain is the shared cross-agent memory layer. Register once per machine:
**MCPs must be registered via `claude mcp add` — not by hand-editing `~/.claude/settings.json`.**
```bash
claude mcp add --scope user --transport http openbrain https://your-openbrain-host/mcp \
--header "Authorization: Bearer YOUR_TOKEN"
```
See [mosaic/openbrain](https://git.mosaicstack.dev/mosaic/openbrain) for setup and API docs.
## Bootstrap Any Repo
Attach any repository to the Mosaic standards layer:
```bash
mosaic bootstrap /path/to/repo
```
This creates `.mosaic/`, `scripts/agent/`, and an `AGENTS.md` if missing.
## Quality Rails
Apply and verify quality templates:
```bash
~/.config/mosaic/bin/mosaic-quality-apply --template typescript-node --target /path/to/repo
~/.config/mosaic/bin/mosaic-quality-verify --target /path/to/repo
```
Templates: `typescript-node`, `typescript-nextjs`, `monorepo`
## Health Audit
```bash
mosaic doctor # Standard audit
~/.config/mosaic/bin/mosaic-doctor --fail-on-warn # Strict mode
```
## Wizard Development
The installation wizard is a TypeScript project in the root of this repo.
```bash
pnpm install # Install dependencies
pnpm dev # Run wizard from source (tsx)
pnpm build # Bundle to dist/mosaic-wizard.mjs
pnpm test # Run tests (30 tests, vitest)
pnpm typecheck # TypeScript type checking
```
The wizard uses `@clack/prompts` for the interactive TUI and supports `--non-interactive` mode via `HeadlessPrompter` for CI and scripted installs. The bundled output (`dist/mosaic-wizard.mjs`) is committed to the repo so installs work without `node_modules`.
## Re-installing / Updating
Pull the latest and re-run the installer:
```bash
cd ~/src/mosaic-bootstrap && git pull && bash install.sh
```
If an existing install is detected, the installer prompts for:
- `keep` (recommended): preserve local `SOUL.md`, `USER.md`, `TOOLS.md`, and `memory/`
- `overwrite`: replace everything in `~/.config/mosaic`
Or use the one-liner again — it always pulls the latest:
```bash
curl -sL https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.sh | sh
claude mcp add --scope user <name> -- npx -y <package>
claude mcp add --scope user --transport http <name> <url> --header "Authorization: Bearer <token>"
claude mcp list
```

View File

@@ -1,38 +0,0 @@
# Mosaic Bootstrap — Remote Installer (Windows PowerShell)
#
# One-liner:
# irm https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.ps1 | iex
#
# Or explicit:
# powershell -ExecutionPolicy Bypass -File remote-install.ps1
#
$ErrorActionPreference = "Stop"
$BootstrapRef = if ($env:MOSAIC_BOOTSTRAP_REF) { $env:MOSAIC_BOOTSTRAP_REF } else { "main" }
$ArchiveUrl = "https://git.mosaicstack.dev/mosaic/bootstrap/archive/$BootstrapRef.zip"
$WorkDir = Join-Path $env:TEMP "mosaic-bootstrap-$PID"
$ZipPath = "$WorkDir.zip"
try {
Write-Host "[mosaic] Downloading bootstrap archive (ref: $BootstrapRef)..."
New-Item -ItemType Directory -Path $WorkDir -Force | Out-Null
Invoke-WebRequest -Uri $ArchiveUrl -OutFile $ZipPath -UseBasicParsing
Write-Host "[mosaic] Extracting..."
Expand-Archive -Path $ZipPath -DestinationPath $WorkDir -Force
$InstallScript = Join-Path $WorkDir "bootstrap\install.ps1"
if (-not (Test-Path $InstallScript)) {
throw "install.ps1 not found in archive"
}
Write-Host "[mosaic] Running install..."
& $InstallScript
Write-Host "[mosaic] Done."
}
finally {
Write-Host "[mosaic] Cleaning up temporary files..."
Remove-Item -Path $ZipPath -Force -ErrorAction SilentlyContinue
Remove-Item -Path $WorkDir -Recurse -Force -ErrorAction SilentlyContinue
}

View File

@@ -1,63 +0,0 @@
#!/usr/bin/env sh
# Mosaic Bootstrap — Remote Installer (POSIX)
#
# One-liner:
# curl -sL https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.sh | sh
#
# Or with wget:
# wget -qO- https://git.mosaicstack.dev/mosaic/bootstrap/raw/branch/main/remote-install.sh | sh
#
set -eu
BOOTSTRAP_REF="${MOSAIC_BOOTSTRAP_REF:-main}"
ARCHIVE_URL="https://git.mosaicstack.dev/mosaic/bootstrap/archive/${BOOTSTRAP_REF}.tar.gz"
TMPDIR_BASE="${TMPDIR:-/tmp}"
WORK_DIR="$TMPDIR_BASE/mosaic-bootstrap-$$"
cleanup() {
rm -rf "$WORK_DIR"
}
trap cleanup EXIT
echo "[mosaic] Downloading bootstrap archive (ref: $BOOTSTRAP_REF)..."
mkdir -p "$WORK_DIR"
if command -v curl >/dev/null 2>&1; then
curl -sL "$ARCHIVE_URL" | tar xz -C "$WORK_DIR"
elif command -v wget >/dev/null 2>&1; then
wget -qO- "$ARCHIVE_URL" | tar xz -C "$WORK_DIR"
else
echo "[mosaic] ERROR: curl or wget required" >&2
exit 1
fi
if [ ! -f "$WORK_DIR/bootstrap/install.sh" ]; then
echo "[mosaic] ERROR: install.sh not found in archive" >&2
exit 1
fi
cd "$WORK_DIR/bootstrap"
# Prefer TypeScript wizard if Node.js 18+ and bundle are available
WIZARD_BIN="$WORK_DIR/bootstrap/dist/mosaic-wizard.mjs"
if command -v node >/dev/null 2>&1 && [ -f "$WIZARD_BIN" ]; then
NODE_MAJOR="$(node -e 'console.log(process.versions.node.split(".")[0])')"
if [ "$NODE_MAJOR" -ge 18 ] 2>/dev/null; then
if [ -e /dev/tty ]; then
echo "[mosaic] Running wizard installer (Node.js $NODE_MAJOR detected)..."
node "$WIZARD_BIN" --source-dir "$WORK_DIR/bootstrap" </dev/tty
else
echo "[mosaic] Running wizard installer in non-interactive mode (no TTY)..."
node "$WIZARD_BIN" --source-dir "$WORK_DIR/bootstrap" --non-interactive
fi
echo "[mosaic] Cleaning up temporary files..."
exit 0
fi
fi
echo "[mosaic] Running legacy install..."
bash install.sh </dev/tty
echo "[mosaic] Cleaning up temporary files..."
# cleanup runs via trap

View File

@@ -1,45 +1,69 @@
#!/usr/bin/env bash
# ─── Mosaic Stack Installer / Upgrader ────────────────────────────────────────
#
# Installs both components:
# 1. Mosaic framework → ~/.config/mosaic/ (bash launcher, guides, runtime configs, tools)
# 2. @mosaic/cli (npm) → ~/.npm-global/ (TUI, gateway client, wizard)
#
# Remote install (recommended):
# bash <(curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh)
#
# Remote install (alternative — use -s -- to pass flags):
# curl -fsSL https://git.mosaicstack.dev/mosaic/mosaic-stack/raw/branch/main/tools/install.sh | bash -s --
# curl -fsSL ... | bash -s -- --check
#
# Local (from repo checkout):
# bash tools/install.sh
# bash tools/install.sh --check # version check only
# Flags:
# --check Version check only, no install
# --framework Install/upgrade framework only (skip npm CLI)
# --cli Install/upgrade npm CLI only (skip framework)
# --ref <branch> Git ref for framework archive (default: main)
#
# Environment:
# MOSAIC_REGISTRY — npm registry URL (default: Gitea instance)
# MOSAIC_SCOPE — npm scope (default: @mosaic)
# MOSAIC_PREFIX — npm global prefix (default: ~/.npm-global)
# MOSAIC_NO_COLOR — disable colour (set to 1)
# MOSAIC_HOME — framework install dir (default: ~/.config/mosaic)
# MOSAIC_REGISTRY — npm registry URL (default: Gitea instance)
# MOSAIC_SCOPE — npm scope (default: @mosaic)
# MOSAIC_PREFIX — npm global prefix (default: ~/.npm-global)
# MOSAIC_NO_COLOR — disable colour (set to 1)
# MOSAIC_REF — git ref for framework (default: main)
# ──────────────────────────────────────────────────────────────────────────────
#
# The entire script is wrapped in main() so that bash reads it fully into
# memory before execution — required for safe curl-pipe usage.
# Wrapped in main() for safe curl-pipe usage.
set -euo pipefail
main() {
# ─── parse flags ──────────────────────────────────────────────────────────────
FLAG_CHECK=false
FLAG_FRAMEWORK=true
FLAG_CLI=true
GIT_REF="${MOSAIC_REF:-main}"
while [[ $# -gt 0 ]]; do
case "$1" in
--check) FLAG_CHECK=true; shift ;;
--framework) FLAG_CLI=false; shift ;;
--cli) FLAG_FRAMEWORK=false; shift ;;
--ref) GIT_REF="${2:-main}"; shift 2 ;;
*) shift ;;
esac
done
# ─── constants ────────────────────────────────────────────────────────────────
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
REGISTRY="${MOSAIC_REGISTRY:-https://git.mosaicstack.dev/api/packages/mosaic/npm/}"
SCOPE="${MOSAIC_SCOPE:-@mosaic}"
PREFIX="${MOSAIC_PREFIX:-$HOME/.npm-global}"
CLI_PKG="${SCOPE}/cli"
REPO_BASE="https://git.mosaicstack.dev/mosaic/mosaic-stack"
ARCHIVE_URL="${REPO_BASE}/archive/${GIT_REF}.tar.gz"
# ─── colours ──────────────────────────────────────────────────────────────────
# Detect colour support: works in terminals and bash <(curl …), but correctly
# disables when piped through a non-tty (curl … | bash).
if [[ "${MOSAIC_NO_COLOR:-0}" == "1" ]] || ! [[ -t 1 ]]; then
R="" G="" Y="" B="" C="" DIM="" RESET=""
R="" G="" Y="" B="" C="" DIM="" BOLD="" RESET=""
else
R=$'\033[0;31m' G=$'\033[0;32m' Y=$'\033[0;33m'
B=$'\033[0;34m' C=$'\033[0;36m' DIM=$'\033[2m' RESET=$'\033[0m'
B=$'\033[0;34m' C=$'\033[0;36m' DIM=$'\033[2m'
BOLD=$'\033[1m' RESET=$'\033[0m'
fi
info() { echo "${B}${RESET} $*"; }
@@ -47,6 +71,7 @@ ok() { echo "${G}✔${RESET} $*"; }
warn() { echo "${Y}${RESET} $*"; }
fail() { echo "${R}${RESET} $*" >&2; }
dim() { echo "${DIM}$*${RESET}"; }
step() { echo ""; echo "${BOLD}$*${RESET}"; }
# ─── helpers ──────────────────────────────────────────────────────────────────
@@ -58,12 +83,10 @@ require_cmd() {
fi
}
# Get the installed version of @mosaic/cli (empty string if not installed)
installed_version() {
installed_cli_version() {
local json
json="$(npm ls -g --depth=0 --json --prefix="$PREFIX" 2>/dev/null)" || true
if [[ -n "$json" ]]; then
# Feed json via heredoc, not stdin pipe — safe in curl-pipe context
node -e "
const d = JSON.parse(process.argv[1]);
const v = d?.dependencies?.['${CLI_PKG}']?.version ?? '';
@@ -72,14 +95,11 @@ installed_version() {
fi
}
# Get the latest published version from the registry
latest_version() {
latest_cli_version() {
npm view "${CLI_PKG}" version --registry="$REGISTRY" 2>/dev/null || true
}
# Compare two semver strings: returns 0 (true) if $1 < $2
version_lt() {
# Semver-aware comparison via node (handles pre-release correctly)
node -e "
const a=process.argv[1], b=process.argv[2];
const sp = v => { const i=v.indexOf('-'); return i===-1 ? [v,null] : [v.slice(0,i),v.slice(i+1)]; };
@@ -93,6 +113,14 @@ version_lt() {
" "$1" "$2" 2>/dev/null
}
framework_version() {
# Read VERSION from the installed mosaic launcher
local mosaic_bin="$MOSAIC_HOME/bin/mosaic"
if [[ -f "$mosaic_bin" ]]; then
grep -m1 '^VERSION=' "$mosaic_bin" 2>/dev/null | cut -d'"' -f2 || true
fi
}
# ─── preflight ────────────────────────────────────────────────────────────────
require_cmd node
@@ -104,116 +132,211 @@ if [[ "$NODE_MAJOR" -lt 20 ]]; then
exit 1
fi
# ─── ensure prefix directory exists ──────────────────────────────────────────
if [[ ! -d "$PREFIX" ]]; then
info "Creating global prefix directory: $PREFIX"
mkdir -p "$PREFIX"/{bin,lib}
fi
# ─── ensure npmrc scope mapping ──────────────────────────────────────────────
NPMRC="$HOME/.npmrc"
SCOPE_LINE="${SCOPE}:registry=${REGISTRY}"
if ! grep -qF "$SCOPE_LINE" "$NPMRC" 2>/dev/null; then
info "Adding ${SCOPE} registry to $NPMRC"
echo "$SCOPE_LINE" >> "$NPMRC"
ok "Registry configured"
fi
# Ensure prefix is set (only if no prefix= line exists yet)
if ! grep -qF "prefix=$PREFIX" "$NPMRC" 2>/dev/null; then
if ! grep -q '^prefix=' "$NPMRC" 2>/dev/null; then
echo "prefix=$PREFIX" >> "$NPMRC"
info "Set npm global prefix to $PREFIX"
fi
fi
# Ensure PREFIX/bin is on PATH
if [[ ":$PATH:" != *":$PREFIX/bin:"* ]]; then
warn "$PREFIX/bin is not on your PATH"
dim " Add to your shell rc: export PATH=\"$PREFIX/bin:\$PATH\""
fi
# ─── version check ───────────────────────────────────────────────────────────
info "Checking versions…"
CURRENT="$(installed_version)"
LATEST="$(latest_version)"
if [[ -z "$LATEST" ]]; then
fail "Could not reach registry at $REGISTRY"
fail "Check network connectivity and registry URL."
exit 1
fi
echo ""
if [[ -n "$CURRENT" ]]; then
dim " Installed: ${CLI_PKG}@${CURRENT}"
else
dim " Installed: (none)"
fi
dim " Latest: ${CLI_PKG}@${LATEST}"
echo "${BOLD}Mosaic Stack Installer${RESET}"
echo ""
# --check flag: just report, don't install
if [[ "${1:-}" == "--check" ]]; then
if [[ -z "$CURRENT" ]]; then
warn "Not installed. Run without --check to install."
exit 1
elif [[ "$CURRENT" == "$LATEST" ]]; then
ok "Up to date."
exit 0
elif version_lt "$CURRENT" "$LATEST"; then
warn "Update available: $CURRENT$LATEST"
exit 2
# ═══════════════════════════════════════════════════════════════════════════════
# PART 1: Framework (bash launcher + guides + runtime configs + tools)
# ═══════════════════════════════════════════════════════════════════════════════
if [[ "$FLAG_FRAMEWORK" == "true" ]]; then
step "Framework (~/.config/mosaic)"
FRAMEWORK_CURRENT="$(framework_version)"
HAS_FRAMEWORK=false
[[ -d "$MOSAIC_HOME/bin" ]] && [[ -f "$MOSAIC_HOME/bin/mosaic" ]] && HAS_FRAMEWORK=true
if [[ -n "$FRAMEWORK_CURRENT" ]]; then
dim " Installed: framework v${FRAMEWORK_CURRENT}"
elif [[ "$HAS_FRAMEWORK" == "true" ]]; then
dim " Installed: framework (version unknown)"
else
ok "Up to date (or ahead of registry)."
exit 0
dim " Installed: (none)"
fi
fi
# ─── install / upgrade ──────────────────────────────────────────────────────
if [[ -z "$CURRENT" ]]; then
info "Installing ${CLI_PKG}@${LATEST}"
elif [[ "$CURRENT" == "$LATEST" ]]; then
ok "Already at latest version ($LATEST). Nothing to do."
exit 0
elif version_lt "$CURRENT" "$LATEST"; then
info "Upgrading ${CLI_PKG}: $CURRENT$LATEST"
else
ok "Installed version ($CURRENT) is at or ahead of registry ($LATEST)."
exit 0
fi
# NOTE: Do NOT pass --registry here. The @mosaic scope is already mapped
# in ~/.npmrc. Passing --registry globally would redirect ALL deps (including
# @clack/prompts, commander, etc.) to the Gitea registry, causing 404s.
npm install -g "${CLI_PKG}@${LATEST}" \
--prefix="$PREFIX" \
2>&1 | sed 's/^/ /'
echo ""
ok "Mosaic CLI installed: $(installed_version)"
dim " Binary: $PREFIX/bin/mosaic"
# ─── post-install: run wizard if first install ───────────────────────────────
if [[ -z "$CURRENT" ]]; then
dim " Source: ${REPO_BASE} (ref: ${GIT_REF})"
echo ""
info "First install detected."
if [[ -t 0 ]] && [[ -t 1 ]]; then
echo " Run ${C}mosaic wizard${RESET} to set up your configuration."
if [[ "$FLAG_CHECK" == "true" ]]; then
if [[ "$HAS_FRAMEWORK" == "true" ]]; then
ok "Framework is installed."
else
warn "Framework not installed."
fi
else
dim " Run 'mosaic wizard' to set up your configuration."
# Download repo archive and extract framework
require_cmd tar
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-install-XXXXXX")"
cleanup_work() { rm -rf "$WORK_DIR"; }
trap cleanup_work EXIT
info "Downloading framework from ${GIT_REF}"
if command -v curl &>/dev/null; then
curl -fsSL "$ARCHIVE_URL" | tar xz -C "$WORK_DIR"
elif command -v wget &>/dev/null; then
wget -qO- "$ARCHIVE_URL" | tar xz -C "$WORK_DIR"
else
fail "curl or wget required to download framework."
exit 1
fi
# Gitea archives extract to <repo-name>/ inside the work dir
EXTRACTED_DIR="$(find "$WORK_DIR" -maxdepth 1 -mindepth 1 -type d | head -1)"
FRAMEWORK_SRC="$EXTRACTED_DIR/packages/mosaic/framework"
if [[ ! -d "$FRAMEWORK_SRC" ]]; then
fail "Framework not found in archive at packages/mosaic/framework/"
fail "Archive contents:"
ls -la "$WORK_DIR" >&2
exit 1
fi
# Run the framework's own install.sh (handles keep/overwrite for SOUL.md etc.)
info "Installing framework to ${MOSAIC_HOME}"
MOSAIC_INSTALL_MODE="${MOSAIC_INSTALL_MODE:-keep}" \
MOSAIC_ALLOW_MISSING_SEQUENTIAL_THINKING=1 \
MOSAIC_SKIP_SKILLS_SYNC="${MOSAIC_SKIP_SKILLS_SYNC:-0}" \
bash "$FRAMEWORK_SRC/install.sh"
ok "Framework installed"
echo ""
# Ensure framework bin is on PATH
FRAMEWORK_BIN="$MOSAIC_HOME/bin"
if [[ ":$PATH:" != *":$FRAMEWORK_BIN:"* ]]; then
warn "$FRAMEWORK_BIN is not on your PATH"
dim " The 'mosaic' launcher lives here. Add to your shell rc:"
dim " export PATH=\"$FRAMEWORK_BIN:\$PATH\""
fi
fi
fi
echo ""
ok "Done."
# ═══════════════════════════════════════════════════════════════════════════════
# PART 2: @mosaic/cli (npm — TUI, gateway client, wizard)
# ═══════════════════════════════════════════════════════════════════════════════
if [[ "$FLAG_CLI" == "true" ]]; then
step "@mosaic/cli (npm package)"
# Ensure prefix dir
if [[ ! -d "$PREFIX" ]]; then
info "Creating global prefix directory: $PREFIX"
mkdir -p "$PREFIX"/{bin,lib}
fi
# Ensure npmrc scope mapping
NPMRC="$HOME/.npmrc"
SCOPE_LINE="${SCOPE}:registry=${REGISTRY}"
if ! grep -qF "$SCOPE_LINE" "$NPMRC" 2>/dev/null; then
info "Adding ${SCOPE} registry to $NPMRC"
echo "$SCOPE_LINE" >> "$NPMRC"
ok "Registry configured"
fi
if ! grep -qF "prefix=$PREFIX" "$NPMRC" 2>/dev/null; then
if ! grep -q '^prefix=' "$NPMRC" 2>/dev/null; then
echo "prefix=$PREFIX" >> "$NPMRC"
info "Set npm global prefix to $PREFIX"
fi
fi
CURRENT="$(installed_cli_version)"
LATEST="$(latest_cli_version)"
if [[ -n "$CURRENT" ]]; then
dim " Installed: ${CLI_PKG}@${CURRENT}"
else
dim " Installed: (none)"
fi
if [[ -n "$LATEST" ]]; then
dim " Latest: ${CLI_PKG}@${LATEST}"
else
dim " Latest: (registry unreachable)"
fi
echo ""
if [[ "$FLAG_CHECK" == "true" ]]; then
if [[ -z "$LATEST" ]]; then
warn "Could not reach registry."
elif [[ -z "$CURRENT" ]]; then
warn "Not installed."
elif [[ "$CURRENT" == "$LATEST" ]]; then
ok "Up to date."
elif version_lt "$CURRENT" "$LATEST"; then
warn "Update available: $CURRENT$LATEST"
else
ok "Up to date (or ahead of registry)."
fi
else
if [[ -z "$LATEST" ]]; then
warn "Could not reach registry at $REGISTRY — skipping npm CLI."
elif [[ -z "$CURRENT" ]]; then
info "Installing ${CLI_PKG}@${LATEST}"
npm install -g "${CLI_PKG}@${LATEST}" --prefix="$PREFIX" 2>&1 | sed 's/^/ /'
ok "CLI installed: $(installed_cli_version)"
elif [[ "$CURRENT" == "$LATEST" ]]; then
ok "Already at latest version ($LATEST)."
elif version_lt "$CURRENT" "$LATEST"; then
info "Upgrading ${CLI_PKG}: $CURRENT$LATEST"
npm install -g "${CLI_PKG}@${LATEST}" --prefix="$PREFIX" 2>&1 | sed 's/^/ /'
ok "CLI upgraded: $(installed_cli_version)"
else
ok "CLI is at or ahead of registry ($CURRENT$LATEST)."
fi
# PATH check for npm prefix
if [[ ":$PATH:" != *":$PREFIX/bin:"* ]]; then
warn "$PREFIX/bin is not on your PATH"
dim " The 'mosaic' TUI/gateway CLI lives here (separate from the launcher)."
dim " Add to your shell rc: export PATH=\"$PREFIX/bin:\$PATH\""
fi
fi
fi
# ═══════════════════════════════════════════════════════════════════════════════
# Summary
# ═══════════════════════════════════════════════════════════════════════════════
if [[ "$FLAG_CHECK" == "false" ]]; then
step "Summary"
echo " ${BOLD}Framework launcher:${RESET} $MOSAIC_HOME/bin/mosaic"
echo " ${DIM}mosaic claude, mosaic yolo claude, mosaic pi, mosaic doctor, …${RESET}"
echo ""
echo " ${BOLD}npm CLI (TUI):${RESET} $PREFIX/bin/mosaic"
echo " ${DIM}mosaic tui, mosaic login, mosaic wizard, mosaic update, …${RESET}"
echo ""
# Warn if there's a naming collision (both on PATH)
FRAMEWORK_BIN="$MOSAIC_HOME/bin"
if [[ ":$PATH:" == *":$FRAMEWORK_BIN:"* ]] && [[ ":$PATH:" == *":$PREFIX/bin:"* ]]; then
# Check which one wins
WHICH_MOSAIC="$(command -v mosaic 2>/dev/null || true)"
if [[ -n "$WHICH_MOSAIC" ]]; then
dim " Active 'mosaic' binary: $WHICH_MOSAIC"
if [[ "$WHICH_MOSAIC" == "$FRAMEWORK_BIN/mosaic" ]]; then
dim " (Framework launcher takes priority — this is correct)"
else
warn "npm CLI shadows the framework launcher!"
dim " Ensure $FRAMEWORK_BIN appears BEFORE $PREFIX/bin in your PATH."
fi
fi
fi
# First install guidance
if [[ ! -f "$MOSAIC_HOME/SOUL.md" ]]; then
echo ""
info "First install detected. Set up your agent identity:"
echo " ${C}mosaic init${RESET} (interactive SOUL.md / USER.md setup)"
echo " ${C}mosaic wizard${RESET} (full guided wizard via Node.js)"
fi
echo ""
ok "Done."
fi
} # end main