chore: consolidate new foundation and archive v1 (#1495)
This commit is contained in:
@@ -0,0 +1,269 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# Link the INSTALLED canonical skills into runtime skill directories.
|
||||
#
|
||||
# The canonical skills ship inside the framework package itself
|
||||
# (packages/mosaic/framework/skills in the monorepo) and are installed into
|
||||
# $MOSAIC_HOME/skills by the framework installer — there is no second
|
||||
# repository to clone. This script only maintains the runtime symlinks.
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
MOSAIC_SKILLS_DIR="$MOSAIC_HOME/skills"
|
||||
MOSAIC_LOCAL_SKILLS_DIR="$MOSAIC_HOME/skills-local"
|
||||
|
||||
# Colon-separated list of skill names to install. When set, only these skills
|
||||
# are linked into runtime skill directories. Empty/unset = link all skills
|
||||
# (the legacy "mosaic sync" full-catalog behavior).
|
||||
MOSAIC_INSTALL_SKILLS="${MOSAIC_INSTALL_SKILLS:-}"
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: $(basename "$0") [options]
|
||||
|
||||
Link installed skills from ~/.config/mosaic/{skills,skills-local} into runtime
|
||||
skill directories. Canonical skills arrive with the framework installer; this
|
||||
script never clones or pulls a second repository.
|
||||
|
||||
Options:
|
||||
--link-only Accepted for compatibility; linking is now the whole job
|
||||
--no-link Do nothing (kept for compatibility)
|
||||
-h, --help Show help
|
||||
|
||||
Env:
|
||||
MOSAIC_HOME Default: ~/.config/mosaic
|
||||
MOSAIC_INSTALL_SKILLS Colon-separated list of skills to link (default: all)
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--link-only)
|
||||
# Compat no-op: fetching a skills repo no longer exists.
|
||||
shift
|
||||
;;
|
||||
--no-link)
|
||||
echo "[mosaic-skills] Nothing to do (--no-link; canonical skills ship with the installer)"
|
||||
exit 0
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
mkdir -p "$MOSAIC_HOME" "$MOSAIC_SKILLS_DIR" "$MOSAIC_LOCAL_SKILLS_DIR"
|
||||
|
||||
if [[ ! -d "$MOSAIC_SKILLS_DIR" ]]; then
|
||||
echo "[mosaic-skills] Canonical skills dir missing: $MOSAIC_SKILLS_DIR" >&2
|
||||
echo "[mosaic-skills] Canonical skills ship with the framework — reinstall or update the framework package" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Skills are linked into the MOSAIC-OWNED harness homes, never a base install.
|
||||
# Paths mirror the config-dir env vars the launcher injects (HARNESS_HOME_ENV in
|
||||
# commands/launch.js):
|
||||
# claude CLAUDE_CONFIG_DIR -> <home>/skills
|
||||
# pi PI_CODING_AGENT_DIR -> <home>/skills (replaces ~/.pi/agent)
|
||||
# codex CODEX_HOME -> <home>/skills
|
||||
# opencode XDG_CONFIG_HOME -> <home>/opencode/skills (XDG adds a level)
|
||||
link_targets=(
|
||||
"$MOSAIC_HOME/.claude/skills"
|
||||
"$MOSAIC_HOME/.codex/skills"
|
||||
"$MOSAIC_HOME/.opencode/opencode/skills"
|
||||
"$MOSAIC_HOME/.pi/skills"
|
||||
)
|
||||
|
||||
# Pre-isolation installs planted the same symlink farm directly in the operator's
|
||||
# base installs. Those are now orphaned: the launcher no longer reads them, but
|
||||
# they persist and make a "clean" base install look mosaic-managed.
|
||||
legacy_link_targets=(
|
||||
"$HOME/.claude/skills"
|
||||
"$HOME/.codex/skills"
|
||||
"$HOME/.config/opencode/skills"
|
||||
"$HOME/.pi/agent/skills"
|
||||
)
|
||||
|
||||
canonical_real="$(readlink -f "$MOSAIC_SKILLS_DIR")"
|
||||
local_real="$(readlink -f "$MOSAIC_LOCAL_SKILLS_DIR")"
|
||||
|
||||
# Build an associative array from the colon-separated whitelist for O(1) lookup.
|
||||
# When MOSAIC_INSTALL_SKILLS is empty, all skills are allowed.
|
||||
declare -A _skill_whitelist=()
|
||||
_whitelist_active=0
|
||||
if [[ -n "$MOSAIC_INSTALL_SKILLS" ]]; then
|
||||
_whitelist_active=1
|
||||
IFS=':' read -ra _wl_items <<< "$MOSAIC_INSTALL_SKILLS"
|
||||
for _item in "${_wl_items[@]}"; do
|
||||
[[ -n "$_item" ]] && _skill_whitelist["$_item"]=1
|
||||
done
|
||||
fi
|
||||
|
||||
is_skill_selected() {
|
||||
local name="$1"
|
||||
if [[ $_whitelist_active -eq 0 ]]; then
|
||||
return 0
|
||||
fi
|
||||
[[ -n "${_skill_whitelist[$name]:-}" ]] && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
link_skill_into_target() {
|
||||
local skill_path="$1"
|
||||
local target_dir="$2"
|
||||
|
||||
local name link_path
|
||||
name="$(basename "$skill_path")"
|
||||
|
||||
# Do not distribute hidden/system skill directories globally.
|
||||
if [[ "$name" == .* ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
# Respect the install whitelist (set during first-run wizard).
|
||||
if ! is_skill_selected "$name"; then
|
||||
return
|
||||
fi
|
||||
|
||||
link_path="$target_dir/$name"
|
||||
|
||||
if [[ -L "$link_path" ]]; then
|
||||
local raw_target resolved_target
|
||||
raw_target="$(readlink "$link_path")"
|
||||
resolved_target="$(node -e 'const p=require("node:path"); process.stdout.write(p.resolve(p.dirname(process.argv[1]), process.argv[2]));' "$link_path" "$raw_target")"
|
||||
if [[ "$resolved_target" == "$canonical_real/"* || "$resolved_target" == "$local_real/"* ]]; then
|
||||
ln -sfn "$skill_path" "$link_path"
|
||||
else
|
||||
echo "[mosaic-skills] Preserve foreign runtime symlink: $link_path"
|
||||
fi
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -e "$link_path" ]]; then
|
||||
echo "[mosaic-skills] Preserve existing runtime-specific entry: $link_path"
|
||||
return
|
||||
fi
|
||||
|
||||
ln -s "$skill_path" "$link_path"
|
||||
}
|
||||
|
||||
is_mosaic_skill_name() {
|
||||
local name="$1"
|
||||
# -d follows symlinks; -L catches broken symlinks that still indicate ownership
|
||||
[[ -d "$MOSAIC_SKILLS_DIR/$name" || -L "$MOSAIC_SKILLS_DIR/$name" ]] && return 0
|
||||
[[ -d "$MOSAIC_LOCAL_SKILLS_DIR/$name" || -L "$MOSAIC_LOCAL_SKILLS_DIR/$name" ]] && return 0
|
||||
return 1
|
||||
}
|
||||
|
||||
prune_stale_links_in_target() {
|
||||
local target_dir="$1"
|
||||
|
||||
while IFS= read -r -d '' link_path; do
|
||||
local name resolved
|
||||
name="$(basename "$link_path")"
|
||||
|
||||
if is_mosaic_skill_name "$name"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
# -m resolves lexical dangling targets too. If resolution fails, ownership
|
||||
# is unproven and the link must be preserved.
|
||||
resolved="$(readlink -m "$link_path" 2>/dev/null || true)"
|
||||
# $canonical_real must be length-checked BEFORE use as a prefix: if it were
|
||||
# ever empty, "$resolved" == "$canonical_real/"* collapses to == "/"* and
|
||||
# matches every absolute path. Combined with the is_mosaic_skill_name skip
|
||||
# above, that inverts the function precisely — it would delete exactly the
|
||||
# FOREIGN symlinks and keep the mosaic ones. (#1087, reported by mos-claude.)
|
||||
if [[ -n "$resolved" && -n "$canonical_real" && "$resolved" == "$canonical_real/"* ]]; then
|
||||
rm -f "$link_path"
|
||||
echo "[mosaic-skills] Removed stale retired skill link: $link_path"
|
||||
fi
|
||||
done < <(find "$target_dir" -mindepth 1 -maxdepth 1 -type l -print0)
|
||||
}
|
||||
|
||||
# Remove mosaic-owned symlinks left in a base install by a pre-isolation sync.
|
||||
#
|
||||
# Ownership is proven by RESOLUTION, not by name: only links resolving inside the
|
||||
# canonical or local skills dirs are removed. Anything else — a real directory, a
|
||||
# link elsewhere, an unresolvable link — is left untouched. This mirrors the
|
||||
# refusal in commands/skill.js ("only symlinks pointing inside the Mosaic skills
|
||||
# directory are managed") and preserves e.g. codex's own `.system` dir.
|
||||
#
|
||||
# The directory itself is kept: mosaic-doctor warns when ~/.pi/agent/skills is
|
||||
# missing, and an empty dir is the correct end state, not an absent one.
|
||||
cleanup_legacy_target() {
|
||||
local target_dir="$1"
|
||||
local removed=0 kept=0
|
||||
|
||||
[[ -d "$target_dir" ]] || return 0
|
||||
|
||||
while IFS= read -r -d '' link_path; do
|
||||
local resolved owned=0
|
||||
resolved="$(readlink -m "$link_path" 2>/dev/null || true)"
|
||||
|
||||
# Guard the empty-prefix trap: an unset *_real would make "$resolved" == "/"*
|
||||
# match every absolute path and delete foreign links.
|
||||
if [[ -n "$resolved" ]]; then
|
||||
if [[ -n "$canonical_real" && "$resolved" == "$canonical_real/"* ]]; then
|
||||
owned=1
|
||||
elif [[ -n "$local_real" && "$resolved" == "$local_real/"* ]]; then
|
||||
owned=1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ $owned -eq 1 ]]; then
|
||||
rm -f "$link_path"
|
||||
removed=$((removed + 1))
|
||||
else
|
||||
kept=$((kept + 1))
|
||||
fi
|
||||
done < <(find "$target_dir" -mindepth 1 -maxdepth 1 -type l -print0)
|
||||
|
||||
if [[ $removed -gt 0 ]]; then
|
||||
echo "[mosaic-skills] Legacy cleanup: removed $removed mosaic symlink(s) from $target_dir (preserved $kept foreign)"
|
||||
fi
|
||||
}
|
||||
|
||||
for legacy in "${legacy_link_targets[@]}"; do
|
||||
# Skip anything that is also a current target, so isolation can never
|
||||
# self-destruct if the two lists ever overlap.
|
||||
skip=0
|
||||
for target in "${link_targets[@]}"; do
|
||||
[[ "$legacy" == "$target" ]] && skip=1
|
||||
done
|
||||
[[ $skip -eq 1 ]] && continue
|
||||
cleanup_legacy_target "$legacy"
|
||||
done
|
||||
|
||||
for target in "${link_targets[@]}"; do
|
||||
mkdir -p "$target"
|
||||
|
||||
# If target already resolves to canonical dir, skip to avoid self-link recursion/corruption.
|
||||
target_real="$(readlink -f "$target" 2>/dev/null || true)"
|
||||
if [[ -n "$target_real" && "$target_real" == "$canonical_real" ]]; then
|
||||
echo "[mosaic-skills] Skip target (already canonical): $target"
|
||||
continue
|
||||
fi
|
||||
|
||||
prune_stale_links_in_target "$target"
|
||||
|
||||
while IFS= read -r -d '' skill; do
|
||||
link_skill_into_target "$skill" "$target"
|
||||
done < <(find "$MOSAIC_SKILLS_DIR" -mindepth 1 -maxdepth 1 -type d -print0)
|
||||
|
||||
if [[ -d "$MOSAIC_LOCAL_SKILLS_DIR" ]]; then
|
||||
while IFS= read -r -d '' skill; do
|
||||
link_skill_into_target "$skill" "$target"
|
||||
done < <(find "$MOSAIC_LOCAL_SKILLS_DIR" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -print0)
|
||||
fi
|
||||
|
||||
echo "[mosaic-skills] Linked skills into: $target"
|
||||
done
|
||||
|
||||
echo "[mosaic-skills] Complete"
|
||||
Reference in New Issue
Block a user