diff --git a/.chezmoi.toml.tmpl b/.chezmoi.toml.tmpl
index d36f76f..a78e103 100644
--- a/.chezmoi.toml.tmpl
+++ b/.chezmoi.toml.tmpl
@@ -1,7 +1,7 @@
[data.git]
name = {{ promptStringOnce . "git.name" "git.name" .chezmoi.username | quote }}
email = {{ promptStringOnce . "git.email" "git.email" "" | quote }}
- signingkey = {{ promptStringOnce . "git.signingkey" "git.signingkey" "" | quote }}
+ signingKey = {{ promptStringOnce . "git.signingKey" "git.signingKey" "" | quote }}
[data.ssh]
bitwardenItem = {{ promptStringOnce . "ssh.bitwardenItem" "ssh.bitwardenItem" "" | quote }}
diff --git a/.chezmoiignore b/.chezmoiignore
index 4b7c06e..34d994e 100644
--- a/.chezmoiignore
+++ b/.chezmoiignore
@@ -12,6 +12,7 @@ justfile
{{- if not (env "BW_SESSION") }}
.ssh/config
.config/ngrok/ngrok.yml
+Library/Application Support/ngrok/ngrok.yml
{{- end }}
{{- if ne .chezmoi.os "darwin" }}
diff --git a/Library/Application Support/iTerm2/DynamicProfiles/chezmoi.json b/Library/Application Support/iTerm2/DynamicProfiles/chezmoi.json
deleted file mode 100644
index 484a7c5..0000000
--- a/Library/Application Support/iTerm2/DynamicProfiles/chezmoi.json
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "Profiles": [
- {
- "Name": "Chezmoi",
- "Guid": "6E4B62A6-1C73-4F91-8E32-DAC9C3FE3BBE",
- "Dynamic Profile Parent Name": "Default",
- "Normal Font": "JetBrainsMonoNFM-Regular 13",
- "Non Ascii Font": "JetBrainsMonoNFM-Regular 13",
- "Use Non-ASCII Font": false
- }
- ]
-}
diff --git a/Library/Application Support/iTerm2/DynamicProfiles/chezmoi.json.tmpl b/Library/Application Support/iTerm2/DynamicProfiles/chezmoi.json.tmpl
new file mode 100644
index 0000000..1c461f6
--- /dev/null
+++ b/Library/Application Support/iTerm2/DynamicProfiles/chezmoi.json.tmpl
@@ -0,0 +1,153 @@
+{
+ "Profiles": [
+ {
+ "Name": "Chezmoi",
+ "Guid": "6E4B62A6-1C73-4F91-8E32-DAC9C3FE3BBE",
+ "Dynamic Profile Parent Name": "Default",
+ "Normal Font": "JetBrainsMonoNFM-Regular 13",
+ "Non Ascii Font": "JetBrainsMonoNFM-Regular 13",
+ "Use Non-ASCII Font": false,
+ "Custom Command": "Yes",
+ "Command": "{{ if eq .chezmoi.arch "arm64" }}/opt/homebrew{{ else }}/usr/local{{ end }}/bin/fish --login",
+ "Use Separate Colors for Light and Dark Mode": false,
+ "Background Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.180392,
+ "Green Component": 0.203922,
+ "Blue Component": 0.25098
+ },
+ "Foreground Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.847059,
+ "Green Component": 0.870588,
+ "Blue Component": 0.913725
+ },
+ "Bold Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.847059,
+ "Green Component": 0.870588,
+ "Blue Component": 0.913725
+ },
+ "Cursor Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.847059,
+ "Green Component": 0.870588,
+ "Blue Component": 0.913725
+ },
+ "Cursor Text Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.180392,
+ "Green Component": 0.203922,
+ "Blue Component": 0.25098
+ },
+ "Selection Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.262745,
+ "Green Component": 0.298039,
+ "Blue Component": 0.368627
+ },
+ "Selected Text Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.847059,
+ "Green Component": 0.870588,
+ "Blue Component": 0.913725
+ },
+ "Ansi 0 Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.231373,
+ "Green Component": 0.258824,
+ "Blue Component": 0.321569
+ },
+ "Ansi 1 Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.74902,
+ "Green Component": 0.380392,
+ "Blue Component": 0.415686
+ },
+ "Ansi 2 Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.639216,
+ "Green Component": 0.745098,
+ "Blue Component": 0.54902
+ },
+ "Ansi 3 Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.921569,
+ "Green Component": 0.796078,
+ "Blue Component": 0.545098
+ },
+ "Ansi 4 Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.505882,
+ "Green Component": 0.631373,
+ "Blue Component": 0.756863
+ },
+ "Ansi 5 Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.705882,
+ "Green Component": 0.556863,
+ "Blue Component": 0.678431
+ },
+ "Ansi 6 Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.533333,
+ "Green Component": 0.752941,
+ "Blue Component": 0.815686
+ },
+ "Ansi 7 Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.898039,
+ "Green Component": 0.913725,
+ "Blue Component": 0.941176
+ },
+ "Ansi 8 Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.298039,
+ "Green Component": 0.337255,
+ "Blue Component": 0.415686
+ },
+ "Ansi 9 Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.74902,
+ "Green Component": 0.380392,
+ "Blue Component": 0.415686
+ },
+ "Ansi 10 Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.639216,
+ "Green Component": 0.745098,
+ "Blue Component": 0.54902
+ },
+ "Ansi 11 Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.921569,
+ "Green Component": 0.796078,
+ "Blue Component": 0.545098
+ },
+ "Ansi 12 Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.505882,
+ "Green Component": 0.631373,
+ "Blue Component": 0.756863
+ },
+ "Ansi 13 Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.705882,
+ "Green Component": 0.556863,
+ "Blue Component": 0.678431
+ },
+ "Ansi 14 Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.560784,
+ "Green Component": 0.737255,
+ "Blue Component": 0.733333
+ },
+ "Ansi 15 Color": {
+ "Color Space": "sRGB",
+ "Red Component": 0.92549,
+ "Green Component": 0.937255,
+ "Blue Component": 0.956863
+ }
+ }
+ ]
+}
diff --git a/Library/Application Support/private_ngrok/private_ngrok.yml.tmpl b/Library/Application Support/private_ngrok/private_ngrok.yml.tmpl
index 099a849..2c25ea5 100644
--- a/Library/Application Support/private_ngrok/private_ngrok.yml.tmpl
+++ b/Library/Application Support/private_ngrok/private_ngrok.yml.tmpl
@@ -3,5 +3,5 @@
{{- if $bitwardenItem }}
version: "3"
agent:
- authtoken: {{ output "bw" "get" "notes" $bitwardenItem }}
+ authtoken: {{ output "bw" "get" "notes" $bitwardenItem "--nointeraction" }}
{{- end }}
diff --git a/Library/private_LaunchAgents/local.mlx.coder-next.plist.tmpl b/Library/private_LaunchAgents/local.mlx.coder-next.plist.tmpl
new file mode 100644
index 0000000..c61cb55
--- /dev/null
+++ b/Library/private_LaunchAgents/local.mlx.coder-next.plist.tmpl
@@ -0,0 +1,31 @@
+
+
+
+
+
+ Label
+ local.mlx.coder-next
+
+ ProgramArguments
+
+ {{ .chezmoi.homeDir }}/.local/bin/mlx_lm.server
+ --model
+ mlx-community/Qwen3-Coder-Next-4bit
+ --host
+ 127.0.0.1
+ --port
+ 8080
+ --prefill-step-size
+ 512
+ --max-tokens
+ 32768
+
+
+ StandardOutPath
+ /tmp/mlx-coder-next.log
+
+ StandardErrorPath
+ /tmp/mlx-coder-next-error.log
+
+
diff --git a/Library/private_LaunchAgents/local.mlx.qwen30b.plist.tmpl b/Library/private_LaunchAgents/local.mlx.qwen30b.plist.tmpl
new file mode 100644
index 0000000..f4df562
--- /dev/null
+++ b/Library/private_LaunchAgents/local.mlx.qwen30b.plist.tmpl
@@ -0,0 +1,29 @@
+
+
+
+
+
+ Label
+ local.mlx.qwen30b
+
+ ProgramArguments
+
+ {{ .chezmoi.homeDir }}/.local/bin/mlx_lm.server
+ --model
+ mlx-community/Qwen3-Coder-30B-A3B-Instruct-8bit
+ --host
+ 127.0.0.1
+ --port
+ 8080
+ --max-tokens
+ 32768
+
+
+ StandardOutPath
+ /tmp/mlx-qwen30b.log
+
+ StandardErrorPath
+ /tmp/mlx-qwen30b-error.log
+
+
diff --git a/README.md b/README.md
index 47de726..67f25ea 100644
--- a/README.md
+++ b/README.md
@@ -24,19 +24,33 @@ chezmoi apply --dry-run --verbose
chezmoi apply
```
+Without `BW_SESSION`, chezmoi skips the private SSH and ngrok configs and leaves
+existing copies untouched. An invalid session fails without prompting; unlock
+Bitwarden again to include secrets in the diff or apply.
+
+The installer includes Atuin and Zed on both supported platforms, plus Raycast
+on macOS. The macOS `Chezmoi` iTerm2 profile launches fish with a Nord dark palette.
+See [Setup](wiki/Setup.md) for updating an existing machine.
+
## Testing
-Prerequisite: [just installed](https://github.com/casey/just).
+Install [just](https://github.com/casey/just). Local checks also need Rust/Cargo;
+template tests need Python 3 and chezmoi. Arch container tests need Docker running.
See [justfile](justfile) for available test commands.
Examples:
```bash
-just test-arch
+just check
+just test-templates
just test-arch-ci
+just test-arch-bootstrap-ci
```
+See [Install Script Testing](wiki/Install-Script-Testing.md) for test scope and
+Apple Silicon Docker limitations.
+
## Wiki
- [Setup](wiki/Setup.md)
@@ -44,3 +58,4 @@ just test-arch-ci
- [Bitwarden SSH hosts](wiki/Bitwarden-SSH.md)
- [Managing dotfiles](wiki/Managing-Dotfiles.md)
- [Template verification](wiki/Template-Verification.md)
+- [Local AI](wiki/Local-AI.md)
diff --git a/dot_config/fish/config.fish.tmpl b/dot_config/fish/config.fish.tmpl
index 676baa5..8da57e2 100644
--- a/dot_config/fish/config.fish.tmpl
+++ b/dot_config/fish/config.fish.tmpl
@@ -18,6 +18,10 @@ fish_add_path "$HOME/.cargo/bin"
fish_add_path "$HOME/go/bin"
fish_add_path "$HOME/.local/bin"
+if test -d "$HOME/.lando/bin"
+ fish_add_path "$HOME/.lando/bin"
+end
+
if functions -q load_nvm
load_nvm > /dev/stderr
end
@@ -92,3 +96,6 @@ set -gx VISUAL zed
if status is-interactive
atuin init fish | source
end
+
+alias qwen-heavy-off='launchctl kill SIGTERM gui/(id -u)/local.mlx.coder-next'
+alias qwen-light-off='launchctl kill SIGTERM gui/(id -u)/local.mlx.qwen30b'
diff --git a/dot_config/fish/functions/qwen-bootstrap.fish b/dot_config/fish/functions/qwen-bootstrap.fish
new file mode 100644
index 0000000..0b7281f
--- /dev/null
+++ b/dot_config/fish/functions/qwen-bootstrap.fish
@@ -0,0 +1,6 @@
+function qwen-bootstrap
+ for name in local.mlx.coder-next local.mlx.qwen30b
+ launchctl bootout gui/(id -u)/$name
+ launchctl bootstrap gui/(id -u) "$HOME/Library/LaunchAgents/$name.plist"
+ end
+end
diff --git a/dot_config/fish/functions/qwen-heavy.fish b/dot_config/fish/functions/qwen-heavy.fish
new file mode 100644
index 0000000..fff6c1f
--- /dev/null
+++ b/dot_config/fish/functions/qwen-heavy.fish
@@ -0,0 +1,4 @@
+function qwen-heavy
+ launchctl kill SIGTERM gui/(id -u)/local.mlx.qwen30b 2>/dev/null
+ launchctl kickstart gui/(id -u)/local.mlx.coder-next
+end
diff --git a/dot_config/fish/functions/qwen-kill-all.fish b/dot_config/fish/functions/qwen-kill-all.fish
new file mode 100644
index 0000000..e1909fb
--- /dev/null
+++ b/dot_config/fish/functions/qwen-kill-all.fish
@@ -0,0 +1,4 @@
+function qwen-kill-all
+ launchctl kill SIGTERM gui/(id -u)/local.mlx.coder-next 2>/dev/null
+ launchctl kill SIGTERM gui/(id -u)/local.mlx.qwen30b 2>/dev/null
+end
diff --git a/dot_config/fish/functions/qwen-light.fish b/dot_config/fish/functions/qwen-light.fish
new file mode 100644
index 0000000..51ead97
--- /dev/null
+++ b/dot_config/fish/functions/qwen-light.fish
@@ -0,0 +1,4 @@
+function qwen-light
+ launchctl kill SIGTERM gui/(id -u)/local.mlx.coder-next 2>/dev/null
+ launchctl kickstart gui/(id -u)/local.mlx.qwen30b
+end
diff --git a/dot_config/private_ngrok/private_ngrok.yml.tmpl b/dot_config/private_ngrok/private_ngrok.yml.tmpl
index 099a849..2c25ea5 100644
--- a/dot_config/private_ngrok/private_ngrok.yml.tmpl
+++ b/dot_config/private_ngrok/private_ngrok.yml.tmpl
@@ -3,5 +3,5 @@
{{- if $bitwardenItem }}
version: "3"
agent:
- authtoken: {{ output "bw" "get" "notes" $bitwardenItem }}
+ authtoken: {{ output "bw" "get" "notes" $bitwardenItem "--nointeraction" }}
{{- end }}
diff --git a/dot_gitconfig.tmpl b/dot_gitconfig.tmpl
index 7dab2c5..6e3dd65 100644
--- a/dot_gitconfig.tmpl
+++ b/dot_gitconfig.tmpl
@@ -3,8 +3,8 @@
useConfigOnly = true
name = {{ .git.name }}
email = {{ .git.email }}
- {{- if .git.signingkey }}
- signingKey = {{ .git.signingkey }}
+ {{- if .git.signingKey }}
+ signingKey = {{ .git.signingKey }}
{{- end }}
[init]
@@ -89,5 +89,5 @@
line-numbers = true
{{ end }}
-[includeIf "gitdir:~/Code/work/n"]
+[includeIf "gitdir:~/Code/work/n/**"]
path = ~/.config/git/n.inc
diff --git a/installer/packages.toml b/installer/packages.toml
index de27f5d..acd4bc3 100644
--- a/installer/packages.toml
+++ b/installer/packages.toml
@@ -3,7 +3,9 @@ pacman = [
"git",
"git-lfs",
"fish",
+ "atuin",
"neovim",
+ "zed",
"tree-sitter-cli",
"base-devel",
"wl-clipboard",
@@ -59,6 +61,7 @@ formula = [
"git",
"git-lfs",
"fish",
+ "atuin",
"neovim",
"tree-sitter-cli",
"ripgrep",
@@ -102,6 +105,8 @@ casks = [
"ngrok",
"utm",
"iterm2",
+ "zed",
+ "raycast",
]
[cargo]
@@ -126,6 +131,7 @@ packages = [
install_uv = true
packages = [
{ check = "harlequin", package = "harlequin" },
+ { check = "mlx-lm", package = "mlx-lm" },
]
[fish]
diff --git a/justfile b/justfile
index df12122..de2d5f9 100644
--- a/justfile
+++ b/justfile
@@ -67,6 +67,10 @@ check-sh:
check-rust:
cd installer && cargo check
+# Test secret templates without accessing Bitwarden
+test-templates:
+ python3 scripts/test-bitwarden-templates.py
+
# Format Rust installer
fmt:
cd installer && cargo fmt
diff --git a/private_dot_ssh/config.tmpl b/private_dot_ssh/config.tmpl
index 53996dc..4704164 100644
--- a/private_dot_ssh/config.tmpl
+++ b/private_dot_ssh/config.tmpl
@@ -8,5 +8,5 @@ Host *
{{- $ssh := get . "ssh" | default dict }}
{{- $bitwardenItem := get $ssh "bitwardenItem" | default "" }}
{{- if $bitwardenItem }}
-{{ output "bw" "get" "notes" $bitwardenItem }}
+{{ output "bw" "get" "notes" $bitwardenItem "--nointeraction" }}
{{- end }}
diff --git a/run_onchange_after_reload-qwen.fish.tmpl b/run_onchange_after_reload-qwen.fish.tmpl
new file mode 100644
index 0000000..f2a179c
--- /dev/null
+++ b/run_onchange_after_reload-qwen.fish.tmpl
@@ -0,0 +1,3 @@
+#!/usr/bin/env fish
+
+qwen-bootstrap
diff --git a/scripts/test-bitwarden-templates.py b/scripts/test-bitwarden-templates.py
new file mode 100644
index 0000000..166bfee
--- /dev/null
+++ b/scripts/test-bitwarden-templates.py
@@ -0,0 +1,114 @@
+"""Exercise real chezmoi diff/apply with an isolated home and fake Bitwarden."""
+
+import json
+import os
+from pathlib import Path
+import shutil
+import subprocess
+import tempfile
+import unittest
+
+
+REPO = Path(__file__).resolve().parents[1]
+TEMPLATES = {
+ "private_dot_ssh/config.tmpl": ".ssh/config",
+ "dot_config/private_ngrok/private_ngrok.yml.tmpl": ".config/ngrok/ngrok.yml",
+ "Library/Application Support/private_ngrok/private_ngrok.yml.tmpl":
+ "Library/Application Support/ngrok/ngrok.yml",
+}
+
+
+class BitwardenTemplatesTest(unittest.TestCase):
+ def setUp(self):
+ temporary = tempfile.TemporaryDirectory()
+ self.addCleanup(temporary.cleanup)
+ self.root = Path(temporary.name)
+ self.source = self.root / "source"
+ self.destination = self.root / "home"
+ self.destination.mkdir()
+ for relative in [".chezmoiignore", *TEMPLATES]:
+ target = self.source / relative
+ target.parent.mkdir(parents=True, exist_ok=True)
+ shutil.copyfile(REPO / relative, target)
+ (self.source / "public.txt").write_text("public config\n")
+ (self.root / "config.toml").write_text("")
+ self.bin = self.root / "bin"
+ self.bin.mkdir()
+ bw = self.bin / "bw"
+ bw.write_text("""#!/bin/sh
+printf 'called\n' >> "$BW_TEST_CALLS"
+if [ "$#" -ne 4 ] || [ "$1" != get ] || [ "$2" != notes ] || [ "$4" != --nointeraction ]; then
+ echo 'Unexpected interactive Bitwarden lookup' >&2
+ exit 2
+fi
+if [ "$BW_SESSION" != test-unlocked ]; then
+ echo 'Vault is locked.' >&2
+ exit 1
+fi
+case "$3" in
+ ssh-test) printf 'Host private-test\n HostName example.invalid\n' ;;
+ ngrok-test) printf 'test-ngrok-token' ;;
+ *) exit 3 ;;
+esac
+""")
+ bw.chmod(0o755)
+ self.calls = self.root / "calls"
+
+ def chezmoi(self, command, platform, session=None):
+ environment = dict(os.environ)
+ environment.pop("BW_SESSION", None)
+ if session is not None:
+ environment["BW_SESSION"] = session
+ environment["PATH"] = str(self.bin) + os.pathsep + environment["PATH"]
+ environment["BW_TEST_CALLS"] = str(self.calls)
+ return subprocess.run(
+ ["chezmoi", "--config", str(self.root / "config.toml"),
+ "--source", str(self.source), "--destination", str(self.destination),
+ "--cache", str(self.root / "cache"),
+ "--persistent-state", str(self.root / "state.boltdb"),
+ "--override-data", json.dumps({
+ "chezmoi": {"os": platform},
+ "ssh": {"bitwardenItem": "ssh-test"},
+ "ngrok": {"bitwardenItem": "ngrok-test"},
+ }), "--no-pager", "--no-tty", command],
+ env=environment, stdin=subprocess.DEVNULL, capture_output=True,
+ text=True, timeout=15,
+ )
+
+ def test_no_session_skips_secrets_and_preserves_existing_files(self):
+ for relative in TEMPLATES.values():
+ target = self.destination / relative
+ target.parent.mkdir(parents=True, exist_ok=True)
+ target.write_text("existing private config\n")
+ for platform in ["darwin", "linux"]:
+ for session in [None, ""]:
+ with self.subTest(platform=platform, session=session):
+ for command in ["diff", "apply"]:
+ result = self.chezmoi(command, platform, session)
+ self.assertEqual(result.returncode, 0, result.stderr)
+ self.assertFalse(self.calls.exists())
+ for relative in TEMPLATES.values():
+ self.assertEqual((self.destination / relative).read_text(),
+ "existing private config\n")
+ self.assertEqual((self.destination / "public.txt").read_text(), "public config\n")
+
+ def test_unlocked_session_renders_secrets(self):
+ for platform in ["darwin", "linux"]:
+ with self.subTest(platform=platform):
+ result = self.chezmoi("diff", platform, "test-unlocked")
+ self.assertEqual(result.returncode, 0, result.stderr)
+ self.assertIn("Host private-test", result.stdout)
+ self.assertEqual(result.stdout.count("authtoken: test-ngrok-token"),
+ 2 if platform == "darwin" else 1)
+
+ def test_invalid_session_fails_without_prompting(self):
+ for platform in ["darwin", "linux"]:
+ with self.subTest(platform=platform):
+ result = self.chezmoi("diff", platform, "test-invalid")
+ self.assertNotEqual(result.returncode, 0)
+ self.assertIn("Vault is locked.", result.stderr)
+ self.assertNotIn("interactive Bitwarden lookup", result.stderr)
+
+
+if __name__ == "__main__":
+ unittest.main()
diff --git a/wiki/Bitwarden-SSH.md b/wiki/Bitwarden-SSH.md
index 72f8cc0..0de4f4a 100644
--- a/wiki/Bitwarden-SSH.md
+++ b/wiki/Bitwarden-SSH.md
@@ -46,7 +46,7 @@ Use the printed UUID as `ssh.bitwardenItem` if name lookup is unreliable.
Bitwarden item notes:
```gotemplate
-{{ output "bw" "get" "notes" $bitwardenItem }}
+{{ output "bw" "get" "notes" $bitwardenItem "--nointeraction" }}
```
Verify rendering without applying:
diff --git a/wiki/Install-Script-Testing.md b/wiki/Install-Script-Testing.md
index e5eca9f..041e58f 100644
--- a/wiki/Install-Script-Testing.md
+++ b/wiki/Install-Script-Testing.md
@@ -27,6 +27,24 @@ just check
`cargo fmt --check` and `actionlint` separately when touching Rust formatting or
workflow YAML.
+Run the Bitwarden template regression tests separately:
+
+```bash
+just test-templates
+```
+
+These require Python 3 and chezmoi. They run real `chezmoi diff` and `apply`
+commands against temporary directories with a fake `bw` executable. They check
+macOS and Linux behavior for missing, empty, unlocked, and invalid sessions,
+including preservation of existing private files when no session is exported.
+They do not read the real vault or modify the home directory.
+
+To validate the installer package schema, run from the repository root:
+
+```bash
+cargo test --locked --manifest-path installer/Cargo.toml
+```
+
## Rebuild Binaries
Build the local platform binary:
@@ -115,6 +133,22 @@ docker build -f Dockerfile.arch-bootstrap-test-ci -t dotfiles-arch-bootstrap-tes
docker run --rm -v "$PWD:/work:ro" dotfiles-arch-bootstrap-test-ci
```
+### Apple Silicon Docker
+
+The Arch image used by these tests requires x86_64. On Apple Silicon, select
+that platform for both image builds and container runs:
+
+```bash
+DOCKER_DEFAULT_PLATFORM=linux/amd64 just test-arch-ci
+DOCKER_DEFAULT_PLATFORM=linux/amd64 just test-arch-bootstrap-ci
+```
+
+Local runs on Apple Silicon have failed during image setup with
+`error restricting syscalls via seccomp: 22` and
+`switching to sandbox user 'alpm' failed`. This happens before the installer
+runs. If encountered, run the Arch tests on an x86_64 Linux Docker host;
+`just check` and `just test-templates` can still run locally.
+
## macOS
The macOS installer supports Apple Silicon only. It rejects Intel macOS.
diff --git a/wiki/Installed-Tools.md b/wiki/Installed-Tools.md
index 882bee2..2afba23 100644
--- a/wiki/Installed-Tools.md
+++ b/wiki/Installed-Tools.md
@@ -9,6 +9,8 @@ runtimes, diagnostics, and media/document utilities.
- `fish`: default interactive shell.
- `functions`: list loaded functions.
- `funced name`: edit a fish function.
+- `atuin`: shell history search, installed on Arch/CachyOS and macOS.
+ - Initialized by the fish config in interactive sessions.
- `zellij`: terminal workspace/session manager.
- `zellij`: start a session.
- `zellij list-sessions`: show sessions.
@@ -18,7 +20,6 @@ runtimes, diagnostics, and media/document utilities.
- `z foo`: jump to a frequently used path matching `foo`.
- `zi`: interactive jump.
- `fzf`: fuzzy finder used directly and by other tools.
- - `Ctrl-r`: fuzzy shell history search.
- `find . -type f | fzf`: pick a file.
- `lsd`: nicer `ls`.
- `ls -la`: detailed listing.
@@ -38,6 +39,8 @@ runtimes, diagnostics, and media/document utilities.
- `nvim .`: open a project.
- `:Lazy`: plugin UI.
- `:Mason`: language/tool installer UI.
+- `zed`: graphical editor, installed on Arch/CachyOS and macOS.
+ - The fish config sets `EDITOR` and `VISUAL` to `zed`.
- `ripgrep` (`rg`): fast text search.
- `rg "text"`: search recursively.
- `rg -n "text" path`: include line numbers.
@@ -48,6 +51,7 @@ runtimes, diagnostics, and media/document utilities.
- `just`: command runner for this repo.
- `just --list`: show recipes.
- `just check`: local checks.
+ - `just test-templates`: isolated Bitwarden template checks without vault access.
- `just test-arch-ci`: run installer test container.
- `actionlint`: validate GitHub Actions workflows.
- `actionlint .github/workflows/install-script.yml`.
@@ -125,7 +129,13 @@ runtimes, diagnostics, and media/document utilities.
## Platform Notes
+- Both platforms install Atuin and Zed through their native package lists:
+ pacman packages on Arch/CachyOS; an Atuin formula and Zed cask on macOS.
- Arch/CachyOS also installs `base-devel`, `wl-clipboard`, `xclip`, Linux
Docker/Tailscale packages, and the JetBrains Mono Nerd Font package.
- macOS installs Homebrew formulae plus optional casks: Docker Desktop,
- Tailscale, and JetBrains Mono Nerd Font. Casks are skipped in CI.
+ Tailscale, JetBrains Mono Nerd Font, ngrok, UTM, iTerm2, Zed, and Raycast.
+ Raycast is included only on macOS. Set `INSTALL_CASKS=0` to skip casks, as
+ the macOS CI job does; they are enabled by default.
+- The macOS `Chezmoi` iTerm2 profile uses fish as a login shell, JetBrains Mono
+ Nerd Font, and Nord dark colors matching the Alacritty config.
diff --git a/wiki/Local-AI.md b/wiki/Local-AI.md
new file mode 100644
index 0000000..ea06ae1
--- /dev/null
+++ b/wiki/Local-AI.md
@@ -0,0 +1,23 @@
+# Local AI
+
+## MLX
+
+MLX is an array framework for efficient and flexible machine learning on Apple silicon. Basically, it allows us to use LLMs more efficiently on Apple silicon.
+
+You should have a few useful fish functions to get started:
+
+```bash
+qwen-bootstrap # Load changed plist files to launchagent
+qwen-light # Start lighter 30B Qwen Coder model
+qwen-heavy # Start heavier Next Coder model
+qwen-kill-all # Stop all models (they consume a lot of memory)
+```
+
+To debug problems:
+
+```bash
+curl http://127.0.0.1:8080/v1/models # Show loaded models
+launchctl print gui/(id -u)/local.mlx.coder-next # Print info about the coder next, see plist files for the names of the other models
+cat /tmp/mlx-coder-next-error.log # See error logs
+launchctl kickstart gui/$(id -u)/local.mlx.coder-next # Manualy start a model
+```
diff --git a/wiki/Setup.md b/wiki/Setup.md
index f3ef5b4..2bd2c09 100644
--- a/wiki/Setup.md
+++ b/wiki/Setup.md
@@ -37,7 +37,7 @@ Answer the prompts from `.chezmoi.toml.tmpl`:
```text
git.name -> git config user.name
git.email -> git config user.email
-git.signingkey -> optional git config user.signingkey
+git.signingKey -> optional git config user.signingKey
ssh.bitwardenItem -> Bitwarden item name or ID for private SSH host blocks
ngrok.bitwardenItem -> Bitwarden item name or ID for the ngrok authtoken
```
@@ -54,6 +54,41 @@ chezmoi apply --dry-run --verbose
chezmoi apply
```
+Without `BW_SESSION`, `chezmoi diff` and `chezmoi apply` skip `.ssh/config`,
+`.config/ngrok/ngrok.yml`, and `Library/Application Support/ngrok/ngrok.yml`.
+Existing files stay untouched. An invalid session produces a non-interactive
+Bitwarden error; unlock again and export the new session before retrying.
+
+## Update an existing machine
+
+Package additions in `installer/packages.toml` do not automatically rerun the
+run-once installer. From the configured fish shell, run:
+
+```fish
+dotsetup install
+```
+
+This installs packages from the current list, including Atuin and Zed on both
+supported platforms and Raycast on macOS. Package-list changes do not require
+rebuilding the committed installer binary.
+
+Run `chezmoi diff` and `chezmoi apply` to update the managed config files.
+
+## iTerm2 on macOS
+
+The managed dynamic profile is named `Chezmoi` and lives at
+`~/Library/Application Support/iTerm2/DynamicProfiles/chezmoi.json`.
+Its source is the corresponding `.json.tmpl` file in this repository.
+
+It launches `/opt/homebrew/bin/fish --login` on Apple Silicon, uses JetBrains
+Mono Nerd Font at size 13, and applies Nord dark colors matching Alacritty.
+The profile keeps the dark palette in both light and dark macOS appearances.
+The template also renders `/usr/local/bin/fish --login` on Intel Macs, although
+the package installer supports only Apple Silicon macOS.
+
+The run-once iTerm2 script selects this profile as the default. After applying
+profile updates, open a new session with the `Chezmoi` profile to use them.
+
## Bootstrap script
`run_once_install-packages.sh.tmpl` is a chezmoi script. Chezmoi runs
diff --git a/wiki/Template-Verification.md b/wiki/Template-Verification.md
index 7f3691b..f509875 100644
--- a/wiki/Template-Verification.md
+++ b/wiki/Template-Verification.md
@@ -6,7 +6,7 @@ Render with explicit test data:
```bash
chezmoi execute-template \
- --override-data '{"git":{"name":"Alice Example","email":"alice@example.com","signingkey":""}}' \
+ --override-data '{"git":{"name":"Alice Example","email":"alice@example.com","signingKey":""}}' \
--file dot_gitconfig.tmpl
```
@@ -47,6 +47,15 @@ cargo check
## Full dry run
+Without `BW_SESSION`, chezmoi skips the SSH config and both ngrok config
+locations, leaving existing files untouched. Unlock Bitwarden and export
+`BW_SESSION` to include them in the diff. Secret lookups use `--nointeraction`
+so an invalid session fails without trying to prompt from a template (which
+can cause Bitwarden's `ERR_USE_AFTER_CLOSE: readline was closed` error).
+
+Run the isolated Bitwarden template regression checks with `just test-templates`.
+These use a fake `bw` command and do not access your vault.
+
```bash
chezmoi diff
chezmoi apply --dry-run --verbose