From 2412b7c92cabbbd515f0c719187c01428084249f Mon Sep 17 00:00:00 2001 From: keito4 Date: Sun, 21 Jun 2026 19:04:19 +0900 Subject: [PATCH 1/5] feat: manage cmux and karabiner with nix --- ...manage-cmux-karabiner-with-home-manager.md | 45 ++++++++++++ nix/home/cmux.nix | 8 +++ nix/home/default.nix | 2 + nix/home/karabiner.nix | 70 +++++++++++++++++++ nix/modules/homebrew.nix | 1 + test/nix-darwin-config.test.js | 48 +++++++++++++ 6 files changed, 174 insertions(+) create mode 100644 docs/adr/0014-manage-cmux-karabiner-with-home-manager.md create mode 100644 nix/home/cmux.nix create mode 100644 nix/home/karabiner.nix create mode 100644 test/nix-darwin-config.test.js diff --git a/docs/adr/0014-manage-cmux-karabiner-with-home-manager.md b/docs/adr/0014-manage-cmux-karabiner-with-home-manager.md new file mode 100644 index 00000000..f0485759 --- /dev/null +++ b/docs/adr/0014-manage-cmux-karabiner-with-home-manager.md @@ -0,0 +1,45 @@ +# ADR 0014: Manage cmux and Karabiner Configuration with Home Manager + +## Status + +Accepted + +## Context + +cmux and Karabiner are part of the local macOS development environment. They +affect terminal input behavior, especially Google Japanese Input shortcuts in +cmux and the Caps Lock to Control mapping. + +These settings were previously applied directly under `~/.config/`, which made +them easy to lose during a machine rebuild and hard to review. ADR 0012 already +defines `nix/` as the source of truth for macOS packages and local environment +configuration. + +## Decision + +Manage cmux and Karabiner user configuration through home-manager modules under +`nix/home/`. + +- Install cmux through the nix-darwin Homebrew cask list. +- Keep Karabiner Elements and Google Japanese Input in the Homebrew cask list. +- Generate `~/.config/cmux/config` from home-manager and keep terminal keybind + overrides empty so input-method shortcuts are not consumed by cmux. +- Generate `~/.config/karabiner/karabiner.json` from home-manager. +- Keep the Karabiner virtual keyboard type as `jis` so Japanese-specific virtual + keys work. +- Explicitly map Caps Lock to left Control. +- Scope the cmux IME shortcut workaround to `com.cmuxterm.app` only: + - `Ctrl+Shift+J` sends the virtual Kana key. + - `Ctrl+Shift+;` and `Ctrl+Shift+'` send the virtual Eisuu key. + +## Consequences + +`darwin-rebuild switch --flake ~/develop/github.com/keito4/config/nix` becomes +the source of truth for these local input settings. + +Manual changes to `~/.config/cmux/config` and +`~/.config/karabiner/karabiner.json` will be overwritten by home-manager. This +is intentional so shortcut behavior stays reproducible across machines. + +The Karabiner workaround is app-scoped to avoid changing the same shortcuts in +other applications. diff --git a/nix/home/cmux.nix b/nix/home/cmux.nix new file mode 100644 index 00000000..61fb7be8 --- /dev/null +++ b/nix/home/cmux.nix @@ -0,0 +1,8 @@ +{ ... }: + +{ + home.file.".config/cmux/config" = { + force = true; + text = ""; + }; +} diff --git a/nix/home/default.nix b/nix/home/default.nix index caed47b9..d72d897c 100644 --- a/nix/home/default.nix +++ b/nix/home/default.nix @@ -5,6 +5,8 @@ ./packages.nix ./git.nix ./zsh.nix + ./cmux.nix + ./karabiner.nix ]; home = { diff --git a/nix/home/karabiner.nix b/nix/home/karabiner.nix new file mode 100644 index 00000000..7d2fa956 --- /dev/null +++ b/nix/home/karabiner.nix @@ -0,0 +1,70 @@ +{ ... }: + +let + cmuxBundleCondition = { + type = "frontmost_application_if"; + bundle_identifiers = [ "^com\\.cmuxterm\\.app$" ]; + }; + + cmuxImeShortcut = fromKey: toKey: { + type = "basic"; + from = { + key_code = fromKey; + modifiers = { + mandatory = [ + "control" + "shift" + ]; + optional = [ "any" ]; + }; + }; + to = [ { key_code = toKey; } ]; + conditions = [ cmuxBundleCondition ]; + }; + + karabinerConfig = { + profiles = [ + { + name = "Default profile"; + selected = true; + + virtual_hid_keyboard = { + keyboard_type_v2 = "jis"; + }; + + simple_modifications = [ + { + from = { + key_code = "caps_lock"; + }; + to = [ { key_code = "left_control"; } ]; + } + ]; + + complex_modifications = { + parameters = { + "basic.to_if_alone_timeout_milliseconds" = 500; + "mouse_motion_to_scroll.speed" = 300; + }; + + rules = [ + { + description = "cmux: keep Google Japanese IME shortcuts out of terminal"; + manipulators = [ + (cmuxImeShortcut "j" "japanese_kana") + (cmuxImeShortcut "semicolon" "japanese_eisuu") + (cmuxImeShortcut "quote" "japanese_eisuu") + ]; + } + ]; + }; + } + ]; + }; +in +{ + home.file.".config/karabiner/karabiner.json" = { + force = true; + text = builtins.toJSON karabinerConfig + "\n"; + }; +} diff --git a/nix/modules/homebrew.nix b/nix/modules/homebrew.nix index 0ee1448e..5ee40a52 100644 --- a/nix/modules/homebrew.nix +++ b/nix/modules/homebrew.nix @@ -69,6 +69,7 @@ # AI Tools "chatgpt" "claude" + "cmux" # Browsers "arc" diff --git a/test/nix-darwin-config.test.js b/test/nix-darwin-config.test.js new file mode 100644 index 00000000..0b5dabf7 --- /dev/null +++ b/test/nix-darwin-config.test.js @@ -0,0 +1,48 @@ +const fs = require('fs'); +const path = require('path'); + +const repoPath = path.resolve(__dirname, '..'); + +function readRepoFile(relativePath) { + return fs.readFileSync(path.join(repoPath, relativePath), 'utf8'); +} + +describe('nix-darwin and home-manager macOS configuration', () => { + test('home-manager imports cmux and Karabiner modules', () => { + const homeDefault = readRepoFile('nix/home/default.nix'); + + expect(homeDefault).toContain('./cmux.nix'); + expect(homeDefault).toContain('./karabiner.nix'); + }); + + test('Homebrew casks install cmux and input tooling', () => { + const homebrewModule = readRepoFile('nix/modules/homebrew.nix'); + + expect(homebrewModule).toContain('"cmux"'); + expect(homebrewModule).toContain('"karabiner-elements"'); + expect(homebrewModule).toContain('"google-japanese-ime"'); + }); + + test('cmux terminal config leaves IME shortcuts unbound', () => { + const cmuxModule = readRepoFile('nix/home/cmux.nix'); + + expect(cmuxModule).toContain('home.file.".config/cmux/config"'); + expect(cmuxModule).toContain('force = true;'); + expect(cmuxModule).toContain('text = "";'); + expect(cmuxModule).not.toContain('C-j'); + expect(cmuxModule).not.toContain('C-Semicolon'); + }); + + test('Karabiner maps Caps Lock and scopes cmux IME shortcuts', () => { + const karabinerModule = readRepoFile('nix/home/karabiner.nix'); + + expect(karabinerModule).toContain('home.file.".config/karabiner/karabiner.json"'); + expect(karabinerModule).toContain('keyboard_type_v2 = "jis"'); + expect(karabinerModule).toContain('key_code = "caps_lock"'); + expect(karabinerModule).toContain('key_code = "left_control"'); + expect(karabinerModule).toContain('^com\\\\.cmuxterm\\\\.app$'); + expect(karabinerModule).toContain('cmuxImeShortcut "j" "japanese_kana"'); + expect(karabinerModule).toContain('cmuxImeShortcut "semicolon" "japanese_eisuu"'); + expect(karabinerModule).toContain('cmuxImeShortcut "quote" "japanese_eisuu"'); + }); +}); From 6a3133f9e93b0e4bb56951e6e125fcc29bcbb764 Mon Sep 17 00:00:00 2001 From: keito4 Date: Sun, 21 Jun 2026 19:28:46 +0900 Subject: [PATCH 2/5] feat: manage cmux app settings with nix --- ...manage-cmux-karabiner-with-home-manager.md | 16 ++-- nix/home/cmux.nix | 89 ++++++++++++++++++- test/nix-darwin-config.test.js | 29 ++++++ 3 files changed, 128 insertions(+), 6 deletions(-) diff --git a/docs/adr/0014-manage-cmux-karabiner-with-home-manager.md b/docs/adr/0014-manage-cmux-karabiner-with-home-manager.md index f0485759..ad6f4a13 100644 --- a/docs/adr/0014-manage-cmux-karabiner-with-home-manager.md +++ b/docs/adr/0014-manage-cmux-karabiner-with-home-manager.md @@ -22,8 +22,13 @@ Manage cmux and Karabiner user configuration through home-manager modules under - Install cmux through the nix-darwin Homebrew cask list. - Keep Karabiner Elements and Google Japanese Input in the Homebrew cask list. -- Generate `~/.config/cmux/config` from home-manager and keep terminal keybind - overrides empty so input-method shortcuts are not consumed by cmux. +- Generate `~/.config/cmux/config` from home-manager and keep legacy terminal + keybind overrides empty so input-method shortcuts are not consumed by cmux. +- Generate `~/.config/cmux/cmux.json` from home-manager for app, automation, + notification, browser, sidebar, and terminal behavior that should be stable + across rebuilds. +- Generate `~/.config/ghostty/config` from home-manager for the terminal + rendering settings that cmux reads through Ghostty. - Generate `~/.config/karabiner/karabiner.json` from home-manager. - Keep the Karabiner virtual keyboard type as `jis` so Japanese-specific virtual keys work. @@ -37,9 +42,10 @@ Manage cmux and Karabiner user configuration through home-manager modules under `darwin-rebuild switch --flake ~/develop/github.com/keito4/config/nix` becomes the source of truth for these local input settings. -Manual changes to `~/.config/cmux/config` and -`~/.config/karabiner/karabiner.json` will be overwritten by home-manager. This -is intentional so shortcut behavior stays reproducible across machines. +Manual changes to `~/.config/cmux/config`, `~/.config/cmux/cmux.json`, +`~/.config/ghostty/config`, and `~/.config/karabiner/karabiner.json` will be +overwritten by home-manager. This is intentional so shortcut behavior and cmux +runtime behavior stay reproducible across machines. The Karabiner workaround is app-scoped to avoid changing the same shortcuts in other applications. diff --git a/nix/home/cmux.nix b/nix/home/cmux.nix index 61fb7be8..5c9d9aba 100644 --- a/nix/home/cmux.nix +++ b/nix/home/cmux.nix @@ -1,8 +1,95 @@ -{ ... }: +{ pkgs, ... }: +let + cmuxSchema = "https://raw.githubusercontent.com/manaflow-ai/cmux/main/web/data/cmux.schema.json"; + + localBrowserHosts = [ + "localhost" + "127.0.0.1" + "::1" + "0.0.0.0" + "*.localhost" + "*.localtest.me" + ]; + + cmuxConfig = { + "$schema" = cmuxSchema; + schemaVersion = 1; + + app = { + confirmQuit = "dirty-only"; + newWorkspacePlacement = "afterCurrent"; + openMarkdownInCmuxViewer = true; + openSupportedFilesInCmux = true; + reorderOnNotification = true; + workspaceInheritWorkingDirectory = true; + }; + + automation = { + claudeCodeIntegration = true; + ripgrepBinaryPath = "${pkgs.ripgrep}/bin/rg"; + socketControlMode = "cmuxOnly"; + suppressSubagentNotifications = true; + }; + + browser = { + defaultSearchEngine = "google"; + hostsToOpenInEmbeddedBrowser = localBrowserHosts; + insecureHttpHostsAllowedInEmbeddedBrowser = localBrowserHosts; + openTerminalLinksInCmuxBrowser = true; + }; + + notifications = { + dockBadge = true; + paneFlash = true; + showInMenuBar = true; + unreadPaneRing = true; + }; + + sidebar = { + openPortLinksInCmuxBrowser = true; + openPullRequestLinksInCmuxBrowser = true; + showBranchDirectory = true; + showCustomMetadata = true; + showLog = true; + showNotificationMessage = true; + showPorts = true; + showProgress = true; + showPullRequests = true; + showSSH = true; + }; + + terminal = { + autoResumeAgentSessions = true; + copyOnSelect = true; + focusTextBoxOnNewTerminals = false; + showScrollBar = false; + showTextBoxOnNewTerminals = false; + }; + }; + + ghosttyConfig = '' + font-family = SF Mono + font-size = 13 + sidebar-font-size = 14 + surface-tab-bar-font-size = 11 + scrollback-limit = 50000000 + split-divider-color = #3e4451 + ''; +in { home.file.".config/cmux/config" = { force = true; text = ""; }; + + home.file.".config/cmux/cmux.json" = { + force = true; + text = builtins.toJSON cmuxConfig + "\n"; + }; + + home.file.".config/ghostty/config" = { + force = true; + text = ghosttyConfig; + }; } diff --git a/test/nix-darwin-config.test.js b/test/nix-darwin-config.test.js index 0b5dabf7..c31d63c6 100644 --- a/test/nix-darwin-config.test.js +++ b/test/nix-darwin-config.test.js @@ -33,6 +33,35 @@ describe('nix-darwin and home-manager macOS configuration', () => { expect(cmuxModule).not.toContain('C-Semicolon'); }); + test('cmux app config manages agent-friendly defaults', () => { + const cmuxModule = readRepoFile('nix/home/cmux.nix'); + + expect(cmuxModule).toContain('home.file.".config/cmux/cmux.json"'); + expect(cmuxModule).toContain('"$schema" = cmuxSchema;'); + expect(cmuxModule).toContain('confirmQuit = "dirty-only";'); + expect(cmuxModule).toContain('workspaceInheritWorkingDirectory = true;'); + expect(cmuxModule).toContain('socketControlMode = "cmuxOnly";'); + expect(cmuxModule).toContain('suppressSubagentNotifications = true;'); + expect(cmuxModule).toContain('ripgrepBinaryPath = "${pkgs.ripgrep}/bin/rg";'); + expect(cmuxModule).toContain('hostsToOpenInEmbeddedBrowser = localBrowserHosts;'); + expect(cmuxModule).toContain('openTerminalLinksInCmuxBrowser = true;'); + expect(cmuxModule).toContain('showPullRequests = true;'); + expect(cmuxModule).toContain('autoResumeAgentSessions = true;'); + expect(cmuxModule).toContain('copyOnSelect = true;'); + }); + + test('Ghostty config is managed for cmux terminal rendering', () => { + const cmuxModule = readRepoFile('nix/home/cmux.nix'); + + expect(cmuxModule).toContain('home.file.".config/ghostty/config"'); + expect(cmuxModule).toContain('font-family = SF Mono'); + expect(cmuxModule).toContain('font-size = 13'); + expect(cmuxModule).toContain('sidebar-font-size = 14'); + expect(cmuxModule).toContain('surface-tab-bar-font-size = 11'); + expect(cmuxModule).toContain('scrollback-limit = 50000000'); + expect(cmuxModule).toContain('split-divider-color = #3e4451'); + }); + test('Karabiner maps Caps Lock and scopes cmux IME shortcuts', () => { const karabinerModule = readRepoFile('nix/home/karabiner.nix'); From d083ca1fe29b068d278a3baaecb76367cb1c2b80 Mon Sep 17 00:00:00 2001 From: keito4 Date: Sun, 21 Jun 2026 22:37:33 +0900 Subject: [PATCH 3/5] feat: manage portable user dotfiles --- .zsh/configs/virtual/go.zsh | 1 + .zsh/configs/virtual/php.zsh | 2 + .zsh/configs/virtual/python.zsh | 1 + ...ortable-user-dotfiles-with-home-manager.md | 57 +++++ dot/aerospace.toml | 206 ++++++++++++++++++ dot/config/act/actrc | 4 + dot/config/agent-deck/config.toml | 20 ++ dot/config/codespaces-secrets/repos.txt | 2 + dot/config/graphite/aliases | 11 + nix/home/default.nix | 1 + nix/home/dotfiles.nix | 52 +++++ test/nix-darwin-config.test.js | 40 ++++ 12 files changed, 397 insertions(+) create mode 100644 .zsh/configs/virtual/go.zsh create mode 100644 .zsh/configs/virtual/php.zsh create mode 100644 .zsh/configs/virtual/python.zsh create mode 100644 docs/adr/0015-manage-portable-user-dotfiles-with-home-manager.md create mode 100644 dot/aerospace.toml create mode 100644 dot/config/act/actrc create mode 100644 dot/config/agent-deck/config.toml create mode 100644 dot/config/codespaces-secrets/repos.txt create mode 100644 dot/config/graphite/aliases create mode 100644 nix/home/dotfiles.nix diff --git a/.zsh/configs/virtual/go.zsh b/.zsh/configs/virtual/go.zsh new file mode 100644 index 00000000..f56e3a0e --- /dev/null +++ b/.zsh/configs/virtual/go.zsh @@ -0,0 +1 @@ +# Go configuration has been moved to dev container environments diff --git a/.zsh/configs/virtual/php.zsh b/.zsh/configs/virtual/php.zsh new file mode 100644 index 00000000..3879a341 --- /dev/null +++ b/.zsh/configs/virtual/php.zsh @@ -0,0 +1,2 @@ +export LDFLAGS="-L/opt/homebrew/opt/php@7.4/lib" +export CPPFLAGS="-I/opt/homebrew/opt/php@7.4/include" diff --git a/.zsh/configs/virtual/python.zsh b/.zsh/configs/virtual/python.zsh new file mode 100644 index 00000000..f4347466 --- /dev/null +++ b/.zsh/configs/virtual/python.zsh @@ -0,0 +1 @@ +# Python/pyenv configuration has been moved to dev container environments diff --git a/docs/adr/0015-manage-portable-user-dotfiles-with-home-manager.md b/docs/adr/0015-manage-portable-user-dotfiles-with-home-manager.md new file mode 100644 index 00000000..4fc74884 --- /dev/null +++ b/docs/adr/0015-manage-portable-user-dotfiles-with-home-manager.md @@ -0,0 +1,57 @@ +# ADR 0015: Manage Portable User Dotfiles with Home Manager + +## Status + +Accepted + +## Context + +The macOS environment should be reproducible without using Apple's default +machine migration flow. ADR 0012 defines `nix/` as the source of truth for +macOS packages and local environment configuration, and ADR 0014 adds cmux and +Karabiner settings. + +Several portable text configuration files still lived only in the user home +directory. These files affect daily shell behavior, local workflow tools, and +window management, so losing them during a rebuild would make a new machine feel +incomplete even if packages were installed. + +Some home-directory files are not safe to manage in git because they contain +credentials, tokens, session state, app databases, histories, or +machine-specific runtime data. + +## Decision + +Manage portable, non-secret user dotfiles through `nix/home/dotfiles.nix`. + +- Source reusable zsh config fragments and functions from the repository. +- Source AeroSpace configuration from `dot/aerospace.toml`. +- Source small workflow-tool configs for act, Agent Deck, Graphite aliases, and + Codespaces secret repository selection. +- Source global git ignore and peco configuration from the repository. +- Use `force = true` for these files so home-manager can take ownership of + existing files during a rebuild. + +Do not manage secret or stateful files in git. Specifically keep the following +outside this repository: + +- SSH keys and `~/.ssh/config` +- cloud auth files under `~/.aws`, `~/.azure`, and `~/.config/gcloud` +- GitHub auth files such as `~/.config/gh/hosts.yml` +- token-bearing CLI configs such as Graphite `user_config` and npm auth config +- Claude/Codex auth, session, history, cache, and SQLite state +- application databases, browser profiles, and update caches + +## Consequences + +`darwin-rebuild switch --flake ~/develop/github.com/keito4/config/nix` can +restore a larger portion of the user environment on a new machine without +copying the old machine's home directory. + +Manual edits to managed dotfiles will be overwritten on the next home-manager +activation. Changes should be made in this repository first, then applied with +the Nix switch command. + +Authentication and app state still require explicit sign-in or a separate +secrets/bootstrap process. This keeps the repository reviewable and avoids +encoding machine-specific runtime state as configuration. diff --git a/dot/aerospace.toml b/dot/aerospace.toml new file mode 100644 index 00000000..10b80415 --- /dev/null +++ b/dot/aerospace.toml @@ -0,0 +1,206 @@ +# Place a copy of this config to ~/.aerospace.toml +# After that, you can edit ~/.aerospace.toml to your liking + +# You can use it to add commands that run after AeroSpace startup. +# Available commands : https://nikitabobko.github.io/AeroSpace/commands +after-startup-command = [] + +# Start AeroSpace at login +start-at-login = false + +# Normalizations. See: https://nikitabobko.github.io/AeroSpace/guide#normalization +enable-normalization-flatten-containers = true +enable-normalization-opposite-orientation-for-nested-containers = true + +# See: https://nikitabobko.github.io/AeroSpace/guide#layouts +# The 'accordion-padding' specifies the size of accordion padding +# You can set 0 to disable the padding feature +accordion-padding = 30 + +# Possible values: tiles|accordion +default-root-container-layout = 'tiles' + +# Possible values: horizontal|vertical|auto +# 'auto' means: wide monitor (anything wider than high) gets horizontal orientation, +# tall monitor (anything higher than wide) gets vertical orientation +default-root-container-orientation = 'auto' + +# Mouse follows focus when focused monitor changes +# Drop it from your config, if you don't like this behavior +# See https://nikitabobko.github.io/AeroSpace/guide#on-focus-changed-callbacks +# See https://nikitabobko.github.io/AeroSpace/commands#move-mouse +# Fallback value (if you omit the key): on-focused-monitor-changed = [] +on-focused-monitor-changed = ['move-mouse monitor-lazy-center'] + +# You can effectively turn off macOS "Hide application" (cmd-h) feature by toggling this flag +# Useful if you don't use this macOS feature, but accidentally hit cmd-h or cmd-alt-h key +# Also see: https://nikitabobko.github.io/AeroSpace/goodies#disable-hide-app +automatically-unhide-macos-hidden-apps = false + +# Possible values: (qwerty|dvorak|colemak) +# See https://nikitabobko.github.io/AeroSpace/guide#key-mapping +[key-mapping] + preset = 'qwerty' + +# Gaps between windows (inner-*) and between monitor edges (outer-*). +# Possible values: +# - Constant: gaps.outer.top = 8 +# - Per monitor: gaps.outer.top = [{ monitor.main = 16 }, { monitor."some-pattern" = 32 }, 24] +# In this example, 24 is a default value when there is no match. +# Monitor pattern is the same as for 'workspace-to-monitor-force-assignment'. +# See: +# https://nikitabobko.github.io/AeroSpace/guide#assign-workspaces-to-monitors +[gaps] + inner.horizontal = 0 + inner.vertical = 0 + outer.left = 0 + outer.bottom = 0 + outer.top = 0 + outer.right = 0 + +# 'main' binding mode declaration +# See: https://nikitabobko.github.io/AeroSpace/guide#binding-modes +# 'main' binding mode must be always presented +# Fallback value (if you omit the key): mode.main.binding = {} +[mode.main.binding] + + # All possible keys: + # - Letters. a, b, c, ..., z + # - Numbers. 0, 1, 2, ..., 9 + # - Keypad numbers. keypad0, keypad1, keypad2, ..., keypad9 + # - F-keys. f1, f2, ..., f20 + # - Special keys. minus, equal, period, comma, slash, backslash, quote, semicolon, + # backtick, leftSquareBracket, rightSquareBracket, space, enter, esc, + # backspace, tab, pageUp, pageDown, home, end, forwardDelete, + # sectionSign (ISO keyboards only, european keyboards only) + # - Keypad special. keypadClear, keypadDecimalMark, keypadDivide, keypadEnter, keypadEqual, + # keypadMinus, keypadMultiply, keypadPlus + # - Arrows. left, down, up, right + + # All possible modifiers: cmd, alt, ctrl, shift + + # All possible commands: https://nikitabobko.github.io/AeroSpace/commands + + # See: https://nikitabobko.github.io/AeroSpace/commands#exec-and-forget + # You can uncomment the following lines to open up terminal with alt + enter shortcut + # (like in i3) + # alt-enter = '''exec-and-forget osascript -e ' + # tell application "Terminal" + # do script + # activate + # end tell' + # ''' + + # See: https://nikitabobko.github.io/AeroSpace/commands#layout + alt-slash = 'layout tiles horizontal vertical' + alt-comma = 'layout accordion horizontal vertical' + + # See: https://nikitabobko.github.io/AeroSpace/commands#focus + alt-h = 'focus left' + alt-j = 'focus down' + alt-k = 'focus up' + alt-l = 'focus right' + + # See: https://nikitabobko.github.io/AeroSpace/commands#move + alt-shift-h = 'move left' + alt-shift-j = 'move down' + alt-shift-k = 'move up' + alt-shift-l = 'move right' + + # See: https://nikitabobko.github.io/AeroSpace/commands#resize + alt-minus = 'resize smart -50' + alt-equal = 'resize smart +50' + + # See: https://nikitabobko.github.io/AeroSpace/commands#workspace + alt-1 = 'workspace 1' + alt-2 = 'workspace 2' + alt-3 = 'workspace 3' + alt-4 = 'workspace 4' + alt-5 = 'workspace 5' + alt-6 = 'workspace 6' + alt-7 = 'workspace 7' + alt-8 = 'workspace 8' + alt-9 = 'workspace 9' + alt-a = 'workspace A' # In your config, you can drop workspace bindings that you don't need + alt-b = 'workspace B' + alt-c = 'workspace C' + alt-d = 'workspace D' + alt-e = 'workspace E' + alt-f = 'workspace F' + alt-g = 'workspace G' + alt-i = 'workspace I' + alt-m = 'workspace M' + alt-n = 'workspace N' + alt-o = 'workspace O' + alt-p = 'workspace P' + alt-q = 'workspace Q' + alt-r = 'workspace R' + alt-s = 'workspace S' + alt-t = 'workspace T' + alt-u = 'workspace U' + alt-v = 'workspace V' + alt-w = 'workspace W' + alt-x = 'workspace X' + alt-y = 'workspace Y' + alt-z = 'workspace Z' + + # See: https://nikitabobko.github.io/AeroSpace/commands#move-node-to-workspace + alt-shift-1 = 'move-node-to-workspace 1' + alt-shift-2 = 'move-node-to-workspace 2' + alt-shift-3 = 'move-node-to-workspace 3' + alt-shift-4 = 'move-node-to-workspace 4' + alt-shift-5 = 'move-node-to-workspace 5' + alt-shift-6 = 'move-node-to-workspace 6' + alt-shift-7 = 'move-node-to-workspace 7' + alt-shift-8 = 'move-node-to-workspace 8' + alt-shift-9 = 'move-node-to-workspace 9' + alt-shift-a = 'move-node-to-workspace A' + alt-shift-b = 'move-node-to-workspace B' + alt-shift-c = 'move-node-to-workspace C' + alt-shift-d = 'move-node-to-workspace D' + alt-shift-e = 'move-node-to-workspace E' + alt-shift-f = 'move-node-to-workspace F' + alt-shift-g = 'move-node-to-workspace G' + alt-shift-i = 'move-node-to-workspace I' + alt-shift-m = 'move-node-to-workspace M' + alt-shift-n = 'move-node-to-workspace N' + alt-shift-o = 'move-node-to-workspace O' + alt-shift-p = 'move-node-to-workspace P' + alt-shift-q = 'move-node-to-workspace Q' + alt-shift-r = 'move-node-to-workspace R' + alt-shift-s = 'move-node-to-workspace S' + alt-shift-t = 'move-node-to-workspace T' + alt-shift-u = 'move-node-to-workspace U' + alt-shift-v = 'move-node-to-workspace V' + alt-shift-w = 'move-node-to-workspace W' + alt-shift-x = 'move-node-to-workspace X' + alt-shift-y = 'move-node-to-workspace Y' + alt-shift-z = 'move-node-to-workspace Z' + + # See: https://nikitabobko.github.io/AeroSpace/commands#workspace-back-and-forth + alt-tab = 'workspace-back-and-forth' + # See: https://nikitabobko.github.io/AeroSpace/commands#move-workspace-to-monitor + alt-shift-tab = 'move-workspace-to-monitor --wrap-around next' + + # See: https://nikitabobko.github.io/AeroSpace/commands#mode + alt-shift-semicolon = 'mode service' + +# 'service' binding mode declaration. +# See: https://nikitabobko.github.io/AeroSpace/guide#binding-modes +[mode.service.binding] + esc = ['reload-config', 'mode main'] + r = ['flatten-workspace-tree', 'mode main'] # reset layout + f = ['layout floating tiling', 'mode main'] # Toggle between floating and tiling layout + backspace = ['close-all-windows-but-current', 'mode main'] + + # sticky is not yet supported https://github.com/nikitabobko/AeroSpace/issues/2 + #s = ['layout sticky tiling', 'mode main'] + + alt-shift-h = ['join-with left', 'mode main'] + alt-shift-j = ['join-with down', 'mode main'] + alt-shift-k = ['join-with up', 'mode main'] + alt-shift-l = ['join-with right', 'mode main'] + + down = 'volume down' + up = 'volume up' + shift-down = ['volume set 0', 'mode main'] diff --git a/dot/config/act/actrc b/dot/config/act/actrc new file mode 100644 index 00000000..eb64a527 --- /dev/null +++ b/dot/config/act/actrc @@ -0,0 +1,4 @@ +-P ubuntu-latest=catthehacker/ubuntu:act-latest +-P ubuntu-22.04=catthehacker/ubuntu:act-22.04 +-P ubuntu-20.04=catthehacker/ubuntu:act-20.04 +-P ubuntu-18.04=catthehacker/ubuntu:act-18.04 diff --git a/dot/config/agent-deck/config.toml b/dot/config/agent-deck/config.toml new file mode 100644 index 00000000..9edbe07d --- /dev/null +++ b/dot/config/agent-deck/config.toml @@ -0,0 +1,20 @@ +# Agent Deck Configuration +# All options: https://github.com/asheshgoplani/agent-deck/blob/main/skills/agent-deck/references/config-reference.md + +default_tool = "claude" +theme = "dark" + +[claude] +dangerous_mode = false + +[global_search] +enabled = true +tier = "auto" +recent_days = 90 + +[logs] +max_size_mb = 10 +max_lines = 10000 + +[instances] +allow_multiple = true diff --git a/dot/config/codespaces-secrets/repos.txt b/dot/config/codespaces-secrets/repos.txt new file mode 100644 index 00000000..5b97b7b0 --- /dev/null +++ b/dot/config/codespaces-secrets/repos.txt @@ -0,0 +1,2 @@ +OYKOT-jp/nomad_japan +keito4/effectuation diff --git a/dot/config/graphite/aliases b/dot/config/graphite/aliases new file mode 100644 index 00000000..0963808c --- /dev/null +++ b/dot/config/graphite/aliases @@ -0,0 +1,11 @@ +# Edit this file to configure aliases for Graphite commands. +# If you delete this file, it will be recreated with the default aliases. +# The first word of each line is the alias, and the rest is the command. +# Lines starting with # are ignored. + +# The aliases for ss, ls, and ll are defined by default and must be overridden to be disabled. +# They are shown below to demonstrate the formatting. + +ls log short +ll log long +ss submit --stack diff --git a/nix/home/default.nix b/nix/home/default.nix index d72d897c..de1ce1e4 100644 --- a/nix/home/default.nix +++ b/nix/home/default.nix @@ -5,6 +5,7 @@ ./packages.nix ./git.nix ./zsh.nix + ./dotfiles.nix ./cmux.nix ./karabiner.nix ]; diff --git a/nix/home/dotfiles.nix b/nix/home/dotfiles.nix new file mode 100644 index 00000000..bfed7dd2 --- /dev/null +++ b/nix/home/dotfiles.nix @@ -0,0 +1,52 @@ +{ ... }: + +let + managedSource = source: { + inherit source; + force = true; + }; +in +{ + home.file = { + ".aerospace.toml" = managedSource ../../dot/aerospace.toml; + + ".config/act/actrc" = managedSource ../../dot/config/act/actrc; + ".config/agent-deck/config.toml" = managedSource ../../dot/config/agent-deck/config.toml; + ".config/codespaces-secrets/repos.txt" = + managedSource ../../dot/config/codespaces-secrets/repos.txt; + ".config/graphite/aliases" = managedSource ../../dot/config/graphite/aliases; + + ".gitignore" = managedSource ../../git/gitignore; + ".peco/config.json" = managedSource ../../dot/.peco/config.json; + ".zshrc.devcontainer" = managedSource ../../dot/.zshrc.devcontainer; + + ".zsh/configs/aliases.zsh" = managedSource ../../.zsh/configs/aliases.zsh; + ".zsh/configs/color.zsh" = managedSource ../../.zsh/configs/color.zsh; + ".zsh/configs/history.zsh" = managedSource ../../.zsh/configs/history.zsh; + ".zsh/configs/keybindings.zsh" = managedSource ../../.zsh/configs/keybindings.zsh; + ".zsh/configs/prompt.zsh" = managedSource ../../.zsh/configs/prompt.zsh; + + ".zsh/configs/pre/.env" = managedSource ../../.zsh/configs/pre/.env; + ".zsh/configs/pre/.env.secret.template" = managedSource ../../.zsh/configs/pre/.env.secret.template; + ".zsh/configs/pre/.gitignore" = managedSource ../../.zsh/configs/pre/.gitignore; + ".zsh/configs/pre/envup.zsh" = managedSource ../../.zsh/configs/pre/envup.zsh; + ".zsh/configs/pre/path.zsh" = managedSource ../../.zsh/configs/pre/path.zsh; + + ".zsh/configs/virtual/dart.zsh" = managedSource ../../.zsh/configs/virtual/dart.zsh; + ".zsh/configs/virtual/go.zsh" = managedSource ../../.zsh/configs/virtual/go.zsh; + ".zsh/configs/virtual/java.zsh" = managedSource ../../.zsh/configs/virtual/java.zsh; + ".zsh/configs/virtual/php.zsh" = managedSource ../../.zsh/configs/virtual/php.zsh; + ".zsh/configs/virtual/python.zsh" = managedSource ../../.zsh/configs/virtual/python.zsh; + ".zsh/configs/virtual/ruby.zsh" = managedSource ../../.zsh/configs/virtual/ruby.zsh; + + ".zsh/functions/docker" = managedSource ../../.zsh/functions/docker; + ".zsh/functions/gcp" = managedSource ../../.zsh/functions/gcp; + ".zsh/functions/git" = managedSource ../../.zsh/functions/git; + ".zsh/functions/mkcd" = managedSource ../../.zsh/functions/mkcd; + ".zsh/functions/op" = managedSource ../../.zsh/functions/op; + ".zsh/functions/peco" = managedSource ../../.zsh/functions/peco; + ".zsh/functions/process" = managedSource ../../.zsh/functions/process; + ".zsh/functions/terraform" = managedSource ../../.zsh/functions/terraform; + ".zsh/functions/utilities" = managedSource ../../.zsh/functions/utilities; + }; +} diff --git a/test/nix-darwin-config.test.js b/test/nix-darwin-config.test.js index c31d63c6..2b8ff8ba 100644 --- a/test/nix-darwin-config.test.js +++ b/test/nix-darwin-config.test.js @@ -11,6 +11,7 @@ describe('nix-darwin and home-manager macOS configuration', () => { test('home-manager imports cmux and Karabiner modules', () => { const homeDefault = readRepoFile('nix/home/default.nix'); + expect(homeDefault).toContain('./dotfiles.nix'); expect(homeDefault).toContain('./cmux.nix'); expect(homeDefault).toContain('./karabiner.nix'); }); @@ -74,4 +75,43 @@ describe('nix-darwin and home-manager macOS configuration', () => { expect(karabinerModule).toContain('cmuxImeShortcut "semicolon" "japanese_eisuu"'); expect(karabinerModule).toContain('cmuxImeShortcut "quote" "japanese_eisuu"'); }); + + test('portable user dotfiles are managed without credential state', () => { + const dotfilesModule = readRepoFile('nix/home/dotfiles.nix'); + + [ + 'dot/aerospace.toml', + 'dot/config/act/actrc', + 'dot/config/agent-deck/config.toml', + 'dot/config/codespaces-secrets/repos.txt', + 'dot/config/graphite/aliases', + 'git/gitignore', + 'dot/.peco/config.json', + '.zsh/configs/aliases.zsh', + '.zsh/configs/virtual/go.zsh', + '.zsh/configs/virtual/php.zsh', + '.zsh/configs/virtual/python.zsh', + '.zsh/functions/git', + ].forEach((relativePath) => { + expect(fs.existsSync(path.join(repoPath, relativePath))).toBe(true); + }); + + expect(dotfilesModule).toContain('managedSource'); + expect(dotfilesModule).toContain('".aerospace.toml"'); + expect(dotfilesModule).toContain('".config/act/actrc"'); + expect(dotfilesModule).toContain('".config/agent-deck/config.toml"'); + expect(dotfilesModule).toContain('".config/codespaces-secrets/repos.txt"'); + expect(dotfilesModule).toContain('".config/graphite/aliases"'); + expect(dotfilesModule).toContain('".gitignore"'); + expect(dotfilesModule).toContain('".peco/config.json"'); + expect(dotfilesModule).toContain('".zsh/configs/virtual/go.zsh"'); + expect(dotfilesModule).toContain('".zsh/configs/virtual/php.zsh"'); + expect(dotfilesModule).toContain('".zsh/configs/virtual/python.zsh"'); + + expect(dotfilesModule).not.toContain('user_config'); + expect(dotfilesModule).not.toContain('hosts.yml'); + expect(dotfilesModule).not.toContain('.npmrc'); + expect(dotfilesModule).not.toContain('.ssh'); + expect(dotfilesModule).not.toContain('.env.secret"'); + }); }); From e21659e19aaae974140167bdacf6c3c916a1fdc8 Mon Sep 17 00:00:00 2001 From: keito4 Date: Sun, 21 Jun 2026 23:06:56 +0900 Subject: [PATCH 4/5] feat: add local config inventory agent command --- nix/home/agent-commands.nix | 15 +++ nix/home/default.nix | 1 + script/agent/collect-local-configs.sh | 184 ++++++++++++++++++++++++++ test/nix-darwin-config.test.js | 21 +++ 4 files changed, 221 insertions(+) create mode 100644 nix/home/agent-commands.nix create mode 100755 script/agent/collect-local-configs.sh diff --git a/nix/home/agent-commands.nix b/nix/home/agent-commands.nix new file mode 100644 index 00000000..fd460afd --- /dev/null +++ b/nix/home/agent-commands.nix @@ -0,0 +1,15 @@ +{ ... }: + +let + managedCommand = source: { + inherit source; + executable = true; + force = true; + }; +in +{ + home.file = { + ".local/bin/agent-collect-local-configs" = + managedCommand ../../script/agent/collect-local-configs.sh; + }; +} diff --git a/nix/home/default.nix b/nix/home/default.nix index de1ce1e4..725d66cc 100644 --- a/nix/home/default.nix +++ b/nix/home/default.nix @@ -6,6 +6,7 @@ ./git.nix ./zsh.nix ./dotfiles.nix + ./agent-commands.nix ./cmux.nix ./karabiner.nix ]; diff --git a/script/agent/collect-local-configs.sh b/script/agent/collect-local-configs.sh new file mode 100755 index 00000000..e8e589b0 --- /dev/null +++ b/script/agent/collect-local-configs.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash +# Inventory local/secret-looking config files without reading their contents. + +set -euo pipefail + +ROOTS=() +CONTEXT_DIR="${CONTEXT_DIR:-.context}" +OUTPUT="" + +usage() { + cat <<'EOF' +Usage: script/agent/collect-local-configs.sh [--root DIR ...] [--output FILE] + +Collects paths, size, and mtime for local config candidates such as +config.local.json, settings.local.json, .env.local, auth.json, and +credentials-like JSON files. + +The report intentionally does not copy or print file contents. + +When --root is omitted, the scan is limited to common configuration locations +instead of walking the entire home directory. +EOF +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --root) + ROOTS+=("${2:?--root requires a value}") + shift 2 + ;; + --output) + OUTPUT="${2:?--output requires a value}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + printf 'Unknown argument: %s\n' "$1" >&2 + usage >&2 + exit 2 + ;; + esac +done + +if [[ -z "$OUTPUT" ]]; then + OUTPUT="$CONTEXT_DIR/local-config-candidates.tsv" +fi + +if [[ "${#ROOTS[@]}" -eq 0 ]]; then + ROOTS=( + "$HOME/.config" + "$HOME/.claude" + "$HOME/.codex" + "$HOME/Library/Application Support/Code/User" + "$HOME/Library/Application Support/Cursor/User" + "$HOME/develop" + ) +fi + +mkdir -p "$(dirname "$OUTPUT")" + +classify_path() { + local path="${1:?path required}" + local base + base="$(basename "$path")" + + case "$base" in + config.local.json|settings.local.json|*.local.json|*.local.jsonc) + printf 'local-json' + ;; + *.local.toml) + printf 'local-toml' + ;; + *.local.yaml|*.local.yml) + printf 'local-yaml' + ;; + .env.local|*.env.local|*.local.env) + printf 'local-env' + ;; + auth.json|hosts.yml|credentials.json|*credentials*.json|secret.json|secrets.json|*.secret.json|*.secrets.json) + printf 'auth-or-secret-candidate' + ;; + *credential*.json) + printf 'auth-or-secret-candidate' + ;; + token.json|tokens.json|*.token.json|*.tokens.json|*auth-token*.json|*auth_token*.json|*access-token*.json|*access_token*.json|*refresh-token*.json|*refresh_token*.json|*api-token*.json|*api_token*.json) + printf 'auth-or-secret-candidate' + ;; + *) + printf 'local-config-candidate' + ;; + esac +} + +file_size() { + local path="${1:?path required}" + stat -f '%z' "$path" 2>/dev/null || stat -c '%s' "$path" +} + +file_mtime() { + local path="${1:?path required}" + stat -f '%Sm' -t '%Y-%m-%dT%H:%M:%S%z' "$path" 2>/dev/null || stat -c '%y' "$path" +} + +printf 'category\tbytes\tmtime\tpath\n' >"$OUTPUT" + +for root in "${ROOTS[@]}"; do + [[ -d "$root" ]] || continue + + while IFS= read -r -d '' path; do + category="$(classify_path "$path")" + bytes="$(file_size "$path")" + mtime="$(file_mtime "$path")" + printf '%s\t%s\t%s\t%s\n' "$category" "$bytes" "$mtime" "$path" >>"$OUTPUT" + done < <( + find "$root" \ + \( \ + -name .git -o \ + -name .context -o \ + -name .next -o \ + -name .terraform -o \ + -name .terragrunt-cache -o \ + -name .turbo -o \ + -name .vercel -o \ + -name .venv -o \ + -name __pycache__ -o \ + -name checkpoints -o \ + -path "$HOME/.claude/cache" -o \ + -path "$HOME/.claude/plugins/cache" -o \ + -path "$HOME/.claude/plugins/marketplaces" -o \ + -path "$HOME/.claude/sessions" -o \ + -path "$HOME/.claude/telemetry" -o \ + -path "$HOME/.codex/cache" -o \ + -path "$HOME/.codex/log" -o \ + -path "$HOME/.codex/logs" -o \ + -path "$HOME/.codex/memories" -o \ + -path "$HOME/.codex/sessions" -o \ + -path "$HOME/.codex/tmp" -o \ + -name node_modules -o \ + -name venv -o \ + -name vendor -o \ + -path '*/globalStorage' -o \ + -path '*/workspaceStorage' \ + \) -prune -o \ + -type f \ + \( \ + -name 'config.local.json' -o \ + -name 'settings.local.json' -o \ + -name '*.local.json' -o \ + -name '*.local.jsonc' -o \ + -name '*.local.toml' -o \ + -name '*.local.yaml' -o \ + -name '*.local.yml' -o \ + -name '.env.local' -o \ + -name '*.env.local' -o \ + -name '*.local.env' -o \ + -name 'auth.json' -o \ + -name 'hosts.yml' -o \ + -name 'credentials.json' -o \ + -name '*credentials*.json' -o \ + -name '*credential*.json' -o \ + -name 'secret.json' -o \ + -name 'secrets.json' -o \ + -name '*.secret.json' -o \ + -name '*.secrets.json' -o \ + -name 'token.json' -o \ + -name 'tokens.json' -o \ + -name '*.token.json' -o \ + -name '*.tokens.json' -o \ + -name '*auth-token*.json' -o \ + -name '*auth_token*.json' -o \ + -name '*access-token*.json' -o \ + -name '*access_token*.json' -o \ + -name '*refresh-token*.json' -o \ + -name '*refresh_token*.json' -o \ + -name '*api-token*.json' -o \ + -name '*api_token*.json' \ + \) -print0 2>/dev/null + ) +done + +printf 'Wrote %s\n' "$OUTPUT" diff --git a/test/nix-darwin-config.test.js b/test/nix-darwin-config.test.js index 2b8ff8ba..f0aafd74 100644 --- a/test/nix-darwin-config.test.js +++ b/test/nix-darwin-config.test.js @@ -12,6 +12,7 @@ describe('nix-darwin and home-manager macOS configuration', () => { const homeDefault = readRepoFile('nix/home/default.nix'); expect(homeDefault).toContain('./dotfiles.nix'); + expect(homeDefault).toContain('./agent-commands.nix'); expect(homeDefault).toContain('./cmux.nix'); expect(homeDefault).toContain('./karabiner.nix'); }); @@ -114,4 +115,24 @@ describe('nix-darwin and home-manager macOS configuration', () => { expect(dotfilesModule).not.toContain('.ssh'); expect(dotfilesModule).not.toContain('.env.secret"'); }); + + test('agent local config collector is installed as a safe command', () => { + const agentCommandsModule = readRepoFile('nix/home/agent-commands.nix'); + const collectorScript = readRepoFile('script/agent/collect-local-configs.sh'); + const collectorPath = path.join(repoPath, 'script/agent/collect-local-configs.sh'); + + expect(fs.statSync(collectorPath).mode & 0o111).toBeTruthy(); + expect(agentCommandsModule).toContain('".local/bin/agent-collect-local-configs"'); + expect(agentCommandsModule).toContain('../../script/agent/collect-local-configs.sh'); + expect(agentCommandsModule).toContain('executable = true;'); + expect(agentCommandsModule).toContain('force = true;'); + + expect(collectorScript).toContain('config.local.json'); + expect(collectorScript).toContain('settings.local.json'); + expect(collectorScript).toContain('.env.local'); + expect(collectorScript).toContain('auth.json'); + expect(collectorScript).toContain('credentials*.json'); + expect(collectorScript).toContain('category\\tbytes\\tmtime\\tpath'); + expect(collectorScript).toContain('intentionally does not copy or print file contents'); + }); }); From 11ecc5b61a04dbf443245da460ef116d22c53fce Mon Sep 17 00:00:00 2001 From: keito4 Date: Mon, 22 Jun 2026 09:56:59 +0900 Subject: [PATCH 5/5] chore: sync macos cask app list --- nix/modules/homebrew.nix | 5 +++-- test/nix-darwin-config.test.js | 7 +++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/nix/modules/homebrew.nix b/nix/modules/homebrew.nix index 5ee40a52..72868210 100644 --- a/nix/modules/homebrew.nix +++ b/nix/modules/homebrew.nix @@ -41,17 +41,19 @@ "1password-cli" # Development Tools + "android-studio" "cursor" "dotnet-sdk" + "flutter" "gcloud-cli" "ngrok" "orbstack" - "rancher" "tableplus" "visual-studio-code" # Communication "discord" + "mattermost" "messenger" "slack" "zoom" @@ -59,7 +61,6 @@ # Productivity "aerospace" "alfred" - "bartender" "bettertouchtool" "karabiner-elements" "notion" diff --git a/test/nix-darwin-config.test.js b/test/nix-darwin-config.test.js index f0aafd74..a1bb5329 100644 --- a/test/nix-darwin-config.test.js +++ b/test/nix-darwin-config.test.js @@ -20,9 +20,16 @@ describe('nix-darwin and home-manager macOS configuration', () => { test('Homebrew casks install cmux and input tooling', () => { const homebrewModule = readRepoFile('nix/modules/homebrew.nix'); + expect(homebrewModule).toContain('"android-studio"'); expect(homebrewModule).toContain('"cmux"'); + expect(homebrewModule).toContain('"flutter"'); expect(homebrewModule).toContain('"karabiner-elements"'); + expect(homebrewModule).toContain('"mattermost"'); expect(homebrewModule).toContain('"google-japanese-ime"'); + expect(homebrewModule).not.toContain('"bartender"'); + expect(homebrewModule).not.toContain('"rancher"'); + expect(homebrewModule).not.toContain('"google-cloud-sdk"'); + expect(homebrewModule).not.toContain('"tailscale"'); }); test('cmux terminal config leaves IME shortcuts unbound', () => {