diff --git a/.php-cs-fixer.php b/.php-cs-fixer.php
index 765404c..3f39878 100644
--- a/.php-cs-fixer.php
+++ b/.php-cs-fixer.php
@@ -8,6 +8,9 @@
$finder = Finder::create()
->in(__DIR__ . '/src')
- ->append([__DIR__ . '/.php-cs-fixer.php']);
+ ->append([
+ __DIR__ . '/bin/readme-examples-sync',
+ __DIR__ . '/.php-cs-fixer.php',
+ ]);
return PhpCsFixerConfigFactory::create(TicketSwapRuleSet::create())->setFinder($finder);
diff --git a/README.md b/README.md
index f635846..6d943cc 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
-# README Examples Sync Hook
+# README Examples Sync
-A [CaptainHook](https://github.com/captainhook-git/captainhook) action that automatically syncs PHP code examples in your README.md with actual source files. This ensures your documentation always shows up-to-date, working code examples.
+A PHP script that automatically syncs code examples in your README.md with actual source files.
+This ensures your documentation always shows up-to-date, working code examples.
## Installation
@@ -8,33 +9,67 @@ A [CaptainHook](https://github.com/captainhook-git/captainhook) action that auto
composer require --dev ruudk/readme-examples-sync-hook
```
-## Configuration
-
-Add the hook to your `captainhook.json` configuration file in the `pre-commit` section:
-
-```json
-{
- "pre-commit": {
- "enabled": true,
- "actions": [
- {
- "action": "\\Ruudk\\ReadmeExamplesSyncHook\\SyncReadmeExamples"
- }
- ]
- }
-}
+## Usage
+
+Run the script from your project root:
+
+```bash
+vendor/bin/readme-examples-sync
+```
+
+### Git Hook Integration
+
+The easiest way to automatically sync your README on commit is using [Lefthook](https://lefthook.dev):
+
+1. Install Lefthook (if not already installed):
+ ```bash
+ # macOS
+ brew install lefthook
+ ```
+
+2. Create a `lefthook.yml` file in your project root:
+ ```yaml
+ pre-commit:
+ parallel: false
+ commands:
+ sync-readme-examples:
+ glob:
+ - "*.php"
+ - "*.md"
+ run: vendor/bin/readme-examples-sync
+ stage_fixed: true
+ ```
+
+3. Install the hooks:
+ ```bash
+ lefthook install
+ ```
+
+That's it! Now your README will automatically sync whenever you commit changes to PHP or Markdown files.
+
+#### Alternative: Manual Git Hook
+
+If you prefer not to use Lefthook, you can manually create a `.git/hooks/pre-commit` file:
+
+```bash
+#!/bin/bash
+vendor/bin/readme-examples-sync
+```
+
+Don't forget to make the hook executable:
+
+```bash
+chmod +x .git/hooks/pre-commit
```
## How It Works
-This hook scans your README.md file for special HTML comments that mark code examples:
+This script scans your README.md file for special HTML comments that mark code examples:
1. **Source code sync**: Updates code blocks marked with `` with the actual content from source files
-2. **Output sync** (optional): When you add `` comments, the hook executes PHP files and captures their output to display results
+2. **Output sync** (optional): When you add `` comments, the script executes PHP files and captures their output to display results
-The hook automatically stages the updated README.md if changes are detected, ensuring your documentation stays in sync with your code.
-
-## Usage
+The script automatically stages the updated README.md if changes are detected (when run in a git repository), ensuring your documentation stays in sync with your code.
### Syncing Source Code
diff --git a/bin/readme-examples-sync b/bin/readme-examples-sync
new file mode 100755
index 0000000..ca4f3a6
--- /dev/null
+++ b/bin/readme-examples-sync
@@ -0,0 +1,40 @@
+#!/usr/bin/env php
+sync($repositoryRoot);
+
+exit($exitCode);
diff --git a/captainhook.json b/captainhook.json
deleted file mode 100644
index 4dc875e..0000000
--- a/captainhook.json
+++ /dev/null
@@ -1,23 +0,0 @@
-{
- "commit-msg": {
- "enabled": true,
- "actions": []
- },
- "pre-commit": {
- "enabled": true,
- "actions": [
- {
- "action": "composer normalize --diff --dry-run"
- },
- {
- "action": "\\CaptainHook\\App\\Hook\\PHP\\Action\\Linting"
- },
- {
- "action": "vendor/bin/php-cs-fixer check --diff"
- },
- {
- "action": "vendor/bin/phpstan"
- }
- ]
- }
-}
diff --git a/composer.json b/composer.json
index ab45a11..e18e6ad 100644
--- a/composer.json
+++ b/composer.json
@@ -16,7 +16,6 @@
"php": "^8.4"
},
"require-dev": {
- "captainhook/captainhook": "^5.25",
"ergebnis/composer-normalize": "^2.48",
"friendsofphp/php-cs-fixer": "^3.85",
"phpstan/extension-installer": "^1.4",
@@ -29,10 +28,18 @@
"Ruudk\\ReadmeExamplesSyncHook\\": "src/"
}
},
+ "bin": [
+ "bin/readme-examples-sync"
+ ],
"config": {
"allow-plugins": {
"ergebnis/composer-normalize": true,
"phpstan/extension-installer": true
}
+ },
+ "scripts": {
+ "post-install-cmd": [
+ "command -v lefthook >/dev/null 2>&1 && lefthook install || true"
+ ]
}
}
diff --git a/composer.lock b/composer.lock
index 4f729d0..e8b90ca 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,149 +4,9 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "9b81486503f5cfa83bccd83172220409",
+ "content-hash": "ba01c0e2ae51a1c4fe2ca0fc6c80d799",
"packages": [],
"packages-dev": [
- {
- "name": "captainhook/captainhook",
- "version": "5.25.11",
- "source": {
- "type": "git",
- "url": "https://github.com/captainhook-git/captainhook.git",
- "reference": "f2278edde4b45af353861aae413fc3840515bb80"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/captainhook-git/captainhook/zipball/f2278edde4b45af353861aae413fc3840515bb80",
- "reference": "f2278edde4b45af353861aae413fc3840515bb80",
- "shasum": ""
- },
- "require": {
- "captainhook/secrets": "^0.9.4",
- "ext-json": "*",
- "ext-spl": "*",
- "ext-xml": "*",
- "php": ">=8.0",
- "sebastianfeldmann/camino": "^0.9.2",
- "sebastianfeldmann/cli": "^3.3",
- "sebastianfeldmann/git": "^3.14",
- "symfony/console": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0",
- "symfony/filesystem": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0",
- "symfony/process": "^2.7 || ^3.0 || ^4.0 || ^5.0 || ^6.0 || ^7.0"
- },
- "replace": {
- "sebastianfeldmann/captainhook": "*"
- },
- "require-dev": {
- "composer/composer": "~1 || ^2.0",
- "mikey179/vfsstream": "~1"
- },
- "bin": [
- "bin/captainhook"
- ],
- "type": "library",
- "extra": {
- "captainhook": {
- "config": "captainhook.json"
- },
- "branch-alias": {
- "dev-main": "6.0.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "CaptainHook\\App\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Sebastian Feldmann",
- "email": "sf@sebastian-feldmann.info"
- }
- ],
- "description": "PHP git hook manager",
- "homepage": "http://php.captainhook.info/",
- "keywords": [
- "commit-msg",
- "git",
- "hooks",
- "post-merge",
- "pre-commit",
- "pre-push",
- "prepare-commit-msg"
- ],
- "support": {
- "issues": "https://github.com/captainhook-git/captainhook/issues",
- "source": "https://github.com/captainhook-git/captainhook/tree/5.25.11"
- },
- "funding": [
- {
- "url": "https://github.com/sponsors/sebastianfeldmann",
- "type": "github"
- }
- ],
- "time": "2025-08-12T12:14:57+00:00"
- },
- {
- "name": "captainhook/secrets",
- "version": "0.9.7",
- "source": {
- "type": "git",
- "url": "https://github.com/captainhook-git/secrets.git",
- "reference": "d62c97f75f81ac98e22f1c282482bd35fa82f631"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/captainhook-git/secrets/zipball/d62c97f75f81ac98e22f1c282482bd35fa82f631",
- "reference": "d62c97f75f81ac98e22f1c282482bd35fa82f631",
- "shasum": ""
- },
- "require": {
- "ext-mbstring": "*",
- "php": ">=8.0"
- },
- "type": "library",
- "autoload": {
- "psr-4": {
- "CaptainHook\\Secrets\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Sebastian Feldmann",
- "email": "sf@sebastian-feldmann.info"
- }
- ],
- "description": "Utility classes to detect secrets",
- "keywords": [
- "commit-msg",
- "keys",
- "passwords",
- "post-merge",
- "prepare-commit-msg",
- "secrets",
- "tokens"
- ],
- "support": {
- "issues": "https://github.com/captainhook-git/secrets/issues",
- "source": "https://github.com/captainhook-git/secrets/tree/0.9.7"
- },
- "funding": [
- {
- "url": "https://github.com/sponsors/sebastianfeldmann",
- "type": "github"
- }
- ],
- "time": "2025-04-08T07:10:48+00:00"
- },
{
"name": "clue/ndjson-react",
"version": "v1.3.0",
@@ -2325,182 +2185,6 @@
],
"time": "2025-02-07T04:55:46+00:00"
},
- {
- "name": "sebastianfeldmann/camino",
- "version": "0.9.5",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianfeldmann/camino.git",
- "reference": "bf2e4c8b2a029e9eade43666132b61331e3e8184"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianfeldmann/camino/zipball/bf2e4c8b2a029e9eade43666132b61331e3e8184",
- "reference": "bf2e4c8b2a029e9eade43666132b61331e3e8184",
- "shasum": ""
- },
- "require": {
- "php": ">=7.1"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "1.0.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "SebastianFeldmann\\Camino\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Sebastian Feldmann",
- "email": "sf@sebastian-feldmann.info"
- }
- ],
- "description": "Path management the OO way",
- "homepage": "https://github.com/sebastianfeldmann/camino",
- "keywords": [
- "file system",
- "path"
- ],
- "support": {
- "issues": "https://github.com/sebastianfeldmann/camino/issues",
- "source": "https://github.com/sebastianfeldmann/camino/tree/0.9.5"
- },
- "funding": [
- {
- "url": "https://github.com/sebastianfeldmann",
- "type": "github"
- }
- ],
- "time": "2022-01-03T13:15:10+00:00"
- },
- {
- "name": "sebastianfeldmann/cli",
- "version": "3.4.2",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianfeldmann/cli.git",
- "reference": "6fa122afd528dae7d7ec988a604aa6c600f5d9b5"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianfeldmann/cli/zipball/6fa122afd528dae7d7ec988a604aa6c600f5d9b5",
- "reference": "6fa122afd528dae7d7ec988a604aa6c600f5d9b5",
- "shasum": ""
- },
- "require": {
- "php": ">=7.2"
- },
- "require-dev": {
- "symfony/process": "^4.3 | ^5.0"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "3.4.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "SebastianFeldmann\\Cli\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Sebastian Feldmann",
- "email": "sf@sebastian-feldmann.info"
- }
- ],
- "description": "PHP cli helper classes",
- "homepage": "https://github.com/sebastianfeldmann/cli",
- "keywords": [
- "cli"
- ],
- "support": {
- "issues": "https://github.com/sebastianfeldmann/cli/issues",
- "source": "https://github.com/sebastianfeldmann/cli/tree/3.4.2"
- },
- "funding": [
- {
- "url": "https://github.com/sebastianfeldmann",
- "type": "github"
- }
- ],
- "time": "2024-11-26T10:19:01+00:00"
- },
- {
- "name": "sebastianfeldmann/git",
- "version": "3.14.3",
- "source": {
- "type": "git",
- "url": "https://github.com/sebastianfeldmann/git.git",
- "reference": "22584df8df01d95b0700000cfd855779fae7d8ea"
- },
- "dist": {
- "type": "zip",
- "url": "https://api.github.com/repos/sebastianfeldmann/git/zipball/22584df8df01d95b0700000cfd855779fae7d8ea",
- "reference": "22584df8df01d95b0700000cfd855779fae7d8ea",
- "shasum": ""
- },
- "require": {
- "ext-json": "*",
- "ext-libxml": "*",
- "ext-simplexml": "*",
- "php": ">=8.0",
- "sebastianfeldmann/cli": "^3.0"
- },
- "require-dev": {
- "mikey179/vfsstream": "^1.6"
- },
- "type": "library",
- "extra": {
- "branch-alias": {
- "dev-master": "4.0.x-dev"
- }
- },
- "autoload": {
- "psr-4": {
- "SebastianFeldmann\\Git\\": "src/"
- }
- },
- "notification-url": "https://packagist.org/downloads/",
- "license": [
- "MIT"
- ],
- "authors": [
- {
- "name": "Sebastian Feldmann",
- "email": "sf@sebastian-feldmann.info"
- }
- ],
- "description": "PHP git wrapper",
- "homepage": "https://github.com/sebastianfeldmann/git",
- "keywords": [
- "git"
- ],
- "support": {
- "issues": "https://github.com/sebastianfeldmann/git/issues",
- "source": "https://github.com/sebastianfeldmann/git/tree/3.14.3"
- },
- "funding": [
- {
- "url": "https://github.com/sebastianfeldmann",
- "type": "github"
- }
- ],
- "time": "2025-06-05T16:05:10+00:00"
- },
{
"name": "symfony/console",
"version": "v7.3.2",
diff --git a/lefthook.yaml b/lefthook.yaml
new file mode 100644
index 0000000..46fcd4a
--- /dev/null
+++ b/lefthook.yaml
@@ -0,0 +1,19 @@
+pre-commit:
+ parallel: true
+ commands:
+ composer-normalize:
+ glob:
+ - "composer.json"
+ - "composer.lock"
+ run: composer normalize --diff
+ stage_fixed: true
+
+ php-cs-fixer:
+ glob:
+ - "bin/readme-examples-sync"
+ - "*.php"
+ run: vendor/bin/php-cs-fixer fix --config .php-cs-fixer.php -- {staged_files}
+ stage_fixed: true
+
+ phpstan:
+ run: vendor/bin/phpstan
diff --git a/src/SyncReadmeExamples.php b/src/SyncReadmeExamples.php
index 92b37f4..ecd8c71 100644
--- a/src/SyncReadmeExamples.php
+++ b/src/SyncReadmeExamples.php
@@ -4,53 +4,54 @@
namespace Ruudk\ReadmeExamplesSyncHook;
-use CaptainHook\App\Config;
-use CaptainHook\App\Config\Action as ActionConfig;
-use CaptainHook\App\Console\IO;
-use CaptainHook\App\Hook\Action;
-use Override;
-use SebastianFeldmann\Git\Repository;
-
-final class SyncReadmeExamples implements Action
+final class SyncReadmeExamples
{
- #[Override]
- public function execute(Config $config, IO $io, Repository $repository, ActionConfig $action) : void
+ /**
+ * Sync README.md with example files
+ *
+ * @return int Exit code (0 for success, 1 for error)
+ */
+ public function sync(string $repositoryRoot) : int
{
- $readmePath = $repository->getRoot() . '/README.md';
+ $readmePath = $repositoryRoot . '/README.md';
if ( ! file_exists($readmePath)) {
- $io->write('README.md not found, skipping sync', true, IO::VERBOSE);
+ $this->writeOutput('README.md not found, skipping sync');
- return;
+ return 0;
}
- $io->write('Syncing README.md with example files...', true, IO::VERBOSE);
+ $this->writeOutput('Syncing README.md with example files...');
$readme = file_get_contents($readmePath);
if ($readme === false) {
- $io->write('Failed to read README.md');
+ $this->writeError('Failed to read README.md');
- return;
+ return 1;
}
- $updatedReadme = $this->syncReadmeWithExamples($readme, $repository->getRoot(), $io);
+ $updatedReadme = $this->syncReadmeWithExamples($readme, $repositoryRoot);
if ($readme !== $updatedReadme) {
file_put_contents($readmePath, $updatedReadme);
- $io->write('✓ README.md has been synced with example files');
+ $this->writeOutput('✓ README.md has been synced with example files');
- // Stage the README changes
- exec('git add README.md');
+ // Stage the README changes if in a git repository
+ if (is_dir($repositoryRoot . '/.git')) {
+ exec('git add README.md');
+ }
} else {
- $io->write('✓ README.md is already in sync');
+ $this->writeOutput('✓ README.md is already in sync');
}
+
+ return 0;
}
/**
* Sync README content with example files
*/
- private function syncReadmeWithExamples(string $readme, string $repositoryRoot, IO $io) : string
+ private function syncReadmeWithExamples(string $readme, string $repositoryRoot) : string
{
$lines = explode("\n", $readme);
$result = [];
@@ -77,7 +78,7 @@ private function syncReadmeWithExamples(string $readme, string $repositoryRoot,
}
// Insert new code from source file
- $code = $this->getExampleCode($sourceFile, $repositoryRoot, $io);
+ $code = $this->getExampleCode($sourceFile, $repositoryRoot);
if ($code !== null) {
// Add code lines without trailing newline on last line
@@ -110,7 +111,7 @@ private function syncReadmeWithExamples(string $readme, string $repositoryRoot,
}
// Insert new output from executing the file
- $output = $this->executeExample($sourceFile, $repositoryRoot, $io);
+ $output = $this->executeExample($sourceFile, $repositoryRoot);
if ($output !== null) {
$outputLines = explode("\n", rtrim($output));
@@ -136,12 +137,12 @@ private function syncReadmeWithExamples(string $readme, string $repositoryRoot,
/**
* Get code from example file with path adjustments
*/
- private function getExampleCode(string $sourceFile, string $repositoryRoot, IO $io) : ?string
+ private function getExampleCode(string $sourceFile, string $repositoryRoot) : ?string
{
$fullPath = $repositoryRoot . '/' . $sourceFile;
if ( ! file_exists($fullPath)) {
- $io->write(sprintf('Source file not found: %s', $sourceFile), true, IO::VERBOSE);
+ $this->writeOutput(sprintf('Warning: Source file not found: %s', $sourceFile));
return null;
}
@@ -149,7 +150,7 @@ private function getExampleCode(string $sourceFile, string $repositoryRoot, IO $
$code = file_get_contents($fullPath);
if ($code === false) {
- $io->write(sprintf('Failed to read source file: %s', $sourceFile), true, IO::VERBOSE);
+ $this->writeOutput(sprintf('Warning: Failed to read source file: %s', $sourceFile));
return null;
}
@@ -186,12 +187,12 @@ private function getExampleCode(string $sourceFile, string $repositoryRoot, IO $
/**
* Execute example file and capture output
*/
- private function executeExample(string $sourceFile, string $repositoryRoot, IO $io) : ?string
+ private function executeExample(string $sourceFile, string $repositoryRoot) : ?string
{
$fullPath = $repositoryRoot . '/' . $sourceFile;
if ( ! file_exists($fullPath)) {
- $io->write(sprintf('Source file not found: %s', $sourceFile), true, IO::VERBOSE);
+ $this->writeOutput(sprintf('Warning: Source file not found: %s', $sourceFile));
return null;
}
@@ -204,7 +205,7 @@ private function executeExample(string $sourceFile, string $repositoryRoot, IO $
$code = file_get_contents($fullPath);
if ($code === false) {
- $io->write(sprintf('Failed to read source file: %s', $sourceFile), true, IO::VERBOSE);
+ $this->writeOutput(sprintf('Warning: Failed to read source file: %s', $sourceFile));
return null;
}
@@ -217,7 +218,7 @@ private function executeExample(string $sourceFile, string $repositoryRoot, IO $
);
if ($tempFile === false) {
- $io->write(sprintf('Failed to create temp file for: %s', $sourceFile), true, IO::VERBOSE);
+ $this->writeOutput(sprintf('Warning: Failed to create temp file for: %s', $sourceFile));
return null;
}
@@ -229,7 +230,7 @@ private function executeExample(string $sourceFile, string $repositoryRoot, IO $
$output = shell_exec($command);
if ($output === null || $output === false) {
- $io->write(sprintf('Failed to execute: %s', $sourceFile), true, IO::VERBOSE);
+ $this->writeOutput(sprintf('Warning: Failed to execute: %s', $sourceFile));
return null;
}
@@ -299,4 +300,20 @@ private function getLanguageFromExtension(string $filePath) : string
default => '',
};
}
+
+ /**
+ * Write output message to stdout
+ */
+ private function writeOutput(string $message) : void
+ {
+ echo $message . PHP_EOL;
+ }
+
+ /**
+ * Write error message to stderr
+ */
+ private function writeError(string $message) : void
+ {
+ fwrite(STDERR, $message . PHP_EOL);
+ }
}