Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# AI
.claude

# Local binaries list (machine-specific)
.local-binaries.txt
.devenv.nix
objectstore

Expand Down
4 changes: 0 additions & 4 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions home-manager/modules/default.nix
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
[
./local-binaries
./npm-globals
./tailscale
./yek
Expand Down
10 changes: 10 additions & 0 deletions home-manager/modules/local-binaries/default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{ config, pkgs, ... }:
{
# Create ~/.local/bin directory
home.file.".local/bin/.keep".text = "";

# Symlink local binaries during activation
home.activation.symlinkLocalBinaries = config.lib.dag.entryAfter [ "writeBoundary" ] ''
$DRY_RUN_CMD ${pkgs.bash}/bin/bash ${./sync-local-binaries.sh}
'';
Comment on lines +7 to +9

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

No error handling for sync script failure.

If sync-local-binaries.sh encounters an error (despite set -e), the activation continues silently. Users won't be notified of synchronization failures.

Consider whether script failures should:

  1. Fail the entire activation (current behavior with -e)
  2. Log errors but continue (remove -e, add explicit error logging)
  3. Provide a configuration option for this behavior

The current approach with set -euo pipefail will fail activation on any error, which may be too strict if a single invalid binary prevents the entire home-manager switch.

}
Comment on lines +1 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Module lacks proper option declarations and documentation.

Per home-manager module structure guidelines, custom modules should include:

  1. An options block with typed declarations
  2. An enable flag to allow users to opt in/out
  3. Configurable paths instead of hardcoded values
  4. Module documentation

Currently, the module always activates without user control and has no configuration options.

As per coding guidelines: "Custom modules should include proper option types and document all options" and "Each module in home-manager/modules/ should have a clear default.nix with proper option declarations following the home-manager module structure."

🔎 Proposed module structure
{ config, lib, pkgs, ... }:

with lib;

let
  cfg = config.programs.local-binaries;
in
{
  options.programs.local-binaries = {
    enable = mkEnableOption "local binaries synchronization";

    binariesFile = mkOption {
      type = types.str;
      default = "${config.home.homeDirectory}/dotfiles/.local-binaries.txt";
      description = "Path to the file containing binary paths to symlink";
    };

    binDir = mkOption {
      type = types.str;
      default = "${config.home.homeDirectory}/.local/bin";
      description = "Directory where symlinks will be created";
    };
  };

  config = mkIf cfg.enable {
    # Create bin directory
    home.file."${cfg.binDir}/.keep".text = "";

    # Symlink local binaries during activation
    home.activation.symlinkLocalBinaries = 
      config.lib.dag.entryAfter [ "writeBoundary" ] ''
        $DRY_RUN_CMD ${pkgs.bash}/bin/bash ${./sync-local-binaries.sh} \
          "${cfg.binariesFile}" "${cfg.binDir}"
      '';
  };
}

Note: This also requires updating sync-local-binaries.sh to accept parameters.

🤖 Prompt for AI Agents
In home-manager/modules/local-binaries/default.nix around lines 1 to 10, the
module is missing proper option declarations, an enable flag, configurable path
options, and documentation; update the file to define
options.programs.local-binaries with mkEnableOption "local binaries
synchronization", add mkOption entries for binariesFile (type str, default to a
file under config.home.homeDirectory) and binDir (type str, default to
${config.home.homeDirectory}/.local/bin) and descriptions, bind cfg =
config.programs.local-binaries and wrap the existing home.file and
home.activation entries in mkIf cfg.enable so the module only activates when
enabled, replace hardcoded paths with ${cfg.binDir} and pass
"${cfg.binariesFile}" and "${cfg.binDir}" as parameters to the
sync-local-binaries.sh invocation, and add a brief module description
block/documentation comment at top; also update sync-local-binaries.sh to accept
and use the two parameters.

47 changes: 47 additions & 0 deletions home-manager/modules/local-binaries/sync-local-binaries.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env bash

set -euo pipefail

# Sync local binaries from ~/.local-binaries.txt to ~/.local/bin
# Each line in the file should be an absolute path to a binary
# Lines starting with # are comments, empty lines are ignored

BINARIES_FILE="${HOME}/dotfiles/.local-binaries.txt"
BIN_DIR="${HOME}/.local/bin"
Comment on lines +9 to +10

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Hardcoded "dotfiles" directory name reduces portability.

The path ${HOME}/dotfiles/.local-binaries.txt assumes the repository is cloned to ~/dotfiles, which may not be true for all users or CI environments.

Consider one of these approaches:

  1. Make the path relative to the script location
  2. Accept the path as a parameter or environment variable
  3. Document this assumption clearly in the PR/README
