diff --git a/lib/archeflow-init.sh b/lib/archeflow-init.sh new file mode 100755 index 0000000..e9d2b5b --- /dev/null +++ b/lib/archeflow-init.sh @@ -0,0 +1,564 @@ +#!/usr/bin/env bash +# archeflow-init.sh — Initialize an ArcheFlow project from a template bundle, clone from +# another project, save the current setup as a template, or list available templates. +# +# Usage: +# archeflow-init.sh [--set key=value ...] Init from named bundle +# archeflow-init.sh --from Clone from another project +# archeflow-init.sh --list List available templates +# archeflow-init.sh --save Save current setup as template +# archeflow-init.sh --share Export template to directory +# +# Examples: +# ./lib/archeflow-init.sh writing-short-story +# ./lib/archeflow-init.sh writing-short-story --set target_words=8000 +# ./lib/archeflow-init.sh --from ../book.giesing-gschichten +# ./lib/archeflow-init.sh --save my-story-setup +# ./lib/archeflow-init.sh --list + +set -euo pipefail + +GLOBAL_TEMPLATES="${HOME}/.archeflow/templates" +LOCAL_TEMPLATES=".archeflow/templates" + +# --- Helpers ---------------------------------------------------------------- + +die() { echo "ERROR: $*" >&2; exit 1; } +warn() { echo "WARNING: $*" >&2; } +info() { echo " $*"; } + +# Parse YAML value (simple single-level extraction — no nested support). +# Falls back to grep+sed when yq is unavailable. +yaml_value() { + local file="$1" key="$2" + if command -v yq &>/dev/null; then + yq -r ".$key // empty" "$file" 2>/dev/null + else + grep -E "^${key}:" "$file" 2>/dev/null | sed 's/^[^:]*:[[:space:]]*//' | sed 's/^["'"'"']\(.*\)["'"'"']$/\1/' + fi +} + +# Parse YAML list (simple — one item per "- " line under key). +yaml_list() { + local file="$1" key="$2" + if command -v yq &>/dev/null; then + yq -r ".$key[]? // empty" "$file" 2>/dev/null + else + sed -n "/^${key}:/,/^[^ -]/{ /^ *- /{ s/^ *- *//; s/^[\"']\(.*\)[\"']$/\1/; p; } }" "$file" 2>/dev/null + fi +} + +# Check if a directory has files matching a glob (safe for empty results). +has_files() { + local dir="$1" pattern="${2:-*}" + # shellcheck disable=SC2086 + compgen -G "${dir}/${pattern}" &>/dev/null +} + +# Confirm overwrite if target exists and has files. +confirm_overwrite() { + local dir="$1" desc="$2" + if [[ -d "$dir" ]] && has_files "$dir"; then + warn "$desc already has files in $dir" + if [[ -t 0 ]]; then + read -r -p " Overwrite? [y/N] " answer + [[ "$answer" =~ ^[Yy]$ ]] || die "Aborted — will not overwrite existing files." + else + die "Non-interactive mode — will not overwrite existing files in $dir. Remove them first." + fi + fi +} + +# --- Commands --------------------------------------------------------------- + +cmd_list() { + echo "ArcheFlow Templates" + echo "====================" + echo "" + + # Bundles + local found_bundle=false + echo "Bundles:" + for base in "$LOCAL_TEMPLATES" "$GLOBAL_TEMPLATES"; do + local scope + [[ "$base" == "$LOCAL_TEMPLATES" ]] && scope="local" || scope="global" + if [[ -d "$base/bundles" ]]; then + for manifest in "$base"/bundles/*/manifest.yaml; do + [[ -f "$manifest" ]] || continue + found_bundle=true + local bname bdir desc + bdir="$(dirname "$manifest")" + bname="$(basename "$bdir")" + desc="$(yaml_value "$manifest" "description")" + printf " %-25s %-45s [%s]\n" "$bname" "${desc:-(no description)}" "$scope" + done + fi + done + $found_bundle || echo " (none)" + echo "" + + # Individual templates + echo "Individual Templates:" + for category in workflows teams archetypes domains; do + local found=false + local label + label="$(echo "$category" | sed 's/^./\U&/')" # Capitalize + echo " ${label}:" + for base in "$LOCAL_TEMPLATES" "$GLOBAL_TEMPLATES"; do + local scope + [[ "$base" == "$LOCAL_TEMPLATES" ]] && scope="local" || scope="global" + if [[ -d "$base/$category" ]]; then + for f in "$base/$category"/*; do + [[ -f "$f" ]] || continue + found=true + printf " %-35s [%s]\n" "$(basename "$f")" "$scope" + done + fi + done + $found || echo " (none)" + done +} + +cmd_init_bundle() { + local bundle_name="$1" + shift + local -A overrides=() + + # Parse --set key=value arguments + while [[ $# -gt 0 ]]; do + case "$1" in + --set) + shift + [[ $# -gt 0 ]] || die "--set requires a key=value argument" + local k="${1%%=*}" v="${1#*=}" + overrides["$k"]="$v" + shift + ;; + *) + die "Unknown argument: $1" + ;; + esac + done + + # Find the bundle + local bundle_dir="" + for base in "$LOCAL_TEMPLATES" "$GLOBAL_TEMPLATES"; do + if [[ -f "$base/bundles/${bundle_name}/manifest.yaml" ]]; then + bundle_dir="$base/bundles/${bundle_name}" + break + fi + done + [[ -n "$bundle_dir" ]] || die "Bundle not found: $bundle_name. Run '$0 --list' to see available templates." + + local manifest="$bundle_dir/manifest.yaml" + echo "Initializing from bundle: $bundle_name" + echo " Source: $bundle_dir" + echo "" + + # Check requires + local req + while IFS= read -r req; do + [[ -z "$req" ]] && continue + if [[ ! -e "$req" ]]; then + die "Required file not found: $req. This bundle requires it in the project root." + fi + info "Requirement satisfied: $req" + done < <(yaml_list "$manifest" "requires") + + # Create target directories + mkdir -p .archeflow/teams .archeflow/workflows .archeflow/archetypes .archeflow/domains + + # Copy team + local team_file + team_file="$(yaml_value "$manifest" "includes.team" 2>/dev/null || true)" + # Fallback for flat YAML parsing + if [[ -z "$team_file" ]] && command -v yq &>/dev/null; then + team_file="$(yq -r '.includes.team // empty' "$manifest" 2>/dev/null)" + fi + if [[ -n "$team_file" && -f "$bundle_dir/$team_file" ]]; then + confirm_overwrite ".archeflow/teams" "Teams directory" + cp "$bundle_dir/$team_file" ".archeflow/teams/$team_file" + info "Team: $team_file -> .archeflow/teams/" + elif [[ -n "$team_file" ]]; then + # team_file might just be the name, check without path + if [[ -f "$bundle_dir/team.yaml" ]]; then + confirm_overwrite ".archeflow/teams" "Teams directory" + cp "$bundle_dir/team.yaml" ".archeflow/teams/$team_file" + info "Team: $team_file -> .archeflow/teams/" + else + warn "Team file not found in bundle: $team_file" + fi + fi + + # Copy workflow + local wf_file + wf_file="$(yaml_value "$manifest" "includes.workflow" 2>/dev/null || true)" + if [[ -z "$wf_file" ]] && command -v yq &>/dev/null; then + wf_file="$(yq -r '.includes.workflow // empty' "$manifest" 2>/dev/null)" + fi + if [[ -n "$wf_file" && -f "$bundle_dir/$wf_file" ]]; then + confirm_overwrite ".archeflow/workflows" "Workflows directory" + cp "$bundle_dir/$wf_file" ".archeflow/workflows/$wf_file" + info "Workflow: $wf_file -> .archeflow/workflows/" + elif [[ -n "$wf_file" && -f "$bundle_dir/workflow.yaml" ]]; then + confirm_overwrite ".archeflow/workflows" "Workflows directory" + cp "$bundle_dir/workflow.yaml" ".archeflow/workflows/$wf_file" + info "Workflow: $wf_file -> .archeflow/workflows/" + elif [[ -n "$wf_file" ]]; then + warn "Workflow file not found in bundle: $wf_file" + fi + + # Copy archetypes + local arch_count=0 + if [[ -d "$bundle_dir/archetypes" ]] && has_files "$bundle_dir/archetypes" "*.md"; then + confirm_overwrite ".archeflow/archetypes" "Archetypes directory" + for f in "$bundle_dir"/archetypes/*.md; do + [[ -f "$f" ]] || continue + cp "$f" ".archeflow/archetypes/$(basename "$f")" + arch_count=$((arch_count + 1)) + done + info "Archetypes: $arch_count files -> .archeflow/archetypes/" + fi + + # Copy domain + local domain_file + domain_file="$(yaml_value "$manifest" "includes.domain" 2>/dev/null || true)" + if [[ -z "$domain_file" ]] && command -v yq &>/dev/null; then + domain_file="$(yq -r '.includes.domain // empty' "$manifest" 2>/dev/null)" + fi + if [[ -n "$domain_file" && -f "$bundle_dir/$domain_file" ]]; then + confirm_overwrite ".archeflow/domains" "Domains directory" + cp "$bundle_dir/$domain_file" ".archeflow/domains/$domain_file" + info "Domain: $domain_file -> .archeflow/domains/" + elif [[ -n "$domain_file" && -f "$bundle_dir/domain.yaml" ]]; then + confirm_overwrite ".archeflow/domains" "Domains directory" + cp "$bundle_dir/domain.yaml" ".archeflow/domains/$domain_file" + info "Domain: $domain_file -> .archeflow/domains/" + elif [[ -n "$domain_file" ]]; then + warn "Domain file not found in bundle: $domain_file" + fi + + # Copy hooks if present + if [[ -f "$bundle_dir/hooks.yaml" ]]; then + cp "$bundle_dir/hooks.yaml" ".archeflow/hooks.yaml" + info "Hooks: hooks.yaml -> .archeflow/" + fi + + # Generate config.yaml with variables + local config_file=".archeflow/config.yaml" + { + echo "# Generated by archeflow init from bundle: $bundle_name" + echo "bundle: $bundle_name" + local version + version="$(yaml_value "$manifest" "version")" + echo "bundle_version: ${version:-1}" + echo "initialized: $(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "variables:" + + # Read default variables from manifest + local -A vars=() + if command -v yq &>/dev/null; then + while IFS='=' read -r k v; do + [[ -n "$k" ]] && vars["$k"]="$v" + done < <(yq -r '.variables // {} | to_entries[] | "\(.key)=\(.value)"' "$manifest" 2>/dev/null) + else + # Simple fallback: parse variables section + local in_vars=false + while IFS= read -r line; do + if [[ "$line" =~ ^variables: ]]; then + in_vars=true; continue + fi + if $in_vars; then + if [[ "$line" =~ ^[[:space:]]+(.*):\ (.*) ]]; then + local vk="${BASH_REMATCH[1]}" vv="${BASH_REMATCH[2]}" + vk="$(echo "$vk" | xargs)" + vv="$(echo "$vv" | sed 's/#.*//' | xargs)" + [[ -n "$vk" ]] && vars["$vk"]="$vv" + elif [[ "$line" =~ ^[^[:space:]] ]]; then + break + fi + fi + done < "$manifest" + fi + + # Apply overrides + for k in "${!overrides[@]}"; do + vars["$k"]="${overrides[$k]}" + done + + # Write variables + if [[ ${#vars[@]} -eq 0 ]]; then + echo " # (no variables defined)" + else + for k in $(echo "${!vars[@]}" | tr ' ' '\n' | sort); do + echo " $k: ${vars[$k]}" + done + fi + } > "$config_file" + info "Config: $config_file" + + echo "" + echo "ArcheFlow initialized from bundle: $bundle_name" + + # Print variable summary + if [[ ${#vars[@]} -gt 0 ]]; then + local var_summary="" + for k in $(echo "${!vars[@]}" | tr ' ' '\n' | sort); do + [[ -n "$var_summary" ]] && var_summary+=", " + var_summary+="${k}=${vars[$k]}" + done + echo " Variables: $var_summary" + fi + + echo "" + echo "Ready to run: archeflow:run" +} + +cmd_init_from() { + local source_path="$1" + + [[ -d "$source_path/.archeflow" ]] || die "No .archeflow/ directory found in $source_path" + + echo "Cloning ArcheFlow setup from: $source_path" + echo "" + + mkdir -p .archeflow + + local copied=0 + for subdir in teams workflows archetypes domains; do + if [[ -d "$source_path/.archeflow/$subdir" ]] && has_files "$source_path/.archeflow/$subdir"; then + confirm_overwrite ".archeflow/$subdir" "$subdir directory" + mkdir -p ".archeflow/$subdir" + cp "$source_path/.archeflow/$subdir"/* ".archeflow/$subdir/" + local count + count=$(find ".archeflow/$subdir" -maxdepth 1 -type f | wc -l) + info "$subdir/: $count files copied" + copied=$((copied + count)) + fi + done + + # Copy config.yaml if present + if [[ -f "$source_path/.archeflow/config.yaml" ]]; then + cp "$source_path/.archeflow/config.yaml" ".archeflow/config.yaml" + info "config.yaml copied" + copied=$((copied + 1)) + fi + + # Copy hooks.yaml if present + if [[ -f "$source_path/.archeflow/hooks.yaml" ]]; then + cp "$source_path/.archeflow/hooks.yaml" ".archeflow/hooks.yaml" + info "hooks.yaml copied" + copied=$((copied + 1)) + fi + + # Explicitly skip run-specific directories + for skip in events artifacts context templates; do + if [[ -d "$source_path/.archeflow/$skip" ]]; then + info "(skipped $skip/ — run-specific data)" + fi + done + + echo "" + echo "Cloned $copied files from $source_path" + echo "Ready to run: archeflow:run" +} + +cmd_save() { + local name="$1" + + [[ -d ".archeflow" ]] || die "No .archeflow/ directory in current project. Nothing to save." + + local bundle_dir="$GLOBAL_TEMPLATES/bundles/$name" + + if [[ -d "$bundle_dir" ]]; then + warn "Template bundle already exists: $bundle_dir" + if [[ -t 0 ]]; then + read -r -p " Overwrite? [y/N] " answer + [[ "$answer" =~ ^[Yy]$ ]] || die "Aborted." + else + die "Non-interactive mode — will not overwrite existing bundle $name." + fi + rm -rf "$bundle_dir" + fi + + mkdir -p "$bundle_dir" + echo "Saving current setup as template: $name" + echo "" + + local team_file="" wf_file="" domain_file="" + local -a arch_files=() + local file_count=0 + + # Copy teams (take first .yaml file) + if [[ -d ".archeflow/teams" ]] && has_files ".archeflow/teams" "*.yaml"; then + team_file="$(ls .archeflow/teams/*.yaml 2>/dev/null | head -1)" + if [[ -n "$team_file" ]]; then + cp "$team_file" "$bundle_dir/$(basename "$team_file")" + team_file="$(basename "$team_file")" + info "Team: $team_file" + file_count=$((file_count + 1)) + fi + fi + + # Copy workflows (take first .yaml file) + if [[ -d ".archeflow/workflows" ]] && has_files ".archeflow/workflows" "*.yaml"; then + wf_file="$(ls .archeflow/workflows/*.yaml 2>/dev/null | head -1)" + if [[ -n "$wf_file" ]]; then + cp "$wf_file" "$bundle_dir/$(basename "$wf_file")" + wf_file="$(basename "$wf_file")" + info "Workflow: $wf_file" + file_count=$((file_count + 1)) + fi + fi + + # Copy archetypes + if [[ -d ".archeflow/archetypes" ]] && has_files ".archeflow/archetypes" "*.md"; then + mkdir -p "$bundle_dir/archetypes" + for f in .archeflow/archetypes/*.md; do + [[ -f "$f" ]] || continue + cp "$f" "$bundle_dir/archetypes/" + arch_files+=("$(basename "$f")") + file_count=$((file_count + 1)) + done + info "Archetypes: ${#arch_files[@]} files" + fi + + # Copy domain (take first .yaml file) + if [[ -d ".archeflow/domains" ]] && has_files ".archeflow/domains" "*.yaml"; then + domain_file="$(ls .archeflow/domains/*.yaml 2>/dev/null | head -1)" + if [[ -n "$domain_file" ]]; then + cp "$domain_file" "$bundle_dir/$(basename "$domain_file")" + domain_file="$(basename "$domain_file")" + info "Domain: $domain_file" + file_count=$((file_count + 1)) + fi + fi + + # Copy hooks if present + if [[ -f ".archeflow/hooks.yaml" ]]; then + cp ".archeflow/hooks.yaml" "$bundle_dir/hooks.yaml" + info "Hooks: hooks.yaml" + file_count=$((file_count + 1)) + fi + + # Detect domain name from domain file + local domain_name="" + if [[ -n "$domain_file" && -f "$bundle_dir/$domain_file" ]]; then + domain_name="$(yaml_value "$bundle_dir/$domain_file" "name")" + fi + + # Read variables from config.yaml if present + local has_vars=false + local vars_yaml="" + if [[ -f ".archeflow/config.yaml" ]]; then + if command -v yq &>/dev/null; then + vars_yaml="$(yq -r '.variables // {} | to_entries[] | " \(.key): \(.value)"' ".archeflow/config.yaml" 2>/dev/null)" + [[ -n "$vars_yaml" ]] && has_vars=true + else + local in_vars=false + while IFS= read -r line; do + if [[ "$line" =~ ^variables: ]]; then + in_vars=true; continue + fi + if $in_vars; then + if [[ "$line" =~ ^[[:space:]] ]]; then + vars_yaml+="$line"$'\n' + has_vars=true + else + break + fi + fi + done < ".archeflow/config.yaml" + fi + fi + + # Generate manifest + local project_dir + project_dir="$(basename "$(pwd)")" + { + echo "name: $name" + echo "description: \"Saved from $project_dir\"" + echo "version: 1" + [[ -n "$domain_name" ]] && echo "domain: $domain_name" + echo "includes:" + [[ -n "$team_file" ]] && echo " team: $team_file" + [[ -n "$wf_file" ]] && echo " workflow: $wf_file" + if [[ ${#arch_files[@]} -gt 0 ]]; then + echo " archetypes:" + for a in "${arch_files[@]}"; do + echo " - $a" + done + fi + [[ -n "$domain_file" ]] && echo " domain: $domain_file" + echo "requires: []" + if $has_vars; then + echo "variables:" + echo "$vars_yaml" + else + echo "variables: {}" + fi + } > "$bundle_dir/manifest.yaml" + + file_count=$((file_count + 1)) # manifest itself + + echo "" + echo "Template saved: $name" + echo " Location: $bundle_dir/" + echo " Files: $file_count" + echo " Use with: archeflow init $name" +} + +cmd_share() { + local name="$1" target="$2" + + local bundle_dir="" + for base in "$LOCAL_TEMPLATES" "$GLOBAL_TEMPLATES"; do + if [[ -d "$base/bundles/$name" ]]; then + bundle_dir="$base/bundles/$name" + break + fi + done + [[ -n "$bundle_dir" ]] || die "Bundle not found: $name. Run '$0 --list' to see available templates." + + mkdir -p "$target" + cp -r "$bundle_dir" "$target/$name" + + echo "Exported: $target/$name/" + echo "To import: cp -r $target/$name ~/.archeflow/templates/bundles/" +} + +# --- Main ------------------------------------------------------------------- + +if [[ $# -eq 0 ]]; then + echo "Usage:" + echo " $0 [--set key=value ...] Init from named bundle" + echo " $0 --from Clone from another project" + echo " $0 --list List available templates" + echo " $0 --save Save current setup as template" + echo " $0 --share Export template to directory" + exit 0 +fi + +case "$1" in + --list) + cmd_list + ;; + --from) + [[ $# -ge 2 ]] || die "--from requires a project path" + cmd_init_from "$2" + ;; + --save) + [[ $# -ge 2 ]] || die "--save requires a template name" + cmd_save "$2" + ;; + --share) + [[ $# -ge 3 ]] || die "--share requires a name and a target path" + cmd_share "$2" "$3" + ;; + -*) + die "Unknown option: $1" + ;; + *) + cmd_init_bundle "$@" + ;; +esac