mirror of
https://gitlab.com/TECHNOFAB/soonix.git
synced 2026-02-02 07:15:06 +01:00
Merge branch 'feat/cli' into 'main'
feat!: add cli with update and list subcommands + gitignore param See merge request TECHNOFAB/soonix!3
This commit is contained in:
commit
9d7257d48c
6 changed files with 245 additions and 105 deletions
|
|
@ -16,7 +16,7 @@
|
|||
},
|
||||
"postUpgradeTasks": {
|
||||
"commands": [
|
||||
"nix-portable nix run .#soonix:update"
|
||||
"nix-portable nix run .#soonix -- update --skip-gitignore"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,4 +22,5 @@ in rec {
|
|||
};
|
||||
|
||||
mkShellHook = userConfig: (make userConfig).config.shellHook;
|
||||
mkCLI = userConfig: (make userConfig).config.packages.soonix;
|
||||
}
|
||||
|
|
|
|||
320
lib/module.nix
320
lib/module.nix
|
|
@ -4,7 +4,7 @@
|
|||
lib,
|
||||
...
|
||||
}: let
|
||||
inherit (lib) types mkOption concatMapStringsSep;
|
||||
inherit (lib) types mkOption concatMapStringsSep mapAttrsToList;
|
||||
soonix_lib = import ./. {inherit pkgs;};
|
||||
inherit (soonix_lib) engines buildAllFiles;
|
||||
in {
|
||||
|
|
@ -146,15 +146,14 @@ in {
|
|||
Packages for updating the hooks without a devshell. (readonly)
|
||||
'';
|
||||
example = {
|
||||
"soonix:update" = "<derivation>";
|
||||
"soonix" = "<derivation>";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
config = let
|
||||
hooks = config.hooks;
|
||||
hookNames = builtins.attrNames hooks;
|
||||
|
||||
# allow excluding gitignore since stuff like renovate can't use/commit it anyways
|
||||
runHooks = concatMapStringsSep "\n" (hookName: let
|
||||
hook = hooks.${hookName};
|
||||
modes = {
|
||||
|
|
@ -163,8 +162,12 @@ in {
|
|||
''
|
||||
if [[ ! -L "${hook.output}" ]] || [[ "$(readlink "${hook.output}")" != "${hook.generatedDerivation}" ]]; then
|
||||
_soonix_log "info" "${hookName}" "Creating symlink: ${hook.output} -> ${hook.generatedDerivation}"
|
||||
mkdir -p "$(dirname "${hook.output}")"
|
||||
ln -sf "${hook.generatedDerivation}" "${hook.output}"
|
||||
if [[ "$CHECK_MODE" != "true" ]]; then
|
||||
mkdir -p "$(dirname "${hook.output}")"
|
||||
ln -sf "${hook.generatedDerivation}" "${hook.output}"
|
||||
else
|
||||
_soonix_log "info" "${hookName}" "Would create symlink: ${hook.output} -> ${hook.generatedDerivation}"
|
||||
fi
|
||||
_changed=true
|
||||
else
|
||||
_soonix_log "info" "${hookName}" "Symlink up to date: ${hook.output}"
|
||||
|
|
@ -175,10 +178,14 @@ in {
|
|||
''
|
||||
if [[ ! -f "${hook.output}" ]] || ! cmp -s "${hook.generatedDerivation}" "${hook.output}"; then
|
||||
_soonix_log "info" "${hookName}" "Copying file: ${hook.generatedDerivation} -> ${hook.output}"
|
||||
mkdir -p "$(dirname "${hook.output}")"
|
||||
# required since they're read only
|
||||
rm -f "${hook.output}"
|
||||
cp "${hook.generatedDerivation}" "${hook.output}"
|
||||
if [[ "$CHECK_MODE" != "true" ]]; then
|
||||
mkdir -p "$(dirname "${hook.output}")"
|
||||
# required since they're read only
|
||||
rm -f "${hook.output}"
|
||||
cp "${hook.generatedDerivation}" "${hook.output}"
|
||||
else
|
||||
_soonix_log "info" "${hookName}" "Would copy file: ${hook.generatedDerivation} -> ${hook.output}"
|
||||
fi
|
||||
_changed=true
|
||||
else
|
||||
_soonix_log "info" "${hookName}" "File up to date: ${hook.output}"
|
||||
|
|
@ -192,104 +199,128 @@ in {
|
|||
_soonix_add_to_gitignore "${hook.output}"
|
||||
''
|
||||
else "";
|
||||
|
||||
isGitignored =
|
||||
if hook.hook.gitignore
|
||||
then "true"
|
||||
else "false";
|
||||
in
|
||||
builtins.addErrorContext "[soonix] while generating script for ${hookName}"
|
||||
# sh
|
||||
''
|
||||
# Process hook: ${hookName}
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
UPDATED) _soonix_updated+=("${hookName}") ;;
|
||||
UPTODATE) _soonix_uptodate+=("${hookName}") ;;
|
||||
*) echo "$line" ;;
|
||||
esac
|
||||
done < <(
|
||||
set -euo pipefail
|
||||
_changed=false
|
||||
# Skip if SKIP_GITIGNORE is set and this hook is gitignored
|
||||
if [[ "$SKIP_GITIGNORE" == "true" && "${isGitignored}" == "true" ]]; then
|
||||
: # skip
|
||||
else
|
||||
while IFS= read -r line; do
|
||||
case "$line" in
|
||||
UPDATED) _soonix_updated+=("${hookName}") ;;
|
||||
UPTODATE) _soonix_uptodate+=("${hookName}") ;;
|
||||
*) echo "$line" ;;
|
||||
esac
|
||||
done < <(
|
||||
set -euo pipefail
|
||||
_changed=false
|
||||
|
||||
${modes.${hook.hook.mode} or (throw "Mode ${hook.hook.mode} doesnt exist")}
|
||||
${modes.${hook.hook.mode} or (throw "Mode ${hook.hook.mode} doesnt exist")}
|
||||
|
||||
# Add to gitignore if requested
|
||||
${optionalGitignore}
|
||||
if [[ "$CHECK_MODE" != "true" ]]; then
|
||||
# Add to gitignore if requested
|
||||
${optionalGitignore}
|
||||
|
||||
# Run extra commands if file changed
|
||||
if [[ "$_changed" == "true" && -n "${hook.hook.extra}" ]]; then
|
||||
_soonix_log "info" "${hookName}" "Running extra command: ${hook.hook.extra}"
|
||||
eval "${hook.hook.extra}"
|
||||
fi
|
||||
|
||||
if [[ "$_changed" == "true" ]]; then
|
||||
echo "UPDATED"
|
||||
else
|
||||
echo "UPTODATE"
|
||||
fi
|
||||
) || {
|
||||
_soonix_log "error" "${hookName}" "Failed to process hook"
|
||||
_soonix_failed+=("${hookName}")
|
||||
}
|
||||
'')
|
||||
hookNames;
|
||||
|
||||
generatedShellHook =
|
||||
# sh
|
||||
''
|
||||
_soonix_log() {
|
||||
local level="$1"
|
||||
local hook="$2"
|
||||
local message="$3"
|
||||
[[ "''${SOONIX_LOG-}" == "true" ]] && echo "$level [$hook]: $message" || true
|
||||
}
|
||||
|
||||
_soonix_add_to_gitignore() {
|
||||
local file="$1"
|
||||
local gitignore=".gitignore"
|
||||
|
||||
if [[ ! -f "$gitignore" ]]; then
|
||||
touch "$gitignore"
|
||||
fi
|
||||
|
||||
# Check if file is already in gitignore
|
||||
if ! grep -Fxq "/$file" "$gitignore"; then
|
||||
# Add sentinel comments if not present
|
||||
if ! grep -q "# soonix" "$gitignore"; then
|
||||
echo "" >> "$gitignore"
|
||||
echo "# soonix" >> "$gitignore"
|
||||
echo "# end soonix" >> "$gitignore"
|
||||
# Run extra commands if file changed
|
||||
if [[ "$_changed" == "true" && -n "${hook.hook.extra}" ]]; then
|
||||
_soonix_log "info" "${hookName}" "Running extra command: ${hook.hook.extra}"
|
||||
eval "${hook.hook.extra}"
|
||||
fi
|
||||
fi
|
||||
|
||||
# Insert the file path before the end comment
|
||||
${pkgs.gnused}/bin/sed -i "/# end soonix/i /$file" "$gitignore"
|
||||
if [[ "$_changed" == "true" ]]; then
|
||||
echo "UPDATED"
|
||||
else
|
||||
echo "UPTODATE"
|
||||
fi
|
||||
) || {
|
||||
_soonix_log "error" "${hookName}" "Failed to process hook"
|
||||
_soonix_failed+=("${hookName}")
|
||||
}
|
||||
fi
|
||||
'')
|
||||
(builtins.attrNames hooks);
|
||||
|
||||
generateShellHook =
|
||||
builtins.addErrorContext "[soonix] while generating shell hook"
|
||||
# sh
|
||||
''
|
||||
function _soonix() {
|
||||
CHECK_MODE=''${CHECK_MODE:-false}
|
||||
SKIP_GITIGNORE=''${SKIP_GITIGNORE:-false}
|
||||
|
||||
_soonix_log() {
|
||||
local level="$1"
|
||||
local hook="$2"
|
||||
local message="$3"
|
||||
[[ "''${SOONIX_LOG-}" == "true" ]] && echo "$level [$hook]: $message" || true
|
||||
}
|
||||
|
||||
_soonix_add_to_gitignore() {
|
||||
local file="$1"
|
||||
local gitignore=".gitignore"
|
||||
|
||||
if [[ ! -f "$gitignore" ]]; then
|
||||
touch "$gitignore"
|
||||
fi
|
||||
|
||||
# Check if file is already in gitignore
|
||||
if ! grep -Fxq "/$file" "$gitignore"; then
|
||||
# Add sentinel comments if not present
|
||||
if ! grep -q "# soonix" "$gitignore"; then
|
||||
echo "" >> "$gitignore"
|
||||
echo "# soonix" >> "$gitignore"
|
||||
echo "# end soonix" >> "$gitignore"
|
||||
fi
|
||||
|
||||
# Insert the file path before the end comment
|
||||
${pkgs.gnused}/bin/sed -i "/# end soonix/i /$file" "$gitignore"
|
||||
fi
|
||||
}
|
||||
|
||||
_soonix_updated=()
|
||||
_soonix_failed=()
|
||||
_soonix_uptodate=()
|
||||
|
||||
${runHooks}
|
||||
|
||||
local output=$'\E[msoonix:\E[38;5;8m'
|
||||
local status=0
|
||||
|
||||
if [[ ''${#_soonix_updated[@]} -gt 0 ]]; then
|
||||
output="$output [updated: ''${_soonix_updated[*]}]" >&2
|
||||
if [[ "$CHECK_MODE" == "true" ]]; then
|
||||
status=2
|
||||
fi
|
||||
fi
|
||||
if [[ ''${#_soonix_uptodate[@]} -gt 0 ]]; then
|
||||
output="$output [unchanged: ''${_soonix_uptodate[*]}]" >&2
|
||||
fi
|
||||
if [[ ''${#_soonix_failed[@]} -gt 0 ]]; then
|
||||
output="$output [failed: ''${_soonix_failed[*]}]" >&2
|
||||
status=1
|
||||
fi
|
||||
|
||||
printf "%s\E[m\n" "$output" >&2
|
||||
|
||||
if [[ $status -ne 0 ]]; then
|
||||
exit $status
|
||||
fi
|
||||
}
|
||||
|
||||
_soonix_updated=()
|
||||
_soonix_failed=()
|
||||
_soonix_uptodate=()
|
||||
|
||||
${runHooks}
|
||||
|
||||
local output=$'\E[msoonix:\E[38;5;8m'
|
||||
local status=0
|
||||
|
||||
if [[ ''${#_soonix_updated[@]} -gt 0 ]]; then
|
||||
output="$output [updated: ''${_soonix_updated[*]}]" >&2
|
||||
fi
|
||||
if [[ ''${#_soonix_uptodate[@]} -gt 0 ]]; then
|
||||
output="$output [unchanged: ''${_soonix_uptodate[*]}]" >&2
|
||||
fi
|
||||
if [[ ''${#_soonix_failed[@]} -gt 0 ]]; then
|
||||
output="$output [failed: ''${_soonix_failed[*]}]" >&2
|
||||
status=1
|
||||
fi
|
||||
|
||||
printf "%s\E[m\n" "$output" >&2
|
||||
|
||||
if [[ $status -eq 1 ]]; then
|
||||
exit 1
|
||||
fi
|
||||
_soonix
|
||||
unset _soonix
|
||||
'';
|
||||
|
||||
allFiles =
|
||||
lib.mapAttrsToList (name: hook: {
|
||||
mapAttrsToList (name: hook: {
|
||||
src = hook.generatedDerivation;
|
||||
path = hook.output;
|
||||
})
|
||||
|
|
@ -297,8 +328,8 @@ in {
|
|||
in rec {
|
||||
# nothing to do if no hooks exist
|
||||
shellHook =
|
||||
if (builtins.length hookNames > 0)
|
||||
then generatedShellHook
|
||||
if (builtins.length (builtins.attrNames config.hooks) > 0)
|
||||
then generateShellHook
|
||||
else "";
|
||||
shellHookFile = pkgs.writeShellScript "shellHook" shellHook;
|
||||
devshellModule = {
|
||||
|
|
@ -307,11 +338,96 @@ in {
|
|||
};
|
||||
finalFiles = buildAllFiles allFiles;
|
||||
# make it simpler to update the hooks without any devshell
|
||||
packages."soonix:update" = pkgs.writeShellScriptBin "soonix:update" ''
|
||||
function _soonix() {
|
||||
${shellHook}
|
||||
}
|
||||
_soonix
|
||||
'';
|
||||
packages = {
|
||||
soonix =
|
||||
pkgs.writeShellScriptBin "soonix"
|
||||
# sh
|
||||
''
|
||||
set -euo pipefail
|
||||
|
||||
SKIP_GITIGNORE=false
|
||||
CHECK_MODE=false
|
||||
COMMAND=""
|
||||
|
||||
show_help() {
|
||||
cat << EOF
|
||||
soonix - Declarative file management tool
|
||||
|
||||
USAGE:
|
||||
soonix <COMMAND> [OPTIONS]
|
||||
|
||||
COMMANDS:
|
||||
update Update all managed files
|
||||
check Check if all files are up to date (dry-run)
|
||||
list List all managed file targets
|
||||
help Show this help message
|
||||
|
||||
OPTIONS:
|
||||
--skip-gitignore Skip files that would be added to .gitignore
|
||||
|
||||
EXAMPLES:
|
||||
soonix update # Update all files
|
||||
soonix update --skip-gitignore # Update only non-gitignored files
|
||||
soonix list # List all targets
|
||||
soonix list --skip-gitignore # List only non-gitignored targets
|
||||
EOF
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
update|check|list|help)
|
||||
COMMAND="$1"
|
||||
shift
|
||||
;;
|
||||
--skip-gitignore)
|
||||
SKIP_GITIGNORE=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "Error: Unknown argument '$1'" >&2
|
||||
echo ""
|
||||
show_help
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ -z "$COMMAND" ]]; then
|
||||
echo "Error: No command specified" >&2
|
||||
echo ""
|
||||
show_help
|
||||
exit 1
|
||||
fi
|
||||
|
||||
case "$COMMAND" in
|
||||
help)
|
||||
show_help
|
||||
;;
|
||||
list)
|
||||
${concatMapStringsSep "\n" (hookName: let
|
||||
hook = hooks.${hookName};
|
||||
in ''
|
||||
if [[ "$SKIP_GITIGNORE" == "true" && "${
|
||||
if hook.hook.gitignore
|
||||
then "true"
|
||||
else "false"
|
||||
}" == "true" ]]; then
|
||||
: # skip
|
||||
else
|
||||
echo "${hook.output}"
|
||||
fi
|
||||
'') (builtins.attrNames hooks)}
|
||||
;;
|
||||
update)
|
||||
${generateShellHook}
|
||||
;;
|
||||
check)
|
||||
CHECK_MODE=true
|
||||
echo "Checking files..."
|
||||
${generateShellHook}
|
||||
;;
|
||||
esac
|
||||
'';
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -41,8 +41,10 @@ in {
|
|||
}
|
||||
{
|
||||
name = "soonix";
|
||||
files = "nix run .#soonix -- list --skip-gitignore";
|
||||
stage_fixed = true;
|
||||
run = "nix run .#soonix:update";
|
||||
# {files} is needed so lefthook git add's them
|
||||
run = "nix run .#soonix -- update --skip-gitignore; #{files}";
|
||||
}
|
||||
];
|
||||
};
|
||||
|
|
|
|||
|
|
@ -14,7 +14,7 @@ in
|
|||
data = {
|
||||
extends = ["config:recommended"];
|
||||
postUpgradeTasks.commands = [
|
||||
"nix-portable nix run .#soonix:update"
|
||||
"nix-portable nix run .#soonix -- update --skip-gitignore"
|
||||
];
|
||||
lockFileMaintenance = {
|
||||
enabled = true;
|
||||
|
|
|
|||
|
|
@ -78,6 +78,27 @@ in {
|
|||
assert_file_contains ${shellHook} "gomplate"
|
||||
'';
|
||||
}
|
||||
{
|
||||
name = "packages";
|
||||
type = "script";
|
||||
script = let
|
||||
conf = (soonix.make {inherit hooks;}).config;
|
||||
soonixBin = conf.packages.soonix + "/bin/soonix";
|
||||
in
|
||||
# sh
|
||||
''
|
||||
${ntlib.helpers.path [pkgs.gnugrep]}
|
||||
${ntlib.helpers.scriptHelpers}
|
||||
|
||||
assert -f "${soonixBin}" "should exist"
|
||||
|
||||
assert_file_contains "${soonixBin}" "gotmpl"
|
||||
assert_file_contains "${soonixBin}" "test.json"
|
||||
|
||||
assert_file_contains "${soonixBin}" "SKIP_GITIGNORE"
|
||||
assert_file_contains "${soonixBin}" "CHECK_MODE"
|
||||
'';
|
||||
}
|
||||
];
|
||||
};
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue