Compare commits

...
4 Commits
Author SHA1 Message Date
Lap Tran 9522a36040 fix sync-music: import as singletons to stop deleting album tracks
The script piped 'R' (remove old) into beets' duplicate-album prompt while
never actually clearing the library DB (it cleared a 0-byte dummy at the
wrong path, not beets/beets_library.db). Any two tracks sharing artist+album
caused the second import to delete the first's files from the NAS.

Import with --singletons so beets never matches against existing albums,
and answer 'S' (skip) if a prompt does appear. Drop the useless DB clear.
2026-08-09 14:28:14 -04:00
Lap Tran 4a0bff0331 dotfiles: add beets, sync-music script, firstmate, ngrok, openssh, gh/tmux/treehouse/opencode/pi brews 2026-08-08 21:20:27 -04:00
Lap Tran 6eee2509a4 Add Grok Build agent config adapted from Claude Code setup
Wire shared AGENTS.md and Claude settings through home-manager, plus a
native Grok config (always-approve, rose-pine) and a `gb` yolo alias.
2026-07-18 23:03:12 -04:00
Lap Tran bcabdec9c7 Fix ls aliases and add herdr config 2026-07-18 22:54:23 -04:00
21 changed files with 557 additions and 5 deletions
Vendored
BIN
View File
Binary file not shown.
+17
View File
@@ -0,0 +1,17 @@
# Project notes for agents
Deliberate decisions in this repo - do NOT silently revert them:
- `homebrew.onActivation.cleanup = "zap"` in `configuration.nix` is intentional. It forces the good habit of declaring every Homebrew package in the Nix config instead of installing things ad-hoc, which keeps the machine reproducible. Do not soften it to `uninstall` or `none`. Users are warned about its effect; this note is for anyone tempted to change the setting itself.
- Agent configs under `home/` are the source of truth. `home.nix` installs them with `mkOutOfStoreSymlink` + `force = true`, so edits to files in `home/` apply live after the first rebuild that creates the symlink. Prefer editing those paths, not the live `~/.claude` / `~/.grok` / `~/.pi` copies. `rebuild.sh` rotates stale `*.backup` files before switch so home-manager never fails with "would be clobbered".
- Grok Build is first-class here: `home/.grok/config.toml` and `home/AGENTS.md` (linked as `~/.grok/AGENTS.md`). Claude, Codex, opencode, and Pi share the same `home/AGENTS.md` for multi-agent work.
- Pi is managed the same way: `home/.pi/agent/{settings,models}.json`, `themes/`, and custom extensions (`terminal-status-title`, `calm`). Do not symlink the whole `~/.pi/agent` tree - sessions, auth, npm cache, and herdr's `extensions/herdr-agent-state.ts` stay live.
- firstmate lives at `~/Documents/firstmate` as a mutable git clone (not in the Nix store). `home.activation.firstmate` clones it if missing and seeds `config/backend=herdr` + `config/crew-harness=pi` only when those files are absent. Launch with shell alias `fm` (`cd` + `pi`). Approve Pi project trust once so `.pi/extensions/*.ts` load. Self-update via `/updatefirstmate`, not rebuild.
- Shell aliases: `cc` (Claude skip-permissions), `co` (Codex full-auto), `gb` (Grok --yolo), `fm` (firstmate via Pi). They are intentional high-agency shortcuts.
## Maintaining this file
Keep this file for knowledge useful to almost every future agent session in this project.
Do not repeat what the codebase already shows; point to the authoritative file or command instead.
Prefer rewriting or pruning existing entries over appending new ones.
When updating this file, preserve this bar for all agents and keep entries concise.
+10
View File
@@ -3,6 +3,10 @@
{
# Determinate already manages the Nix daemon, so nix-darwin shouldn't.
nix.enable = false;
programs.zsh.enable = true;
# SSH server (macOS 14.4+ gates `systemsetup -setremotelogin` behind Full Disk Access; nix-darwin loads ssh.plist via launchd directly instead).
services.openssh.enable = true;
nixpkgs.config.allowUnfree = true;
nixpkgs.hostPlatform = "aarch64-darwin"; # use x86_64-darwin for Intel CPU
@@ -39,10 +43,16 @@
onActivation.autoUpdate = true;
onActivation.extraFlags = [ "--force" ];
brews = [
"gh"
"herdr"
"opencode"
"pi-coding-agent"
"tmux"
"treehouse"
];
casks = [
"wezterm"
"grok-build"
];
};
}
+2 -1
View File
@@ -22,7 +22,8 @@
nix-homebrew.darwinModules.nix-homebrew
home-manager.darwinModules.home-manager
{
home-manager.useGlobalPkgs = true;
home-manager.backupFileExtension = "backup";
home-manager.useGlobalPkgs = true;
home-manager.useUserPackages = true;
home-manager.users.laptran = import ./home.nix;
}
+166 -4
View File
@@ -1,7 +1,9 @@
{ config, pkgs, ... }:
{ config, pkgs, lib, ... }:
let
dotfiles = "${config.home.homeDirectory}/.dotfiles";
home = config.home.homeDirectory;
dotfiles = "${home}/.dotfiles";
firstmateHome = "${home}/Documents/firstmate";
in
{
@@ -16,10 +18,170 @@ in
lazygit
neovim
nerd-fonts.hack
ngrok # reverse TCP tunnel so the phone can SSH in over cellular (unfree)
beets # music tagger / library organizer + navidrome sync plugin
];
fonts.fontconfig.enable = true;
home.sessionVariables.EDITOR = "nvim";
home.file.".config/wezterm".source =
config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/wezterm";
programs.zsh = {
enable = true;
autosuggestion.enable = true; # ghost text from history
syntaxHighlighting.enable = true; # commands turn green when valid
initContent = ''
bindkey '^f' autosuggest-accept
'';
shellAliases = {
".." = "cd ..";
ll = "ls -plart";
add = "git add .";
push = "git push";
pull = "git pull";
m = "git switch main";
# High-agency agent launchers (same idea across tools)
cc = "claude --dangerously-skip-permissions";
co = "codex --full-auto";
gb = "grok --yolo";
# firstmate primary session via Pi (approve project trust once per clone)
fm = "cd ${firstmateHome} && exec pi";
# Sync ~/Music into the beets library on Unraid (requires NAS mounted).
sync-music = "~/.local/bin/sync-music";
# Start the reverse tunnel so you can SSH into this Mac from your phone.
# Run once when you go remote: `ngrok-tunnel`. Reads authtoken from
# ~/.config/ngrok (set once per machine with `ngrok config add-authtoken <TOK>`)
ngrok-tunnel = "ngrok tcp 22";
};
};
programs.starship = {
enable = true;
settings = {
add_newline = false;
format = "$directory$git_branch$git_status$cmd_duration$line_break$character";
character = {
success_symbol = "[❯](purple)";
error_symbol = "[❯](red)";
};
cmd_duration.format = "[$duration]($style) ";
};
};
# Edit-in-place: real file stays in the repo; live path is an out-of-store symlink.
# force = true: replace a pre-existing regular file once; source of truth is home/.
home.file.".config/wezterm" = {
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/wezterm";
force = true;
};
home.file.".config/nvim" = {
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/nvim";
force = true;
};
home.file.".config/herdr" = {
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/herdr";
force = true;
};
# Beets music library manager - managed by nixpkgs package, config symlinked below.
home.file.".config/beets" = {
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.config/beets";
force = true;
};
# Claude Code settings (also read by Grok for permissions/compat)
home.file.".claude/settings.json" = {
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.claude/settings.json";
force = true;
};
# Shared agent policy - one file, many harnesses
home.file.".claude/CLAUDE.md" = {
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md";
force = true;
};
home.file.".codex/AGENTS.md" = {
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md";
force = true;
};
home.file.".config/opencode/AGENTS.md" = {
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md";
force = true;
};
home.file.".grok/AGENTS.md" = {
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md";
force = true;
};
# Grok Build native config
home.file.".grok/config.toml" = {
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.grok/config.toml";
force = true;
};
# Pi agent - source of truth under home/.pi (sessions/auth/npm stay live under ~/.pi)
home.file.".pi/agent/settings.json" = {
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.pi/agent/settings.json";
force = true;
};
home.file.".pi/agent/models.json" = {
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.pi/agent/models.json";
force = true;
};
home.file.".pi/agent/themes" = {
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.pi/agent/themes";
force = true;
};
home.file.".pi/agent/extensions/terminal-status-title.js" = {
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.pi/agent/extensions/terminal-status-title.js";
force = true;
};
home.file.".pi/agent/extensions/calm" = {
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/.pi/agent/extensions/calm";
force = true;
};
home.file.".pi/agent/AGENTS.md" = {
source = config.lib.file.mkOutOfStoreSymlink "${dotfiles}/home/AGENTS.md";
force = true;
};
# firstmate is a mutable agent distro (self-update, state/, projects/). Clone once;
# never put it in the Nix store. Seed Pi+herdr defaults only when absent.
# Do NOT npm install -g here: activation PATH often resolves Nix's npm, which
# cannot write into the store (EACCES). Use Homebrew's node for globals:
# /opt/homebrew/bin/npm install -g tasks-axi quota-axi no-mistakes gh-axi lavish-axi chrome-devtools-axi
home.activation.firstmate = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
set -euo pipefail
fm="${firstmateHome}"
git="${pkgs.git}/bin/git"
if [ ! -d "$fm/.git" ]; then
mkdir -p "$(dirname "$fm")"
$git clone https://github.com/kunchenguid/firstmate.git "$fm"
fi
mkdir -p "$fm/config" "$fm/data" "$fm/state" "$fm/projects"
# Local gitignored operating choices (do not overwrite captain edits)
[ -f "$fm/config/backend" ] || printf 'herdr\n' > "$fm/config/backend"
[ -f "$fm/config/crew-harness" ] || printf 'pi\n' > "$fm/config/crew-harness"
'';
# Authorize the SSH key so the phone can log in through the ngrok tunnel.
# (home-manager 26.05 removed programs.ssh.authorizedKeys; manage the file
# here so ~/.ssh is 0700 and authorized_keys is 0600. Public key is not a secret.)
home.activation.authorizeSSHKey = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
set -euo pipefail
mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"
printf '%s\n' "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGCZEGVYMDztSryFwoZ6cfpBH3ksP3h0yxZSanlcbrZ0 unraid-omada" > "$HOME/.ssh/authorized_keys"
chmod 600 "$HOME/.ssh/authorized_keys"
'';
# Wire the sync-music script into ~/.local/bin without touching anything else
# that lives there (node, python3.11, hermes, etc.). Source of truth is the
# dotfiles repo so a rebuild restores it if it ever disappears.
home.activation.syncMusic = lib.hm.dag.entryAfter [ "writeBoundary" ] ''
set -euo pipefail
ln -sfn "${dotfiles}/home/bin/sync-music" "$HOME/.local/bin/sync-music"
'';
}
+7
View File
@@ -0,0 +1,7 @@
{
"theme": "dark-ansi",
"statusLine": {
"type": "command",
"command": "input=$(cat); model=$(echo \"$input\" | jq -r '.model.display_name'); used=$(echo \"$input\" | jq -r '.context_window.used_percentage // empty'); if [ -n \"$used\" ]; then printf \"%s | ctx: %.0f%% used\" \"$model\" \"$used\"; else printf \"%s\" \"$model\"; fi"
}
}
+15
View File
@@ -0,0 +1,15 @@
directory: /Volumes/data/media/music
library: /Volumes/data/media/music/beets/beets_library.db
import:
move: yes
copy: no
resume: ask
timid: no
quiet: no
paths:
default: $artist/$album/$track $title
singleton: Non-Album/$artist/$title
comp: Compilations/$album/$track $title
albumtype_soundtrack: Soundtracks/$album/$track $title
+18
View File
@@ -0,0 +1,18 @@
onboarding = false
[keys]
prefix = "ctrl+b"
focus_pane_left = "prefix+h"
focus_pane_down = "prefix+j"
focus_pane_up = "prefix+k"
focus_pane_right = "prefix+l"
split_horizontal = "prefix+double_quote"
split_vertical = "prefix+percent"
new_tab = "prefix+c"
close_tab = "prefix+ampersand"
workspace_picker = "prefix+w"
goto = "prefix+g"
copy_mode = "prefix+y" # herdr's copy-mode entry key; copy-mode's own internal keys (v/space select, y/Enter copy, q/Esc cancel) aren't configurable
[ui]
# Agents panel order: explicit choice of "spaces" (grouped by space, the default) over "priority".
agent_panel_sort = "spaces"
+3
View File
@@ -0,0 +1,3 @@
require('vim_config')
require('plugin')
require('keys')
+10
View File
@@ -0,0 +1,10 @@
{
"diffview.nvim": { "branch": "main", "commit": "4516612fe98ff56ae0415a259ff6361a89419b0a" },
"gitsigns.nvim": { "branch": "main", "commit": "31d6fb2d618bca1482b9f274751ead5f03461408" },
"lazy.nvim": { "branch": "main", "commit": "306a05526ada86a7b30af95c5cc81ffba93fef97" },
"neogit": { "branch": "master", "commit": "f8674ec894315c02449b61c9de9a116c5aafeb90" },
"oil.nvim": { "branch": "master", "commit": "b73018b75affd13fa38e2fc94ef753b465f770d7" },
"plenary.nvim": { "branch": "master", "commit": "74b06c6c75e4eeb3108ec01852001636d85a932b" },
"snacks.nvim": { "branch": "main", "commit": "882c996cf28183f4d63640de0b4c02ec886d01f2" },
"which-key.nvim": { "branch": "main", "commit": "3aab2147e74890957785941f0c1ad87d0a44c15a" }
}
+7
View File
@@ -0,0 +1,7 @@
-- save by pressing Escape
vim.keymap.set('n', '<Esc>', ':w<CR>', { desc = 'Save' })
-- select all
vim.keymap.set('n', '<C-a>', 'ggVG', { desc = 'Select All' })
-- pasting over a selection no longer clobbers your clipboard
vim.cmd([[ xnoremap <expr> p 'pgv"'.v:register.'y' ]])
+8
View File
@@ -0,0 +1,8 @@
local lazypath = vim.fn.stdpath('data') .. '/lazy/lazy.nvim'
if not vim.uv.fs_stat(lazypath) then
vim.fn.system({ 'git', 'clone', '--filter=blob:none',
'https://github.com/folke/lazy.nvim.git', '--branch=stable', lazypath })
end
vim.opt.rtp:prepend(lazypath)
require('lazy').setup('plugins') -- load every file in lua/plugins/
+13
View File
@@ -0,0 +1,13 @@
return {
{
'NeogitOrg/neogit',
dependencies = { 'nvim-lua/plenary.nvim', 'sindrets/diffview.nvim' },
keys = { { '<leader>g', function() require('neogit').open() end, desc = 'Neogit' } },
},
{
'lewis6991/gitsigns.nvim',
event = 'BufWinEnter',
opts = { current_line_blame = true }, -- who last touched this line
},
}
@@ -0,0 +1,24 @@
return {
{
'stevearc/oil.nvim',
opts = { view_options = { show_hidden = true } },
keys = { { '<leader>e', '<cmd>Oil<cr>', desc = 'File Browser' } },
},
{
'folke/snacks.nvim',
priority = 1000,
lazy = false,
opts = {
picker = { enabled = true },
notifier = { enabled = true },
input = { enabled = true },
},
keys = {
{ '<leader>f', function() Snacks.picker.files() end, desc = 'Find Files' },
{ '<leader>s', function() Snacks.picker.grep() end, desc = 'Search Text' },
{ '<leader>b', function() Snacks.picker.buffers() end, desc = 'Buffers' },
{ 'gd', function() Snacks.picker.lsp_definitions() end, desc = 'Goto Definition' },
},
},
}
+8
View File
@@ -0,0 +1,8 @@
return {
{
'folke/which-key.nvim',
lazy = false,
config = true, -- popup that shows what my leader keys do
},
}
+13
View File
@@ -0,0 +1,13 @@
local o = vim.opt
vim.g.mapleader = ' ' -- space is the leader key
o.expandtab = true -- spaces, not tabs
o.shiftwidth = 2 -- 2 spaces per indent level
o.number = true -- absolute number on the cursor line, relative elsewhere
o.relativenumber = true -- relative line numbers for fast jumps
o.ignorecase = true -- search is case-insensitive by default
o.smartcase = true -- case-sensitive only if i type a capital
o.clipboard = 'unnamedplus' -- share the system clipboard
o.scrolloff = 16 -- keep cursor away from the screen edge
o.undofile = true -- persistent undo across sessions
o.mouse = '' -- no mouse in nvim; also lets Herdr keep host mouse capture off so Escape isn't swallowed
+13
View File
@@ -0,0 +1,13 @@
local wezterm = require("wezterm")
local config = wezterm.config_builder()
config.color_scheme = "rose-pine-moon"
config.font = wezterm.font("Hack Nerd Font")
config.font_size = 15.0
config.window_background_opacity = 0.8
config.macos_window_background_blur = 50
config.hide_tab_bar_if_only_one_tab = true
config.window_decorations = "RESIZE"
return config
+45
View File
@@ -0,0 +1,45 @@
# Grok Build user config - managed via home-manager (edit here, then rebuild if needed).
# Source of truth: home/.grok/config.toml in dotfiles.
[marketplace]
official_marketplace_auto_installed = true
[[marketplace.sources]]
name = "xAI Official"
git = "https://github.com/xai-org/plugin-marketplace.git"
[models]
default = "ornith"
[model.ornith]
model = "/models/ornith-1.0-35b-Q6_K.gguf"
base_url = "http://10.37.0.165:30081/v1"
name = "Ornith 1.0 35B (Talos)"
description = "Local Ornith-1.0-35B Q6_K via llama.cpp on Talos worker 1 (RTX 4070 Super)"
api_backend = "chat_completions"
temperature = 0.7
top_p = 0.95
context_window = 132768
max_completion_tokens = 16384
[model.genesis]
model = "/models/Hermes3.6-35B-A3B-Uncensored-Genesis-V6-APEX.gguf"
base_url = "http://10.37.0.219:30082/v1"
name = "Genesis Hermes V6 (Talos W2)"
description = "Local Qwen3.6-35B-A3B Genesis Hermes V6 APEX + vision via llama.cpp on Talos worker 2 (RTX 3080)"
api_backend = "chat_completions"
temperature = 0.7
top_p = 0.95
context_window = 131072
max_completion_tokens = 16384
[ui]
theme = "rosepine"
max_thoughts_width = 120
fork_secondary_model = "grok-build"
permission_mode = "always-approve"
yolo = false
compact_mode = false
[cli]
installer = "internal"
+14
View File
@@ -0,0 +1,14 @@
# global agent instructions
- Never use the em dash "—". Use plain dash "-" instead
- When writing commit messages, NEVER auto-add your agent name as co-author
- Never manually modify CHANGELOG.md files or any files that are marked as auto-generated
- When making technical decisions, do not give much weight to development cost.
Instead, prefer quality, simplicity, robustness, scalability, and long term maintainability.
- When doing bug fixes, always start with reproducing the bug in an E2E setting as closely aligned with how an end user would experience it as possible.
This makes sure you find the real problem so your fix will actually solve it.
- When end-to-end testing a product, be picky about the UI you see and be obsessed with pixel perfection.
If something clearly looks off, even if it is not directly related to what you are doing, try to get it fixed along the way.
- Apply that same high standard to engineering excellence: lint, test failures, and test flakiness.
If you see one, even if it is not caused by what you are working on right now, still get it fixed.
- Before using "dynamic workflows", "ultra code" or any harness feature that immediately spawns a large swarm of subagents, always explain the tradeoffs and ask the user for explicit approval.
+125
View File
@@ -0,0 +1,125 @@
#!/bin/bash
# sync-music — manually sync ~/Music into the beets library on the Unraid NAS.
#
# sync-music # shell alias -> ~/.local/bin/sync-music
#
# How it works:
# 1. Ensures the NAS SMB share is mounted at /Volumes/data (uses your keychain
# credentials; falls back to a clear error if it can't mount).
# 2. Imports every audio file in ~/Music into beets via
# `beet import --noautotag --move --quiet`, which moves each file into the
# library and removes it from ~/Music. Files already in the library are
# skipped by beets (left in place). Safe to re-run any time.
#
# Why a manual script (not the old daemon)? The daemon kept dying because
# launchd/WezTerm spawn a minimal PATH that lacks /opt/homebrew/bin (where
# `beet` lives). Running this script yourself in your own shell avoids that.
#
# Configure your server below.
NAS_HOST="unraid.local"
NAS_SHARE="data"
NAS_USER="backup"
MOUNT_POINT="/Volumes/data"
LIBRARY_DIR="$MOUNT_POINT/media/music"
WATCH_DIR="$HOME/Music"
LOG_FILE="$HOME/Library/Logs/beet-watch.log"
AUDIO_EXT="flac|mp3|m4a|aac|wav|alac|ogg|wma|opus|aiff|aif"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"; }
# --- locate beet — works whether it comes from nix profile or Homebrew ---
BEET="$(command -v beet 2>/dev/null || true)"
if [ ! -x "$BEET" ]; then
echo "ERROR: beet not found." >&2
exit 1
fi
# --- 1. ensure the NAS is mounted ---
if [ ! -d "$MOUNT_POINT" ]; then
log "NAS not mounted; mounting smb://$NAS_USER@$NAS_HOST/$NAS_SHARE ..."
open "smb://$NAS_USER@$NAS_HOST/$NAS_SHARE" 2>/dev/null || true
for _ in $(seq 1 30); do
[ -d "$MOUNT_POINT" ] && break
sleep 1
done
fi
if [ ! -d "$MOUNT_POINT" ]; then
echo "ERROR: $MOUNT_POINT is not mounted." >&2
echo " Mount it, then re-run:" >&2
echo " open smb://$NAS_USER@$NAS_HOST/$NAS_SHARE" >&2
exit 1
fi
# --- 2. import every (finished) audio file in ~/Music ---
# NOTE: We do NOT clear the beets DB. Files are imported as singletons
# (--singletons) so beets never matches them against existing albums in the
# library, which is what previously made it delete the prior tracks of an
# album via the 'R' (remove old) prompt. The DB lives at
# $LIBRARY_DIR/beets/beets_library.db (per ~/.config/beets/config.yaml).
log "sync-music: scanning $WATCH_DIR -> $LIBRARY_DIR"
shopt -s nullglob nocaseglob
files=()
for f in "$WATCH_DIR"/*; do
[ -f "$f" ] || continue
[[ "${f##*.}" =~ ^($AUDIO_EXT)$ ]] && files+=("$f")
done
shopt -u nocaseglob nullglob
total=${#files[@]}
log "Found $total audio file(s)."
ok=0; skipped=0; failed=0
for f in "${files[@]}"; do
# skip files still being written (size must be stable for 1s)
s1=$(stat -f%z "$f" 2>/dev/null || echo 0)
sleep 1
s2=$(stat -f%z "$f" 2>/dev/null || echo 0)
if [ "$s1" != "$s2" ]; then
log " still writing, skipping: $(basename "$f")"
skipped=$((skipped + 1))
continue
fi
bn=$(basename "$f")
# Pre-check: has beets already imported this file? Match by path or title.
existing="$(beet ls --path "$f" 2>/dev/null || true)"
stripped="$(echo "$bn" | sed 's/\.flac$//; s/\.mp3$//')"
if [ -n "$existing" ] || [ -n "$(beet ls "$stripped" 2>/dev/null || true)" ]; then
log " already in library (skipped): $bn"
ok=$((ok + 1))
continue
fi
log " importing: $bn"
# Import as a singleton so beets never offers to "remove old" and delete
# other tracks from the same album. Reads confirm from stdin; with no
# matches there is no prompt, and if one does appear 'S' (=skip new) is the
# safe default that never deletes anything.
out=$(printf 'S\n' | "$BEET" import --noautotag --move --singletons "$f" 2>&1)
rc=$?
if [ $rc -eq 0 ] && [ ! -f "$f" ]; then
log " moved into library: $bn"
ok=$((ok + 1))
elif [ $rc -eq 0 ]; then
# Import succeeded but source still exists — beets kept new file (didn't remove old)
if echo "$out" | grep -qi 'keep'; then
log " kept existing, source remains: $bn"
ok=$((ok + 1))
else
log " IMPORTED but source still present (rc=0): $bn :: $(printf '%s' "$out" | head -c 200)"
failed=$((failed + 1))
fi
else
log " FAILED (rc=$rc): $bn :: $(printf '%s' "$out" | head -c 200)"
failed=$((failed + 1))
fi
done
log "sync-music complete: $total considered, ok=$ok, still-writing-skipped=$skipped, failed=$failed"
[ "$failed" -gt 0 ] && log " some imports failed; fix and re-run."
exit 0
+39
View File
@@ -1,5 +1,44 @@
#!/usr/bin/env bash
# Rebuild nix-darwin + home-manager. Rotates stale home-manager *.backup files
# so a second switch never fails with "would be clobbered".
set -euo pipefail
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd -P)"
ln -sfn "$DIR" ~/.dotfiles
stamp="$(date +%Y%m%d-%H%M%S)"
# Paths home-manager manages via home.file (regular files become out-of-store symlinks).
managed=(
"$HOME/.grok/config.toml"
"$HOME/.grok/AGENTS.md"
"$HOME/.claude/settings.json"
"$HOME/.claude/CLAUDE.md"
"$HOME/.codex/AGENTS.md"
"$HOME/.config/opencode/AGENTS.md"
"$HOME/.pi/agent/settings.json"
"$HOME/.pi/agent/models.json"
"$HOME/.pi/agent/AGENTS.md"
"$HOME/.pi/agent/extensions/terminal-status-title.js"
)
for path in "${managed[@]}"; do
bak="${path}.backup"
if [ -e "$bak" ] || [ -L "$bak" ]; then
mv "$bak" "${bak}.${stamp}"
echo "rotated stale backup: ${bak} -> ${bak}.${stamp}"
fi
done
# Directory targets use the same backup suffix on collision.
for path in \
"$HOME/.config/wezterm" \
"$HOME/.config/nvim" \
"$HOME/.config/herdr" \
"$HOME/.pi/agent/themes" \
"$HOME/.pi/agent/extensions/calm"
do
bak="${path}.backup"
if [ -e "$bak" ] || [ -L "$bak" ]; then
mv "$bak" "${bak}.${stamp}"
echo "rotated stale backup: ${bak} -> ${bak}.${stamp}"
fi
done
exec sudo darwin-rebuild switch --flake ~/.dotfiles#lt-mbp