feat!: unify mosaic CLI — native launcher, no bin/ directory
BREAKING CHANGE: ~/.config/mosaic/bin/ is removed entirely. The mosaic npm CLI is now the only executable. ## What changed - **bin/ → deleted**: All scripts moved to tools/_scripts/ (internal) - **mosaic-launch → deleted**: Launcher logic is native TypeScript in packages/cli/src/commands/launch.ts - **mosaic.ps1 → deleted**: PowerShell launcher removed - **Framework install.sh**: Complete rewrite with migration system - **Version tracking**: .framework-version file (schema v2) - **Migration v1→v2**: Auto-removes bin/, cleans old PATH entries from shell profiles ## Native TypeScript launcher (commands/launch.ts) All runtime launch logic ported from bash: - Runtime prompt builder (AGENTS.md + RUNTIME.md + USER.md + TOOLS.md) - Mission context injection (reads .mosaic/orchestrator/mission.json) - PRD status injection (scans docs/PRD.md) - Pre-flight checks (MOSAIC_HOME, AGENTS.md, SOUL.md, runtime binary) - Session lock management with signal cleanup - Per-runtime launch: Claude, Codex, OpenCode, Pi - Yolo mode flags per runtime - Pi skill discovery + extension loading - Framework management (init, doctor, sync, bootstrap) delegates to tools/_scripts/ bash implementations ## Installer - tools/install.sh: detects framework by .framework-version or AGENTS.md - Framework install.sh: migration system with schema versioning - Forward-compatible: add migrations as numbered blocks - No PATH manipulation for framework (npm bin is the only PATH entry)
This commit is contained in:
126
packages/mosaic/framework/tools/_scripts/mosaic-bootstrap-repo
Executable file
126
packages/mosaic/framework/tools/_scripts/mosaic-bootstrap-repo
Executable file
@@ -0,0 +1,126 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
TARGET_DIR="$(pwd)"
|
||||
FORCE=0
|
||||
QUALITY_TEMPLATE=""
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--force)
|
||||
FORCE=1
|
||||
shift
|
||||
;;
|
||||
--quality-template)
|
||||
QUALITY_TEMPLATE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
TARGET_DIR="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ! -d "$TARGET_DIR" ]]; then
|
||||
echo "[mosaic] Target directory does not exist: $TARGET_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
TEMPLATE_ROOT="$MOSAIC_HOME/templates/repo"
|
||||
|
||||
if [[ ! -d "$TEMPLATE_ROOT" ]]; then
|
||||
echo "[mosaic] Missing templates at $TEMPLATE_ROOT" >&2
|
||||
echo "[mosaic] Install or refresh framework: ~/.config/mosaic/install.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$TARGET_DIR/.mosaic" "$TARGET_DIR/scripts/agent"
|
||||
mkdir -p "$TARGET_DIR/.mosaic/orchestrator" "$TARGET_DIR/.mosaic/orchestrator/logs" "$TARGET_DIR/.mosaic/orchestrator/results"
|
||||
|
||||
copy_file() {
|
||||
local src="$1"
|
||||
local dst="$2"
|
||||
|
||||
if [[ -f "$dst" && "$FORCE" -ne 1 ]]; then
|
||||
echo "[mosaic] Skip existing: $dst"
|
||||
return
|
||||
fi
|
||||
|
||||
cp "$src" "$dst"
|
||||
echo "[mosaic] Wrote: $dst"
|
||||
}
|
||||
|
||||
copy_file "$TEMPLATE_ROOT/.mosaic/README.md" "$TARGET_DIR/.mosaic/README.md"
|
||||
copy_file "$TEMPLATE_ROOT/.mosaic/repo-hooks.sh" "$TARGET_DIR/.mosaic/repo-hooks.sh"
|
||||
copy_file "$TEMPLATE_ROOT/.mosaic/quality-rails.yml" "$TARGET_DIR/.mosaic/quality-rails.yml"
|
||||
copy_file "$TEMPLATE_ROOT/.mosaic/orchestrator/config.json" "$TARGET_DIR/.mosaic/orchestrator/config.json"
|
||||
copy_file "$TEMPLATE_ROOT/.mosaic/orchestrator/tasks.json" "$TARGET_DIR/.mosaic/orchestrator/tasks.json"
|
||||
copy_file "$TEMPLATE_ROOT/.mosaic/orchestrator/state.json" "$TARGET_DIR/.mosaic/orchestrator/state.json"
|
||||
copy_file "$TEMPLATE_ROOT/.mosaic/orchestrator/matrix_state.json" "$TARGET_DIR/.mosaic/orchestrator/matrix_state.json"
|
||||
copy_file "$TEMPLATE_ROOT/.mosaic/orchestrator/logs/.gitkeep" "$TARGET_DIR/.mosaic/orchestrator/logs/.gitkeep"
|
||||
copy_file "$TEMPLATE_ROOT/.mosaic/orchestrator/results/.gitkeep" "$TARGET_DIR/.mosaic/orchestrator/results/.gitkeep"
|
||||
|
||||
for file in "$TEMPLATE_ROOT"/scripts/agent/*.sh; do
|
||||
base="$(basename "$file")"
|
||||
copy_file "$file" "$TARGET_DIR/scripts/agent/$base"
|
||||
chmod +x "$TARGET_DIR/scripts/agent/$base"
|
||||
done
|
||||
|
||||
if [[ ! -f "$TARGET_DIR/AGENTS.md" ]]; then
|
||||
cat > "$TARGET_DIR/AGENTS.md" <<'AGENTS_EOF'
|
||||
# Agent Guidelines
|
||||
|
||||
## Required Load Order
|
||||
|
||||
1. `~/.config/mosaic/SOUL.md`
|
||||
2. `~/.config/mosaic/STANDARDS.md`
|
||||
3. `~/.config/mosaic/AGENTS.md`
|
||||
4. `~/.config/mosaic/guides/E2E-DELIVERY.md`
|
||||
5. `AGENTS.md` (this file)
|
||||
6. Runtime-specific guide: `~/.config/mosaic/runtime/<runtime>/RUNTIME.md`
|
||||
7. `.mosaic/repo-hooks.sh`
|
||||
|
||||
## Session Lifecycle
|
||||
|
||||
```bash
|
||||
bash scripts/agent/session-start.sh
|
||||
bash scripts/agent/critical.sh
|
||||
bash scripts/agent/session-end.sh
|
||||
```
|
||||
|
||||
## Shared Tools
|
||||
|
||||
- Quality and orchestration guides: `~/.config/mosaic/guides/`
|
||||
- Shared automation tools: `~/.config/mosaic/tools/`
|
||||
|
||||
## Repo-Specific Notes
|
||||
|
||||
- Add project constraints and workflows here.
|
||||
- Implement hook functions in `.mosaic/repo-hooks.sh`.
|
||||
- Scratchpads are mandatory for non-trivial tasks.
|
||||
AGENTS_EOF
|
||||
echo "[mosaic] Wrote: $TARGET_DIR/AGENTS.md"
|
||||
else
|
||||
echo "[mosaic] AGENTS.md exists; add standards load order if missing"
|
||||
fi
|
||||
|
||||
echo "[mosaic] Repo bootstrap complete: $TARGET_DIR"
|
||||
echo "[mosaic] Next: edit $TARGET_DIR/.mosaic/repo-hooks.sh with project workflows"
|
||||
echo "[mosaic] Optional: apply quality tools via ~/.config/mosaic/bin/mosaic-quality-apply --template <template> --target $TARGET_DIR"
|
||||
echo "[mosaic] Optional: run orchestrator rail via ~/.config/mosaic/bin/mosaic-orchestrator-drain"
|
||||
echo "[mosaic] Optional: run detached orchestrator via bash $TARGET_DIR/scripts/agent/orchestrator-daemon.sh start"
|
||||
|
||||
if [[ -n "$QUALITY_TEMPLATE" ]]; then
|
||||
if [[ -x "$MOSAIC_HOME/tools/_scripts/mosaic-quality-apply" ]]; then
|
||||
"$MOSAIC_HOME/tools/_scripts/mosaic-quality-apply" --template "$QUALITY_TEMPLATE" --target "$TARGET_DIR"
|
||||
if [[ -f "$TARGET_DIR/.mosaic/quality-rails.yml" ]]; then
|
||||
sed -i "s/^enabled:.*/enabled: true/" "$TARGET_DIR/.mosaic/quality-rails.yml"
|
||||
sed -i "s/^template:.*/template: \"$QUALITY_TEMPLATE\"/" "$TARGET_DIR/.mosaic/quality-rails.yml"
|
||||
fi
|
||||
echo "[mosaic] Applied quality tools template: $QUALITY_TEMPLATE"
|
||||
else
|
||||
echo "[mosaic] WARN: mosaic-quality-apply not found; skipping quality tools apply" >&2
|
||||
fi
|
||||
fi
|
||||
147
packages/mosaic/framework/tools/_scripts/mosaic-clean-runtime
Executable file
147
packages/mosaic/framework/tools/_scripts/mosaic-clean-runtime
Executable file
@@ -0,0 +1,147 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
RUNTIME="claude"
|
||||
APPLY=0
|
||||
ALL_EMPTY=0
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: $(basename "$0") [options]
|
||||
|
||||
Remove empty runtime directories created by migration/drift.
|
||||
Default mode only checks managed legacy surfaces. Use --all-empty for broader cleanup.
|
||||
|
||||
Options:
|
||||
--runtime <name> Runtime to clean (default: claude)
|
||||
--all-empty Scan all runtime directories (except protected paths)
|
||||
--apply Perform deletions (default: dry-run)
|
||||
-h, --help Show help
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--runtime)
|
||||
[[ $# -lt 2 ]] && { echo "Missing value for --runtime" >&2; exit 1; }
|
||||
RUNTIME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--all-empty)
|
||||
ALL_EMPTY=1
|
||||
shift
|
||||
;;
|
||||
--apply)
|
||||
APPLY=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$RUNTIME" in
|
||||
claude)
|
||||
TARGET_ROOT="$HOME/.claude"
|
||||
managed_roots=(
|
||||
"$HOME/.claude/agent-guides"
|
||||
"$HOME/.claude/scripts"
|
||||
"$HOME/.claude/templates"
|
||||
"$HOME/.claude/presets"
|
||||
"$HOME/.claude/skills"
|
||||
"$HOME/.claude/agents"
|
||||
"$HOME/.claude/agents.bak"
|
||||
)
|
||||
protected_roots=(
|
||||
"$HOME/.claude/.git"
|
||||
"$HOME/.claude/debug"
|
||||
"$HOME/.claude/file-history"
|
||||
"$HOME/.claude/projects"
|
||||
"$HOME/.claude/session-env"
|
||||
"$HOME/.claude/tasks"
|
||||
"$HOME/.claude/todos"
|
||||
"$HOME/.claude/plugins"
|
||||
"$HOME/.claude/statsig"
|
||||
"$HOME/.claude/logs"
|
||||
"$HOME/.claude/shell-snapshots"
|
||||
"$HOME/.claude/paste-cache"
|
||||
"$HOME/.claude/plans"
|
||||
"$HOME/.claude/ide"
|
||||
"$HOME/.claude/cache"
|
||||
)
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported runtime: $RUNTIME" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
[[ -d "$TARGET_ROOT" ]] || { echo "[mosaic-clean] Runtime dir missing: $TARGET_ROOT" >&2; exit 1; }
|
||||
|
||||
is_protected() {
|
||||
local path="$1"
|
||||
for p in "${protected_roots[@]}"; do
|
||||
[[ -e "$p" ]] || continue
|
||||
case "$path" in
|
||||
"$p"|"$p"/*)
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
collect_empty_dirs() {
|
||||
if [[ $ALL_EMPTY -eq 1 ]]; then
|
||||
find "$TARGET_ROOT" -depth -type d -empty
|
||||
else
|
||||
for r in "${managed_roots[@]}"; do
|
||||
[[ -d "$r" ]] || continue
|
||||
find "$r" -depth -type d -empty
|
||||
done
|
||||
fi
|
||||
}
|
||||
|
||||
count_candidates=0
|
||||
count_deletable=0
|
||||
|
||||
while IFS= read -r d; do
|
||||
[[ -n "$d" ]] || continue
|
||||
|
||||
count_candidates=$((count_candidates + 1))
|
||||
|
||||
# Never remove runtime root.
|
||||
[[ "$d" == "$TARGET_ROOT" ]] && continue
|
||||
|
||||
if is_protected "$d"; then
|
||||
continue
|
||||
fi
|
||||
|
||||
count_deletable=$((count_deletable + 1))
|
||||
|
||||
if [[ $APPLY -eq 1 ]]; then
|
||||
rmdir "$d" 2>/dev/null || true
|
||||
if [[ ! -d "$d" ]]; then
|
||||
echo "[mosaic-clean] deleted: $d"
|
||||
fi
|
||||
else
|
||||
echo "[mosaic-clean] would delete: $d"
|
||||
fi
|
||||
done < <(collect_empty_dirs | sort -u)
|
||||
|
||||
mode="managed"
|
||||
[[ $ALL_EMPTY -eq 1 ]] && mode="all-empty"
|
||||
|
||||
if [[ $APPLY -eq 1 ]]; then
|
||||
echo "[mosaic-clean] complete: mode=$mode deleted_or_attempted=$count_deletable candidates=$count_candidates runtime=$RUNTIME"
|
||||
else
|
||||
echo "[mosaic-clean] dry-run: mode=$mode deletable=$count_deletable candidates=$count_candidates runtime=$RUNTIME"
|
||||
echo "[mosaic-clean] re-run with --apply to delete"
|
||||
fi
|
||||
9
packages/mosaic/framework/tools/_scripts/mosaic-critical
Executable file
9
packages/mosaic/framework/tools/_scripts/mosaic-critical
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -x "scripts/agent/critical.sh" ]]; then
|
||||
exec bash scripts/agent/critical.sh
|
||||
fi
|
||||
|
||||
echo "[mosaic] Missing scripts/agent/critical.sh in $(pwd)" >&2
|
||||
exit 1
|
||||
435
packages/mosaic/framework/tools/_scripts/mosaic-doctor
Executable file
435
packages/mosaic/framework/tools/_scripts/mosaic-doctor
Executable file
@@ -0,0 +1,435 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
FAIL_ON_WARN=0
|
||||
VERBOSE=0
|
||||
FIX_MODE=0
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: $(basename "$0") [options]
|
||||
|
||||
Audit Mosaic runtime state and detect drift across agent runtimes.
|
||||
|
||||
Options:
|
||||
--fix Auto-fix: create missing dirs, wire skills into all harnesses
|
||||
--fail-on-warn Exit non-zero when warnings are found
|
||||
--verbose Print pass checks too
|
||||
-h, --help Show help
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--fix)
|
||||
FIX_MODE=1
|
||||
shift
|
||||
;;
|
||||
--fail-on-warn)
|
||||
FAIL_ON_WARN=1
|
||||
shift
|
||||
;;
|
||||
--verbose)
|
||||
VERBOSE=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
fix_count=0
|
||||
fix() { fix_count=$((fix_count + 1)); echo "[FIX] $*"; }
|
||||
|
||||
warn_count=0
|
||||
warn() { warn_count=$((warn_count + 1)); echo "[WARN] $*"; }
|
||||
pass() {
|
||||
if [[ $VERBOSE -eq 1 ]]; then
|
||||
echo "[OK] $*"
|
||||
fi
|
||||
return 0
|
||||
}
|
||||
|
||||
expect_dir() {
|
||||
local d="$1"
|
||||
if [[ ! -d "$d" ]]; then
|
||||
warn "Missing directory: $d"
|
||||
else
|
||||
pass "Directory present: $d"
|
||||
fi
|
||||
}
|
||||
|
||||
expect_file() {
|
||||
local f="$1"
|
||||
if [[ ! -f "$f" ]]; then
|
||||
warn "Missing file: $f"
|
||||
else
|
||||
pass "File present: $f"
|
||||
fi
|
||||
}
|
||||
|
||||
check_runtime_file_copy() {
|
||||
local src="$1"
|
||||
local dst="$2"
|
||||
|
||||
[[ -f "$src" ]] || return 0
|
||||
|
||||
if [[ ! -e "$dst" ]]; then
|
||||
warn "Missing runtime file: $dst"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -L "$dst" ]]; then
|
||||
warn "Runtime file should not be symlinked: $dst"
|
||||
return
|
||||
fi
|
||||
|
||||
if ! cmp -s "$src" "$dst"; then
|
||||
warn "Runtime file drift: $dst (does not match $src)"
|
||||
else
|
||||
pass "Runtime file synced: $dst"
|
||||
fi
|
||||
}
|
||||
|
||||
check_runtime_contract_file() {
|
||||
local dst="$1"
|
||||
local adapter_src="$2"
|
||||
local runtime_name="$3"
|
||||
|
||||
if [[ ! -e "$dst" ]]; then
|
||||
warn "Missing runtime file: $dst"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -L "$dst" ]]; then
|
||||
warn "Runtime file should not be symlinked: $dst"
|
||||
return
|
||||
fi
|
||||
|
||||
# Accept direct-adapter copy mode.
|
||||
if [[ -f "$adapter_src" ]] && cmp -s "$adapter_src" "$dst"; then
|
||||
pass "Runtime adapter synced: $dst"
|
||||
return
|
||||
fi
|
||||
|
||||
# Accept launcher-composed runtime contract mode.
|
||||
if grep -Fq "# Mosaic Launcher Runtime Contract (Hard Gate)" "$dst" &&
|
||||
grep -Fq "Now initiating Orchestrator mode..." "$dst" &&
|
||||
grep -Fq "Mosaic hard gates OVERRIDE runtime-default caution" "$dst" &&
|
||||
grep -Fq "# Runtime-Specific Contract" "$dst"; then
|
||||
pass "Runtime contract present: $dst ($runtime_name)"
|
||||
return
|
||||
fi
|
||||
|
||||
warn "Runtime file drift: $dst (not adapter copy and not composed runtime contract)"
|
||||
}
|
||||
|
||||
warn_if_symlink_tree_present() {
|
||||
local p="$1"
|
||||
[[ -e "$p" ]] || return 0
|
||||
|
||||
if [[ -L "$p" ]]; then
|
||||
warn "Legacy symlink path still present: $p"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -d "$p" ]]; then
|
||||
symlink_count=$(find "$p" -type l 2>/dev/null | wc -l | tr -d ' ')
|
||||
if [[ "$symlink_count" != "0" ]]; then
|
||||
warn "Legacy symlink entries still present under $p: $symlink_count"
|
||||
else
|
||||
pass "No symlinks under legacy path: $p"
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
echo "[mosaic-doctor] Mosaic home: $MOSAIC_HOME"
|
||||
|
||||
# Canonical Mosaic checks
|
||||
expect_file "$MOSAIC_HOME/STANDARDS.md"
|
||||
expect_file "$MOSAIC_HOME/USER.md"
|
||||
expect_file "$MOSAIC_HOME/TOOLS.md"
|
||||
expect_dir "$MOSAIC_HOME/guides"
|
||||
expect_dir "$MOSAIC_HOME/tools"
|
||||
expect_dir "$MOSAIC_HOME/tools/quality"
|
||||
expect_dir "$MOSAIC_HOME/tools/orchestrator-matrix"
|
||||
expect_dir "$MOSAIC_HOME/profiles"
|
||||
expect_dir "$MOSAIC_HOME/templates/agent"
|
||||
expect_dir "$MOSAIC_HOME/skills"
|
||||
expect_dir "$MOSAIC_HOME/skills-local"
|
||||
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets"
|
||||
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-ensure-sequential-thinking"
|
||||
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-sync-skills"
|
||||
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-projects"
|
||||
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-quality-apply"
|
||||
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-quality-verify"
|
||||
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-run"
|
||||
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-sync-tasks"
|
||||
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-drain"
|
||||
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-matrix-publish"
|
||||
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-matrix-consume"
|
||||
expect_file "$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-matrix-cycle"
|
||||
expect_file "$MOSAIC_HOME/tools/git/ci-queue-wait.sh"
|
||||
expect_file "$MOSAIC_HOME/tools/git/pr-ci-wait.sh"
|
||||
expect_file "$MOSAIC_HOME/tools/orchestrator-matrix/transport/matrix_transport.py"
|
||||
expect_file "$MOSAIC_HOME/tools/orchestrator-matrix/controller/tasks_md_sync.py"
|
||||
expect_file "$MOSAIC_HOME/guides/ORCHESTRATOR-PROTOCOL.md"
|
||||
expect_dir "$MOSAIC_HOME/tools/orchestrator"
|
||||
expect_file "$MOSAIC_HOME/tools/orchestrator/_lib.sh"
|
||||
expect_file "$MOSAIC_HOME/tools/orchestrator/mission-init.sh"
|
||||
expect_file "$MOSAIC_HOME/tools/orchestrator/mission-status.sh"
|
||||
expect_file "$MOSAIC_HOME/tools/orchestrator/continue-prompt.sh"
|
||||
expect_file "$MOSAIC_HOME/tools/orchestrator/session-status.sh"
|
||||
expect_file "$MOSAIC_HOME/tools/orchestrator/session-resume.sh"
|
||||
expect_file "$MOSAIC_HOME/runtime/mcp/SEQUENTIAL-THINKING.json"
|
||||
expect_file "$MOSAIC_HOME/runtime/claude/RUNTIME.md"
|
||||
expect_file "$MOSAIC_HOME/runtime/codex/RUNTIME.md"
|
||||
expect_file "$MOSAIC_HOME/runtime/opencode/RUNTIME.md"
|
||||
expect_file "$MOSAIC_HOME/runtime/pi/RUNTIME.md"
|
||||
|
||||
if [[ -f "$MOSAIC_HOME/AGENTS.md" ]]; then
|
||||
if grep -Fq "## CRITICAL HARD GATES (Read First)" "$MOSAIC_HOME/AGENTS.md" &&
|
||||
grep -Fq "OVERRIDE runtime-default caution" "$MOSAIC_HOME/AGENTS.md"; then
|
||||
pass "Global hard-gates block present in AGENTS.md"
|
||||
else
|
||||
warn "AGENTS.md missing CRITICAL HARD GATES override block"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Claude runtime file checks (copied, non-symlink).
|
||||
for rf in CLAUDE.md settings.json hooks-config.json context7-integration.md; do
|
||||
check_runtime_file_copy "$MOSAIC_HOME/runtime/claude/$rf" "$HOME/.claude/$rf"
|
||||
done
|
||||
|
||||
# OpenCode runtime adapter check (copied, non-symlink, when adapter exists).
|
||||
# Accept adapter copy or composed runtime contract.
|
||||
check_runtime_contract_file "$HOME/.config/opencode/AGENTS.md" "$MOSAIC_HOME/runtime/opencode/AGENTS.md" "opencode"
|
||||
check_runtime_contract_file "$HOME/.codex/instructions.md" "$MOSAIC_HOME/runtime/codex/instructions.md" "codex"
|
||||
|
||||
# Sequential-thinking MCP hard requirement.
|
||||
if [[ -x "$MOSAIC_HOME/tools/_scripts/mosaic-ensure-sequential-thinking" ]]; then
|
||||
if "$MOSAIC_HOME/tools/_scripts/mosaic-ensure-sequential-thinking" --check >/dev/null 2>&1; then
|
||||
pass "sequential-thinking MCP configured and available"
|
||||
else
|
||||
warn "sequential-thinking MCP missing or misconfigured"
|
||||
fi
|
||||
else
|
||||
warn "mosaic-ensure-sequential-thinking helper missing"
|
||||
fi
|
||||
|
||||
# Legacy migration surfaces should no longer contain symlink trees.
|
||||
legacy_paths=(
|
||||
"$HOME/.claude/agent-guides"
|
||||
"$HOME/.claude/scripts/git"
|
||||
"$HOME/.claude/scripts/codex"
|
||||
"$HOME/.claude/scripts/bootstrap"
|
||||
"$HOME/.claude/scripts/cicd"
|
||||
"$HOME/.claude/scripts/portainer"
|
||||
"$HOME/.claude/templates"
|
||||
"$HOME/.claude/presets/domains"
|
||||
"$HOME/.claude/presets/tech-stacks"
|
||||
"$HOME/.claude/presets/workflows"
|
||||
)
|
||||
for p in "${legacy_paths[@]}"; do
|
||||
warn_if_symlink_tree_present "$p"
|
||||
done
|
||||
|
||||
# Skills runtime checks (still symlinked into runtime-specific skills dirs).
|
||||
for runtime_skills in "$HOME/.claude/skills" "$HOME/.codex/skills" "$HOME/.config/opencode/skills" "$HOME/.pi/agent/skills"; do
|
||||
[[ -d "$runtime_skills" ]] || continue
|
||||
|
||||
while IFS= read -r -d '' skill; do
|
||||
name="$(basename "$skill")"
|
||||
[[ "$name" == .* ]] && continue
|
||||
target="$runtime_skills/$name"
|
||||
|
||||
if [[ ! -e "$target" ]]; then
|
||||
warn "Missing skill link: $target"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ ! -L "$target" ]]; then
|
||||
warn "Non-symlink skill entry: $target"
|
||||
continue
|
||||
fi
|
||||
|
||||
target_real="$(readlink -f "$target" 2>/dev/null || true)"
|
||||
skill_real="$(readlink -f "$skill" 2>/dev/null || true)"
|
||||
if [[ -z "$target_real" || -z "$skill_real" || "$target_real" != "$skill_real" ]]; then
|
||||
warn "Drifted skill link: $target (expected -> $skill)"
|
||||
else
|
||||
pass "Linked skill: $target"
|
||||
fi
|
||||
done < <(find "$MOSAIC_HOME/skills" "$MOSAIC_HOME/skills-local" -mindepth 1 -maxdepth 1 -type d -print0)
|
||||
done
|
||||
|
||||
# Broken links only in managed runtime skill dirs.
|
||||
link_roots=(
|
||||
"$HOME/.claude/skills"
|
||||
"$HOME/.codex/skills"
|
||||
"$HOME/.config/opencode/skills"
|
||||
"$HOME/.pi/agent/skills"
|
||||
)
|
||||
existing_link_roots=()
|
||||
for d in "${link_roots[@]}"; do
|
||||
[[ -e "$d" ]] && existing_link_roots+=("$d")
|
||||
done
|
||||
|
||||
broken_links=0
|
||||
if [[ ${#existing_link_roots[@]} -gt 0 ]]; then
|
||||
broken_links=$(find "${existing_link_roots[@]}" -xtype l 2>/dev/null | wc -l | tr -d ' ')
|
||||
fi
|
||||
if [[ "$broken_links" != "0" ]]; then
|
||||
warn "Broken skill symlinks detected: $broken_links"
|
||||
fi
|
||||
|
||||
# Pi agent skills directory check.
|
||||
if [[ ! -d "$HOME/.pi/agent/skills" ]]; then
|
||||
warn "Pi skills directory missing: $HOME/.pi/agent/skills"
|
||||
else
|
||||
pass "Pi skills directory present: $HOME/.pi/agent/skills"
|
||||
fi
|
||||
|
||||
# Pi settings.json — check skills path is configured.
|
||||
pi_settings="$HOME/.pi/agent/settings.json"
|
||||
if [[ -f "$pi_settings" ]]; then
|
||||
if grep -q 'skills' "$pi_settings" 2>/dev/null; then
|
||||
pass "Pi settings.json has skills configuration"
|
||||
else
|
||||
warn "Pi settings.json missing skills array — Mosaic skills may not load"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Mosaic-specific skills presence check.
|
||||
mosaic_skills=(mosaic-board mosaic-forge mosaic-prdy mosaic-macp mosaic-standards mosaic-prd mosaic-jarvis mosaic-setup-cicd)
|
||||
for skill_name in "${mosaic_skills[@]}"; do
|
||||
if [[ -d "$MOSAIC_HOME/skills/$skill_name" ]] || [[ -L "$MOSAIC_HOME/skills/$skill_name" ]]; then
|
||||
pass "Mosaic skill present: $skill_name"
|
||||
elif [[ -d "$MOSAIC_HOME/skills-local/$skill_name" ]]; then
|
||||
pass "Mosaic skill present (local): $skill_name"
|
||||
else
|
||||
warn "Missing Mosaic skill: $skill_name"
|
||||
fi
|
||||
done
|
||||
|
||||
# ── --fix mode: auto-wire skills into all harness directories ──────────────
|
||||
if [[ $FIX_MODE -eq 1 ]]; then
|
||||
echo ""
|
||||
echo "[mosaic-doctor] Running auto-fix..."
|
||||
|
||||
# 1. Ensure all harness skill directories exist
|
||||
harness_skill_dirs=(
|
||||
"$HOME/.claude/skills"
|
||||
"$HOME/.codex/skills"
|
||||
"$HOME/.config/opencode/skills"
|
||||
"$HOME/.pi/agent/skills"
|
||||
)
|
||||
|
||||
for hdir in "${harness_skill_dirs[@]}"; do
|
||||
if [[ ! -d "$hdir" ]]; then
|
||||
mkdir -p "$hdir"
|
||||
fix "Created missing directory: $hdir"
|
||||
fi
|
||||
done
|
||||
|
||||
# 2. Wire all Mosaic skills (canonical + local) into every harness
|
||||
skill_sources=("$MOSAIC_HOME/skills" "$MOSAIC_HOME/skills-local")
|
||||
|
||||
for hdir in "${harness_skill_dirs[@]}"; do
|
||||
# Skip if target resolves to canonical dir (avoid self-link)
|
||||
hdir_real="$(readlink -f "$hdir" 2>/dev/null || true)"
|
||||
canonical_real="$(readlink -f "$MOSAIC_HOME/skills" 2>/dev/null || true)"
|
||||
if [[ -n "$hdir_real" && -n "$canonical_real" && "$hdir_real" == "$canonical_real" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
for src_dir in "${skill_sources[@]}"; do
|
||||
[[ -d "$src_dir" ]] || continue
|
||||
|
||||
while IFS= read -r -d '' skill_path; do
|
||||
skill_name="$(basename "$skill_path")"
|
||||
[[ "$skill_name" == .* ]] && continue
|
||||
|
||||
link_path="$hdir/$skill_name"
|
||||
|
||||
if [[ -L "$link_path" ]]; then
|
||||
# Repoint if target differs
|
||||
current_target="$(readlink -f "$link_path" 2>/dev/null || true)"
|
||||
expected_target="$(readlink -f "$skill_path" 2>/dev/null || true)"
|
||||
if [[ "$current_target" != "$expected_target" ]]; then
|
||||
ln -sfn "$skill_path" "$link_path"
|
||||
fix "Repointed skill link: $link_path -> $skill_path"
|
||||
fi
|
||||
elif [[ -e "$link_path" ]]; then
|
||||
# Non-symlink entry — preserve runtime-specific override
|
||||
continue
|
||||
else
|
||||
ln -s "$skill_path" "$link_path"
|
||||
fix "Linked skill: $link_path -> $skill_path"
|
||||
fi
|
||||
done < <(find "$src_dir" -mindepth 1 -maxdepth 1 -type d -print0; find "$src_dir" -mindepth 1 -maxdepth 1 -type l -print0)
|
||||
done
|
||||
|
||||
# Prune broken symlinks in this harness dir
|
||||
while IFS= read -r -d '' broken_link; do
|
||||
rm -f "$broken_link"
|
||||
fix "Removed broken link: $broken_link"
|
||||
done < <(find "$hdir" -mindepth 1 -maxdepth 1 -xtype l -print0 2>/dev/null)
|
||||
done
|
||||
|
||||
# 3. Ensure Pi settings.json includes Mosaic skills path
|
||||
pi_settings_dir="$HOME/.pi/agent"
|
||||
pi_settings_file="$pi_settings_dir/settings.json"
|
||||
mkdir -p "$pi_settings_dir"
|
||||
|
||||
if [[ ! -f "$pi_settings_file" ]]; then
|
||||
echo '{}' > "$pi_settings_file"
|
||||
fix "Created Pi settings.json: $pi_settings_file"
|
||||
fi
|
||||
|
||||
# Add skills paths if not already present
|
||||
mosaic_skills_path="$MOSAIC_HOME/skills"
|
||||
mosaic_local_path="$MOSAIC_HOME/skills-local"
|
||||
if ! grep -q "$mosaic_skills_path" "$pi_settings_file" 2>/dev/null; then
|
||||
# Use a simple approach: read, patch, write
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
python3 -c "
|
||||
import json, sys
|
||||
with open('$pi_settings_file', 'r') as f:
|
||||
data = json.load(f)
|
||||
skills = data.get('skills', [])
|
||||
if not isinstance(skills, list):
|
||||
skills = []
|
||||
for p in ['$mosaic_skills_path', '$mosaic_local_path']:
|
||||
if p not in skills:
|
||||
skills.append(p)
|
||||
data['skills'] = skills
|
||||
with open('$pi_settings_file', 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write('\\n')
|
||||
" 2>/dev/null && fix "Added Mosaic skills paths to Pi settings.json"
|
||||
else
|
||||
warn "python3 not available — cannot patch Pi settings.json. Add manually: skills: [\"$mosaic_skills_path\", \"$mosaic_local_path\"]"
|
||||
fi
|
||||
fi
|
||||
|
||||
# 4. Run link-runtime-assets if available
|
||||
if [[ -x "$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets" ]]; then
|
||||
"$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets" >/dev/null 2>&1 && fix "Re-ran mosaic-link-runtime-assets"
|
||||
fi
|
||||
|
||||
echo "[mosaic-doctor] fixes=$fix_count"
|
||||
fi
|
||||
|
||||
echo "[mosaic-doctor] warnings=$warn_count"
|
||||
if [[ $FAIL_ON_WARN -eq 1 && $warn_count -gt 0 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
283
packages/mosaic/framework/tools/_scripts/mosaic-doctor.ps1
Normal file
283
packages/mosaic/framework/tools/_scripts/mosaic-doctor.ps1
Normal file
@@ -0,0 +1,283 @@
|
||||
# mosaic-doctor.ps1
|
||||
# Audits Mosaic runtime state and detects drift across agent runtimes.
|
||||
# PowerShell equivalent of mosaic-doctor (bash).
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
param(
|
||||
[switch]$FailOnWarn,
|
||||
[switch]$Verbose,
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:USERPROFILE ".config\mosaic" }
|
||||
|
||||
if ($Help) {
|
||||
Write-Host @"
|
||||
Usage: mosaic-doctor.ps1 [-FailOnWarn] [-Verbose] [-Help]
|
||||
|
||||
Audit Mosaic runtime state and detect drift across agent runtimes.
|
||||
"@
|
||||
exit 0
|
||||
}
|
||||
|
||||
$script:warnCount = 0
|
||||
|
||||
function Warn {
|
||||
param([string]$Message)
|
||||
$script:warnCount++
|
||||
Write-Host "[WARN] $Message" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
function Pass {
|
||||
param([string]$Message)
|
||||
if ($Verbose) { Write-Host "[OK] $Message" -ForegroundColor Green }
|
||||
}
|
||||
|
||||
function Expect-Dir {
|
||||
param([string]$Path)
|
||||
if (-not (Test-Path $Path -PathType Container)) { Warn "Missing directory: $Path" }
|
||||
else { Pass "Directory present: $Path" }
|
||||
}
|
||||
|
||||
function Expect-File {
|
||||
param([string]$Path)
|
||||
if (-not (Test-Path $Path -PathType Leaf)) { Warn "Missing file: $Path" }
|
||||
else { Pass "File present: $Path" }
|
||||
}
|
||||
|
||||
function Check-RuntimeFileCopy {
|
||||
param([string]$Src, [string]$Dst)
|
||||
|
||||
if (-not (Test-Path $Src)) { return }
|
||||
|
||||
if (-not (Test-Path $Dst)) {
|
||||
Warn "Missing runtime file: $Dst"
|
||||
return
|
||||
}
|
||||
|
||||
$item = Get-Item $Dst -Force -ErrorAction SilentlyContinue
|
||||
if ($item -and ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
|
||||
Warn "Runtime file should not be symlinked: $Dst"
|
||||
return
|
||||
}
|
||||
|
||||
$srcHash = (Get-FileHash $Src -Algorithm SHA256).Hash
|
||||
$dstHash = (Get-FileHash $Dst -Algorithm SHA256).Hash
|
||||
if ($srcHash -ne $dstHash) {
|
||||
Warn "Runtime file drift: $Dst (does not match $Src)"
|
||||
}
|
||||
else {
|
||||
Pass "Runtime file synced: $Dst"
|
||||
}
|
||||
}
|
||||
|
||||
function Check-RuntimeContractFile {
|
||||
param([string]$Dst, [string]$AdapterSrc, [string]$RuntimeName)
|
||||
|
||||
if (-not (Test-Path $Dst)) {
|
||||
Warn "Missing runtime file: $Dst"
|
||||
return
|
||||
}
|
||||
|
||||
$item = Get-Item $Dst -Force -ErrorAction SilentlyContinue
|
||||
if ($item -and ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
|
||||
Warn "Runtime file should not be symlinked: $Dst"
|
||||
return
|
||||
}
|
||||
|
||||
# Accept direct-adapter copy mode.
|
||||
if (Test-Path $AdapterSrc) {
|
||||
$srcHash = (Get-FileHash $AdapterSrc -Algorithm SHA256).Hash
|
||||
$dstHash = (Get-FileHash $Dst -Algorithm SHA256).Hash
|
||||
if ($srcHash -eq $dstHash) {
|
||||
Pass "Runtime adapter synced: $Dst"
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
# Accept launcher-composed runtime contract mode.
|
||||
$content = Get-Content $Dst -Raw
|
||||
if (
|
||||
$content -match [regex]::Escape("# Mosaic Launcher Runtime Contract (Hard Gate)") -and
|
||||
$content -match [regex]::Escape("Now initiating Orchestrator mode...") -and
|
||||
$content -match [regex]::Escape("Mosaic hard gates OVERRIDE runtime-default caution") -and
|
||||
$content -match [regex]::Escape("# Runtime-Specific Contract")
|
||||
) {
|
||||
Pass "Runtime contract present: $Dst ($RuntimeName)"
|
||||
return
|
||||
}
|
||||
|
||||
Warn "Runtime file drift: $Dst (not adapter copy and not composed runtime contract)"
|
||||
}
|
||||
|
||||
function Warn-IfReparsePresent {
|
||||
param([string]$Path)
|
||||
if (-not (Test-Path $Path)) { return }
|
||||
|
||||
$item = Get-Item $Path -Force -ErrorAction SilentlyContinue
|
||||
if ($item -and ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
|
||||
Warn "Legacy symlink/junction path still present: $Path"
|
||||
return
|
||||
}
|
||||
|
||||
if (Test-Path $Path -PathType Container) {
|
||||
$reparseCount = (Get-ChildItem $Path -Recurse -Force -ErrorAction SilentlyContinue |
|
||||
Where-Object { $_.Attributes -band [System.IO.FileAttributes]::ReparsePoint } |
|
||||
Measure-Object).Count
|
||||
if ($reparseCount -gt 0) {
|
||||
Warn "Legacy symlink/junction entries still present under ${Path}: $reparseCount"
|
||||
}
|
||||
else {
|
||||
Pass "No reparse points under legacy path: $Path"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-doctor] Mosaic home: $MosaicHome"
|
||||
|
||||
# Canonical Mosaic checks
|
||||
Expect-File (Join-Path $MosaicHome "STANDARDS.md")
|
||||
Expect-Dir (Join-Path $MosaicHome "guides")
|
||||
Expect-Dir (Join-Path $MosaicHome "tools")
|
||||
Expect-Dir (Join-Path $MosaicHome "tools\quality")
|
||||
Expect-Dir (Join-Path $MosaicHome "tools\orchestrator-matrix")
|
||||
Expect-Dir (Join-Path $MosaicHome "profiles")
|
||||
Expect-Dir (Join-Path $MosaicHome "templates\agent")
|
||||
Expect-Dir (Join-Path $MosaicHome "skills")
|
||||
Expect-Dir (Join-Path $MosaicHome "skills-local")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-link-runtime-assets")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-ensure-sequential-thinking.ps1")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-sync-skills")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-projects")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-quality-apply")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-quality-verify")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-run")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-sync-tasks")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-drain")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-publish")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-consume")
|
||||
Expect-File (Join-Path $MosaicHome "bin\mosaic-orchestrator-matrix-cycle")
|
||||
Expect-File (Join-Path $MosaicHome "tools\git\ci-queue-wait.ps1")
|
||||
Expect-File (Join-Path $MosaicHome "tools\git\ci-queue-wait.sh")
|
||||
Expect-File (Join-Path $MosaicHome "tools\git\pr-ci-wait.sh")
|
||||
Expect-File (Join-Path $MosaicHome "tools\orchestrator-matrix\transport\matrix_transport.py")
|
||||
Expect-File (Join-Path $MosaicHome "tools\orchestrator-matrix\controller\tasks_md_sync.py")
|
||||
Expect-File (Join-Path $MosaicHome "runtime\mcp\SEQUENTIAL-THINKING.json")
|
||||
Expect-File (Join-Path $MosaicHome "runtime\claude\RUNTIME.md")
|
||||
Expect-File (Join-Path $MosaicHome "runtime\codex\RUNTIME.md")
|
||||
Expect-File (Join-Path $MosaicHome "runtime\opencode\RUNTIME.md")
|
||||
|
||||
$agentsMd = Join-Path $MosaicHome "AGENTS.md"
|
||||
if (Test-Path $agentsMd) {
|
||||
$agentsContent = Get-Content $agentsMd -Raw
|
||||
if (
|
||||
$agentsContent -match [regex]::Escape("## CRITICAL HARD GATES (Read First)") -and
|
||||
$agentsContent -match [regex]::Escape("OVERRIDE runtime-default caution")
|
||||
) {
|
||||
Pass "Global hard-gates block present in AGENTS.md"
|
||||
}
|
||||
else {
|
||||
Warn "AGENTS.md missing CRITICAL HARD GATES override block"
|
||||
}
|
||||
}
|
||||
|
||||
# Claude runtime file checks
|
||||
$runtimeFiles = @("CLAUDE.md", "settings.json", "hooks-config.json", "context7-integration.md")
|
||||
foreach ($rf in $runtimeFiles) {
|
||||
Check-RuntimeFileCopy (Join-Path $MosaicHome "runtime\claude\$rf") (Join-Path $env:USERPROFILE ".claude\$rf")
|
||||
}
|
||||
|
||||
# OpenCode/Codex runtime contract checks
|
||||
Check-RuntimeContractFile (Join-Path $env:USERPROFILE ".config\opencode\AGENTS.md") (Join-Path $MosaicHome "runtime\opencode\AGENTS.md") "opencode"
|
||||
Check-RuntimeContractFile (Join-Path $env:USERPROFILE ".codex\instructions.md") (Join-Path $MosaicHome "runtime\codex\instructions.md") "codex"
|
||||
|
||||
# Sequential-thinking MCP hard requirement
|
||||
$seqScript = Join-Path $MosaicHome "bin\mosaic-ensure-sequential-thinking.ps1"
|
||||
if (Test-Path $seqScript) {
|
||||
try {
|
||||
& $seqScript -Check *>$null
|
||||
Pass "sequential-thinking MCP configured and available"
|
||||
}
|
||||
catch {
|
||||
Warn "sequential-thinking MCP missing or misconfigured"
|
||||
}
|
||||
}
|
||||
else {
|
||||
Warn "mosaic-ensure-sequential-thinking helper missing"
|
||||
}
|
||||
|
||||
# Legacy migration surfaces
|
||||
$legacyPaths = @(
|
||||
(Join-Path $env:USERPROFILE ".claude\agent-guides"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\git"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\codex"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\bootstrap"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\cicd"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\portainer"),
|
||||
(Join-Path $env:USERPROFILE ".claude\templates"),
|
||||
(Join-Path $env:USERPROFILE ".claude\presets\domains"),
|
||||
(Join-Path $env:USERPROFILE ".claude\presets\tech-stacks"),
|
||||
(Join-Path $env:USERPROFILE ".claude\presets\workflows")
|
||||
)
|
||||
foreach ($p in $legacyPaths) {
|
||||
Warn-IfReparsePresent $p
|
||||
}
|
||||
|
||||
# Skills runtime checks (junctions or symlinks into runtime-specific dirs)
|
||||
$linkTargets = @(
|
||||
(Join-Path $env:USERPROFILE ".claude\skills"),
|
||||
(Join-Path $env:USERPROFILE ".codex\skills"),
|
||||
(Join-Path $env:USERPROFILE ".config\opencode\skills")
|
||||
)
|
||||
|
||||
$skillSources = @($MosaicHome + "\skills", $MosaicHome + "\skills-local")
|
||||
|
||||
foreach ($runtimeSkills in $linkTargets) {
|
||||
if (-not (Test-Path $runtimeSkills)) { continue }
|
||||
|
||||
foreach ($sourceDir in $skillSources) {
|
||||
if (-not (Test-Path $sourceDir)) { continue }
|
||||
|
||||
Get-ChildItem $sourceDir -Directory | Where-Object { -not $_.Name.StartsWith(".") } | ForEach-Object {
|
||||
$name = $_.Name
|
||||
$skillPath = $_.FullName
|
||||
$target = Join-Path $runtimeSkills $name
|
||||
|
||||
if (-not (Test-Path $target)) {
|
||||
Warn "Missing skill link: $target"
|
||||
return
|
||||
}
|
||||
|
||||
$item = Get-Item $target -Force -ErrorAction SilentlyContinue
|
||||
if (-not ($item.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
|
||||
Warn "Non-junction skill entry: $target"
|
||||
return
|
||||
}
|
||||
|
||||
$targetResolved = $item.Target
|
||||
if (-not $targetResolved -or (Resolve-Path $targetResolved -ErrorAction SilentlyContinue).Path -ne (Resolve-Path $skillPath -ErrorAction SilentlyContinue).Path) {
|
||||
Warn "Drifted skill link: $target (expected -> $skillPath)"
|
||||
}
|
||||
else {
|
||||
Pass "Linked skill: $target"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
# Broken junctions/symlinks in managed runtime skill dirs
|
||||
$brokenLinks = 0
|
||||
foreach ($d in $linkTargets) {
|
||||
if (-not (Test-Path $d)) { continue }
|
||||
Get-ChildItem $d -Force -ErrorAction SilentlyContinue | Where-Object {
|
||||
($_.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -and -not (Test-Path $_.FullName)
|
||||
} | ForEach-Object { $brokenLinks++ }
|
||||
}
|
||||
if ($brokenLinks -gt 0) {
|
||||
Warn "Broken skill junctions/symlinks detected: $brokenLinks"
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-doctor] warnings=$($script:warnCount)"
|
||||
if ($FailOnWarn -and $script:warnCount -gt 0) {
|
||||
exit 1
|
||||
}
|
||||
119
packages/mosaic/framework/tools/_scripts/mosaic-ensure-excalidraw
Executable file
119
packages/mosaic/framework/tools/_scripts/mosaic-ensure-excalidraw
Executable file
@@ -0,0 +1,119 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
TOOLS_DIR="$MOSAIC_HOME/tools/excalidraw"
|
||||
MODE="apply"
|
||||
SCOPE="user"
|
||||
|
||||
err() { echo "[mosaic-excalidraw] ERROR: $*" >&2; }
|
||||
log() { echo "[mosaic-excalidraw] $*"; }
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--check) MODE="check"; shift ;;
|
||||
--scope)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
err "--scope requires a value: user|local"
|
||||
exit 2
|
||||
fi
|
||||
SCOPE="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
err "Unknown argument: $1"
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
require_binary() {
|
||||
local name="$1"
|
||||
if ! command -v "$name" >/dev/null 2>&1; then
|
||||
err "Required binary missing: $name"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_software() {
|
||||
require_binary node
|
||||
require_binary npm
|
||||
}
|
||||
|
||||
check_tool_dir() {
|
||||
[[ -d "$TOOLS_DIR" ]] || { err "Tool dir not found: $TOOLS_DIR"; return 1; }
|
||||
[[ -f "$TOOLS_DIR/package.json" ]] || { err "package.json not found in $TOOLS_DIR"; return 1; }
|
||||
[[ -f "$TOOLS_DIR/launch.sh" ]] || { err "launch.sh not found in $TOOLS_DIR"; return 1; }
|
||||
}
|
||||
|
||||
check_npm_deps() {
|
||||
[[ -d "$TOOLS_DIR/node_modules/@modelcontextprotocol" ]] || return 1
|
||||
[[ -d "$TOOLS_DIR/node_modules/@excalidraw" ]] || return 1
|
||||
[[ -d "$TOOLS_DIR/node_modules/jsdom" ]] || return 1
|
||||
}
|
||||
|
||||
install_npm_deps() {
|
||||
if check_npm_deps; then
|
||||
return 0
|
||||
fi
|
||||
log "Installing npm deps in $TOOLS_DIR..."
|
||||
(cd "$TOOLS_DIR" && npm install --silent) || {
|
||||
err "npm install failed in $TOOLS_DIR"
|
||||
return 1
|
||||
}
|
||||
}
|
||||
|
||||
check_claude_config() {
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
p = Path.home() / ".claude.json"
|
||||
if not p.exists():
|
||||
raise SystemExit(1)
|
||||
try:
|
||||
data = json.loads(p.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
raise SystemExit(1)
|
||||
mcp = data.get("mcpServers")
|
||||
if not isinstance(mcp, dict):
|
||||
raise SystemExit(1)
|
||||
entry = mcp.get("excalidraw")
|
||||
if not isinstance(entry, dict):
|
||||
raise SystemExit(1)
|
||||
cmd = entry.get("command", "")
|
||||
if not cmd.endswith("launch.sh"):
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
apply_claude_config() {
|
||||
require_binary claude
|
||||
local launch_sh="$TOOLS_DIR/launch.sh"
|
||||
claude mcp add --scope user excalidraw -- "$launch_sh"
|
||||
}
|
||||
|
||||
# ── Check mode ────────────────────────────────────────────────────────────────
|
||||
|
||||
if [[ "$MODE" == "check" ]]; then
|
||||
check_software
|
||||
check_tool_dir
|
||||
if ! check_npm_deps; then
|
||||
err "npm deps not installed in $TOOLS_DIR (run without --check to install)"
|
||||
exit 1
|
||||
fi
|
||||
if ! check_claude_config; then
|
||||
err "excalidraw not registered in ~/.claude.json"
|
||||
exit 1
|
||||
fi
|
||||
log "excalidraw MCP is configured and available"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── Apply mode ────────────────────────────────────────────────────────────────
|
||||
|
||||
check_software
|
||||
check_tool_dir
|
||||
install_npm_deps
|
||||
apply_claude_config
|
||||
log "excalidraw MCP configured (scope: $SCOPE)"
|
||||
262
packages/mosaic/framework/tools/_scripts/mosaic-ensure-sequential-thinking
Executable file
262
packages/mosaic/framework/tools/_scripts/mosaic-ensure-sequential-thinking
Executable file
@@ -0,0 +1,262 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
MODE="apply"
|
||||
RUNTIME="all"
|
||||
STRICT_CHECK=0
|
||||
|
||||
PKG="@modelcontextprotocol/server-sequential-thinking"
|
||||
|
||||
err() { echo "[mosaic-seq] ERROR: $*" >&2; }
|
||||
log() { echo "[mosaic-seq] $*"; }
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--check)
|
||||
MODE="check"
|
||||
shift
|
||||
;;
|
||||
--runtime)
|
||||
if [[ $# -lt 2 ]]; then
|
||||
err "--runtime requires a value: claude|codex|opencode|all"
|
||||
exit 2
|
||||
fi
|
||||
RUNTIME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--strict)
|
||||
STRICT_CHECK=1
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
err "Unknown argument: $1"
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$RUNTIME" in
|
||||
all|claude|codex|opencode) ;;
|
||||
*)
|
||||
err "Invalid runtime: $RUNTIME (expected claude|codex|opencode|all)"
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
|
||||
require_binary() {
|
||||
local name="$1"
|
||||
if ! command -v "$name" >/dev/null 2>&1; then
|
||||
err "Required binary missing: $name"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_software() {
|
||||
require_binary node
|
||||
require_binary npx
|
||||
}
|
||||
|
||||
warm_package() {
|
||||
local timeout_sec="${MOSAIC_SEQ_WARM_TIMEOUT_SEC:-15}"
|
||||
if command -v timeout >/dev/null 2>&1; then
|
||||
timeout "$timeout_sec" npx -y "$PKG" --help >/dev/null 2>&1
|
||||
else
|
||||
npx -y "$PKG" --help >/dev/null 2>&1
|
||||
fi
|
||||
}
|
||||
|
||||
check_claude_config() {
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
p = Path.home() / ".claude" / "settings.json"
|
||||
if not p.exists():
|
||||
raise SystemExit(1)
|
||||
try:
|
||||
data = json.loads(p.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
raise SystemExit(1)
|
||||
mcp = data.get("mcpServers")
|
||||
if not isinstance(mcp, dict):
|
||||
raise SystemExit(1)
|
||||
entry = mcp.get("sequential-thinking")
|
||||
if not isinstance(entry, dict):
|
||||
raise SystemExit(1)
|
||||
if entry.get("command") != "npx":
|
||||
raise SystemExit(1)
|
||||
args = entry.get("args")
|
||||
if args != ["-y", "@modelcontextprotocol/server-sequential-thinking"]:
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
apply_claude_config() {
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
p = Path.home() / ".claude" / "settings.json"
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
if p.exists():
|
||||
try:
|
||||
data = json.loads(p.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
data = {}
|
||||
else:
|
||||
data = {}
|
||||
mcp = data.get("mcpServers")
|
||||
if not isinstance(mcp, dict):
|
||||
mcp = {}
|
||||
mcp["sequential-thinking"] = {
|
||||
"command": "npx",
|
||||
"args": ["-y", "@modelcontextprotocol/server-sequential-thinking"]
|
||||
}
|
||||
data["mcpServers"] = mcp
|
||||
p.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
||||
PY
|
||||
}
|
||||
|
||||
check_codex_config() {
|
||||
local cfg="$HOME/.codex/config.toml"
|
||||
[[ -f "$cfg" ]] || return 1
|
||||
grep -Eq '^\[mcp_servers\.(sequential-thinking|sequential_thinking)\]' "$cfg" && \
|
||||
grep -q '^command = "npx"' "$cfg" && \
|
||||
grep -q '@modelcontextprotocol/server-sequential-thinking' "$cfg"
|
||||
}
|
||||
|
||||
apply_codex_config() {
|
||||
local cfg="$HOME/.codex/config.toml"
|
||||
mkdir -p "$(dirname "$cfg")"
|
||||
[[ -f "$cfg" ]] || touch "$cfg"
|
||||
|
||||
local tmp
|
||||
tmp="$(mktemp)"
|
||||
awk '
|
||||
BEGIN { skip = 0 }
|
||||
/^\[mcp_servers\.(sequential-thinking|sequential_thinking)\]/ { skip = 1; next }
|
||||
skip && /^\[/ { skip = 0 }
|
||||
!skip { print }
|
||||
' "$cfg" > "$tmp"
|
||||
mv "$tmp" "$cfg"
|
||||
|
||||
{
|
||||
echo ""
|
||||
echo "[mcp_servers.sequential-thinking]"
|
||||
echo "command = \"npx\""
|
||||
echo "args = [\"-y\", \"@modelcontextprotocol/server-sequential-thinking\"]"
|
||||
} >> "$cfg"
|
||||
}
|
||||
|
||||
check_opencode_config() {
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
p = Path.home() / ".config" / "opencode" / "config.json"
|
||||
if not p.exists():
|
||||
raise SystemExit(1)
|
||||
try:
|
||||
data = json.loads(p.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
raise SystemExit(1)
|
||||
mcp = data.get("mcp")
|
||||
if not isinstance(mcp, dict):
|
||||
raise SystemExit(1)
|
||||
entry = mcp.get("sequential-thinking")
|
||||
if not isinstance(entry, dict):
|
||||
raise SystemExit(1)
|
||||
if entry.get("type") != "local":
|
||||
raise SystemExit(1)
|
||||
if entry.get("command") != ["npx", "-y", "@modelcontextprotocol/server-sequential-thinking"]:
|
||||
raise SystemExit(1)
|
||||
if entry.get("enabled") is not True:
|
||||
raise SystemExit(1)
|
||||
PY
|
||||
}
|
||||
|
||||
apply_opencode_config() {
|
||||
python3 - <<'PY'
|
||||
import json
|
||||
from pathlib import Path
|
||||
p = Path.home() / ".config" / "opencode" / "config.json"
|
||||
p.parent.mkdir(parents=True, exist_ok=True)
|
||||
if p.exists():
|
||||
try:
|
||||
data = json.loads(p.read_text(encoding="utf-8"))
|
||||
except Exception:
|
||||
data = {}
|
||||
else:
|
||||
data = {}
|
||||
mcp = data.get("mcp")
|
||||
if not isinstance(mcp, dict):
|
||||
mcp = {}
|
||||
mcp["sequential-thinking"] = {
|
||||
"type": "local",
|
||||
"command": ["npx", "-y", "@modelcontextprotocol/server-sequential-thinking"],
|
||||
"enabled": True
|
||||
}
|
||||
data["mcp"] = mcp
|
||||
p.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8")
|
||||
PY
|
||||
}
|
||||
|
||||
check_runtime_config() {
|
||||
case "$RUNTIME" in
|
||||
all)
|
||||
check_claude_config
|
||||
check_codex_config
|
||||
check_opencode_config
|
||||
;;
|
||||
claude)
|
||||
check_claude_config
|
||||
;;
|
||||
codex)
|
||||
check_codex_config
|
||||
;;
|
||||
opencode)
|
||||
check_opencode_config
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
apply_runtime_config() {
|
||||
case "$RUNTIME" in
|
||||
all)
|
||||
apply_claude_config
|
||||
apply_codex_config
|
||||
apply_opencode_config
|
||||
;;
|
||||
claude)
|
||||
apply_claude_config
|
||||
;;
|
||||
codex)
|
||||
apply_codex_config
|
||||
;;
|
||||
opencode)
|
||||
apply_opencode_config
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
if [[ "$MODE" == "check" ]]; then
|
||||
check_software
|
||||
check_runtime_config
|
||||
|
||||
# Runtime launch checks should be local/fast by default.
|
||||
if [[ "$STRICT_CHECK" -eq 1 || "${MOSAIC_SEQ_CHECK_WARM:-0}" == "1" ]]; then
|
||||
if ! warm_package; then
|
||||
err "sequential-thinking package warm-up failed in strict mode"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
log "sequential-thinking MCP is configured and available (${RUNTIME})"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
check_software
|
||||
if ! warm_package; then
|
||||
err "Unable to warm sequential-thinking package (npx timeout/failure)"
|
||||
exit 1
|
||||
fi
|
||||
apply_runtime_config
|
||||
log "sequential-thinking MCP configured (${RUNTIME})"
|
||||
114
packages/mosaic/framework/tools/_scripts/mosaic-ensure-sequential-thinking.ps1
Executable file
114
packages/mosaic/framework/tools/_scripts/mosaic-ensure-sequential-thinking.ps1
Executable file
@@ -0,0 +1,114 @@
|
||||
# mosaic-ensure-sequential-thinking.ps1
|
||||
param(
|
||||
[switch]$Check
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$Pkg = "@modelcontextprotocol/server-sequential-thinking"
|
||||
|
||||
function Require-Binary {
|
||||
param([string]$Name)
|
||||
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
|
||||
throw "Required binary missing: $Name"
|
||||
}
|
||||
}
|
||||
|
||||
function Warm-Package {
|
||||
$null = & npx -y $Pkg --help 2>$null
|
||||
}
|
||||
|
||||
function Set-ClaudeConfig {
|
||||
$path = Join-Path $env:USERPROFILE ".claude\settings.json"
|
||||
New-Item -ItemType Directory -Path (Split-Path $path -Parent) -Force | Out-Null
|
||||
|
||||
$data = @{}
|
||||
if (Test-Path $path) {
|
||||
try { $data = Get-Content $path -Raw | ConvertFrom-Json -AsHashtable } catch { $data = @{} }
|
||||
}
|
||||
if (-not $data.ContainsKey("mcpServers") -or -not ($data["mcpServers"] -is [hashtable])) {
|
||||
$data["mcpServers"] = @{}
|
||||
}
|
||||
$data["mcpServers"]["sequential-thinking"] = @{
|
||||
command = "npx"
|
||||
args = @("-y", "@modelcontextprotocol/server-sequential-thinking")
|
||||
}
|
||||
$data | ConvertTo-Json -Depth 20 | Set-Content -Path $path -Encoding UTF8
|
||||
}
|
||||
|
||||
function Set-CodexConfig {
|
||||
$path = Join-Path $env:USERPROFILE ".codex\config.toml"
|
||||
New-Item -ItemType Directory -Path (Split-Path $path -Parent) -Force | Out-Null
|
||||
if (-not (Test-Path $path)) { New-Item -ItemType File -Path $path -Force | Out-Null }
|
||||
|
||||
$content = Get-Content $path -Raw
|
||||
$content = [regex]::Replace($content, "(?ms)^\[mcp_servers\.(sequential-thinking|sequential_thinking)\].*?(?=^\[|\z)", "")
|
||||
$content = $content.TrimEnd() + "`n`n[mcp_servers.sequential-thinking]`ncommand = `"npx`"`nargs = [`"-y`", `"@modelcontextprotocol/server-sequential-thinking`"]`n"
|
||||
Set-Content -Path $path -Value $content -Encoding UTF8
|
||||
}
|
||||
|
||||
function Set-OpenCodeConfig {
|
||||
$path = Join-Path $env:USERPROFILE ".config\opencode\config.json"
|
||||
New-Item -ItemType Directory -Path (Split-Path $path -Parent) -Force | Out-Null
|
||||
|
||||
$data = @{}
|
||||
if (Test-Path $path) {
|
||||
try { $data = Get-Content $path -Raw | ConvertFrom-Json -AsHashtable } catch { $data = @{} }
|
||||
}
|
||||
if (-not $data.ContainsKey("mcp") -or -not ($data["mcp"] -is [hashtable])) {
|
||||
$data["mcp"] = @{}
|
||||
}
|
||||
$data["mcp"]["sequential-thinking"] = @{
|
||||
type = "local"
|
||||
command = @("npx", "-y", "@modelcontextprotocol/server-sequential-thinking")
|
||||
enabled = $true
|
||||
}
|
||||
$data | ConvertTo-Json -Depth 20 | Set-Content -Path $path -Encoding UTF8
|
||||
}
|
||||
|
||||
function Test-Configs {
|
||||
$claudeOk = $false
|
||||
$codexOk = $false
|
||||
$opencodeOk = $false
|
||||
|
||||
$claudePath = Join-Path $env:USERPROFILE ".claude\settings.json"
|
||||
if (Test-Path $claudePath) {
|
||||
try {
|
||||
$c = Get-Content $claudePath -Raw | ConvertFrom-Json -AsHashtable
|
||||
$claudeOk = $c.ContainsKey("mcpServers") -and $c["mcpServers"].ContainsKey("sequential-thinking")
|
||||
} catch {}
|
||||
}
|
||||
|
||||
$codexPath = Join-Path $env:USERPROFILE ".codex\config.toml"
|
||||
if (Test-Path $codexPath) {
|
||||
$raw = Get-Content $codexPath -Raw
|
||||
$codexOk = $raw -match "\[mcp_servers\.(sequential-thinking|sequential_thinking)\]" -and $raw -match "@modelcontextprotocol/server-sequential-thinking"
|
||||
}
|
||||
|
||||
$opencodePath = Join-Path $env:USERPROFILE ".config\opencode\config.json"
|
||||
if (Test-Path $opencodePath) {
|
||||
try {
|
||||
$o = Get-Content $opencodePath -Raw | ConvertFrom-Json -AsHashtable
|
||||
$opencodeOk = $o.ContainsKey("mcp") -and $o["mcp"].ContainsKey("sequential-thinking")
|
||||
} catch {}
|
||||
}
|
||||
|
||||
if (-not ($claudeOk -and $codexOk -and $opencodeOk)) {
|
||||
throw "Sequential-thinking MCP runtime config is incomplete"
|
||||
}
|
||||
}
|
||||
|
||||
Require-Binary node
|
||||
Require-Binary npx
|
||||
Warm-Package
|
||||
|
||||
if ($Check) {
|
||||
Test-Configs
|
||||
Write-Host "[mosaic-seq] sequential-thinking MCP is configured and available"
|
||||
exit 0
|
||||
}
|
||||
|
||||
Set-ClaudeConfig
|
||||
Set-CodexConfig
|
||||
Set-OpenCodeConfig
|
||||
Write-Host "[mosaic-seq] sequential-thinking MCP configured for Claude, Codex, and OpenCode"
|
||||
424
packages/mosaic/framework/tools/_scripts/mosaic-init
Executable file
424
packages/mosaic/framework/tools/_scripts/mosaic-init
Executable file
@@ -0,0 +1,424 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# mosaic-init — Interactive agent identity, user profile, and tool config generator
|
||||
#
|
||||
# Usage:
|
||||
# mosaic-init # Interactive mode
|
||||
# mosaic-init --name "Jarvis" --style direct # Flag overrides
|
||||
# mosaic-init --name "Jarvis" --role "memory steward" --style direct \
|
||||
# --accessibility "ADHD-friendly chunking" --guardrails "Never auto-commit"
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
SOUL_TEMPLATE="$MOSAIC_HOME/templates/SOUL.md.template"
|
||||
USER_TEMPLATE="$MOSAIC_HOME/templates/USER.md.template"
|
||||
TOOLS_TEMPLATE="$MOSAIC_HOME/templates/TOOLS.md.template"
|
||||
SOUL_OUTPUT="$MOSAIC_HOME/SOUL.md"
|
||||
USER_OUTPUT="$MOSAIC_HOME/USER.md"
|
||||
TOOLS_OUTPUT="$MOSAIC_HOME/TOOLS.md"
|
||||
|
||||
# Defaults
|
||||
AGENT_NAME=""
|
||||
ROLE_DESCRIPTION=""
|
||||
STYLE=""
|
||||
ACCESSIBILITY=""
|
||||
CUSTOM_GUARDRAILS=""
|
||||
|
||||
# USER.md defaults
|
||||
USER_NAME=""
|
||||
PRONOUNS=""
|
||||
TIMEZONE=""
|
||||
BACKGROUND=""
|
||||
COMMUNICATION_PREFS=""
|
||||
PERSONAL_BOUNDARIES=""
|
||||
PROJECTS_TABLE=""
|
||||
|
||||
# TOOLS.md defaults
|
||||
GIT_PROVIDERS_TABLE=""
|
||||
CREDENTIALS_LOCATION=""
|
||||
CUSTOM_TOOLS_SECTION=""
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: $(basename "$0") [options]
|
||||
|
||||
Generate Mosaic identity and configuration files:
|
||||
- SOUL.md — Agent identity contract
|
||||
- USER.md — User profile and accessibility
|
||||
- TOOLS.md — Machine-level tool reference
|
||||
|
||||
Interactive by default. Use flags to skip prompts.
|
||||
|
||||
Options:
|
||||
--name <name> Agent name (e.g., "Jarvis", "Assistant")
|
||||
--role <description> Role description (e.g., "memory steward, execution partner")
|
||||
--style <style> Communication style: direct, friendly, or formal
|
||||
--accessibility <prefs> Accessibility preferences (e.g., "ADHD-friendly chunking")
|
||||
--guardrails <rules> Custom guardrails (appended to defaults)
|
||||
--user-name <name> Your name for USER.md
|
||||
--pronouns <pronouns> Your pronouns (e.g., "He/Him")
|
||||
--timezone <tz> Your timezone (e.g., "America/Chicago")
|
||||
--non-interactive Fail if any required value is missing (no prompts)
|
||||
--soul-only Only generate SOUL.md
|
||||
-h, --help Show help
|
||||
USAGE
|
||||
}
|
||||
|
||||
NON_INTERACTIVE=0
|
||||
SOUL_ONLY=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--name) AGENT_NAME="$2"; shift 2 ;;
|
||||
--role) ROLE_DESCRIPTION="$2"; shift 2 ;;
|
||||
--style) STYLE="$2"; shift 2 ;;
|
||||
--accessibility) ACCESSIBILITY="$2"; shift 2 ;;
|
||||
--guardrails) CUSTOM_GUARDRAILS="$2"; shift 2 ;;
|
||||
--user-name) USER_NAME="$2"; shift 2 ;;
|
||||
--pronouns) PRONOUNS="$2"; shift 2 ;;
|
||||
--timezone) TIMEZONE="$2"; shift 2 ;;
|
||||
--non-interactive) NON_INTERACTIVE=1; shift ;;
|
||||
--soul-only) SOUL_ONLY=1; shift ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
*) echo "Unknown argument: $1" >&2; usage >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
prompt_if_empty() {
|
||||
local var_name="$1"
|
||||
local prompt_text="$2"
|
||||
local default_value="${3:-}"
|
||||
local current_value="${!var_name}"
|
||||
|
||||
if [[ -n "$current_value" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ $NON_INTERACTIVE -eq 1 ]]; then
|
||||
if [[ -n "$default_value" ]]; then
|
||||
eval "$var_name=\"$default_value\""
|
||||
return
|
||||
fi
|
||||
echo "[mosaic-init] ERROR: --$var_name is required in non-interactive mode" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -n "$default_value" ]]; then
|
||||
prompt_text="$prompt_text [$default_value]"
|
||||
fi
|
||||
|
||||
printf "%s: " "$prompt_text"
|
||||
read -r value
|
||||
if [[ -z "$value" && -n "$default_value" ]]; then
|
||||
value="$default_value"
|
||||
fi
|
||||
eval "$var_name=\"$value\""
|
||||
}
|
||||
|
||||
prompt_multiline() {
|
||||
local var_name="$1"
|
||||
local prompt_text="$2"
|
||||
local default_value="${3:-}"
|
||||
local current_value="${!var_name}"
|
||||
|
||||
if [[ -n "$current_value" ]]; then
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ $NON_INTERACTIVE -eq 1 ]]; then
|
||||
eval "$var_name=\"$default_value\""
|
||||
return
|
||||
fi
|
||||
|
||||
echo "$prompt_text"
|
||||
printf "(Press Enter to skip, or type your response): "
|
||||
read -r value
|
||||
if [[ -z "$value" ]]; then
|
||||
value="$default_value"
|
||||
fi
|
||||
eval "$var_name=\"$value\""
|
||||
}
|
||||
|
||||
# ── SOUL.md Generation ────────────────────────────────────────
|
||||
echo "[mosaic-init] Generating SOUL.md — agent identity contract"
|
||||
echo ""
|
||||
|
||||
prompt_if_empty AGENT_NAME "What name should agents use" "Assistant"
|
||||
prompt_if_empty ROLE_DESCRIPTION "Agent role description" "execution partner and visibility engine"
|
||||
|
||||
if [[ -z "$STYLE" && $NON_INTERACTIVE -eq 0 ]]; then
|
||||
echo ""
|
||||
echo "Communication style:"
|
||||
echo " 1) direct — Concise, no fluff, actionable"
|
||||
echo " 2) friendly — Warm but efficient, conversational"
|
||||
echo " 3) formal — Professional, structured, thorough"
|
||||
printf "Choose [1/2/3] (default: 1): "
|
||||
read -r style_choice
|
||||
case "${style_choice:-1}" in
|
||||
1|direct) STYLE="direct" ;;
|
||||
2|friendly) STYLE="friendly" ;;
|
||||
3|formal) STYLE="formal" ;;
|
||||
*) STYLE="direct" ;;
|
||||
esac
|
||||
elif [[ -z "$STYLE" ]]; then
|
||||
STYLE="direct"
|
||||
fi
|
||||
|
||||
prompt_if_empty ACCESSIBILITY "Accessibility preferences (or 'none')" "none"
|
||||
|
||||
if [[ $NON_INTERACTIVE -eq 0 && -z "$CUSTOM_GUARDRAILS" ]]; then
|
||||
echo ""
|
||||
printf "Custom guardrails (optional, press Enter to skip): "
|
||||
read -r CUSTOM_GUARDRAILS
|
||||
fi
|
||||
|
||||
# Build behavioral principles based on style + accessibility
|
||||
BEHAVIORAL_PRINCIPLES=""
|
||||
case "$STYLE" in
|
||||
direct)
|
||||
BEHAVIORAL_PRINCIPLES="1. Clarity over performance theater.
|
||||
2. Practical execution over abstract planning.
|
||||
3. Truthfulness over confidence: state uncertainty explicitly.
|
||||
4. Visible state over hidden assumptions.
|
||||
5. Accessibility-aware — see \`~/.config/mosaic/USER.md\` for user-specific accommodations."
|
||||
;;
|
||||
friendly)
|
||||
BEHAVIORAL_PRINCIPLES="1. Be helpful and approachable while staying efficient.
|
||||
2. Provide context and explain reasoning when helpful.
|
||||
3. Truthfulness over confidence: state uncertainty explicitly.
|
||||
4. Visible state over hidden assumptions.
|
||||
5. Accessibility-aware — see \`~/.config/mosaic/USER.md\` for user-specific accommodations."
|
||||
;;
|
||||
formal)
|
||||
BEHAVIORAL_PRINCIPLES="1. Maintain professional, structured communication.
|
||||
2. Provide thorough analysis with explicit tradeoffs.
|
||||
3. Truthfulness over confidence: state uncertainty explicitly.
|
||||
4. Document decisions and rationale clearly.
|
||||
5. Accessibility-aware — see \`~/.config/mosaic/USER.md\` for user-specific accommodations."
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ "$ACCESSIBILITY" != "none" && -n "$ACCESSIBILITY" ]]; then
|
||||
BEHAVIORAL_PRINCIPLES="$BEHAVIORAL_PRINCIPLES
|
||||
6. $ACCESSIBILITY."
|
||||
fi
|
||||
|
||||
# Build communication style section
|
||||
COMMUNICATION_STYLE=""
|
||||
case "$STYLE" in
|
||||
direct)
|
||||
COMMUNICATION_STYLE="- Be direct, concise, and concrete.
|
||||
- Avoid fluff, hype, and anthropomorphic roleplay.
|
||||
- Do not simulate certainty when facts are missing.
|
||||
- Prefer actionable next steps and explicit tradeoffs."
|
||||
;;
|
||||
friendly)
|
||||
COMMUNICATION_STYLE="- Be warm and conversational while staying focused.
|
||||
- Explain your reasoning when it helps the user.
|
||||
- Do not simulate certainty when facts are missing.
|
||||
- Prefer actionable next steps with clear context."
|
||||
;;
|
||||
formal)
|
||||
COMMUNICATION_STYLE="- Use professional, structured language.
|
||||
- Provide thorough explanations with supporting detail.
|
||||
- Do not simulate certainty when facts are missing.
|
||||
- Present options with explicit tradeoffs and recommendations."
|
||||
;;
|
||||
esac
|
||||
|
||||
# Format custom guardrails
|
||||
FORMATTED_GUARDRAILS=""
|
||||
if [[ -n "$CUSTOM_GUARDRAILS" ]]; then
|
||||
FORMATTED_GUARDRAILS="- $CUSTOM_GUARDRAILS"
|
||||
fi
|
||||
|
||||
# Verify template exists
|
||||
if [[ ! -f "$SOUL_TEMPLATE" ]]; then
|
||||
echo "[mosaic-init] ERROR: Template not found: $SOUL_TEMPLATE" >&2
|
||||
echo "[mosaic-init] Run the Mosaic installer first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Generate SOUL.md from template using awk (handles multi-line values)
|
||||
awk -v name="$AGENT_NAME" \
|
||||
-v role="$ROLE_DESCRIPTION" \
|
||||
-v principles="$BEHAVIORAL_PRINCIPLES" \
|
||||
-v comms="$COMMUNICATION_STYLE" \
|
||||
-v guardrails="$FORMATTED_GUARDRAILS" \
|
||||
'{
|
||||
gsub(/\{\{AGENT_NAME\}\}/, name)
|
||||
gsub(/\{\{ROLE_DESCRIPTION\}\}/, role)
|
||||
gsub(/\{\{BEHAVIORAL_PRINCIPLES\}\}/, principles)
|
||||
gsub(/\{\{COMMUNICATION_STYLE\}\}/, comms)
|
||||
gsub(/\{\{CUSTOM_GUARDRAILS\}\}/, guardrails)
|
||||
print
|
||||
}' "$SOUL_TEMPLATE" > "$SOUL_OUTPUT"
|
||||
|
||||
echo ""
|
||||
echo "[mosaic-init] Generated: $SOUL_OUTPUT"
|
||||
echo "[mosaic-init] Agent name: $AGENT_NAME"
|
||||
echo "[mosaic-init] Style: $STYLE"
|
||||
|
||||
if [[ $SOUL_ONLY -eq 1 ]]; then
|
||||
# Push to runtime adapters and exit
|
||||
if [[ -x "$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets" ]]; then
|
||||
echo "[mosaic-init] Updating runtime adapters..."
|
||||
"$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets"
|
||||
fi
|
||||
echo "[mosaic-init] Done. Launch with: mosaic claude"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# ── USER.md Generation ────────────────────────────────────────
|
||||
echo ""
|
||||
echo "[mosaic-init] Generating USER.md — user profile"
|
||||
echo ""
|
||||
|
||||
prompt_if_empty USER_NAME "Your name" ""
|
||||
prompt_if_empty PRONOUNS "Your pronouns" "They/Them"
|
||||
prompt_if_empty TIMEZONE "Your timezone" "UTC"
|
||||
|
||||
prompt_multiline BACKGROUND "Your professional background (brief summary)" "(not configured)"
|
||||
|
||||
# Build accessibility section
|
||||
ACCESSIBILITY_SECTION=""
|
||||
if [[ "$ACCESSIBILITY" != "none" && -n "$ACCESSIBILITY" ]]; then
|
||||
ACCESSIBILITY_SECTION="$ACCESSIBILITY"
|
||||
else
|
||||
if [[ $NON_INTERACTIVE -eq 0 ]]; then
|
||||
echo ""
|
||||
prompt_multiline ACCESSIBILITY_SECTION \
|
||||
"Accessibility or neurodivergence accommodations (or press Enter to skip)" \
|
||||
"(No specific accommodations configured. Edit this section to add any.)"
|
||||
else
|
||||
ACCESSIBILITY_SECTION="(No specific accommodations configured. Edit this section to add any.)"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Build communication preferences
|
||||
if [[ -z "$COMMUNICATION_PREFS" ]]; then
|
||||
case "$STYLE" in
|
||||
direct)
|
||||
COMMUNICATION_PREFS="- Direct and concise
|
||||
- No sycophancy
|
||||
- Executive summaries and tables for overview"
|
||||
;;
|
||||
friendly)
|
||||
COMMUNICATION_PREFS="- Warm and conversational
|
||||
- Explain reasoning when helpful
|
||||
- Balance thoroughness with brevity"
|
||||
;;
|
||||
formal)
|
||||
COMMUNICATION_PREFS="- Professional and structured
|
||||
- Thorough explanations with supporting detail
|
||||
- Formal tone with explicit recommendations"
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
prompt_multiline PERSONAL_BOUNDARIES \
|
||||
"Personal boundaries or preferences agents should respect" \
|
||||
"(Edit this section to add any personal boundaries.)"
|
||||
|
||||
if [[ -z "$PROJECTS_TABLE" ]]; then
|
||||
PROJECTS_TABLE="| Project | Stack | Registry |
|
||||
|---------|-------|----------|
|
||||
| (none configured) | | |"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$USER_TEMPLATE" ]]; then
|
||||
echo "[mosaic-init] WARN: USER.md template not found: $USER_TEMPLATE" >&2
|
||||
echo "[mosaic-init] Skipping USER.md generation." >&2
|
||||
else
|
||||
awk -v user_name="$USER_NAME" \
|
||||
-v pronouns="$PRONOUNS" \
|
||||
-v timezone="$TIMEZONE" \
|
||||
-v background="$BACKGROUND" \
|
||||
-v accessibility="$ACCESSIBILITY_SECTION" \
|
||||
-v comms="$COMMUNICATION_PREFS" \
|
||||
-v boundaries="$PERSONAL_BOUNDARIES" \
|
||||
-v projects="$PROJECTS_TABLE" \
|
||||
'{
|
||||
gsub(/\{\{USER_NAME\}\}/, user_name)
|
||||
gsub(/\{\{PRONOUNS\}\}/, pronouns)
|
||||
gsub(/\{\{TIMEZONE\}\}/, timezone)
|
||||
gsub(/\{\{BACKGROUND\}\}/, background)
|
||||
gsub(/\{\{ACCESSIBILITY_SECTION\}\}/, accessibility)
|
||||
gsub(/\{\{COMMUNICATION_PREFS\}\}/, comms)
|
||||
gsub(/\{\{PERSONAL_BOUNDARIES\}\}/, boundaries)
|
||||
gsub(/\{\{PROJECTS_TABLE\}\}/, projects)
|
||||
print
|
||||
}' "$USER_TEMPLATE" > "$USER_OUTPUT"
|
||||
|
||||
echo "[mosaic-init] Generated: $USER_OUTPUT"
|
||||
fi
|
||||
|
||||
# ── TOOLS.md Generation ───────────────────────────────────────
|
||||
echo ""
|
||||
echo "[mosaic-init] Generating TOOLS.md — machine-level tool reference"
|
||||
echo ""
|
||||
|
||||
if [[ -z "$GIT_PROVIDERS_TABLE" ]]; then
|
||||
if [[ $NON_INTERACTIVE -eq 0 ]]; then
|
||||
echo "Git providers (add rows for your Gitea/GitHub/GitLab instances):"
|
||||
printf "Primary git provider URL (or press Enter to skip): "
|
||||
read -r git_url
|
||||
if [[ -n "$git_url" ]]; then
|
||||
printf "Provider name: "
|
||||
read -r git_name
|
||||
printf "CLI tool (tea/gh/glab): "
|
||||
read -r git_cli
|
||||
printf "Purpose: "
|
||||
read -r git_purpose
|
||||
GIT_PROVIDERS_TABLE="| Instance | URL | CLI | Purpose |
|
||||
|----------|-----|-----|---------|
|
||||
| $git_name | $git_url | \`$git_cli\` | $git_purpose |"
|
||||
else
|
||||
GIT_PROVIDERS_TABLE="| Instance | URL | CLI | Purpose |
|
||||
|----------|-----|-----|---------|
|
||||
| (add your git providers here) | | | |"
|
||||
fi
|
||||
else
|
||||
GIT_PROVIDERS_TABLE="| Instance | URL | CLI | Purpose |
|
||||
|----------|-----|-----|---------|
|
||||
| (add your git providers here) | | | |"
|
||||
fi
|
||||
fi
|
||||
|
||||
prompt_if_empty CREDENTIALS_LOCATION "Credential file path (or 'none')" "none"
|
||||
|
||||
if [[ -z "$CUSTOM_TOOLS_SECTION" ]]; then
|
||||
CUSTOM_TOOLS_SECTION="## Custom Tools
|
||||
|
||||
(Add any machine-specific tools, scripts, or workflows here.)"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$TOOLS_TEMPLATE" ]]; then
|
||||
echo "[mosaic-init] WARN: TOOLS.md template not found: $TOOLS_TEMPLATE" >&2
|
||||
echo "[mosaic-init] Skipping TOOLS.md generation." >&2
|
||||
else
|
||||
awk -v providers="$GIT_PROVIDERS_TABLE" \
|
||||
-v creds="$CREDENTIALS_LOCATION" \
|
||||
-v custom="$CUSTOM_TOOLS_SECTION" \
|
||||
'{
|
||||
gsub(/\{\{GIT_PROVIDERS_TABLE\}\}/, providers)
|
||||
gsub(/\{\{CREDENTIALS_LOCATION\}\}/, creds)
|
||||
gsub(/\{\{CUSTOM_TOOLS_SECTION\}\}/, custom)
|
||||
print
|
||||
}' "$TOOLS_TEMPLATE" > "$TOOLS_OUTPUT"
|
||||
|
||||
echo "[mosaic-init] Generated: $TOOLS_OUTPUT"
|
||||
fi
|
||||
|
||||
# ── Finalize ──────────────────────────────────────────────────
|
||||
|
||||
# Push to runtime adapters
|
||||
if [[ -x "$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets" ]]; then
|
||||
echo ""
|
||||
echo "[mosaic-init] Updating runtime adapters..."
|
||||
"$MOSAIC_HOME/tools/_scripts/mosaic-link-runtime-assets"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "[mosaic-init] Done. Launch with: mosaic claude"
|
||||
echo "[mosaic-init] Edit USER.md and TOOLS.md directly for further customization."
|
||||
144
packages/mosaic/framework/tools/_scripts/mosaic-init.ps1
Normal file
144
packages/mosaic/framework/tools/_scripts/mosaic-init.ps1
Normal file
@@ -0,0 +1,144 @@
|
||||
# mosaic-init.ps1 — Interactive SOUL.md generator (Windows)
|
||||
#
|
||||
# Usage:
|
||||
# mosaic-init.ps1 # Interactive mode
|
||||
# mosaic-init.ps1 -Name "Jarvis" -Style direct # Flag overrides
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
param(
|
||||
[string]$Name,
|
||||
[string]$Role,
|
||||
[ValidateSet("direct", "friendly", "formal")]
|
||||
[string]$Style,
|
||||
[string]$Accessibility,
|
||||
[string]$Guardrails,
|
||||
[switch]$NonInteractive,
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:USERPROFILE ".config\mosaic" }
|
||||
$Template = Join-Path $MosaicHome "templates\SOUL.md.template"
|
||||
$Output = Join-Path $MosaicHome "SOUL.md"
|
||||
|
||||
if ($Help) {
|
||||
Write-Host @"
|
||||
Usage: mosaic-init.ps1 [-Name <name>] [-Role <desc>] [-Style direct|friendly|formal]
|
||||
[-Accessibility <prefs>] [-Guardrails <rules>] [-NonInteractive]
|
||||
|
||||
Generate ~/.config/mosaic/SOUL.md - the universal agent identity contract.
|
||||
Interactive by default. Use flags to skip prompts.
|
||||
"@
|
||||
exit 0
|
||||
}
|
||||
|
||||
function Prompt-IfEmpty {
|
||||
param([string]$Current, [string]$PromptText, [string]$Default = "")
|
||||
|
||||
if ($Current) { return $Current }
|
||||
|
||||
if ($NonInteractive) {
|
||||
if ($Default) { return $Default }
|
||||
Write-Host "[mosaic-init] ERROR: Value required in non-interactive mode: $PromptText" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$display = if ($Default) { "$PromptText [$Default]" } else { $PromptText }
|
||||
$value = Read-Host $display
|
||||
if (-not $value -and $Default) { return $Default }
|
||||
return $value
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-init] Generating SOUL.md - your universal agent identity contract"
|
||||
Write-Host ""
|
||||
|
||||
$Name = Prompt-IfEmpty $Name "What name should agents use" "Assistant"
|
||||
$Role = Prompt-IfEmpty $Role "Agent role description" "execution partner and visibility engine"
|
||||
|
||||
if (-not $Style) {
|
||||
if ($NonInteractive) {
|
||||
$Style = "direct"
|
||||
}
|
||||
else {
|
||||
Write-Host ""
|
||||
Write-Host "Communication style:"
|
||||
Write-Host " 1) direct - Concise, no fluff, actionable"
|
||||
Write-Host " 2) friendly - Warm but efficient, conversational"
|
||||
Write-Host " 3) formal - Professional, structured, thorough"
|
||||
$choice = Read-Host "Choose [1/2/3] (default: 1)"
|
||||
$Style = switch ($choice) {
|
||||
"2" { "friendly" }
|
||||
"3" { "formal" }
|
||||
default { "direct" }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$Accessibility = Prompt-IfEmpty $Accessibility "Accessibility preferences (or 'none')" "none"
|
||||
|
||||
if (-not $Guardrails -and -not $NonInteractive) {
|
||||
Write-Host ""
|
||||
$Guardrails = Read-Host "Custom guardrails (optional, press Enter to skip)"
|
||||
}
|
||||
|
||||
# Build behavioral principles
|
||||
$BehavioralPrinciples = switch ($Style) {
|
||||
"direct" {
|
||||
"1. Clarity over performance theater.`n2. Practical execution over abstract planning.`n3. Truthfulness over confidence: state uncertainty explicitly.`n4. Visible state over hidden assumptions."
|
||||
}
|
||||
"friendly" {
|
||||
"1. Be helpful and approachable while staying efficient.`n2. Provide context and explain reasoning when helpful.`n3. Truthfulness over confidence: state uncertainty explicitly.`n4. Visible state over hidden assumptions."
|
||||
}
|
||||
"formal" {
|
||||
"1. Maintain professional, structured communication.`n2. Provide thorough analysis with explicit tradeoffs.`n3. Truthfulness over confidence: state uncertainty explicitly.`n4. Document decisions and rationale clearly."
|
||||
}
|
||||
}
|
||||
|
||||
if ($Accessibility -and $Accessibility -ne "none") {
|
||||
$BehavioralPrinciples += "`n5. $Accessibility."
|
||||
}
|
||||
|
||||
# Build communication style
|
||||
$CommunicationStyle = switch ($Style) {
|
||||
"direct" {
|
||||
"- Be direct, concise, and concrete.`n- Avoid fluff, hype, and anthropomorphic roleplay.`n- Do not simulate certainty when facts are missing.`n- Prefer actionable next steps and explicit tradeoffs."
|
||||
}
|
||||
"friendly" {
|
||||
"- Be warm and conversational while staying focused.`n- Explain your reasoning when it helps the user.`n- Do not simulate certainty when facts are missing.`n- Prefer actionable next steps with clear context."
|
||||
}
|
||||
"formal" {
|
||||
"- Use professional, structured language.`n- Provide thorough explanations with supporting detail.`n- Do not simulate certainty when facts are missing.`n- Present options with explicit tradeoffs and recommendations."
|
||||
}
|
||||
}
|
||||
|
||||
# Format custom guardrails
|
||||
$FormattedGuardrails = if ($Guardrails) { "- $Guardrails" } else { "" }
|
||||
|
||||
# Verify template
|
||||
if (-not (Test-Path $Template)) {
|
||||
Write-Host "[mosaic-init] ERROR: Template not found: $Template" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Generate SOUL.md
|
||||
$content = Get-Content $Template -Raw
|
||||
$content = $content -replace '\{\{AGENT_NAME\}\}', $Name
|
||||
$content = $content -replace '\{\{ROLE_DESCRIPTION\}\}', $Role
|
||||
$content = $content -replace '\{\{BEHAVIORAL_PRINCIPLES\}\}', $BehavioralPrinciples
|
||||
$content = $content -replace '\{\{COMMUNICATION_STYLE\}\}', $CommunicationStyle
|
||||
$content = $content -replace '\{\{CUSTOM_GUARDRAILS\}\}', $FormattedGuardrails
|
||||
|
||||
Set-Content -Path $Output -Value $content -Encoding UTF8
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "[mosaic-init] Generated: $Output"
|
||||
Write-Host "[mosaic-init] Agent name: $Name"
|
||||
Write-Host "[mosaic-init] Style: $Style"
|
||||
|
||||
# Push to runtime adapters
|
||||
$linkScript = Join-Path $MosaicHome "bin\mosaic-link-runtime-assets.ps1"
|
||||
if (Test-Path $linkScript) {
|
||||
Write-Host "[mosaic-init] Updating runtime adapters..."
|
||||
& $linkScript
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-init] Done. Launch with: mosaic claude"
|
||||
136
packages/mosaic/framework/tools/_scripts/mosaic-link-runtime-assets
Executable file
136
packages/mosaic/framework/tools/_scripts/mosaic-link-runtime-assets
Executable file
@@ -0,0 +1,136 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
backup_stamp="$(date +%Y%m%d%H%M%S)"
|
||||
|
||||
copy_file_managed() {
|
||||
local src="$1"
|
||||
local dst="$2"
|
||||
|
||||
mkdir -p "$(dirname "$dst")"
|
||||
|
||||
if [[ -L "$dst" ]]; then
|
||||
rm -f "$dst"
|
||||
fi
|
||||
|
||||
if [[ -f "$dst" ]]; then
|
||||
if cmp -s "$src" "$dst"; then
|
||||
return
|
||||
fi
|
||||
mv "$dst" "${dst}.mosaic-bak-${backup_stamp}"
|
||||
fi
|
||||
|
||||
cp "$src" "$dst"
|
||||
}
|
||||
|
||||
remove_legacy_path() {
|
||||
local p="$1"
|
||||
|
||||
if [[ -L "$p" ]]; then
|
||||
rm -f "$p"
|
||||
return
|
||||
fi
|
||||
|
||||
if [[ -d "$p" ]]; then
|
||||
find "$p" -depth -type l -delete 2>/dev/null || true
|
||||
find "$p" -depth -type d -empty -delete 2>/dev/null || true
|
||||
return
|
||||
fi
|
||||
|
||||
# Remove stale symlinked files if present.
|
||||
if [[ -e "$p" && -L "$p" ]]; then
|
||||
rm -f "$p"
|
||||
fi
|
||||
}
|
||||
|
||||
# Remove compatibility symlink surfaces for migrated content.
|
||||
legacy_paths=(
|
||||
"$HOME/.claude/agent-guides"
|
||||
"$HOME/.claude/scripts/git"
|
||||
"$HOME/.claude/scripts/codex"
|
||||
"$HOME/.claude/scripts/bootstrap"
|
||||
"$HOME/.claude/scripts/cicd"
|
||||
"$HOME/.claude/scripts/portainer"
|
||||
"$HOME/.claude/scripts/debug-hook.sh"
|
||||
"$HOME/.claude/scripts/qa-hook-handler.sh"
|
||||
"$HOME/.claude/scripts/qa-hook-stdin.sh"
|
||||
"$HOME/.claude/scripts/qa-hook-wrapper.sh"
|
||||
"$HOME/.claude/scripts/qa-queue-monitor.sh"
|
||||
"$HOME/.claude/scripts/remediation-hook-handler.sh"
|
||||
"$HOME/.claude/templates"
|
||||
"$HOME/.claude/presets/domains"
|
||||
"$HOME/.claude/presets/tech-stacks"
|
||||
"$HOME/.claude/presets/workflows"
|
||||
"$HOME/.claude/presets/jarvis-loop.json"
|
||||
)
|
||||
|
||||
for p in "${legacy_paths[@]}"; do
|
||||
remove_legacy_path "$p"
|
||||
done
|
||||
|
||||
# Claude-specific runtime files (settings, hooks — NOT CLAUDE.md which is now a thin pointer)
|
||||
for runtime_file in \
|
||||
CLAUDE.md \
|
||||
settings.json \
|
||||
hooks-config.json \
|
||||
context7-integration.md; do
|
||||
src="$MOSAIC_HOME/runtime/claude/$runtime_file"
|
||||
[[ -f "$src" ]] || continue
|
||||
copy_file_managed "$src" "$HOME/.claude/$runtime_file"
|
||||
done
|
||||
|
||||
# OpenCode runtime adapter (thin pointer to AGENTS.md)
|
||||
opencode_adapter="$MOSAIC_HOME/runtime/opencode/AGENTS.md"
|
||||
if [[ -f "$opencode_adapter" ]]; then
|
||||
copy_file_managed "$opencode_adapter" "$HOME/.config/opencode/AGENTS.md"
|
||||
fi
|
||||
|
||||
# Codex runtime adapter (thin pointer to AGENTS.md)
|
||||
codex_adapter="$MOSAIC_HOME/runtime/codex/instructions.md"
|
||||
if [[ -f "$codex_adapter" ]]; then
|
||||
mkdir -p "$HOME/.codex"
|
||||
copy_file_managed "$codex_adapter" "$HOME/.codex/instructions.md"
|
||||
fi
|
||||
|
||||
# Pi runtime settings (MCP + skills paths)
|
||||
pi_settings_dir="$HOME/.pi/agent"
|
||||
pi_settings_file="$pi_settings_dir/settings.json"
|
||||
mkdir -p "$pi_settings_dir"
|
||||
|
||||
if [[ ! -f "$pi_settings_file" ]]; then
|
||||
echo '{}' > "$pi_settings_file"
|
||||
fi
|
||||
|
||||
# Ensure Pi settings.json has Mosaic skills paths
|
||||
mosaic_skills_path="$MOSAIC_HOME/skills"
|
||||
mosaic_local_path="$MOSAIC_HOME/skills-local"
|
||||
if ! grep -q "$mosaic_skills_path" "$pi_settings_file" 2>/dev/null; then
|
||||
if command -v python3 >/dev/null 2>&1; then
|
||||
python3 -c "
|
||||
import json
|
||||
with open('$pi_settings_file', 'r') as f:
|
||||
data = json.load(f)
|
||||
skills = data.get('skills', [])
|
||||
if not isinstance(skills, list):
|
||||
skills = []
|
||||
for p in ['$mosaic_skills_path', '$mosaic_local_path']:
|
||||
if p not in skills:
|
||||
skills.append(p)
|
||||
data['skills'] = skills
|
||||
with open('$pi_settings_file', 'w') as f:
|
||||
json.dump(data, f, indent=2)
|
||||
f.write('\\n')
|
||||
" 2>/dev/null
|
||||
fi
|
||||
fi
|
||||
|
||||
# Pi extension is loaded via --extension flag in the mosaic launcher.
|
||||
# Do NOT copy into ~/.pi/agent/extensions/ — that causes duplicate loading.
|
||||
|
||||
if [[ -x "$MOSAIC_HOME/tools/_scripts/mosaic-ensure-sequential-thinking" ]]; then
|
||||
"$MOSAIC_HOME/tools/_scripts/mosaic-ensure-sequential-thinking"
|
||||
fi
|
||||
|
||||
echo "[mosaic-link] Runtime assets synced (non-symlink mode)"
|
||||
echo "[mosaic-link] Canonical source: $MOSAIC_HOME"
|
||||
@@ -0,0 +1,111 @@
|
||||
# mosaic-link-runtime-assets.ps1
|
||||
# Syncs Mosaic runtime config files into agent runtime directories.
|
||||
# PowerShell equivalent of mosaic-link-runtime-assets (bash).
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:USERPROFILE ".config\mosaic" }
|
||||
$BackupStamp = Get-Date -Format "yyyyMMddHHmmss"
|
||||
|
||||
function Copy-FileManaged {
|
||||
param([string]$Src, [string]$Dst)
|
||||
|
||||
$parent = Split-Path $Dst -Parent
|
||||
if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null }
|
||||
|
||||
# Remove existing symlink/junction
|
||||
$item = Get-Item $Dst -Force -ErrorAction SilentlyContinue
|
||||
if ($item -and $item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) {
|
||||
Remove-Item $Dst -Force
|
||||
}
|
||||
|
||||
if (Test-Path $Dst) {
|
||||
$srcHash = (Get-FileHash $Src -Algorithm SHA256).Hash
|
||||
$dstHash = (Get-FileHash $Dst -Algorithm SHA256).Hash
|
||||
if ($srcHash -eq $dstHash) { return }
|
||||
Rename-Item $Dst "$Dst.mosaic-bak-$BackupStamp"
|
||||
}
|
||||
|
||||
Copy-Item $Src $Dst -Force
|
||||
}
|
||||
|
||||
function Remove-LegacyPath {
|
||||
param([string]$Path)
|
||||
|
||||
if (-not (Test-Path $Path)) { return }
|
||||
|
||||
$item = Get-Item $Path -Force -ErrorAction SilentlyContinue
|
||||
if ($item -and $item.Attributes -band [System.IO.FileAttributes]::ReparsePoint) {
|
||||
Remove-Item $Path -Force
|
||||
return
|
||||
}
|
||||
|
||||
if (Test-Path $Path -PathType Container) {
|
||||
# Remove symlinks/junctions inside, then empty dirs
|
||||
Get-ChildItem $Path -Recurse -Force | Where-Object {
|
||||
$_.Attributes -band [System.IO.FileAttributes]::ReparsePoint
|
||||
} | Remove-Item -Force
|
||||
|
||||
Get-ChildItem $Path -Recurse -Directory -Force |
|
||||
Sort-Object { $_.FullName.Length } -Descending |
|
||||
Where-Object { (Get-ChildItem $_.FullName -Force | Measure-Object).Count -eq 0 } |
|
||||
Remove-Item -Force
|
||||
}
|
||||
}
|
||||
|
||||
# Remove legacy compatibility paths
|
||||
$legacyPaths = @(
|
||||
(Join-Path $env:USERPROFILE ".claude\agent-guides"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\git"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\codex"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\bootstrap"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\cicd"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\portainer"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\debug-hook.sh"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\qa-hook-handler.sh"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\qa-hook-stdin.sh"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\qa-hook-wrapper.sh"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\qa-queue-monitor.sh"),
|
||||
(Join-Path $env:USERPROFILE ".claude\scripts\remediation-hook-handler.sh"),
|
||||
(Join-Path $env:USERPROFILE ".claude\templates"),
|
||||
(Join-Path $env:USERPROFILE ".claude\presets\domains"),
|
||||
(Join-Path $env:USERPROFILE ".claude\presets\tech-stacks"),
|
||||
(Join-Path $env:USERPROFILE ".claude\presets\workflows"),
|
||||
(Join-Path $env:USERPROFILE ".claude\presets\jarvis-loop.json")
|
||||
)
|
||||
|
||||
foreach ($p in $legacyPaths) {
|
||||
Remove-LegacyPath $p
|
||||
}
|
||||
|
||||
# Claude-specific runtime files (settings, hooks — CLAUDE.md is now a thin pointer)
|
||||
$runtimeFiles = @("CLAUDE.md", "settings.json", "hooks-config.json", "context7-integration.md")
|
||||
foreach ($rf in $runtimeFiles) {
|
||||
$src = Join-Path $MosaicHome "runtime\claude\$rf"
|
||||
if (-not (Test-Path $src)) { continue }
|
||||
$dst = Join-Path $env:USERPROFILE ".claude\$rf"
|
||||
Copy-FileManaged $src $dst
|
||||
}
|
||||
|
||||
# OpenCode runtime adapter
|
||||
$opencodeSrc = Join-Path $MosaicHome "runtime\opencode\AGENTS.md"
|
||||
if (Test-Path $opencodeSrc) {
|
||||
$opencodeDst = Join-Path $env:USERPROFILE ".config\opencode\AGENTS.md"
|
||||
Copy-FileManaged $opencodeSrc $opencodeDst
|
||||
}
|
||||
|
||||
# Codex runtime adapter
|
||||
$codexSrc = Join-Path $MosaicHome "runtime\codex\instructions.md"
|
||||
if (Test-Path $codexSrc) {
|
||||
$codexDir = Join-Path $env:USERPROFILE ".codex"
|
||||
if (-not (Test-Path $codexDir)) { New-Item -ItemType Directory -Path $codexDir -Force | Out-Null }
|
||||
$codexDst = Join-Path $codexDir "instructions.md"
|
||||
Copy-FileManaged $codexSrc $codexDst
|
||||
}
|
||||
|
||||
$seqScript = Join-Path $MosaicHome "bin\mosaic-ensure-sequential-thinking.ps1"
|
||||
if (Test-Path $seqScript) {
|
||||
& $seqScript
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-link] Runtime assets synced (non-symlink mode)"
|
||||
Write-Host "[mosaic-link] Canonical source: $MosaicHome"
|
||||
9
packages/mosaic/framework/tools/_scripts/mosaic-log-limitation
Executable file
9
packages/mosaic/framework/tools/_scripts/mosaic-log-limitation
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -x "scripts/agent/log-limitation.sh" ]]; then
|
||||
exec bash scripts/agent/log-limitation.sh "$@"
|
||||
fi
|
||||
|
||||
echo "[mosaic] Missing scripts/agent/log-limitation.sh in $(pwd)" >&2
|
||||
exit 1
|
||||
88
packages/mosaic/framework/tools/_scripts/mosaic-migrate-local-skills
Executable file
88
packages/mosaic/framework/tools/_scripts/mosaic-migrate-local-skills
Executable file
@@ -0,0 +1,88 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
APPLY=0
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: $(basename "$0") [--apply]
|
||||
|
||||
Migrate runtime-local skill directories (e.g. ~/.claude/skills/jarvis) to Mosaic-managed
|
||||
skills by replacing local directories with symlinks to ~/.config/mosaic/skills-local.
|
||||
|
||||
Default mode is dry-run.
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--apply)
|
||||
APPLY=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
skill_roots=(
|
||||
"$HOME/.claude/skills"
|
||||
"$HOME/.codex/skills"
|
||||
"$HOME/.config/opencode/skills"
|
||||
"$HOME/.pi/agent/skills"
|
||||
)
|
||||
|
||||
if [[ ! -d "$MOSAIC_HOME/skills-local" ]]; then
|
||||
echo "[mosaic-local-skills] Missing local skills dir: $MOSAIC_HOME/skills-local" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
count=0
|
||||
|
||||
while IFS= read -r -d '' local_skill; do
|
||||
name="$(basename "$local_skill")"
|
||||
src="$MOSAIC_HOME/skills-local/$name"
|
||||
[[ -d "$src" ]] || continue
|
||||
|
||||
for root in "${skill_roots[@]}"; do
|
||||
[[ -d "$root" ]] || continue
|
||||
target="$root/$name"
|
||||
|
||||
# Already linked correctly.
|
||||
if [[ -L "$target" ]]; then
|
||||
target_real="$(readlink -f "$target" 2>/dev/null || true)"
|
||||
src_real="$(readlink -f "$src" 2>/dev/null || true)"
|
||||
if [[ -n "$target_real" && -n "$src_real" && "$target_real" == "$src_real" ]]; then
|
||||
continue
|
||||
fi
|
||||
fi
|
||||
|
||||
# Only migrate local directories containing SKILL.md
|
||||
if [[ -d "$target" && -f "$target/SKILL.md" && ! -L "$target" ]]; then
|
||||
count=$((count + 1))
|
||||
if [[ $APPLY -eq 1 ]]; then
|
||||
stamp="$(date +%Y%m%d%H%M%S)"
|
||||
mv "$target" "${target}.mosaic-bak-${stamp}"
|
||||
ln -s "$src" "$target"
|
||||
echo "[mosaic-local-skills] migrated: $target -> $src"
|
||||
else
|
||||
echo "[mosaic-local-skills] would migrate: $target -> $src"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
done < <(find "$MOSAIC_HOME/skills-local" -mindepth 1 -maxdepth 1 \( -type d -o -type l \) -print0)
|
||||
|
||||
if [[ $APPLY -eq 1 ]]; then
|
||||
echo "[mosaic-local-skills] complete: migrated=$count"
|
||||
else
|
||||
echo "[mosaic-local-skills] dry-run: migratable=$count"
|
||||
echo "[mosaic-local-skills] re-run with --apply to migrate"
|
||||
fi
|
||||
@@ -0,0 +1,90 @@
|
||||
# mosaic-migrate-local-skills.ps1
|
||||
# Migrates runtime-local skill directories to Mosaic-managed junctions.
|
||||
# Uses directory junctions (no elevation required) with fallback to copies.
|
||||
# PowerShell equivalent of mosaic-migrate-local-skills (bash).
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
param(
|
||||
[switch]$Apply,
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:USERPROFILE ".config\mosaic" }
|
||||
$LocalSkillsDir = Join-Path $MosaicHome "skills-local"
|
||||
|
||||
if ($Help) {
|
||||
Write-Host @"
|
||||
Usage: mosaic-migrate-local-skills.ps1 [-Apply] [-Help]
|
||||
|
||||
Migrate runtime-local skill directories (e.g. ~/.claude/skills/jarvis) to
|
||||
Mosaic-managed skills by replacing local directories with junctions to
|
||||
~/.config/mosaic/skills-local.
|
||||
|
||||
Default mode is dry-run.
|
||||
"@
|
||||
exit 0
|
||||
}
|
||||
|
||||
if (-not (Test-Path $LocalSkillsDir)) {
|
||||
Write-Host "[mosaic-local-skills] Missing local skills dir: $LocalSkillsDir" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$skillRoots = @(
|
||||
(Join-Path $env:USERPROFILE ".claude\skills"),
|
||||
(Join-Path $env:USERPROFILE ".codex\skills"),
|
||||
(Join-Path $env:USERPROFILE ".config\opencode\skills")
|
||||
)
|
||||
|
||||
$count = 0
|
||||
|
||||
Get-ChildItem $LocalSkillsDir -Directory | ForEach-Object {
|
||||
$name = $_.Name
|
||||
$src = $_.FullName
|
||||
|
||||
foreach ($root in $skillRoots) {
|
||||
if (-not (Test-Path $root)) { continue }
|
||||
$target = Join-Path $root $name
|
||||
|
||||
# Already a junction/symlink — check if it points to the right place
|
||||
$existing = Get-Item $target -Force -ErrorAction SilentlyContinue
|
||||
if ($existing -and ($existing.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
|
||||
$currentTarget = $existing.Target
|
||||
if ($currentTarget -and ($currentTarget -eq $src -or (Resolve-Path $currentTarget -ErrorAction SilentlyContinue).Path -eq (Resolve-Path $src -ErrorAction SilentlyContinue).Path)) {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
# Only migrate local directories containing SKILL.md
|
||||
if ((Test-Path $target -PathType Container) -and
|
||||
(Test-Path (Join-Path $target "SKILL.md")) -and
|
||||
-not ($existing -and ($existing.Attributes -band [System.IO.FileAttributes]::ReparsePoint))) {
|
||||
|
||||
$count++
|
||||
if ($Apply) {
|
||||
$stamp = Get-Date -Format "yyyyMMddHHmmss"
|
||||
Rename-Item $target "$target.mosaic-bak-$stamp"
|
||||
try {
|
||||
New-Item -ItemType Junction -Path $target -Target $src -ErrorAction Stop | Out-Null
|
||||
Write-Host "[mosaic-local-skills] migrated: $target -> $src"
|
||||
}
|
||||
catch {
|
||||
Write-Host "[mosaic-local-skills] Junction failed for $name, falling back to copy"
|
||||
Copy-Item $src $target -Recurse -Force
|
||||
Write-Host "[mosaic-local-skills] copied: $target <- $src"
|
||||
}
|
||||
}
|
||||
else {
|
||||
Write-Host "[mosaic-local-skills] would migrate: $target -> $src"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ($Apply) {
|
||||
Write-Host "[mosaic-local-skills] complete: migrated=$count"
|
||||
}
|
||||
else {
|
||||
Write-Host "[mosaic-local-skills] dry-run: migratable=$count"
|
||||
Write-Host "[mosaic-local-skills] re-run with -Apply to migrate"
|
||||
}
|
||||
33
packages/mosaic/framework/tools/_scripts/mosaic-orchestrator-drain
Executable file
33
packages/mosaic/framework/tools/_scripts/mosaic-orchestrator-drain
Executable file
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
sync_cmd="$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-sync-tasks"
|
||||
run_cmd="$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-run"
|
||||
|
||||
do_sync=1
|
||||
poll_sec=15
|
||||
extra_args=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--no-sync)
|
||||
do_sync=0
|
||||
shift
|
||||
;;
|
||||
--poll-sec)
|
||||
poll_sec="${2:-15}"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
extra_args+=("$1")
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ $do_sync -eq 1 ]]; then
|
||||
"$sync_cmd" --apply
|
||||
fi
|
||||
|
||||
exec "$run_cmd" --until-drained --poll-sec "$poll_sec" "${extra_args[@]}"
|
||||
12
packages/mosaic/framework/tools/_scripts/mosaic-orchestrator-matrix-consume
Executable file
12
packages/mosaic/framework/tools/_scripts/mosaic-orchestrator-matrix-consume
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
BRIDGE="$MOSAIC_HOME/tools/orchestrator-matrix/transport/matrix_transport.py"
|
||||
|
||||
if [[ ! -f "$BRIDGE" ]]; then
|
||||
echo "[mosaic-orch-matrix] missing transport bridge: $BRIDGE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec python3 "$BRIDGE" --repo "$(pwd)" --mode consume "$@"
|
||||
19
packages/mosaic/framework/tools/_scripts/mosaic-orchestrator-matrix-cycle
Executable file
19
packages/mosaic/framework/tools/_scripts/mosaic-orchestrator-matrix-cycle
Executable file
@@ -0,0 +1,19 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
|
||||
consume="$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-matrix-consume"
|
||||
run="$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-run"
|
||||
publish="$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-matrix-publish"
|
||||
|
||||
for cmd in "$consume" "$run" "$publish"; do
|
||||
if [[ ! -x "$cmd" ]]; then
|
||||
echo "[mosaic-orch-cycle] missing executable: $cmd" >&2
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
"$consume"
|
||||
"$run" --once "$@"
|
||||
"$publish"
|
||||
12
packages/mosaic/framework/tools/_scripts/mosaic-orchestrator-matrix-publish
Executable file
12
packages/mosaic/framework/tools/_scripts/mosaic-orchestrator-matrix-publish
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
BRIDGE="$MOSAIC_HOME/tools/orchestrator-matrix/transport/matrix_transport.py"
|
||||
|
||||
if [[ ! -f "$BRIDGE" ]]; then
|
||||
echo "[mosaic-orch-matrix] missing transport bridge: $BRIDGE" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec python3 "$BRIDGE" --repo "$(pwd)" --mode publish "$@"
|
||||
12
packages/mosaic/framework/tools/_scripts/mosaic-orchestrator-run
Executable file
12
packages/mosaic/framework/tools/_scripts/mosaic-orchestrator-run
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
CTRL="$MOSAIC_HOME/tools/orchestrator-matrix/controller/mosaic_orchestrator.py"
|
||||
|
||||
if [[ ! -f "$CTRL" ]]; then
|
||||
echo "[mosaic-orchestrator] missing controller: $CTRL" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec python3 "$CTRL" --repo "$(pwd)" "$@"
|
||||
12
packages/mosaic/framework/tools/_scripts/mosaic-orchestrator-sync-tasks
Executable file
12
packages/mosaic/framework/tools/_scripts/mosaic-orchestrator-sync-tasks
Executable file
@@ -0,0 +1,12 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
SYNC="$MOSAIC_HOME/tools/orchestrator-matrix/controller/tasks_md_sync.py"
|
||||
|
||||
if [[ ! -f "$SYNC" ]]; then
|
||||
echo "[mosaic-orchestrator-sync] missing sync script: $SYNC" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec python3 "$SYNC" --repo "$(pwd)" "$@"
|
||||
218
packages/mosaic/framework/tools/_scripts/mosaic-projects
Executable file
218
packages/mosaic/framework/tools/_scripts/mosaic-projects
Executable file
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
PROJECTS_FILE="$MOSAIC_HOME/projects.txt"
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: $(basename "$0") <command> [options]
|
||||
|
||||
Commands:
|
||||
init
|
||||
Create projects registry file at ~/.config/mosaic/projects.txt
|
||||
|
||||
add <repo-path> [repo-path...]
|
||||
Add one or more repos to the registry
|
||||
|
||||
remove <repo-path> [repo-path...]
|
||||
Remove one or more repos from the registry
|
||||
|
||||
list
|
||||
Show registered repos
|
||||
|
||||
bootstrap [--all|repo-path...] [--force] [--quality-template <name>]
|
||||
Bootstrap registered repos or explicit repo paths
|
||||
|
||||
orchestrate <drain|start|status|stop> [--all|repo-path...] [--poll-sec N] [--no-sync] [--worker-cmd "cmd"]
|
||||
Run orchestrator actions across repos from one command
|
||||
|
||||
Examples:
|
||||
mosaic-projects init
|
||||
mosaic-projects add ~/src/syncagent ~/src/inventory-stickers
|
||||
mosaic-projects bootstrap --all
|
||||
mosaic-projects orchestrate drain --all --worker-cmd "codex -p"
|
||||
mosaic-projects orchestrate start ~/src/syncagent --worker-cmd "opencode -p"
|
||||
USAGE
|
||||
}
|
||||
|
||||
ensure_registry() {
|
||||
mkdir -p "$MOSAIC_HOME"
|
||||
if [[ ! -f "$PROJECTS_FILE" ]]; then
|
||||
cat > "$PROJECTS_FILE" <<EOF
|
||||
# Mosaic managed projects (one absolute path per line)
|
||||
# Lines starting with # are ignored.
|
||||
EOF
|
||||
fi
|
||||
}
|
||||
|
||||
norm_path() {
|
||||
local p="$1"
|
||||
if [[ -d "$p" ]]; then
|
||||
(cd "$p" && pwd)
|
||||
else
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
read_registry() {
|
||||
ensure_registry
|
||||
grep -vE '^\s*#|^\s*$' "$PROJECTS_FILE" | while read -r p; do
|
||||
[[ -d "$p" ]] && echo "$p"
|
||||
done
|
||||
}
|
||||
|
||||
add_repo() {
|
||||
local p="$1"
|
||||
ensure_registry
|
||||
local np
|
||||
np="$(norm_path "$p")" || { echo "[mosaic-projects] skip missing dir: $p" >&2; return 1; }
|
||||
if grep -Fxq "$np" "$PROJECTS_FILE"; then
|
||||
echo "[mosaic-projects] already registered: $np"
|
||||
return 0
|
||||
fi
|
||||
echo "$np" >> "$PROJECTS_FILE"
|
||||
echo "[mosaic-projects] added: $np"
|
||||
}
|
||||
|
||||
remove_repo() {
|
||||
local p="$1"
|
||||
ensure_registry
|
||||
local np
|
||||
np="$(norm_path "$p" 2>/dev/null || echo "$p")"
|
||||
tmp="$(mktemp)"
|
||||
grep -vFx "$np" "$PROJECTS_FILE" > "$tmp" || true
|
||||
mv "$tmp" "$PROJECTS_FILE"
|
||||
echo "[mosaic-projects] removed: $np"
|
||||
}
|
||||
|
||||
resolve_targets() {
|
||||
local use_all="$1"
|
||||
shift
|
||||
if [[ "$use_all" == "1" ]]; then
|
||||
read_registry
|
||||
return 0
|
||||
fi
|
||||
if [[ $# -gt 0 ]]; then
|
||||
for p in "$@"; do
|
||||
norm_path "$p" || { echo "[mosaic-projects] missing target: $p" >&2; exit 1; }
|
||||
done
|
||||
return 0
|
||||
fi
|
||||
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
git rev-parse --show-toplevel
|
||||
return 0
|
||||
fi
|
||||
echo "[mosaic-projects] no targets provided. Use --all or pass repo paths." >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
cmd="${1:-}"
|
||||
if [[ -z "$cmd" ]]; then
|
||||
usage
|
||||
exit 1
|
||||
fi
|
||||
shift || true
|
||||
|
||||
case "$cmd" in
|
||||
init)
|
||||
ensure_registry
|
||||
echo "[mosaic-projects] registry ready: $PROJECTS_FILE"
|
||||
;;
|
||||
add)
|
||||
[[ $# -gt 0 ]] || { echo "[mosaic-projects] add requires repo path(s)" >&2; exit 1; }
|
||||
for p in "$@"; do add_repo "$p"; done
|
||||
;;
|
||||
remove)
|
||||
[[ $# -gt 0 ]] || { echo "[mosaic-projects] remove requires repo path(s)" >&2; exit 1; }
|
||||
for p in "$@"; do remove_repo "$p"; done
|
||||
;;
|
||||
list)
|
||||
read_registry
|
||||
;;
|
||||
bootstrap)
|
||||
use_all=0
|
||||
force=0
|
||||
quality_template=""
|
||||
targets=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--all) use_all=1; shift ;;
|
||||
--force) force=1; shift ;;
|
||||
--quality-template) quality_template="${2:-}"; shift 2 ;;
|
||||
*) targets+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
mapfile -t repos < <(resolve_targets "$use_all" "${targets[@]}")
|
||||
[[ ${#repos[@]} -gt 0 ]] || { echo "[mosaic-projects] no repos resolved"; exit 1; }
|
||||
for repo in "${repos[@]}"; do
|
||||
args=()
|
||||
[[ $force -eq 1 ]] && args+=(--force)
|
||||
[[ -n "$quality_template" ]] && args+=(--quality-template "$quality_template")
|
||||
args+=("$repo")
|
||||
echo "[mosaic-projects] bootstrap: $repo"
|
||||
"$MOSAIC_HOME/tools/_scripts/mosaic-bootstrap-repo" "${args[@]}"
|
||||
add_repo "$repo" || true
|
||||
done
|
||||
;;
|
||||
orchestrate)
|
||||
action="${1:-}"
|
||||
[[ -n "$action" ]] || { echo "[mosaic-projects] orchestrate requires action: drain|start|status|stop" >&2; exit 1; }
|
||||
shift || true
|
||||
use_all=0
|
||||
poll_sec=15
|
||||
no_sync=0
|
||||
worker_cmd=""
|
||||
targets=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--all) use_all=1; shift ;;
|
||||
--poll-sec) poll_sec="${2:-15}"; shift 2 ;;
|
||||
--no-sync) no_sync=1; shift ;;
|
||||
--worker-cmd) worker_cmd="${2:-}"; shift 2 ;;
|
||||
*) targets+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
mapfile -t repos < <(resolve_targets "$use_all" "${targets[@]}")
|
||||
[[ ${#repos[@]} -gt 0 ]] || { echo "[mosaic-projects] no repos resolved"; exit 1; }
|
||||
|
||||
for repo in "${repos[@]}"; do
|
||||
echo "[mosaic-projects] orchestrate:$action -> $repo"
|
||||
(
|
||||
cd "$repo"
|
||||
if [[ -n "$worker_cmd" ]]; then
|
||||
export MOSAIC_WORKER_EXEC="$worker_cmd"
|
||||
fi
|
||||
if [[ -x "scripts/agent/orchestrator-daemon.sh" ]]; then
|
||||
args=()
|
||||
[[ "$action" == "start" || "$action" == "drain" ]] && args+=(--poll-sec "$poll_sec")
|
||||
[[ $no_sync -eq 1 ]] && args+=(--no-sync)
|
||||
bash scripts/agent/orchestrator-daemon.sh "$action" "${args[@]}"
|
||||
else
|
||||
case "$action" in
|
||||
drain)
|
||||
args=(--poll-sec "$poll_sec")
|
||||
[[ $no_sync -eq 1 ]] && args+=(--no-sync)
|
||||
"$MOSAIC_HOME/tools/_scripts/mosaic-orchestrator-drain" "${args[@]}"
|
||||
;;
|
||||
status)
|
||||
echo "[mosaic-projects] no daemon script in repo; run from bootstrapped repo or re-bootstrap"
|
||||
;;
|
||||
start|stop)
|
||||
echo "[mosaic-projects] action '$action' requires scripts/agent/orchestrator-daemon.sh (run bootstrap first)" >&2
|
||||
exit 1
|
||||
;;
|
||||
*)
|
||||
echo "[mosaic-projects] unsupported action: $action" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
)
|
||||
done
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
95
packages/mosaic/framework/tools/_scripts/mosaic-prune-legacy-runtime
Executable file
95
packages/mosaic/framework/tools/_scripts/mosaic-prune-legacy-runtime
Executable file
@@ -0,0 +1,95 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
RUNTIME="claude"
|
||||
APPLY=0
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: $(basename "$0") [options]
|
||||
|
||||
Remove legacy runtime files that were preserved as *.mosaic-bak-* after Mosaic linking.
|
||||
Only removes backups when the active file is a symlink to ~/.config/mosaic.
|
||||
|
||||
Options:
|
||||
--runtime <name> Runtime to prune (default: claude)
|
||||
--apply Perform deletions (default: dry-run)
|
||||
-h, --help Show help
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--runtime)
|
||||
[[ $# -lt 2 ]] && { echo "Missing value for --runtime" >&2; exit 1; }
|
||||
RUNTIME="$2"
|
||||
shift 2
|
||||
;;
|
||||
--apply)
|
||||
APPLY=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$RUNTIME" in
|
||||
claude)
|
||||
TARGET_ROOT="$HOME/.claude"
|
||||
;;
|
||||
*)
|
||||
echo "Unsupported runtime: $RUNTIME" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
if [[ ! -d "$TARGET_ROOT" ]]; then
|
||||
echo "[mosaic-prune] Runtime directory not found: $TARGET_ROOT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mosaic_real="$(readlink -f "$MOSAIC_HOME")"
|
||||
count_candidates=0
|
||||
count_deletable=0
|
||||
|
||||
while IFS= read -r -d '' bak; do
|
||||
count_candidates=$((count_candidates + 1))
|
||||
|
||||
base="${bak%%.mosaic-bak-*}"
|
||||
if [[ ! -L "$base" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
base_real="$(readlink -f "$base" 2>/dev/null || true)"
|
||||
if [[ -z "$base_real" ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$base_real" != "$mosaic_real"/* ]]; then
|
||||
continue
|
||||
fi
|
||||
|
||||
count_deletable=$((count_deletable + 1))
|
||||
if [[ $APPLY -eq 1 ]]; then
|
||||
rm -rf "$bak"
|
||||
echo "[mosaic-prune] deleted: $bak"
|
||||
else
|
||||
echo "[mosaic-prune] would delete: $bak"
|
||||
fi
|
||||
done < <(find "$TARGET_ROOT" \( -type f -o -type d \) -name '*.mosaic-bak-*' -print0)
|
||||
|
||||
if [[ $APPLY -eq 1 ]]; then
|
||||
echo "[mosaic-prune] complete: deleted=$count_deletable candidates=$count_candidates runtime=$RUNTIME"
|
||||
else
|
||||
echo "[mosaic-prune] dry-run: deletable=$count_deletable candidates=$count_candidates runtime=$RUNTIME"
|
||||
echo "[mosaic-prune] re-run with --apply to delete"
|
||||
fi
|
||||
65
packages/mosaic/framework/tools/_scripts/mosaic-quality-apply
Executable file
65
packages/mosaic/framework/tools/_scripts/mosaic-quality-apply
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
TARGET_DIR="$(pwd)"
|
||||
TEMPLATE=""
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: $(basename "$0") --template <name> [--target <dir>]
|
||||
|
||||
Apply Mosaic quality tools templates into a project.
|
||||
|
||||
Templates:
|
||||
typescript-node
|
||||
typescript-nextjs
|
||||
monorepo
|
||||
|
||||
Examples:
|
||||
$(basename "$0") --template typescript-node --target ~/src/my-project
|
||||
$(basename "$0") --template monorepo
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--template)
|
||||
TEMPLATE="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
--target)
|
||||
TARGET_DIR="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$TEMPLATE" ]]; then
|
||||
echo "[mosaic-quality] Missing required --template" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -d "$TARGET_DIR" ]]; then
|
||||
echo "[mosaic-quality] Target directory does not exist: $TARGET_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT="$MOSAIC_HOME/tools/quality/scripts/install.sh"
|
||||
if [[ ! -x "$SCRIPT" ]]; then
|
||||
echo "[mosaic-quality] Missing install script: $SCRIPT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[mosaic-quality] Applying template '$TEMPLATE' to $TARGET_DIR"
|
||||
"$SCRIPT" --template "$TEMPLATE" --target "$TARGET_DIR"
|
||||
52
packages/mosaic/framework/tools/_scripts/mosaic-quality-verify
Executable file
52
packages/mosaic/framework/tools/_scripts/mosaic-quality-verify
Executable file
@@ -0,0 +1,52 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
TARGET_DIR="$(pwd)"
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: $(basename "$0") [--target <dir>]
|
||||
|
||||
Run quality-rails verification checks inside a target repository.
|
||||
|
||||
Examples:
|
||||
$(basename "$0")
|
||||
$(basename "$0") --target ~/src/my-project
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--target)
|
||||
TARGET_DIR="${2:-}"
|
||||
shift 2
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ! -d "$TARGET_DIR" ]]; then
|
||||
echo "[mosaic-quality] Target directory does not exist: $TARGET_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
SCRIPT="$MOSAIC_HOME/tools/quality/scripts/verify.sh"
|
||||
if [[ ! -x "$SCRIPT" ]]; then
|
||||
echo "[mosaic-quality] Missing verify script: $SCRIPT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "[mosaic-quality] Running verification in $TARGET_DIR"
|
||||
(
|
||||
cd "$TARGET_DIR"
|
||||
"$SCRIPT"
|
||||
)
|
||||
124
packages/mosaic/framework/tools/_scripts/mosaic-release-upgrade
Executable file
124
packages/mosaic/framework/tools/_scripts/mosaic-release-upgrade
Executable file
@@ -0,0 +1,124 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# mosaic-release-upgrade — Upgrade installed Mosaic framework release.
|
||||
#
|
||||
# This re-runs the remote installer with explicit install mode controls.
|
||||
# Default behavior is safe/idempotent (keep SOUL.md + memory).
|
||||
#
|
||||
# Usage:
|
||||
# mosaic-release-upgrade
|
||||
# mosaic-release-upgrade --ref main --keep
|
||||
# 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/mosaic-stack/raw/branch/main/tools/install.sh}"
|
||||
BOOTSTRAP_REF="${MOSAIC_BOOTSTRAP_REF:-main}"
|
||||
INSTALL_MODE="${MOSAIC_INSTALL_MODE:-keep}" # keep|overwrite
|
||||
YES=false
|
||||
DRY_RUN=false
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: $(basename "$0") [options]
|
||||
|
||||
Upgrade the installed Mosaic framework release.
|
||||
|
||||
Options:
|
||||
--ref <name> Bootstrap archive ref (branch/tag/commit). Default: main
|
||||
--keep Keep local files (SOUL.md, memory/) during upgrade (default)
|
||||
--overwrite Overwrite target install directory contents
|
||||
-y, --yes Skip confirmation prompt
|
||||
--dry-run Show actions without executing
|
||||
-h, --help Show this help
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--ref)
|
||||
[[ $# -lt 2 ]] && { echo "Missing value for --ref" >&2; exit 1; }
|
||||
BOOTSTRAP_REF="$2"
|
||||
shift 2
|
||||
;;
|
||||
--keep)
|
||||
INSTALL_MODE="keep"
|
||||
shift
|
||||
;;
|
||||
--overwrite)
|
||||
INSTALL_MODE="overwrite"
|
||||
shift
|
||||
;;
|
||||
-y|--yes)
|
||||
YES=true
|
||||
shift
|
||||
;;
|
||||
--dry-run)
|
||||
DRY_RUN=true
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
case "$INSTALL_MODE" in
|
||||
keep|overwrite) ;;
|
||||
*)
|
||||
echo "[mosaic-release-upgrade] Invalid install mode: $INSTALL_MODE" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
current_version="unknown"
|
||||
if [[ -x "$MOSAIC_HOME/bin/mosaic" ]]; then
|
||||
current_version="$("$MOSAIC_HOME/bin/mosaic" --version 2>/dev/null | awk '{print $2}' || true)"
|
||||
[[ -n "$current_version" ]] || current_version="unknown"
|
||||
fi
|
||||
|
||||
echo "[mosaic-release-upgrade] Current version: $current_version"
|
||||
echo "[mosaic-release-upgrade] Target ref: $BOOTSTRAP_REF"
|
||||
echo "[mosaic-release-upgrade] Install mode: $INSTALL_MODE"
|
||||
echo "[mosaic-release-upgrade] Installer URL: $REMOTE_SCRIPT_URL"
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo "[mosaic-release-upgrade] Dry run: no changes applied."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ "$YES" != "true" && -t 0 ]]; then
|
||||
printf "Proceed with Mosaic release upgrade? [y/N]: "
|
||||
read -r confirm
|
||||
case "${confirm:-n}" in
|
||||
y|Y|yes|YES) ;;
|
||||
*)
|
||||
echo "[mosaic-release-upgrade] Aborted."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
fi
|
||||
|
||||
if command -v curl >/dev/null 2>&1; then
|
||||
curl -sL "$REMOTE_SCRIPT_URL" | \
|
||||
MOSAIC_BOOTSTRAP_REF="$BOOTSTRAP_REF" \
|
||||
MOSAIC_INSTALL_MODE="$INSTALL_MODE" \
|
||||
MOSAIC_HOME="$MOSAIC_HOME" \
|
||||
sh
|
||||
elif command -v wget >/dev/null 2>&1; then
|
||||
wget -qO- "$REMOTE_SCRIPT_URL" | \
|
||||
MOSAIC_BOOTSTRAP_REF="$BOOTSTRAP_REF" \
|
||||
MOSAIC_INSTALL_MODE="$INSTALL_MODE" \
|
||||
MOSAIC_HOME="$MOSAIC_HOME" \
|
||||
sh
|
||||
else
|
||||
echo "[mosaic-release-upgrade] ERROR: curl or wget required." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
# mosaic-release-upgrade.ps1 — Upgrade installed Mosaic framework release (Windows)
|
||||
#
|
||||
# Usage:
|
||||
# mosaic-release-upgrade.ps1
|
||||
# mosaic-release-upgrade.ps1 -Ref main -Keep
|
||||
# mosaic-release-upgrade.ps1 -Ref v0.2.0 -Overwrite -Yes
|
||||
#
|
||||
param(
|
||||
[string]$Ref = $(if ($env:MOSAIC_BOOTSTRAP_REF) { $env:MOSAIC_BOOTSTRAP_REF } else { "main" }),
|
||||
[switch]$Keep,
|
||||
[switch]$Overwrite,
|
||||
[switch]$Yes,
|
||||
[switch]$DryRun
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
$MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:USERPROFILE ".config\mosaic" }
|
||||
$RemoteInstallerUrl = if ($env:MOSAIC_REMOTE_INSTALL_URL) {
|
||||
$env:MOSAIC_REMOTE_INSTALL_URL
|
||||
} else {
|
||||
"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" }
|
||||
if ($installMode -notin @("keep", "overwrite")) {
|
||||
Write-Host "[mosaic-release-upgrade] Invalid install mode: $installMode" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$currentVersion = "unknown"
|
||||
$mosaicCmd = Join-Path $MosaicHome "bin\mosaic.ps1"
|
||||
if (Test-Path $mosaicCmd) {
|
||||
try {
|
||||
$currentVersion = (& $mosaicCmd --version) -replace '^mosaic\s+', ''
|
||||
}
|
||||
catch {
|
||||
$currentVersion = "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-release-upgrade] Current version: $currentVersion"
|
||||
Write-Host "[mosaic-release-upgrade] Target ref: $Ref"
|
||||
Write-Host "[mosaic-release-upgrade] Install mode: $installMode"
|
||||
Write-Host "[mosaic-release-upgrade] Installer URL: $RemoteInstallerUrl"
|
||||
|
||||
if ($DryRun) {
|
||||
Write-Host "[mosaic-release-upgrade] Dry run: no changes applied."
|
||||
exit 0
|
||||
}
|
||||
|
||||
if (-not $Yes) {
|
||||
$confirmation = Read-Host "Proceed with Mosaic release upgrade? [y/N]"
|
||||
if ($confirmation -notin @("y", "Y", "yes", "YES")) {
|
||||
Write-Host "[mosaic-release-upgrade] Aborted."
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
$env:MOSAIC_BOOTSTRAP_REF = $Ref
|
||||
$env:MOSAIC_INSTALL_MODE = $installMode
|
||||
$env:MOSAIC_HOME = $MosaicHome
|
||||
|
||||
Invoke-RestMethod -Uri $RemoteInstallerUrl | Invoke-Expression
|
||||
|
||||
9
packages/mosaic/framework/tools/_scripts/mosaic-session-end
Executable file
9
packages/mosaic/framework/tools/_scripts/mosaic-session-end
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -x "scripts/agent/session-end.sh" ]]; then
|
||||
exec bash scripts/agent/session-end.sh "$@"
|
||||
fi
|
||||
|
||||
echo "[mosaic] Missing scripts/agent/session-end.sh in $(pwd)" >&2
|
||||
exit 1
|
||||
9
packages/mosaic/framework/tools/_scripts/mosaic-session-start
Executable file
9
packages/mosaic/framework/tools/_scripts/mosaic-session-start
Executable file
@@ -0,0 +1,9 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
if [[ -x "scripts/agent/session-start.sh" ]]; then
|
||||
exec bash scripts/agent/session-start.sh
|
||||
fi
|
||||
|
||||
echo "[mosaic] Missing scripts/agent/session-start.sh in $(pwd)" >&2
|
||||
exit 1
|
||||
183
packages/mosaic/framework/tools/_scripts/mosaic-sync-skills
Executable file
183
packages/mosaic/framework/tools/_scripts/mosaic-sync-skills
Executable file
@@ -0,0 +1,183 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
SKILLS_REPO_URL="${MOSAIC_SKILLS_REPO_URL:-https://git.mosaicstack.dev/mosaic/agent-skills.git}"
|
||||
SKILLS_REPO_DIR="${MOSAIC_SKILLS_REPO_DIR:-$MOSAIC_HOME/sources/agent-skills}"
|
||||
MOSAIC_SKILLS_DIR="$MOSAIC_HOME/skills"
|
||||
MOSAIC_LOCAL_SKILLS_DIR="$MOSAIC_HOME/skills-local"
|
||||
|
||||
fetch=1
|
||||
link_only=0
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: $(basename "$0") [options]
|
||||
|
||||
Sync canonical skills into ~/.config/mosaic/skills and link all Mosaic skills into runtime skill directories.
|
||||
|
||||
Options:
|
||||
--link-only Skip git clone/pull and only relink from ~/.config/mosaic/{skills,skills-local}
|
||||
--no-link Sync canonical skills but do not update runtime links
|
||||
-h, --help Show help
|
||||
|
||||
Env:
|
||||
MOSAIC_HOME Default: ~/.config/mosaic
|
||||
MOSAIC_SKILLS_REPO_URL Default: https://git.mosaicstack.dev/mosaic/agent-skills.git
|
||||
MOSAIC_SKILLS_REPO_DIR Default: ~/.config/mosaic/sources/agent-skills
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--link-only)
|
||||
fetch=0
|
||||
shift
|
||||
;;
|
||||
--no-link)
|
||||
link_only=1
|
||||
shift
|
||||
;;
|
||||
-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 [[ $fetch -eq 1 ]]; then
|
||||
if [[ -d "$SKILLS_REPO_DIR/.git" ]]; then
|
||||
echo "[mosaic-skills] Updating skills source: $SKILLS_REPO_DIR"
|
||||
git -C "$SKILLS_REPO_DIR" pull --rebase
|
||||
else
|
||||
echo "[mosaic-skills] Cloning skills source to: $SKILLS_REPO_DIR"
|
||||
mkdir -p "$(dirname "$SKILLS_REPO_DIR")"
|
||||
git clone "$SKILLS_REPO_URL" "$SKILLS_REPO_DIR"
|
||||
fi
|
||||
|
||||
SOURCE_SKILLS_DIR="$SKILLS_REPO_DIR/skills"
|
||||
if [[ ! -d "$SOURCE_SKILLS_DIR" ]]; then
|
||||
echo "[mosaic-skills] Missing source skills dir: $SOURCE_SKILLS_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if command -v rsync >/dev/null 2>&1; then
|
||||
rsync -a --delete "$SOURCE_SKILLS_DIR/" "$MOSAIC_SKILLS_DIR/"
|
||||
else
|
||||
rm -rf "$MOSAIC_SKILLS_DIR"/*
|
||||
cp -R "$SOURCE_SKILLS_DIR"/* "$MOSAIC_SKILLS_DIR"/
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ ! -d "$MOSAIC_SKILLS_DIR" ]]; then
|
||||
echo "[mosaic-skills] Canonical skills dir missing: $MOSAIC_SKILLS_DIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ $link_only -eq 1 ]]; then
|
||||
echo "[mosaic-skills] Canonical sync completed (link update skipped)"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
link_targets=(
|
||||
"$HOME/.claude/skills"
|
||||
"$HOME/.codex/skills"
|
||||
"$HOME/.config/opencode/skills"
|
||||
"$HOME/.pi/agent/skills"
|
||||
)
|
||||
|
||||
canonical_real="$(readlink -f "$MOSAIC_SKILLS_DIR")"
|
||||
|
||||
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
|
||||
|
||||
link_path="$target_dir/$name"
|
||||
|
||||
if [[ -L "$link_path" ]]; then
|
||||
ln -sfn "$skill_path" "$link_path"
|
||||
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
|
||||
|
||||
resolved="$(readlink -f "$link_path" 2>/dev/null || true)"
|
||||
if [[ -z "$resolved" ]]; then
|
||||
rm -f "$link_path"
|
||||
echo "[mosaic-skills] Removed stale broken skill link: $link_path"
|
||||
continue
|
||||
fi
|
||||
|
||||
if [[ "$resolved" == "$MOSAIC_HOME/"* ]]; 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)
|
||||
}
|
||||
|
||||
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"
|
||||
126
packages/mosaic/framework/tools/_scripts/mosaic-sync-skills.ps1
Normal file
126
packages/mosaic/framework/tools/_scripts/mosaic-sync-skills.ps1
Normal file
@@ -0,0 +1,126 @@
|
||||
# mosaic-sync-skills.ps1
|
||||
# Syncs canonical skills and links them into agent runtime skill directories.
|
||||
# Uses directory junctions (no elevation required) with fallback to copies.
|
||||
# PowerShell equivalent of mosaic-sync-skills (bash).
|
||||
$ErrorActionPreference = "Stop"
|
||||
|
||||
param(
|
||||
[switch]$LinkOnly,
|
||||
[switch]$NoLink,
|
||||
[switch]$Help
|
||||
)
|
||||
|
||||
$MosaicHome = if ($env:MOSAIC_HOME) { $env:MOSAIC_HOME } else { Join-Path $env:USERPROFILE ".config\mosaic" }
|
||||
$SkillsRepoUrl = if ($env:MOSAIC_SKILLS_REPO_URL) { $env:MOSAIC_SKILLS_REPO_URL } else { "https://git.mosaicstack.dev/mosaic/agent-skills.git" }
|
||||
$SkillsRepoDir = if ($env:MOSAIC_SKILLS_REPO_DIR) { $env:MOSAIC_SKILLS_REPO_DIR } else { Join-Path $MosaicHome "sources\agent-skills" }
|
||||
$MosaicSkillsDir = Join-Path $MosaicHome "skills"
|
||||
$MosaicLocalSkillsDir = Join-Path $MosaicHome "skills-local"
|
||||
|
||||
if ($Help) {
|
||||
Write-Host @"
|
||||
Usage: mosaic-sync-skills.ps1 [-LinkOnly] [-NoLink] [-Help]
|
||||
|
||||
Sync canonical skills into ~/.config/mosaic/skills and link all Mosaic skills
|
||||
into runtime skill directories using directory junctions.
|
||||
|
||||
Options:
|
||||
-LinkOnly Skip git clone/pull and only relink
|
||||
-NoLink Sync canonical skills but do not update runtime links
|
||||
-Help Show help
|
||||
"@
|
||||
exit 0
|
||||
}
|
||||
|
||||
foreach ($d in @($MosaicHome, $MosaicSkillsDir, $MosaicLocalSkillsDir)) {
|
||||
if (-not (Test-Path $d)) { New-Item -ItemType Directory -Path $d -Force | Out-Null }
|
||||
}
|
||||
|
||||
# Fetch skills from git
|
||||
if (-not $LinkOnly) {
|
||||
if (Test-Path (Join-Path $SkillsRepoDir ".git")) {
|
||||
Write-Host "[mosaic-skills] Updating skills source: $SkillsRepoDir"
|
||||
git -C $SkillsRepoDir pull --rebase
|
||||
}
|
||||
else {
|
||||
Write-Host "[mosaic-skills] Cloning skills source to: $SkillsRepoDir"
|
||||
$parentDir = Split-Path $SkillsRepoDir -Parent
|
||||
if (-not (Test-Path $parentDir)) { New-Item -ItemType Directory -Path $parentDir -Force | Out-Null }
|
||||
git clone $SkillsRepoUrl $SkillsRepoDir
|
||||
}
|
||||
|
||||
$sourceSkillsDir = Join-Path $SkillsRepoDir "skills"
|
||||
if (-not (Test-Path $sourceSkillsDir)) {
|
||||
Write-Host "[mosaic-skills] Missing source skills dir: $sourceSkillsDir" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Sync: remove old, copy new
|
||||
if (Test-Path $MosaicSkillsDir) {
|
||||
Get-ChildItem $MosaicSkillsDir | Remove-Item -Recurse -Force
|
||||
}
|
||||
Copy-Item "$sourceSkillsDir\*" $MosaicSkillsDir -Recurse -Force
|
||||
}
|
||||
|
||||
if (-not (Test-Path $MosaicSkillsDir)) {
|
||||
Write-Host "[mosaic-skills] Canonical skills dir missing: $MosaicSkillsDir" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($NoLink) {
|
||||
Write-Host "[mosaic-skills] Canonical sync completed (link update skipped)"
|
||||
exit 0
|
||||
}
|
||||
|
||||
function Link-SkillIntoTarget {
|
||||
param([string]$SkillPath, [string]$TargetDir)
|
||||
|
||||
$name = Split-Path $SkillPath -Leaf
|
||||
if ($name.StartsWith(".")) { return }
|
||||
|
||||
$linkPath = Join-Path $TargetDir $name
|
||||
|
||||
# Already a junction/symlink — recreate
|
||||
$existing = Get-Item $linkPath -Force -ErrorAction SilentlyContinue
|
||||
if ($existing -and ($existing.Attributes -band [System.IO.FileAttributes]::ReparsePoint)) {
|
||||
Remove-Item $linkPath -Force
|
||||
}
|
||||
elseif ($existing) {
|
||||
Write-Host "[mosaic-skills] Preserve existing runtime-specific entry: $linkPath"
|
||||
return
|
||||
}
|
||||
|
||||
# Try junction first, fall back to copy
|
||||
try {
|
||||
New-Item -ItemType Junction -Path $linkPath -Target $SkillPath -ErrorAction Stop | Out-Null
|
||||
}
|
||||
catch {
|
||||
Write-Host "[mosaic-skills] Junction failed for $name, falling back to copy"
|
||||
Copy-Item $SkillPath $linkPath -Recurse -Force
|
||||
}
|
||||
}
|
||||
|
||||
$linkTargets = @(
|
||||
(Join-Path $env:USERPROFILE ".claude\skills"),
|
||||
(Join-Path $env:USERPROFILE ".codex\skills"),
|
||||
(Join-Path $env:USERPROFILE ".config\opencode\skills")
|
||||
)
|
||||
|
||||
foreach ($target in $linkTargets) {
|
||||
if (-not (Test-Path $target)) { New-Item -ItemType Directory -Path $target -Force | Out-Null }
|
||||
|
||||
# Link canonical skills
|
||||
Get-ChildItem $MosaicSkillsDir -Directory | ForEach-Object {
|
||||
Link-SkillIntoTarget $_.FullName $target
|
||||
}
|
||||
|
||||
# Link local skills
|
||||
if (Test-Path $MosaicLocalSkillsDir) {
|
||||
Get-ChildItem $MosaicLocalSkillsDir -Directory | ForEach-Object {
|
||||
Link-SkillIntoTarget $_.FullName $target
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-skills] Linked skills into: $target"
|
||||
}
|
||||
|
||||
Write-Host "[mosaic-skills] Complete"
|
||||
218
packages/mosaic/framework/tools/_scripts/mosaic-upgrade
Executable file
218
packages/mosaic/framework/tools/_scripts/mosaic-upgrade
Executable file
@@ -0,0 +1,218 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# mosaic-upgrade — Clean up stale per-project files after Mosaic centralization
|
||||
#
|
||||
# SOUL.md → Now global at ~/.config/mosaic/SOUL.md (remove from projects)
|
||||
# CLAUDE.md → Now a thin pointer or removable (replace with pointer or remove)
|
||||
# AGENTS.md → Keep project-specific content, strip stale load-order directives
|
||||
#
|
||||
# Usage:
|
||||
# mosaic-upgrade [path] Upgrade a specific project (default: current dir)
|
||||
# mosaic-upgrade --all Scan ~/src/* for projects to upgrade
|
||||
# mosaic-upgrade --dry-run Show what would change without touching anything
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
|
||||
# Colors (disabled if not a terminal)
|
||||
if [[ -t 1 ]]; then
|
||||
GREEN='\033[0;32m' YELLOW='\033[0;33m' RED='\033[0;31m'
|
||||
CYAN='\033[0;36m' BOLD='\033[1m' DIM='\033[2m' RESET='\033[0m'
|
||||
else
|
||||
GREEN='' YELLOW='' RED='' CYAN='' BOLD='' DIM='' RESET=''
|
||||
fi
|
||||
|
||||
ok() { echo -e " ${GREEN}✓${RESET} $1"; }
|
||||
skip() { echo -e " ${DIM}–${RESET} $1"; }
|
||||
warn() { echo -e " ${YELLOW}⚠${RESET} $1"; }
|
||||
act() { echo -e " ${CYAN}→${RESET} $1"; }
|
||||
|
||||
DRY_RUN=false
|
||||
ALL=false
|
||||
TARGET=""
|
||||
SEARCH_ROOT="${HOME}/src"
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
mosaic-upgrade — Clean up stale per-project files
|
||||
|
||||
Usage:
|
||||
mosaic-upgrade [path] Upgrade a specific project (default: cwd)
|
||||
mosaic-upgrade --all Scan ~/src/* for all git projects
|
||||
mosaic-upgrade --dry-run Preview changes without writing
|
||||
mosaic-upgrade --all --dry-run Preview all projects
|
||||
|
||||
After Mosaic centralization:
|
||||
SOUL.md → Removed (now global at ~/.config/mosaic/SOUL.md)
|
||||
CLAUDE.md → Replaced with thin pointer or removed
|
||||
AGENTS.md → Stale load-order sections stripped; project content preserved
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--all) ALL=true; shift ;;
|
||||
--root) SEARCH_ROOT="$2"; shift 2 ;;
|
||||
-h|--help) usage; exit 0 ;;
|
||||
-*) echo "Unknown flag: $1" >&2; usage >&2; exit 1 ;;
|
||||
*) TARGET="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Generate the thin CLAUDE.md pointer
|
||||
CLAUDE_POINTER='# CLAUDE Compatibility Pointer
|
||||
|
||||
This file exists so Claude Code sessions load Mosaic standards.
|
||||
|
||||
## MANDATORY — Read Before Any Response
|
||||
|
||||
BEFORE responding to any user message, READ `~/.config/mosaic/AGENTS.md`.
|
||||
|
||||
That file is the universal agent configuration. Do NOT respond until you have loaded it.
|
||||
Then read the project-local `AGENTS.md` in this repository for project-specific guidance.'
|
||||
|
||||
upgrade_project() {
|
||||
local project_dir="$1"
|
||||
local project_name
|
||||
project_name="$(basename "$project_dir")"
|
||||
local changed=false
|
||||
|
||||
echo -e "\n${BOLD}$project_name${RESET} ${DIM}($project_dir)${RESET}"
|
||||
|
||||
# ── SOUL.md ──────────────────────────────────────────────
|
||||
local soul="$project_dir/SOUL.md"
|
||||
if [[ -f "$soul" ]]; then
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
act "Would remove SOUL.md (now global at ~/.config/mosaic/SOUL.md)"
|
||||
else
|
||||
rm "$soul"
|
||||
ok "Removed SOUL.md (now global)"
|
||||
fi
|
||||
changed=true
|
||||
else
|
||||
skip "No SOUL.md (already clean)"
|
||||
fi
|
||||
|
||||
# ── CLAUDE.md ────────────────────────────────────────────
|
||||
local claude_md="$project_dir/CLAUDE.md"
|
||||
if [[ -f "$claude_md" ]]; then
|
||||
local claude_content
|
||||
claude_content="$(cat "$claude_md")"
|
||||
|
||||
# Check if it's already a thin pointer to AGENTS.md
|
||||
if echo "$claude_content" | grep -q "READ.*~/.config/mosaic/AGENTS.md"; then
|
||||
skip "CLAUDE.md already points to global AGENTS.md"
|
||||
else
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
act "Would replace CLAUDE.md with thin pointer to global AGENTS.md"
|
||||
else
|
||||
# Back up the original
|
||||
cp "$claude_md" "${claude_md}.mosaic-bak"
|
||||
echo "$CLAUDE_POINTER" > "$claude_md"
|
||||
ok "Replaced CLAUDE.md with pointer (backup: CLAUDE.md.mosaic-bak)"
|
||||
fi
|
||||
changed=true
|
||||
fi
|
||||
else
|
||||
skip "No CLAUDE.md"
|
||||
fi
|
||||
|
||||
# ── AGENTS.md (strip stale load-order, preserve project content) ─
|
||||
local agents="$project_dir/AGENTS.md"
|
||||
if [[ -f "$agents" ]]; then
|
||||
# Detect stale load-order patterns
|
||||
local has_stale=false
|
||||
|
||||
# Pattern 1: References to SOUL.md in load order
|
||||
if grep -qE "(Read|READ|Load).*SOUL\.md" "$agents" 2>/dev/null; then
|
||||
has_stale=true
|
||||
fi
|
||||
|
||||
# Pattern 2: Old "## Load Order" section that references centralized files
|
||||
if grep -q "## Load Order" "$agents" 2>/dev/null && \
|
||||
grep -qE "STANDARDS\.md|SOUL\.md" "$agents" 2>/dev/null; then
|
||||
has_stale=true
|
||||
fi
|
||||
|
||||
# Pattern 3: Old ~/.mosaic/ path (pre-centralization)
|
||||
if grep -q '~/.mosaic/' "$agents" 2>/dev/null; then
|
||||
has_stale=true
|
||||
fi
|
||||
|
||||
if [[ "$has_stale" == "true" ]]; then
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
act "Would strip stale load-order section from AGENTS.md"
|
||||
# Show what we detect
|
||||
if grep -qn "## Load Order" "$agents" 2>/dev/null; then
|
||||
local line
|
||||
line=$(grep -n "## Load Order" "$agents" | head -1 | cut -d: -f1)
|
||||
echo -e " ${DIM}Line $line: Found '## Load Order' section referencing SOUL.md/STANDARDS.md${RESET}"
|
||||
fi
|
||||
if grep -qn '~/.mosaic/' "$agents" 2>/dev/null; then
|
||||
echo -e " ${DIM}Found references to old ~/.mosaic/ path${RESET}"
|
||||
fi
|
||||
else
|
||||
cp "$agents" "${agents}.mosaic-bak"
|
||||
|
||||
# Strip the Load Order section (from "## Load Order" to next "##" or "---")
|
||||
if grep -q "## Load Order" "$agents"; then
|
||||
awk '
|
||||
/^## Load Order/ { skip=1; next }
|
||||
skip && /^(## |---)/ { skip=0 }
|
||||
skip { next }
|
||||
{ print }
|
||||
' "${agents}.mosaic-bak" > "$agents"
|
||||
fi
|
||||
|
||||
# Fix old ~/.mosaic/ → ~/.config/mosaic/
|
||||
if grep -q '~/.mosaic/' "$agents"; then
|
||||
sed -i 's|~/.mosaic/|~/.config/mosaic/|g' "$agents"
|
||||
fi
|
||||
|
||||
ok "Stripped stale load-order from AGENTS.md (backup: AGENTS.md.mosaic-bak)"
|
||||
fi
|
||||
changed=true
|
||||
else
|
||||
skip "AGENTS.md has no stale directives"
|
||||
fi
|
||||
else
|
||||
skip "No AGENTS.md"
|
||||
fi
|
||||
|
||||
# ── .claude/settings.json (leave alone) ──────────────────
|
||||
# Project-specific settings are fine — don't touch them.
|
||||
|
||||
if [[ "$changed" == "false" ]]; then
|
||||
echo -e " ${GREEN}Already up to date.${RESET}"
|
||||
fi
|
||||
}
|
||||
|
||||
# ── Main ───────────────────────────────────────────────────
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo -e "${BOLD}Mode: DRY RUN (no files will be changed)${RESET}"
|
||||
fi
|
||||
|
||||
if [[ "$ALL" == "true" ]]; then
|
||||
echo -e "${BOLD}Scanning $SEARCH_ROOT for projects...${RESET}"
|
||||
count=0
|
||||
for dir in "$SEARCH_ROOT"/*/; do
|
||||
[[ -d "$dir/.git" ]] || continue
|
||||
upgrade_project "$dir"
|
||||
count=$((count + 1))
|
||||
done
|
||||
echo -e "\n${BOLD}Scanned $count projects.${RESET}"
|
||||
elif [[ -n "$TARGET" ]]; then
|
||||
if [[ ! -d "$TARGET" ]]; then
|
||||
echo "[mosaic-upgrade] ERROR: $TARGET is not a directory." >&2
|
||||
exit 1
|
||||
fi
|
||||
upgrade_project "$TARGET"
|
||||
else
|
||||
upgrade_project "$(pwd)"
|
||||
fi
|
||||
|
||||
if [[ "$DRY_RUN" == "true" ]]; then
|
||||
echo -e "\n${YELLOW}This was a dry run. Run without --dry-run to apply changes.${RESET}"
|
||||
fi
|
||||
116
packages/mosaic/framework/tools/_scripts/mosaic-upgrade-slaves
Executable file
116
packages/mosaic/framework/tools/_scripts/mosaic-upgrade-slaves
Executable file
@@ -0,0 +1,116 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
BOOTSTRAP_CMD="$MOSAIC_HOME/tools/_scripts/mosaic-bootstrap-repo"
|
||||
|
||||
roots=("$HOME/src")
|
||||
apply=0
|
||||
|
||||
usage() {
|
||||
cat <<USAGE
|
||||
Usage: $(basename "$0") [options]
|
||||
|
||||
Upgrade all Mosaic-linked slave repositories by re-running repo bootstrap with --force.
|
||||
|
||||
Options:
|
||||
--root <path> Add a search root (repeatable). Default: $HOME/src
|
||||
--apply Execute upgrades. Without this flag, script is dry-run.
|
||||
-h, --help Show this help
|
||||
USAGE
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--root)
|
||||
[[ $# -lt 2 ]] && { echo "Missing value for --root" >&2; exit 1; }
|
||||
roots+=("$2")
|
||||
shift 2
|
||||
;;
|
||||
--apply)
|
||||
apply=1
|
||||
shift
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
exit 0
|
||||
;;
|
||||
*)
|
||||
echo "Unknown argument: $1" >&2
|
||||
usage >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ! -x "$BOOTSTRAP_CMD" ]]; then
|
||||
echo "[mosaic-upgrade] Missing bootstrap command: $BOOTSTRAP_CMD" >&2
|
||||
echo "[mosaic-upgrade] Install/refresh framework first: ~/.config/mosaic/install.sh" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# De-duplicate roots while preserving order.
|
||||
uniq_roots=()
|
||||
for r in "${roots[@]}"; do
|
||||
skip=0
|
||||
for e in "${uniq_roots[@]}"; do
|
||||
[[ "$r" == "$e" ]] && { skip=1; break; }
|
||||
done
|
||||
[[ $skip -eq 0 ]] && uniq_roots+=("$r")
|
||||
done
|
||||
|
||||
candidates=()
|
||||
for root in "${uniq_roots[@]}"; do
|
||||
[[ -d "$root" ]] || continue
|
||||
|
||||
while IFS= read -r marker; do
|
||||
repo_dir="$(dirname "$(dirname "$marker")")"
|
||||
if [[ -d "$repo_dir/.git" ]]; then
|
||||
candidates+=("$repo_dir")
|
||||
fi
|
||||
done < <(find "$root" -type f -path '*/.mosaic/README.md' 2>/dev/null)
|
||||
done
|
||||
|
||||
# De-duplicate repos while preserving order.
|
||||
repos=()
|
||||
for repo in "${candidates[@]}"; do
|
||||
skip=0
|
||||
for existing in "${repos[@]}"; do
|
||||
[[ "$repo" == "$existing" ]] && { skip=1; break; }
|
||||
done
|
||||
[[ $skip -eq 0 ]] && repos+=("$repo")
|
||||
done
|
||||
|
||||
count_total=${#repos[@]}
|
||||
count_ok=0
|
||||
count_fail=0
|
||||
|
||||
mode="DRY-RUN"
|
||||
[[ $apply -eq 1 ]] && mode="APPLY"
|
||||
|
||||
echo "[mosaic-upgrade] Mode: $mode"
|
||||
echo "[mosaic-upgrade] Roots: ${uniq_roots[*]}"
|
||||
echo "[mosaic-upgrade] Linked repos found: $count_total"
|
||||
|
||||
if [[ $count_total -eq 0 ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
for repo in "${repos[@]}"; do
|
||||
if [[ $apply -eq 1 ]]; then
|
||||
if "$BOOTSTRAP_CMD" "$repo" --force >/dev/null; then
|
||||
echo "[mosaic-upgrade] upgraded: $repo"
|
||||
count_ok=$((count_ok + 1))
|
||||
else
|
||||
echo "[mosaic-upgrade] FAILED: $repo" >&2
|
||||
count_fail=$((count_fail + 1))
|
||||
fi
|
||||
else
|
||||
echo "[mosaic-upgrade] would upgrade: $repo"
|
||||
fi
|
||||
done
|
||||
|
||||
if [[ $apply -eq 1 ]]; then
|
||||
echo "[mosaic-upgrade] complete: ok=$count_ok failed=$count_fail total=$count_total"
|
||||
[[ $count_fail -gt 0 ]] && exit 1
|
||||
fi
|
||||
25
packages/mosaic/framework/tools/_scripts/mosaic-wizard
Executable file
25
packages/mosaic/framework/tools/_scripts/mosaic-wizard
Executable file
@@ -0,0 +1,25 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# mosaic-wizard — Thin shell wrapper for the bundled TypeScript wizard
|
||||
MOSAIC_HOME="${MOSAIC_HOME:-$HOME/.config/mosaic}"
|
||||
|
||||
# Look for the bundle in the installed location first, then the source repo
|
||||
WIZARD_BIN="$MOSAIC_HOME/dist/mosaic-wizard.mjs"
|
||||
if [[ ! -f "$WIZARD_BIN" ]]; then
|
||||
WIZARD_BIN="$(cd "$(dirname "$0")/.." && pwd)/dist/mosaic-wizard.mjs"
|
||||
fi
|
||||
|
||||
if [[ ! -f "$WIZARD_BIN" ]]; then
|
||||
echo "[mosaic-wizard] ERROR: Wizard bundle not found." >&2
|
||||
echo "[mosaic-wizard] Re-install with: npm install -g @mosaic/mosaic" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
echo "[mosaic-wizard] ERROR: Node.js is required but not found." >&2
|
||||
echo "[mosaic-wizard] Install Node.js 18+ from https://nodejs.org" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exec node "$WIZARD_BIN" "$@"
|
||||
Reference in New Issue
Block a user