feat: mosaic uninstall (IUH-M01) (#429)
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
ci/woodpecker/push/publish Pipeline was successful

This commit was merged in pull request #429.
This commit is contained in:
2026-04-05 17:06:21 +00:00
parent be6553101c
commit 25cada7735
7 changed files with 1205 additions and 0 deletions

View File

@@ -18,6 +18,7 @@
# --ref <branch> Git ref for framework archive (default: main)
# --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)
@@ -41,6 +42,7 @@ FLAG_FRAMEWORK=true
FLAG_CLI=true
FLAG_NO_AUTO_LAUNCH=false
FLAG_YES=false
FLAG_UNINSTALL=false
GIT_REF="${MOSAIC_REF:-main}"
# MOSAIC_ASSUME_YES env var acts the same as --yes
@@ -56,6 +58,7 @@ while [[ $# -gt 0 ]]; do
--ref) GIT_REF="${2:-main}"; shift 2 ;;
--yes|-y) FLAG_YES=true; shift ;;
--no-auto-launch) FLAG_NO_AUTO_LAUNCH=true; shift ;;
--uninstall) FLAG_UNINSTALL=true; shift ;;
*) shift ;;
esac
done
@@ -69,6 +72,109 @@ CLI_PKG="${SCOPE}/mosaic"
REPO_BASE="https://git.mosaicstack.dev/mosaicstack/mosaic-stack"
ARCHIVE_URL="${REPO_BASE}/archive/${GIT_REF}.tar.gz"
# ─── 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="$(ls -1t "$dir/${base}.mosaic-bak-"* 2>/dev/null | head -1 || true)"
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=""
@@ -358,6 +464,88 @@ if [[ "$FLAG_CHECK" == "false" ]]; then
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="$(ls -1t "$dir/${base}.mosaic-bak-"* 2>/dev/null | head -1 || true)"
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
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 \
&& ok "Install manifest written: $MANIFEST_PATH" \
|| warn "Could not write install manifest (non-fatal)"
echo ""
ok "Done."
fi