ci/woodpecker/pr/ci Pipeline was successful
`mosaic fleet --help` reads "Manage the local Mosaic tmux fleet" and every roster the CLI scaffolds sets `transport: tmux`, but neither `tools/install.sh` nor `tools/_scripts/mosaic-doctor` contained the string "tmux" at all. A greenfield host therefore came out of the installer able to install a fleet, start a fleet, and run no seat, with `mosaic fleet ps` as the operator's first and only signal. Measured on mosaic-sbx-dev (Debian, no tmux, framework installed): `mosaic-doctor` reported 11 warnings and not one of them named the reason no seat could launch. The installer gets a warning, not a `require_cmd` hard failure: tmux is required by the fleet, not by mosaic. Hosts that install this to run `mosaic claude` and never scaffold a roster are common, and failing their install over a binary they do not need would be wrong. The check runs in `--check` mode too — "what is the state of this host" is the question `--check` is asked. Both checks read the roster's own `transport:` rather than assuming tmux, so a host declaring something else is pointed at the binary it actually needs instead of at the wrong package. The two implementations are deliberately parallel and each carries a comment pointing at the other. They are separate because the installer must answer this before the framework's own scripts are guaranteed to be on disk. One harness drives BOTH from the shipped text — the functions are extracted from the scripts by awk rather than copied — so the pair cannot drift silently, and the test cannot keep passing after the shipped copy changes. The harness is wired into `test:framework-shell`. Without that it would have tripped the #1017 enumeration guard as UNENUMERATED, which is the guard doing its job: a check nothing runs is not a check. Evidence: - red: the harness fails against origin/next ("could not extract fleet_declared_transport"); `grep -ci tmux` on both files at origin/next = 0. - green on real hosts, all four branches: - dev (no tmux, no roster) -> WARN naming tmux, points at `mosaic fleet init` - dev (no tmux, v2 roster) -> WARN naming the roster, points at `mosaic fleet start` - dev installer --check -> WARN saying start "reports success and no seat comes up" - canary (tmux present) -> `[OK] Fleet transport available: tmux` under --verbose, silent by default (pass() is verbose-gated), installer silent - harness green on node:24-alpine/busybox, the CI base image. - `bash -n` x3, `pnpm typecheck` 45/45, fleet specs 342 passed, enumeration guard OK, its self-test OK, prettier clean. Refs #1240. Upstream of #1237/#1243 and #1241/#1244: a correct fix for either of those still leaves this host with no live seat.
918 lines
35 KiB
Bash
Executable File
918 lines
35 KiB
Bash
Executable File
#!/usr/bin/env bash
|
||
# ─── Mosaic Stack Installer / Upgrader ────────────────────────────────────────
|
||
#
|
||
# Installs both components:
|
||
# 1. Mosaic framework → ~/.config/mosaic/ (bash launcher, guides, runtime configs, tools)
|
||
# 2. @mosaicstack/mosaic (npm) → ~/.npm-global/ (CLI, TUI, gateway client, wizard)
|
||
#
|
||
# Quick: curl -fsSL https://mosaicstack.dev/install.sh | bash
|
||
# Direct: bash <(curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh)
|
||
#
|
||
# Remote install (alternative — use -s -- to pass flags):
|
||
# curl -fsSL https://git.mosaicstack.dev/mosaicstack/stack/raw/branch/main/tools/install.sh | bash -s --
|
||
#
|
||
# 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)
|
||
# --next Prerelease lane: try fast npm @next install for CLI +
|
||
# gateway from the Gitea registry, then fall back to a
|
||
# source build at next if unavailable. Explicit
|
||
# --ref/MOSAIC_REF wins and uses the source path.
|
||
# --dev Build CLI + gateway FROM SOURCE at --ref instead of the
|
||
# registry @latest. Zero registry writes — packs local
|
||
# tarballs and installs them globally. Use to test a branch
|
||
# end-to-end before cutting a release.
|
||
# --yes Accept all defaults; headless/non-interactive install
|
||
# --no-auto-launch Skip automatic mosaic wizard + gateway install on first install
|
||
# --uninstall Reverse the install: remove framework dir, CLI package, and npmrc line
|
||
#
|
||
# Environment:
|
||
# MOSAIC_HOME — framework install dir (default: ~/.config/mosaic)
|
||
# MOSAIC_REGISTRY — npm registry URL (default: Gitea instance)
|
||
# MOSAIC_SCOPE — npm scope (default: @mosaicstack)
|
||
# MOSAIC_PREFIX — npm global prefix (default: ~/.npm-global)
|
||
# MOSAIC_NO_COLOR — disable colour (set to 1)
|
||
# MOSAIC_REF — git ref for framework (default: main)
|
||
# MOSAIC_NEXT — equivalent to --next (set to 1)
|
||
# MOSAIC_DEV — equivalent to --dev (set to 1)
|
||
# MOSAIC_ASSUME_YES — equivalent to --yes (set to 1)
|
||
# ──────────────────────────────────────────────────────────────────────────────
|
||
#
|
||
# Wrapped in main() for safe curl-pipe usage.
|
||
|
||
set -euo pipefail
|
||
|
||
main() {
|
||
|
||
# ─── parse flags ──────────────────────────────────────────────────────────────
|
||
FLAG_CHECK=false
|
||
FLAG_FRAMEWORK=true
|
||
FLAG_CLI=true
|
||
FLAG_NO_AUTO_LAUNCH=false
|
||
FLAG_YES=false
|
||
FLAG_UNINSTALL=false
|
||
FLAG_DEV=false
|
||
FLAG_NEXT=false
|
||
GIT_REF="${MOSAIC_REF:-main}"
|
||
GIT_REF_EXPLICIT=false
|
||
if [[ -n "${MOSAIC_REF:-}" ]]; then
|
||
GIT_REF_EXPLICIT=true
|
||
fi
|
||
|
||
# MOSAIC_ASSUME_YES env var acts the same as --yes
|
||
if [[ "${MOSAIC_ASSUME_YES:-0}" == "1" ]]; then
|
||
FLAG_YES=true
|
||
fi
|
||
|
||
# MOSAIC_DEV env var acts the same as --dev
|
||
if [[ "${MOSAIC_DEV:-0}" == "1" ]]; then
|
||
FLAG_DEV=true
|
||
fi
|
||
|
||
# MOSAIC_NEXT env var acts the same as --next: fast npm @next install with
|
||
# source fallback from the permanent next integration branch unless
|
||
# MOSAIC_REF/--ref explicitly wins.
|
||
if [[ "${MOSAIC_NEXT:-0}" == "1" ]]; then
|
||
FLAG_NEXT=true
|
||
if [[ "$GIT_REF_EXPLICIT" == "false" ]]; then
|
||
GIT_REF="next"
|
||
fi
|
||
fi
|
||
|
||
installer_usage() {
|
||
printf 'Usage: install.sh [--check] [--framework] [--cli] [--ref <branch>] [--next] [--dev] [--yes|-y] [--no-auto-launch] [--uninstall]\n' >&2
|
||
}
|
||
|
||
while [[ $# -gt 0 ]]; do
|
||
case "$1" in
|
||
--check) FLAG_CHECK=true; shift ;;
|
||
--framework) FLAG_CLI=false; shift ;;
|
||
--cli) FLAG_FRAMEWORK=false; shift ;;
|
||
--ref)
|
||
if [[ $# -lt 2 ]] || [[ -z "$2" ]]; then
|
||
printf 'Error: Missing value for --ref\n' >&2
|
||
installer_usage
|
||
exit 2
|
||
fi
|
||
if [[ "$2" == -* ]]; then
|
||
printf 'Error: Unknown argument: %s\n' "$2" >&2
|
||
installer_usage
|
||
exit 2
|
||
fi
|
||
GIT_REF="$2"
|
||
GIT_REF_EXPLICIT=true
|
||
shift 2
|
||
;;
|
||
--dev) FLAG_DEV=true; shift ;;
|
||
--next) FLAG_NEXT=true; if [[ "$GIT_REF_EXPLICIT" == "false" ]]; then GIT_REF="next"; fi; shift ;;
|
||
--yes|-y) FLAG_YES=true; shift ;;
|
||
--no-auto-launch) FLAG_NO_AUTO_LAUNCH=true; shift ;;
|
||
--uninstall) FLAG_UNINSTALL=true; shift ;;
|
||
*)
|
||
printf 'Error: Unknown argument: %s\n' "$1" >&2
|
||
installer_usage
|
||
exit 2
|
||
;;
|
||
esac
|
||
done
|
||
|
||
# Explicit refs represent a request for that exact source tree. Keep --next as
|
||
# a lane selector, but do not install the registry @next package for a different
|
||
# ref than the permanent next branch.
|
||
if [[ "$FLAG_NEXT" == "true" && "$GIT_REF_EXPLICIT" == "true" ]]; then
|
||
FLAG_DEV=true
|
||
fi
|
||
|
||
if [[ "$FLAG_YES" == "true" ]]; then
|
||
export MOSAIC_ASSUME_YES=1
|
||
fi
|
||
|
||
# ─── constants ────────────────────────────────────────────────────────────────
|
||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||
REGISTRY="${MOSAIC_REGISTRY:-https://git.mosaicstack.dev/api/packages/mosaicstack/npm/}"
|
||
SCOPE="${MOSAIC_SCOPE:-@mosaicstack}"
|
||
PREFIX="${MOSAIC_PREFIX:-$HOME/.npm-global}"
|
||
CLI_PKG="${SCOPE}/mosaic"
|
||
GATEWAY_PKG="${SCOPE}/gateway"
|
||
REPO_BASE="https://git.mosaicstack.dev/mosaicstack/stack"
|
||
ARCHIVE_URL="${REPO_BASE}/archive/${GIT_REF}.tar.gz"
|
||
|
||
# In dev (build-from-source) mode the gateway is installed globally from a
|
||
# locally-built tarball. Tell the wizard / gateway-config stage NOT to overwrite
|
||
# it with the registry @latest build (honored by gatewayConfigStage).
|
||
if [[ "$FLAG_DEV" == "true" ]]; then
|
||
export MOSAIC_GATEWAY_SKIP_NPM_INSTALL=1
|
||
fi
|
||
|
||
# Shared monorepo checkout (populated on demand by ensure_monorepo).
|
||
WORK_DIR=""
|
||
EXTRACTED_DIR=""
|
||
|
||
newest_matching_file() {
|
||
local dir="$1"
|
||
local pattern="$2"
|
||
local matches=()
|
||
[[ -d "$dir" ]] || return 0
|
||
shopt -s nullglob
|
||
# shellcheck disable=SC2206 # Intentional glob expansion for caller-provided file pattern.
|
||
matches=("$dir"/$pattern)
|
||
shopt -u nullglob
|
||
[[ "${#matches[@]}" -gt 0 ]] || return 0
|
||
# shellcheck disable=SC2012 # Need portable mtime sorting across Linux/macOS.
|
||
ls -1t "${matches[@]}" 2>/dev/null | head -1
|
||
}
|
||
|
||
# ─── uninstall path ───────────────────────────────────────────────────────────
|
||
# Shell-level uninstall for when the CLI is broken or not available.
|
||
# Handles: framework directory, npm CLI package, npmrc scope line.
|
||
# Gateway teardown: if mosaic CLI is still available, delegates to it.
|
||
# Does NOT touch gateway DB/storage — user must handle that separately.
|
||
|
||
if [[ "$FLAG_UNINSTALL" == "true" ]]; then
|
||
echo ""
|
||
echo "${BOLD:-}Mosaic Uninstaller (shell fallback)${RESET:-}"
|
||
echo ""
|
||
|
||
SCOPE_LINE="${SCOPE:-@mosaicstack}:registry=${REGISTRY:-https://git.mosaicstack.dev/api/packages/mosaicstack/npm/}"
|
||
NPMRC_FILE="$HOME/.npmrc"
|
||
|
||
# Gateway: try mosaic CLI first, then check pid file
|
||
if command -v mosaic &>/dev/null; then
|
||
echo "${B:-}ℹ${RESET:-} Attempting gateway uninstall via mosaic CLI…"
|
||
if mosaic gateway uninstall --yes 2>/dev/null; then
|
||
echo "${G:-}✔${RESET:-} Gateway uninstalled via CLI."
|
||
else
|
||
echo "${Y:-}⚠${RESET:-} Gateway uninstall via CLI failed or not installed — skipping."
|
||
fi
|
||
else
|
||
# Look for pid file and stop daemon if running
|
||
GATEWAY_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}/../mosaic-gateway"
|
||
PID_FILE="$GATEWAY_HOME/gateway.pid"
|
||
if [[ -f "$PID_FILE" ]]; then
|
||
PID="$(cat "$PID_FILE" 2>/dev/null || true)"
|
||
if [[ -n "$PID" ]] && kill -0 "$PID" 2>/dev/null; then
|
||
echo "${B:-}ℹ${RESET:-} Stopping gateway daemon (pid $PID)…"
|
||
kill "$PID" 2>/dev/null || true
|
||
sleep 1
|
||
fi
|
||
fi
|
||
echo "${Y:-}⚠${RESET:-} mosaic CLI not found — skipping full gateway teardown."
|
||
echo " Run 'mosaic gateway uninstall' separately if the CLI is available."
|
||
fi
|
||
|
||
# Framework directory
|
||
if [[ -d "$MOSAIC_HOME" ]]; then
|
||
echo "${B:-}ℹ${RESET:-} Removing framework: $MOSAIC_HOME"
|
||
rm -rf "$MOSAIC_HOME"
|
||
echo "${G:-}✔${RESET:-} Framework removed."
|
||
else
|
||
echo "${Y:-}⚠${RESET:-} Framework directory not found: $MOSAIC_HOME"
|
||
fi
|
||
|
||
# Runtime assets: restore backups or remove managed copies
|
||
echo "${B:-}ℹ${RESET:-} Reversing runtime asset copies…"
|
||
declare -a RUNTIME_DESTS=(
|
||
"$HOME/.claude/CLAUDE.md"
|
||
"$HOME/.claude/settings.json"
|
||
"$HOME/.claude/hooks-config.json"
|
||
"$HOME/.claude/context7-integration.md"
|
||
"$HOME/.config/opencode/AGENTS.md"
|
||
"$HOME/.codex/instructions.md"
|
||
)
|
||
for dest in "${RUNTIME_DESTS[@]}"; do
|
||
base="$(basename "$dest")"
|
||
dir="$(dirname "$dest")"
|
||
# Find most recent backup
|
||
backup=""
|
||
if [[ -d "$dir" ]]; then
|
||
backup="$(newest_matching_file "$dir" "${base}.mosaic-bak-*")"
|
||
fi
|
||
if [[ -n "$backup" ]] && [[ -f "$backup" ]]; then
|
||
cp "$backup" "$dest"
|
||
rm -f "$backup"
|
||
echo " Restored: $dest"
|
||
elif [[ -f "$dest" ]]; then
|
||
rm -f "$dest"
|
||
echo " Removed: $dest"
|
||
fi
|
||
done
|
||
|
||
# npmrc scope line
|
||
if [[ -f "$NPMRC_FILE" ]] && grep -qF "$SCOPE_LINE" "$NPMRC_FILE" 2>/dev/null; then
|
||
echo "${B:-}ℹ${RESET:-} Removing $SCOPE_LINE from $NPMRC_FILE…"
|
||
# Use sed to remove the exact line (in-place, portable)
|
||
if sed -i.mosaic-uninstall-bak "\|^${SCOPE_LINE}\$|d" "$NPMRC_FILE" 2>/dev/null; then
|
||
rm -f "${NPMRC_FILE}.mosaic-uninstall-bak"
|
||
echo "${G:-}✔${RESET:-} npmrc entry removed."
|
||
else
|
||
# BSD sed syntax (macOS)
|
||
sed -i '' "\|^${SCOPE_LINE}\$|d" "$NPMRC_FILE" 2>/dev/null || \
|
||
echo "${Y:-}⚠${RESET:-} Could not auto-remove npmrc line — remove it manually: $SCOPE_LINE"
|
||
fi
|
||
fi
|
||
|
||
# npm CLI package
|
||
echo "${B:-}ℹ${RESET:-} Uninstalling npm package: ${CLI_PKG}…"
|
||
if npm uninstall -g "${CLI_PKG}" --prefix="$PREFIX" 2>&1 | sed 's/^/ /'; then
|
||
echo "${G:-}✔${RESET:-} CLI package removed."
|
||
else
|
||
echo "${Y:-}⚠${RESET:-} npm uninstall failed — you may need to run manually:"
|
||
echo " npm uninstall -g ${CLI_PKG}"
|
||
fi
|
||
|
||
echo ""
|
||
echo "${G:-}✔${RESET:-} Uninstall complete."
|
||
exit 0
|
||
fi
|
||
|
||
# ─── colours ──────────────────────────────────────────────────────────────────
|
||
if [[ "${MOSAIC_NO_COLOR:-0}" == "1" ]] || ! [[ -t 1 ]]; then
|
||
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'
|
||
BOLD=$'\033[1m' RESET=$'\033[0m'
|
||
fi
|
||
|
||
info() { echo "${B}ℹ${RESET} $*"; }
|
||
ok() { echo "${G}✔${RESET} $*"; }
|
||
warn() { echo "${Y}⚠${RESET} $*"; }
|
||
fail() { echo "${R}✖${RESET} $*" >&2; }
|
||
dim() { echo "${DIM}$*${RESET}"; }
|
||
step() { printf '\n%s%s%s\n' "$BOLD" "$*" "$RESET"; }
|
||
|
||
is_next_registry_lane() {
|
||
[[ "$FLAG_NEXT" == "true" && "$FLAG_DEV" == "false" && "$GIT_REF" == "next" && "$GIT_REF_EXPLICIT" == "false" ]]
|
||
}
|
||
|
||
source_ref_details() {
|
||
if is_next_registry_lane; then
|
||
echo "ref: next, --next prerelease lane"
|
||
elif [[ "$FLAG_NEXT" == "true" && "$GIT_REF" == "next" ]]; then
|
||
echo "ref: next, --next prerelease lane (build-from-source)"
|
||
elif [[ "$FLAG_NEXT" == "true" ]]; then
|
||
echo "ref: ${GIT_REF}, --next requested, explicit ref wins"
|
||
else
|
||
echo "ref: ${GIT_REF}"
|
||
fi
|
||
}
|
||
|
||
# ─── helpers ──────────────────────────────────────────────────────────────────
|
||
|
||
require_cmd() {
|
||
if ! command -v "$1" &>/dev/null; then
|
||
fail "Required command not found: $1"
|
||
echo " Install it and re-run this script."
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# Fleet transport binary (#1240).
|
||
#
|
||
# `mosaic fleet --help` reads "Manage the local Mosaic tmux fleet" and every
|
||
# roster the CLI scaffolds sets `transport: tmux`, but nothing in this script
|
||
# provides tmux and, until now, nothing in it mentioned tmux at all. A
|
||
# greenfield host came out of this installer able to install a fleet, start a
|
||
# fleet, and run no seat — the operator's first signal was `mosaic fleet ps`.
|
||
#
|
||
# Not a `require_cmd`: tmux is required by the fleet, not by mosaic. Plenty of
|
||
# hosts install this to run `mosaic claude` and will never scaffold a roster,
|
||
# and failing their install over a binary they do not need would be wrong. It
|
||
# is a warning that names precisely what it blocks.
|
||
#
|
||
# `tools/_scripts/mosaic-doctor` carries a deliberately parallel check, so the
|
||
# same host state gets the same answer from an audit as from an install. They
|
||
# are separate implementations because this one has to work before the
|
||
# framework's scripts are guaranteed to be on disk; keep their wording in step.
|
||
check_fleet_transport() {
|
||
local transport=tmux
|
||
local roster="$MOSAIC_HOME/fleet/roster.yaml"
|
||
local declared=""
|
||
|
||
if [[ -f "$roster" ]]; then
|
||
declared="$(sed -n 's/^[[:space:]]*transport:[[:space:]]*//p' "$roster" | head -1 |
|
||
tr -d '"'\''' | tr -d '\r' | awk '{print $1}')"
|
||
[[ -n "$declared" ]] && transport="$declared"
|
||
fi
|
||
|
||
command -v "$transport" &>/dev/null && return 0
|
||
|
||
warn "Fleet transport '$transport' is not installed."
|
||
echo " The Mosaic fleet runs its agent seats inside $transport. Without it,"
|
||
echo " ${C}mosaic fleet start${RESET} reports success and no seat comes up."
|
||
echo " Install it before using the fleet, e.g. ${C}sudo apt-get install -y $transport${RESET}"
|
||
echo " (this does not affect ${C}mosaic claude${RESET} or the other single-runtime commands)."
|
||
}
|
||
|
||
installed_cli_version() {
|
||
local json
|
||
json="$(npm ls -g --depth=0 --json --prefix="$PREFIX" 2>/dev/null)" || true
|
||
if [[ -n "$json" ]]; then
|
||
node -e "
|
||
const d = JSON.parse(process.argv[1]);
|
||
const v = d?.dependencies?.['${CLI_PKG}']?.version ?? '';
|
||
process.stdout.write(v);
|
||
" "$json" 2>/dev/null || true
|
||
fi
|
||
}
|
||
|
||
installed_gateway_version() {
|
||
local json
|
||
json="$(npm ls -g --depth=0 --json --prefix="$PREFIX" 2>/dev/null)" || true
|
||
if [[ -n "$json" ]]; then
|
||
node -e "
|
||
const d = JSON.parse(process.argv[1]);
|
||
const v = d?.dependencies?.['${GATEWAY_PKG}']?.version ?? '';
|
||
process.stdout.write(v);
|
||
" "$json" 2>/dev/null || true
|
||
fi
|
||
}
|
||
|
||
latest_cli_version() {
|
||
npm view "${CLI_PKG}" version --registry="$REGISTRY" 2>/dev/null || true
|
||
}
|
||
|
||
next_cli_version() {
|
||
npm view "${CLI_PKG}@next" version --registry="$REGISTRY" 2>/dev/null || true
|
||
}
|
||
|
||
next_gateway_version() {
|
||
npm view "${GATEWAY_PKG}@next" version --registry="$REGISTRY" 2>/dev/null || true
|
||
}
|
||
|
||
next_pipeline_suffix() {
|
||
printf '%s' "$1" | sed -n 's/.*-next\.\([0-9][0-9]*\)$/\1/p'
|
||
}
|
||
|
||
next_versions_share_pipeline() {
|
||
local cli_next="$1"
|
||
local gateway_next="$2"
|
||
local cli_pipeline gateway_pipeline
|
||
cli_pipeline="$(next_pipeline_suffix "$cli_next")"
|
||
gateway_pipeline="$(next_pipeline_suffix "$gateway_next")"
|
||
[[ -n "$cli_pipeline" && -n "$gateway_pipeline" && "$cli_pipeline" == "$gateway_pipeline" ]]
|
||
}
|
||
|
||
version_lt() {
|
||
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)]; };
|
||
const [cA,pA]=sp(a.replace(/^v/,'')), [cB,pB]=sp(b.replace(/^v/,''));
|
||
const nA=cA.split('.').map(Number), nB=cB.split('.').map(Number);
|
||
for(let i=0;i<3;i++){if((nA[i]||0)<(nB[i]||0))process.exit(0);if((nA[i]||0)>(nB[i]||0))process.exit(1);}
|
||
if(pA!==null&&pB===null)process.exit(0);
|
||
if(pA===null)process.exit(1);
|
||
if(pA<pB)process.exit(0);
|
||
process.exit(1);
|
||
" "$1" "$2" 2>/dev/null
|
||
}
|
||
|
||
framework_version() {
|
||
# Read framework schema version stamp
|
||
local vf="$MOSAIC_HOME/.framework-version"
|
||
if [[ -f "$vf" ]]; then
|
||
cat "$vf" 2>/dev/null || true
|
||
fi
|
||
}
|
||
|
||
# Download + extract the monorepo archive at $GIT_REF exactly once per run.
|
||
# Sets the script-level EXTRACTED_DIR to the repo root. Reused by both the
|
||
# framework install (Part 1) and the dev build-from-source path (Part 2).
|
||
ensure_monorepo() {
|
||
if [[ -n "$EXTRACTED_DIR" ]] && [[ -d "$EXTRACTED_DIR" ]]; then
|
||
return 0
|
||
fi
|
||
|
||
require_cmd tar
|
||
|
||
WORK_DIR="$(mktemp -d "${TMPDIR:-/tmp}/mosaic-install-XXXXXX")"
|
||
# shellcheck disable=SC2317
|
||
cleanup_work() { [[ -n "$WORK_DIR" ]] && rm -rf "$WORK_DIR"; }
|
||
trap cleanup_work EXIT
|
||
|
||
info "Downloading source 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 source."
|
||
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)"
|
||
if [[ -z "$EXTRACTED_DIR" ]] || [[ ! -d "$EXTRACTED_DIR" ]]; then
|
||
fail "Could not locate extracted source in archive."
|
||
ls -la "$WORK_DIR" >&2
|
||
exit 1
|
||
fi
|
||
}
|
||
|
||
# Build @mosaicstack/mosaic + @mosaicstack/gateway from source and install both
|
||
# globally from locally-packed tarballs. ZERO registry writes. Workspace deps
|
||
# (brain/config/db/…) are pulled from the registry at the versions pinned in
|
||
# each package.json — `pnpm pack` rewrites `workspace:*` to those versions.
|
||
install_cli_from_source() {
|
||
local src="$EXTRACTED_DIR"
|
||
local out_dir="$WORK_DIR/dist-tarballs"
|
||
mkdir -p "$out_dir"
|
||
|
||
# pnpm via corepack (ships with Node >= 16.9; required by Node >= 20 preflight).
|
||
# Pin to the repo's packageManager version so the build matches CI. Surface
|
||
# corepack failures so the fresh-machine case gives an actionable error
|
||
# instead of a bare "command not found".
|
||
if ! command -v pnpm &>/dev/null; then
|
||
info "Activating pnpm via corepack…"
|
||
corepack enable 2>&1 | sed 's/^/ /' || warn "corepack enable failed — pnpm may need manual install."
|
||
corepack prepare [email protected] --activate 2>&1 | sed 's/^/ /' \
|
||
|| warn "corepack prepare failed — pnpm may need manual install."
|
||
fi
|
||
if ! command -v pnpm &>/dev/null; then
|
||
fail "pnpm not available after corepack activation."
|
||
echo " Install pnpm manually (https://pnpm.io/installation) and re-run with --dev."
|
||
exit 1
|
||
fi
|
||
|
||
info "Installing workspace dependencies (pnpm install)…"
|
||
( cd "$src" && pnpm install ) 2>&1 | sed 's/^/ /'
|
||
|
||
info "Building CLI + gateway from source…"
|
||
( cd "$src" && pnpm --filter "@mosaicstack/mosaic..." --filter "@mosaicstack/gateway..." run build ) 2>&1 | sed 's/^/ /'
|
||
|
||
info "Packing local tarballs…"
|
||
( cd "$src/packages/mosaic" && pnpm pack --pack-destination "$out_dir" ) 2>&1 | sed 's/^/ /'
|
||
( cd "$src/apps/gateway" && pnpm pack --pack-destination "$out_dir" ) 2>&1 | sed 's/^/ /'
|
||
|
||
local cli_tgz gw_tgz
|
||
cli_tgz="$(newest_matching_file "$out_dir" 'mosaicstack-mosaic-*.tgz')"
|
||
gw_tgz="$(newest_matching_file "$out_dir" 'mosaicstack-gateway-*.tgz')"
|
||
|
||
if [[ ! -f "$cli_tgz" ]]; then
|
||
fail "CLI tarball was not produced by pnpm pack."
|
||
exit 1
|
||
fi
|
||
if [[ ! -f "$gw_tgz" ]]; then
|
||
fail "Gateway tarball was not produced by pnpm pack."
|
||
exit 1
|
||
fi
|
||
|
||
# Gateway first so it is present globally before the CLI's wizard runs (which
|
||
# skips its own gateway install via MOSAIC_GATEWAY_SKIP_NPM_INSTALL=1).
|
||
info "Installing gateway from source tarball (global)…"
|
||
npm install -g "$gw_tgz" --prefix="$PREFIX" 2>&1 | sed 's/^/ /'
|
||
|
||
info "Installing CLI from source tarball (global)…"
|
||
npm install -g "$cli_tgz" --prefix="$PREFIX" 2>&1 | sed 's/^/ /'
|
||
|
||
ok "Installed from source: CLI $(installed_cli_version)"
|
||
}
|
||
|
||
install_next_cli_from_registry() {
|
||
local cli_next gateway_next
|
||
cli_next="$(next_cli_version)"
|
||
gateway_next="$(next_gateway_version)"
|
||
|
||
if [[ -z "$cli_next" ]]; then
|
||
warn "${CLI_PKG}@next is unavailable from $REGISTRY."
|
||
return 1
|
||
fi
|
||
if [[ -z "$gateway_next" ]]; then
|
||
warn "${GATEWAY_PKG}@next is unavailable from $REGISTRY."
|
||
return 1
|
||
fi
|
||
|
||
if ! next_versions_share_pipeline "$cli_next" "$gateway_next"; then
|
||
warn "@next CLI/gateway versions do not share a pipeline suffix (${cli_next}, ${gateway_next})."
|
||
return 1
|
||
fi
|
||
|
||
info "Installing ${CLI_PKG}@${cli_next} from registry…"
|
||
if ! npm install -g "${CLI_PKG}@${cli_next}" --prefix="$PREFIX" 2>&1 | sed 's/^/ /'; then
|
||
warn "Fast CLI @next install failed."
|
||
return 1
|
||
fi
|
||
|
||
info "Installing ${GATEWAY_PKG}@${gateway_next} from registry…"
|
||
if ! npm install -g "${GATEWAY_PKG}@${gateway_next}" --prefix="$PREFIX" 2>&1 | sed 's/^/ /'; then
|
||
warn "Fast gateway @next install failed."
|
||
return 1
|
||
fi
|
||
|
||
local installed_cli installed_gateway
|
||
installed_cli="$(installed_cli_version)"
|
||
installed_gateway="$(installed_gateway_version)"
|
||
if [[ "$installed_cli" != "$cli_next" || "$installed_gateway" != "$gateway_next" ]]; then
|
||
warn "Installed @next versions did not match resolved versions (CLI: ${installed_cli:-missing}, gateway: ${installed_gateway:-missing})."
|
||
return 1
|
||
fi
|
||
|
||
export MOSAIC_GATEWAY_SKIP_NPM_INSTALL=1
|
||
ok "Installed @next packages: CLI ${installed_cli}, gateway ${installed_gateway}"
|
||
}
|
||
|
||
# ─── preflight ────────────────────────────────────────────────────────────────
|
||
|
||
require_cmd node
|
||
require_cmd npm
|
||
|
||
NODE_MAJOR="$(node -e 'process.stdout.write(String(process.versions.node.split(".")[0]))')"
|
||
if [[ "$NODE_MAJOR" -lt 20 ]]; then
|
||
fail "Node.js >= 20 required (found v$(node --version))"
|
||
exit 1
|
||
fi
|
||
if [[ "$FLAG_NEXT" == "true" && "$NODE_MAJOR" -lt 22 ]]; then
|
||
fail "Node.js >= 22 required for the --next lane (found v$(node --version))"
|
||
exit 1
|
||
fi
|
||
|
||
echo ""
|
||
echo "${BOLD}Mosaic Stack Installer${RESET}"
|
||
echo ""
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# 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
|
||
[[ -f "$MOSAIC_HOME/AGENTS.md" ]] || [[ -f "$MOSAIC_HOME/.framework-version" ]] && 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
|
||
dim " Installed: (none)"
|
||
fi
|
||
dim " Source: ${REPO_BASE} ($(source_ref_details))"
|
||
echo ""
|
||
|
||
if [[ "$FLAG_CHECK" == "true" ]]; then
|
||
if [[ "$HAS_FRAMEWORK" == "true" ]]; then
|
||
ok "Framework is installed."
|
||
else
|
||
warn "Framework not installed."
|
||
fi
|
||
else
|
||
# Download repo archive and extract framework (shared with the dev build)
|
||
ensure_monorepo
|
||
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 ""
|
||
|
||
# Framework bin is no longer needed on PATH — the npm CLI delegates
|
||
# to mosaic-launch directly via its absolute path.
|
||
fi
|
||
fi
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# PART 2: @mosaicstack/mosaic (npm — TUI, gateway client, wizard, CLI)
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
if [[ "$FLAG_CLI" == "true" ]]; then
|
||
step "@mosaicstack/mosaic (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)"
|
||
NEXT_GATEWAY=""
|
||
if [[ "$FLAG_DEV" == "true" ]]; then
|
||
LATEST=""
|
||
elif is_next_registry_lane; then
|
||
LATEST="$(next_cli_version)"
|
||
NEXT_GATEWAY="$(next_gateway_version)"
|
||
else
|
||
LATEST="$(latest_cli_version)"
|
||
fi
|
||
|
||
if [[ -n "$CURRENT" ]]; then
|
||
dim " Installed: ${CLI_PKG}@${CURRENT}"
|
||
else
|
||
dim " Installed: (none)"
|
||
fi
|
||
|
||
if [[ "$FLAG_DEV" == "true" ]]; then
|
||
dim " Source: ${REPO_BASE} ($(source_ref_details), build-from-source)"
|
||
elif is_next_registry_lane; then
|
||
if [[ -n "$LATEST" ]]; then
|
||
dim " Next CLI: ${CLI_PKG}@${LATEST}"
|
||
else
|
||
dim " Next CLI: (registry @next unreachable)"
|
||
fi
|
||
if [[ -n "$NEXT_GATEWAY" ]]; then
|
||
dim " Next GW: ${GATEWAY_PKG}@${NEXT_GATEWAY}"
|
||
else
|
||
dim " Next GW: (registry @next unreachable)"
|
||
fi
|
||
dim " Fallback: ${REPO_BASE} (ref: next, build-from-source)"
|
||
elif [[ -n "$LATEST" ]]; then
|
||
dim " Latest: ${CLI_PKG}@${LATEST}"
|
||
else
|
||
dim " Latest: (registry unreachable)"
|
||
fi
|
||
echo ""
|
||
|
||
if [[ "$FLAG_CHECK" == "true" ]]; then
|
||
if [[ "$FLAG_DEV" == "true" ]]; then
|
||
info "Dev mode: installed version is ${CURRENT:-(none)} (no registry comparison)."
|
||
elif is_next_registry_lane; then
|
||
if [[ -n "$LATEST" && -n "$NEXT_GATEWAY" ]] && next_versions_share_pipeline "$LATEST" "$NEXT_GATEWAY"; then
|
||
ok "@next registry lane available: ${CLI_PKG}@${LATEST}, ${GATEWAY_PKG}@${NEXT_GATEWAY}."
|
||
else
|
||
warn "@next registry lane incomplete, mismatched, or unreachable; --next would fall back to source."
|
||
fi
|
||
elif [[ -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
|
||
elif [[ "$FLAG_DEV" == "true" ]]; then
|
||
info "Dev mode — building CLI + gateway from source at ref ${GIT_REF}…"
|
||
ensure_monorepo
|
||
install_cli_from_source
|
||
|
||
# PATH check for npm prefix
|
||
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
|
||
elif is_next_registry_lane; then
|
||
info "Next mode — trying fast npm @next install from ${REGISTRY}…"
|
||
if install_next_cli_from_registry; then
|
||
:
|
||
else
|
||
warn "Falling back to source build at ref ${GIT_REF}; --next will not hard-fail on registry issues."
|
||
unset MOSAIC_GATEWAY_SKIP_NPM_INSTALL
|
||
ensure_monorepo
|
||
install_cli_from_source
|
||
export MOSAIC_GATEWAY_SKIP_NPM_INSTALL=1
|
||
fi
|
||
|
||
# PATH check for npm prefix
|
||
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
|
||
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 " Add to your shell rc: export PATH=\"$PREFIX/bin:\$PATH\""
|
||
fi
|
||
fi
|
||
fi
|
||
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
# Summary
|
||
# ═══════════════════════════════════════════════════════════════════════════════
|
||
|
||
if [[ "$FLAG_CHECK" == "false" ]]; then
|
||
step "Summary"
|
||
|
||
echo " ${BOLD}mosaic:${RESET} $PREFIX/bin/mosaic"
|
||
dim " Framework data: $MOSAIC_HOME/"
|
||
echo ""
|
||
|
||
# First install guidance / auto-launch
|
||
if [[ ! -f "$MOSAIC_HOME/SOUL.md" ]]; then
|
||
echo ""
|
||
if [[ "$FLAG_NO_AUTO_LAUNCH" == "false" ]] && [[ -t 0 ]] && [[ -t 1 ]]; then
|
||
# Interactive TTY and auto-launch not suppressed: run the unified wizard.
|
||
# `mosaic wizard` now runs the full first-run flow end-to-end: identity
|
||
# setup → runtimes → hooks preview → skills → finalize → gateway
|
||
# config → admin bootstrap. No second call needed.
|
||
info "First install detected — launching unified setup wizard…"
|
||
echo ""
|
||
|
||
MOSAIC_BIN="$PREFIX/bin/mosaic"
|
||
|
||
if ! command -v "$MOSAIC_BIN" &>/dev/null && ! command -v mosaic &>/dev/null; then
|
||
warn "mosaic binary not found on PATH — skipping auto-launch."
|
||
warn "Add $PREFIX/bin to PATH and run: mosaic wizard"
|
||
else
|
||
# Prefer the absolute path from the prefix we just installed to
|
||
MOSAIC_CMD="mosaic"
|
||
if [[ -x "$MOSAIC_BIN" ]]; then
|
||
MOSAIC_CMD="$MOSAIC_BIN"
|
||
fi
|
||
|
||
if "$MOSAIC_CMD" wizard; then
|
||
ok "Wizard complete."
|
||
else
|
||
fail "Wizard failed; installation is incomplete."
|
||
echo " Completed: framework and CLI installation"
|
||
echo " Failed: gateway configuration or admin bootstrap"
|
||
echo " You can retry with: ${C}mosaic wizard${RESET}"
|
||
echo " Or run gateway install alone: ${C}mosaic gateway install${RESET}"
|
||
exit 1
|
||
fi
|
||
fi
|
||
else
|
||
# Non-interactive or --no-auto-launch: print guidance only
|
||
info "First install detected. Set up your agent identity:"
|
||
echo " ${C}mosaic wizard${RESET} (unified first-run wizard — identity + gateway + admin)"
|
||
echo " ${C}mosaic gateway install${RESET} (standalone gateway (re)configure)"
|
||
fi
|
||
fi
|
||
|
||
# ── Write install manifest ──────────────────────────────────────────────────
|
||
# Records what was mutated so that `mosaic uninstall` can precisely reverse it.
|
||
# Written last (after all mutations) so an incomplete install leaves no manifest.
|
||
MANIFEST_PATH="$MOSAIC_HOME/.install-manifest.json"
|
||
MANIFEST_CLI_VERSION="$(installed_cli_version)"
|
||
MANIFEST_FW_VERSION="$(framework_version)"
|
||
MANIFEST_SCOPE_LINE="${SCOPE}:registry=${REGISTRY}"
|
||
MANIFEST_TS="$(date -u +"%Y-%m-%dT%H:%M:%SZ" 2>/dev/null || date -u +"%Y-%m-%dT%H:%M:%SZ")"
|
||
|
||
# Build runtimeAssetCopies array by scanning known destinations for backups
|
||
collect_runtime_copies() {
|
||
local home_dir="$HOME"
|
||
local copies="[]"
|
||
local dests=(
|
||
"$home_dir/.claude/CLAUDE.md"
|
||
"$home_dir/.claude/settings.json"
|
||
"$home_dir/.claude/hooks-config.json"
|
||
"$home_dir/.claude/context7-integration.md"
|
||
"$home_dir/.config/opencode/AGENTS.md"
|
||
"$home_dir/.codex/instructions.md"
|
||
)
|
||
copies="["
|
||
local first=true
|
||
for dest in "${dests[@]}"; do
|
||
[[ -f "$dest" ]] || continue
|
||
local base dir backup_path backup_val
|
||
base="$(basename "$dest")"
|
||
dir="$(dirname "$dest")"
|
||
backup_path="$(newest_matching_file "$dir" "${base}.mosaic-bak-*")"
|
||
if [[ -n "$backup_path" ]]; then
|
||
backup_val="\"$backup_path\""
|
||
else
|
||
backup_val="null"
|
||
fi
|
||
if [[ "$first" == "true" ]]; then
|
||
first=false
|
||
else
|
||
copies="$copies,"
|
||
fi
|
||
copies="$copies{\"source\":\"\",\"dest\":\"$dest\",\"backup\":$backup_val}"
|
||
done
|
||
copies="$copies]"
|
||
echo "$copies"
|
||
}
|
||
|
||
RUNTIME_COPIES="$(collect_runtime_copies)"
|
||
|
||
# Check whether the npmrc line was present (we may have added it above)
|
||
NPMRC_LINES_JSON="[]"
|
||
if grep -qF "$MANIFEST_SCOPE_LINE" "$HOME/.npmrc" 2>/dev/null; then
|
||
NPMRC_LINES_JSON="[\"$MANIFEST_SCOPE_LINE\"]"
|
||
fi
|
||
|
||
if node -e "
|
||
const fs = require('fs');
|
||
const path = require('path');
|
||
const p = process.argv[1];
|
||
const m = {
|
||
version: 1,
|
||
installedAt: process.argv[2],
|
||
cliVersion: process.argv[3] || '(unknown)',
|
||
frameworkVersion: parseInt(process.argv[4] || '0', 10),
|
||
mutations: {
|
||
directories: [path.dirname(p)],
|
||
npmGlobalPackages: ['@mosaicstack/mosaic'],
|
||
npmrcLines: JSON.parse(process.argv[5]),
|
||
shellProfileEdits: [],
|
||
runtimeAssetCopies: JSON.parse(process.argv[6]),
|
||
}
|
||
};
|
||
fs.mkdirSync(path.dirname(p), { recursive: true });
|
||
fs.writeFileSync(p, JSON.stringify(m, null, 2) + '\\n', { mode: 0o600 });
|
||
" \
|
||
"$MANIFEST_PATH" \
|
||
"$MANIFEST_TS" \
|
||
"$MANIFEST_CLI_VERSION" \
|
||
"$MANIFEST_FW_VERSION" \
|
||
"$NPMRC_LINES_JSON" \
|
||
"$RUNTIME_COPIES" 2>/dev/null; then
|
||
ok "Install manifest written: $MANIFEST_PATH"
|
||
else
|
||
warn "Could not write install manifest (non-fatal)"
|
||
fi
|
||
|
||
echo ""
|
||
ok "Done."
|
||
fi
|
||
|
||
# Fleet readiness (#1240). Runs in both normal and --check mode: "what is the
|
||
# state of this host" is exactly the question --check is asked, and a host that
|
||
# cannot run a seat should not have to discover it from `fleet ps`.
|
||
check_fleet_transport
|
||
|
||
} # end main
|
||
|
||
main "$@"
|