From 90f693d6910c67ed2df03dd978429b6719ba8989 Mon Sep 17 00:00:00 2001 From: aaron Date: Sat, 8 Aug 2026 22:51:45 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20armonia=20workspace=20setup=20scripts?= =?UTF-8?q?=20=E2=80=94=20bootstrap=20+=20migration=20(#307)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tools/bootstrap-armonia.sh: fresh-machine canonical layout with clone tiers (--minimal|--standard|--full), curl-able, idempotent - tools/migrate-to-armonia.sh: three-bucket repos routing (packages/, demos/, flat), bucket-converging on re-run --- tools/bootstrap-armonia.sh | 171 ++++++++++++++++++++++++++ tools/migrate-to-armonia.sh | 237 ++++++++++++++++++++++++++++++++++++ 2 files changed, 408 insertions(+) create mode 100755 tools/bootstrap-armonia.sh create mode 100755 tools/migrate-to-armonia.sh diff --git a/tools/bootstrap-armonia.sh b/tools/bootstrap-armonia.sh new file mode 100755 index 0000000..b9b75ea --- /dev/null +++ b/tools/bootstrap-armonia.sh @@ -0,0 +1,171 @@ +#!/usr/bin/env bash +set -euo pipefail + +# bootstrap-armonia.sh +# Fresh-machine setup for the canonical Amicode workspace layout. +# Any user — Harmoniqs dev or external contributor — gets the same tree: +# +# ~/armonia/repos/packages/ Julia libraries (Piccolo.jl, …) +# ~/armonia/repos/demos/ demo galleries (atoms-demo, …) +# ~/armonia/repos/ apps, forks, projects (amicode, …) +# ~/armonia/data/{env,problems,runs,vaults} +# +# Usage: +# bootstrap-armonia.sh [--minimal|--standard|--full] +# +# --minimal layout + data dirs only (extension users who never touch source) +# --standard + public packages and demos via plain git clone (default) +# --full + private Harmoniqs repos via `gh` (requires gh auth with access) +# +# Curl-able for users who have not cloned anything: +# bash <(curl -fsSL https://raw.githubusercontent.com/harmoniqs/amicode/main/tools/bootstrap-armonia.sh) +# +# Idempotent: existing clones are skipped (git pull --ff-only is attempted), +# existing symlinks are left alone. + +ARMONIA="${HOME}/armonia" +AMICO="${HOME}/.amico" +TIER="standard" + +for arg in "$@"; do + case "$arg" in + --minimal|--standard|--full) TIER="${arg#--}" ;; + -h|--help) + sed -n '2,22p' "$0"; exit 0 ;; + *) echo "unknown arg: $arg" >&2; exit 64 ;; + esac +done + +# Public Julia libraries (registered packages; plain git clone works). +PUBLIC_PACKAGES=( + Piccolo.jl + NamedTrajectories.jl + DirectTrajOpt.jl +) + +# Public demo galleries. +PUBLIC_DEMOS=( + atoms-demo + fluxonium-demo + ions +) + +# Private Harmoniqs repos (cloned with gh; requires org access). +PRIVATE_PACKAGES=( + Piccolissimo.jl +) +PRIVATE_APPS=( + amicode +) + +# =================================================================== +main() { + echo "==> bootstrap-armonia tier=${TIER}" + echo + + make_layout + wire_amico_links + + case "$TIER" in + minimal) echo "tier=minimal — no repos cloned" ;; + standard) clone_public ;; + full) clone_public; clone_private ;; + esac + + echo + echo "==> Done." + echo " Layout: ~/armonia/{repos/{packages,demos,...}, data/{env,problems,runs,vaults}}" + echo " Open it: open ~/armonia/ (or add ~/armonia to your VS Code workspace)" +} + +# ------------------------------------------------------------------- +make_layout() { + echo "--- layout ---" + mkdir -p "${ARMONIA}/repos/packages" "${ARMONIA}/repos/demos" \ + "${ARMONIA}/data/env" "${ARMONIA}/data/problems" \ + "${ARMONIA}/data/runs" "${ARMONIA}/data/vaults" + echo " ~/armonia/{repos/{packages,demos}, data/{env,problems,runs,vaults}} ready" +} + +# ------------------------------------------------------------------- +# ~/.amico/ → ~/armonia/data/, created only when safe. +# If ~/.amico/ already exists as a REAL directory with content, that is +# the migration case — point the user at migrate-to-armonia.sh instead of +# clobbering it. +wire_amico_links() { + echo "--- ~/.amico links ---" + local pairs=("julia:env" "problems:problems" "runs:runs" "vaults:vaults") + for pair in "${pairs[@]}"; do + local name="${pair%%:*}" target="${pair##*:}" + local src="${AMICO}/${name}" dest="${ARMONIA}/data/${target}" + mkdir -p "$AMICO" + if [[ -L "$src" ]]; then + echo " (symlink) ~/.amico/${name}" + elif [[ -d "$src" && -n "$(ls -A "$src" 2>/dev/null)" ]]; then + echo " (real dir, not empty) ~/.amico/${name} — run tools/migrate-to-armonia.sh first" + elif [[ -d "$src" ]]; then + rmdir "$src" && ln -s "$dest" "$src" + echo " linked ~/.amico/${name} → data/${target}" + else + ln -s "$dest" "$src" + echo " linked ~/.amico/${name} → data/${target}" + fi + done +} + +# ------------------------------------------------------------------- +clone_public() { + echo "--- clone (public) ---" + for repo in "${PUBLIC_PACKAGES[@]}"; do + clone_or_update "https://github.com/harmoniqs/${repo}.git" "${ARMONIA}/repos/packages/${repo}" + done + for repo in "${PUBLIC_DEMOS[@]}"; do + clone_or_update "https://github.com/harmoniqs/${repo}.git" "${ARMONIA}/repos/demos/${repo}" + done +} + +# ------------------------------------------------------------------- +clone_private() { + echo "--- clone (private, via gh) ---" + if ! command -v gh >/dev/null 2>&1; then + echo " gh not installed — skipping private tier"; return 0 + fi + if ! gh auth status >/dev/null 2>&1; then + echo " gh not authenticated — skipping private tier (run: gh auth login)"; return 0 + fi + for repo in "${PRIVATE_PACKAGES[@]}"; do + gh_clone_or_update "harmoniqs/${repo}" "${ARMONIA}/repos/packages/${repo}" + done + for repo in "${PRIVATE_APPS[@]}"; do + gh_clone_or_update "harmoniqs/${repo}" "${ARMONIA}/repos/${repo}" + done +} + +# ------------------------------------------------------------------- +clone_or_update() { + local url="$1" dest="$2" + if [[ -d "${dest}/.git" ]]; then + echo " (exists) $(basename "$dest") — pulling" + git -C "$dest" pull --ff-only 2>/dev/null || echo " (pull skipped: not fast-forwardable)" + elif [[ -e "$dest" ]]; then + echo " (exists, not a git repo — left alone) $(basename "$dest")" + else + echo " clone $(basename "$dest")" + git clone "$url" "$dest" + fi +} + +gh_clone_or_update() { + local repo="$1" dest="$2" + if [[ -d "${dest}/.git" ]]; then + echo " (exists) $(basename "$dest") — pulling" + git -C "$dest" pull --ff-only 2>/dev/null || echo " (pull skipped: not fast-forwardable)" + elif [[ -e "$dest" ]]; then + echo " (exists, not a git repo — left alone) $(basename "$dest")" + else + echo " clone $repo" + gh repo clone "$repo" "$dest" + fi +} + +main diff --git a/tools/migrate-to-armonia.sh b/tools/migrate-to-armonia.sh new file mode 100755 index 0000000..029c563 --- /dev/null +++ b/tools/migrate-to-armonia.sh @@ -0,0 +1,237 @@ +#!/usr/bin/env bash +set -euo pipefail + +# migrate-to-armonia.sh +# Idempotent migration: consolidate repos + Amico data into ~/armonia/. +# Safe to run multiple times — skips what is already in place, and converges +# the repos/ buckets on re-run (flat .jl packages → packages/, demo dirs → demos/). +# +# Canonical layout: +# ~/armonia/repos/packages/ Julia libraries (Piccolo.jl, …) +# ~/armonia/repos/demos/ demo galleries (atoms-demo, …) +# ~/armonia/repos/ apps, forks, research projects (amicode, passaggio, …) +# ~/armonia/data/{env,problems,runs,vaults} + +ARMONIA="${HOME}/armonia" +AMICO="${HOME}/.amico" + +# ---- discover source repos ---- +# Directories that might hold git checkouts to move. +REPO_SOURCES=( + "${HOME}/_dev/harmoniqs" + "${HOME}/harmoniqs" + "${HOME}/AmicodeProjects" + "${HOME}/_dev" +) + +# ---- discover data dirs to migrate ---- +# Each entry: "amico_dir armonia_target" +DATA_DIRS=( + "julia env" + "problems problems" + "runs runs" + "vaults vaults" +) + +# Known demo repo names → routed to repos/demos/. A source dir literally named +# "demos" is moved as-is (its contents are already grouped). +is_demo() { [[ "$1" == *demo* || "$1" == "atoms" || "$1" == "fluxonium" || "$1" == "ions" ]]; } + +# Julia packages route to repos/packages/ by the .jl suffix convention. +is_package() { [[ "$1" == *.jl || "$1" == *.jl-* ]]; } + +# =================================================================== +main() { + echo "==> migrate-to-armonia (idempotent)" + echo + + mkdir -p "${ARMONIA}/repos/packages" "${ARMONIA}/repos/demos" \ + "${ARMONIA}/data/env" "${ARMONIA}/data/problems" \ + "${ARMONIA}/data/runs" "${ARMONIA}/data/vaults" + + converge_buckets + migrate_repos + migrate_data + cleanup_empty_parents + + echo + echo "==> Done." + echo " Run: open ~/armonia/" +} + +# ------------------------------------------------------------------- +# Re-run convergence: repos/ that already migrated flat get bucketed. +converge_buckets() { + local moved=0 + for child in "${ARMONIA}/repos"/*/; do + [[ -d "$child" ]] || continue + local name; name=$(basename "$child") + case "$name" in packages|demos) continue ;; esac + if is_package "$name"; then + echo " bucket: repos/$name → repos/packages/$name" + mv "$child" "${ARMONIA}/repos/packages/$name" + moved=1 + elif is_demo "$name"; then + echo " bucket: repos/$name → repos/demos/$name" + mv "$child" "${ARMONIA}/repos/demos/$name" + moved=1 + fi + done + # A shared Julia dev env (Project.toml/Manifest.toml) stranded at repos/ + # belongs with the packages it references. + for f in Project.toml Manifest.toml; do + if [[ -f "${ARMONIA}/repos/$f" && ! -f "${ARMONIA}/repos/packages/$f" ]]; then + echo " bucket: repos/$f → repos/packages/$f" + mv "${ARMONIA}/repos/$f" "${ARMONIA}/repos/packages/$f" + fi + done + [[ $moved -eq 1 ]] && echo + return 0 +} + +# ------------------------------------------------------------------- +migrate_repos() { + echo "--- repos ---" + + for src_dir in "${REPO_SOURCES[@]}"; do + if [[ ! -d "$src_dir" ]]; then + echo " (skip) not found: $src_dir" + continue + fi + local src_children + src_children=$(find "$src_dir" -mindepth 1 -maxdepth 1 ! -name 'node_modules' 2>/dev/null || true) + if [[ -z "$src_children" ]]; then + echo " (skip) empty: $src_dir" + continue + fi + echo " source: $src_dir" + while IFS= read -r child; do + [[ -z "$child" ]] && continue + local name + name=$(basename "$child") + + # Shell scripts and loose files stay; only directories move. A root-level + # Project.toml/Manifest.toml accompanies the packages. + if [[ ! -d "$child" ]]; then + case "$name" in + Project.toml|Manifest.toml) + if [[ ! -f "${ARMONIA}/repos/packages/$name" ]]; then + echo " mv $name → packages/" + mv "$child" "${ARMONIA}/repos/packages/$name" + fi + ;; + *) echo " (skip file) $name" ;; + esac + continue + fi + + # Route to the right bucket. + local bucket="${ARMONIA}/repos" + if is_package "$name"; then + bucket="${ARMONIA}/repos/packages" + elif is_demo "$name" || [[ "$name" == "demos" ]]; then + bucket="${ARMONIA}/repos/demos" + fi + # A "demos" source dir lands AS repos/demos (contents grouped inside); + # merging into it rather than nesting demos/demos. + local dest + if [[ "$name" == "demos" ]]; then + dest="$bucket" + else + dest="${bucket}/${name}" + fi + + if [[ -e "$dest" && "$name" != "demos" ]]; then + echo " (exists) $name" + continue + fi + if [[ "$name" == "demos" && -d "$dest" ]]; then + # merge contents into the existing demos bucket + local demo_children + demo_children=$(find "$child" -mindepth 1 -maxdepth 1 2>/dev/null || true) + while IFS= read -r d; do + [[ -z "$d" ]] && continue + local dname; dname=$(basename "$d") + if [[ -e "${dest}/${dname}" ]]; then + echo " (exists) demos/$dname" + else + echo " mv demos/$dname" + mv "$d" "${dest}/${dname}" + fi + done <<< "$demo_children" + continue + fi + + echo " mv $name → ${bucket#"$ARMONIA"/}" + mv "$child" "$dest" + done <<< "$src_children" + done +} + +# ------------------------------------------------------------------- +migrate_data() { + echo "--- data ---" + + for entry in "${DATA_DIRS[@]}"; do + read -r amico_name armonia_name <<< "$entry" + local src="${AMICO}/${amico_name}" + local dest="${ARMONIA}/data/${armonia_name}" + + # already a symlink → done + if [[ -L "$src" ]]; then + echo " (symlink) ~/.amico/${amico_name}" + continue + fi + + # dest already populated → assume already migrated + if [[ -d "$dest" && -n "$(ls -A "$dest" 2>/dev/null)" ]]; then + # source still a real dir → just symlink it + if [[ -d "$src" && ! -L "$src" ]]; then + echo " (dest exists) ~/.amico/${amico_name} → symlink" + rm -rf "$src" + ln -s "$dest" "$src" + else + echo " (ok) ~/.amico/${amico_name}" + fi + continue + fi + + # source is a real dir, dest does not exist → move + symlink + if [[ -d "$src" && ! -L "$src" ]]; then + echo " mv ~/.amico/${amico_name} → data/${armonia_name}" + mv "$src" "$dest" + ln -s "$dest" "$src" + else + echo " (skip) ~/.amico/${amico_name} does not exist" + fi + done +} + +# ------------------------------------------------------------------- +cleanup_empty_parents() { + echo "--- cleanup ---" + for src_dir in "${REPO_SOURCES[@]}"; do + # never remove HOME or root-level dirs + case "$src_dir" in + "$HOME"|"$HOME/Desktop"|"$HOME/Documents"|"$HOME/Downloads") continue ;; + esac + if [[ -d "$src_dir" ]]; then + local remaining + remaining=$(find "$src_dir" -mindepth 1 -maxdepth 1 2>/dev/null || true) + if [[ -z "$remaining" ]]; then + echo " rmdir $src_dir" + rmdir "$src_dir" + # try to remove the parent if it is now empty + local parent + parent=$(dirname "$src_dir") + local parent_remaining + parent_remaining=$(find "$parent" -mindepth 1 -maxdepth 1 2>/dev/null || true) + if [[ -z "$parent_remaining" && "$parent" != "$HOME" ]]; then + rmdir "$parent" 2>/dev/null || true + fi + fi + fi + done +} + +main