🔎 Example: Use script location
-BINARIES_FILE="${HOME}/dotfiles/.local-binaries.txt"
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
+BINARIES_FILE="$REPO_ROOT/.local-binaries.txt"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
BINARIES_FILE="${HOME}/dotfiles/.local-binaries.txt"
BIN_DIR="${HOME}/.local/bin"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
BINARIES_FILE="$REPO_ROOT/.local-binaries.txt"
BIN_DIR="${HOME}/.local/bin"
🤖 Prompt for AI Agents
In home-manager/modules/local-binaries/sync-local-binaries.sh around lines 9-10
the path to .local-binaries.txt is hardcoded to
${HOME}/dotfiles/.local-binaries.txt which breaks portability; change the script
to resolve the file relative to the script location (e.g. determine SCRIPT_DIR
from $0 and use ${SCRIPT_DIR}/.local-binaries.txt) and also accept an override
via an environment variable or CLI parameter (e.g. LOCAL_BINARIES_FILE) so
callers/CI can specify a different location; update any docs/README to note the
new env/arg option if present.


# Exit if no binaries file exists
if [ ! -f "$BINARIES_FILE" ]; then
echo "No ${BINARIES_FILE} found, skipping local binaries sync"
exit 0
fi

# Ensure bin directory exists
mkdir -p "$BIN_DIR"

