From 635d2c39a1e985721eaee6786d732b983efea1e0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 24 May 2026 18:21:40 +0000 Subject: [PATCH 1/9] Initial plan From 0c73d74b6e127990a1793cb5c4013d12af2e0af0 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 24 May 2026 18:31:22 +0000 Subject: [PATCH 2/9] chore(common-utils): add npm install lifecycle handling for workspaces Agent-Logs-Url: https://github.com/tomgrv/devcontainer-features/sessions/73785d8c-b0a4-4e2a-8955-82823605ca7d Co-authored-by: tomgrv <1809566+tomgrv@users.noreply.github.com> --- package.json | 1 + src/act/package.json | 2 ++ src/common-utils/_configure-feature.sh | 40 ++++++++++++++------------ src/common-utils/_install-bin.sh | 8 ++++-- src/common-utils/_install-feature.sh | 16 ++++++----- src/common-utils/_merge-json.sh | 16 ++++++----- src/common-utils/_zz_args.sh | 4 ++- src/common-utils/_zz_context.sh | 6 ++-- src/common-utils/_zz_log.sh | 4 ++- src/common-utils/package.json | 5 +++- src/gateway/package.json | 5 +++- src/githooks/package.json | 2 ++ src/gitutils/package.json | 5 +++- src/gitversion/package.json | 5 +++- src/larasets/package.json | 2 ++ src/minikube/package.json | 5 +++- 16 files changed, 81 insertions(+), 45 deletions(-) diff --git a/package.json b/package.json index ae6c496e..c13fd546 100644 --- a/package.json +++ b/package.json @@ -40,6 +40,7 @@ "url": "https://buymeacoffee.com/tomgrv" }, "scripts": { + "install": "sh ./install.sh -s", "lint": "npx --yes lint-staged", "release": "commit-and-tag-version --no-verify --", "test": "echo \"Warning: no test specified\"", diff --git a/src/act/package.json b/src/act/package.json index 986ad6ca..f2825df8 100644 --- a/src/act/package.json +++ b/src/act/package.json @@ -13,6 +13,8 @@ "act-run": "./_act-run.sh" }, "scripts": { + "install": "if [ \"$INIT_CWD\" = \"$PWD\" ]; then sh ./install.sh; fi", + "postinstall": "if [ \"$INIT_CWD\" = \"$PWD\" ]; then configure-feature -s . act; fi", "install-act": "./install-act.sh" }, "peerDependencies": { diff --git a/src/common-utils/_configure-feature.sh b/src/common-utils/_configure-feature.sh index 688f00a9..2639d614 100755 --- a/src/common-utils/_configure-feature.sh +++ b/src/common-utils/_configure-feature.sh @@ -1,11 +1,13 @@ #!/bin/sh +script_dir=$(dirname "$(readlink -f "$0")") + # Source colors script -. zz_colors +. "$script_dir/_zz_colors.sh" # Source the argument parsing script to handle input arguments eval $( - zz_args "Configure specified feature" $0 "$@" <<-help + "$script_dir/_zz_args.sh" "Configure specified feature" $0 "$@" <<-help s source source Force source directory - feature feature Feature name help @@ -22,20 +24,20 @@ export source=${source:-/usr/local/share/$feature} # Get the indent size from devcontainer.json with jq, default to 2 if not found export tabSize=4 -zz_log i "Configure feature <{Purple $feature}>" -zz_log - "In {U $(pwd)}" -zz_log - "From {U $source}" +"$script_dir/_zz_log.sh" i "Configure feature <{Purple $feature}>" +"$script_dir/_zz_log.sh" - "In {U $(pwd)}" +"$script_dir/_zz_log.sh" - "From {U $source}" # Ensure the source directory exists if [ ! -d $source ]; then - zz_log e "Source directory <$source> does not exist" + "$script_dir/_zz_log.sh" e "Source directory <$source> does not exist" exit 1 fi # Deploy stubs if existing if [ -d $source/stubs ]; then - zz_log i "Deploy stubs" + "$script_dir/_zz_log.sh" i "Deploy stubs" find $source/stubs -type f -name ".*" -o -type f | while read file; do @@ -55,7 +57,7 @@ if [ -d $source/stubs ]; then dest=$(echo $dest | sed 's/\/\#/\//g') # Add to .gitignore if not already there - zz_log i "Add {U $dest} to .gitignore" + "$script_dir/_zz_log.sh" i "Add {U $dest} to .gitignore" # Add to .gitignore if not already there grep -qxF $dest .gitignore || echo "$dest" >>.gitignore @@ -66,15 +68,15 @@ if [ -d $source/stubs ]; then # if json file, use merge-json to merge the file if [ "$(basename $file | cut -d. -f2)" = "json" ]; then - zz_log i "Merging {U $file} into {U $dest}..." - merge-json -t ${tabSize:-4} $dest $file + "$script_dir/_zz_log.sh" i "Merging {U $file} into {U $dest}..." + "$script_dir/_merge-json.sh" -t ${tabSize:-4} $dest $file else - zz_log i "Using git merge-file to merge {U $file} into {U $dest}..." + "$script_dir/_zz_log.sh" i "Using git merge-file to merge {U $file} into {U $dest}..." git merge-file -q $dest $file $file fi else - zz_log i "Destination file {U $dest} does not exist. Copying {U $file} to {U $dest}..." + "$script_dir/_zz_log.sh" i "Destination file {U $dest} does not exist. Copying {U $file} to {U $dest}..." cp $file $dest fi @@ -85,7 +87,7 @@ if [ -d $source/stubs ]; then fi # Log the merging process -zz_log i "Merge all package folder json files into top level xxx.json" +"$script_dir/_zz_log.sh" i "Merge all package folder json files into top level xxx.json" for type in package composer; do @@ -103,10 +105,10 @@ for type in package composer; do fi # Merge the tmpl & add keys if not already there. make sure source json does not contain any comments - zz_log i "Merge {U $tmpl} in {U $package}..." + "$script_dir/_zz_log.sh" i "Merge {U $tmpl} in {U $package}..." # Remove comments from the source json and merge it with the target package.json - merge-json -t ${tabSize:-4} $package $tmpl + "$script_dir/_merge-json.sh" -t ${tabSize:-4} $package $tmpl done # Reset the tmpl variable @@ -118,13 +120,13 @@ done # if in top level directory, call configure scripts if [ "$(pwd)" = "$(git rev-parse --show-toplevel)" ]; then - zz_log s "Running on top level directory!" + "$script_dir/_zz_log.sh" s "Running on top level directory!" # Call all configure-xxx.sh scripts find $source -maxdepth 1 -name configure-*.sh | sort | while read file; do - zz_log i "Calling {U $file}..." - sh -c "$file" && zz_log s "Done!" || zz_log e "Failed!" + "$script_dir/_zz_log.sh" i "Calling {U $file}..." + sh -c "$file" && "$script_dir/_zz_log.sh" s "Done!" || "$script_dir/_zz_log.sh" e "Failed!" done else - zz_log w "Not in top level directory, skipping configure scripts" + "$script_dir/_zz_log.sh" w "Not in top level directory, skipping configure scripts" fi diff --git a/src/common-utils/_install-bin.sh b/src/common-utils/_install-bin.sh index a25d9573..d9156ad0 100755 --- a/src/common-utils/_install-bin.sh +++ b/src/common-utils/_install-bin.sh @@ -1,8 +1,10 @@ #!/bin/sh +script_dir=$(dirname "$(readlink -f "$0")") + # Source the context script to initialize variables and settings eval $( - zz_context "$@" + "$script_dir/_zz_context.sh" "$@" ) if [ -z "$feature" ]; then @@ -10,11 +12,11 @@ if [ -z "$feature" ]; then exit 1 fi -zz_log i "Installing bin scripts for {Purple $feature}..." +"$script_dir/_zz_log.sh" i "Installing bin scripts for {Purple $feature}..." # Find all shell scripts in the target directory, make them executable, and create symbolic links in /usr/local/bin find $target -type f -name "_*.sh" -exec echo {} \; -exec chmod +x {} \; | while read file; do # Create a symbolic link in /usr/local/bin with the script name (without the leading underscore and .sh extension) link=/usr/local/bin/$(basename $file | sed 's/^_//;s/.sh$//') - ln -sf $file $link && zz_log s "Linked {U $file} to {U $link}" + ln -sf $file $link && "$script_dir/_zz_log.sh" s "Linked {U $file} to {U $link}" done diff --git a/src/common-utils/_install-feature.sh b/src/common-utils/_install-feature.sh index b34e1e38..da64ab0b 100755 --- a/src/common-utils/_install-feature.sh +++ b/src/common-utils/_install-feature.sh @@ -1,11 +1,13 @@ #!/bin/sh +script_dir=$(dirname "$(readlink -f "$0")") + # Source colors script -. zz_colors +. "$script_dir/_zz_colors.sh" # Source the context script to initialize variables and settings eval $( - zz_context "$@" + "$script_dir/_zz_context.sh" "$@" ) if [ -z "$feature" ]; then @@ -13,14 +15,14 @@ if [ -z "$feature" ]; then exit 1 fi -zz_log i "Installing feature {Purple $feature}..." +"$script_dir/_zz_log.sh" i "Installing feature {Purple $feature}..." # Copy stubs to the target directory if [ -d $source/stubs ]; then - zz_log i "Copying stubs to {U $target}..." + "$script_dir/_zz_log.sh" i "Copying stubs to {U $target}..." cp -a $source/stubs $target else - zz_log w "No stubs found in {U $source}" + "$script_dir/_zz_log.sh" w "No stubs found in {U $source}" fi # Install specific utils by copying them to the target directory and making them executable @@ -29,6 +31,6 @@ find $target -type f -name "*.sh" -exec chmod +x {} \; # Call all the install-xxx scripts in the feature directory find $source -type f -name "install-*.sh" | while read script; do - zz_log i "Calling {U $script}..." - sh -c "$script $@" && zz_log s "Done!" || zz_log e "Failed!" +"$script_dir/_zz_log.sh" i "Calling {U $script}..." +sh -c "$script $@" && "$script_dir/_zz_log.sh" s "Done!" || "$script_dir/_zz_log.sh" e "Failed!" done diff --git a/src/common-utils/_merge-json.sh b/src/common-utils/_merge-json.sh index 4dbc2f30..299582cd 100755 --- a/src/common-utils/_merge-json.sh +++ b/src/common-utils/_merge-json.sh @@ -1,12 +1,14 @@ #!/bin/sh set -e +script_dir=$(dirname "$(readlink -f "$0")") + # Source colors script -. zz_colors +. "$script_dir/_zz_colors.sh" # Manage arguments eval $( - zz_args "Merge 2 json files" $0 "$@" <<-help + "$script_dir/_zz_args.sh" "Merge 2 json files" $0 "$@" <<-help t tabSize tabSize tab size for indentation - target target Target JSON file to merge into - source source Source JSON file to merge from @@ -15,16 +17,16 @@ help # Validate arguments if [ -z "$target" ] || [ -z "$source" ]; then - zz_log e "Usage: json-merge " + "$script_dir/_zz_log.sh" e "Usage: json-merge " exit 1 fi # Validate target file if [ ! -f "$target" ]; then - zz_log e "Target file {U $target} not found" + "$script_dir/_zz_log.sh" e "Target file {U $target} not found" exit 1 elif ! jq empty "$target" >/dev/null 2>&1; then - zz_log e "Target file {U $target} is not a valid JSON" + "$script_dir/_zz_log.sh" e "Target file {U $target} is not a valid JSON" exit 1 fi @@ -33,7 +35,7 @@ if [ $source = "-" ]; then source=/dev/stdin fi -zz_log i "Merging JSON from {U $source} into {U $target}..." +"$script_dir/_zz_log.sh" i "Merging JSON from {U $source} into {U $target}..." # Merge the source JSON into the target JSON jq 'def merge($a; $b): @@ -54,4 +56,4 @@ jq 'def merge($a; $b): $a end; -merge(.; input)' $target $source | normalize-json -c -a -i -t ${tabSize:-4} -f local -l true 2>/dev/null > /tmp/$$.merge && mv /tmp/$$.merge $target && zz_log s "JSON merged successfully" +merge(.; input)' $target $source | "$script_dir/_normalize-json.sh" -c -a -i -t ${tabSize:-4} -f local -l true 2>/dev/null > /tmp/$$.merge && mv /tmp/$$.merge $target && "$script_dir/_zz_log.sh" s "JSON merged successfully" diff --git a/src/common-utils/_zz_args.sh b/src/common-utils/_zz_args.sh index e75676dc..7aef8187 100755 --- a/src/common-utils/_zz_args.sh +++ b/src/common-utils/_zz_args.sh @@ -1,7 +1,9 @@ #!/bin/sh +script_dir=$(dirname "$(readlink -f "$0")") + # Source colors script -. zz_colors +. "$script_dir/_zz_colors.sh" # Initialize variables count="0" diff --git a/src/common-utils/_zz_context.sh b/src/common-utils/_zz_context.sh index c5105c6d..ab7be771 100755 --- a/src/common-utils/_zz_context.sh +++ b/src/common-utils/_zz_context.sh @@ -1,11 +1,13 @@ #!/bin/sh +script_dir=$(dirname "$(readlink -f "$0")") + # Source colors script -. zz_colors +. "$script_dir/_zz_colors.sh" # Manage arguments eval $( - zz_args "Export Source/Targets folders depending on feature context" $0 "$@" <<-help + "$script_dir/_zz_args.sh" "Export Source/Targets folders depending on feature context" $0 "$@" <<-help s source source Force source directory t target target Force target directory - caller caller Force caller script diff --git a/src/common-utils/_zz_log.sh b/src/common-utils/_zz_log.sh index 49f9a76b..b3270d28 100755 --- a/src/common-utils/_zz_log.sh +++ b/src/common-utils/_zz_log.sh @@ -1,7 +1,9 @@ #!/bin/sh +script_dir=$(dirname "$(readlink -f "$0")") + # Source colors script -. zz_colors +. "$script_dir/_zz_colors.sh" lvl="$1" && shift diff --git a/src/common-utils/package.json b/src/common-utils/package.json index f3cc9c49..8d25e0b4 100644 --- a/src/common-utils/package.json +++ b/src/common-utils/package.json @@ -26,6 +26,9 @@ "zz-json": "./_zz_json.sh", "zz-log": "./_zz_log.sh" }, - "scripts": {}, + "scripts": { + "install": "if [ \"$INIT_CWD\" = \"$PWD\" ]; then sh ./install.sh; fi", + "postinstall": "if [ \"$INIT_CWD\" = \"$PWD\" ]; then configure-feature -s . common-utils; fi" + }, "dependencies": {} } diff --git a/src/gateway/package.json b/src/gateway/package.json index 3f378e30..6faaa1ba 100644 --- a/src/gateway/package.json +++ b/src/gateway/package.json @@ -10,7 +10,10 @@ "bin": { "gateway-curl": "./_gateway-curl.sh" }, - "scripts": {}, + "scripts": { + "install": "if [ \"$INIT_CWD\" = \"$PWD\" ]; then sh ./install.sh; fi", + "postinstall": "if [ \"$INIT_CWD\" = \"$PWD\" ]; then configure-feature -s . gateway; fi" + }, "dependencies": {}, "peerDependencies": { "@tomgrv-devcontainer-features/common-utils": ">=5.0.0" diff --git a/src/githooks/package.json b/src/githooks/package.json index 680941b0..89c3d813 100644 --- a/src/githooks/package.json +++ b/src/githooks/package.json @@ -21,6 +21,8 @@ "update-version": "./_update-version.sh" }, "scripts": { + "install": "if [ \"$INIT_CWD\" = \"$PWD\" ]; then sh ./install.sh; fi", + "postinstall": "if [ \"$INIT_CWD\" = \"$PWD\" ]; then configure-feature -s . githooks; fi", "configure-cz": "./configure-cz.sh", "configure-hooks": "./configure-hooks.sh" }, diff --git a/src/gitutils/package.json b/src/gitutils/package.json index 089561bb..7bf5d161 100644 --- a/src/gitutils/package.json +++ b/src/gitutils/package.json @@ -36,7 +36,10 @@ "setrights": "./_setrights.sh", "unset": "./_unset.sh" }, - "scripts": {}, + "scripts": { + "install": "if [ \"$INIT_CWD\" = \"$PWD\" ]; then sh ./install.sh; fi", + "postinstall": "if [ \"$INIT_CWD\" = \"$PWD\" ]; then configure-feature -s . gitutils; fi" + }, "dependencies": {}, "peerDependencies": { "@tomgrv-devcontainer-features/common-utils": ">=5.0.0", diff --git a/src/gitversion/package.json b/src/gitversion/package.json index e26ec936..cde93487 100644 --- a/src/gitversion/package.json +++ b/src/gitversion/package.json @@ -16,7 +16,10 @@ "checkout-version": "./_checkout-version.sh", "gv": "./_gv.sh" }, - "scripts": {}, + "scripts": { + "install": "if [ \"$INIT_CWD\" = \"$PWD\" ]; then sh ./install.sh; fi", + "postinstall": "if [ \"$INIT_CWD\" = \"$PWD\" ]; then configure-feature -s . gitversion; fi" + }, "dependencies": {}, "peerDependencies": { "@tomgrv-devcontainer-features/common-utils": ">=5.0.0" diff --git a/src/larasets/package.json b/src/larasets/package.json index 603bb5c1..1f90eafd 100644 --- a/src/larasets/package.json +++ b/src/larasets/package.json @@ -21,6 +21,8 @@ "srv": "./_srv.sh" }, "scripts": { + "install": "if [ \"$INIT_CWD\" = \"$PWD\" ]; then sh ./install.sh; fi", + "postinstall": "if [ \"$INIT_CWD\" = \"$PWD\" ]; then configure-feature -s . larasets; fi", "configure-composer": "./configure-composer.sh", "configure-deployer": "./configure-deployer.sh", "configure-sail": "./configure-sail.sh", diff --git a/src/minikube/package.json b/src/minikube/package.json index a6a48bf8..0adc5f80 100644 --- a/src/minikube/package.json +++ b/src/minikube/package.json @@ -10,7 +10,10 @@ "stubs/**/*" ], "bin": {}, - "scripts": {}, + "scripts": { + "install": "if [ \"$INIT_CWD\" = \"$PWD\" ]; then sh ./install.sh; fi", + "postinstall": "if [ \"$INIT_CWD\" = \"$PWD\" ]; then configure-feature -s . minikube; fi" + }, "dependencies": {}, "peerDependencies": { "@tomgrv-devcontainer-features/common-utils": ">=5.0.0" From 655514c46ad489f205a0678a1a1ea6437b5761cd Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 24 May 2026 19:27:20 +0000 Subject: [PATCH 3/9] fix(common-utils): fallback to writable bin path during install Agent-Logs-Url: https://github.com/tomgrv/devcontainer-features/sessions/daeb1c9f-3fab-49b2-a946-1a94cccd8d15 Co-authored-by: tomgrv <1809566+tomgrv@users.noreply.github.com> --- src/common-utils/_install-bin.sh | 51 +++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/src/common-utils/_install-bin.sh b/src/common-utils/_install-bin.sh index d9156ad0..f8044a64 100755 --- a/src/common-utils/_install-bin.sh +++ b/src/common-utils/_install-bin.sh @@ -14,9 +14,58 @@ fi "$script_dir/_zz_log.sh" i "Installing bin scripts for {Purple $feature}..." +# Resolve a writable bin directory. Defaults to /usr/local/bin, then falls back to +# ~/.local/bin, then to the first writable directory already in PATH, then to $target/bin. +link_dir=${INSTALL_BIN_DIR:-/usr/local/bin} + +if [ ! -d "$link_dir" ]; then + mkdir -p "$link_dir" 2>/dev/null || true +fi + +if [ ! -d "$link_dir" ] || [ ! -w "$link_dir" ]; then + if [ -n "$HOME" ]; then + mkdir -p "$HOME/.local/bin" 2>/dev/null || true + if [ -d "$HOME/.local/bin" ] && [ -w "$HOME/.local/bin" ]; then + link_dir="$HOME/.local/bin" + fi + fi +fi + +if [ ! -d "$link_dir" ] || [ ! -w "$link_dir" ]; then + for dir in $(echo "$PATH" | tr ':' '\n'); do + if [ -d "$dir" ] && [ -w "$dir" ] && [ "$dir" != "." ] && [ "$dir" != "$PWD" ]; then + case "$dir" in + */node_modules/.bin) continue ;; + esac + link_dir="$dir" + break + fi + done +fi + +if [ ! -d "$link_dir" ] || [ ! -w "$link_dir" ]; then + mkdir -p "$target/bin" 2>/dev/null || true + if [ -d "$target/bin" ] && [ -w "$target/bin" ]; then + link_dir="$target/bin" + fi +fi + +if [ ! -d "$link_dir" ] || [ ! -w "$link_dir" ]; then + "$script_dir/_zz_log.sh" e "No writable bin directory found" + exit 1 +fi + +case ":$PATH:" in +*":$link_dir:"*) ;; +*) + export PATH="$link_dir:$PATH" + "$script_dir/_zz_log.sh" w "Added {U $link_dir} to PATH for current install session" + ;; +esac + # Find all shell scripts in the target directory, make them executable, and create symbolic links in /usr/local/bin find $target -type f -name "_*.sh" -exec echo {} \; -exec chmod +x {} \; | while read file; do # Create a symbolic link in /usr/local/bin with the script name (without the leading underscore and .sh extension) - link=/usr/local/bin/$(basename $file | sed 's/^_//;s/.sh$//') + link=$link_dir/$(basename $file | sed 's/^_//;s/.sh$//') ln -sf $file $link && "$script_dir/_zz_log.sh" s "Linked {U $file} to {U $link}" done From 37b4361e833ef6b3e73a2adeb12b821efbec2b68 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 24 May 2026 20:12:05 +0000 Subject: [PATCH 4/9] fix(common-utils): simplify bin path selection and restore PATH scripts Agent-Logs-Url: https://github.com/tomgrv/devcontainer-features/sessions/3a277216-ecb3-4d12-877c-b427b3ca59b4 Co-authored-by: tomgrv <1809566+tomgrv@users.noreply.github.com> --- src/common-utils/_configure-feature.sh | 40 ++++++------- src/common-utils/_install-bin.sh | 82 ++++++++++++++------------ src/common-utils/_install-feature.sh | 16 +++-- src/common-utils/_merge-json.sh | 16 +++-- src/common-utils/_zz_args.sh | 4 +- src/common-utils/_zz_context.sh | 6 +- src/common-utils/_zz_log.sh | 4 +- 7 files changed, 82 insertions(+), 86 deletions(-) diff --git a/src/common-utils/_configure-feature.sh b/src/common-utils/_configure-feature.sh index 2639d614..688f00a9 100755 --- a/src/common-utils/_configure-feature.sh +++ b/src/common-utils/_configure-feature.sh @@ -1,13 +1,11 @@ #!/bin/sh -script_dir=$(dirname "$(readlink -f "$0")") - # Source colors script -. "$script_dir/_zz_colors.sh" +. zz_colors # Source the argument parsing script to handle input arguments eval $( - "$script_dir/_zz_args.sh" "Configure specified feature" $0 "$@" <<-help + zz_args "Configure specified feature" $0 "$@" <<-help s source source Force source directory - feature feature Feature name help @@ -24,20 +22,20 @@ export source=${source:-/usr/local/share/$feature} # Get the indent size from devcontainer.json with jq, default to 2 if not found export tabSize=4 -"$script_dir/_zz_log.sh" i "Configure feature <{Purple $feature}>" -"$script_dir/_zz_log.sh" - "In {U $(pwd)}" -"$script_dir/_zz_log.sh" - "From {U $source}" +zz_log i "Configure feature <{Purple $feature}>" +zz_log - "In {U $(pwd)}" +zz_log - "From {U $source}" # Ensure the source directory exists if [ ! -d $source ]; then - "$script_dir/_zz_log.sh" e "Source directory <$source> does not exist" + zz_log e "Source directory <$source> does not exist" exit 1 fi # Deploy stubs if existing if [ -d $source/stubs ]; then - "$script_dir/_zz_log.sh" i "Deploy stubs" + zz_log i "Deploy stubs" find $source/stubs -type f -name ".*" -o -type f | while read file; do @@ -57,7 +55,7 @@ if [ -d $source/stubs ]; then dest=$(echo $dest | sed 's/\/\#/\//g') # Add to .gitignore if not already there - "$script_dir/_zz_log.sh" i "Add {U $dest} to .gitignore" + zz_log i "Add {U $dest} to .gitignore" # Add to .gitignore if not already there grep -qxF $dest .gitignore || echo "$dest" >>.gitignore @@ -68,15 +66,15 @@ if [ -d $source/stubs ]; then # if json file, use merge-json to merge the file if [ "$(basename $file | cut -d. -f2)" = "json" ]; then - "$script_dir/_zz_log.sh" i "Merging {U $file} into {U $dest}..." - "$script_dir/_merge-json.sh" -t ${tabSize:-4} $dest $file + zz_log i "Merging {U $file} into {U $dest}..." + merge-json -t ${tabSize:-4} $dest $file else - "$script_dir/_zz_log.sh" i "Using git merge-file to merge {U $file} into {U $dest}..." + zz_log i "Using git merge-file to merge {U $file} into {U $dest}..." git merge-file -q $dest $file $file fi else - "$script_dir/_zz_log.sh" i "Destination file {U $dest} does not exist. Copying {U $file} to {U $dest}..." + zz_log i "Destination file {U $dest} does not exist. Copying {U $file} to {U $dest}..." cp $file $dest fi @@ -87,7 +85,7 @@ if [ -d $source/stubs ]; then fi # Log the merging process -"$script_dir/_zz_log.sh" i "Merge all package folder json files into top level xxx.json" +zz_log i "Merge all package folder json files into top level xxx.json" for type in package composer; do @@ -105,10 +103,10 @@ for type in package composer; do fi # Merge the tmpl & add keys if not already there. make sure source json does not contain any comments - "$script_dir/_zz_log.sh" i "Merge {U $tmpl} in {U $package}..." + zz_log i "Merge {U $tmpl} in {U $package}..." # Remove comments from the source json and merge it with the target package.json - "$script_dir/_merge-json.sh" -t ${tabSize:-4} $package $tmpl + merge-json -t ${tabSize:-4} $package $tmpl done # Reset the tmpl variable @@ -120,13 +118,13 @@ done # if in top level directory, call configure scripts if [ "$(pwd)" = "$(git rev-parse --show-toplevel)" ]; then - "$script_dir/_zz_log.sh" s "Running on top level directory!" + zz_log s "Running on top level directory!" # Call all configure-xxx.sh scripts find $source -maxdepth 1 -name configure-*.sh | sort | while read file; do - "$script_dir/_zz_log.sh" i "Calling {U $file}..." - sh -c "$file" && "$script_dir/_zz_log.sh" s "Done!" || "$script_dir/_zz_log.sh" e "Failed!" + zz_log i "Calling {U $file}..." + sh -c "$file" && zz_log s "Done!" || zz_log e "Failed!" done else - "$script_dir/_zz_log.sh" w "Not in top level directory, skipping configure scripts" + zz_log w "Not in top level directory, skipping configure scripts" fi diff --git a/src/common-utils/_install-bin.sh b/src/common-utils/_install-bin.sh index f8044a64..be890ce5 100755 --- a/src/common-utils/_install-bin.sh +++ b/src/common-utils/_install-bin.sh @@ -1,10 +1,8 @@ #!/bin/sh -script_dir=$(dirname "$(readlink -f "$0")") - # Source the context script to initialize variables and settings eval $( - "$script_dir/_zz_context.sh" "$@" + zz_context "$@" ) if [ -z "$feature" ]; then @@ -12,54 +10,64 @@ if [ -z "$feature" ]; then exit 1 fi -"$script_dir/_zz_log.sh" i "Installing bin scripts for {Purple $feature}..." +zz_log i "Installing bin scripts for {Purple $feature}..." -# Resolve a writable bin directory. Defaults to /usr/local/bin, then falls back to -# ~/.local/bin, then to the first writable directory already in PATH, then to $target/bin. -link_dir=${INSTALL_BIN_DIR:-/usr/local/bin} +# Build candidate bin directories and pick the first writable one. +candidates="" -if [ ! -d "$link_dir" ]; then - mkdir -p "$link_dir" 2>/dev/null || true -fi +add_candidate() { + candidate="$1" + [ -n "$candidate" ] || return 0 -if [ ! -d "$link_dir" ] || [ ! -w "$link_dir" ]; then - if [ -n "$HOME" ]; then - mkdir -p "$HOME/.local/bin" 2>/dev/null || true - if [ -d "$HOME/.local/bin" ] && [ -w "$HOME/.local/bin" ]; then - link_dir="$HOME/.local/bin" + case ":$candidates:" in + *:"$candidate":*) ;; + *) + if [ -n "$candidates" ]; then + candidates="$candidates:$candidate" + else + candidates="$candidate" fi - fi -fi + ;; + esac +} -if [ ! -d "$link_dir" ] || [ ! -w "$link_dir" ]; then - for dir in $(echo "$PATH" | tr ':' '\n'); do - if [ -d "$dir" ] && [ -w "$dir" ] && [ "$dir" != "." ] && [ "$dir" != "$PWD" ]; then - case "$dir" in - */node_modules/.bin) continue ;; - esac - link_dir="$dir" - break - fi - done +add_candidate "${INSTALL_BIN_DIR:-/usr/local/bin}" + +if [ -n "$HOME" ]; then + add_candidate "$HOME/.local/bin" fi -if [ ! -d "$link_dir" ] || [ ! -w "$link_dir" ]; then - mkdir -p "$target/bin" 2>/dev/null || true - if [ -d "$target/bin" ] && [ -w "$target/bin" ]; then - link_dir="$target/bin" +for dir in $(echo "$PATH" | tr ':' '\n'); do + case "$dir" in + "" | "." | "$PWD" | */node_modules/.bin) continue ;; + esac + add_candidate "$dir" +done + +add_candidate "$target/bin" + +link_dir="" +old_ifs=$IFS +IFS=':' +for candidate in $candidates; do + [ -d "$candidate" ] || mkdir -p "$candidate" 2>/dev/null || true + if [ -d "$candidate" ] && [ -w "$candidate" ]; then + link_dir="$candidate" + break fi -fi +done +IFS=$old_ifs -if [ ! -d "$link_dir" ] || [ ! -w "$link_dir" ]; then - "$script_dir/_zz_log.sh" e "No writable bin directory found" +[ -n "$link_dir" ] || { + zz_log e "No writable bin directory found" exit 1 -fi +} case ":$PATH:" in *":$link_dir:"*) ;; *) export PATH="$link_dir:$PATH" - "$script_dir/_zz_log.sh" w "Added {U $link_dir} to PATH for current install session" + zz_log w "Added {U $link_dir} to PATH for current install session" ;; esac @@ -67,5 +75,5 @@ esac find $target -type f -name "_*.sh" -exec echo {} \; -exec chmod +x {} \; | while read file; do # Create a symbolic link in /usr/local/bin with the script name (without the leading underscore and .sh extension) link=$link_dir/$(basename $file | sed 's/^_//;s/.sh$//') - ln -sf $file $link && "$script_dir/_zz_log.sh" s "Linked {U $file} to {U $link}" + ln -sf $file $link && zz_log s "Linked {U $file} to {U $link}" done diff --git a/src/common-utils/_install-feature.sh b/src/common-utils/_install-feature.sh index da64ab0b..b34e1e38 100755 --- a/src/common-utils/_install-feature.sh +++ b/src/common-utils/_install-feature.sh @@ -1,13 +1,11 @@ #!/bin/sh -script_dir=$(dirname "$(readlink -f "$0")") - # Source colors script -. "$script_dir/_zz_colors.sh" +. zz_colors # Source the context script to initialize variables and settings eval $( - "$script_dir/_zz_context.sh" "$@" + zz_context "$@" ) if [ -z "$feature" ]; then @@ -15,14 +13,14 @@ if [ -z "$feature" ]; then exit 1 fi -"$script_dir/_zz_log.sh" i "Installing feature {Purple $feature}..." +zz_log i "Installing feature {Purple $feature}..." # Copy stubs to the target directory if [ -d $source/stubs ]; then - "$script_dir/_zz_log.sh" i "Copying stubs to {U $target}..." + zz_log i "Copying stubs to {U $target}..." cp -a $source/stubs $target else - "$script_dir/_zz_log.sh" w "No stubs found in {U $source}" + zz_log w "No stubs found in {U $source}" fi # Install specific utils by copying them to the target directory and making them executable @@ -31,6 +29,6 @@ find $target -type f -name "*.sh" -exec chmod +x {} \; # Call all the install-xxx scripts in the feature directory find $source -type f -name "install-*.sh" | while read script; do -"$script_dir/_zz_log.sh" i "Calling {U $script}..." -sh -c "$script $@" && "$script_dir/_zz_log.sh" s "Done!" || "$script_dir/_zz_log.sh" e "Failed!" + zz_log i "Calling {U $script}..." + sh -c "$script $@" && zz_log s "Done!" || zz_log e "Failed!" done diff --git a/src/common-utils/_merge-json.sh b/src/common-utils/_merge-json.sh index 299582cd..4dbc2f30 100755 --- a/src/common-utils/_merge-json.sh +++ b/src/common-utils/_merge-json.sh @@ -1,14 +1,12 @@ #!/bin/sh set -e -script_dir=$(dirname "$(readlink -f "$0")") - # Source colors script -. "$script_dir/_zz_colors.sh" +. zz_colors # Manage arguments eval $( - "$script_dir/_zz_args.sh" "Merge 2 json files" $0 "$@" <<-help + zz_args "Merge 2 json files" $0 "$@" <<-help t tabSize tabSize tab size for indentation - target target Target JSON file to merge into - source source Source JSON file to merge from @@ -17,16 +15,16 @@ help # Validate arguments if [ -z "$target" ] || [ -z "$source" ]; then - "$script_dir/_zz_log.sh" e "Usage: json-merge " + zz_log e "Usage: json-merge " exit 1 fi # Validate target file if [ ! -f "$target" ]; then - "$script_dir/_zz_log.sh" e "Target file {U $target} not found" + zz_log e "Target file {U $target} not found" exit 1 elif ! jq empty "$target" >/dev/null 2>&1; then - "$script_dir/_zz_log.sh" e "Target file {U $target} is not a valid JSON" + zz_log e "Target file {U $target} is not a valid JSON" exit 1 fi @@ -35,7 +33,7 @@ if [ $source = "-" ]; then source=/dev/stdin fi -"$script_dir/_zz_log.sh" i "Merging JSON from {U $source} into {U $target}..." +zz_log i "Merging JSON from {U $source} into {U $target}..." # Merge the source JSON into the target JSON jq 'def merge($a; $b): @@ -56,4 +54,4 @@ jq 'def merge($a; $b): $a end; -merge(.; input)' $target $source | "$script_dir/_normalize-json.sh" -c -a -i -t ${tabSize:-4} -f local -l true 2>/dev/null > /tmp/$$.merge && mv /tmp/$$.merge $target && "$script_dir/_zz_log.sh" s "JSON merged successfully" +merge(.; input)' $target $source | normalize-json -c -a -i -t ${tabSize:-4} -f local -l true 2>/dev/null > /tmp/$$.merge && mv /tmp/$$.merge $target && zz_log s "JSON merged successfully" diff --git a/src/common-utils/_zz_args.sh b/src/common-utils/_zz_args.sh index 7aef8187..e75676dc 100755 --- a/src/common-utils/_zz_args.sh +++ b/src/common-utils/_zz_args.sh @@ -1,9 +1,7 @@ #!/bin/sh -script_dir=$(dirname "$(readlink -f "$0")") - # Source colors script -. "$script_dir/_zz_colors.sh" +. zz_colors # Initialize variables count="0" diff --git a/src/common-utils/_zz_context.sh b/src/common-utils/_zz_context.sh index ab7be771..c5105c6d 100755 --- a/src/common-utils/_zz_context.sh +++ b/src/common-utils/_zz_context.sh @@ -1,13 +1,11 @@ #!/bin/sh -script_dir=$(dirname "$(readlink -f "$0")") - # Source colors script -. "$script_dir/_zz_colors.sh" +. zz_colors # Manage arguments eval $( - "$script_dir/_zz_args.sh" "Export Source/Targets folders depending on feature context" $0 "$@" <<-help + zz_args "Export Source/Targets folders depending on feature context" $0 "$@" <<-help s source source Force source directory t target target Force target directory - caller caller Force caller script diff --git a/src/common-utils/_zz_log.sh b/src/common-utils/_zz_log.sh index b3270d28..49f9a76b 100755 --- a/src/common-utils/_zz_log.sh +++ b/src/common-utils/_zz_log.sh @@ -1,9 +1,7 @@ #!/bin/sh -script_dir=$(dirname "$(readlink -f "$0")") - # Source colors script -. "$script_dir/_zz_colors.sh" +. zz_colors lvl="$1" && shift From 82b1036b6305d6fbd862a2d4933d0e398fa8a64b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 24 May 2026 20:28:37 +0000 Subject: [PATCH 5/9] refactor(common-utils): externalize bin directory selector Agent-Logs-Url: https://github.com/tomgrv/devcontainer-features/sessions/e017071e-498e-46e9-8391-13a1bd896378 Co-authored-by: tomgrv <1809566+tomgrv@users.noreply.github.com> --- src/common-utils/_install-bin.sh | 47 +---------------------------- src/common-utils/_select-bin.sh | 52 ++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 46 deletions(-) create mode 100755 src/common-utils/_select-bin.sh diff --git a/src/common-utils/_install-bin.sh b/src/common-utils/_install-bin.sh index be890ce5..4b8f3763 100755 --- a/src/common-utils/_install-bin.sh +++ b/src/common-utils/_install-bin.sh @@ -12,52 +12,7 @@ fi zz_log i "Installing bin scripts for {Purple $feature}..." -# Build candidate bin directories and pick the first writable one. -candidates="" - -add_candidate() { - candidate="$1" - [ -n "$candidate" ] || return 0 - - case ":$candidates:" in - *:"$candidate":*) ;; - *) - if [ -n "$candidates" ]; then - candidates="$candidates:$candidate" - else - candidates="$candidate" - fi - ;; - esac -} - -add_candidate "${INSTALL_BIN_DIR:-/usr/local/bin}" - -if [ -n "$HOME" ]; then - add_candidate "$HOME/.local/bin" -fi - -for dir in $(echo "$PATH" | tr ':' '\n'); do - case "$dir" in - "" | "." | "$PWD" | */node_modules/.bin) continue ;; - esac - add_candidate "$dir" -done - -add_candidate "$target/bin" - -link_dir="" -old_ifs=$IFS -IFS=':' -for candidate in $candidates; do - [ -d "$candidate" ] || mkdir -p "$candidate" 2>/dev/null || true - if [ -d "$candidate" ] && [ -w "$candidate" ]; then - link_dir="$candidate" - break - fi -done -IFS=$old_ifs - +link_dir="$(sh "$(dirname "$0")/_select-bin.sh" "$target")" [ -n "$link_dir" ] || { zz_log e "No writable bin directory found" exit 1 diff --git a/src/common-utils/_select-bin.sh b/src/common-utils/_select-bin.sh new file mode 100755 index 00000000..3f94529d --- /dev/null +++ b/src/common-utils/_select-bin.sh @@ -0,0 +1,52 @@ +#!/bin/sh + +target="${1:-$target}" + +candidates="" + +add_candidate() { + candidate="$1" + [ -n "$candidate" ] || return 0 + + case ":$candidates:" in + *:"$candidate":*) ;; + *) + if [ -n "$candidates" ]; then + candidates="$candidates:$candidate" + else + candidates="$candidate" + fi + ;; + esac +} + +add_candidate "${INSTALL_BIN_DIR:-/usr/local/bin}" + +if [ -n "$HOME" ]; then + add_candidate "$HOME/.local/bin" +fi + +for dir in $(echo "$PATH" | tr ':' '\n'); do + case "$dir" in + "" | "." | "$PWD" | */node_modules/.bin) continue ;; + esac + add_candidate "$dir" +done + +add_candidate "$target/bin" + +link_dir="" +old_ifs=$IFS +IFS=':' +for candidate in $candidates; do + [ -d "$candidate" ] || mkdir -p "$candidate" 2>/dev/null || true + if [ -d "$candidate" ] && [ -w "$candidate" ]; then + link_dir="$candidate" + break + fi +done +IFS=$old_ifs + +[ -n "$link_dir" ] || exit 1 + +echo "$link_dir" From 78df9ff3bb13e08645ee5643cae6bec23da39e3b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 09:07:51 +0000 Subject: [PATCH 6/9] docs(common-utils): document bin selection helper Agent-Logs-Url: https://github.com/tomgrv/devcontainer-features/sessions/36ca616c-1e49-477a-976d-c8ec39b73152 Co-authored-by: tomgrv <1809566+tomgrv@users.noreply.github.com> --- src/common-utils/README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/common-utils/README.md b/src/common-utils/README.md index efbdbe45..b6eef1bf 100644 --- a/src/common-utils/README.md +++ b/src/common-utils/README.md @@ -64,6 +64,15 @@ The feature also includes the following VS Code customizations: - Installs specified common utilities such as jq and dos2unix. - Supports specifying additional utilities to install. +## Install internals + +- `_install-bin.sh` installs links for common-utils scripts in a writable bin directory. +- `_select-bin.sh` resolves the writable bin directory with this fallback order: + 1. `INSTALL_BIN_DIR` (or `/usr/local/bin` by default) + 2. `~/.local/bin` + 3. First writable directory found in `$PATH` (excluding `node_modules/.bin`) + 4. `/bin` + ## Additional utilities In addition to the specified utilities, some additional local utilities are also provided: From 78e5a23828fde063f13ffe70a53598d1a9893b88 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 18:23:27 +0000 Subject: [PATCH 7/9] feat(common-utils): handle dependencies from devcontainer files in install.sh Agent-Logs-Url: https://github.com/tomgrv/devcontainer-features/sessions/a7655ecf-fb1d-4b9f-9583-dfd8bba2aa3c Co-authored-by: tomgrv <1809566+tomgrv@users.noreply.github.com> --- install.sh | 56 +++++++++++++++++++++++++------- src/common-utils/_install-bin.sh | 10 +++--- src/common-utils/_select-bin.sh | 37 +++++++++++++++++---- 3 files changed, 80 insertions(+), 23 deletions(-) diff --git a/install.sh b/install.sh index 2866950f..b4e21de7 100755 --- a/install.sh +++ b/install.sh @@ -27,27 +27,50 @@ help zz_log i "Installing devcontainer/features" +# Extract tomgrv devcontainer features listed in a devcontainer.json file +devcontainer_features() { + _file="$1" + [ -f "$_file" ] || return 0 + sed '/^\s*\/\//d' "$_file" | \ + jq -r '.features // {} | to_entries[] | + select(.key | contains("tomgrv/devcontainer-features")) | + .key | split("/")[-1] | split(":")[0]' 2>/dev/null +} + +# Find devcontainer.json files in standard locations and extract features +find_devcontainer_features() { + _search_dir="${1:-.}" + + # Check standard devcontainer file locations in priority order + for _f in \ + "$_search_dir/.devcontainer/devcontainer.json" \ + "$_search_dir/devcontainer.json" \ + "$_search_dir/.devcontainer.json"; do + if [ -f "$_f" ]; then + devcontainer_features "$_f" + return 0 + fi + done + + # Check .devcontainer//devcontainer.json for multiple configurations + _found=$(find "$_search_dir/.devcontainer" -maxdepth 2 -mindepth 2 -name "devcontainer.json" 2>/dev/null | head -1) + if [ -n "$_found" ]; then + devcontainer_features "$_found" + fi +} + # If 'all' argument is provided, set stubs and features to install all default features if [ -n "$all" ]; then echo "${Yellow}Add default features${End}" stubs=1 - features=$(sed '/^\s*\/\//d' $source/stubs/.devcontainer/devcontainer.json | jq -r '.features | to_entries[] | select(.key | contains("tomgrv/devcontainer-features"))| .key| - split("/")[-1] | split(":")[0]') + features=$(find_devcontainer_features "$source/stubs") fi # If 'upd' argument is provided, set stubs and features to update all features if [ -n "$upd" ]; then echo "${Green}Update features${End}" stubs=1 - features=$( - sed '/^\s*\/\//d' .devcontainer/devcontainer.json | - jq -r '.features | to_entries[] | - select(.key | contains("tomgrv/devcontainer-features")) | - .key | - split("/")[-1] | - split(":")[0]' | - tr '\n' ' ' - ) + features=$(find_devcontainer_features "." | tr '\n' ' ') fi # If 'package' argument is provided, use the specified package.json file @@ -62,7 +85,16 @@ if [ -n "$package" ]; then # Extract features from the package.json file if not already set if [ -z "$features" ]; then - features=$(cat $file | jq -r '.devcontainer.features | to_entries[] | select(.key | contains("tomgrv/devcontainer-features"))| .key| split("/")[-1] | split(":")[0]') + features=$(jq -r '.devcontainer.features // {} | to_entries[] | select(.key | contains("tomgrv/devcontainer-features"))| .key| split("/")[-1] | split(":")[0]' "$file") + fi +fi + +# If no features specified so far, auto-detect from devcontainer files in current directory +if [ -z "$features" ] && [ -z "$stubs" ] && [ -z "$all" ]; then + detected=$(find_devcontainer_features "." | tr '\n' ' ') + if [ -n "$detected" ]; then + features="$detected" + echo "${Green}Detected features from devcontainer files: $features${End}" fi fi diff --git a/src/common-utils/_install-bin.sh b/src/common-utils/_install-bin.sh index 4b8f3763..f61d7659 100755 --- a/src/common-utils/_install-bin.sh +++ b/src/common-utils/_install-bin.sh @@ -26,9 +26,9 @@ case ":$PATH:" in ;; esac -# Find all shell scripts in the target directory, make them executable, and create symbolic links in /usr/local/bin -find $target -type f -name "_*.sh" -exec echo {} \; -exec chmod +x {} \; | while read file; do - # Create a symbolic link in /usr/local/bin with the script name (without the leading underscore and .sh extension) - link=$link_dir/$(basename $file | sed 's/^_//;s/.sh$//') - ln -sf $file $link && zz_log s "Linked {U $file} to {U $link}" +# Find all shell scripts in the target directory, make them executable, and create symbolic links in the selected bin directory +find "$target" -type f -name "_*.sh" -exec echo {} \; -exec chmod +x {} \; | while IFS= read -r file; do + # Create a symbolic link in the selected bin directory with the script name (without the leading underscore and .sh extension) + link="$link_dir/$(basename "$file" | sed 's/^_//;s/.sh$//')" + ln -sf "$file" "$link" && zz_log s "Linked {U $file} to {U $link}" done diff --git a/src/common-utils/_select-bin.sh b/src/common-utils/_select-bin.sh index 3f94529d..28b2fbcc 100755 --- a/src/common-utils/_select-bin.sh +++ b/src/common-utils/_select-bin.sh @@ -3,9 +3,11 @@ target="${1:-$target}" candidates="" +creatable="" add_candidate() { candidate="$1" + can_create="${2:-0}" [ -n "$candidate" ] || return 0 case ":$candidates:" in @@ -16,35 +18,58 @@ add_candidate() { else candidates="$candidate" fi + if [ "$can_create" = "1" ]; then + if [ -n "$creatable" ]; then + creatable="$creatable:$candidate" + else + creatable="$candidate" + fi + fi ;; esac } -add_candidate "${INSTALL_BIN_DIR:-/usr/local/bin}" +add_candidate "${INSTALL_BIN_DIR:-/usr/local/bin}" 1 if [ -n "$HOME" ]; then - add_candidate "$HOME/.local/bin" + add_candidate "$HOME/.local/bin" 1 fi -for dir in $(echo "$PATH" | tr ':' '\n'); do +old_ifs=$IFS +IFS=':' +for dir in $PATH; do case "$dir" in "" | "." | "$PWD" | */node_modules/.bin) continue ;; esac add_candidate "$dir" done +IFS=$old_ifs -add_candidate "$target/bin" +add_candidate "$target/bin" 1 link_dir="" old_ifs=$IFS IFS=':' + +# First pass: find an existing writable and executable directory for candidate in $candidates; do - [ -d "$candidate" ] || mkdir -p "$candidate" 2>/dev/null || true - if [ -d "$candidate" ] && [ -w "$candidate" ]; then + if [ -d "$candidate" ] && [ -w "$candidate" ] && [ -x "$candidate" ]; then link_dir="$candidate" break fi done + +# Second pass: try to create known/safe directories if none found yet +if [ -z "$link_dir" ]; then + for candidate in $creatable; do + mkdir -p "$candidate" 2>/dev/null || true + if [ -d "$candidate" ] && [ -w "$candidate" ] && [ -x "$candidate" ]; then + link_dir="$candidate" + break + fi + done +fi + IFS=$old_ifs [ -n "$link_dir" ] || exit 1 From 481827c63fce18deb38073373cfb0015d2da44eb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 18:30:57 +0000 Subject: [PATCH 8/9] feat(common-utils): resolve transitive deps from devcontainer-feature.json in install.sh Agent-Logs-Url: https://github.com/tomgrv/devcontainer-features/sessions/db7bb9c2-a804-4a41-8459-156d6f45c41d Co-authored-by: tomgrv <1809566+tomgrv@users.noreply.github.com> --- install.sh | 71 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/install.sh b/install.sh index b4e21de7..63aa8a2d 100755 --- a/install.sh +++ b/install.sh @@ -59,6 +59,71 @@ find_devcontainer_features() { fi } +# Extract tomgrv/devcontainer-features dependencies from a feature's devcontainer-feature.json +feature_deps() { + _manifest="$source/src/$1/devcontainer-feature.json" + [ -f "$_manifest" ] || return 0 + jq -r '.dependsOn // {} | to_entries[] | + select(.key | contains("tomgrv/devcontainer-features")) | + .key | split("/")[-1] | split(":")[0]' "$_manifest" 2>/dev/null +} + +# Expand a whitespace-separated feature list to include all transitive tomgrv/devcontainer-features +# dependencies, ordered so dependencies appear before the features that need them. +resolve_feature_deps() { + # Step 1: collect the full set of reachable features (original + all transitive deps) + _all="" + _queue="$1" + while [ -n "$_queue" ]; do + _next="" + for _f in $_queue; do + case " $_all " in + *" $_f "*) continue ;; + esac + _all="$_all $_f" + for _dep in $(feature_deps "$_f"); do + case " $_all $_next " in + *" $_dep "*) ;; + *) _next="$_next $_dep" ;; + esac + done + done + _queue="$_next" + done + + # Step 2: topological sort — repeatedly emit features whose tomgrv deps are all emitted + _emitted="" + _pending="$_all" + _changed=1 + while [ "$_changed" = "1" ] && [ -n "$_pending" ]; do + _changed=0 + _still_pending="" + for _f in $_pending; do + _deps_ok=1 + for _dep in $(feature_deps "$_f"); do + case " $_emitted " in + *" $_dep "*) ;; + *) + _deps_ok=0 + break + ;; + esac + done + if [ "$_deps_ok" = "1" ]; then + _emitted="$_emitted $_f" + _changed=1 + else + _still_pending="$_still_pending $_f" + fi + done + _pending="$_still_pending" + done + # Append any remaining features (handles cycles or missing manifests) + _emitted="$_emitted $_pending" + + echo "$_emitted" | tr ' ' '\n' | grep -v '^$' +} + # If 'all' argument is provided, set stubs and features to install all default features if [ -n "$all" ]; then echo "${Yellow}Add default features${End}" @@ -98,6 +163,12 @@ if [ -z "$features" ] && [ -z "$stubs" ] && [ -z "$all" ]; then fi fi +# Expand feature list with transitive tomgrv/devcontainer-features dependencies +# from each feature's devcontainer-feature.json, ordered deps-first +if [ -n "$features" ]; then + features=$(resolve_feature_deps "$features" | tr '\n' ' ') +fi + # Merge all files from the stub folder to the root using git merge-file if stubs are selected if [ -n "$stubs" ]; then echo "${Yellow}Installing stubs from ${UYellow}$source/src/common-utils/${Yellow}...${End}" From f72aac6beadbc028d032e38e563d1c8a566f68bb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 25 May 2026 19:04:04 +0000 Subject: [PATCH 9/9] refactor(common-utils): extract dep resolution to _install-deps.sh and feature install to _install-feat.sh Agent-Logs-Url: https://github.com/tomgrv/devcontainer-features/sessions/e5fc286c-1e46-438a-a07d-ef0ff70f873b Co-authored-by: tomgrv <1809566+tomgrv@users.noreply.github.com> --- install.sh | 165 +++++------------------------- src/common-utils/_install-deps.sh | 78 ++++++++++++++ src/common-utils/_install-feat.sh | 73 +++++++++++++ 3 files changed, 179 insertions(+), 137 deletions(-) create mode 100755 src/common-utils/_install-deps.sh create mode 100755 src/common-utils/_install-feat.sh diff --git a/install.sh b/install.sh index 63aa8a2d..2faf4ac0 100755 --- a/install.sh +++ b/install.sh @@ -8,11 +8,17 @@ source=$(dirname $(readlink -f $0)) alias zz_log=$source/src/common-utils/_zz_log.sh +# Track whether this is the root-level invocation to control symlink lifecycle +_install_root="${INSTALL_ROOT_CALL:-1}" +export INSTALL_ROOT_CALL=0 + # Prepare for local installation by creating a temporary directory and linking common utils -find $source/src/common-utils/ -type f -name "_*.sh" -exec echo {} \; -exec chmod +x {} \; | while read file; do - ln -sf $file $source/src/common-utils/$(basename $file | sed 's/^_//;s/.sh$//') -done -export PATH=$PATH:$source/src/common-utils +if [ "$_install_root" = "1" ]; then + find $source/src/common-utils/ -type f -name "_*.sh" -exec echo {} \; -exec chmod +x {} \; | while read file; do + ln -sf $file $source/src/common-utils/$(basename $file | sed 's/^_//;s/.sh$//') + done + export PATH=$PATH:$source/src/common-utils +fi # Load arguments for the script eval $( @@ -59,71 +65,6 @@ find_devcontainer_features() { fi } -# Extract tomgrv/devcontainer-features dependencies from a feature's devcontainer-feature.json -feature_deps() { - _manifest="$source/src/$1/devcontainer-feature.json" - [ -f "$_manifest" ] || return 0 - jq -r '.dependsOn // {} | to_entries[] | - select(.key | contains("tomgrv/devcontainer-features")) | - .key | split("/")[-1] | split(":")[0]' "$_manifest" 2>/dev/null -} - -# Expand a whitespace-separated feature list to include all transitive tomgrv/devcontainer-features -# dependencies, ordered so dependencies appear before the features that need them. -resolve_feature_deps() { - # Step 1: collect the full set of reachable features (original + all transitive deps) - _all="" - _queue="$1" - while [ -n "$_queue" ]; do - _next="" - for _f in $_queue; do - case " $_all " in - *" $_f "*) continue ;; - esac - _all="$_all $_f" - for _dep in $(feature_deps "$_f"); do - case " $_all $_next " in - *" $_dep "*) ;; - *) _next="$_next $_dep" ;; - esac - done - done - _queue="$_next" - done - - # Step 2: topological sort — repeatedly emit features whose tomgrv deps are all emitted - _emitted="" - _pending="$_all" - _changed=1 - while [ "$_changed" = "1" ] && [ -n "$_pending" ]; do - _changed=0 - _still_pending="" - for _f in $_pending; do - _deps_ok=1 - for _dep in $(feature_deps "$_f"); do - case " $_emitted " in - *" $_dep "*) ;; - *) - _deps_ok=0 - break - ;; - esac - done - if [ "$_deps_ok" = "1" ]; then - _emitted="$_emitted $_f" - _changed=1 - else - _still_pending="$_still_pending $_f" - fi - done - _pending="$_still_pending" - done - # Append any remaining features (handles cycles or missing manifests) - _emitted="$_emitted $_pending" - - echo "$_emitted" | tr ' ' '\n' | grep -v '^$' -} - # If 'all' argument is provided, set stubs and features to install all default features if [ -n "$all" ]; then echo "${Yellow}Add default features${End}" @@ -163,10 +104,10 @@ if [ -z "$features" ] && [ -z "$stubs" ] && [ -z "$all" ]; then fi fi -# Expand feature list with transitive tomgrv/devcontainer-features dependencies -# from each feature's devcontainer-feature.json, ordered deps-first +# Resolve transitive dependencies and expand the feature list in topological order +# using install-deps as the dedicated resolver if [ -n "$features" ]; then - features=$(resolve_feature_deps "$features" | tr '\n' ' ') + features=$(install-deps "$source" $features | tr '\n' ' ') fi # Merge all files from the stub folder to the root using git merge-file if stubs are selected @@ -176,75 +117,25 @@ if [ -n "$stubs" ]; then echo "${Green}Stubs installed${End}" fi -# If features are selected, proceed with installation +# If features are selected, delegate installation of each to install-feat if [ -n "$features" ]; then echo "${Green}Selected features: $features${End}" - # Create an alias for the _install-feature.sh script - alias install-feature=$(dirname $0)/src/common-utils/_install-feature.sh - - # Check if the script is running inside a container - if [ "$CODESPACES" != "true" ] && [ "$REMOTE_CONTAINERS" != "true" ] && [ -z "$DEV_CONTAINER_FILE_PATH" ]; then - - echo "${Red}You are not in a container${End}" - - # Run the install.sh script for each selected feature - for feature in $features; do - if [ -f "$source/src/$feature/install.sh" ]; then - echo "${Yellow}Running src/$feature/install.sh...${End}" - if sh $source/src/$feature/install.sh; then - echo "${Green}$feature installed${End}" - else - echo "${Red}$feature installation failed${End}" - exit 1 - fi - else - echo "${Red}$feature not found${End}" - exit 1 - fi - done - - # Run the configure.sh script for each selected feature - for feature in $features; do - featureSource="" - if [ -d "/tmp/$feature" ]; then - featureSource="/tmp/$feature" - elif [ -d "/usr/local/share/$feature" ]; then - featureSource="/usr/local/share/$feature" - fi - - if [ -n "$featureSource" ]; then - echo "${Yellow}Configuring $featureSource...${End}" - sh $source/src/common-utils/_configure-feature.sh -s $featureSource $feature - echo "${Green}$feature configured${End}" - else - echo "${Red}$feature not found${End}" - exit 1 - fi - done - - elif [ -n "$stubs" ]; then - - # stubs are selected, configure stubs of the selected features - for feature in $features; do - echo "${Yellow}Deploying stubs for $feature...${End}" - $source/src/common-utils/_configure-feature.sh -s $source/src/$feature $feature - echo "${Green}Stubs for $feature deployed${End}" - done - - else - # If inside a container, suggest using devutils as devcontainer features - echo "${Purple}You are in a container: use as devcontainer features:${End}" - for feature in $features; do - echo "${Purple}ghcr.io/tomgrv/devcontainer-features/$feature${End}" - done + for feature in $features; do + if [ -n "$stubs" ]; then + install-feat "$source" "$feature" --stubs + else + install-feat "$source" "$feature" + fi + done - fi fi -# Remoce all links to common utils -echo "Remove temp files..." -find $source/src/common-utils/ -type f -name "_*.sh" -exec echo {} \; -exec chmod +x {} \; | while read file; do - rm $source/src/common-utils/$(basename $file | sed 's/^_//;s/.sh$//') -done +# Remove all links to common utils (only at root-level invocation) +if [ "$_install_root" = "1" ]; then + echo "Remove temp files..." + find $source/src/common-utils/ -type f -name "_*.sh" -exec echo {} \; -exec chmod +x {} \; | while read file; do + rm $source/src/common-utils/$(basename $file | sed 's/^_//;s/.sh$//') + done +fi diff --git a/src/common-utils/_install-deps.sh b/src/common-utils/_install-deps.sh new file mode 100755 index 00000000..5a87bb9b --- /dev/null +++ b/src/common-utils/_install-deps.sh @@ -0,0 +1,78 @@ +#!/bin/sh + +# Resolves transitive tomgrv/devcontainer-features dependencies from devcontainer-feature.json files. +# Usage: install-deps [...] +# Outputs the complete dependency list in topological order (deps before dependents), one per line. + +_source="$1" +shift + +if [ -z "$_source" ]; then + echo "Usage: install-deps ..." >&2 + exit 1 +fi + +features="$*" +[ -z "$features" ] && exit 0 + +# Extract direct tomgrv/devcontainer-features dependencies of a single feature +_feature_deps() { + _manifest="$_source/src/$1/devcontainer-feature.json" + [ -f "$_manifest" ] || return 0 + jq -r '.dependsOn // {} | to_entries[] | + select(.key | contains("tomgrv/devcontainer-features")) | + .key | split("/")[-1] | split(":")[0]' "$_manifest" 2>/dev/null +} + +# BFS: collect the full set of reachable features (input + all transitive deps) +_all="" +_queue="$features" +while [ -n "$_queue" ]; do + _next="" + for _f in $_queue; do + case " $_all " in + *" $_f "*) continue ;; + esac + _all="$_all $_f" + for _dep in $(_feature_deps "$_f"); do + case " $_all $_next " in + *" $_dep "*) ;; + *) _next="$_next $_dep" ;; + esac + done + done + _queue="$_next" +done + +# Topological sort: emit features whose tomgrv deps are all already emitted +_emitted="" +_pending="$_all" +_changed=1 +while [ "$_changed" = "1" ] && [ -n "$_pending" ]; do + _changed=0 + _still_pending="" + for _f in $_pending; do + _deps_ok=1 + for _dep in $(_feature_deps "$_f"); do + case " $_emitted " in + *" $_dep "*) ;; + *) + _deps_ok=0 + break + ;; + esac + done + if [ "$_deps_ok" = "1" ]; then + _emitted="$_emitted $_f" + _changed=1 + else + _still_pending="$_still_pending $_f" + fi + done + _pending="$_still_pending" +done + +# Append any remaining features (handles cycles or missing manifests) +_emitted="$_emitted $_pending" + +echo "$_emitted" | tr ' ' '\n' | grep -v '^$' diff --git a/src/common-utils/_install-feat.sh b/src/common-utils/_install-feat.sh new file mode 100755 index 00000000..9670daf8 --- /dev/null +++ b/src/common-utils/_install-feat.sh @@ -0,0 +1,73 @@ +#!/bin/sh + +# Installs a single devcontainer feature and its tomgrv/devcontainer-features dependencies. +# Uses install.sh as the orchestrator for recursive dependency installation. +# A tracker file (INSTALL_FEAT_TRACKER) prevents re-installation within the same session. +# +# Usage: install-feat [--stubs] + +_source="$1" +_feature="$2" +_stubs="${3:-}" + +if [ -z "$_source" ] || [ -z "$_feature" ]; then + echo "Usage: install-feat [--stubs]" >&2 + exit 1 +fi + +# Prevent re-installation within the same install session +_tracker="${INSTALL_FEAT_TRACKER:-/tmp/.install-feat-$$}" +export INSTALL_FEAT_TRACKER="$_tracker" + +if grep -qxF "$_feature" "$_tracker" 2>/dev/null; then + exit 0 +fi +echo "$_feature" >>"$_tracker" + +# Install each dependency first, using install.sh as orchestrator (recursive) +for _dep in $(install-deps "$_source" "$_feature"); do + [ "$_dep" = "$_feature" ] && continue + if [ -n "$_stubs" ]; then + sh "$_source/install.sh" -s "$_dep" + else + sh "$_source/install.sh" "$_dep" + fi +done + +# Check if the script is running inside a container +if [ "$CODESPACES" != "true" ] && [ "$REMOTE_CONTAINERS" != "true" ] && [ -z "$DEV_CONTAINER_FILE_PATH" ]; then + + # Install the feature itself + if [ -f "$_source/src/$_feature/install.sh" ]; then + sh "$_source/src/$_feature/install.sh" || exit 1 + else + echo "Feature $_feature not found in $_source/src/" >&2 + exit 1 + fi + + # Configure the feature after installation + _featureSource="" + if [ -d "/tmp/$_feature" ]; then + _featureSource="/tmp/$_feature" + elif [ -d "/usr/local/share/$_feature" ]; then + _featureSource="/usr/local/share/$_feature" + fi + + if [ -n "$_featureSource" ]; then + sh "$_source/src/common-utils/_configure-feature.sh" -s "$_featureSource" "$_feature" + else + echo "Feature $_feature installation target not found" >&2 + exit 1 + fi + +elif [ -n "$_stubs" ]; then + + # In container with stubs: deploy stubs for this feature + sh "$_source/src/common-utils/_configure-feature.sh" -s "$_source/src/$_feature" "$_feature" + +else + + # Inside a container without stubs: suggest using as devcontainer feature + echo "You are in a container: use as devcontainer feature: ghcr.io/tomgrv/devcontainer-features/$_feature" + +fi