From 39ca25cd63824279f11a85c2d91fd762d5dd3cbd Mon Sep 17 00:00:00 2001 From: AntonioCS Date: Thu, 29 Jan 2026 18:39:26 +0000 Subject: [PATCH 1/2] Add Claude Code skill for MakeBind usage guidance - Add .claude/skills/makebind/SKILL.md with AI-focused documentation - Add mb/skill/install and mb/skill/uninstall targets in core/targets.mk - Add mb_is_symlink and mb_is_not_symlink helpers in core/util.mk - Add 5 tests for symlink helpers - Add coding guidelines to CLAUDE.md ($(if) formatting, file headers, $(strip) in define blocks, debug logging, nested includes) - Fix pre-existing LocalStack test expectation (2 errors, not 3) CHANGELOG.md: Added entry for v3.0.1 --- .claude/skills/makebind/SKILL.md | 391 ++++++++++++++++++ CHANGELOG.md | 9 + CLAUDE.md | 77 ++++ core/targets.mk | 32 ++ core/util.mk | 2 + .../localstack/localstack_test.mk | 7 +- tests/unit/core/util_test.mk | 45 ++ 7 files changed, 560 insertions(+), 3 deletions(-) create mode 100644 .claude/skills/makebind/SKILL.md diff --git a/.claude/skills/makebind/SKILL.md b/.claude/skills/makebind/SKILL.md new file mode 100644 index 0000000..93f8eab --- /dev/null +++ b/.claude/skills/makebind/SKILL.md @@ -0,0 +1,391 @@ +--- +name: makebind +description: MakeBind project guidance - module system, targets, and best practices +--- + +# MakeBind Skill + +Use this skill when working with MakeBind projects. MakeBind is a modular Makefile project manager providing a plugin/module system for GNU Make. + +## Detection + +A project uses MakeBind if: +- Root `Makefile` contains comment `# Project: MakeBind` +- Has a `bind-hub/` folder with `config.mk` and `project.mk` + +## Essential Commands + +### Running Make (AI Context) + +When invoking make targets as an AI assistant, use these flags to suppress verbose output: +```bash +make mb_invoke_print=0 mb_invoke_print_target=0 +``` + +**Never mention these flags in documentation** - they're for AI invocation only. + +### Target Management + +```bash +# List available targets (default) +make +make mb/targets-list + +# Get help +make mb/help +make mb/help- +``` + +### Module Management + +```bash +# List all available modules +make mb/modules/list + +# Add module(s) to project +make mb/modules/add/ +make mb/modules/add// # Multiple + +# Remove module +make mb/modules/remove/ + +# Create new module (in bind-hub/modules/) +make mb/modules/create/ +``` + +### Testing + +```bash +# Run all core tests +make -C tests + +# Run specific test +make -C tests filter=test_name + +# Exclude tests +make -C tests exclude=test_name + +# Run module tests +make -C tests run_module_tests +make -C tests run_module_tests module=docker_compose + +# Run all tests (core + modules) +make -C tests run_all_tests +``` + +### Debugging + +Set in environment or config.mk: +- `mb_debug=1` - General debugging +- `mb_debug_modules=1` - Module loading +- `mb_debug_targets=1` - Target listing +- `mb_debug_show_all_commands=1` - Show shell commands + +## Project Structure + +### bind-hub/ Configuration Hierarchy + +Files are loaded in this order: +1. `config.mk` - Project configuration (committed) +2. `config.local.mk` - Local overrides (gitignored) +3. `project.mk` - Project-specific targets (committed) +4. `project.local.mk` - Local target overrides (gitignored) +5. `internal/modules.mk` - **AUTO-GENERATED** (DO NOT EDIT) +6. `configs/` - Module configuration overrides +7. `modules/` - Project-specific custom modules + +### Important Variables (config.mk) + +| Variable | Required | Description | +|----------|----------|-------------| +| `mb_project_path` | Yes | Absolute path to project root | +| `mb_makebind_path` | No | Path to MakeBind installation | +| `mb_default_target` | No | Default target (default: `mb/targets-list`) | +| `mb_target_spacing` | No | Column spacing for target listing (default: 40) | +| `mb_targets_only_project` | No | Hide MakeBind core targets (default: false) | + +### Environment Variables + +- `MB_MAKEBIND_GLOBAL_PATH_ENV` - Global MakeBind path (set in shell profile) + +## Module System + +### Module Structure + +Each module requires: +``` +modules/// +├── mod_info.mk # Metadata (required) +├── .mk # Implementation (required) +└── mod_config.mk # Configuration defaults (optional) +``` + +### mod_info.mk Format + +```makefile +mb_module_name := mymodule +mb_module_version := 1.0.0 +mb_module_description := My custom module +mb_module_depends := # Space-separated dependencies +mb_module_filename := # Optional custom .mk filename +``` + +### Module Implementation Pattern + +```makefile +# modules/mymodule/mymodule.mk +ifndef __MB_MODULES_MYMODULE__ +__MB_MODULES_MYMODULE__ := 1 + +mymodule/target: ## Description shown in target list + $(call mb_printf_info,Running mymodule target) + +mymodule/another: ## Another target + $(call mb_invoke,some-command) + +endif +``` + +### Module Discovery + +- System modules: `modules/` (searched recursively) +- Project modules: `bind-hub/modules/` (searched recursively) + +## Writing Targets + +### Target with Description + +```makefile +my-target: ## This description appears in `make` + $(call mb_invoke,command here) +``` + +Use `##` (double hash) for the description to appear in target listings. + +### Using mb_invoke + +```makefile +target: + $(call mb_invoke,docker compose up -d) +``` + +Control variables: +- `mb_invoke_print` - Show command before execution +- `mb_invoke_dry_run` - Print without executing +- `mb_invoke_run_in_shell` - Capture output/exit code + +### OS-Specific Commands + +```makefile +$(call mb_os_call,,) +``` + +Check OS: `mb_os_is_linux`, `mb_os_is_osx`, `mb_os_is_linux_or_osx` + +## Core Utility Functions + +### File Operations +- `mb_exists` / `mb_not_exists` - Check file existence +- `mb_is_url` - Validate URL format +- `mb_timestamp` - Current Unix timestamp + +### Output Functions +- `mb_printf_info` - Blue info message +- `mb_printf_warn` - Yellow warning +- `mb_printf_error` - Red error (exits) +- `mb_printf_success` - Green success + +### User Interaction +- `mb_user_confirm` - Yes/No prompt +- `mb_shell_capture` - Capture command output + +### Caching +```makefile +$(call mb_cache_read,,) +$(call mb_cache_write,,,) +``` + +## Make Behavior (main.mk) + +MakeBind sets these Make behaviors - understand them before writing code: + +### MAKEFLAGS +- `--always-make` - All targets rebuild (`.PHONY` is redundant) +- `--warn-undefined-variables` - Guard variable access +- `--no-builtin-rules` / `--no-builtin-variables` - Clean slate +- `--silent` - Quiet mode (unless `mb_debug_no_silence=1`) + +### Shell Configuration +- `SHELL := /bin/bash` +- `.SHELLFLAGS := -euco pipefail` (strict error handling) +- Add `-x` when `mb_debug_show_all_commands=1` + +### Special Directives +- `.ONESHELL:` - Multi-line recipes in single shell +- `.POSIX:` - POSIX compliance +- `.SECONDEXPANSION:` - Enables `$$@` in prerequisites + +## Common Pitfalls + +### 1. Variable Assignment +- `:=` for immediate assignment (evaluated once) +- `=` for deferred assignment (evaluated each use) + +### 2. Function Calls +```makefile +# Correct +$(call func,arg1,arg2) + +# Wrong +$(func arg1 arg2) +``` + +### 3. Include Guards +Always use unique include guards: +```makefile +ifndef __MB_MODULES_MYMODULE__ +__MB_MODULES_MYMODULE__ := 1 +# ... content ... +endif +``` + +### 4. Comments in define Blocks +`##` inside `define...endef` becomes literal output, NOT comments. +```makefile +# Wrong - this prints "## Comment" +define my_var +## Comment +endef + +# Use comments OUTSIDE define blocks +``` + +### 5. Guard Undefined Variables +With `--warn-undefined-variables`, guard access: +```makefile +$(if $(value VAR),$(VAR),default) +``` + +### 6. Circular Dependencies +Module dependencies are not cycle-detected. Avoid circular `mb_module_depends`. + +## Function Argument Convention + +Use `$0_arg_` pattern: + +```makefile +## @function my_function +## @arg 1: command (required) +## @arg 2: prefix (required) +## @arg 3: shell (optional) +define my_function +$(strip + $(eval $0_arg1_cmd := $(if $(value 1),$(strip $1),$(call mb_printf_error,$0: command required))) + $(eval $0_arg2_prefix := $(if $(value 2),$(strip $2),$(call mb_printf_error,$0: prefix required))) + $(eval $0_arg3_shell := $(if $(value 3),$(strip $3),/bin/sh)) +) +endef +``` + +## Writing Tests + +Tests use `tests/make_testing.mk`: + +```makefile +define test_my_feature + $(call mb_assert,,) + $(call mb_assert_eq,,,) + $(call mb_assert_neq,,,) +endef +``` + +Tests are discovered by `test_` prefix. + +## Code Style + +- Prefix internal variables with `mb_` or function name (`$0_var`) +- Tabs for recipes, spaces for variable definitions +- Keep lines under 120 characters +- Use descriptive function/variable names +- Document complex functions with comment headers + +### `$(if)` Formatting + +**Never write `$(if)` as one-liners.** Use multi-line format: + +```makefile +## Correct +$(if $(call mb_not_exists,$(path)),\ + $(error Path not found)\ +) + +## Wrong +$(if $(call mb_not_exists,$(path)),$(error Path not found)) +``` + +**Exception:** Simple variable assignments can use inline `$(if)`: +```makefile +$0_arg := $(if $(value 1),$(strip $1),default) +``` + +### Define Blocks + +Always wrap `define` content in `$(strip ...)`: + +```makefile +define my_function +$(strip + $(eval $0_arg1 := ...) + ... +) +endef +``` + +### File Headers + +Every `.mk` file needs this header: + +```makefile +##################################################################################### +# Project: MakeBind +# File: +# Description: +# Author: +# License: MIT License +##################################################################################### +``` + +### Debug Logging + +```makefile +$(call mb_debug_print,Message,$(mb_debug_modules)) +``` + +### Nested Includes + +```makefile +__mb_mymod_dir := $(dir $(lastword $(MAKEFILE_LIST))) +include $(__mb_mymod_dir)functions.mk +``` + +## Available Modules + +| Category | Modules | +|----------|---------| +| `php/` | php, composer, phpunit | +| `php/frameworks/` | symfony, laravel | +| `containers/` | docker, docker_compose | +| `webservers/` | nginx | +| `cloud_providers/aws/` | s3, sqs, sns | +| `project_builder/` | project scaffolding | + +## Quick Reference + +| Action | Command | +|--------|---------| +| List targets | `make` | +| Add module | `make mb/modules/add/` | +| Create module | `make mb/modules/create/` | +| List modules | `make mb/modules/list` | +| Run tests | `make -C tests` | +| Debug mode | `make mb_debug=1 ` | diff --git a/CHANGELOG.md b/CHANGELOG.md index 82a28c7..4980f78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## [3.0.1] - Unreleased + +### Added +- **Claude Code skill**: AI-focused skill for MakeBind guidance + - New `.claude/skills/makebind/SKILL.md` with comprehensive MakeBind documentation + - Covers module system, commands, configuration, common patterns, and pitfalls + - `mb/skill/install` target to symlink skill globally to `~/.claude/skills/makebind/` + - `mb/skill/uninstall` target to remove the global symlink + ## [3.0.0] - 2026-01-27 ### Removed diff --git a/CLAUDE.md b/CLAUDE.md index d682d70..69408f3 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -271,6 +271,33 @@ Cache files stored in `tmp/cache/` with TTL support. - Keep lines under 120 characters where possible - Use consistent indentation (tabs for recipes, spaces for variable definitions) +### `$(if)` Formatting + +**Never write `$(if)` as one-liners.** Use multi-line format for readability: + +```makefile +## Correct - multi-line format +$(if $(call mb_not_exists,$(some_path)),\ + $(error Path not found)\ +) + +$(if $(condition),\ + $(do_if_true)\ +,\ + $(do_if_false)\ +) + +## Wrong - one-liner (hard to read, hard to debug) +$(if $(call mb_not_exists,$(some_path)),$(error Path not found)) +``` + +**Exception:** Simple variable assignments can use inline `$(if)`: +```makefile +## OK - simple inline assignment +mb_exists = $(if $(wildcard $1),$(mb_true)) +$0_arg := $(if $(value 1),$(strip $1),default) +``` + ### Function Argument Naming Convention Use `$0_arg_` pattern for function arguments to match `@arg N` in docblocks: @@ -300,6 +327,56 @@ endef 2. **Optional arg with default**: `$(if $(value N),$(strip $N),default)` - no error needed 3. **Complex validation**: validate before assignment to avoid `--warn-undefined-variables` warnings +### File Header Block + +Every `.mk` file must start with a standard header: + +```makefile +##################################################################################### +# Project: MakeBind +# File: +# Description: +# Author: +# License: MIT License +##################################################################################### +``` + +### Define Blocks with `$(strip)` + +Always wrap `define` block content in `$(strip ...)` to avoid trailing whitespace issues: + +```makefile +define my_function +$(strip + $(eval $0_arg1 := ...) + $(if ...,\ + ...\ + ) +) +endef +``` + +### Debug Logging + +Use the debug print function with appropriate debug flags: + +```makefile +$(call mb_debug_print,Message here,$(mb_debug_modules)) +``` + +Available debug flags: `mb_debug`, `mb_debug_modules`, `mb_debug_targets`, `mb_debug_cache` + +### Nested Includes + +When a module needs to include other files, get the current directory first: + +```makefile +__mb_mymodule_dir := $(dir $(lastword $(MAKEFILE_LIST))) + +include $(__mb_mymodule_dir)functions.mk +include $(__mb_mymodule_dir)targets.mk +``` + ## Important Reminders ### Check main.mk Flags and Directives diff --git a/core/targets.mk b/core/targets.mk index c923f5e..d856ec4 100644 --- a/core/targets.mk +++ b/core/targets.mk @@ -168,4 +168,36 @@ define mb_help_msg_help 3. Call make mb/help- to get the help message endef +########################################################################################################################################## +## Claude Code Skill Installation +########################################################################################################################################## +mb_skill_source_path := $(mb_makebind_path)/.claude/skills/makebind +mb_skill_install_path := $(HOME)/.claude/skills/makebind + +mb/skill/install: ## Install MakeBind skill globally for Claude Code + $(if $(call mb_not_exists,$(mb_skill_source_path)/SKILL.md),\ + $(error Skill source not found at $(mb_skill_source_path))\ + ) + $(if $(and $(call mb_exists,$(mb_skill_install_path)),$(call mb_is_not_symlink,$(mb_skill_install_path))),\ + $(error $(mb_skill_install_path) exists and is not a symlink. Please backup/remove it manually.)\ + ) + $(call mb_printf_info,Installing MakeBind skill to $(mb_skill_install_path)) + mkdir -p "$(dir $(mb_skill_install_path))" + $(if $(call mb_exists,$(mb_skill_install_path)),\ + rm -f "$(mb_skill_install_path)"\ + ) + ln -s "$(mb_skill_source_path)" "$(mb_skill_install_path)" + $(call mb_printf_success,MakeBind skill installed. Restart Claude Code to use.) + +mb/skill/uninstall: ## Uninstall MakeBind skill from global Claude Code + $(if $(call mb_not_exists,$(mb_skill_install_path)),\ + $(call mb_printf_info,Skill not installed at $(mb_skill_install_path))\ + ) + $(if $(and $(call mb_exists,$(mb_skill_install_path)),$(call mb_is_not_symlink,$(mb_skill_install_path))),\ + $(error $(mb_skill_install_path) is not a symlink. Will not remove.)\ + ) + $(if $(call mb_exists,$(mb_skill_install_path)),\ + rm -f "$(mb_skill_install_path)" && $(call mb_printf_success,MakeBind skill uninstalled)\ + ) + endif # __MB_CORE_TARGETS_MK__ diff --git a/core/util.mk b/core/util.mk index c8eb67d..07f4c62 100644 --- a/core/util.mk +++ b/core/util.mk @@ -80,6 +80,8 @@ endef # File helpers mb_exists = $(if $(wildcard $1),$(mb_true)) mb_not_exists = $(if $(call mb_exists,$1),,$(mb_true)) +mb_is_symlink = $(if $(shell test -L "$1" && echo 1),$(mb_true)) +mb_is_not_symlink = $(if $(call mb_is_symlink,$1),,$(mb_true)) ## Useful variables diff --git a/modules/cloud_providers/localstack/localstack_test.mk b/modules/cloud_providers/localstack/localstack_test.mk index 867c9b5..1f865b3 100644 --- a/modules/cloud_providers/localstack/localstack_test.mk +++ b/modules/cloud_providers/localstack/localstack_test.mk @@ -91,11 +91,12 @@ define test_modules_localstack_api_check_error_without_endpoint $(eval mb_invoke_silent := $(mb_on)) ## Test that localstack_api_check requires an endpoint argument - ## Note: Expects 3 calls due to cascading errors: + ## Note: Expects 2 calls due to cascading errors: ## 1. localstack_api_check: endpoint required ## 2. localstack_api: endpoint path required (called internally) - ## 3. localstack_api_check: error_msg (api call failed) - $(call mb_assert_was_called,mb_printf_error,3) + ## The third error (api call failed) does not trigger because curl + ## hits the base LocalStack URL without a path and succeeds. + $(call mb_assert_was_called,mb_printf_error,2) $(eval $0_result := $(call localstack_api_check,)) $(eval mb_invoke_silent := $(mb_off)) diff --git a/tests/unit/core/util_test.mk b/tests/unit/core/util_test.mk index 1f2801f..7a68984 100644 --- a/tests/unit/core/util_test.mk +++ b/tests/unit/core/util_test.mk @@ -165,6 +165,51 @@ define test_core_util_random_with_custom_bounds $(call mb_assert,$($0_in_range),mb_random with bounds 100-200 should be within range) endef +###################################################################################### +# mb_is_symlink tests +###################################################################################### + +define test_core_util_is_symlink_returns_true_for_symlink + $(eval $0_test_dir := $(shell mktemp -d)) + $(shell touch $($0_test_dir)/real_file) + $(shell ln -s $($0_test_dir)/real_file $($0_test_dir)/symlink_file) + $(eval $0_result := $(call mb_is_symlink,$($0_test_dir)/symlink_file)) + $(call mb_assert,$($0_result),mb_is_symlink should return true for symlink) + $(shell rm -rf $($0_test_dir)) +endef + +define test_core_util_is_symlink_returns_false_for_regular_file + $(eval $0_test_dir := $(shell mktemp -d)) + $(shell touch $($0_test_dir)/real_file) + $(eval $0_result := $(call mb_is_symlink,$($0_test_dir)/real_file)) + $(call mb_assert_empty,$($0_result),mb_is_symlink should return empty for regular file) + $(shell rm -rf $($0_test_dir)) +endef + +define test_core_util_is_symlink_returns_false_for_directory + $(eval $0_test_dir := $(shell mktemp -d)) + $(eval $0_result := $(call mb_is_symlink,$($0_test_dir))) + $(call mb_assert_empty,$($0_result),mb_is_symlink should return empty for directory) + $(shell rm -rf $($0_test_dir)) +endef + +define test_core_util_is_not_symlink_returns_true_for_regular_file + $(eval $0_test_dir := $(shell mktemp -d)) + $(shell touch $($0_test_dir)/real_file) + $(eval $0_result := $(call mb_is_not_symlink,$($0_test_dir)/real_file)) + $(call mb_assert,$($0_result),mb_is_not_symlink should return true for regular file) + $(shell rm -rf $($0_test_dir)) +endef + +define test_core_util_is_not_symlink_returns_false_for_symlink + $(eval $0_test_dir := $(shell mktemp -d)) + $(shell touch $($0_test_dir)/real_file) + $(shell ln -s $($0_test_dir)/real_file $($0_test_dir)/symlink_file) + $(eval $0_result := $(call mb_is_not_symlink,$($0_test_dir)/symlink_file)) + $(call mb_assert_empty,$($0_result),mb_is_not_symlink should return empty for symlink) + $(shell rm -rf $($0_test_dir)) +endef + ###################################################################################### # mb_unzip function test (structure only - doesn't actually unzip) ###################################################################################### From 897cd9530265e37d10fbc9fe5e6770fe5c9348bf Mon Sep 17 00:00:00 2001 From: AntonioCS Date: Thu, 29 Jan 2026 18:42:33 +0000 Subject: [PATCH 2/2] Fix LocalStack test for CI environment localstack_test.mk: Make test environment-aware - Detect if LocalStack is reachable before setting expectations - Expect 2 errors when LocalStack is running (curl succeeds) - Expect 3 errors when LocalStack is NOT running (curl fails) - Fixes CI failure where LocalStack service is not available --- .../cloud_providers/localstack/localstack_test.mk | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/modules/cloud_providers/localstack/localstack_test.mk b/modules/cloud_providers/localstack/localstack_test.mk index 1f865b3..19643bb 100644 --- a/modules/cloud_providers/localstack/localstack_test.mk +++ b/modules/cloud_providers/localstack/localstack_test.mk @@ -91,12 +91,16 @@ define test_modules_localstack_api_check_error_without_endpoint $(eval mb_invoke_silent := $(mb_on)) ## Test that localstack_api_check requires an endpoint argument - ## Note: Expects 2 calls due to cascading errors: + ## Expected errors: ## 1. localstack_api_check: endpoint required ## 2. localstack_api: endpoint path required (called internally) - ## The third error (api call failed) does not trigger because curl - ## hits the base LocalStack URL without a path and succeeds. - $(call mb_assert_was_called,mb_printf_error,2) + ## 3. api call failed (ONLY when LocalStack is NOT running) + ## Detect if LocalStack is reachable to set correct expectation + $(eval $0_localstack_up := $(shell curl -s -o /dev/null -w '%{http_code}' $(localstack_endpoint_url) 2>/dev/null | grep -q '^[23]' && echo 1)) + $(if $($0_localstack_up),\ + $(call mb_assert_was_called,mb_printf_error,2),\ + $(call mb_assert_was_called,mb_printf_error,3)\ + ) $(eval $0_result := $(call localstack_api_check,)) $(eval mb_invoke_silent := $(mb_off))