From 2601041008e614c3e3e11075935066f112d554ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Wed, 17 Jun 2026 19:30:07 +0200 Subject: [PATCH] chore: squash history --- .editorconfig | 16 + .gitconfig | 18 + .gitignore | 24 + .hushlogin | 0 .nvmrc | 1 + .p10k.zsh | 193 +++++++ .python-version | 1 + .ruby-version | 1 + .zlogin | 0 .zlogout | 0 .zprofile | 21 + .zshenv | 0 .zshrc | 217 ++++++++ LICENSE | 21 + README.md | 281 ++++++++++ arty.yml | 73 +++ scripts/arty.sh | 1326 +++++++++++++++++++++++++++++++++++++++++++++++ signature.txt | 10 + 18 files changed, 2203 insertions(+) create mode 100755 .editorconfig create mode 100755 .gitconfig create mode 100755 .gitignore create mode 100644 .hushlogin create mode 100644 .nvmrc create mode 100644 .p10k.zsh create mode 100644 .python-version create mode 100644 .ruby-version create mode 100755 .zlogin create mode 100755 .zlogout create mode 100755 .zprofile create mode 100644 .zshenv create mode 100644 .zshrc create mode 100644 LICENSE create mode 100755 README.md create mode 100644 arty.yml create mode 100755 scripts/arty.sh create mode 100644 signature.txt diff --git a/.editorconfig b/.editorconfig new file mode 100755 index 0000000..f38cff4 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,16 @@ +# EditorConfig is awesome: https://editorconfig.org + +# top-most EditorConfig file +root = true + +# Unix-style newlines with a newline ending every file +[*] +end_of_line = lf +insert_final_newline = true +indent_size = 2 +indent_style = space +charset = utf-8 + +# 4 space indentation +[*.py] +indent_size = 4 diff --git a/.gitconfig b/.gitconfig new file mode 100755 index 0000000..782d500 --- /dev/null +++ b/.gitconfig @@ -0,0 +1,18 @@ +[user] + name = Sebastian Krüger + email = valknar@pivoine.art +[filter "lfs"] + clean = git-lfs clean -- %f + smudge = git-lfs smudge -- %f + process = git-lfs filter-process + required = true +[core] + editor = nano +[init] + defaultBranch = main +[alias] + squash-all = "!f(){ git reset $(git commit-tree \"HEAD^{tree}\" \"$@\");};f" +[push] + autoSetupRemote = true +[pull] + ff = only diff --git a/.gitignore b/.gitignore new file mode 100755 index 0000000..1dbac38 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Ignore everything +* + +# But not these files. +!.gitignore +!.gitconfig +!.editorconfig +!.nvmrc +!.ruby-version +!.python-version +!.p10k.zsh +!.zprofile +!.zlogout +!.zlogin +!.zshrc +!.zshenv +!.hushlogin +!arty.yml +!signature.txt +!LICENSE +!README.md + +!scripts/ +!scripts/**/* \ No newline at end of file diff --git a/.hushlogin b/.hushlogin new file mode 100644 index 0000000..e69de29 diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..f62f0b2 --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v20.19.1 diff --git a/.p10k.zsh b/.p10k.zsh new file mode 100644 index 0000000..b9a4ffb --- /dev/null +++ b/.p10k.zsh @@ -0,0 +1,193 @@ +# Generated by Powerlevel10k configuration wizard on 2025-09-04 at 02:33 CEST. +# Based on romkatv/powerlevel10k/config/p10k-pure.zsh, checksum 07533. +# Wizard options: nerdfont-v3 + powerline, small icons, pure, snazzy, 24h time, 1 line, +# compact, instant_prompt=verbose. +# Type `p10k configure` to generate another config. +# +# Config file for Powerlevel10k with the style of Pure (https://github.com/sindresorhus/pure). +# +# Differences from Pure: +# +# - Git: +# - `@c4d3ec2c` instead of something like `v1.4.0~11` when in detached HEAD state. +# - No automatic `git fetch` (the same as in Pure with `PURE_GIT_PULL=0`). +# +# Apart from the differences listed above, the replication of Pure prompt is exact. This includes +# even the questionable parts. For example, just like in Pure, there is no indication of Git status +# being stale; prompt symbol is the same in command, visual and overwrite vi modes; when prompt +# doesn't fit on one line, it wraps around with no attempt to shorten it. +# +# If you like the general style of Pure but not particularly attached to all its quirks, type +# `p10k configure` and pick "Lean" style. This will give you slick minimalist prompt while taking +# advantage of Powerlevel10k features that aren't present in Pure. + +# Temporarily change options. +'builtin' 'local' '-a' 'p10k_config_opts' +[[ ! -o 'aliases' ]] || p10k_config_opts+=('aliases') +[[ ! -o 'sh_glob' ]] || p10k_config_opts+=('sh_glob') +[[ ! -o 'no_brace_expand' ]] || p10k_config_opts+=('no_brace_expand') +'builtin' 'setopt' 'no_aliases' 'no_sh_glob' 'brace_expand' + +() { + emulate -L zsh -o extended_glob + + # Unset all configuration options. + unset -m '(POWERLEVEL9K_*|DEFAULT_USER)~POWERLEVEL9K_GITSTATUS_DIR' + + # Zsh >= 5.1 is required. + [[ $ZSH_VERSION == (5.<1->*|<6->.*) ]] || return + + # Prompt colors. + local grey='242' + local red='#FF5C57' + local yellow='#F3F99D' + local blue='#57C7FF' + local magenta='#FF6AC1' + local cyan='#9AEDFE' + local white='#F1F1F0' + + # Left prompt segments. + typeset -g POWERLEVEL9K_LEFT_PROMPT_ELEMENTS=( + context # user@host + dir # current directory + vcs # git status + command_execution_time # previous command duration + # virtualenv # python virtual environment + prompt_char # prompt symbol + ) + + # Right prompt segments. + typeset -g POWERLEVEL9K_RIGHT_PROMPT_ELEMENTS=( + # command_execution_time # previous command duration + # virtualenv # python virtual environment + # context # user@host + time # current time + ) + + # Basic style options that define the overall prompt look. + typeset -g POWERLEVEL9K_BACKGROUND= # transparent background + typeset -g POWERLEVEL9K_{LEFT,RIGHT}_{LEFT,RIGHT}_WHITESPACE= # no surrounding whitespace + typeset -g POWERLEVEL9K_{LEFT,RIGHT}_SUBSEGMENT_SEPARATOR=' ' # separate segments with a space + typeset -g POWERLEVEL9K_{LEFT,RIGHT}_SEGMENT_SEPARATOR= # no end-of-line symbol + typeset -g POWERLEVEL9K_VISUAL_IDENTIFIER_EXPANSION= # no segment icons + + # Add an empty line before each prompt except the first. This doesn't emulate the bug + # in Pure that makes prompt drift down whenever you use the Alt-C binding from fzf or similar. + typeset -g POWERLEVEL9K_PROMPT_ADD_NEWLINE=false + + # Magenta prompt symbol if the last command succeeded. + typeset -g POWERLEVEL9K_PROMPT_CHAR_OK_{VIINS,VICMD,VIVIS}_FOREGROUND=$magenta + # Red prompt symbol if the last command failed. + typeset -g POWERLEVEL9K_PROMPT_CHAR_ERROR_{VIINS,VICMD,VIVIS}_FOREGROUND=$red + # Default prompt symbol. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIINS_CONTENT_EXPANSION='❯' + # Prompt symbol in command vi mode. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VICMD_CONTENT_EXPANSION='❮' + # Prompt symbol in visual vi mode is the same as in command mode. + typeset -g POWERLEVEL9K_PROMPT_CHAR_{OK,ERROR}_VIVIS_CONTENT_EXPANSION='❮' + # Prompt symbol in overwrite vi mode is the same as in command mode. + typeset -g POWERLEVEL9K_PROMPT_CHAR_OVERWRITE_STATE=false + + # Grey Python Virtual Environment. + typeset -g POWERLEVEL9K_VIRTUALENV_FOREGROUND=$grey + # Don't show Python version. + typeset -g POWERLEVEL9K_VIRTUALENV_SHOW_PYTHON_VERSION=false + typeset -g POWERLEVEL9K_VIRTUALENV_{LEFT,RIGHT}_DELIMITER= + + # Blue current directory. + typeset -g POWERLEVEL9K_DIR_FOREGROUND=$blue + + # Context format when root: user@host. The first part white, the rest grey. + typeset -g POWERLEVEL9K_CONTEXT_ROOT_TEMPLATE="%F{$white}%n%f%F{$grey}@%m%f" + # Context format when not root: user@host. The whole thing grey. + typeset -g POWERLEVEL9K_CONTEXT_TEMPLATE="%F{$grey}%n@%m%f" + # Don't show context unless root or in SSH. + typeset -g POWERLEVEL9K_CONTEXT_{DEFAULT,SUDO}_CONTENT_EXPANSION= + + # Show previous command duration only if it's >= 5s. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_THRESHOLD=5 + # Don't show fractional seconds. Thus, 7s rather than 7.3s. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_PRECISION=0 + # Duration format: 1d 2h 3m 4s. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FORMAT='d h m s' + # Yellow previous command duration. + typeset -g POWERLEVEL9K_COMMAND_EXECUTION_TIME_FOREGROUND=$yellow + + # Grey Git prompt. This makes stale prompts indistinguishable from up-to-date ones. + typeset -g POWERLEVEL9K_VCS_FOREGROUND=$grey + + # Disable async loading indicator to make directories that aren't Git repositories + # indistinguishable from large Git repositories without known state. + typeset -g POWERLEVEL9K_VCS_LOADING_TEXT= + + # Don't wait for Git status even for a millisecond, so that prompt always updates + # asynchronously when Git state changes. + typeset -g POWERLEVEL9K_VCS_MAX_SYNC_LATENCY_SECONDS=0 + + # Cyan ahead/behind arrows. + typeset -g POWERLEVEL9K_VCS_{INCOMING,OUTGOING}_CHANGESFORMAT_FOREGROUND=$cyan + # Don't show remote branch, current tag or stashes. + typeset -g POWERLEVEL9K_VCS_GIT_HOOKS=(vcs-detect-changes git-untracked git-aheadbehind) + # Don't show the branch icon. + typeset -g POWERLEVEL9K_VCS_BRANCH_ICON= + # When in detached HEAD state, show @commit where branch normally goes. + typeset -g POWERLEVEL9K_VCS_COMMIT_ICON='@' + # Don't show staged, unstaged, untracked indicators. + typeset -g POWERLEVEL9K_VCS_{STAGED,UNSTAGED,UNTRACKED}_ICON= + # Show '*' when there are staged, unstaged or untracked files. + typeset -g POWERLEVEL9K_VCS_DIRTY_ICON='*' + # Show '⇣' if local branch is behind remote. + typeset -g POWERLEVEL9K_VCS_INCOMING_CHANGES_ICON=':⇣' + # Show '⇡' if local branch is ahead of remote. + typeset -g POWERLEVEL9K_VCS_OUTGOING_CHANGES_ICON=':⇡' + # Don't show the number of commits next to the ahead/behind arrows. + typeset -g POWERLEVEL9K_VCS_{COMMITS_AHEAD,COMMITS_BEHIND}_MAX_NUM=1 + # Remove space between '⇣' and '⇡' and all trailing spaces. + typeset -g POWERLEVEL9K_VCS_CONTENT_EXPANSION='${${${P9K_CONTENT/⇣* :⇡/⇣⇡}// }//:/ }' + + # Grey current time. + typeset -g POWERLEVEL9K_TIME_FOREGROUND=$grey + # Format for the current time: 09:51:02. See `man 3 strftime`. + typeset -g POWERLEVEL9K_TIME_FORMAT='%D{%H:%M:%S}' + # If set to true, time will update when you hit enter. This way prompts for the past + # commands will contain the start times of their commands rather than the end times of + # their preceding commands. + typeset -g POWERLEVEL9K_TIME_UPDATE_ON_COMMAND=false + + # Transient prompt works similarly to the builtin transient_rprompt option. It trims down prompt + # when accepting a command line. Supported values: + # + # - off: Don't change prompt when accepting a command line. + # - always: Trim down prompt when accepting a command line. + # - same-dir: Trim down prompt when accepting a command line unless this is the first command + # typed after changing current working directory. + typeset -g POWERLEVEL9K_TRANSIENT_PROMPT=off + + # Instant prompt mode. + # + # - off: Disable instant prompt. Choose this if you've tried instant prompt and found + # it incompatible with your zsh configuration files. + # - quiet: Enable instant prompt and don't print warnings when detecting console output + # during zsh initialization. Choose this if you've read and understood + # https://github.com/romkatv/powerlevel10k#instant-prompt. + # - verbose: Enable instant prompt and print a warning when detecting console output during + # zsh initialization. Choose this if you've never tried instant prompt, haven't + # seen the warning, or if you are unsure what this all means. + typeset -g POWERLEVEL9K_INSTANT_PROMPT=off + + # Hot reload allows you to change POWERLEVEL9K options after Powerlevel10k has been initialized. + # For example, you can type POWERLEVEL9K_BACKGROUND=red and see your prompt turn red. Hot reload + # can slow down prompt by 1-2 milliseconds, so it's better to keep it turned off unless you + # really need it. + typeset -g POWERLEVEL9K_DISABLE_HOT_RELOAD=off + + # If p10k is already loaded, reload configuration. + # This works even with POWERLEVEL9K_DISABLE_HOT_RELOAD=true. + (( ! $+functions[p10k] )) || p10k reload +} + +# Tell `p10k configure` which file it should overwrite. +typeset -g POWERLEVEL9K_CONFIG_FILE=${${(%):-%x}:a} + +(( ${#p10k_config_opts} )) && setopt ${p10k_config_opts[@]} +'builtin' 'unset' 'p10k_config_opts' diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..47b322c --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.4.1 diff --git a/.zlogin b/.zlogin new file mode 100755 index 0000000..e69de29 diff --git a/.zlogout b/.zlogout new file mode 100755 index 0000000..e69de29 diff --git a/.zprofile b/.zprofile new file mode 100755 index 0000000..ffed80f --- /dev/null +++ b/.zprofile @@ -0,0 +1,21 @@ +SSH_ENV="$HOME/.ssh/agent-environment" + +function start_agent { + echo "Initialising new SSH agent..." + /usr/bin/ssh-agent | sed 's/^echo/#echo/' >"$SSH_ENV" + echo succeeded + chmod 600 "$SSH_ENV" + . "$SSH_ENV" >/dev/null + /usr/bin/ssh-add; +} + +# Source SSH settings, if applicable +if [ -f "$SSH_ENV" ]; then + . "$SSH_ENV" >/dev/null + #ps $SSH_AGENT_PID doesn't work under Cygwin + ps -ef | grep $SSH_AGENT_PID | grep ssh-agent$ >/dev/null || { + start_agent + } +else + start_agent +fi \ No newline at end of file diff --git a/.zshenv b/.zshenv new file mode 100644 index 0000000..e69de29 diff --git a/.zshrc b/.zshrc new file mode 100644 index 0000000..7aa7f2a --- /dev/null +++ b/.zshrc @@ -0,0 +1,217 @@ +# Source .env file early to export environment variables +if [ -f "$HOME/.env" ] ; then + set -a + source "$HOME/.env" + set +a +fi + +fpath=(~/.zsh/completions $fpath) + +if [ -d "$HOME/bin" ]; then + export PATH="$HOME/bin:$PATH" +fi + +if [ -d "$HOME/.local/bin" ]; then + export PATH="$HOME/.local/bin:$PATH" +fi + +if [ -d "$HOME/.rbenv/bin" ]; then + export PATH="$PATH:$HOME/.rbenv/bin" +fi + +if [ -d "$HOME/.pyenv/bin" ]; then + export PYENV_ROOT="$HOME/.pyenv" + export PATH="$PYENV_ROOT/bin:$PATH" +fi + +if [ -d "$HOME/.cargo/bin" ]; then + export PATH="$PATH:$HOME/.cargo/bin" +fi + +if [ -d "$HOME/.gvm/bin" ]; then + export PATH="$PATH:$HOME/.gvm/bin" +fi + +if [ -d "/usr/local/go/bin" ]; then + export PATH="$PATH:/usr/local/go/bin" +fi + +if [ -d "$HOME/go/bin" ]; then + export PATH="$PATH:$HOME/go/bin" +fi + +if [ -d "$HOME/node_modules/.bin" ]; then + export PATH="$PATH:$HOME/node_modules/.bin" +fi + +if [ -d "$HOME/scripts" ]; then + export PATH="$HOME/scripts:$PATH" +fi + +if [ -d "$HOME/stacks" ]; then + export PATH="$HOME/stacks:$PATH" +fi + +if [ -n "$BASH_VERSION" ]; then + # include .bashrc if it exists + if [ -f "$HOME/.bashrc" ]; then + . "$HOME/.bashrc" + fi +fi + +if [ -s "$NVM_DIR/nvm.sh" ] ; then + . "$NVM_DIR/nvm.sh" +fi + +if [ -s "$NVM_DIR/bash_completion" ] ; then + . "$NVM_DIR/bash_completion" +fi + +if [ -s "$HOME/.rvm/scripts/rvm" ] ; then + . "$HOME/.rvm/scripts/rvm" +fi + +if [ -s "$HOME/.cargo/env" ] ; then + . "$HOME/.cargo/env" +fi + +if [ -s "$HOME/.gvm/scripts/gvm" ] ; then + . "$HOME/.gvm/scripts/gvm" +fi + +if [ -s "$HOME/.sdkman/bin/sdkman-init.sh" ] ; then + export SDKMAN_DIR="$HOME/.sdkman" + . "$HOME/.sdkman/bin/sdkman-init.sh" +fi + + +if command -v oh-my-posh 2>&1 >/dev/null; then + eval "$(! oh-my-posh init zsh --config=~/worker.omp.json)" +fi + +if command -v rbenv 2>&1 >/dev/null; then + eval "$(rbenv init - --no-rehash zsh)" +fi + +if command -v pyenv 2>&1 >/dev/null; then + eval "$(pyenv init --path)" +fi + + +# Enable Powerlevel10k instant prompt. Should stay close to the top of ~/.zshrc. +# Initialization code that may require console input (password prompts, [y/n] +# confirmations, etc.) must go above this block; everything else may go below. +if [[ -r "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" ]]; then + source "${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh" +fi + +# If you come from bash you might have to change your $PATH. +# export PATH=$HOME/bin:$HOME/.local/bin:/usr/local/bin:$PATH + +# Path to your Oh My Zsh installation. +export ZSH="$HOME/.oh-my-zsh" + +# Set name of the theme to load --- if set to "random", it will +# load a random theme each time Oh My Zsh is loaded, in which case, +# to know which specific one was loaded, run: echo $RANDOM_THEME +# See https://github.com/ohmyzsh/ohmyzsh/wiki/Themes +ZSH_THEME="powerlevel10k/powerlevel10k" + +# Set list of themes to pick from when loading at random +# Setting this variable when ZSH_THEME=random will cause zsh to load +# a theme from this variable instead of looking in $ZSH/themes/ +# If set to an empty array, this variable will have no effect. +# ZSH_THEME_RANDOM_CANDIDATES=( "robbyrussell" "agnoster" ) + +# Uncomment the following line to use case-sensitive completion. +# CASE_SENSITIVE="true" + +# Uncomment the following line to use hyphen-insensitive completion. +# Case-sensitive completion must be off. _ and - will be interchangeable. +# HYPHEN_INSENSITIVE="true" + +# Uncomment one of the following lines to change the auto-update behavior +# zstyle ':omz:update' mode disabled # disable automatic updates +# zstyle ':omz:update' mode auto # update automatically without asking +# zstyle ':omz:update' mode reminder # just remind me to update when it's time + +# Uncomment the following line to change how often to auto-update (in days). +# zstyle ':omz:update' frequency 13 + +# Uncomment the following line if pasting URLs and other text is messed up. +# DISABLE_MAGIC_FUNCTIONS="true" + +# Uncomment the following line to disable colors in ls. +# DISABLE_LS_COLORS="true" + +# Uncomment the following line to disable auto-setting terminal title. +# DISABLE_AUTO_TITLE="true" + +# Uncomment the following line to enable command auto-correction. +# ENABLE_CORRECTION="true" + +# Uncomment the following line to display red dots whilst waiting for completion. +# You can also set it to another string to have that shown instead of the default red dots. +# e.g. COMPLETION_WAITING_DOTS="%F{yellow}waiting...%f" +# Caution: this setting can cause issues with multiline prompts in zsh < 5.7.1 (see #5765) +# COMPLETION_WAITING_DOTS="true" + +# Uncomment the following line if you want to disable marking untracked files +# under VCS as dirty. This makes repository status check for large repositories +# much, much faster. +# DISABLE_UNTRACKED_FILES_DIRTY="true" + +# Uncomment the following line if you want to change the command execution time +# stamp shown in the history command output. +# You can set one of the optional three formats: +# "mm/dd/yyyy"|"dd.mm.yyyy"|"yyyy-mm-dd" +# or set a custom format using the strftime function format specifications, +# see 'man strftime' for details. +# HIST_STAMPS="mm/dd/yyyy" + +# Would you like to use another custom folder than $ZSH/custom? +# ZSH_CUSTOM=/path/to/new-custom-folder + +# Which plugins would you like to load? +# Standard plugins can be found in $ZSH/plugins/ +# Custom plugins may be added to $ZSH_CUSTOM/plugins/ +# Example format: plugins=(rails git textmate ruby lighthouse) +# Add wisely, as too many plugins slow down shell startup. +plugins=(git pm2 gh sudo ssh ruby rust python node github rsync nvm rbenv pyenv docker docker-compose qrcode zsh-autosuggestions zsh-syntax-highlighting zsh-interactive-cd zsh-navigation-tools) + +[[ ! -f $ZSH/oh-my-zsh.sh ]] || source $ZSH/oh-my-zsh.sh + +# User configuration + +# export MANPATH="/usr/local/man:$MANPATH" + +# You may need to manually set your language environment +# export LANG=en_US.UTF-8 + +# Preferred editor for local and remote sessions +# if [[ -n $SSH_CONNECTION ]]; then +# export EDITOR='vim' +# else +# export EDITOR='nvim' +# fi + +# Compilation flags +# export ARCHFLAGS="-arch $(uname -m)" + +# Set personal aliases, overriding those provided by Oh My Zsh libs, +# plugins, and themes. Aliases can be placed here, though Oh My Zsh +# users are encouraged to define aliases within a top-level file in +# the $ZSH_CUSTOM folder, with .zsh extension. Examples: +# - $ZSH_CUSTOM/aliases.zsh +# - $ZSH_CUSTOM/macos.zsh +# For a full list of active aliases, run `alias`. +# +# Example aliases +# alias zshconfig="mate ~/.zshrc" +# alias ohmyzsh="mate ~/.oh-my-zsh" + +alias arty="arty.sh" +alias stacks="stacks.sh" + +# To customize prompt, run `p10k configure` or edit ~/.p10k.zsh. +[[ ! -f ~/.p10k.zsh ]] || source ~/.p10k.zsh diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..370c70e --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Valknar + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100755 index 0000000..dd5bf70 --- /dev/null +++ b/README.md @@ -0,0 +1,281 @@ +
+ +
+ _    _____    __    __ __ _   _____    ____ _ _____
+| |  / /   |  / /   / //_// | / /   |  / __ ( ) ___/
+| | / / /| | / /   / ,<  /  |/ / /| | / /_/ //\__ \
+| |/ / ___ |/ /___/ /| |/ /|  / ___ |/ _, _/ ___/ /
+|___/_/  |_/_____/_/ |_/_/ |_/_/  |_/_/ |_| /____/
+
+    __________  ____  ____________
+   / ____/ __ \/ __ \/ ____/ ____/
+  / /_  / / / / /_/ / / __/ __/
+ / __/ / /_/ / _, _/ /_/ / /___
+/_/    \____/_/ |_|\____/_____/
+
+ +# WHERE CODE MEETS CHAOS + +[![Debian](https://img.shields.io/badge/Debian-Trixie-A81D33?style=for-the-badge&logo=debian&logoColor=white)](https://www.debian.org/) +[![Powered by Metal](https://img.shields.io/badge/POWERED%20BY-METAL-FF0000?style=for-the-badge)](https://www.slayer.net/) +[![Built with Blood](https://img.shields.io/badge/BUILT%20WITH-BLOOD%20%26%20SWEAT-8B0000?style=for-the-badge)](/) +[![License: MIT](https://img.shields.io/badge/License-MIT-990000?style=for-the-badge)](LICENSE) + +**My Debian home directory - forged in the fires of chaos, tempered with configuration files,** +**and wielded with the fury of a thousand riffs.** + +*This is where dotfiles headbang and shell scripts scream.* + +
+ +--- + +## THE ARSENAL + +### WEAPONS OF MASS DEVELOPMENT + +``` ++-----------------------------------------------------+ +| NODE.JS | Managed by nvm | +| PYTHON | Managed by pyenv | +| RUBY | Managed by rbenv | +| RUST | Managed by rustup | +| DOCKER | Containerized destruction | ++-----------------------------------------------------+ +``` + +--- + +## QUICK START RITUAL + +### Summoning the Environment + +```bash +# 1. Clone this unholy repository +git init && git remote add origin git@dev.pivoine.art:2222/valknar/home.git +git fetch && git reset --hard origin/main +git branch --set-upstream-to=origin/main main + +# 2. Configure git +git config --global init.defaultBranch main +git config --global --add safe.directory /home/$USER + +# 3. Sync all dependencies (version managers, shell config, etc.) +arty.sh sync + +# 4. Install system packages +arty.sh debian/update && arty.sh debian/install + +# 5. Install development tools +arty.sh rust/install +arty.sh node/install +arty.sh ruby/install +arty.sh python/install + +# 6. Install global packages +arty.sh npm/install +arty.sh gem/install + +# 7. Source the shell configuration +source ~/.zshrc +``` + +--- + +## arty.sh - REPOSITORY ORCHESTRATION + +### Available Scripts + +```bash +# System setup +arty.sh debian/update # Enable non-free repos and update sources +arty.sh debian/install # curl, wget, zsh, fzf, davfs2, make, imagemagick, ffmpeg, yt-dlp, pkg-config, zstd, libffi-dev, libyaml-dev, libssl-dev, liblzma-dev, libbz2-dev, libncurses-dev, libreadline-dev, libsqlite3-dev, python3-tk + +# Development tools +arty.sh docker/install # Docker Engine with user group +arty.sh rust/install # Rust toolchain via rustup +arty.sh node/install # Node.js via nvm +arty.sh ruby/install # Ruby via rbenv +arty.sh python/install # Python via pyenv +arty.sh go/install # Go via apt +arty.sh hugo/install # Hugo extended via go install + +# CLI tools +arty.sh claude/install # Claude AI CLI +arty.sh yq/install # yq YAML processor +arty.sh gh/install # GitHub CLI +arty.sh tailscale/install # Tailscale +arty.sh ollama/install # Ollama +``` + +### What arty.sh Manages + +- **Version managers** (nvm, rbenv, pyenv) +- **Shell configuration** (Oh-My-Zsh, Powerlevel10k, plugins) + +--- + +## COMMAND LINE BRUTALITY + +### Custom Scripts + +Scripts in `~/scripts/` are loaded via PATH. Check `scripts/` directory for available utilities. + +--- + +## ENVIRONMENT STRUCTURE + +``` +~/ ++-- .nvm/ # Node Version Manager ++-- .rbenv/ # Ruby Version Manager +| +-- plugins/ruby-build/ ++-- .pyenv/ # Python Version Manager ++-- .oh-my-zsh/ # Oh-My-Zsh framework +| +-- custom/ +| +-- themes/ +| | +-- powerlevel10k/ +| +-- plugins/ +| +-- zsh-autosuggestions/ +| +-- zsh-syntax-highlighting/ +``` + +--- + +## DOTFILE HIGHLIGHTS + +### Shell Configuration +- **`.zshrc`** - Oh-My-Zsh with Powerlevel10k theme +- **`.zlogin`** - Login script (mounts HiDrive, creates symbolic links) +- **`.p10k.zsh`** - Powerlevel10k configuration +- **`.zprofile`**, **`.zlogout`**, **`.zshenv`** - Zsh startup files + +### Version Files +- **`.nvmrc`** - Node.js version +- **`.ruby-version`** - Ruby version +- **`.python-version`** - Python version + +### Git Configuration +- **`.gitignore`** - INVERTED PATTERN (ignore all, allow specific files) +- **`.gitconfig`** - Git user configuration + +### Personal +- **`signature.txt`** - ASCII art email signature +- **`.hushlogin`** - Suppress login banners + +--- + +## GIT SELECTIVE TRACKING + +This repository uses an **inverted `.gitignore`** pattern: + +```gitignore +# Ignore everything +* + +# But not these files. +!.gitignore +!.gitconfig +!.editorconfig +!.nvmrc +!.ruby-version +!.python-version +!.p10k.zsh +!.zprofile +!.zlogout +!.zlogin +!.zshrc +!.zshenv +!.hushlogin +!arty.yml +!signature.txt +!LICENSE +!README.md + +!scripts/ +!scripts/**/* +# ... +``` + +**Why?** To track only essential dotfiles and configurations while ignoring cache, logs, and user data. + +--- + +## SHELL PLUGIN POWER + +**Oh-My-Zsh Plugins Loaded:** +``` +git pm2 gh sudo ssh ruby rust python node github +rsync nvm rbenv pyenv docker docker-compose qrcode +zsh-autosuggestions zsh-syntax-highlighting +zsh-interactive-cd zsh-navigation-tools +``` + +--- + +## USEFUL RESOURCES + +### System & Shell +- [Oh-My-Zsh](https://ohmyz.sh/) +- [Powerlevel10k](https://github.com/romkatv/powerlevel10k) + +### Language Managers +- [nvm](https://github.com/nvm-sh/nvm) - Node Version Manager +- [rbenv](https://github.com/rbenv/rbenv) - Ruby Version Manager +- [pyenv](https://github.com/pyenv/pyenv) - Python Version Manager +- [rustup](https://rustup.rs/) - Rust Toolchain Manager + +### CLI Tools +- [GitHub CLI](https://cli.github.com/) +- [Tailscale](https://tailscale.com/) +- [Ollama](https://ollama.com/) +- [yq](https://github.com/mikefarah/yq) - YAML processor + +--- + +## LICENSE + +MIT License - Do whatever the hell you want with it. + +--- + +
+ +
+=================================================================
+
+__________  ____  ______   _________
+  / ____/ __ \/ __ \/ ____/  /  _/ ___/
+ / /   / / / / / / / __/     / / \__ \
+/ /___/ /_/ / /_/ / /___   _/ / ___/ /
+\____/\____/_____/_____/  /___//____/
+
+  ______________  _______  ____  ____  ___    ______  __
+ /_  __/ ____/  |/  / __ \/ __ \/ __ \/   |  / __ \ \/ /
+  / / / __/ / /|_/ / /_/ / / / / /_/ / /| | / /_/ /\  /
+ / / / /___/ /  / / ____/ /_/ / _, _/ ___ |/ _, _/ / /
+/_/ /_____/_/  /_/_/    \____/_/ |_/_/  |_/_/ |_| /_/
+
+    __  __________________    __       _________
+   /  |/  / ____/_  __/   |  / /      /  _/ ___/
+  / /|_/ / __/   / / / /| | / /       / / \__ \
+ / /  / / /___  / / / ___ |/ /___   _/ / ___/ /
+/_/  /_/_____/ /_/ /_/  |_/_____/  /___//____/
+
+    ______________________  _   _____    __
+   / ____/_  __/ ____/ __ \/ | / /   |  / /
+  / __/   / / / __/ / /_/ /  |/ / /| | / /
+ / /___  / / / /___/ _, _/ /|  / ___ |/ /___
+/_____/ /_/ /_____/_/ |_/_/ |_/_/  |_/_____/
+
+=================================================================
+
+FORGED BY VALKNAR
+valknar@pivoine.art
+Powered by Debian | Fueled by Metal
+
+
+ +**[BACK TO THE TOP](#)** + +
diff --git a/arty.yml b/arty.yml new file mode 100644 index 0000000..60d3e82 --- /dev/null +++ b/arty.yml @@ -0,0 +1,73 @@ +name: "Valknar's home" +version: "1.0.0" +description: "Valknar's home repository" +author: "valknar@pivoine.art" +license: "MIT" +notes: | + # Valknar's Home Repository + + This is a **personal development environment** configuration managed with `arty`. + + ## Quick Start + + ```bash + # Install all dependencies + arty deps + + # Update system packages + arty debian/update && arty debian/install + + # Install development tools + arty node/install + arty rust/install + arty python/install + ``` + + ## Key Features + + - **Shell Configuration**: Zsh with Oh-My-Zsh and Powerlevel10k theme + - **Version Managers**: nvm (Node), rbenv (Ruby), pyenv (Python), gvm (Go) + - **Development Tools**: Docker, Rust, Node.js, Python, Ruby + - **Utilities**: Custom scripts in `bin/` directory + + ## Links + + - [Repository](ssh://git@dev.pivoine.art:2222/valknar/home.git) + - [Documentation](https://dev.pivoine.art) +references: + - url: https://github.com/nvm-sh/nvm.git + into: .nvm + - url: https://github.com/rbenv/rbenv.git + into: .rbenv + - url: https://github.com/rbenv/ruby-build.git + into: .rbenv/plugins/ruby-build + - url: https://github.com/pyenv/pyenv.git + into: .pyenv + - url: https://github.com/ohmyzsh/ohmyzsh.git + into: .oh-my-zsh + - url: https://github.com/romkatv/powerlevel10k.git + into: .oh-my-zsh/custom/themes/powerlevel10k + - url: https://github.com/marlonrichert/zsh-autocomplete.git + into: .oh-my-zsh/custom/plugins/zsh-autocomplete + - url: https://github.com/zsh-users/zsh-autosuggestions.git + into: .oh-my-zsh/custom/plugins/zsh-autosuggestions + - url: https://github.com/zsh-users/zsh-syntax-highlighting.git + into: .oh-my-zsh/custom/plugins/zsh-syntax-highlighting +scripts: + debian/update: sudo sed -i 's/main non-free-firmware/main contrib non-free non-free-firmware/g' /etc/apt/sources.list && sudo apt update + debian/install: sudo apt install curl wget zsh fzf davfs2 make imagemagick rsvg-convert ffmpeg yt-dlp pkg-config zstd libffi-dev zlib1g-dev libyaml-dev libssl-dev liblzma-dev libbz2-dev libncurses-dev libreadline-dev libsqlite3-dev python3-tk tk-dev unzip zip + rust/install: curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh + go/install: sudo apt install golang + node/install: nvm install + ruby/install: rbenv install + python/install: pyenv install + sdkman/install: curl -s "https://get.sdkman.io" | bash + docker/install: curl -fsSL -o- https://get.docker.com | sudo sh && sudo usermod -aG docker $USER + claude/install: curl -fsSL https://claude.ai/install.sh | bash + yq/install: wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/local/bin/yq && chmod +x /usr/local/bin/yq + gh/install: curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg && sudo chmod go+r /usr/share/keyrings/githubcli-archive-keyring.gpg && echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null && sudo apt update && sudo apt install -y gh + tailscale/install: curl -fsSL https://tailscale.com/install.sh | sh + ollama/install: curl -fsSL https://ollama.com/install.sh | sh + hugo/install: CGO_ENABLED=1 go install -tags extended,withdeploy github.com/gohugoio/hugo@latest + npm/install: npm i -g npm pnpm + gem/install: gem install bundler diff --git a/scripts/arty.sh b/scripts/arty.sh new file mode 100755 index 0000000..ec3f6a0 --- /dev/null +++ b/scripts/arty.sh @@ -0,0 +1,1326 @@ +#!/bin/bash + +# arty.sh - A bash library repository and release management system +# Version: 1.0.0 + +set -euo pipefail + +ARTY_CONFIG_FILE="${ARTY_CONFIG_FILE:-arty.yml}" +ARTY_ENV="${ARTY_ENV:-default}" +ARTY_DRY_RUN="${ARTY_DRY_RUN:-0}" +ARTY_VERBOSE="${ARTY_VERBOSE:-0}" + +# Enable colors only when stdout is a real terminal (or FORCE_COLOR=1 overrides) +if [[ -t 1 && "${TERM:-}" != "dumb" ]]; then + export FORCE_COLOR=${FORCE_COLOR:-"1"} +else + export FORCE_COLOR=${FORCE_COLOR:-"0"} +fi +if [[ "$FORCE_COLOR" = "0" ]]; then + export RED='' + export GREEN='' + export YELLOW='' + export BLUE='' + export CYAN='' + export MAGENTA='' + export BOLD='' + export NC='' + else + export RED='\033[0;31m' + export GREEN='\033[0;32m' + export YELLOW='\033[1;33m' + export BLUE='\033[0;34m' + export CYAN='\033[0;36m' + export MAGENTA='\033[0;35m' + export BOLD='\033[1m' + export NC='\033[0m' +fi + +# Global array to track installation stack (prevent circular dependencies) +declare -g -A ARTY_INSTALL_STACK + +# Logging functions +log_info() { + echo -e "${BLUE}[INFO]${NC} $1" +} + +log_success() { + echo -e "${GREEN}[ok]${NC} $1" +} + +log_warn() { + echo -e "${YELLOW}[!]${NC} $1" +} + +log_error() { + echo -e "${RED}[x]${NC} $1" +} + +log_debug() { + if [[ "$ARTY_VERBOSE" == "1" ]]; then + echo -e "${CYAN}[DEBUG]${NC} $1" + fi +} + +log_step() { + echo -e "${CYAN}[!]${NC} $1" +} + +# ============================================================================ +# ARTY.SH CORE FUNCTIONS - Repository Management +# ============================================================================ + +# Expand environment variables in a string with validation +# Supports nested expansion (e.g., $AI_ROOT/subdir where AI_ROOT contains another var) +# Fails if any undefined variables remain after expansion +expand_env_vars() { + local input="$1" + local max_iterations=10 + local iteration=0 + local expanded="$input" + + # Keep expanding until no more variables or max iterations reached + while [[ "$expanded" =~ \$ ]] && [[ $iteration -lt $max_iterations ]]; do + # Use eval to expand variables + expanded=$(eval echo "$expanded" 2>/dev/null || echo "$expanded") + iteration=$((iteration+1)) + done + + # Check if any unexpanded variables remain + if [[ "$expanded" =~ \$[A-Za-z_][A-Za-z0-9_]* ]]; then + # Extract the undefined variable name for better error message + local undefined_var=$(echo "$expanded" | grep -o '\$[A-Za-z_][A-Za-z0-9_]*' | head -1) + log_error "Undefined environment variable in path: ${undefined_var}" + log_error " Original: $input" + log_error " Expanded: $expanded" + log_error "" + log_error "Available environment variables from arty.yml:" + if [[ -f "$ARTY_CONFIG_FILE" ]]; then + yq eval '.envs.default | keys | .[]' "$ARTY_CONFIG_FILE" 2>/dev/null | sed 's/^/ - /' || true + fi + return 1 + fi + + echo "$expanded" +} + +# Load environment variables from arty.yml +load_env_vars() { + local config_file="${1:-$ARTY_CONFIG_FILE}" + + if [[ ! -f "$config_file" ]]; then + return 0 # No config file, nothing to load + fi + + # Check if YAML is valid + if ! yq eval '.' "$config_file" >/dev/null 2>&1; then + log_warn "Invalid YAML in config file, skipping env vars" + return 0 + fi + + # Check if envs section exists + local has_envs=$(yq eval '.envs' "$config_file" 2>/dev/null) + if [[ "$has_envs" == "null" ]] || [[ -z "$has_envs" ]]; then + return 0 # No envs section + fi + + local current_env="$ARTY_ENV" + log_debug "Loading environment variables from '$current_env' environment" + + # First load default variables if they exist + local default_envs=$(yq eval '.envs.default | to_entries | .[] | .key + "=" + .value' "$config_file" 2>/dev/null) + if [[ -n "$default_envs" ]] && [[ "$default_envs" != "null" ]]; then + while IFS='=' read -r key value; do + if [[ -n "$key" ]] && [[ "$key" != "null" ]] && [[ -n "$value" ]]; then + # Only export if not already set (for default env only) + if [[ "$current_env" == "default" ]] && [[ -n "${!key:-}" ]]; then + log_debug " Skipping $key (already set)" + continue + fi + export "$key=$value" + log_debug " Set $key (from default)" + fi + done <<< "$default_envs" + fi + + # Then load environment-specific variables (which can override defaults) + if [[ "$current_env" != "default" ]]; then + local env_envs=$(yq eval ".envs.$current_env | to_entries | .[] | .key + \"=\" + .value" "$config_file" 2>/dev/null) + if [[ -n "$env_envs" ]] && [[ "$env_envs" != "null" ]]; then + while IFS='=' read -r key value; do + if [[ -n "$key" ]] && [[ "$key" != "null" ]] && [[ -n "$value" ]]; then + export "$key=$value" + log_debug " Set $key (from $current_env)" + fi + done <<< "$env_envs" + fi + fi +} + +# Wrapper to limit Go thread usage on hosts with low ulimit -u (e.g. IONOS shared hosting). +# Always returns 0 so set -e doesn't propagate a yq crash through `local var=$(yq ...)`. +yq() { + GOMAXPROCS=1 GOGC=off command yq "$@" || return 0 +} + +# Auto-install mikefarah/yq v4 using whatever package manager is available. +_install_yq() { + if command -v brew &>/dev/null; then + log_info "Installing yq via Homebrew..." + brew install yq && return 0 + fi + + if command -v snap &>/dev/null; then + log_info "Installing yq via snap..." + snap install yq && return 0 + fi + + if command -v apk &>/dev/null; then + log_info "Installing yq via apk..." + apk add --no-cache yq-go && return 0 + fi + + if command -v pacman &>/dev/null; then + log_info "Installing yq via pacman..." + pacman -S --noconfirm go-yq && return 0 + fi + + if command -v go &>/dev/null; then + log_info "Installing yq via go install..." + local go_bin="${HOME}/.local/bin" + mkdir -p "$go_bin" + GOBIN="$go_bin" go install github.com/mikefarah/yq/v4@latest && { + export PATH="${go_bin}:${PATH}" + return 0 + } + fi + + # Binary download fallback (Linux / macOS) + local os arch install_dir use_sudo=0 + os=$(uname -s | tr '[:upper:]' '[:lower:]') + case "$(uname -m)" in + x86_64) arch="amd64" ;; + aarch64|arm64) arch="arm64" ;; + armv7*) arch="arm" ;; + *) arch="amd64" ;; + esac + + local yq_url="https://github.com/mikefarah/yq/releases/latest/download/yq_${os}_${arch}" + + if [[ -w /usr/local/bin ]]; then + install_dir="/usr/local/bin" + elif command -v sudo &>/dev/null && sudo -n true 2>/dev/null; then + install_dir="/usr/local/bin" + use_sudo=1 + else + install_dir="${HOME}/.local/bin" + mkdir -p "$install_dir" + export PATH="${install_dir}:${PATH}" + fi + + log_info "Downloading yq binary (${os}/${arch}) to ${install_dir}..." + + if command -v wget &>/dev/null; then + if [[ "$use_sudo" == "1" ]]; then + sudo wget -q "$yq_url" -O "${install_dir}/yq" || return 1 + sudo chmod +x "${install_dir}/yq" + else + wget -q "$yq_url" -O "${install_dir}/yq" || return 1 + chmod +x "${install_dir}/yq" + fi + return 0 + elif command -v curl &>/dev/null; then + if [[ "$use_sudo" == "1" ]]; then + sudo curl -fsSL "$yq_url" -o "${install_dir}/yq" || return 1 + sudo chmod +x "${install_dir}/yq" + else + curl -fsSL "$yq_url" -o "${install_dir}/yq" || return 1 + chmod +x "${install_dir}/yq" + fi + return 0 + fi + + return 1 +} + +# Check if mikefarah/yq v4 is installed; auto-install if not. +check_yq() { + local need_install=0 + + # Use type -P to find the actual binary; command -v also matches the yq shell wrapper + # function defined in this script and would give a false positive. + if ! type -P yq &>/dev/null; then + log_warn "yq is not installed — attempting automatic installation..." + need_install=1 + else + local ver_out + ver_out=$(command yq --version 2>&1) || true # guard: command not found → exit 127 + set -e + if [[ ! "$ver_out" =~ mikefarah ]]; then + log_warn "Found yq but it is not mikefarah/yq v4 (got: ${ver_out})" + log_warn "arty requires mikefarah/yq v4 — attempting automatic installation..." + need_install=1 + fi + fi + + if [[ "$need_install" == "0" ]]; then + return 0 + fi + + if _install_yq && command -v yq &>/dev/null; then + local ver_out + ver_out=$(command yq --version 2>&1) + log_success "yq installed: ${ver_out}" + return 0 + fi + + log_error "Could not install yq automatically. Please install mikefarah/yq v4:" + log_error " brew install yq (macOS / Linuxbrew)" + log_error " snap install yq (Linux with snap)" + log_error " apk add yq-go (Alpine Linux)" + log_error " pacman -S go-yq (Arch Linux)" + log_error " go install github.com/mikefarah/yq/v4@latest (Go toolchain)" + log_error " wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 \\" + log_error " -O /usr/local/bin/yq && chmod +x /usr/local/bin/yq" + log_error "See: https://github.com/mikefarah/yq#install" + exit 1 +} + +# Initialize arty environment +init_arty() { + local arty_home="${ARTY_HOME:-$PWD/.arty}" + local libs_dir="${ARTY_LIBS_DIR:-$arty_home/libs}" + local bin_dir="${ARTY_BIN_DIR:-$arty_home/bin}" + + if [[ ! -d "$arty_home" ]]; then + mkdir -p "$libs_dir" + mkdir -p "$bin_dir" + log_success "Initialized arty at $arty_home" + fi +} + +# Get a field from YAML using yq +get_yaml_field() { + local file="$1" + local field="$2" + + if [[ ! -f "$file" ]]; then + return 1 + fi + + # Check if file has valid YAML - yq will exit with error on invalid YAML + if ! yq eval '.' "$file" >/dev/null 2>&1; then + return 1 + fi + + yq eval ".$field" "$file" 2>/dev/null || echo "" +} + +# Get array items from YAML using yq +get_yaml_array() { + local file="$1" + local field="$2" + + if [[ ! -f "$file" ]]; then + return 1 + fi + + # Check if file has valid YAML - yq will exit with error on invalid YAML + if ! yq eval '.' "$file" >/dev/null 2>&1; then + return 1 + fi + + yq eval ".${field}[]" "$file" 2>/dev/null +} + +# Get script command from YAML +get_yaml_script() { + local file="$1" + local script_name="$2" + + if [[ ! -f "$file" ]]; then + return 1 + fi + + yq eval ".scripts.${script_name}" "$file" 2>/dev/null || echo "null" +} + +# List all script names from YAML +list_yaml_scripts() { + local file="$1" + + if [[ ! -f "$file" ]]; then + return 1 + fi + + yq eval '.scripts | keys | .[]' "$file" 2>/dev/null +} + +# Execute a script defined in arty.yml +exec_script() { + local script_name="$1" + shift + + if [[ ! -f "$ARTY_CONFIG_FILE" ]]; then + log_error "Config file not found: $ARTY_CONFIG_FILE" + log_info "Run this command in a directory with arty.yml" + return 1 + fi + + local cmd + cmd=$(get_yaml_script "$ARTY_CONFIG_FILE" "$script_name") + + if [[ -z "$cmd" ]] || [[ "$cmd" == "null" ]]; then + log_error "Unknown command: $script_name" + log_info "Available scripts:" + while IFS= read -r name; do + [[ -n "$name" ]] && echo " - $name" + done < <(list_yaml_scripts "$ARTY_CONFIG_FILE") + return 1 + fi + + log_info "Running: $script_name" + [[ ! "$cmd" =~ \$@ ]] && cmd="$cmd"' "$@"' + bash -c "$cmd" -- "$@" +} + +# Get library name from repository URL +get_lib_name() { + local repo_url="$1" + basename "$repo_url" .git +} + +# Parse reference - can be a string URL or an object with url, into, ref, env +# Returns: url|into|ref|env (pipe-delimited) +# env can be a single value or comma-separated list +parse_reference() { + local config_file="$1" + local ref_index="$2" + + # Single yq call using only // and select — if-then-else is not supported in + # all yq v4 builds. .url//. handles both string refs (no .url key) and object + # refs. env array is joined; null env falls back to empty string. + yq eval ".references[$ref_index] | (.url // .) + \"|\" + (.into // \"\") + \"|\" + (.ref // \"\") + \"|\" + ((.env | select(tag == \"!!seq\") | join(\",\")) // .env // \"\")" "$config_file" 2>/dev/null +} + +# Check if current environment matches the filter +# env_filter can be a single env or comma-separated list +check_env_match() { + local current_env="$1" + local env_filter="$2" + + # No filter means match all + [[ -z "$env_filter" ]] && return 0 + + # Convert comma-separated list to array + IFS=',' read -ra env_list <<< "$env_filter" + + # Check if current env is in the list + for env in "${env_list[@]}"; do + # Trim whitespace + env=$(echo "$env" | xargs) + if [[ "$env" == "$current_env" ]]; then + return 0 + fi + done + + return 1 +} + +# Get git information for a repository +get_git_info() { + local repo_dir="$1" + + if [[ ! -d "$repo_dir/.git" ]]; then + echo "|||0" + return + fi + + # Get short commit hash + local commit_hash=$(cd "$repo_dir" && git rev-parse --short HEAD 2>/dev/null || echo "") + + # Get all refs pointing to current commit (tags, branches) + local refs=$(cd "$repo_dir" && git describe --all --exact-match 2>/dev/null || git symbolic-ref --short HEAD 2>/dev/null || echo "") + + # Clean up refs (remove heads/ and tags/ prefixes) + refs=$(echo "$refs" | sed 's#^heads/##' | sed 's#^tags/##') + + # Check if dirty (has uncommitted changes) + local is_dirty=0 + if [[ -n "$(cd "$repo_dir" && git status --porcelain 2>/dev/null)" ]]; then + is_dirty=1 + fi + + echo "$commit_hash|$refs|$is_dirty" +} + +# Normalize library identifier for tracking +normalize_lib_id() { + local repo_url="$1" + # Convert to lowercase and remove .git suffix for consistent tracking + local normalized="${repo_url,,}" + normalized=$(echo "$normalized" | sed 's/\.git$//') + + # Normalize different Git URL formats to the same path + # https://github.com/user/repo -> github.com/user/repo + # git@github.com:user/repo -> github.com/user/repo + # ssh://git@github.com/user/repo -> github.com/user/repo + normalized=$(echo "$normalized" | sed -E 's#^https?://##' | sed -E 's#^git@([^:]+):#\1/#' | sed -E 's#^ssh://git@##') + + echo "$normalized" +} + +# Check if library is in installation stack +is_installing() { + local lib_id="$1" + [[ -n "${ARTY_INSTALL_STACK[$lib_id]:-}" ]] +} + +# Add library to installation stack +mark_installing() { + local lib_id="$1" + ARTY_INSTALL_STACK[$lib_id]=1 +} + +# Remove library from installation stack +unmark_installing() { + local lib_id="$1" + unset ARTY_INSTALL_STACK[$lib_id] +} + +# Check if library is already installed +is_installed() { + local lib_name="$1" + local libs_dir="${ARTY_LIBS_DIR:-${ARTY_HOME:-$PWD/.arty}/libs}" + [[ -d "$libs_dir/$lib_name" ]] +} + +# Install a library from git repository +install_lib() { + local repo_url="$1" + local lib_name="${2:-$(get_lib_name "$repo_url")}" + local git_ref="${3:-}" + local custom_into="${4:-}" + local config_file="${5:-$ARTY_CONFIG_FILE}" + + # Determine installation directory + local lib_dir + if [[ -n "$custom_into" ]]; then + # Expand environment variables in custom_into path + local expanded_into + if ! expanded_into=$(expand_env_vars "$custom_into"); then + log_error "Failed to expand environment variables in 'into' path for ${repo_url}" + return 1 + fi + + # Check if expanded path is absolute or relative + if [[ "$expanded_into" == /* ]]; then + # Absolute path - use as-is + lib_dir="$expanded_into" + else + # Relative path - make relative to config file directory + local config_dir=$(dirname "$(realpath "${config_file}")") + lib_dir="$config_dir/$expanded_into" + fi + else + # Use global .arty/libs for libraries without custom 'into' + lib_dir="$ARTY_LIBS_DIR/$lib_name" + fi + + # Normalize the library identifier for circular dependency detection + # Include installation path to allow same library at different locations + local lib_id=$(normalize_lib_id "$repo_url") + local lib_id_with_path="${lib_id}@${lib_dir}" + + # Check for circular dependency + if is_installing "$lib_id_with_path"; then + log_warn "Circular dependency detected: $lib_name (already being installed at $lib_dir)" + log_info "Skipping to prevent infinite loop" + return 0 + fi + + # Check if already installed (optimization) + if [[ -d "$lib_dir" ]]; then + log_info "Library '$lib_name' already installed at $lib_dir" + + if [[ "$ARTY_DRY_RUN" == "1" ]]; then + log_info "[DRY RUN] Would check for updates..." + return 0 + fi + + # Try to update + if [[ -n "$git_ref" ]]; then + (cd "$lib_dir" && git fetch -q 2>/dev/null && git checkout -q "$git_ref" && git pull -q 2>/dev/null) || { + log_warn "Failed to update library (continuing with existing version)" + } + else + (cd "$lib_dir" && git fetch -q 2>/dev/null && git pull -q 2>/dev/null) || { + log_warn "Failed to update library (continuing with existing version)" + } + fi + + return 0 + fi + + # Mark as currently installing + mark_installing "$lib_id_with_path" + + if [[ "$ARTY_DRY_RUN" != "1" ]]; then + init_arty + fi + + log_info "Installing library: $lib_name" + log_info "Repository: $repo_url" + log_info "Git ref: ${git_ref:-}" + log_info "Location: $lib_dir" + + if [[ "$ARTY_DRY_RUN" == "1" ]]; then + log_info "[DRY RUN] Would clone repository${git_ref:+ and checkout $git_ref}" + unmark_installing "$lib_id_with_path" + return 0 + fi + + # Clone the repository + git clone "$repo_url" "$lib_dir" || { + log_error "Failed to clone repository" + unmark_installing "$lib_id_with_path" + return 1 + } + + # Checkout the specified ref (skip if none given; clone already checked out the default branch) + if [[ -n "$git_ref" ]]; then + (cd "$lib_dir" && git checkout -q "$git_ref") || { + log_warn "Failed to checkout ref '$git_ref', using default branch" + } + fi + + # Run setup hook if exists + if [[ -f "$lib_dir/setup.sh" ]]; then + log_info "Running setup hook..." + (cd "$lib_dir" && bash setup.sh) || { + log_warn "Setup hook failed, continuing anyway..." + } + fi + + # Process arty.yml if it exists + if [[ -f "$lib_dir/arty.yml" ]]; then + # Link main script to .arty/bin (only for standard installations, not custom 'into') + if [[ -z "$custom_into" ]]; then + local main_script=$(get_yaml_field "$lib_dir/arty.yml" "main") + if [[ -n "$main_script" ]] && [[ "$main_script" != "null" ]]; then + local main_file="$lib_dir/$main_script" + if [[ -f "$main_file" ]]; then + local local_bin_dir="$ARTY_BIN_DIR" + local lib_name_stripped="$(basename $main_file .sh)" + local bin_link="$local_bin_dir/$lib_name_stripped" + + log_info "Linking main script: $main_script -> $bin_link" + ln -sf "$main_file" "$bin_link" + chmod +x "$main_file" + log_success "Main script linked to $bin_link" + fi + fi + fi + + # Install nested dependencies (always, regardless of 'into') + log_info "Found arty.yml, checking for references..." + install_references "$lib_dir/arty.yml" + fi + + # Unmark as installing (we're done with this library) + unmark_installing "$lib_id_with_path" + + log_success "Library '$lib_name' installed successfully" + log_info "Location: $lib_dir" +} + +# Find if a library is defined in current or ancestor configs with 'into' +find_ancestor_into() { + local lib_url="$1" + local current_config="$2" + local lib_url_normalized=$(normalize_lib_id "$lib_url") + + # Build list of ancestor configs to check + # Start with current config's directory and walk up to find parent arty.yml files + local config_to_check="$current_config" + local current_dir=$(dirname "$(realpath "$current_config")") + + # Check current config and walk up the directory tree + while true; do + if [[ -f "$config_to_check" ]]; then + # Get count of references + local ref_count=$(yq eval '.references | length' "$config_to_check" 2>/dev/null) + if [[ -n "$ref_count" ]] && [[ "$ref_count" != "null" ]] && [[ "$ref_count" != "0" ]]; then + # Check each reference + for ((i = 0; i < ref_count; i++)); do + local ref_data=$(parse_reference "$config_to_check" "$i") + IFS='|' read -r url into git_ref env_filter <<<"$ref_data" + + local url_normalized=$(normalize_lib_id "$url") + if [[ "$url_normalized" == "$lib_url_normalized" ]] && [[ -n "$into" ]]; then + # Found it with an 'into' directive + echo "$into|$config_to_check" + return + fi + done + fi + fi + + # Check if we've reached the root ARTY_CONFIG_FILE (after checking it) + if [[ "$(realpath "$config_to_check" 2>/dev/null)" == "$(realpath "$ARTY_CONFIG_FILE" 2>/dev/null)" ]]; then + # We've checked the root config, stop here + break + fi + + # Move up one directory + local parent_dir=$(dirname "$current_dir") + if [[ "$parent_dir" == "$current_dir" ]] || [[ "$parent_dir" == "/" ]]; then + # Reached filesystem root, stop + break + fi + current_dir="$parent_dir" + config_to_check="$current_dir/arty.yml" + done + + echo "" +} + +# Install all references from arty.yml +install_references() { + local config_file="${1:-$ARTY_CONFIG_FILE}" + + if [[ ! -f "$config_file" ]]; then + log_error "Config file not found: $config_file" + return 1 + fi + + # Check if YAML is valid by trying to read it + if ! yq eval '.' "$config_file" >/dev/null 2>&1; then + log_error "Invalid YAML in config file: $config_file" + return 1 + fi + + # Initialize arty directory structure first (unless dry run) + if [[ "$ARTY_DRY_RUN" != "1" ]]; then + init_arty + fi + + # Count references + local ref_count=$(yq eval '.references | length' "$config_file" 2>/dev/null) + if [[ -z "$ref_count" ]] || [[ "$ref_count" == "null" ]] || [[ "$ref_count" == "0" ]]; then + log_info "No references to install" + return 0 + fi + + # Process each reference by index + local i + for ((i = 0; i < ref_count; i++)); do + # Parse the reference + local ref_data=$(parse_reference "$config_file" "$i") + IFS='|' read -r url into git_ref env_filter <<<"$ref_data" + + # Skip empty URLs + if [[ -z "$url" ]] || [[ "$url" == "null" ]]; then + continue + fi + + # Check environment filter using the new check_env_match function + if [[ -n "$env_filter" ]] && ! check_env_match "$ARTY_ENV" "$env_filter"; then + log_info "Skipping reference (env filter: [$env_filter], current: $ARTY_ENV): $url" + continue + fi + + # Get library name + local lib_name=$(get_lib_name "$url") + + # If no 'into' specified, check if an ancestor config defines it with 'into' + local effective_config="$config_file" + if [[ -z "$into" ]] && [[ "$config_file" != "$ARTY_CONFIG_FILE" ]]; then + local ancestor_result=$(find_ancestor_into "$url" "$config_file") + if [[ -n "$ancestor_result" ]]; then + IFS='|' read -r ancestor_into ancestor_config <<<"$ancestor_result" + into="$ancestor_into" + effective_config="$ancestor_config" + fi + fi + + log_info "Installing reference: $url" + [[ -n "$into" ]] && log_info "Custom location: $into" + [[ -n "$env_filter" ]] && log_info "Environment filter: [$env_filter]" + + install_lib "$url" "$lib_name" "$git_ref" "$into" "$effective_config" || log_warn "Failed to install reference: $url" + done +} + +# Build a reference tree from arty.yml showing all dependencies +build_reference_tree() { + local config_file="${1:-$ARTY_CONFIG_FILE}" + local indent="${2:-}" + local is_last="${3:-1}" + local visited_file="${4:-/tmp/arty_visited_$$}" + local depth="${5:-0}" + + if [[ ! -f "$config_file" ]]; then + return 0 + fi + + # Track visited library directories to prevent infinite circular recursion + # Only track at depth > 0 to allow root-level refs to show even if already nested + local config_dir=$(dirname "$(realpath "$config_file" 2>/dev/null || echo "$config_file")") + if [[ "$depth" -gt 0 ]]; then + if grep -qx "$config_dir" "$visited_file" 2>/dev/null; then + return 0 + fi + echo "$config_dir" >>"$visited_file" + fi + + # Count references + local ref_count=$(yq eval '.references | length' "$config_file" 2>/dev/null) + if [[ -z "$ref_count" ]] || [[ "$ref_count" == "null" ]] || [[ "$ref_count" == "0" ]]; then + return 0 + fi + + # Process each reference + local i + for ((i = 0; i < ref_count; i++)); do + local ref_data=$(parse_reference "$config_file" "$i") + IFS='|' read -r url into git_ref env_filter <<<"$ref_data" + + # Skip if no URL or env filter doesn't match + if [[ -z "$url" ]] || [[ "$url" == "null" ]]; then + continue + fi + if [[ -n "$env_filter" ]] && ! check_env_match "$ARTY_ENV" "$env_filter"; then + continue + fi + + local lib_name=$(get_lib_name "$url") + + # Calculate is_last_ref by counting remaining valid refs + local is_last_ref=0 + local remaining=0 + for ((j = i + 1; j < ref_count; j++)); do + local check_ref=$(parse_reference "$config_file" "$j") + IFS='|' read -r check_url _ _ check_env <<<"$check_ref" + if [[ -n "$check_url" ]] && [[ "$check_url" != "null" ]]; then + if [[ -z "$check_env" ]] || check_env_match "$ARTY_ENV" "$check_env"; then + remaining=1 + break + fi + fi + done + [[ "$remaining" == "0" ]] && is_last_ref=1 + + # If no 'into' specified, check if an ancestor config defines it with 'into' + local effective_config="$config_file" + if [[ -z "$into" ]] && [[ "$config_file" != "$ARTY_CONFIG_FILE" ]]; then + local ancestor_result=$(find_ancestor_into "$url" "$config_file") + if [[ -n "$ancestor_result" ]]; then + IFS='|' read -r ancestor_into ancestor_config <<<"$ancestor_result" + into="$ancestor_into" + effective_config="$ancestor_config" + fi + fi + + # Determine installation directory + local lib_dir + if [[ -n "$into" ]]; then + # Expand environment variables + local expanded_into + expanded_into=$(expand_env_vars "$into" 2>/dev/null) || expanded_into="$into" + + # Check if expanded path is absolute + if [[ "$expanded_into" == /* ]]; then + lib_dir="$expanded_into" + else + local config_dir=$(dirname "$(realpath "${effective_config}")") + lib_dir="$config_dir/$expanded_into" + fi + else + lib_dir="$ARTY_LIBS_DIR/$lib_name" + fi + + # Get version and git info + local version="" + local location="$lib_dir" + + if [[ -f "$lib_dir/arty.yml" ]]; then + version=$(get_yaml_field "$lib_dir/arty.yml" "version") + [[ "$version" == "null" ]] || [[ -z "$version" ]] && version="" + fi + + # Get git information + local git_info=$(get_git_info "$lib_dir") + IFS='|' read -r commit_hash git_refs is_dirty <<<"$git_info" + + # Determine display version + local display_version="${version:-$commit_hash}" + [[ -z "$display_version" ]] && display_version="unknown" + + # Tree characters + local tree_char="" + local tree_continue=" " + if [[ "$is_last_ref" == "1" ]]; then + tree_char="" + tree_continue=" " + fi + + # Print the reference line + printf "%s${tree_char} ${BOLD}${GREEN}%s${NC}" "$indent" "$lib_name" + printf " ${CYAN}%s${NC}" "$display_version" + + # Add git refs if available + if [[ -n "$git_refs" ]]; then + printf " ${BLUE}(%s)${NC}" "$git_refs" + fi + + # Add dirty indicator + if [[ "$is_dirty" == "1" ]]; then + printf " ${YELLOW}*${NC}" + fi + + # Add location info + if [[ -n "$into" ]]; then + printf " ${MAGENTA}-> %s${NC}" "$into" + fi + + echo + + # Recursively show nested dependencies + if [[ -f "$lib_dir/arty.yml" ]]; then + local next_depth=$((depth + 1)) + build_reference_tree "$lib_dir/arty.yml" "${indent}${tree_continue}" "$is_last_ref" "$visited_file" "$next_depth" + fi + done +} + +# List installed libraries with tree visualization +list_libs() { + init_arty + + if [[ ! -f "$ARTY_CONFIG_FILE" ]]; then + # Fallback to simple list if no arty.yml + if [[ ! -d "$ARTY_LIBS_DIR" ]] || [[ -z "$(ls -A "$ARTY_LIBS_DIR" 2>/dev/null)" ]]; then + log_info "No libraries installed" + return 0 + fi + + log_info "Installed libraries:" + echo + + for lib_dir in "$ARTY_LIBS_DIR"/*; do + if [[ -d "$lib_dir" ]]; then + local lib_name=$(basename "$lib_dir") + local version="" + + # Try to get version from arty.yml using yq + if [[ -f "$lib_dir/arty.yml" ]]; then + version=$(get_yaml_field "$lib_dir/arty.yml" "version") + if [[ "$version" == "null" ]] || [[ -z "$version" ]]; then + version="" + fi + fi + + printf " ${GREEN}%-20s${NC} %s\n" "$lib_name" "${version:-(unknown version)}" + fi + done + echo + return 0 + fi + + # Get project info + local project_name=$(get_yaml_field "$ARTY_CONFIG_FILE" "name") + local project_version=$(get_yaml_field "$ARTY_CONFIG_FILE" "version") + + # Clean up name/version + [[ "$project_name" == "null" ]] && project_name="$(basename "$PWD")" + [[ "$project_version" == "null" ]] && project_version="" + + # Check if there are any references + local ref_count=$(yq eval '.references | length' "$ARTY_CONFIG_FILE" 2>/dev/null) + if [[ -z "$ref_count" ]] || [[ "$ref_count" == "null" ]] || [[ "$ref_count" == "0" ]]; then + # No references defined, check for installed libraries + local libs_dir="${ARTY_LIBS_DIR:-${ARTY_HOME:-$PWD/.arty}/libs}" + if [[ ! -d "$libs_dir" ]] || [[ -z "$(ls -A "$libs_dir" 2>/dev/null)" ]]; then + # Show header but indicate no libraries + echo + printf "${BOLD}${GREEN}%s${NC}" "$project_name" + if [[ -n "$project_version" ]]; then + printf " ${CYAN}%s${NC}" "$project_version" + fi + echo + echo + log_info "No libraries installed" + echo + return 0 + fi + fi + + # Print header + echo + printf "${BOLD}${GREEN}%s${NC}" "$project_name" + if [[ -n "$project_version" ]]; then + printf " ${CYAN}%s${NC}" "$project_version" + fi + echo + echo + + # Create temporary file for tracking visited configs + local visited_file="/tmp/arty_visited_$$" + : >"$visited_file" + + # Build and display tree + build_reference_tree "$ARTY_CONFIG_FILE" "" 1 "$visited_file" + + # Cleanup + rm -f "$visited_file" + + echo +} + +# Display notes from arty.yml with markdown rendering +show_notes() { + local config_file="${1:-$ARTY_CONFIG_FILE}" + + if [[ ! -f "$config_file" ]]; then + log_error "Config file not found: $config_file" + return 1 + fi + + # Get notes field from YAML + local notes=$(yq eval '.notes' "$config_file" 2>/dev/null) + + if [[ -z "$notes" ]] || [[ "$notes" == "null" ]]; then + log_info "No notes found in $config_file" + log_info "Add a 'notes' field to your arty.yml to display project notes" + return 0 + fi + + # Get project name for header + local project_name=$(get_yaml_field "$config_file" "name") + [[ "$project_name" == "null" ]] && project_name="$(basename "$PWD")" + + echo + echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo -e "${BOLD}${GREEN} Notes: ${project_name}${NC}" + echo -e "${BOLD}${CYAN}━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━${NC}" + echo + + # Try to render markdown with available tools + if command -v glow &>/dev/null; then + # Use glow if available (best option) + echo "$notes" | glow - + elif command -v mdcat &>/dev/null; then + # Use mdcat if available (good alternative) + echo "$notes" | mdcat + elif command -v python3 &>/dev/null; then + # Fallback to Python markdown rendering - create temp file to avoid quoting issues + local temp_notes=$(mktemp) + echo "$notes" > "$temp_notes" + python3 - "$temp_notes" <<'EOPY' 2>/dev/null || echo "$notes" +import sys +import re + +# Read from temp file +with open(sys.argv[1], 'r') as f: + text = f.read() + +# Simple markdown-like formatting for terminal +# Code blocks first (before other processing) +lines = text.split('\n') +in_code_block = False +processed_lines = [] +for line in lines: + if line.strip().startswith('```'): + in_code_block = not in_code_block + if in_code_block: + processed_lines.append('\033[2;37m' + '─' * 60 + '\033[0m') + else: + processed_lines.append('\033[2;37m' + '─' * 60 + '\033[0m') + continue + if in_code_block: + processed_lines.append('\033[0;36m ' + line + '\033[0m') + else: + processed_lines.append(line) + +text = '\n'.join(processed_lines) + +# Headers +text = re.sub(r'^### (.+)$', r'\033[1;36m\1\033[0m', text, flags=re.MULTILINE) +text = re.sub(r'^## (.+)$', r'\033[1;32m\1\033[0m', text, flags=re.MULTILINE) +text = re.sub(r'^# (.+)$', r'\033[1;33m\1\033[0m', text, flags=re.MULTILINE) + +# Bold +text = re.sub(r'\*\*(.+?)\*\*', r'\033[1m\1\033[0m', text) +text = re.sub(r'__(.+?)__', r'\033[1m\1\033[0m', text) + +# Italic (avoid matching bold) +text = re.sub(r'(?"$ARTY_CONFIG_FILE" < [arguments] [--dry-run] [-v|--verbose] + +COMMANDS: + install [name] Install a library from git repository + deps [--dry-run] Install all dependencies from arty.yml + list List installed libraries with dependency tree + notes Display project notes from arty.yml (supports markdown) + remove Remove an installed library + init [name] Initialize a new arty.yml project + [args] Run a script defined in arty.yml scripts section + +FLAGS: + --dry-run Simulate installation without making changes + -v, --verbose Enable verbose/debug output + -h, --help Show this help message + +EXAMPLES: + arty install https://github.com/user/bash-utils.git + arty install https://github.com/user/lib.git my-lib + arty deps + arty deps --dry-run + arty list + arty notes + arty init my-project + arty npm/install + arty rust/install + +REFERENCE FORMATS: + Simple URL: + references: + - https://github.com/user/repo.git + + Extended object: + references: + - url: git@github.com:user/repo.git + into: custom/path # installation directory (relative to arty.yml) + ref: v1.0.0 # branch, tag, or commit hash + env: production # only install in this environment + + Multiple environments: + references: + - url: git@github.com:user/dev-tools.git + env: [dev, ci] + +PROJECT STRUCTURE: + project/ + .arty/ + libs/ # cloned dependencies + arty.yml + +ENVIRONMENT: + ARTY_HOME Home directory for arty (default: $PWD/.arty) + ARTY_ENV Environment name to load from arty.yml (default: default) + +EOF +} + +# ============================================================================ +# MAIN FUNCTION +# ============================================================================ + +# Main function +main() { + # Check for yq availability first + check_yq + + # Configuration + PROJECT_DIR="$PWD/.arty" + ARTY_HOME="${ARTY_HOME:-$PROJECT_DIR}" + ARTY_LIBS_DIR="${ARTY_LIBS_DIR:-$ARTY_HOME/libs}" + ARTY_BIN_DIR="${ARTY_BIN_DIR:-$ARTY_HOME/bin}" + + if [[ $# -eq 0 ]]; then + show_usage + exit 0 + fi + + # Scan flags first + local dry_run_flag=0 + local verbose_flag=0 + + for arg in "$@"; do + case "$arg" in + --dry-run) dry_run_flag=1 ;; + --verbose|-v) verbose_flag=1 ;; + esac + done + + if [[ "$verbose_flag" == "1" ]]; then + export ARTY_VERBOSE=1 + fi + + load_env_vars + + if [[ "$dry_run_flag" == "1" ]]; then + export ARTY_DRY_RUN=1 + log_info "${YELLOW}[DRY RUN MODE]${NC} Simulating actions, no changes will be made" + echo + fi + + # Extract command and positional args, stripping flags + local args=() + local command="" + + for arg in "$@"; do + case "$arg" in + --dry-run|--verbose|-v|-h|--help) continue ;; + *) + if [[ -z "$command" ]]; then + command="$arg" + else + args+=("$arg") + fi + ;; + esac + done + + if [[ "$command" == "help" ]] || [[ "$command" == "--help" ]] || [[ "$command" == "-h" ]]; then + show_usage + exit 0 + fi + + case "$command" in + install) + if [[ ${#args[@]} -eq 0 ]]; then + install_references + local install_result=$? + else + install_lib "${args[@]}" + local install_result=$? + fi + if [[ $install_result -eq 0 ]] && [[ -f "$ARTY_CONFIG_FILE" ]]; then + local ref_count=$(yq eval '.references | length' "$ARTY_CONFIG_FILE" 2>/dev/null) + if [[ -n "$ref_count" ]] && [[ "$ref_count" != "null" ]] && [[ "$ref_count" != "0" ]]; then + echo + log_success "Installation complete!" + list_libs + fi + fi + ;; + deps) + install_references + local install_result=$? + if [[ $install_result -eq 0 ]] && [[ -f "$ARTY_CONFIG_FILE" ]]; then + local ref_count=$(yq eval '.references | length' "$ARTY_CONFIG_FILE" 2>/dev/null) + if [[ -n "$ref_count" ]] && [[ "$ref_count" != "null" ]] && [[ "$ref_count" != "0" ]]; then + echo + log_success "Dependencies installed!" + list_libs + fi + fi + ;; + list | ls) + list_libs + ;; + notes) + show_notes + ;; + remove | rm) + if [[ ${#args[@]} -eq 0 ]]; then + log_error "Library name required" + exit 1 + fi + remove_lib "${args[0]}" + ;; + init) + init_project "${args[@]}" + ;; + *) + exec_script "$command" "${args[@]}" + ;; + esac +} + +# Run main if script is executed directly +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + main "$@" +fi diff --git a/signature.txt b/signature.txt new file mode 100644 index 0000000..0f6524a --- /dev/null +++ b/signature.txt @@ -0,0 +1,10 @@ + + ██╗ ██╗ █████╗ ██╗ ██╗ ██╗███╗ ██╗ █████╗ ██████╗ + ██║ ██║██╔══██╗██║ ██║ ██╔╝████╗ ██║██╔══██╗██╔══██╗ + ██║ ██║███████║██║ █████╔╝ ██╔██╗ ██║███████║██████╔╝ + ╚██╗ ██╔╝██╔══██║██║ ██╔═██╗ ██║╚██╗██║██╔══██║██╔══██╗ + ╚████╔╝ ██║ ██║███████╗██║ ██╗██║ ╚████║██║ ██║██║ ██║ + ╚═══╝ ╚═╝ ╚═╝╚══════╝╚═╝ ╚═╝╚═╝ ╚═══╝╚═╝ ╚═╝╚═╝ ╚═╝ + ─────────────────────────────────────────────────────────────────── + valknar@pivoine.art │ (+49) 174 8188918 │ https://pivoine.art + ───────────────────────────────────────────────────────────────────