# Process each line in the binaries file
while IFS= read -r line || [ -n "$line" ]; do
# Trim leading and trailing whitespace
line="${line#"${line%%[![:space:]]*}"}"
line="${line%"${line##*[![:space:]]}"}"

# Skip empty lines
[ -z "$line" ] && continue

# Skip comments
case "$line" in
\#*) continue ;;
esac

# Check if binary exists and is executable
if [ ! -f "$line" ] || [ ! -x "$line" ]; then

@cubic-dev-ai cubic-dev-ai Bot Dec 29, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: Missing validation for absolute paths. The script documents that paths should be absolute but doesn't enforce it. A relative path that happens to resolve could create a broken symlink. Consider adding validation after the comment check:

case "$line" in
  /*) ;;
  *) echo "Skipping (not an absolute path): $line"; continue ;;
esac
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At home-manager/modules/local-binaries/sync-local-binaries.sh, line 36:

<comment>Missing validation for absolute paths. The script documents that paths should be absolute but doesn&#39;t enforce it. A relative path that happens to resolve could create a broken symlink. Consider adding validation after the comment check:
```bash
case &quot;$line&quot; in
  /*) ;;
  *) echo &quot;Skipping (not an absolute path): $line&quot;; continue ;;
esac
```</comment>

<file context>
@@ -0,0 +1,47 @@
+  esac
+
+  # Check if binary exists and is executable
+  if [ ! -f &quot;$line&quot; ] || [ ! -x &quot;$line&quot; ]; then
+    echo &quot;Skipping (not found or not executable): $line&quot;
+    continue
</file context>
Fix with Cubic

echo "Skipping (not found or not executable): $line"
continue
fi

# Get binary name and create symlink
bin_name="$(basename "$line")"
target="$BIN_DIR/$bin_name"

ln -sf "$line" "$target"
echo "Linked: $bin_name -> $line"
Comment on lines +41 to +46

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Basename conflicts are not detected or reported.

If multiple binaries in .local-binaries.txt have the same basename (e.g., /opt/tool/v1/binary and /opt/tool/v2/binary), the last one silently overwrites earlier symlinks without warning.

Consider warning users when basename conflicts occur:

🔎 Proposed fix
   # Get binary name and create symlink
   bin_name="$(basename "$line")"
   target="$BIN_DIR/$bin_name"
+  
+  if [ -L "$target" ] && [ "$(readlink "$target")" != "$line" ]; then
+    echo "Warning: $bin_name already links to $(readlink "$target"), overwriting with $line"
+  fi

   ln -sf "$line" "$target"
   echo "Linked: $bin_name -> $line"
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
# Get binary name and create symlink
bin_name="$(basename "$line")"
target="$BIN_DIR/$bin_name"
ln -sf "$line" "$target"
echo "Linked: $bin_name -> $line"
# Get binary name and create symlink
bin_name="$(basename "$line")"
target="$BIN_DIR/$bin_name"
if [ -L "$target" ] && [ "$(readlink "$target")" != "$line" ]; then
echo "Warning: $bin_name already links to $(readlink "$target"), overwriting with $line"
fi
ln -sf "$line" "$target"
echo "Linked: $bin_name -> $line"

done <"$BINARIES_FILE"
2 changes: 0 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@
"license": "ISC",
"packageManager": "bun@1.3.0",
"dependencies": {
"@beads/bd": "^0.39.0",
"@biomejs/biome": "^2.3.8",
"@ccusage/codex": "^17.2.0",
"@ccusage/mcp": "^17.2.0",
Expand All @@ -30,7 +29,6 @@
"typescript": "^5.9.3"
},
"trustedDependencies": [
"@beads/bd",
"@biomejs/biome",
"@ccusage/codex",
"@ccusage/mcp",
Expand Down
5 changes: 5 additions & 0 deletions spec/coverage_spec.sh
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,10 @@ End
It 'has spec file for scripts/update-gitalias.sh'
The path "spec/update_gitalias_spec.sh" should be exist
End

It 'has spec file for home-manager/modules/local-binaries/sync-local-binaries.sh'
The path "spec/local_binaries_spec.sh" should be exist
End
End

Describe 'no shell scripts are missing from coverage list'
Expand All @@ -81,6 +85,7 @@ covered_scripts="config/claude/notify.sh
config/claude/pushover.sh
config/claude/security.sh
config/claude/statusline-git.sh
home-manager/modules/local-binaries/sync-local-binaries.sh
home-manager/programs/neovim/run_tests.sh
home-manager/services/brew-upgrader/upgrade.sh
home-manager/services/cliproxyapi/scripts/backup-and-recover.sh
Expand Down
72 changes: 72 additions & 0 deletions spec/local_binaries_spec.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
#!/usr/bin/env bash
# shellcheck disable=SC2329

Describe 'local-binaries/sync-local-binaries.sh'
SCRIPT="$PWD/home-manager/modules/local-binaries/sync-local-binaries.sh"

Describe 'script properties'
It 'uses bash shebang'
When run bash -c "head -1 '$SCRIPT'"
The output should include '#!/usr/bin/env bash'
End

It 'uses strict mode'
When run bash -c "head -5 '$SCRIPT'"
The output should include 'set -euo pipefail'
End
End

Describe 'configuration'
It 'reads from dotfiles/.local-binaries.txt'
When run bash -c "grep 'BINARIES_FILE=' '$SCRIPT'"
The output should include 'dotfiles/.local-binaries.txt'
End

It 'writes to ~/.local/bin'
When run bash -c "grep 'BIN_DIR=' '$SCRIPT'"
The output should include '.local/bin'
End
End

Describe 'file handling'
It 'exits gracefully when binaries file is missing'
When run bash -c "grep -A 2 'if \[ ! -f' '$SCRIPT'"
The output should include 'exit 0'
End

It 'creates bin directory if needed'
When run bash -c "grep 'mkdir -p' '$SCRIPT'"
The output should include 'mkdir -p'
End
End

Describe 'line processing'
It 'skips empty lines'
When run bash -c "grep -E '\\[ -z' '$SCRIPT'"
The output should include 'continue'
End

It 'skips comment lines'
When run bash -c "grep -A 2 'Skip comments' '$SCRIPT'"
The output should include 'continue'
End

It 'checks if binary exists and is executable'
When run bash -c "grep '\[ ! -f' '$SCRIPT'"
The output should include '! -x'
End
End

Describe 'symlink creation'
It 'uses basename for symlink name'
When run bash -c "grep 'basename' '$SCRIPT'"
The output should include 'basename'
End

It 'creates symlinks with ln -sf'
When run bash -c "grep 'ln -sf' '$SCRIPT'"
The output should include 'ln -sf'
End
End

End
Comment on lines +4 to +72

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The tests in this file check the implementation details of the script by greping its source code, rather than testing its behavior. This makes the tests brittle; they can break even with valid refactoring that doesn't change the script's functionality.

For more robust and maintainable tests, I recommend switching to behavioral tests. You could use shellspec's features to:

  1. Set up a temporary HOME directory.
  2. Create a mock .local-binaries.txt file with various test cases (e.g., valid paths, paths with spaces, commented lines, non-existent files).
  3. Create dummy executable files.
  4. Run the script.
  5. Assert that the expected symlinks are created (or not created) in the temporary ~/.local/bin directory.

This approach tests what the script does, not how it's written, which is much more valuable.
Here's an example of what a behavioral test could look like:

Describe 'sync-local-binaries.sh behavior'
  setup() {
    # Set up a temporary environment for testing
    HOME="$SHELLSPEC_TMPDIR"
    export HOME
    BINARIES_FILE="$HOME/dotfiles/.local-binaries.txt"
    BIN_DIR="$HOME/.local/bin"
    mkdir -p "$(dirname "$BINARIES_FILE")"

    # Create a dummy executable
    DUMMY_BIN_PATH="$SHELLSPEC_TMPDIR/my-test-binary"
    echo '#!/bin/sh' > "$DUMMY_BIN_PATH"
    chmod +x "$DUMMY_BIN_PATH"
  }
  Before 'setup'

  It 'creates a symlink for a valid binary path'
    # Arrange
    echo "$DUMMY_BIN_PATH" > "$BINARIES_FILE"

    # Act
    When run "$SCRIPT"

    # Assert
    The status should be success
    The path "$BIN_DIR/my-test-binary" should be a symlink to "$DUMMY_BIN_PATH"
    The output should include "Linked: my-test-binary -> $DUMMY_BIN_PATH"
  End
End

Loading