#!/usr/bin/env bash set -e help-table() { local -a rows local max_cmd=7 max_opt=6 max_desc=11 rows+=("Command|Option|Description") rows+=("===ENV===| | ") rows+=("version||Show tool versions.") rows+=("copy-dotenv|[env][env]|Copy .env file.") rows+=("create-dotenv|[env][type]|Create .env file of type nextcloud, odoo, upgrade.") rows+=("edit-dotenv|[env]|Open .env file in default editor.") rows+=("list-dotenv||List .env files.") rows+=("load-dotenv|[env]|Load and export .env file.") rows+=("remove-dotenv|[env]|Remove environment config.") rows+=("rename-dotenv|[env][env]|Rename .env file.") rows+=("restore-dotenv-files|[path]|Extract and copy .env files from backup file.") rows+=("show-dotenv|[env]|Output content of the .env file.") rows+=("===VISUALIZATION===| | ") rows+=("visualize-dependencies||Visualize Ansible role dependencies.") rows+=("===LOGOS===| | ") rows+=("resize-logos|[path]|Resize logos to 200x200.") rows+=("convert-logos||Convert all .jpg logos to .png.") rows+=("===GALAXY===| | ") rows+=("build-galaxy||Create tarball of collection.") rows+=("publish-galaxy||Publish tarball to Ansible Galaxy.") rows+=("===DOCS===| | ") rows+=("build-vuepress||Create vuepress build.") rows+=("dev-vuepress||Start vuepress development server.") rows+=("update-docs||Update docs.") rows+=("install-vuepress||Install build dependencies.") rows+=("serve-vuepress||Serve vuepress build.") rows+=("format-role-readmes||Update readme files in roles folder.") rows+=("===LLM===| | ") rows+=("commit-with-llm||Commit with llm generated commit message.") rows+=("update-with-llm|[glob][prompt]|Feed files matching and prompt to LLM and apply file changes.") rows+=("===DOCKER===| | ") rows+=("login-docker|[user][token]|Run Docker login and create docker config.") rows+=("===VAULT===| | ") rows+=("convert-vault-file|[vault_id] [path]|Re-encrypt vault file as single strings.") rows+=("decrypt-file|[path]|Decrypt Ansible Vault file.") rows+=("encrypt-file|[vault_id] [path]|Encrypt file with Ansible Vault.") rows+=("encrypt-string|[vault_id] [key] [value] -q|Generate encrypted string with Ansible Vault.") rows+=("generate-encrypted-string|[vault_id] [key] -q|Generate encrypted string for key with Ansible Vault.") rows+=("generate-vault-password|[vault_id] [password]|Generate a password file for Ansible Vault.") rows+=("show-vault-file|[vault_id] [path]|Show content of encrypted strings or encrypted vault file.") rows+=("===SETUP===| | ") rows+=("install||Install Ansible and dependencies with uv.") rows+=("install-ansible-build-scripts|[role]|Install scripts of the specified Ansbile role.") rows+=("install-exo||Install Exoscale CLI.") rows+=("show-passwordless-sudo|[username]|Give instructions to setup passwordless sudo.") rows+=("upgrade||Upgrade Ansible and dependencies with uv.") rows+=("===DEVELOPMENT===| | ") rows+=("lint||Lint Ansible roles and playbooks.") rows+=("source||Source the Python virtual env.") rows+=("===REPORT===| | ") rows+=("get-last-hostname|[role]|Search inventory for \$ROLE_hostname entries.") rows+=("list-hosts|[path] [grep]|Lists all hosts of an inventory.") rows+=("list-roles||Lists all roles.") rows+=("list-vault||Lists vault identities.") rows+=("list-deployments-odoo|[path]|List Odoo deplyments.") rows+=("===GIT===| | ") rows+=("pull-repos||Pull git repos in inventories folder.") rows+=("===RUN===| | ") rows+=("play|[params]|Run ansible-playbook command.") rows+=("list-tags|[path]|List tags of playbook.") rows+=("run||Run ansible command.") rows+=("connect-host|[host][username]|Connect to host with configs.") rows+=("upgrade-odoo|[env][step]|Run the Odoo upgrade for a target environment.") rows+=("===TEST===| | ") rows+=("test-project|[cleanup]|Deploy to localhost. Pass option 'cleanup' to reset test env.") rows+=("test-scripts|[cleanup]|Install and test scripts. Pass option 'cleanup' to reset env.") for row in "${rows[@]}"; do IFS='|' read -r cmd opt desc <<< "$row" (( ${#cmd} > max_cmd )) && max_cmd=${#cmd} (( ${#opt} > max_opt )) && max_opt=${#opt} (( ${#desc} > max_desc )) && max_desc=${#desc} done printf '| %-*s | %-*s | %-*s |\n' \ "$max_cmd" "Command" \ "$max_opt" "Option" \ "$max_desc" "Description" printf '|-%*s-|-%*s-|-%*s-|\n' \ "$max_cmd" "$(printf '%*s' "$max_cmd" '' | tr ' ' '-')" \ "$max_opt" "$(printf '%*s' "$max_opt" '' | tr ' ' '-')" \ "$max_desc" "$(printf '%*s' "$max_desc" '' | tr ' ' '-')" local first_row=true for row in "${rows[@]:1}"; do IFS='|' read -r cmd opt desc <<< "$row" if [[ "$cmd" =~ ^===.*===$ ]]; then group_name="${cmd/===/}" group_name="${group_name/===/}" if [ "$first_row" = true ]; then first_row=false else printf '|-%*s-|-%*s-|-%*s-|\n' \ "$max_cmd" "$(printf '%*s' "$max_cmd" '' | tr ' ' '-')" \ "$max_opt" "$(printf '%*s' "$max_opt" '' | tr ' ' '-')" \ "$max_desc" "$(printf '%*s' "$max_desc" '' | tr ' ' '-')" fi local left_pad=$(( (max_cmd - ${#group_name}) / 2 )) local right_pad=$(( max_cmd - left_pad - ${#group_name} )) printf '| %*s%s%*s | %*s | %*s |\n' \ "$left_pad" "" \ "$group_name" \ "$right_pad" "" \ "$max_opt" "" \ "$max_desc" "" printf '|-%*s-|-%*s-|-%*s-|\n' \ "$max_cmd" "$(printf '%*s' "$max_cmd" '' | tr ' ' '-')" \ "$max_opt" "$(printf '%*s' "$max_opt" '' | tr ' ' '-')" \ "$max_desc" "$(printf '%*s' "$max_desc" '' | tr ' ' '-')" else first_row=false printf '| %-*s | %-*s | %-*s |\n' \ "$max_cmd" "$cmd" \ "$max_opt" "$opt" \ "$max_desc" "$desc" fi done } help() { echo if [[ -n "$1" ]]; then help-table | grep -i "$1" | column -t -s'|' else echo 'task [options]' echo echo 'commands:' echo help-table fi echo } # Dynamic env vars TASK_DOTENV_DIR="$PWD/inventories/dotenv" GIT_BRANCH=$(git symbolic-ref --short -q HEAD) LLM_MODEL=${LLM_MODEL:="llama"} if [[ -a ".env" ]]; then export $(cat .env | sed 's/^#.*//g' | xargs) fi # Import commands clone-taskfile(){ if [ ! -d "$HOME/taskfile.build" ]; then echo -e "\033[38;5;214mGit\033[0m: Clone taskfile repo" git clone https://git.taskfile.build "$HOME/taskfile.build" else echo -e "\033[38;5;214mGit\033[0m: Pull taskfile repo" git -C "$HOME/taskfile.build" pull fi } if [ -d "$HOME/taskfile.build/bin" ]; then for file in "$HOME/taskfile.build/bin/"*; do if [ -f "$file" ]; then source "$file" fi done fi # Vault connect-host() { if [[ -z "$1" ]]; then echo 'Host name cannot be empty' return 1 fi local search_host="$1" local override_user="$2" # Find hosts.yaml file in inventories folder local hosts_files hosts_files=$(find ./inventories -type f -name 'hosts.yml') # Search for host name match local host_config="" local hosts_file for hosts_file in $hosts_files; do local command="yq e '.all.children.*.hosts.$search_host' $hosts_file" host_config=$(eval "$command" | sed '/null/d' | head -5) if [[ -z "$host_config" ]]; then command="yq e '.all.children.*.children.*.hosts.$search_host' $hosts_file" host_config=$(eval "$command" | sed '/null/d' | head -5) fi if [[ -z "$host_config" ]]; then command="yq e '.all.hosts.$search_host' $hosts_file" host_config=$(eval "$command" | sed '/null/d' | head -5) fi if [[ -n "$host_config" ]]; then break fi done if [[ -z "$host_config" ]]; then echo "Host '$search_host' not found in hosts.yml file" return 1 fi # Get Ansible vars from the host configuration local ansible_host local ansible_port local ansible_user local ansible_common_args ansible_host=$(echo "$host_config" | grep -m1 '^ansible_host' | cut -d: -f2 | xargs) ansible_port=$(echo "$host_config" | grep -m1 '^ansible_port' | cut -d: -f2 | xargs) ansible_user=$(echo "$host_config" | grep -m1 '^ansible_user' | cut -d: -f2 | xargs) ansible_common_args=$(echo "$host_config" | grep -m1 'ansible_ssh_common_args' | cut -d: -f2 | xargs) # Build SSH command using extracted connection details local ssh_command="ssh" if [[ "$ansible_port" != "null" ]] && [[ -n "$ansible_port" ]]; then ssh_command+=" -p $ansible_port" fi if [[ "$ansible_common_args" != "null" ]] && [[ -n "$ansible_common_args" ]]; then ssh_command+=" $ansible_common_args" fi # Use override user if provided, else fall back to ansible_user local effective_user="${override_user:-$ansible_user}" if [[ "$effective_user" != "null" ]] && [[ -n "$effective_user" ]]; then ssh_command+=" $effective_user@$ansible_host" else ssh_command+=" $ansible_host" fi echo "Run command: $ssh_command" $ssh_command } decrypt-file() { if [[ -z "$1" ]]; then echo '$1 is empty'; exit; fi echo "Decrypt file $1." ansible-vault decrypt --output="$1" "$1" } encrypt-file() { if [[ -z "$1" ]]; then echo '$1 is empty'; exit; fi if [[ -z "$2" ]]; then echo '$2 is empty'; exit; fi echo "Encrypt file $2 with vault id $1." ansible-vault encrypt --encrypt-vault-id="$1" "$2" } list-vault() { grep 'vault_identity_list' ansible.cfg | sed 's/^vault_identity_list = //' | tr ',' '\n' | sed '/^$/d' } generate-vault-password() { if [[ -z "$1" ]]; then echo '$1 is empty'; exit; fi if [[ -z "$2" ]]; then echo '$2 is empty'; exit; fi local vault_id="$1" local vaultpassword="$2" export VAULT_ID="$vault_id" export VAULTPASSWORD="$vaultpassword" echo "$vaultpassword" > ".vault_pass_$vault_id" chmod 600 ".vault_pass_$vault_id" echo "Password file .vault_pass_$vault_id created." } encrypt-string() { if [[ -z "$1" ]]; then echo '$1 is empty'; exit; fi if [[ -z "$2" ]]; then echo '$2 is empty'; exit; fi if [[ -z "$3" ]]; then echo '$3 is empty'; exit; fi local string_key="$2" local string_value="$3" # string_key=$(echo "$2" | cut -d ':' -f 1) # string_value=$(echo "$2" | cut -d ':' -f 2 | sed 's/^ *//') if [[ "$4" != "-q" ]]; then echo "Found key: $string_key" echo "Encrypting value: $string_value" fi ansible-vault encrypt_string --encrypt-vault-id="$1" --name "$string_key" "$string_value" } generate-encrypted-string() { if [[ -z "$1" ]]; then echo '$1 is empty'; exit; fi if [[ -z "$2" ]]; then echo '$2 is empty'; exit; fi local string_key="$2" local string_value="$(pwgen -c 20 1)" encrypt-string "$1" "$string_key" "$string_value" } convert-vault-file() { if [[ -z "$1" ]]; then echo '$1 is empty'; exit; fi if [[ -z "$2" ]]; then echo '$2 is empty'; exit; fi local vault_id="$1" local file_path="$2" local temp_file temp_file=$(mktemp) # Decrypt the vault file ansible-vault decrypt --output="$file_path" "$file_path" # Process each key-value pair yq eval '.' "$file_path" | while IFS= read -r line; do local key local value key=$(echo "$line" | cut -d':' -f1) value=$(echo "$line" | cut -d':' -f2-) value=$(echo "$value" | sed 's/^[[:space:]"]*//;s/[[:space:]"]*$//') # Re-encrypt the key-value pair local encrypted encrypted=$(ansible-vault encrypt_string --encrypt-vault-id="$vault_id" --name "$key" "$value" 2>/dev/null) echo "$encrypted" >> "$temp_file" done # Move temp file to vault file mv "$temp_file" "$file_path" echo "Converted file saved as $file_path" } show-vault-file() { if [[ -z "$1" ]]; then echo '$1 is empty'; exit 1; fi if [[ -z "$2" ]]; then echo '$2 is empty'; exit 1; fi local vault_id="$1" local file_path="$2" local temp_file local temp_part_file temp_file=$(mktemp) temp_part_file=$(mktemp) if head -n 1 "$file_path" | grep -q "^\$ANSIBLE_VAULT;1.2;AES256;$vault_id"; then ansible-vault decrypt --output="$temp_file" "$file_path" else local write_finished=false local current_key="" while IFS= read -r line; do if [[ "$line" =~ "!vault" ]]; then # Process previous vault entry if it exists if [[ "$write_finished" = true ]] && [[ -n "$current_key" ]] && [[ -s "$temp_part_file" ]]; then ansible-vault decrypt "$temp_part_file" local value value=$(cat "$temp_part_file") echo "$current_key: $value" >> "$temp_file" fi # Set up for new vault entry current_key=$(echo "$line" | cut -d':' -f1) : > "$temp_part_file" write_finished=true else if [[ "$write_finished" = true ]]; then echo "$line" | sed 's/^[[:space:]"]*//;s/[[:space:]"]*$//' >> "$temp_part_file" fi fi done < "$file_path" # Process the final vault entry if [[ "$write_finished" = true ]] && [[ -n "$current_key" ]] && [[ -s "$temp_part_file" ]]; then ansible-vault decrypt "$temp_part_file" local value value=$(cat "$temp_part_file") echo "$current_key: $value" >> "$temp_file" fi fi bat --paging=always "$temp_file" rm -f "$temp_file" rm -f "$temp_part_file" } # Env activate-venv() { if [[ -f ".venv/bin/activate" ]]; then source ".venv/bin/activate" else echo "Could not find .venv/bin/active" echo "Init venv with $(uv --version)" uv venv fi } install() { echo "Ensure Python version $(cat .python-version) is installed." uv python install activate-venv echo "Install Ansible and Python dependencies" uv pip install -r requirements.txt } install-exo() { curl -fsSL https://raw.githubusercontent.com/exoscale/cli/master/install-latest.sh | sh } visualize-dependencies() { mkdir -p tmp ./bin/visualize-dependencies ./roles open-with-os tmp/ansible-role-dependencies.html } version() { uv --version echo "python $(cat .python-version)" ansible --version } show-passwordless-sudo() { local user="${1:-$USERNAME}" echo "echo \"$user ALL=(ALL) NOPASSWD: ALL\" >> /etc/sudoers" } # Run play() { ansible-playbook "$@" } list-tags() { if [[ -z "$1" ]]; then echo '$1 is empty'; exit 1; fi ansible-playbook "$1" --list-tags } run() { ansible "$@" } upgrade() { echo "Update Ansible" uv pip install --upgrade ansible } lint() { ansible-lint --fix --offline roles/* } upgrade-odoo() { source ./bin/upgrade-odoo upgrade-odoo "$@" } # Report list-hosts() { if [[ -z "$1" ]]; then echo '$1 is empty'; exit; fi local grep_filter="$2" find "$1" -name 'hosts.yml' | sort | while read -r hosts_file; do echo -e "\n<<< $hosts_file >>>\n" local aliases aliases=$(yq e '.. | select(has("ansible_host")) | (path | .[-1])' "$hosts_file") local hosts hosts=$(yq e '.. | select(has("ansible_host")) | .ansible_host' "$hosts_file") local functions functions=$(yq e '.. | select(has("ansible_host")) | .function // ""' "$hosts_file") local customers customers=$(yq e '.. | select(has("ansible_host")) | .customer // ""' "$hosts_file") local providers providers=$(yq e '.. | select(has("ansible_host")) | .hosting_provider // ""' "$hosts_file") if [[ -z "$aliases" || -z "$hosts" ]]; then echo "No hosts found in $hosts_file" continue fi local alias_width=25 local host_width=30 local func_width=25 local cust_width=33 local prov_width=20 local column="| %-${alias_width}s | %-${host_width}s | %-${func_width}s | %-${cust_width}s | %-${prov_width}s |\n" printf "$column" 'Alias' 'Host' 'Function' 'Customer' 'Provider' echo "|$(printf '%*s' $((alias_width + 2)) '' | tr ' ' '-')|$(printf '%*s' $((host_width + 2)) '' | tr ' ' '-')|$(printf '%*s' $((func_width + 2)) '' | tr ' ' '-')|$(printf '%*s' $((cust_width + 2)) '' | tr ' ' '-')|$(printf '%*s' $((prov_width + 2)) '' | tr ' ' '-')|" paste <(echo "$aliases") <(echo "$hosts") <(echo "$functions") <(echo "$customers") <(echo "$providers") | sort -t$'\t' -k1,1 -k4,4r | awk -F'\t' '!seen[$1]++' | while IFS=$'\t' read -r alias host function customer provider; do local row row=$(printf "$column" "$alias" "$host" "$function" "$customer" "$provider") if [[ -z "$grep_filter" ]] || echo "$row" | grep -qi "$grep_filter"; then echo "$row" fi done done } list-roles() { local role_width=25 local kind_width=15 local description_width=50 local column="| %-${role_width}s | %-${kind_width}s | %-${description_width}s |\n" printf "$column" 'Role' 'Kind' 'Description' echo "|$(printf '%*s' $((role_width + 2)) '' | tr ' ' '-')|$(printf '%*s' $((kind_width + 2)) '' | tr ' ' '-')|$(printf '%*s' $((description_width + 2)) '' | tr ' ' '-')|" local md_files md_files=$(find roles -name 'README.md' | sort) local readme for readme in $md_files; do local role role=$(basename "$(dirname "$readme")") local kind kind=$(grep "^kind:" "$readme" | sed 's/kind: //') local description description=$(sed -n '5,$p' "$readme" | grep -v '^$' | grep -v ' $code"; fail=1; } local code=$(curl -o /dev/null -s -w "%{http_code}" -L "http://odoo.local:8080") [[ $code -eq 200 ]] || { echo "http://odoo.local:8080 -> $code"; fail=1; } local code=$(curl -o /dev/null -s -w "%{http_code}" -L "http://odoo.local:8080/website/info") [[ $code -eq 403 ]] || { echo "http://odoo.local:8080/website/info -> $code"; fail=1; } #FIXME: Metabase container does not work GitHub Runner env. # local code=$(curl -o /dev/null -s -w "%{http_code}" -L "http://metabase.local:8080") # [[ $code -eq 200 ]] || { echo "http://metabase.local:8080 -> $code"; fail=1; } local code=$(curl -o /dev/null -s -w "%{http_code}" -L "http://grafana.local:8080") [[ $code -eq 200 ]] || { echo "http://grafana.local:8080 -> $code"; fail=1; } echo "Exit code is: $fail" if [[ $fail -eq 1 ]]; then echo "Show Docker container status..." docker ps # echo "Show Docker container logs..." # containers=$(docker ps -a --format "{{.Names}}:{{.Status}}") # while IFS= read -r container; do # name="${container%%:*}" # echo "<<< $container >>>" # if docker logs --tail 50 --timestamps "$name" 2>/dev/null | grep -q .; then # docker logs --tail 50 --timestamps "$name" 2>/dev/null # else # echo "No logs available for $name" # fi # echo "" # done <<< "$containers" fi exit $fail fi } test-scripts() { set -e if [[ "$1" = "cleanup" ]]; then echo -e "\033[38;5;214mTEST\033[0m: Cleanup test environment." ansible localhost -m ansible.builtin.cron -a 'name="test" state="absent"' ansible-playbook -i test_inventory plays/cleanup.yml -t "odoo,odoo_data,odoo_volume,\ postgres,postgres_data,postgres_volume" else echo -e "\033[38;5;214mTEST\033[0m: Test restic scripts" sudo ./roles/cron/files/install # Add cron job ansible localhost -m ansible.builtin.cron -a 'name="test" minute="0" hour="*/6" job="echo \"test\""' # List cron job and compare output [[ "$(cron-job-list)" == "[1] 0 */6 * * * test" ]] cron-job-run -i 1 echo -e "\033[38;5;214mTEST\033[0m: Test postgres scripts" sudo ./roles/postgres/files/install # Start postgres container ansible-playbook -i test_inventory plays/localhost.yml -t postgres docker-postgres-create -c postgres01 -d test docker-postgres-list -c postgres01 | grep "test" docker-postgres-shell -c postgres01 -d test -s 'CREATE TABLE example(id int);' docker-postgres-duplicate -c postgres01 -s test -t example docker-postgres-drop -c postgres01 -d test docker-postgres-size -c postgres01 -d example | grep "0 bytes" docker-postgres-backup -c postgres01 -d example docker-postgres-restore -c postgres01 -f /var/tmp/postgres01/example.sql -d test -i docker-postgres-size -c postgres01 -d test | grep "0 bytes" mkdir -p tmp echo 'DROP TABLE example;' > tmp/drop.sql docker-postgres-patch -c postgres01 -d test -f ./tmp/drop.sql docker-postgres-rename -c postgres01 -s test -t example -r echo -e "\033[38;5;214mTEST\033[0m: Test odoo scripts" sudo ./roles/odoo/files/install # Start odoo container ansible-playbook -i test_inventory plays/localhost.yml -t odoo --skip-tags depends docker-odoo-init -c odoo01-prod -d odoo-prod -i contacts -w docker-odoo-rename -c odoo01-prod -s odoo-prod -t odoo docker-odoo-list -c odoo01-prod | grep "odoo$" docker-odoo-user -c odoo01-prod -d odoo -a list | grep "admin" docker-odoo-drop -c odoo01-int -d odoo-int docker-odoo-duplicate -c odoo01-prod -s odoo -t odoo-int docker-odoo-neutralize -c odoo01-int -d odoo-int docker exec odoo01-int clone-git-addons docker restart odoo01-int docker-odoo-init -c odoo01-int -d odoo-int -i partner_firstname docker-odoo-shell -c odoo01-int -d odoo-int -f -p 'env["res.partner"].browse(1).write({"name": "admint"})' [[ "admint" == $(docker-odoo-shell -c odoo01-int -d odoo-int -p 'print(env["res.partner"].browse(1).name)') ]] docker-odoo-upgrade -c postgres01 -d odoo-int -s "18.0" -n upgrade -t "19.0" -m test -l "$ODOO_ENTERPRISE_CODE" echo -e "\033[38;5;214mTEST\033[0m: Test docker volume scripts" docker stop postgres01 docker-volume-backup -c postgres01 -v postgres01 docker-volume-backup -c postgres01 -a docker rm -f postgres01 docker volume rm postgres01 docker-volume-restore -v postgres01 -f /var/tmp/postgres01/postgres01.tar ansible-playbook -i test_inventory plays/localhost.yml -t postgres --skip-tags depends docker-postgres-list -c postgres01 | grep "example" fi } # Docs update-docs() { echo "Update roles.md" echo "# Roles" > "docs/roles.md" echo "" >> "docs/roles.md" echo "| Logo | Role | Kind | Description |" >> "docs/roles.md" echo "| --- | --- | --- | --- |" >> "docs/roles.md" local md_files=$(find roles -name 'README.md' | sort) for readme in $md_files; do local role role=$(basename "$(dirname "$readme")") local kind kind=$(grep "^kind:" "$readme" | sed 's/kind: //') local description description=$(sed -n '5,$p' "$readme" | grep -v '^$' | grep -v '> "docs/roles.md" done } format-role-readmes() { local md_files=$(find roles -name 'README.md' | sort) for readme in $md_files; do local role role=$(basename "$(dirname "$readme")") echo "Update README of $role role." local kind kind=$(grep "^kind:" "$readme" | sed 's/kind: //') local logo_line="\"$role" local tmpfile tmpfile=$(mktemp) # Check if README already starts with frontmatter if head -1 "$readme" | grep -q "^---$"; then # Already has frontmatter at top, rebuild with single logo { echo "---" echo "kind: $kind" echo "---" echo "" echo "$logo_line" echo "" # Get content after frontmatter (skip first 5 lines), remove duplicate logo tail -n +6 "$readme" | grep -v "^ "$tmpfile" && mv "$tmpfile" "$readme" else # No frontmatter at top, add it and remove duplicates { echo "---" echo "kind: $kind" echo "---" echo "" echo "$logo_line" echo "" # Skip first 8 lines (logo, blank, frontmatter 3 lines, blank, logo, blank) tail -n +9 "$readme" } > "$tmpfile" && mv "$tmpfile" "$readme" fi done } install-vuepress() { pnpm install } dev-vuepress() { echo "Copy logos folder to vuepress public folder." cp -r logos .vuepress/public/ open-url http://localhost:8080 pnpm run dev } build-vuepress() { echo "Copy logos folder to vuepress public folder." cp -r logos .vuepress/public/ pnpm run build echo "Create vuepress script folders." mkdir -p .vuepress/dist/ansible_scripts mkdir -p .vuepress/dist/cron mkdir -p .vuepress/dist/certbot mkdir -p .vuepress/dist/docker mkdir -p .vuepress/dist/docker_volume mkdir -p .vuepress/dist/grafana mkdir -p .vuepress/dist/mysql mkdir -p .vuepress/dist/nginx mkdir -p .vuepress/dist/odoo mkdir -p .vuepress/dist/postgres echo "Copy script files to vuepress folder." cp -r roles/ansible_scripts/files/* .vuepress/dist/ansible_scripts cp -r roles/cron/files/* .vuepress/dist/cron cp -r roles/certbot/files/* .vuepress/dist/certbot cp -r roles/docker/files/* .vuepress/dist/docker cp -r roles/docker_volume/files/* .vuepress/dist/docker_volume cp -r roles/grafana/files/* .vuepress/dist/grafana cp -r roles/mysql/files/* .vuepress/dist/mysql cp -r roles/nginx/files/* .vuepress/dist/nginx cp -r roles/odoo/files/* .vuepress/dist/odoo cp -r roles/postgres/files/* .vuepress/dist/postgres } serve-vuepress() { cd .vuepress/dist pnpx serve } resize-logos() { cd logos || return 1 if [[ -n "$1" ]] && [[ -f "$1" ]]; then magick "$1" -resize 200x200\! "$1" echo "Resized: $1" else local file for file in *.png; do [[ -e "$file" ]] && magick "$file" -resize 200x200\! "$file" && echo "Resized: $file" done fi } convert-logos() { cd logos || return 1 local file local base for file in *.jpg; do if [[ -e "$file" ]]; then base="${file%.jpg}" magick "$file" "${base}.png" echo "Converted: $file -> ${base}.png" fi done } if [[ "$1" == *","* ]]; then # Split comma-separated commands IFS=',' read -ra COMMANDS <<< "$1" for cmd in "${COMMANDS[@]}"; do # Trim whitespace cmd=$(echo "$cmd" | xargs) if declare -f "$cmd" > /dev/null; then "$cmd" else echo "Unknown command: $cmd" help exit 1 fi done elif declare -f "$1" > /dev/null; then activate-venv || true "$1" "${@:2}" else case "$1" in *) echo "Unknown command: $1" help exit 1 ;; esac fi