Compare commits
4
Commits
ee7ed15ce7
...
9522a36040
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9522a36040 | ||
|
|
4a0bff0331 | ||
|
|
6eee2509a4 | ||
|
|
bcabdec9c7 |
@@ -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.
|
||||
@@ -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"
|
||||
];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
'';
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -0,0 +1,3 @@
|
||||
require('vim_config')
|
||||
require('plugin')
|
||||
require('keys')
|
||||
@@ -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" }
|
||||
}
|
||||
@@ -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' ]])
|
||||
|
||||
@@ -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/
|
||||
|
||||
@@ -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' },
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
return {
|
||||
{
|
||||
'folke/which-key.nvim',
|
||||
lazy = false,
|
||||
config = true, -- popup that shows what my leader keys do
|
||||
},
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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.
|
||||
Executable
+125
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user