mirror of
https://github.com/logos-co/logos-workspace.git
synced 2026-08-31 04:31:13 +00:00
1819 lines
59 KiB
Bash
Executable File
1819 lines
59 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
set -euo pipefail
|
|
|
|
# ── Logos Workspace CLI ──────────────────────────────────────────────────────
|
|
# Unified interface for building, testing, and managing the multi-repo workspace.
|
|
|
|
WORKSPACE_ROOT="${LOGOS_WORKSPACE_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
|
|
REPOS_DIR="$WORKSPACE_ROOT/repos"
|
|
DEP_GRAPH="$WORKSPACE_ROOT/nix/dep-graph.nix"
|
|
|
|
# Show help text and exit. Usage: show_help "text"
|
|
show_help() { echo -e "$1"; exit 0; }
|
|
# Check if any argument is --help or -h
|
|
wants_help() { for a in "$@"; do [[ "$a" == "--help" || "$a" == "-h" ]] && return 0; done; return 1; }
|
|
|
|
# Global quiet flag — suppresses informational output
|
|
QUIET=false
|
|
|
|
# Colors
|
|
RED='\033[0;31m'
|
|
GREEN='\033[0;32m'
|
|
YELLOW='\033[0;33m'
|
|
BLUE='\033[0;34m'
|
|
CYAN='\033[0;36m'
|
|
BOLD='\033[1m'
|
|
NC='\033[0m'
|
|
|
|
# Logging: log() is suppressed in quiet mode; errors always print to stderr
|
|
log() { $QUIET || echo -e "$@"; }
|
|
|
|
|
|
# ── Repo registry ───────────────────────────────────────────────────────────
|
|
# Format: "input_name|directory_name|git_url|has_flake"
|
|
# input_name: name in flake.nix inputs (empty string for non-flake repos)
|
|
# directory_name: submodule dir under repos/
|
|
# git_url: clone URL
|
|
# has_flake: "yes" or "no"
|
|
|
|
REPOS=(
|
|
# Foundation
|
|
"logos-cpp-sdk|logos-cpp-sdk|https://github.com/logos-co/logos-cpp-sdk.git|yes"
|
|
"logos-module|logos-module|https://github.com/logos-co/logos-module.git|yes"
|
|
"logos-liblogos|logos-liblogos|https://github.com/logos-co/logos-liblogos.git|yes"
|
|
"logos-capability-module|logos-capability-module|https://github.com/logos-co/logos-capability-module.git|yes"
|
|
"logos-package|logos-package|https://github.com/logos-co/logos-package.git|yes"
|
|
"logos-module-builder|logos-module-builder|https://github.com/logos-co/logos-module-builder.git|yes"
|
|
|
|
# Packaging / Bundling
|
|
"nix-bundle-dir|nix-bundle-dir|https://github.com/logos-co/nix-bundle-dir.git|yes"
|
|
"nix-bundle-lgx|nix-bundle-lgx|https://github.com/logos-co/nix-bundle-lgx.git|yes"
|
|
|
|
# App
|
|
"logos-app-poc|logos-app-poc|https://github.com/logos-co/logos-app-poc.git|yes"
|
|
|
|
# Accounts
|
|
"logos-accounts-module|logos-accounts-module|https://github.com/logos-co/logos-accounts-module.git|yes"
|
|
"logos-accounts-ui|logos-accounts-ui|https://github.com/logos-co/logos-accounts-ui.git|yes"
|
|
|
|
# Chat & Messaging
|
|
"logos-chat-module|logos-chat-module|https://github.com/logos-co/logos-chat-module.git|yes"
|
|
"logos-chat-legacy-module|logos-chat-legacy-module|https://github.com/logos-co/logos-chat-legacy-module.git|yes"
|
|
"logos-chat-tui|logos-chat-tui|https://github.com/logos-co/logos-chat-tui.git|yes"
|
|
"logos-chat-ui|logos-chat-ui|https://github.com/logos-co/logos-chat-ui.git|yes"
|
|
"logos-chatsdk-module|logos-chatsdk-module|https://github.com/logos-co/logos-chatsdk-module.git|yes"
|
|
"logos-chatsdk-ui|logos-chatsdk-ui|https://github.com/logos-co/logos-chatsdk-ui.git|yes"
|
|
"logos-irc-module|logos-irc-module|https://github.com/logos-co/logos-irc-module.git|yes"
|
|
|
|
# Waku
|
|
"logos-waku-module|logos-waku-module|https://github.com/logos-co/logos-waku-module.git|yes"
|
|
"logos-waku-ui|logos-waku-ui|https://github.com/logos-co/logos-waku-ui.git|yes"
|
|
|
|
# Package Manager
|
|
"logos-package-manager-module|logos-package-manager-module|https://github.com/logos-co/logos-package-manager-module.git|yes"
|
|
"logos-package-manager-ui|logos-package-manager-ui|https://github.com/logos-co/logos-package-manager-ui.git|yes"
|
|
|
|
# Storage
|
|
"logos-storage-module|logos-storage-module|https://github.com/logos-co/logos-storage-module.git|yes"
|
|
"logos-storage-ui|logos-storage-ui|https://github.com/logos-co/logos-storage-ui.git|yes"
|
|
|
|
# Wallet
|
|
"logos-wallet-module|logos-wallet-module|https://github.com/logos-co/logos-wallet-module.git|yes"
|
|
"logos-wallet-ui|logos-wallet-ui|https://github.com/logos-co/logos-wallet-ui.git|yes"
|
|
|
|
# Other modules
|
|
"logos-libp2p-module|logos-libp2p-module|https://github.com/logos-co/logos-libp2p-module.git|yes"
|
|
"logos-simple-module|logos-simple-module|https://github.com/logos-co/logos-simple-module.git|yes"
|
|
"logos-webview-app|logos-webview-app|https://github.com/logos-co/logos-webview-app.git|yes"
|
|
"counter_qml|counter_qml|https://github.com/logos-co/counter_qml.git|yes"
|
|
"counter|counter|https://github.com/logos-co/counter.git|yes"
|
|
|
|
# UI
|
|
"logos-design-system|logos-design-system|https://github.com/logos-co/logos-design-system.git|yes"
|
|
|
|
# SDKs
|
|
"logos-js-sdk|logos-js-sdk|https://github.com/logos-co/logos-js-sdk.git|yes"
|
|
"logos-nim-sdk|logos-nim-sdk|https://github.com/logos-co/logos-nim-sdk.git|yes"
|
|
"logos-rust-sdk|logos-rust-sdk|https://github.com/logos-co/logos-rust-sdk.git|yes"
|
|
|
|
# Other
|
|
"logos-modules|logos-modules|https://github.com/logos-co/logos-modules.git|yes"
|
|
"logos-module-viewer|logos-module-viewer|https://github.com/logos-co/logos-module-viewer.git|yes"
|
|
|
|
# Non-flake repos (submodules only)
|
|
"|logos-blockchain-module|https://github.com/logos-blockchain/logos-blockchain-module.git|no"
|
|
"|logos-template-module|https://github.com/logos-co/logos-template-module.git|no"
|
|
"|logos-docs|https://github.com/logos-co/logos-docs.git|no"
|
|
"|logos-website|https://github.com/logos-co/logos-website.git|no"
|
|
"|logos-tutorial|https://github.com/logos-co/logos-tutorial.git|no"
|
|
"|node-configs|https://github.com/logos-co/node-configs.git|no"
|
|
)
|
|
|
|
# ── Helper functions ─────────────────────────────────────────────────────────
|
|
|
|
get_field() {
|
|
echo "$1" | cut -d'|' -f"$2"
|
|
}
|
|
|
|
find_repo_entry() {
|
|
local query="$1"
|
|
for entry in "${REPOS[@]}"; do
|
|
local input_name dir_name
|
|
input_name=$(get_field "$entry" 1)
|
|
dir_name=$(get_field "$entry" 2)
|
|
if [[ "$dir_name" == "$query" || "$input_name" == "$query" ]]; then
|
|
echo "$entry"
|
|
return 0
|
|
fi
|
|
done
|
|
return 1
|
|
}
|
|
|
|
get_input_name() {
|
|
local dir_name="$1"
|
|
for entry in "${REPOS[@]}"; do
|
|
if [[ "$(get_field "$entry" 2)" == "$dir_name" ]]; then
|
|
echo "$(get_field "$entry" 1)"
|
|
return 0
|
|
fi
|
|
done
|
|
echo "$dir_name"
|
|
}
|
|
|
|
# Get repos that are dirty (have local uncommitted changes)
|
|
get_dirty_repos() {
|
|
local dirty=()
|
|
for entry in "${REPOS[@]}"; do
|
|
local dir_name
|
|
dir_name=$(get_field "$entry" 2)
|
|
local repo_path="$REPOS_DIR/$dir_name"
|
|
if [[ -d "$repo_path/.git" ]] || [[ -f "$repo_path/.git" ]]; then
|
|
if [[ -n "$(git -C "$repo_path" status --porcelain 2>/dev/null)" ]] || \
|
|
[[ -n "$(git -C "$repo_path" log '@{upstream}..HEAD' --oneline 2>/dev/null)" ]]; then
|
|
dirty+=("$dir_name")
|
|
fi
|
|
fi
|
|
done
|
|
echo "${dirty[@]}"
|
|
}
|
|
|
|
# Build --override-input flags for dirty repos
|
|
build_override_flags() {
|
|
local flags=()
|
|
local explicit_overrides=("$@")
|
|
|
|
for dir_name in "${explicit_overrides[@]}"; do
|
|
local input_name
|
|
input_name=$(get_input_name "$dir_name")
|
|
if [[ -n "$input_name" ]]; then
|
|
flags+=("--override-input" "$input_name" "path:$REPOS_DIR/$dir_name")
|
|
fi
|
|
done
|
|
echo "${flags[@]}"
|
|
}
|
|
|
|
# Check if a repo has tests according to dep-graph metadata
|
|
has_tests() {
|
|
local target="$1"
|
|
if [[ ! -f "$DEP_GRAPH" ]]; then
|
|
return 0 # assume yes if no metadata
|
|
fi
|
|
local result
|
|
result=$(nix eval --extra-experimental-features "nix-command flakes" \
|
|
--impure --raw --expr '
|
|
let graph = import '"$DEP_GRAPH"';
|
|
in if (graph.${"'"$target"'"} or null) == null then "unknown"
|
|
else if graph.${"'"$target"'"}.hasTests then "true"
|
|
else "false"
|
|
' 2>/dev/null)
|
|
[[ "$result" != "false" ]]
|
|
}
|
|
|
|
# Get dependents of a repo (repos that depend on it)
|
|
get_dependents() {
|
|
local target="$1"
|
|
if [[ ! -f "$DEP_GRAPH" ]]; then
|
|
return
|
|
fi
|
|
nix eval --extra-experimental-features "nix-command flakes" \
|
|
--impure --raw --expr '
|
|
let
|
|
graph = import '"$DEP_GRAPH"';
|
|
names = builtins.attrNames graph;
|
|
dependents = builtins.filter (n: builtins.elem "'"$target"'" graph.${n}.deps) names;
|
|
in builtins.concatStringsSep "\n" dependents
|
|
' 2>/dev/null
|
|
}
|
|
|
|
# ── Commands ─────────────────────────────────────────────────────────────────
|
|
|
|
cmd_init() {
|
|
wants_help "$@" && show_help "Usage: ws init [jobs]
|
|
|
|
Clone all submodules under repos/.
|
|
|
|
Arguments:
|
|
jobs Number of parallel clone jobs (default: 4)"
|
|
|
|
log "${BOLD}Initializing workspace submodules...${NC}"
|
|
local jobs="${1:-4}"
|
|
|
|
# Add submodules that don't exist yet
|
|
local count=0
|
|
local total=${#REPOS[@]}
|
|
|
|
for entry in "${REPOS[@]}"; do
|
|
local dir_name git_url
|
|
dir_name=$(get_field "$entry" 2)
|
|
git_url=$(get_field "$entry" 3)
|
|
count=$((count + 1))
|
|
|
|
if [[ -d "$REPOS_DIR/$dir_name/.git" ]] || [[ -f "$REPOS_DIR/$dir_name/.git" ]]; then
|
|
log " [${count}/${total}] ${GREEN}exists${NC} $dir_name"
|
|
continue
|
|
fi
|
|
|
|
log " [${count}/${total}] ${YELLOW}cloning${NC} $dir_name ..."
|
|
git -C "$WORKSPACE_ROOT" submodule add --depth 1 "$git_url" "repos/$dir_name" 2>/dev/null || \
|
|
echo -e " ${RED}failed${NC} $dir_name (may already be in .gitmodules)"
|
|
done
|
|
|
|
log ""
|
|
log "${BOLD}Initializing and updating submodules...${NC}"
|
|
git -C "$WORKSPACE_ROOT" submodule update --init --depth 1 --jobs "$jobs"
|
|
|
|
log ""
|
|
log "${GREEN}Done.${NC} $(ls -d "$REPOS_DIR"/*/ 2>/dev/null | wc -l) repos under repos/"
|
|
}
|
|
|
|
cmd_build() {
|
|
wants_help "$@" && show_help "Usage: ws build <repo> [options]
|
|
|
|
Build a repo via nix.
|
|
|
|
Options:
|
|
--auto-local, -a Auto-detect dirty repos and use as local overrides
|
|
--local, -l <repo1> <repo2> Use specific repos as local overrides
|
|
|
|
Examples:
|
|
ws build logos-app-poc
|
|
ws build logos-app-poc --auto-local
|
|
ws build logos-app-poc --local logos-cpp-sdk logos-liblogos"
|
|
|
|
local repo="$1"; shift
|
|
local auto_local=false
|
|
local local_overrides=()
|
|
local extra_args=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--auto-local|-a)
|
|
auto_local=true; shift ;;
|
|
--local|-l)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local_overrides+=("$1"); shift
|
|
done ;;
|
|
*)
|
|
extra_args+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
# Resolve input name
|
|
local input_name
|
|
input_name=$(get_input_name "$repo")
|
|
if [[ -z "$input_name" ]]; then
|
|
echo -e "${RED}Error:${NC} repo '$repo' has no flake input (non-flake repo)" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Auto-detect dirty repos
|
|
if $auto_local; then
|
|
for dir in $(get_dirty_repos); do
|
|
local name
|
|
name=$(get_input_name "$dir")
|
|
if [[ -n "$name" ]]; then
|
|
local_overrides+=("$dir")
|
|
fi
|
|
done
|
|
fi
|
|
|
|
# Build override flags
|
|
local flags=()
|
|
if [[ ${#local_overrides[@]} -gt 0 ]]; then
|
|
local unique_overrides
|
|
unique_overrides=($(printf '%s\n' "${local_overrides[@]}" | sort -u))
|
|
log "${CYAN}Local overrides:${NC}"
|
|
for dir in "${unique_overrides[@]}"; do
|
|
log " ${YELLOW}*${NC} $dir → path:$REPOS_DIR/$dir"
|
|
done
|
|
log ""
|
|
flags=($(build_override_flags "${unique_overrides[@]}"))
|
|
fi
|
|
|
|
log "${BOLD}Building ${CYAN}$input_name${NC}${BOLD}...${NC}"
|
|
exec nix build \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
"$WORKSPACE_ROOT#$input_name" \
|
|
${flags[@]+"${flags[@]}"} \
|
|
${extra_args[@]+"${extra_args[@]}"}
|
|
}
|
|
|
|
cmd_run() {
|
|
wants_help "$@" && show_help "Usage: ws run <repo> [options]
|
|
|
|
Build and run a repo via nix.
|
|
|
|
Options:
|
|
--auto-local, -a Auto-detect dirty repos and use as local overrides
|
|
--local, -l <repo1> <repo2> Use specific repos as local overrides
|
|
|
|
Examples:
|
|
ws run logos-app-poc
|
|
ws run logos-app-poc --auto-local
|
|
ws run logos-app-poc --local logos-cpp-sdk"
|
|
|
|
local repo="$1"; shift
|
|
local input_name
|
|
input_name=$(get_input_name "$repo")
|
|
if [[ -z "$input_name" ]]; then
|
|
echo -e "${RED}Error:${NC} repo '$repo' has no flake input" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local auto_local=false
|
|
local local_overrides=()
|
|
local extra_args=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--auto-local|-a) auto_local=true; shift ;;
|
|
--local|-l)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local_overrides+=("$1"); shift
|
|
done ;;
|
|
*)
|
|
extra_args+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
if $auto_local; then
|
|
for dir in $(get_dirty_repos); do
|
|
local name
|
|
name=$(get_input_name "$dir")
|
|
[[ -n "$name" ]] && local_overrides+=("$dir")
|
|
done
|
|
fi
|
|
|
|
local flags=()
|
|
if [[ ${#local_overrides[@]} -gt 0 ]]; then
|
|
local unique=($(printf '%s\n' "${local_overrides[@]}" | sort -u))
|
|
log "${CYAN}Local overrides:${NC}"
|
|
for dir in "${unique[@]}"; do
|
|
log " ${YELLOW}*${NC} $dir"
|
|
done
|
|
log ""
|
|
flags=($(build_override_flags "${unique[@]}"))
|
|
fi
|
|
|
|
log "${BOLD}Running ${CYAN}$input_name${NC}${BOLD}...${NC}"
|
|
|
|
# For app repos, build first and check for Qt conflicts before launching.
|
|
case "$input_name" in
|
|
logos-app-poc|logos-app|logos-basecamp)
|
|
local store_path
|
|
store_path=$(nix build --extra-experimental-features "nix-command flakes" \
|
|
--no-link --print-out-paths "$WORKSPACE_ROOT#$input_name" \
|
|
${flags[@]+"${flags[@]}"}) || exit $?
|
|
|
|
# Check closure for multiple Qt versions
|
|
local qt_versions
|
|
qt_versions=$(nix-store -qR "$store_path" 2>/dev/null | grep -oE 'qtbase-[0-9]+\.[0-9]+\.[0-9]+' | sort -u || true)
|
|
local qt_count
|
|
qt_count=$(echo "$qt_versions" | grep -c . 2>/dev/null || true)
|
|
if [[ "$qt_count" -gt 1 ]]; then
|
|
echo -e "${YELLOW}WARNING: Multiple Qt versions found in closure:${NC}" >&2
|
|
echo "$qt_versions" | while read -r v; do echo -e " ${YELLOW}*${NC} $v" >&2; done
|
|
echo -e "${YELLOW}If the app crashes, run: ${BOLD}ws check-qt $repo${NC}" >&2
|
|
fi
|
|
|
|
# Check for stale local module cache
|
|
local app_qt
|
|
app_qt=$(nix-store -qR "$store_path" 2>/dev/null | grep -oE 'qtbase-[0-9]+\.[0-9]+\.[0-9]+' | sort -u | tail -1 || true)
|
|
if [[ -n "$app_qt" ]]; then
|
|
local cache_dir
|
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
cache_dir="$HOME/Library/Application Support/LogosAppNix"
|
|
else
|
|
cache_dir="${XDG_DATA_HOME:-$HOME/.local/share}/LogosAppNix"
|
|
fi
|
|
if [[ -d "$cache_dir" ]]; then
|
|
local cached_qt
|
|
cached_qt=$(find "$cache_dir" -type f \( -name "*.dylib" -o -name "*.so" \) -print0 2>/dev/null \
|
|
| xargs -0 strings 2>/dev/null | grep -oE 'qtbase-[0-9]+\.[0-9]+\.[0-9]+' | sort -u || true)
|
|
if [[ -n "$cached_qt" && "$cached_qt" != *"$app_qt"* ]]; then
|
|
echo -e "${RED}STALE MODULE CACHE: Local modules use $cached_qt but build uses $app_qt${NC}" >&2
|
|
echo -e "${YELLOW}Fix: rm -rf $(printf '%q' "$cache_dir")${NC}" >&2
|
|
echo -e "${YELLOW}Then run again.${NC}" >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
fi
|
|
|
|
# Run the already-built output directly from the store
|
|
exec nix run --extra-experimental-features "nix-command flakes" \
|
|
"$WORKSPACE_ROOT#$input_name" \
|
|
${flags[@]+"${flags[@]}"} \
|
|
${extra_args[@]+"${extra_args[@]}"}
|
|
;;
|
|
*)
|
|
exec nix run \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
"$WORKSPACE_ROOT#$input_name" \
|
|
${flags[@]+"${flags[@]}"} \
|
|
${extra_args[@]+"${extra_args[@]}"}
|
|
;;
|
|
esac
|
|
}
|
|
|
|
cmd_develop() {
|
|
wants_help "$@" && show_help "Usage: ws develop [repo] [options]
|
|
|
|
Enter a nix devShell. Without a repo, enters the workspace shell (zsh + tmux + tools).
|
|
With a repo, enters that repo's own devShell.
|
|
|
|
Options:
|
|
--fresh Force rebuild (clear cached shell environment)
|
|
--auto-local, -a Auto-detect dirty repos and use as local overrides
|
|
--local, -l <repo1> <repo2> Use specific repos as local overrides
|
|
|
|
Examples:
|
|
ws develop Enter the workspace devShell
|
|
ws develop --fresh Rebuild the workspace devShell from scratch
|
|
ws develop logos-cpp-sdk Enter logos-cpp-sdk's own devShell"
|
|
|
|
local repo=""
|
|
local auto_local=false
|
|
local fresh=false
|
|
local local_overrides=()
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--auto-local|-a) auto_local=true; shift ;;
|
|
--fresh) fresh=true; shift ;;
|
|
--local|-l)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local_overrides+=("$1"); shift
|
|
done ;;
|
|
-*) shift ;;
|
|
*) [[ -z "$repo" ]] && repo="$1"; shift ;;
|
|
esac
|
|
done
|
|
|
|
if $auto_local; then
|
|
for dir in $(get_dirty_repos); do
|
|
local name
|
|
name=$(get_input_name "$dir")
|
|
[[ -n "$name" ]] && local_overrides+=("$dir")
|
|
done
|
|
fi
|
|
|
|
local flags=()
|
|
if [[ ${#local_overrides[@]} -gt 0 ]]; then
|
|
local unique=($(printf '%s\n' "${local_overrides[@]}" | sort -u))
|
|
flags=($(build_override_flags "${unique[@]}"))
|
|
fi
|
|
|
|
if [[ -z "$repo" ]]; then
|
|
# Workspace devShell
|
|
# Use cached dev environment to skip slow nix flake evaluation.
|
|
# Cache is invalidated when devshell.nix, flake.nix, or flake.lock change.
|
|
# Use --fresh to force rebuild.
|
|
local cache_dir="$WORKSPACE_ROOT/.dev-shell-cache"
|
|
local cache_env="$cache_dir/dev-env.sh"
|
|
local cache_hash_file="$cache_dir/hash"
|
|
|
|
if $fresh; then
|
|
rm -rf "$cache_dir"
|
|
fi
|
|
|
|
local flake_hash=""
|
|
if command -v shasum &>/dev/null; then
|
|
flake_hash=$(cat "$WORKSPACE_ROOT/nix/devshell.nix" "$WORKSPACE_ROOT/flake.nix" "$WORKSPACE_ROOT/flake.lock" 2>/dev/null | shasum | cut -d' ' -f1)
|
|
fi
|
|
|
|
# Check cache validity
|
|
local use_cache=false
|
|
if [[ ${#flags[@]} -eq 0 && -n "$flake_hash" && -f "$cache_hash_file" && -f "$cache_env" ]]; then
|
|
local cached_hash
|
|
cached_hash=$(cat "$cache_hash_file" 2>/dev/null)
|
|
[[ "$cached_hash" == "$flake_hash" ]] && use_cache=true
|
|
fi
|
|
|
|
if $use_cache; then
|
|
# Fast path: source cached env + run shellHook (sub-second)
|
|
set +eu
|
|
source "$cache_env"
|
|
eval "$shellHook"
|
|
return 0
|
|
fi
|
|
|
|
# Slow path: full nix evaluation
|
|
# Cache the result in background for next time
|
|
if [[ ${#flags[@]} -eq 0 && -n "$flake_hash" ]]; then
|
|
mkdir -p "$cache_dir"
|
|
echo ".dev-shell-cache/" >> "$WORKSPACE_ROOT/.gitignore" 2>/dev/null || true
|
|
nix print-dev-env \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
"$WORKSPACE_ROOT" > "$cache_env.tmp" 2>/dev/null && \
|
|
mv "$cache_env.tmp" "$cache_env" && \
|
|
echo "$flake_hash" > "$cache_hash_file" &
|
|
fi
|
|
|
|
exec nix develop \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
"$WORKSPACE_ROOT" \
|
|
${flags[@]+"${flags[@]}"}
|
|
else
|
|
# Sub-repo devShell — use the repo's own flake directly
|
|
local repo_path="$REPOS_DIR/$repo"
|
|
if [[ ! -d "$repo_path" ]]; then
|
|
echo -e "${RED}Error:${NC} repo '$repo' not found at $repo_path" >&2
|
|
exit 1
|
|
fi
|
|
exec nix develop \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
"path:$repo_path" \
|
|
${flags[@]+"${flags[@]}"}
|
|
fi
|
|
}
|
|
|
|
_detect_system() {
|
|
nix eval --extra-experimental-features "nix-command flakes" \
|
|
--impure --raw --expr builtins.currentSystem 2>/dev/null || echo "x86_64-linux"
|
|
}
|
|
|
|
# Parse a JSON array string like ["a","b","c"] into newline-separated values (no jq needed)
|
|
_json_array_to_lines() {
|
|
echo "$1" | tr -d '[]"' | tr ',' '\n' | sed '/^$/d'
|
|
}
|
|
|
|
# Discover checks and test-like packages in a single nix eval call
|
|
_discover_test_outputs() {
|
|
local repo_path="$1"
|
|
local system="$2"
|
|
|
|
# Single eval: get both checks and test-like packages at once
|
|
nix eval --extra-experimental-features "nix-command flakes" \
|
|
--impure --raw --expr '
|
|
let
|
|
flake = builtins.getFlake "path:'"$repo_path"'";
|
|
checks = builtins.attrNames (flake.checks.'"$system"' or {});
|
|
packages = builtins.attrNames (flake.packages.'"$system"' or {});
|
|
testPkgs = builtins.filter (n: builtins.match ".*[Tt]est.*" n != null) packages;
|
|
in builtins.toJSON { inherit checks testPkgs; }
|
|
' 2>/dev/null || echo '{"checks":[],"testPkgs":[]}'
|
|
}
|
|
|
|
# Run test for a single repo.
|
|
# Priority:
|
|
# 1. checks.<system>.tests (convention: runs tests)
|
|
# 2. Other checks.<system>.* (also run tests)
|
|
# 3. packages.<system>.*test* (test packages — build only)
|
|
# 4. Skip
|
|
_test_one_repo() {
|
|
local input_name="$1"
|
|
local dir_name="$2"
|
|
local system="$3"
|
|
local repo_path="$REPOS_DIR/$dir_name"
|
|
|
|
log "${BOLD}Testing ${CYAN}$dir_name${NC}${BOLD}...${NC}"
|
|
|
|
if [[ ! -d "$repo_path" ]]; then
|
|
echo -e " ${RED}not cloned${NC}"
|
|
return 1
|
|
fi
|
|
|
|
# Fast path: skip repos known to have no tests
|
|
local skip_known=false
|
|
has_tests "$input_name" || skip_known=true
|
|
if $skip_known; then
|
|
log " ${YELLOW}SKIP${NC} no tests (per nix/dep-graph.nix)"
|
|
return 2
|
|
fi
|
|
|
|
# Discover checks + test packages in one eval
|
|
log " ${BLUE}Discovering test outputs...${NC}"
|
|
local outputs
|
|
outputs=$(_discover_test_outputs "$repo_path" "$system")
|
|
|
|
local checks_json test_pkgs_json
|
|
checks_json=$(echo "$outputs" | sed 's/.*"checks":\([^,}]*\).*/\1/')
|
|
test_pkgs_json=$(echo "$outputs" | sed 's/.*"testPkgs":\([^}]*\).*/\1/')
|
|
|
|
local ran_something=false
|
|
local all_pass=true
|
|
|
|
# 1. Run checks (convention: checks.*.tests first, then others)
|
|
local checks
|
|
checks=$(_json_array_to_lines "$checks_json")
|
|
|
|
if [[ -n "$checks" ]]; then
|
|
# If "tests" exists, run it first (convention)
|
|
if echo "$checks" | grep -qx "tests"; then
|
|
ran_something=true
|
|
log " ${YELLOW}check${NC} tests"
|
|
if nix build --extra-experimental-features "nix-command flakes" \
|
|
"path:$repo_path#checks.$system.tests" -L; then
|
|
echo -e " ${GREEN}PASS${NC} tests"
|
|
else
|
|
echo -e " ${RED}FAIL${NC} tests"
|
|
all_pass=false
|
|
fi
|
|
fi
|
|
|
|
# Run any other checks
|
|
while IFS= read -r check; do
|
|
[[ -z "$check" || "$check" == "tests" ]] && continue
|
|
ran_something=true
|
|
log " ${YELLOW}check${NC} $check"
|
|
if nix build --extra-experimental-features "nix-command flakes" \
|
|
"path:$repo_path#checks.$system.$check" -L; then
|
|
echo -e " ${GREEN}PASS${NC} $check"
|
|
else
|
|
echo -e " ${RED}FAIL${NC} $check"
|
|
all_pass=false
|
|
fi
|
|
done <<< "$checks"
|
|
fi
|
|
|
|
# 2. If no checks, try test-like packages (build only)
|
|
if ! $ran_something; then
|
|
while IFS= read -r pkg; do
|
|
[[ -z "$pkg" ]] && continue
|
|
ran_something=true
|
|
log " ${YELLOW}package${NC} $pkg (build only — add checks.*.tests to run)"
|
|
if nix build --extra-experimental-features "nix-command flakes" \
|
|
"path:$repo_path#$pkg" -L; then
|
|
echo -e " ${GREEN}PASS${NC} $pkg"
|
|
else
|
|
echo -e " ${RED}FAIL${NC} $pkg"
|
|
all_pass=false
|
|
fi
|
|
done <<< "$(_json_array_to_lines "$test_pkgs_json")"
|
|
fi
|
|
|
|
# 3. Nothing found despite hasTests=true — metadata may be stale
|
|
if ! $ran_something; then
|
|
log " ${YELLOW}SKIP${NC} hasTests=true in dep-graph.nix but no checks/test packages found"
|
|
return 2
|
|
fi
|
|
|
|
$all_pass
|
|
}
|
|
|
|
cmd_test() {
|
|
wants_help "$@" && show_help "Usage: ws test [repo] [options]
|
|
|
|
Run checks/tests for a repo or all repos.
|
|
|
|
Options:
|
|
--all Test all repos (default if no repo given)
|
|
--type, -t <type> Filter by project type: cpp, rust, nim, js, qml
|
|
|
|
Examples:
|
|
ws test logos-module Test a single repo
|
|
ws test --all Test all repos
|
|
ws test --type cpp Test only C++ repos
|
|
ws test logos-module --type cpp"
|
|
|
|
local target=""
|
|
local test_type=""
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--type|-t) test_type="$2"; shift 2 ;;
|
|
--all) target="--all"; shift ;;
|
|
-*) shift ;; # skip unknown flags
|
|
*)
|
|
# First non-flag arg is the repo name
|
|
[[ -z "$target" ]] && target="$1"
|
|
shift ;;
|
|
esac
|
|
done
|
|
|
|
local system
|
|
system=$(_detect_system)
|
|
|
|
if [[ "$target" == "--all" || -z "$target" ]]; then
|
|
log "${BOLD}Running tests across all repos (system: $system)${NC}"
|
|
log ""
|
|
|
|
local passed=0 failed=0 skipped=0
|
|
|
|
for entry in "${REPOS[@]}"; do
|
|
local input_name dir_name has_flake
|
|
input_name=$(get_field "$entry" 1)
|
|
dir_name=$(get_field "$entry" 2)
|
|
has_flake=$(get_field "$entry" 4)
|
|
|
|
if [[ "$has_flake" != "yes" || -z "$input_name" ]]; then
|
|
skipped=$((skipped + 1))
|
|
continue
|
|
fi
|
|
|
|
# Filter by type if specified
|
|
if [[ -n "$test_type" ]]; then
|
|
local repo_path="$REPOS_DIR/$dir_name"
|
|
local matches=false
|
|
case "$test_type" in
|
|
cpp|cmake)
|
|
[[ -f "$repo_path/CMakeLists.txt" ]] && matches=true ;;
|
|
rust|cargo)
|
|
[[ -f "$repo_path/Cargo.toml" ]] && matches=true ;;
|
|
nim|nimble)
|
|
ls "$repo_path"/*.nimble >/dev/null 2>&1 && matches=true ;;
|
|
js|node|npm)
|
|
[[ -f "$repo_path/package.json" ]] && matches=true ;;
|
|
qml)
|
|
ls "$repo_path"/*.qml >/dev/null 2>&1 && matches=true
|
|
[[ -f "$repo_path/CMakeLists.txt" ]] && grep -q "qml" "$repo_path/CMakeLists.txt" 2>/dev/null && matches=true ;;
|
|
*)
|
|
echo -e "${RED}Unknown type: $test_type${NC}" >&2
|
|
echo "Available types: cpp, rust, nim, js, qml" >&2
|
|
exit 1 ;;
|
|
esac
|
|
if ! $matches; then
|
|
skipped=$((skipped + 1))
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
local rc=0
|
|
_test_one_repo "$input_name" "$dir_name" "$system" || rc=$?
|
|
if [[ $rc -eq 0 ]]; then
|
|
passed=$((passed + 1))
|
|
elif [[ $rc -eq 2 ]]; then
|
|
skipped=$((skipped + 1))
|
|
else
|
|
failed=$((failed + 1))
|
|
fi
|
|
log ""
|
|
done
|
|
|
|
echo -e "${BOLD}Results:${NC} ${GREEN}$passed passed${NC} ${RED}$failed failed${NC} ${YELLOW}$skipped skipped${NC}"
|
|
[[ $failed -eq 0 ]] || exit 1
|
|
else
|
|
# Single repo
|
|
local input_name dir_name
|
|
input_name=$(get_input_name "$target")
|
|
dir_name="$target"
|
|
|
|
if [[ -z "$input_name" ]]; then
|
|
echo -e "${RED}Error:${NC} '$target' has no flake input" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local rc=0
|
|
_test_one_repo "$input_name" "$dir_name" "$system" || rc=$?
|
|
if [[ $rc -eq 1 ]]; then exit 1; fi
|
|
fi
|
|
}
|
|
|
|
cmd_status() {
|
|
wants_help "$@" && show_help "Usage: ws status
|
|
|
|
Show git status across all repos — branch, dirty/clean, ahead/behind.
|
|
|
|
Examples:
|
|
ws status"
|
|
|
|
log "${BOLD}Workspace status${NC}"
|
|
log ""
|
|
|
|
local dirty=0 clean=0 missing=0
|
|
|
|
for entry in "${REPOS[@]}"; do
|
|
local dir_name has_flake
|
|
dir_name=$(get_field "$entry" 2)
|
|
has_flake=$(get_field "$entry" 4)
|
|
local repo_path="$REPOS_DIR/$dir_name"
|
|
|
|
if [[ ! -d "$repo_path" ]]; then
|
|
echo -e " ${RED}missing${NC} $dir_name"
|
|
missing=$((missing + 1))
|
|
continue
|
|
fi
|
|
|
|
if [[ ! -d "$repo_path/.git" && ! -f "$repo_path/.git" ]]; then
|
|
echo -e " ${RED}no-git${NC} $dir_name"
|
|
missing=$((missing + 1))
|
|
continue
|
|
fi
|
|
|
|
local status
|
|
status=$(git -C "$repo_path" status --porcelain 2>/dev/null)
|
|
local branch
|
|
branch=$(git -C "$repo_path" rev-parse --abbrev-ref HEAD 2>/dev/null || echo "???")
|
|
local ahead_behind
|
|
ahead_behind=$(git -C "$repo_path" rev-list --left-right --count '@{upstream}...HEAD' 2>/dev/null || echo "")
|
|
|
|
if [[ -n "$status" ]]; then
|
|
local modified added untracked
|
|
modified=$(echo "$status" | grep -c '^ M\| ^M' 2>/dev/null || echo 0)
|
|
added=$(echo "$status" | grep -c '^A ' 2>/dev/null || echo 0)
|
|
untracked=$(echo "$status" | grep -c '^??' 2>/dev/null || echo 0)
|
|
echo -e " ${YELLOW}dirty${NC} $dir_name ${BLUE}($branch)${NC} [M:$modified A:$added ?:$untracked]"
|
|
dirty=$((dirty + 1))
|
|
else
|
|
local marker="${GREEN}clean${NC}"
|
|
if [[ -n "$ahead_behind" ]]; then
|
|
local behind ahead
|
|
behind=$(echo "$ahead_behind" | awk '{print $1}')
|
|
ahead=$(echo "$ahead_behind" | awk '{print $2}')
|
|
if [[ "$ahead" -gt 0 ]]; then
|
|
marker="${GREEN}clean${NC} ${CYAN}+${ahead}${NC}"
|
|
fi
|
|
fi
|
|
echo -e " $marker $dir_name ${BLUE}($branch)${NC}"
|
|
clean=$((clean + 1))
|
|
fi
|
|
done
|
|
|
|
echo ""
|
|
echo -e " ${GREEN}$clean clean${NC} ${YELLOW}$dirty dirty${NC} ${RED}$missing missing${NC}"
|
|
}
|
|
|
|
cmd_dirty() {
|
|
wants_help "$@" && show_help "Usage: ws dirty
|
|
|
|
Show repos with uncommitted changes and what downstream repos they affect.
|
|
|
|
Examples:
|
|
ws dirty"
|
|
|
|
log "${BOLD}Repos with local changes:${NC}"
|
|
log ""
|
|
|
|
local dirty_repos
|
|
dirty_repos=($(get_dirty_repos))
|
|
|
|
if [[ ${#dirty_repos[@]} -eq 0 ]]; then
|
|
echo -e " ${GREEN}All repos are clean.${NC}"
|
|
return
|
|
fi
|
|
|
|
for dir_name in "${dirty_repos[@]}"; do
|
|
local input_name
|
|
input_name=$(get_input_name "$dir_name")
|
|
echo -e " ${YELLOW}*${NC} ${BOLD}$dir_name${NC}"
|
|
|
|
# Show what would be affected
|
|
local dependents
|
|
dependents=$(get_dependents "${input_name:-$dir_name}")
|
|
if [[ -n "$dependents" ]]; then
|
|
echo -e " affects: $(echo "$dependents" | tr '\n' ' ')"
|
|
fi
|
|
done
|
|
|
|
log ""
|
|
log "Use ${CYAN}ws build <repo> --auto-local${NC} to build with these overrides."
|
|
}
|
|
|
|
cmd_graph() {
|
|
wants_help "$@" && show_help "Usage: ws graph [repo]
|
|
|
|
Show the dependency graph. With a repo, shows its deps and dependents.
|
|
Without a repo, outputs the full graph in DOT format.
|
|
|
|
Examples:
|
|
ws graph logos-app-poc Show deps and dependents of a repo
|
|
ws graph Output full DOT graph
|
|
ws graph | dot -Tsvg > g.svg Render as SVG (requires graphviz)"
|
|
|
|
local target="${1:-}"
|
|
|
|
if [[ ! -f "$DEP_GRAPH" ]]; then
|
|
echo -e "${RED}Error:${NC} dep-graph.nix not found" >&2
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -n "$target" ]]; then
|
|
log "${BOLD}Dependencies of ${CYAN}$target${NC}${BOLD}:${NC}"
|
|
nix eval --extra-experimental-features "nix-command flakes" --impure --raw --expr '
|
|
let graph = import '"$DEP_GRAPH"';
|
|
in builtins.concatStringsSep "\n" ((graph.${"'"$target"'"} or { deps = []; }).deps or [])
|
|
' 2>/dev/null | while IFS= read -r dep || [[ -n "$dep" ]]; do
|
|
[[ -n "$dep" ]] && echo -e " -> $dep"
|
|
done
|
|
|
|
log ""
|
|
log "${BOLD}Dependents (repos that depend on ${CYAN}$target${NC}${BOLD}):${NC}"
|
|
nix eval --extra-experimental-features "nix-command flakes" --impure --raw --expr '
|
|
let
|
|
graph = import '"$DEP_GRAPH"';
|
|
names = builtins.attrNames graph;
|
|
dependents = builtins.filter (n: builtins.elem "'"$target"'" graph.${n}.deps) names;
|
|
in builtins.concatStringsSep "\n" dependents
|
|
' 2>/dev/null | while IFS= read -r dep || [[ -n "$dep" ]]; do
|
|
[[ -n "$dep" ]] && echo -e " <- $dep"
|
|
done
|
|
else
|
|
# Full graph in DOT format
|
|
nix eval --extra-experimental-features "nix-command flakes" --impure --raw --expr '
|
|
let
|
|
graph = import '"$DEP_GRAPH"';
|
|
names = builtins.attrNames graph;
|
|
edges = builtins.concatMap (k:
|
|
map (dep: " \"" + dep + "\" -> \"" + k + "\";") graph.${k}.deps
|
|
) names;
|
|
in "digraph logos {\n rankdir=BT;\n node [shape=box, fontsize=10];\n\n"
|
|
+ builtins.concatStringsSep "\n" edges
|
|
+ "\n}"
|
|
' 2>/dev/null
|
|
echo ""
|
|
$QUIET || echo -e "${CYAN}Pipe to 'dot -Tsvg > graph.svg' for visualization${NC}" >&2
|
|
fi
|
|
}
|
|
|
|
cmd_override_inputs() {
|
|
wants_help "$@" && show_help "Usage: ws override-inputs <repo> [options]
|
|
|
|
Print the nix --override-input flags for building a repo with local overrides.
|
|
|
|
Options:
|
|
--auto-local, -a Auto-detect dirty repos and use as local overrides
|
|
--local, -l <repo1> <repo2> Use specific repos as local overrides
|
|
|
|
Examples:
|
|
ws override-inputs logos-app-poc --auto-local
|
|
ws override-inputs logos-app-poc --local logos-cpp-sdk"
|
|
|
|
local repo="$1"; shift || true
|
|
local auto_local=false
|
|
local local_overrides=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--auto-local|-a) auto_local=true; shift ;;
|
|
--local|-l)
|
|
shift
|
|
while [[ $# -gt 0 && "$1" != -* ]]; do
|
|
local_overrides+=("$1"); shift
|
|
done ;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
|
|
if $auto_local; then
|
|
for dir in $(get_dirty_repos); do
|
|
local name
|
|
name=$(get_input_name "$dir")
|
|
[[ -n "$name" ]] && local_overrides+=("$dir")
|
|
done
|
|
fi
|
|
|
|
if [[ ${#local_overrides[@]} -eq 0 ]]; then
|
|
echo -e "${YELLOW}No local overrides specified.${NC}"
|
|
echo "Use --auto-local or --local <repo1> <repo2> ..."
|
|
return
|
|
fi
|
|
|
|
local unique=($(printf '%s\n' "${local_overrides[@]}" | sort -u))
|
|
|
|
log "${BOLD}Override flags for building ${CYAN}$repo${NC}${BOLD}:${NC}"
|
|
log ""
|
|
echo -n "nix build .#$repo"
|
|
for dir in "${unique[@]}"; do
|
|
local input_name
|
|
input_name=$(get_input_name "$dir")
|
|
echo -n " \\"
|
|
echo ""
|
|
echo -n " --override-input $input_name path:./repos/$dir"
|
|
done
|
|
echo ""
|
|
}
|
|
|
|
cmd_loc() {
|
|
wants_help "$@" && show_help "Usage: ws loc [repo] [options]
|
|
|
|
Count lines of code using tokei. Without a repo, counts the whole workspace.
|
|
|
|
Options:
|
|
--all Per-repo breakdown sorted by LOC
|
|
--no-nix, --code-only Exclude .nix, .lock, .json, .md files
|
|
|
|
Examples:
|
|
ws loc Count all code in workspace
|
|
ws loc logos-cpp-sdk Count code in a single repo
|
|
ws loc --all Per-repo breakdown
|
|
ws loc --all --no-nix Per-repo breakdown, code only"
|
|
|
|
local target="${1:-}"
|
|
shift || true
|
|
|
|
local exclude_nix=false
|
|
local repos_list=()
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--no-nix) exclude_nix=true; shift ;;
|
|
--code-only) exclude_nix=true; shift ;;
|
|
*) repos_list+=("$1"); shift ;;
|
|
esac
|
|
done
|
|
|
|
# Also check if first arg was a flag
|
|
case "$target" in
|
|
--no-nix|--code-only) exclude_nix=true; target="" ;;
|
|
--all) target="--all" ;;
|
|
esac
|
|
|
|
if ! command -v tokei &>/dev/null; then
|
|
echo -e "${YELLOW}tokei not found, installing...${NC}" >&2
|
|
nix profile install --extra-experimental-features "nix-command flakes" nixpkgs#tokei 2>/dev/null || {
|
|
echo -e "${RED}Error:${NC} could not install tokei. Run 'ws develop' or install tokei manually." >&2
|
|
exit 1
|
|
}
|
|
fi
|
|
|
|
local tokei_args=()
|
|
if $exclude_nix; then
|
|
tokei_args+=(-e '*.nix' -e '*.lock' -e 'flake.lock' -e '*.json' -e '*.md')
|
|
fi
|
|
|
|
if [[ "$target" == "--all" ]]; then
|
|
# Per-repo breakdown
|
|
local total_header_shown=false
|
|
for entry in "${REPOS[@]}"; do
|
|
local dir_name
|
|
dir_name=$(get_field "$entry" 2)
|
|
local repo_path="$REPOS_DIR/$dir_name"
|
|
[[ -d "$repo_path" ]] || continue
|
|
|
|
local count
|
|
count=$(tokei "$repo_path" ${tokei_args[@]+"${tokei_args[@]}"} --output json 2>/dev/null | \
|
|
sed -n 's/.*"Total":{[^}]*"code":\([0-9]*\).*/\1/p' 2>/dev/null || echo "0")
|
|
[[ -z "$count" ]] && count=0
|
|
|
|
if [[ "$count" -gt 0 ]]; then
|
|
printf " ${CYAN}%7s${NC} %s\n" "$count" "$dir_name"
|
|
fi
|
|
done | sort -rn -k1
|
|
|
|
echo ""
|
|
log "${BOLD}Total across all repos:${NC}"
|
|
tokei "$REPOS_DIR" ${tokei_args[@]+"${tokei_args[@]}"}
|
|
|
|
elif [[ -n "$target" ]]; then
|
|
# Single repo
|
|
local repo_path="$REPOS_DIR/$target"
|
|
if [[ ! -d "$repo_path" ]]; then
|
|
echo -e "${RED}Error:${NC} repo '$target' not found" >&2
|
|
exit 1
|
|
fi
|
|
tokei "$repo_path" ${tokei_args[@]+"${tokei_args[@]}"}
|
|
|
|
else
|
|
# Whole workspace
|
|
tokei "$REPOS_DIR" ${tokei_args[@]+"${tokei_args[@]}"}
|
|
fi
|
|
}
|
|
|
|
cmd_watch() {
|
|
wants_help "$@" && show_help "Usage: ws watch <test|build|run> [args]
|
|
|
|
Auto-run commands on file changes using watchexec.
|
|
|
|
Subcommands:
|
|
test <repo> [opts] Re-run tests when repo files change
|
|
build <repo> [opts] Re-build when repo files change
|
|
run <cmd> [-w repo ...] Run arbitrary command on file changes
|
|
|
|
Examples:
|
|
ws watch test logos-module
|
|
ws watch build logos-app-poc --auto-local
|
|
ws watch run 'ws test logos-module' -w logos-module -w logos-cpp-sdk"
|
|
|
|
local subcmd="${1:-}"
|
|
shift || true
|
|
|
|
if ! command -v watchexec &>/dev/null; then
|
|
echo -e "${YELLOW}watchexec not found, installing...${NC}" >&2
|
|
nix profile install --extra-experimental-features "nix-command flakes" nixpkgs#watchexec 2>/dev/null || {
|
|
echo -e "${RED}Error:${NC} could not install watchexec. Run 'ws develop' or install watchexec manually." >&2
|
|
exit 1
|
|
}
|
|
fi
|
|
|
|
case "$subcmd" in
|
|
test)
|
|
local repo="${1:-}"
|
|
if [[ -z "$repo" ]]; then
|
|
echo -e "${RED}Usage:${NC} ws watch test <repo> [ws-test-opts]" >&2
|
|
exit 1
|
|
fi
|
|
shift
|
|
local repo_path="$REPOS_DIR/$repo"
|
|
if [[ ! -d "$repo_path" ]]; then
|
|
echo -e "${RED}Error:${NC} repo '$repo' not found" >&2
|
|
exit 1
|
|
fi
|
|
log "${BOLD}Watching ${CYAN}$repo${NC}${BOLD} for changes — running tests on save${NC}"
|
|
exec watchexec -w "$repo_path" --restart -- \
|
|
"$WORKSPACE_ROOT/scripts/ws" test "$repo" "$@"
|
|
;;
|
|
|
|
build)
|
|
local repo="${1:-}"
|
|
if [[ -z "$repo" ]]; then
|
|
echo -e "${RED}Usage:${NC} ws watch build <repo> [ws-build-opts]" >&2
|
|
exit 1
|
|
fi
|
|
shift
|
|
local repo_path="$REPOS_DIR/$repo"
|
|
if [[ ! -d "$repo_path" ]]; then
|
|
echo -e "${RED}Error:${NC} repo '$repo' not found" >&2
|
|
exit 1
|
|
fi
|
|
log "${BOLD}Watching ${CYAN}$repo${NC}${BOLD} for changes — building on save${NC}"
|
|
exec watchexec -w "$repo_path" --restart -- \
|
|
"$WORKSPACE_ROOT/scripts/ws" build "$repo" "$@"
|
|
;;
|
|
|
|
run)
|
|
local cmd="${1:-}"
|
|
if [[ -z "$cmd" ]]; then
|
|
echo -e "${RED}Usage:${NC} ws watch run <command> [-w repo1 -w repo2 ...]" >&2
|
|
echo " Runs <command> whenever watched files change." >&2
|
|
echo "" >&2
|
|
echo "Examples:" >&2
|
|
echo " ws watch run 'ws test logos-module --auto-local' -w logos-module -w logos-cpp-sdk" >&2
|
|
echo " ws watch run 'echo changed' -w logos-liblogos" >&2
|
|
exit 1
|
|
fi
|
|
shift
|
|
|
|
# Collect -w flags and convert repo names to paths
|
|
local watch_args=()
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-w)
|
|
local repo_name="$2"
|
|
local rpath="$REPOS_DIR/$repo_name"
|
|
if [[ -d "$rpath" ]]; then
|
|
watch_args+=(-w "$rpath")
|
|
else
|
|
watch_args+=(-w "$repo_name")
|
|
fi
|
|
shift 2
|
|
;;
|
|
*) shift ;;
|
|
esac
|
|
done
|
|
|
|
if [[ ${#watch_args[@]} -eq 0 ]]; then
|
|
watch_args=(-w "$REPOS_DIR")
|
|
fi
|
|
|
|
log "${BOLD}Watching for changes...${NC}"
|
|
exec watchexec "${watch_args[@]}" --restart -- bash -c "$cmd"
|
|
;;
|
|
|
|
*)
|
|
echo -e "${RED}Usage:${NC} ws watch <test|build|run>" >&2
|
|
echo "" >&2
|
|
echo " test <repo> [opts] Re-run tests on file changes" >&2
|
|
echo " build <repo> [opts] Re-build on file changes" >&2
|
|
echo " run <cmd> [-w repo...] Run arbitrary command on file changes" >&2
|
|
echo "" >&2
|
|
echo "Examples:" >&2
|
|
echo " ws watch test logos-module" >&2
|
|
echo " ws watch build logos-app-poc --auto-local" >&2
|
|
echo " ws watch run 'ws test logos-module --auto-local' -w logos-module -w logos-cpp-sdk" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
cmd_foreach() {
|
|
wants_help "$@" && show_help "Usage: ws foreach <command>
|
|
|
|
Run a command in every cloned repo.
|
|
|
|
When the command is a single quoted string with spaces, it runs via bash -c
|
|
(so shell operators like && and || work).
|
|
|
|
Examples:
|
|
ws foreach git status -s
|
|
ws foreach 'git add . && git commit -m \"update\"'
|
|
ws foreach pwd"
|
|
|
|
local cmd=("$@")
|
|
|
|
if [[ ${#cmd[@]} -eq 0 ]]; then
|
|
echo -e "${RED}Usage:${NC} ws foreach <command>" >&2
|
|
exit 1
|
|
fi
|
|
|
|
for entry in "${REPOS[@]}"; do
|
|
local dir_name
|
|
dir_name=$(get_field "$entry" 2)
|
|
local repo_path="$REPOS_DIR/$dir_name"
|
|
|
|
if [[ ! -d "$repo_path" ]]; then
|
|
continue
|
|
fi
|
|
|
|
log "${BOLD}── $dir_name ──${NC}"
|
|
(cd "$repo_path" && if [[ ${#cmd[@]} -eq 1 && "${cmd[0]}" == *" "* ]]; then bash -c "${cmd[0]}"; else "${cmd[@]}"; fi) || true
|
|
log ""
|
|
done
|
|
}
|
|
|
|
cmd_worktree() {
|
|
wants_help "$@" && show_help "Usage: ws worktree <add|list|remove>
|
|
|
|
Manage workspace worktrees for isolated multi-repo feature branches.
|
|
|
|
Subcommands:
|
|
add <name> [-b branch] Create a worktree with submodules and ws/<branch> branches
|
|
list List all worktrees
|
|
remove <name> Remove a worktree
|
|
|
|
Examples:
|
|
ws worktree add my-feature
|
|
ws worktree add my-feature -b custom-branch
|
|
ws worktree list
|
|
ws worktree remove my-feature"
|
|
|
|
local subcmd="${1:-}"
|
|
shift || true
|
|
|
|
case "$subcmd" in
|
|
add)
|
|
local name=""
|
|
local branch=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
-b) branch="$2"; shift 2 ;;
|
|
*) name="$1"; shift ;;
|
|
esac
|
|
done
|
|
|
|
if [[ -z "$name" ]]; then
|
|
echo -e "${RED}Usage:${NC} ws worktree add <name> [-b branch]" >&2
|
|
echo " Creates a workspace worktree with submodules initialized." >&2
|
|
echo " Each subrepo gets a ws/<branch> branch." >&2
|
|
exit 1
|
|
fi
|
|
|
|
branch="${branch:-$name}"
|
|
local wt_path="$WORKSPACE_ROOT/../logos-workspace--$name"
|
|
|
|
if [[ -d "$wt_path" ]]; then
|
|
echo -e "${RED}Worktree already exists:${NC} $wt_path" >&2
|
|
exit 1
|
|
fi
|
|
|
|
log "${BOLD}Creating workspace worktree...${NC}"
|
|
git -C "$WORKSPACE_ROOT" worktree add "$wt_path" -b "$branch"
|
|
|
|
log "${BOLD}Initializing submodules...${NC}"
|
|
(cd "$wt_path" && git submodule update --init --depth 1 --jobs 4)
|
|
|
|
local repo_branch="ws/$branch"
|
|
log "${BOLD}Creating branch ${CYAN}$repo_branch${NC}${BOLD} in all repos...${NC}"
|
|
for entry in "${REPOS[@]}"; do
|
|
local dir_name
|
|
dir_name=$(get_field "$entry" 2)
|
|
local repo_path="$wt_path/repos/$dir_name"
|
|
if [[ -d "$repo_path/.git" ]] || [[ -f "$repo_path/.git" ]]; then
|
|
(cd "$repo_path" && git checkout -b "$repo_branch" 2>/dev/null && \
|
|
log " ${GREEN}✓${NC} $dir_name") || \
|
|
log " ${YELLOW}skip${NC} $dir_name (branch may already exist)"
|
|
fi
|
|
done
|
|
|
|
log ""
|
|
echo -e "${GREEN}Worktree ready:${NC} $wt_path"
|
|
echo -e " Workspace branch: ${CYAN}$branch${NC}"
|
|
echo -e " Subrepo branches: ${CYAN}$repo_branch${NC}"
|
|
;;
|
|
|
|
list)
|
|
git -C "$WORKSPACE_ROOT" worktree list
|
|
;;
|
|
|
|
remove)
|
|
local name="${1:-}"
|
|
if [[ -z "$name" ]]; then
|
|
echo -e "${RED}Usage:${NC} ws worktree remove <name>" >&2
|
|
exit 1
|
|
fi
|
|
|
|
local wt_path="$WORKSPACE_ROOT/../logos-workspace--$name"
|
|
|
|
if [[ ! -d "$wt_path" ]]; then
|
|
echo -e "${RED}Worktree not found:${NC} $wt_path" >&2
|
|
exit 1
|
|
fi
|
|
|
|
log "${BOLD}Removing worktree...${NC}"
|
|
git -C "$WORKSPACE_ROOT" worktree remove "$wt_path" --force
|
|
echo -e "${GREEN}Removed:${NC} $wt_path"
|
|
;;
|
|
|
|
*)
|
|
echo -e "${RED}Usage:${NC} ws worktree <add|list|remove>" >&2
|
|
echo " add <name> [-b branch] Create a worktree with submodules and branches" >&2
|
|
echo " list List all worktrees" >&2
|
|
echo " remove <name> Remove a worktree" >&2
|
|
exit 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
cmd_update() {
|
|
wants_help "$@" && show_help "Usage: ws update [repo]
|
|
|
|
Update flake.lock inputs. Without a repo, updates all inputs.
|
|
|
|
Examples:
|
|
ws update Update all flake inputs
|
|
ws update logos-cpp-sdk Update a single input"
|
|
|
|
local target="${1:-}"
|
|
|
|
if [[ "$target" == "--all" || -z "$target" ]]; then
|
|
log "${BOLD}Updating all flake inputs...${NC}"
|
|
nix flake update \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
"$WORKSPACE_ROOT"
|
|
else
|
|
local input_name
|
|
input_name=$(get_input_name "$target")
|
|
log "${BOLD}Updating ${CYAN}$input_name${NC}${BOLD}...${NC}"
|
|
nix flake lock \
|
|
--extra-experimental-features "nix-command flakes" \
|
|
--update-input "$input_name" \
|
|
"$WORKSPACE_ROOT"
|
|
fi
|
|
}
|
|
|
|
cmd_list() {
|
|
wants_help "$@" && show_help "Usage: ws list
|
|
|
|
List all repos in the workspace with their flake and clone status.
|
|
|
|
Examples:
|
|
ws list"
|
|
|
|
log "${BOLD}Workspace repos (${#REPOS[@]} total)${NC}"
|
|
log ""
|
|
|
|
local current_section=""
|
|
for entry in "${REPOS[@]}"; do
|
|
local input_name dir_name has_flake
|
|
input_name=$(get_field "$entry" 1)
|
|
dir_name=$(get_field "$entry" 2)
|
|
has_flake=$(get_field "$entry" 4)
|
|
|
|
local flake_marker=""
|
|
if [[ "$has_flake" == "yes" ]]; then
|
|
flake_marker="${GREEN}flake${NC}"
|
|
else
|
|
flake_marker="${YELLOW}no-flake${NC}"
|
|
fi
|
|
|
|
local exists_marker=""
|
|
if [[ -d "$REPOS_DIR/$dir_name/.git" ]] || [[ -f "$REPOS_DIR/$dir_name/.git" ]]; then
|
|
exists_marker="${GREEN}cloned${NC}"
|
|
else
|
|
exists_marker="${RED}not-cloned${NC}"
|
|
fi
|
|
|
|
printf " %-35s %-15b %b\n" "$dir_name" "$flake_marker" "$exists_marker"
|
|
done
|
|
}
|
|
|
|
cmd_sync_graph() {
|
|
wants_help "$@" && show_help "Usage: ws sync-graph
|
|
|
|
Regenerate nix/dep-graph.nix by scanning each repo's flake.nix for
|
|
workspace dependencies and test outputs.
|
|
|
|
Examples:
|
|
ws sync-graph"
|
|
|
|
log "${BOLD}Syncing nix/dep-graph.nix from repo flake.nix files...${NC}"
|
|
log ""
|
|
|
|
# Build list of workspace input names
|
|
local ws_names=()
|
|
for entry in "${REPOS[@]}"; do
|
|
local iname
|
|
iname=$(get_field "$entry" 1)
|
|
[[ -n "$iname" ]] && ws_names+=("$iname")
|
|
done
|
|
|
|
# Find max name length for alignment
|
|
local max_len=0
|
|
for name in "${ws_names[@]}"; do
|
|
[[ ${#name} -gt $max_len ]] && max_len=${#name}
|
|
done
|
|
|
|
local tmpfile
|
|
tmpfile=$(mktemp)
|
|
|
|
cat > "$tmpfile" << 'HEADER'
|
|
# Dependency graph: flake input name → { deps, hasTests }
|
|
#
|
|
# deps: list of workspace input names this repo depends on
|
|
# (only logos-co deps; external deps like go-wallet-sdk are omitted)
|
|
# hasTests: whether this repo exposes checks or test packages in its flake
|
|
# Used by `ws test` to skip expensive nix eval for repos without tests.
|
|
#
|
|
# AUTO-GENERATED by `ws sync-graph`. To update: ws sync-graph
|
|
#
|
|
# If you add a `checks` output to a repo's flake.nix, re-run this command
|
|
# so that `ws test --all` picks it up.
|
|
{
|
|
HEADER
|
|
|
|
for entry in "${REPOS[@]}"; do
|
|
local input_name dir_name has_flake
|
|
input_name=$(get_field "$entry" 1)
|
|
dir_name=$(get_field "$entry" 2)
|
|
has_flake=$(get_field "$entry" 4)
|
|
|
|
[[ "$has_flake" != "yes" || -z "$input_name" ]] && continue
|
|
|
|
local flake_file="$REPOS_DIR/$dir_name/flake.nix"
|
|
if [[ ! -f "$flake_file" ]]; then
|
|
log " ${YELLOW}skip${NC} $dir_name (not cloned)"
|
|
continue
|
|
fi
|
|
|
|
# Find deps: which workspace inputs does this flake reference?
|
|
local deps=()
|
|
for ws_name in "${ws_names[@]}"; do
|
|
[[ "$ws_name" == "$input_name" ]] && continue # skip self
|
|
# Match by input name (name.url, name.follows, name = {)
|
|
# OR by URL referencing the repo (github:logos-co/<name>)
|
|
if grep -qE "^[[:space:]]+${ws_name}(\.|[[:space:]]*=)" "$flake_file" 2>/dev/null || \
|
|
grep -qE "logos-co/${ws_name}(\.git)?\"" "$flake_file" 2>/dev/null; then
|
|
deps+=("$ws_name")
|
|
fi
|
|
done
|
|
|
|
# Check hasTests: does the flake declare a checks output?
|
|
local has_tests="false"
|
|
if grep -qE '^[[:space:]]+checks[[:space:]]*=' "$flake_file" 2>/dev/null; then
|
|
has_tests="true"
|
|
fi
|
|
|
|
# Format deps
|
|
local deps_str="[]"
|
|
if [[ ${#deps[@]} -gt 0 ]]; then
|
|
deps_str="[ $(printf '"%s" ' "${deps[@]}")]"
|
|
fi
|
|
|
|
# Write entry with padding
|
|
local pad
|
|
pad=$(printf "%-${max_len}s" "$input_name")
|
|
echo " ${pad} = { deps = ${deps_str}; hasTests = ${has_tests}; };" >> "$tmpfile"
|
|
|
|
# Console status
|
|
local marker="${GREEN}ok${NC}"
|
|
[[ "$has_tests" == "true" ]] && marker="${GREEN}ok${NC} ${CYAN}(tests)${NC}"
|
|
log " $marker $input_name (${#deps[@]} deps)"
|
|
done
|
|
|
|
echo "}" >> "$tmpfile"
|
|
log ""
|
|
|
|
# Compare and update
|
|
if [[ -f "$DEP_GRAPH" ]] && diff -q "$DEP_GRAPH" "$tmpfile" >/dev/null 2>&1; then
|
|
echo -e "${GREEN}No changes${NC} — nix/dep-graph.nix is already up to date."
|
|
rm "$tmpfile"
|
|
else
|
|
if [[ -f "$DEP_GRAPH" ]]; then
|
|
echo -e "${BOLD}Changes:${NC}"
|
|
diff --color=auto -u "$DEP_GRAPH" "$tmpfile" || true
|
|
echo ""
|
|
fi
|
|
mv "$tmpfile" "$DEP_GRAPH"
|
|
echo -e "${GREEN}Updated${NC} nix/dep-graph.nix"
|
|
fi
|
|
}
|
|
|
|
cmd_check_qt() {
|
|
wants_help "$@" && show_help "Usage: ws check-qt <repo>
|
|
|
|
Check a repo's build for Qt version conflicts that cause runtime crashes
|
|
('Class QT_ROOT_LEVEL_POOL ... is implemented in both ...').
|
|
|
|
Checks two things:
|
|
1. Whether the repo's nix closure has a DIRECT dependency on multiple Qt
|
|
versions (broken follows chain — nix-level fix needed).
|
|
2. Whether the user's local module cache contains modules built against a
|
|
different Qt than the current build (stale cache — clear the data dir).
|
|
|
|
Examples:
|
|
ws check-qt logos-app-poc
|
|
ws check-qt logos-liblogos"
|
|
|
|
local repo="${1:?Usage: ws check-qt <repo>}"
|
|
local input_name
|
|
input_name=$(get_input_name "$repo")
|
|
if [[ -z "$input_name" ]]; then
|
|
echo -e "${RED}Error:${NC} repo '$repo' has no flake input" >&2
|
|
exit 1
|
|
fi
|
|
|
|
log "${BOLD}Checking Qt versions for ${CYAN}$input_name${NC}${BOLD}...${NC}"
|
|
|
|
local store_path
|
|
store_path=$(nix build --extra-experimental-features "nix-command flakes" \
|
|
--no-link --print-out-paths "$WORKSPACE_ROOT#$input_name" 2>/dev/null)
|
|
if [[ -z "$store_path" ]]; then
|
|
echo -e "${RED}Error:${NC} failed to build $input_name" >&2
|
|
exit 1
|
|
fi
|
|
|
|
# Find all qtbase store paths in the transitive closure
|
|
local all_qt_paths
|
|
all_qt_paths=$(nix-store -qR "$store_path" 2>/dev/null | grep 'qtbase-[0-9]' || true)
|
|
if [[ -z "$all_qt_paths" ]]; then
|
|
echo -e "${GREEN}OK:${NC} No Qt found in closure (non-Qt repo)"
|
|
return 0
|
|
fi
|
|
|
|
# Use nix why-depends to check which Qt versions the build DIRECTLY depends on.
|
|
# External deps (logos-chat, logos-delivery, etc.) may bring their own nixpkgs
|
|
# with a different qtbase transitively, but that's harmless if it's not loaded
|
|
# by the app at runtime.
|
|
local direct_qt=()
|
|
while IFS= read -r qt_path; do
|
|
if nix why-depends --extra-experimental-features "nix-command flakes" \
|
|
"$WORKSPACE_ROOT#$input_name" "$qt_path" >/dev/null 2>&1; then
|
|
local ver
|
|
ver=$(echo "$qt_path" | grep -oE 'qtbase-[0-9]+\.[0-9]+\.[0-9]+')
|
|
direct_qt+=("$ver")
|
|
fi
|
|
done <<< "$all_qt_paths"
|
|
|
|
# Deduplicate
|
|
local unique_qt
|
|
unique_qt=$(printf '%s\n' "${direct_qt[@]}" | sort -u)
|
|
local qt_count
|
|
qt_count=$(echo "$unique_qt" | grep -c . 2>/dev/null || true)
|
|
|
|
if [[ "$qt_count" -gt 1 ]]; then
|
|
echo -e "${RED}CONFLICT:${NC} Build directly depends on multiple Qt versions:"
|
|
echo "$unique_qt" | while read -r v; do echo -e " ${YELLOW}*${NC} $v"; done
|
|
echo ""
|
|
echo -e "This ${BOLD}will crash${NC} at runtime. Fix the follows chain in flake.nix."
|
|
echo -e "Trace each version with:"
|
|
while IFS= read -r qt_path; do
|
|
local ver
|
|
ver=$(echo "$qt_path" | grep -oE 'qtbase-[0-9]+\.[0-9]+\.[0-9]+')
|
|
if nix why-depends --extra-experimental-features "nix-command flakes" \
|
|
"$WORKSPACE_ROOT#$input_name" "$qt_path" >/dev/null 2>&1; then
|
|
echo -e " ${BOLD}nix why-depends .#$input_name $qt_path${NC}"
|
|
fi
|
|
done <<< "$all_qt_paths"
|
|
exit 1
|
|
elif [[ "$qt_count" -eq 1 ]]; then
|
|
echo -e "${GREEN}OK:${NC} Single Qt version in direct deps: $unique_qt"
|
|
fi
|
|
|
|
# Note transitive Qt versions (informational, not a build error)
|
|
local all_qt_versions
|
|
all_qt_versions=$(echo "$all_qt_paths" | grep -oE 'qtbase-[0-9]+\.[0-9]+\.[0-9]+' | sort -u)
|
|
local all_qt_count
|
|
all_qt_count=$(echo "$all_qt_versions" | grep -c . 2>/dev/null || true)
|
|
if [[ "$all_qt_count" -gt 1 ]]; then
|
|
log ""
|
|
log "${YELLOW}Note:${NC} Transitive closure contains other Qt versions (from external deps):"
|
|
echo "$all_qt_versions" | while read -r v; do
|
|
if [[ "$unique_qt" != *"$v"* ]]; then
|
|
log " ${YELLOW}*${NC} $v (indirect — not loaded by $input_name)"
|
|
fi
|
|
done
|
|
log "This is harmless unless modules built against these versions are loaded at runtime."
|
|
fi
|
|
|
|
# Check local module cache for stale Qt
|
|
local app_qt
|
|
app_qt=$(echo "$unique_qt" | head -1)
|
|
if [[ -n "$app_qt" ]]; then
|
|
local cache_dirs=()
|
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
cache_dirs+=("$HOME/Library/Application Support/LogosAppNix/modules")
|
|
cache_dirs+=("$HOME/Library/Application Support/LogosAppNix/plugins")
|
|
cache_dirs+=("$HOME/Library/Application Support/LogosApp/modules")
|
|
cache_dirs+=("$HOME/Library/Application Support/LogosApp/plugins")
|
|
else
|
|
local xdg="${XDG_DATA_HOME:-$HOME/.local/share}"
|
|
cache_dirs+=("$xdg/LogosAppNix/modules" "$xdg/LogosAppNix/plugins")
|
|
cache_dirs+=("$xdg/LogosApp/modules" "$xdg/LogosApp/plugins")
|
|
fi
|
|
|
|
local stale_found=false
|
|
for dir in "${cache_dirs[@]}"; do
|
|
[[ -d "$dir" ]] || continue
|
|
local cached_qt
|
|
cached_qt=$(find "$dir" -type f \( -name "*.dylib" -o -name "*.so" \) -exec strings {} + 2>/dev/null \
|
|
| grep -oE 'qtbase-[0-9]+\.[0-9]+\.[0-9]+' | sort -u || true)
|
|
if [[ -n "$cached_qt" && "$cached_qt" != *"$app_qt"* ]]; then
|
|
if ! $stale_found; then
|
|
echo ""
|
|
echo -e "${RED}STALE MODULE CACHE:${NC} Local modules reference a different Qt:"
|
|
stale_found=true
|
|
fi
|
|
echo -e " ${YELLOW}$dir${NC} → $cached_qt (build uses $app_qt)"
|
|
fi
|
|
done
|
|
|
|
if $stale_found; then
|
|
echo ""
|
|
echo -e "${YELLOW}Fix:${NC} Clear the stale cache:"
|
|
if [[ "$(uname)" == "Darwin" ]]; then
|
|
echo -e " ${BOLD}rm -rf ~/Library/Application\\ Support/LogosAppNix${NC}"
|
|
else
|
|
echo -e " ${BOLD}rm -rf \${XDG_DATA_HOME:-~/.local/share}/LogosAppNix${NC}"
|
|
fi
|
|
exit 1
|
|
fi
|
|
|
|
if ! $stale_found; then
|
|
echo -e "${GREEN}OK:${NC} No stale modules in local cache"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
cmd_help() {
|
|
cat <<'EOF'
|
|
Logos Workspace CLI
|
|
|
|
USAGE:
|
|
ws [--quiet] <command> [args]
|
|
|
|
GLOBAL OPTIONS:
|
|
--quiet, -q Suppress informational output (for CI/scripting)
|
|
|
|
COMMANDS:
|
|
init [jobs] Clone all submodules (default: 4 parallel jobs)
|
|
list List all repos and their status
|
|
|
|
build <repo> [opts] Build a repo via nix
|
|
--auto-local, -a Auto-detect dirty repos and use as overrides
|
|
--local, -l <r1> <r2> ... Use specific local repos as overrides
|
|
|
|
run <repo> [opts] Build and run a repo (same options as build)
|
|
|
|
develop [repo] [opts] Enter a nix devShell (workspace shell if no repo)
|
|
--auto-local, -a Include overrides for dirty repos
|
|
--local, -l <r1> <r2> ... Include specific local overrides
|
|
--fresh Force rebuild (ignore cache)
|
|
|
|
test [repo|--all] [--type T] Run checks/tests
|
|
--type, -t <type> Filter by: cpp, rust, nim, js, qml
|
|
|
|
status Git status across all repos
|
|
dirty Show dirty repos and what they affect
|
|
graph [repo] Show dependency graph (DOT format, or repo details)
|
|
override-inputs <repo> [opts] Show the nix --override-input flags that would be used
|
|
update [repo|--all] Update flake.lock inputs
|
|
loc [repo|--all] [--no-nix] Count lines of code (uses tokei)
|
|
watch test <repo> Re-run tests on file changes
|
|
watch build <repo> Re-build on file changes
|
|
watch run <cmd> [-w repo...] Run command on file changes
|
|
foreach <command> Run a command in every repo directory
|
|
worktree add <name> [-b br] Create a worktree with submodules and ws/<branch> branches
|
|
worktree list List all worktrees
|
|
worktree remove <name> Remove a worktree
|
|
sync-graph Regenerate nix/dep-graph.nix from repo flake.nix files
|
|
check-qt <repo> Check for Qt version conflicts in a repo's nix closure
|
|
|
|
lm <args> Module inspector (auto-builds logos-module)
|
|
logoscore <args> Headless runtime (auto-builds logos-liblogos)
|
|
lgx <args> Package tool (auto-builds logos-package)
|
|
lgpm <args> Package manager (auto-builds logos-package-manager-module)
|
|
logos-cpp-generator <args> SDK code generator (auto-builds logos-cpp-sdk)
|
|
|
|
help Show this help
|
|
|
|
EXAMPLES:
|
|
# Set up the workspace
|
|
ws init
|
|
|
|
# Edit logos-cpp-sdk, then build logos-app-poc using your local changes
|
|
cd repos/logos-cpp-sdk && vim src/something.cpp
|
|
ws build logos-app-poc --auto-local
|
|
|
|
# Explicitly override specific deps
|
|
ws build logos-app-poc --local logos-cpp-sdk logos-liblogos
|
|
|
|
# Run all tests
|
|
ws test --all
|
|
|
|
# Run only C++ repo tests
|
|
ws test --all --type cpp
|
|
|
|
# Test a specific repo
|
|
ws test logos-cpp-sdk
|
|
|
|
# See what your dirty repos affect
|
|
ws dirty
|
|
|
|
# Enter logos-app-poc's devShell with local logos-cpp-sdk
|
|
ws develop logos-app-poc --local logos-cpp-sdk
|
|
|
|
# Visualize the dependency graph
|
|
ws graph | dot -Tsvg > graph.svg
|
|
|
|
# See deps of a specific repo
|
|
ws graph logos-app-poc
|
|
|
|
# Run git pull in every repo
|
|
ws foreach git pull
|
|
|
|
# Show override flags without executing
|
|
ws override-inputs logos-app-poc --auto-local
|
|
|
|
# Create an isolated worktree for a feature branch
|
|
ws worktree add my-feature -b my-feature-branch
|
|
# Result: ../logos-workspace--my-feature/ with all repos on ws/my-feature-branch
|
|
|
|
# Remove a worktree when done
|
|
ws worktree remove my-feature
|
|
|
|
# Lines of code across all repos
|
|
ws loc --all --no-nix
|
|
|
|
# Lines of code for a single repo
|
|
ws loc logos-cpp-sdk
|
|
|
|
# Auto-run tests when files change
|
|
ws watch test logos-module
|
|
|
|
# Auto-build on changes with local overrides
|
|
ws watch build logos-app-poc --auto-local
|
|
|
|
# Watch specific repos and run a custom command
|
|
ws watch run 'ws test logos-module --auto-local' -w logos-module -w logos-cpp-sdk
|
|
|
|
# Regenerate dep-graph.nix after adding tests to a repo
|
|
ws sync-graph
|
|
EOF
|
|
}
|
|
|
|
# ── Parse global flags ────────────────────────────────────────────────────────
|
|
_args=()
|
|
for _arg in "$@"; do
|
|
case "$_arg" in
|
|
--quiet|-q) QUIET=true ;;
|
|
*) _args+=("$_arg") ;;
|
|
esac
|
|
done
|
|
set -- "${_args[@]}"
|
|
|
|
if $QUIET; then
|
|
RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' NC=''
|
|
fi
|
|
|
|
# ── Main dispatch ────────────────────────────────────────────────────────────
|
|
|
|
case "${1:-help}" in
|
|
init) shift; cmd_init "$@" ;;
|
|
build) shift; cmd_build "$@" ;;
|
|
run) shift; cmd_run "$@" ;;
|
|
develop|dev) shift; cmd_develop "$@" ;;
|
|
test) shift; cmd_test "$@" ;;
|
|
status|st) shift; cmd_status "$@" ;;
|
|
dirty) shift; cmd_dirty "$@" ;;
|
|
graph) shift; cmd_graph "$@" ;;
|
|
override-inputs|oi) shift; cmd_override_inputs "$@" ;;
|
|
update) shift; cmd_update "$@" ;;
|
|
loc) shift; cmd_loc "$@" ;;
|
|
watch) shift; cmd_watch "$@" ;;
|
|
foreach) shift; cmd_foreach "$@" ;;
|
|
worktree|wt) shift; cmd_worktree "$@" ;;
|
|
sync-graph) shift; cmd_sync_graph "$@" ;;
|
|
check-qt) shift; cmd_check_qt "$@" ;;
|
|
list|ls) shift; cmd_list "$@" ;;
|
|
help|--help|-h) cmd_help ;;
|
|
# CLI tool wrappers — delegate to scripts/
|
|
lm|logoscore|lgx|lgpm|logos-cpp-generator)
|
|
exec "$WORKSPACE_ROOT/scripts/$1" "${@:2}" ;;
|
|
*)
|
|
echo -e "${RED}Unknown command:${NC} $1" >&2
|
|
echo "Run 'ws help' for usage." >&2
|
|
exit 1
|
|
;;
|
|
esac
|