#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LEADTECH_ROOT="${LEADTECH:-$(cd "$SCRIPT_DIR/.." && pwd)}" PROJECTS_CONF="${PROJECTS_CONF:-$LEADTECH_ROOT/_projects.conf}" PROJECT_ROOT="" PROJECT_NAME="" STACK_OVERRIDE="" SCOPE_OVERRIDE="" STATE_OVERRIDE="" SYNC_EXISTING="false" usage() { cat < Project root directory (default: current directory) --project-name Project name override (default: basename project-root) --stack Stack override --scope Scope override (perso|mindleaf|lab|archive) --state State override --sync-existing Update existing line (safe mode: preserve scope/state unless explicit override) -h, --help Show help USAGE } while [ $# -gt 0 ]; do case "$1" in --project-root) PROJECT_ROOT="$2" shift 2 ;; --project-name) PROJECT_NAME="$2" shift 2 ;; --stack) STACK_OVERRIDE="$2" shift 2 ;; --scope) SCOPE_OVERRIDE="$2" shift 2 ;; --state) STATE_OVERRIDE="$2" shift 2 ;; --sync-existing) SYNC_EXISTING="true" shift ;; -h|--help) usage exit 0 ;; *) echo "Erreur: option inconnue: $1" >&2 usage >&2 exit 1 ;; esac done PROJECT_ROOT="${PROJECT_ROOT:-$PWD}" if [ ! -d "$PROJECT_ROOT" ]; then echo "Erreur: project-root introuvable: $PROJECT_ROOT" >&2 exit 1 fi if [ ! -f "$PROJECTS_CONF" ]; then echo "Erreur: _projects.conf introuvable: $PROJECTS_CONF" >&2 exit 1 fi trim() { local s="$1" # shellcheck disable=SC2001 s="$(echo "$s" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" printf '%s' "$s" } infer_stack() { local root="$1" local out=() if [ -f "$root/pnpm-workspace.yaml" ]; then out+=("pnpm monorepo") fi if [ -f "$root/package.json" ]; then if grep -Eq '"@nestjs/' "$root/package.json"; then out+=("NestJS"); fi if grep -Eq '"next"' "$root/package.json"; then out+=("Next.js"); fi if grep -Eq '"expo"' "$root/package.json"; then out+=("Expo"); fi if grep -Eq '"react-native"' "$root/package.json"; then out+=("React Native"); fi if grep -Eq '"prisma"|"@prisma/' "$root/package.json"; then out+=("Prisma"); fi fi if [ -f "$root/prisma/schema.prisma" ] && grep -qi 'provider *= *"postgresql"' "$root/prisma/schema.prisma"; then out+=("PostgreSQL") fi if [ -f "$root/CLAUDE.md" ]; then local from_claude from_claude="$(awk ' BEGIN {in_stack=0} /^## Stack/ {in_stack=1; next} /^## / {if (in_stack) exit} in_stack && /^- / {sub(/^- /, ""); print} ' "$root/CLAUDE.md" | paste -sd ' + ' -)" from_claude="$(trim "$from_claude")" if [ -n "$from_claude" ]; then printf '%s' "$from_claude" return fi fi if [ ${#out[@]} -eq 0 ]; then printf '%s' "stack-a-completer" else local joined joined="$(printf '%s\n' "${out[@]}" | awk '!seen[$0]++' | paste -sd ' + ' -)" printf '%s' "$joined" fi } infer_scope() { local root="$1" case "$root" in *"/__Mindleaf/"*) printf '%s' "mindleaf" ;; *"/Labs/"*) printf '%s' "lab" ;; *"/Archives_Projets/"*) printf '%s' "archive" ;; *) printf '%s' "perso" ;; esac } infer_state() { local root="$1" local sprint_status_file="$root/_bmad-output/implementation-artifacts/sprint-status.yaml" local stories_dir="$root/_bmad-output/implementation-artifacts/stories" local first_story local latest_story local epic_num if [ -f "$sprint_status_file" ]; then local in_dev="false" local line trimmed key val story_epic local active_epic="" active_ts="-1" local prep_epic="" local declared_in_progress_epic="" local story_file story_ts local -a matches=() while IFS= read -r line || [ -n "$line" ]; do if [ "$line" = "development_status:" ]; then in_dev="true" continue fi if [ "$in_dev" = "true" ] && [ -n "$(trim "$line")" ] && [[ ! "$line" =~ ^[[:space:]] ]]; then in_dev="false" fi [ "$in_dev" = "true" ] || continue trimmed="$(trim "$line")" [[ "$trimmed" == *:* ]] || continue key="$(trim "${trimmed%%:*}")" val="$(trim "${trimmed#*:}")" if [[ "$key" =~ ^epic-([0-9]+)$ ]]; then story_epic="${BASH_REMATCH[1]}" if [ "$val" = "in-progress" ]; then if [ -z "$declared_in_progress_epic" ] || [ "$story_epic" -lt "$declared_in_progress_epic" ]; then declared_in_progress_epic="$story_epic" fi fi continue fi if [[ "$key" =~ ^([0-9]+)-[0-9]+[a-z]?-.*$ ]]; then story_epic="${BASH_REMATCH[1]}" if [ "$val" = "in-progress" ] || [ "$val" = "review" ]; then matches=() shopt -s nullglob matches=( "$stories_dir/epic-$story_epic/${key}"*.md ) shopt -u nullglob story_ts="0" if [ ${#matches[@]} -gt 0 ]; then story_file="${matches[0]}" story_ts="$(stat -c %Y "$story_file" 2>/dev/null || printf '0')" fi if [ "$story_ts" -gt "$active_ts" ] || { [ "$story_ts" -eq "$active_ts" ] && { [ -z "$active_epic" ] || [ "$story_epic" -gt "$active_epic" ]; }; }; then active_ts="$story_ts" active_epic="$story_epic" fi elif [ "$val" = "ready-for-dev" ] || [ "$val" = "backlog" ]; then if [ -z "$prep_epic" ] || [ "$story_epic" -lt "$prep_epic" ]; then prep_epic="$story_epic" fi fi fi done < "$sprint_status_file" if [ -n "$active_epic" ]; then printf 'Epic %s en cours' "$active_epic" return fi if [ -n "$prep_epic" ]; then printf 'Epic %s en préparation' "$prep_epic" return fi if [ -n "$declared_in_progress_epic" ]; then printf 'Epic %s en préparation' "$declared_in_progress_epic" return fi fi if [ -d "$stories_dir" ]; then first_story="$(find "$stories_dir" -maxdepth 2 -type f -name '*.md' \ ! -name 'epic-*-retro*.md' -print -quit 2>/dev/null)" if [ -n "$first_story" ]; then latest_story="$(find "$stories_dir" -maxdepth 2 -type f -name '*.md' \ ! -name 'epic-*-retro*.md' -print 2>/dev/null \ | xargs ls -1t 2>/dev/null \ | head -n 1)" if [ -n "$latest_story" ]; then epic_num="$(echo "$latest_story" | sed -n 's#.*\/epic-\([0-9][0-9]*\)\/.*#\1#p')" if [ -n "$epic_num" ]; then printf 'Epic %s en préparation' "$epic_num" return fi fi fi fi printf '%s' "dev" } is_progress_state() { local state="$1" [ -z "$state" ] && return 0 case "$state" in dev|active) return 0 ;; esac echo "$state" | grep -Eq '^Epic [0-9]+ en (préparation|cours)$' } PROJECT_NAME="${PROJECT_NAME:-$(basename "$PROJECT_ROOT")}" STACK_INFERRED="$(infer_stack "$PROJECT_ROOT")" SCOPE_INFERRED="$(infer_scope "$PROJECT_ROOT")" STATE_INFERRED="$(infer_state "$PROJECT_ROOT")" STACK="${STACK_OVERRIDE:-$STACK_INFERRED}" SCOPE="${SCOPE_OVERRIDE:-$SCOPE_INFERRED}" STATE="${STATE_OVERRIDE:-$STATE_INFERRED}" if ! [[ "$SCOPE" =~ ^(perso|mindleaf|lab|archive)$ ]]; then echo "Erreur: scope invalide '$SCOPE' (attendu: perso|mindleaf|lab|archive)" >&2 exit 1 fi if grep -q "^${PROJECT_NAME}|" "$PROJECTS_CONF"; then if [ "$SYNC_EXISTING" != "true" ]; then echo "OK: projet déjà présent dans _projects.conf: $PROJECT_NAME" exit 0 fi existing_line="$(grep "^${PROJECT_NAME}|" "$PROJECTS_CONF" | head -n 1)" IFS='|' read -r _ existing_stack existing_scope existing_state <&2 exit 1 fi tmp="$(mktemp)" awk -F'|' -v OFS='|' -v n="$PROJECT_NAME" -v st="$TARGET_STACK" -v sc="$TARGET_SCOPE" -v et="$TARGET_STATE" ' $0 ~ /^#/ || NF < 4 { print; next } $1 == n { print $1, st, sc, et; next } { print } ' "$PROJECTS_CONF" > "$tmp" mv "$tmp" "$PROJECTS_CONF" echo "OK: projet synchronisé dans _projects.conf: $PROJECT_NAME" exit 0 fi last_char="$(tail -c 1 "$PROJECTS_CONF" 2>/dev/null || true)" if [ -n "$last_char" ]; then echo >> "$PROJECTS_CONF" fi printf '%s|%s|%s|%s\n' "$PROJECT_NAME" "$STACK" "$SCOPE" "$STATE" >> "$PROJECTS_CONF" echo "OK: projet ajouté à _projects.conf: $PROJECT_NAME"