diff --git a/.gitignore b/.gitignore index 17dbd95..b5b32b0 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,9 @@ # SSH private/sensitive local state home/private_dot_ssh/* !home/private_dot_ssh/config.tmpl + +# Local VCS metadata and private maintenance artifacts +.jj/ +*.bundle +chezmoi-rendered-*.tar.gz +dotfiles-clean-*.tar diff --git a/home/.chezmoi.toml.tmpl b/home/.chezmoi.toml.tmpl index 721dae2..013dd7a 100644 --- a/home/.chezmoi.toml.tmpl +++ b/home/.chezmoi.toml.tmpl @@ -27,4 +27,3 @@ useBuiltinGit = false [edit] command = "hx" {{ end }} - diff --git a/home/dot_config/nushell/config.nu b/home/dot_config/nushell/config.nu index 9621dab..6c43a2d 100644 --- a/home/dot_config/nushell/config.nu +++ b/home/dot_config/nushell/config.nu @@ -41,3 +41,6 @@ starship init nu | save -f ($nu.data-dir | path join "vendor/autoload/starship.n # tracker (issue tracker) shell completions source ~/.config/nushell/tracker-completions.nu use completions * + +# dotfile workflow helpers +use ~/.config/nushell/scripts/dotfiles.nu * diff --git a/home/dot_config/nushell/scripts/dotfiles.nu b/home/dot_config/nushell/scripts/dotfiles.nu new file mode 100644 index 0000000..3895b17 --- /dev/null +++ b/home/dot_config/nushell/scripts/dotfiles.nu @@ -0,0 +1,44 @@ +# Dotfile workflow helpers. +# +# Chezmoi remains the renderer/apply engine. The `dotfiles` script wraps jj for +# repository history. These Nushell commands provide shell-friendly aliases and +# the one operation a child process cannot do for us: changing the current dir. + +export def --env "dotfiles cd" [] { + cd (^dotfiles root) +} + +export def "dotfiles status" [] { ^dotfiles status } +export def "dotfiles log" [...args: string] { ^dotfiles log ...$args } +export def "dotfiles diff" [...targets: string] { ^dotfiles diff ...$targets } +export def "dotfiles apply" [...targets: string] { ^dotfiles apply ...$targets } +export def "dotfiles edit" [...targets: string] { ^dotfiles edit ...$targets } +export def "dotfiles watch" [...targets: string] { ^dotfiles watch ...$targets } +export def "dotfiles edit-apply" [...targets: string] { ^dotfiles edit-apply ...$targets } +export def "dotfiles fetch" [] { ^dotfiles fetch } +export def "dotfiles update" [] { ^dotfiles update } +export def "dotfiles commit" [message: string] { ^dotfiles commit $message } +export def "dotfiles push" [] { ^dotfiles push } +export def "dotfiles audit" [] { ^dotfiles audit } +export def "dotfiles archive-private" [output_dir?: path] { + if ($output_dir | is-empty) { + ^dotfiles archive-private + } else { + ^dotfiles archive-private $output_dir + } +} + +export alias cdf = dotfiles cd +export alias dfs = dotfiles status +export alias dfl = dotfiles log +export alias dfd = dotfiles diff +export alias dfa = dotfiles apply +export alias dfe = dotfiles edit +export alias dfw = dotfiles watch +export alias dfc = dotfiles commit +export alias dfp = dotfiles push +export alias dff = dotfiles fetch +export alias dfu = dotfiles update +export alias dfaudit = dotfiles audit +export alias chzew = dotfiles watch +export alias chzea = dotfiles edit-apply diff --git a/home/dot_local/bin/executable_dotfiles b/home/dot_local/bin/executable_dotfiles new file mode 100755 index 0000000..14f84bb --- /dev/null +++ b/home/dot_local/bin/executable_dotfiles @@ -0,0 +1,177 @@ +#!/usr/bin/env bash +set -euo pipefail + +REMOTE=${DOTFILES_REMOTE:-origin} +BOOKMARK=${DOTFILES_BOOKMARK:-trunk} + +usage() { + cat <<'USAGE' +Usage: dotfiles [args] + +Commands: + root Print the chezmoi repository root + status Show jj status, bookmarks, and chezmoi status + log [args...] Run jj log + diff [target...] Run chezmoi diff + apply [target...] Run chezmoi apply + edit [target...] Run chezmoi edit + watch [target...] Run chezmoi edit --watch + edit-apply [target...] Run chezmoi edit --apply + fetch Fetch the configured jj/git remote + update Fetch, apply current source state, and show status + commit Commit current jj working-copy changes and move trunk + push Push trunk; refuses uncommitted working-copy changes + audit Check for known private paths in source and rendered archive + archive-private [dir] Write a private rendered chezmoi archive to dir or $HOME +USAGE +} + +die() { + printf 'dotfiles: %s\n' "$*" >&2 + exit 1 +} + +repo_root() { + if command -v jq >/dev/null 2>&1; then + chezmoi data --format=json | jq -r '.chezmoi.workingTree' + return + fi + + local source + source=$(chezmoi source-path) + if [ -f "$(dirname "$source")/.chezmoiroot" ]; then + dirname "$source" + else + printf '%s\n' "$source" + fi +} + +in_repo() { + cd "$(repo_root)" +} + +chezmoi_archive() { + chezmoi archive "$@" 2> >(sed '/^chezmoi: warning: config file template has changed, run chezmoi init to regenerate config file$/d' >&2) +} + +working_copy_has_changes() { + [ -n "$(jj diff --summary)" ] +} + +require_clean_working_copy() { + if working_copy_has_changes; then + die 'working-copy changes are not committed; run `dotfiles status`, then `dotfiles commit "message"` first' + fi +} + +commit_bookmark_target() { + # After `jj commit`, @ is the new empty working-copy commit and @- is the + # committed dotfiles tree. Keep the Git-visible bookmark on @-. + jj bookmark set "$BOOKMARK" -r @- +} + +cmd=${1:-help} +case "$cmd" in + root) + repo_root + ;; + cd) + repo_root + ;; + status) + in_repo + printf '== jj ==\n' + jj status + printf '\n== bookmarks ==\n' + jj bookmark list --all-remotes + printf '\n== chezmoi ==\n' + chezmoi status + ;; + log) + shift + in_repo + jj log "$@" + ;; + diff) + shift + chezmoi diff "$@" + ;; + apply) + shift + chezmoi apply "$@" + ;; + edit) + shift + chezmoi edit "$@" + ;; + watch) + shift + chezmoi edit --watch "$@" + ;; + edit-apply) + shift + chezmoi edit --apply "$@" + ;; + fetch) + in_repo + jj git fetch --remote "$REMOTE" + jj status + ;; + update) + in_repo + jj git fetch --remote "$REMOTE" + chezmoi apply + jj status + ;; + commit) + shift + [ "$#" -gt 0 ] || die 'commit requires a message' + in_repo + if ! working_copy_has_changes; then + die 'no dotfile changes to commit' + fi + jj commit -m "$*" + commit_bookmark_target + jj status + ;; + push) + in_repo + require_clean_working_copy + commit_bookmark_target + jj git push --remote "$REMOTE" --bookmark "$BOOKMARK" + ;; + audit) + in_repo + source_hits=$(jj file list | grep -E '(^|/)history\.txt$|(^|/)fish_history$|(^|/)\.zsh_history$|(^|/)\.bash_history$|^home/private_dot_ssh/config$' || true) + if [ -n "$source_hits" ]; then + printf 'Tracked private/shell-history source paths:\n%s\n' "$source_hits" >&2 + die 'source audit failed' + fi + printf 'OK: jj-tracked source tree excludes known private paths and shell histories.\n' + + rendered_hits=$(chezmoi_archive | tar tzf - | grep -E '(^|/)\.ssh/config$' || true) + if [ -n "$rendered_hits" ]; then + printf 'NOTE: rendered chezmoi archive contains private target paths; keep rendered archives private:\n%s\n' "$rendered_hits" + fi + ;; + archive-private) + shift + out_dir=${1:-$HOME} + mkdir -p "$out_dir" + stamp=$(date +%Y%m%d-%H%M%S) + output="$out_dir/chezmoi-rendered-$stamp.tar.gz" + chezmoi_archive --output "$output" + printf 'Wrote private rendered chezmoi archive: %s\n' "$output" + rendered_hits=$(tar tzf "$output" | grep -E '(^|/)\.ssh/config$' || true) + if [ -n "$rendered_hits" ]; then + printf 'NOTE: archive contains private target paths; do not publish it:\n%s\n' "$rendered_hits" + fi + ;; + help|-h|--help) + usage + ;; + *) + usage >&2 + die "unknown command: $cmd" + ;; +esac