Explorar o código

feat: refactor configuration to use JSON-based command system

Moved command configuration from .env to commands.json for better flexibility and structure.

Changes:
- Add commands.json.example with detailed command configuration format
- Refactor config.js to load both .env and commands.json
- Update commandExecutor.js to support shell and Node.js execution modes
- Add per-command settings (cwd, timeout, type, args)
- Support multiple commands per event type
- Maintain backward compatibility with legacy .env command format
- Update .env.example to only enable/disable events
- Update documentation (README.md and CLAUDE.md)
- Add commands.json to .gitignore

Resolves fszontagh/agent-manager#1

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
Claude hai 9 meses
pai
achega
ee1121f04c
Modificáronse 8 ficheiros con 522 adicións e 170 borrados
  1. 9 61
      .env.example
  2. 1 0
      .gitignore
  3. 56 20
      CLAUDE.md
  4. 151 57
      README.md
  5. 142 0
      commands.json.example
  6. 60 11
      src/commandExecutor.js
  7. 92 14
      src/config.js
  8. 11 7
      src/index.js

+ 9 - 61
.env.example

@@ -4,73 +4,21 @@ PORT=3000
 WEBHOOK_PATH=/webhook
 WEBHOOK_SECRET=
 
-# Command Execution on Webhook Events
-# Available variables for substitution:
-#   {{repo}}         - Repository name (e.g., "my-repo")
-#   {{full_repo}}    - Full repository name (e.g., "user/my-repo")
-#   {{branch}}       - Branch name (e.g., "main", "feature/new-feature")
-#   {{pusher}}       - Username of person who triggered the event
-#   {{event}}        - Event type (e.g., "push", "pull_request")
-#   {{commit}}       - Latest commit hash (for push events)
-#   {{commit_msg}}   - Latest commit message (for push events)
-#   {{pr_number}}    - Pull request number (for PR events)
-#   {{pr_action}}    - Pull request action (opened, closed, etc.)
-#   {{tag}}          - Tag name (for create/delete tag events)
-#   {{issue_number}} - Issue number (for issue events)
-#   {{issue_title}}  - Issue title (for issue events)
-#   {{issue_action}} - Issue action (opened, closed, reopened, etc.)
-#   {{issue_body}}   - Issue description/body (for issue events)
-#   {{comment_body}} - Comment text (for issue_comment events)
-
-# Push Event Commands
-# Execute command when push event is received
+# Webhook Event Configuration
+# Enable/disable webhook events (commands are configured in commands.json)
+# Set to 'true' to enable event handling, 'false' to disable
 WEBHOOK_PUSH_ENABLED=true
-WEBHOOK_PUSH_COMMAND=echo "Push to {{branch}} by {{pusher}} in {{repo}}"
-
-# Example: Run deployment script
-# WEBHOOK_PUSH_COMMAND=/path/to/deploy.sh {{branch}} {{repo}} {{pusher}}
-
-# Example: Only deploy main branch
-# WEBHOOK_PUSH_FILTER_BRANCH=main
-# WEBHOOK_PUSH_COMMAND=/path/to/deploy-production.sh
-
-# Pull Request Event Commands
 WEBHOOK_PULL_REQUEST_ENABLED=false
-WEBHOOK_PULL_REQUEST_COMMAND=echo "PR #{{pr_number}} {{pr_action}} by {{pusher}}"
-
-# Create Event Commands (new branch or tag)
 WEBHOOK_CREATE_ENABLED=false
-WEBHOOK_CREATE_COMMAND=echo "Created {{ref_type}} {{branch}} in {{repo}}"
-
-# Delete Event Commands
 WEBHOOK_DELETE_ENABLED=false
-WEBHOOK_DELETE_COMMAND=echo "Deleted {{ref_type}} {{branch}} from {{repo}}"
-
-# Release Event Commands
 WEBHOOK_RELEASE_ENABLED=false
-WEBHOOK_RELEASE_COMMAND=echo "Release {{tag}} {{pr_action}} in {{repo}}"
-
-# Issues Event Commands (ticket/bug created, closed, reopened)
 WEBHOOK_ISSUES_ENABLED=false
-WEBHOOK_ISSUES_COMMAND=echo "Issue #{{issue_number}} {{issue_action}} by {{pusher}} in {{repo}}: {{issue_title}}"
-
-# Example: Send notification when issue is opened
-# WEBHOOK_ISSUES_COMMAND=/path/to/notify-team.sh "{{issue_title}}" "{{issue_number}}" "{{pusher}}"
-
-# Example: Auto-label or assign issues
-# WEBHOOK_ISSUES_COMMAND=/path/to/triage-issue.sh {{repo}} {{issue_number}} "{{issue_body}}"
-
-# Issue Comment Event Commands
 WEBHOOK_ISSUE_COMMENT_ENABLED=false
-WEBHOOK_ISSUE_COMMENT_COMMAND=echo "Comment on issue #{{issue_number}} by {{pusher}}: {{comment_body}}"
-
-# Example: Trigger CI on specific comment commands
-# WEBHOOK_ISSUE_COMMENT_COMMAND=/path/to/check-comment-commands.sh "{{comment_body}}" {{issue_number}}
-
-# Global Command (runs for ALL events)
 WEBHOOK_GLOBAL_ENABLED=false
-WEBHOOK_GLOBAL_COMMAND=echo "Webhook {{event}} received for {{repo}}"
 
-# Command Execution Settings
-COMMAND_TIMEOUT=300000
-COMMAND_WORKING_DIR=/workspace
+# Legacy Command Configuration (deprecated - use commands.json instead)
+# These settings are maintained for backward compatibility
+# WEBHOOK_PUSH_COMMAND=echo "Push to {{branch}} by {{pusher}} in {{repo}}"
+# WEBHOOK_PUSH_FILTER_BRANCH=main
+# COMMAND_TIMEOUT=300000
+# COMMAND_WORKING_DIR=/workspace

+ 1 - 0
.gitignore

@@ -8,3 +8,4 @@ yarn-error.log*
 *.log
 .DS_Store
 queue.json
+commands.json

+ 56 - 20
CLAUDE.md

@@ -95,9 +95,13 @@ curl -X POST http://localhost:3000/webhook \
 
 ## Configuration
 
-All configuration is in `.env` file (copy from `.env.example`).
+Configuration is split between two files:
 
-### Server Configuration
+### 1. Environment Configuration (`.env`)
+
+Copy from `.env.example` and configure:
+
+**Server Configuration:**
 ```env
 HOST=0.0.0.0          # Network interface (0.0.0.0=all, 127.0.0.1=localhost only)
 PORT=3000             # Port number
@@ -105,36 +109,63 @@ WEBHOOK_PATH=/webhook # Webhook endpoint path
 WEBHOOK_SECRET=       # Optional webhook verification secret
 ```
 
-### Command Configuration Pattern
+**Event Enable/Disable:**
 ```env
 WEBHOOK_{EVENT_TYPE}_ENABLED=true|false
-WEBHOOK_{EVENT_TYPE}_COMMAND=<shell command with {{variables}}>
-WEBHOOK_{EVENT_TYPE}_FILTER_BRANCH=<optional branch name>
 ```
 
-**Supported Event Types:** push, pull_request, create, delete, release, issues, issue_comment
+**Supported Event Types:** push, pull_request, create, delete, release, issues, issue_comment, global
 
-**Global commands (runs for all events):**
-```env
-WEBHOOK_GLOBAL_ENABLED=true
-WEBHOOK_GLOBAL_COMMAND=<command>
-```
+### 2. Command Configuration (`commands.json`)
 
-**Command execution settings:**
-```env
-COMMAND_TIMEOUT=300000          # Timeout in ms
-COMMAND_WORKING_DIR=/workspace  # Working directory for commands
+Copy from `commands.json.example` and configure commands for each event type.
+
+**Command Structure:**
+```json
+{
+  "commands": {
+    "event_type": [
+      {
+        "name": "command-identifier",
+        "description": "Human-readable description",
+        "type": "shell" | "node",
+        "command": "path/to/command",
+        "args": ["arg1", "{{variable}}", "arg3"],
+        "cwd": "/working/directory" | null,
+        "timeout": 300000,
+        "filterBranch": "branch-name" | null
+      }
+    ]
+  }
+}
 ```
 
+**Command Types:**
+- `shell` - Execute bash shell command/script
+- `node` - Execute Node.js script (automatically prepends `node` command)
+
+**Per-Command Settings:**
+- `name` - Unique identifier for the command
+- `description` - Human-readable description
+- `type` - Execution type (shell or node)
+- `command` - Command or script path
+- `args` - Array of arguments (supports variable substitution)
+- `cwd` - Working directory (null = project root)
+- `timeout` - Timeout in milliseconds
+- `filterBranch` - Only execute for specific branch (null = all branches)
+
 ## Important Patterns
 
 1. **ES6 Modules**: All files use `import/export` syntax (`"type": "module"` in package.json)
 2. **Singleton Exports**: Config, Logger, CommandExecutor are singletons (created on import)
 3. **Zero Dependencies**: Pure Node.js - no npm packages required beyond Node.js built-ins
 4. **Graceful Shutdown**: Both SIGTERM and SIGINT trigger `server.stop()` before exit
-5. **Configuration Precedence**: Environment variables override `.env` file values
-6. **Error Handling**: Try-catch blocks with logging; errors don't stop server
-7. **Branch Filtering**: Optional per-event branch filtering via `WEBHOOK_{EVENT}_FILTER_BRANCH`
+5. **Configuration Files**: `.env` for enable/disable flags, `commands.json` for command definitions
+6. **Backward Compatibility**: Legacy .env command format still supported (deprecated)
+7. **Error Handling**: Try-catch blocks with logging; errors don't stop server
+8. **Branch Filtering**: Optional per-command branch filtering via `filterBranch` field
+9. **Multiple Commands**: Each event type can have multiple commands that execute sequentially
+10. **Shell & Node Execution**: Commands can be shell scripts or Node.js scripts
 
 ## Extending with Callbacks
 
@@ -248,5 +279,10 @@ No formal test suite currently. Test manually using:
 2. Use curl to send webhook payloads
 3. Check console logs for execution results
 4. Verify commands execute with expected variable substitution
-- Add to memory: You always have to push the changes into the git reposiroty. Before pushing changes, you have to test the project with building and running it. Take care when running the project to use an unused port if the project already running. Never keep running instance from the project
-- Add to memory: Always push into git the changes using short commit messages
+You always have to push the changes into the git reposiroty. Before pushing changes, you have to test the project with building and running it. Take care when running the project to use an unused port if the project already running. Never keep running instance from the project
+
+## Issue handling
+ - always update the issue which you works on
+ - always add label to the issue if no label added
+ - always write comment with the current status of the work 
+

+ 151 - 57
README.md

@@ -6,10 +6,13 @@ A modular Node.js server for receiving and processing Gogs webhooks. The server
 
 - **Persistent Job Queue**: FIFO queue ensures sequential execution and crash recovery
 - **Modular Architecture**: Clean separation of concerns (server, handler, queue, logger)
-- **Command Execution**: Execute shell commands on webhook events with variable substitution
-- **Environment Configuration**: Configure commands via .env file
+- **Command Execution**: Execute shell or Node.js commands on webhook events
+- **JSON Configuration**: Flexible command configuration via `commands.json`
+- **Multiple Commands per Event**: Run multiple commands for each webhook event type
 - **Variable Substitution**: Access repo name, branch, pusher, issue details, and more in commands
 - **Branch Filtering**: Execute commands only for specific branches
+- **Node.js & Shell Support**: Run both shell scripts and Node.js scripts
+- **Per-Command Settings**: Configure working directory and timeout per command
 - **Extensible Callback System**: Register callbacks for specific events or all events
 - **Console Logging**: Colored, formatted logging of all webhook events
 - **Health Check Endpoint**: Built-in health monitoring
@@ -30,14 +33,17 @@ npm install
 
 ### Quick Start
 
-1. Copy the example environment file:
+1. Copy the example files:
 ```bash
 cp .env.example .env
+cp commands.json.example commands.json
 ```
 
-2. Edit `.env` to configure commands for webhook events (see Configuration section below)
+2. Edit `.env` to enable/disable webhook events
 
-3. Start the server:
+3. Edit `commands.json` to configure commands for webhook events (see Configuration section below)
+
+4. Start the server:
 ```bash
 npm start
 ```
@@ -52,13 +58,11 @@ npm run dev
 
 ### Configuration
 
-The server is configured using a `.env` file. Copy `.env.example` to `.env` and edit it:
-
-```bash
-cp .env.example .env
-```
+The server uses two configuration files:
+1. `.env` - Enable/disable webhook events and server settings
+2. `commands.json` - Define commands to execute for each event type
 
-#### Basic Server Configuration
+#### Basic Server Configuration (`.env`)
 
 ```env
 HOST=0.0.0.0
@@ -72,9 +76,63 @@ WEBHOOK_SECRET=your-secret-here
 - `WEBHOOK_PATH` - The URL path for webhooks (default: `/webhook`)
 - `WEBHOOK_SECRET` - Optional secret for webhook verification
 
-#### Command Execution Configuration
+#### Enable/Disable Events (`.env`)
 
-Configure commands to execute when webhooks are received. Commands support variable substitution:
+```env
+WEBHOOK_PUSH_ENABLED=true
+WEBHOOK_PULL_REQUEST_ENABLED=false
+WEBHOOK_CREATE_ENABLED=false
+WEBHOOK_DELETE_ENABLED=false
+WEBHOOK_RELEASE_ENABLED=false
+WEBHOOK_ISSUES_ENABLED=false
+WEBHOOK_ISSUE_COMMENT_ENABLED=false
+WEBHOOK_GLOBAL_ENABLED=false
+```
+
+#### Command Configuration (`commands.json`)
+
+Commands are configured in a JSON file with the following structure:
+
+```json
+{
+  "commands": {
+    "push": [
+      {
+        "name": "deploy-production",
+        "description": "Deploy to production on main branch",
+        "type": "shell",
+        "command": "/path/to/deploy.sh",
+        "args": ["{{branch}}", "{{repo}}", "{{pusher}}"],
+        "cwd": "/workspace",
+        "timeout": 600000,
+        "filterBranch": "main"
+      }
+    ],
+    "issues": [
+      {
+        "name": "process-issue",
+        "description": "Process issue with Node.js script",
+        "type": "node",
+        "command": "./scripts/process-issue.js",
+        "args": ["--repo", "{{repo}}", "--issue", "{{issue_number}}"],
+        "cwd": null,
+        "timeout": 300000,
+        "filterBranch": null
+      }
+    ]
+  }
+}
+```
+
+**Command Fields:**
+- `name` - Unique identifier for the command
+- `description` - Human-readable description
+- `type` - Either `"shell"` (bash script) or `"node"` (Node.js script)
+- `command` - The command or script to execute
+- `args` - Array of arguments (supports variable substitution)
+- `cwd` - Working directory (null uses project root)
+- `timeout` - Timeout in milliseconds
+- `filterBranch` - Only run for specific branch (null = all branches)
 
 **Available Variables:**
 - `{{repo}}` - Repository name (e.g., "my-repo")
@@ -87,47 +145,22 @@ Configure commands to execute when webhooks are received. Commands support varia
 - `{{pr_number}}` - Pull request number (for PR events)
 - `{{pr_action}}` - Pull request action (opened, closed, etc.)
 - `{{tag}}` - Tag name (for create/delete tag events)
+- `{{ref_type}}` - "branch" or "tag"
 - `{{issue_number}}` - Issue number (for issue events)
 - `{{issue_title}}` - Issue title (for issue events)
 - `{{issue_action}}` - Issue action (opened, closed, reopened, etc.)
 - `{{issue_body}}` - Issue description/body (for issue events)
 - `{{comment_body}}` - Comment text (for issue_comment events)
 
-**Example .env configuration:**
-
-```env
-# Execute command on push events
-WEBHOOK_PUSH_ENABLED=true
-WEBHOOK_PUSH_COMMAND=echo "Push to {{branch}} by {{pusher}} in {{repo}}"
-
-# Deploy only when pushing to main branch
-WEBHOOK_PUSH_FILTER_BRANCH=main
-WEBHOOK_PUSH_COMMAND=/path/to/deploy.sh {{branch}} {{repo}} {{pusher}}
-
-# Execute command on pull request events
-WEBHOOK_PULL_REQUEST_ENABLED=true
-WEBHOOK_PULL_REQUEST_COMMAND=echo "PR #{{pr_number}} {{pr_action}} by {{pusher}}"
-
-# Global command (runs for all events)
-WEBHOOK_GLOBAL_ENABLED=true
-WEBHOOK_GLOBAL_COMMAND=/path/to/notify.sh {{event}} {{repo}} {{pusher}}
-```
-
 **Supported Event Types:**
-- `WEBHOOK_PUSH_*` - Push events
-- `WEBHOOK_PULL_REQUEST_*` - Pull request events
-- `WEBHOOK_CREATE_*` - Branch/tag creation
-- `WEBHOOK_DELETE_*` - Branch/tag deletion
-- `WEBHOOK_RELEASE_*` - Release events
-- `WEBHOOK_ISSUES_*` - Issue events (opened, closed, reopened, etc.)
-- `WEBHOOK_ISSUE_COMMENT_*` - Issue comment events
-- `WEBHOOK_GLOBAL_*` - All events
-
-**Command Settings:**
-```env
-COMMAND_TIMEOUT=300000          # Command timeout in ms (default: 5 minutes)
-COMMAND_WORKING_DIR=/workspace  # Working directory for commands
-```
+- `push` - Push events
+- `pull_request` - Pull request events
+- `create` - Branch/tag creation
+- `delete` - Branch/tag deletion
+- `release` - Release events
+- `issues` - Issue events (opened, closed, reopened, etc.)
+- `issue_comment` - Issue comment events
+- `global` - All events
 
 ### Endpoints
 
@@ -181,8 +214,26 @@ This file is automatically managed and should not be manually edited. It's exclu
 **.env:**
 ```env
 WEBHOOK_PUSH_ENABLED=true
-WEBHOOK_PUSH_FILTER_BRANCH=main
-WEBHOOK_PUSH_COMMAND=/home/deploy/deploy.sh {{repo}} {{branch}} {{commit}}
+```
+
+**commands.json:**
+```json
+{
+  "commands": {
+    "push": [
+      {
+        "name": "deploy-production",
+        "description": "Deploy to production on main branch",
+        "type": "shell",
+        "command": "/home/deploy/deploy.sh",
+        "args": ["{{repo}}", "{{branch}}", "{{commit}}"],
+        "cwd": null,
+        "timeout": 600000,
+        "filterBranch": "main"
+      }
+    ]
+  }
+}
 ```
 
 **deploy.sh:**
@@ -205,15 +256,53 @@ systemctl restart $REPO
 **.env:**
 ```env
 WEBHOOK_GLOBAL_ENABLED=true
-WEBHOOK_GLOBAL_COMMAND=curl -X POST https://api.slack.com/webhook -d '{"text":"{{event}} in {{repo}} by {{pusher}}"}'
 ```
 
-### Example 3: Run tests on pull request
+**commands.json:**
+```json
+{
+  "commands": {
+    "global": [
+      {
+        "name": "slack-notification",
+        "description": "Send Slack notification for all events",
+        "type": "shell",
+        "command": "curl",
+        "args": ["-X", "POST", "https://api.slack.com/webhook", "-d", "{\"text\":\"{{event}} in {{repo}} by {{pusher}}\"}"],
+        "cwd": null,
+        "timeout": 30000,
+        "filterBranch": null
+      }
+    ]
+  }
+}
+```
+
+### Example 3: Run tests on pull request with Node.js
 
 **.env:**
 ```env
 WEBHOOK_PULL_REQUEST_ENABLED=true
-WEBHOOK_PULL_REQUEST_COMMAND=/home/scripts/run-tests.sh {{repo}} {{pr_number}} {{pusher}}
+```
+
+**commands.json:**
+```json
+{
+  "commands": {
+    "pull_request": [
+      {
+        "name": "run-tests",
+        "description": "Run automated tests on pull request",
+        "type": "node",
+        "command": "./scripts/run-tests.js",
+        "args": ["--repo", "{{repo}}", "--pr", "{{pr_number}}", "--author", "{{pusher}}"],
+        "cwd": "/home/ci",
+        "timeout": 900000,
+        "filterBranch": null
+      }
+    ]
+  }
+}
 ```
 
 ## Extending with Callbacks
@@ -291,11 +380,14 @@ node examples/with-callbacks.js
 │   ├── webhookHandler.js  # Webhook processing and callbacks
 │   ├── jobQueue.js        # Persistent job queue with FIFO processing
 │   ├── commandExecutor.js # Command execution with variable substitution
-│   ├── config.js          # Configuration loader (.env parser)
+│   ├── config.js          # Configuration loader (.env and commands.json)
 │   └── logger.js          # Logging utility
 ├── examples/
 │   └── with-callbacks.js  # Example with custom callbacks
-├── .env.example           # Example configuration file
+├── .env.example           # Example environment configuration
+├── commands.json.example  # Example command configuration
+├── .env                   # Your environment configuration (not in git)
+├── commands.json          # Your command configuration (not in git)
 ├── queue.json             # Queue state (auto-generated, gitignored)
 ├── package.json
 └── README.md
@@ -323,8 +415,9 @@ node examples/with-callbacks.js
 - Provides queue statistics and status
 
 ### CommandExecutor (`src/commandExecutor.js`)
-- Executes shell commands on webhook events
-- Performs variable substitution
+- Executes shell and Node.js commands on webhook events
+- Performs variable substitution in commands and arguments
+- Supports both shell scripts and Node.js scripts
 - Handles command timeouts and errors
 - Extracts webhook payload data
 
@@ -334,9 +427,10 @@ node examples/with-callbacks.js
 - Handles timestamp formatting
 
 ### Config (`src/config.js`)
-- Loads and parses .env file
+- Loads and parses .env file and commands.json
 - Provides configuration access methods
 - Merges environment variables with file config
+- Supports backward compatibility with legacy .env commands
 
 ## Testing
 

+ 142 - 0
commands.json.example

@@ -0,0 +1,142 @@
+{
+  "$schema": "https://json-schema.org/draft-07/schema#",
+  "description": "Command configuration for webhook events. Available variables: {{repo}}, {{full_repo}}, {{branch}}, {{pusher}}, {{event}}, {{commit}}, {{commit_msg}}, {{pr_number}}, {{pr_action}}, {{tag}}, {{ref_type}}, {{issue_number}}, {{issue_title}}, {{issue_action}}, {{issue_body}}, {{comment_body}}",
+  "commands": {
+    "push": [
+      {
+        "name": "echo-push-event",
+        "description": "Simple echo command for push events",
+        "type": "shell",
+        "command": "echo",
+        "args": ["Push to {{branch}} by {{pusher}} in {{repo}}"],
+        "cwd": null,
+        "timeout": 300000,
+        "filterBranch": null
+      },
+      {
+        "name": "deploy-production",
+        "description": "Deploy to production on main branch",
+        "type": "shell",
+        "command": "/path/to/deploy.sh",
+        "args": ["{{branch}}", "{{repo}}", "{{pusher}}"],
+        "cwd": "/workspace",
+        "timeout": 600000,
+        "filterBranch": "main"
+      }
+    ],
+    "pull_request": [
+      {
+        "name": "pr-notification",
+        "description": "Notify team about pull requests",
+        "type": "shell",
+        "command": "echo",
+        "args": ["PR #{{pr_number}} {{pr_action}} by {{pusher}}"],
+        "cwd": null,
+        "timeout": 300000,
+        "filterBranch": null
+      }
+    ],
+    "create": [
+      {
+        "name": "branch-created",
+        "description": "Handle new branch or tag creation",
+        "type": "shell",
+        "command": "echo",
+        "args": ["Created {{ref_type}} {{branch}} in {{repo}}"],
+        "cwd": null,
+        "timeout": 300000,
+        "filterBranch": null
+      }
+    ],
+    "delete": [
+      {
+        "name": "branch-deleted",
+        "description": "Handle branch or tag deletion",
+        "type": "shell",
+        "command": "echo",
+        "args": ["Deleted {{ref_type}} {{branch}} from {{repo}}"],
+        "cwd": null,
+        "timeout": 300000,
+        "filterBranch": null
+      }
+    ],
+    "release": [
+      {
+        "name": "release-notification",
+        "description": "Handle release events",
+        "type": "shell",
+        "command": "echo",
+        "args": ["Release {{tag}} {{pr_action}} in {{repo}}"],
+        "cwd": null,
+        "timeout": 300000,
+        "filterBranch": null
+      }
+    ],
+    "issues": [
+      {
+        "name": "issue-notification",
+        "description": "Notify team about issues",
+        "type": "shell",
+        "command": "echo",
+        "args": ["Issue #{{issue_number}} {{issue_action}} by {{pusher}} in {{repo}}: {{issue_title}}"],
+        "cwd": null,
+        "timeout": 300000,
+        "filterBranch": null
+      },
+      {
+        "name": "triage-issue",
+        "description": "Auto-triage issues with a script",
+        "type": "shell",
+        "command": "/path/to/triage-issue.sh",
+        "args": ["{{repo}}", "{{issue_number}}", "{{issue_body}}"],
+        "cwd": null,
+        "timeout": 300000,
+        "filterBranch": null
+      },
+      {
+        "name": "process-issue-with-node",
+        "description": "Process issue with Node.js script",
+        "type": "node",
+        "command": "./scripts/process-issue.js",
+        "args": ["--repo", "{{repo}}", "--issue", "{{issue_number}}", "--action", "{{issue_action}}"],
+        "cwd": null,
+        "timeout": 300000,
+        "filterBranch": null
+      }
+    ],
+    "issue_comment": [
+      {
+        "name": "comment-notification",
+        "description": "Handle issue comments",
+        "type": "shell",
+        "command": "echo",
+        "args": ["Comment on issue #{{issue_number}} by {{pusher}}: {{comment_body}}"],
+        "cwd": null,
+        "timeout": 300000,
+        "filterBranch": null
+      },
+      {
+        "name": "check-comment-commands",
+        "description": "Check for special commands in comments",
+        "type": "shell",
+        "command": "/path/to/check-comment-commands.sh",
+        "args": ["{{comment_body}}", "{{issue_number}}"],
+        "cwd": null,
+        "timeout": 300000,
+        "filterBranch": null
+      }
+    ],
+    "global": [
+      {
+        "name": "log-all-events",
+        "description": "Log all webhook events (runs for every event)",
+        "type": "shell",
+        "command": "echo",
+        "args": ["Webhook {{event}} received for {{repo}}"],
+        "cwd": null,
+        "timeout": 300000,
+        "filterBranch": null
+      }
+    ]
+  }
+}

+ 60 - 11
src/commandExecutor.js

@@ -113,21 +113,50 @@ export class CommandExecutor {
     return result;
   }
 
+  /**
+   * Build command string from command and args
+   */
+  buildCommandString(command, args = []) {
+    // Escape arguments for shell execution
+    const escapeArg = (arg) => {
+      // If arg contains spaces or special chars, wrap in single quotes
+      if (arg.includes(' ') || arg.includes('$') || arg.includes('`') || arg.includes('"')) {
+        return `'${arg.replace(/'/g, "'\\''")}'`;
+      }
+      return arg;
+    };
+
+    const escapedArgs = args.map(escapeArg);
+    return [command, ...escapedArgs].join(' ');
+  }
+
   /**
    * Execute a command with timeout
    */
   async executeCommand(command, options = {}) {
     const {
       timeout = 300000,
-      cwd = process.cwd()
+      cwd = process.cwd(),
+      type = 'shell',
+      args = []
     } = options;
 
-    logger.info('Executing command', { command, cwd, timeout });
+    let fullCommand;
+
+    if (type === 'node') {
+      // For Node.js execution, use 'node' as the shell command
+      fullCommand = this.buildCommandString('node', [command, ...args]);
+    } else {
+      // For shell execution, build command from command and args
+      fullCommand = this.buildCommandString(command, args);
+    }
+
+    logger.info('Executing command', { type, command, args, fullCommand, cwd, timeout });
 
     try {
-      const { stdout, stderr } = await execAsync(command, {
+      const { stdout, stderr } = await execAsync(fullCommand, {
         timeout,
-        cwd,
+        cwd: cwd || process.cwd(),
         shell: '/bin/bash',
         maxBuffer: 10 * 1024 * 1024 // 10MB
       });
@@ -171,7 +200,10 @@ export class CommandExecutor {
    * Execute configured command for webhook event
    */
   async executeWebhookCommand(eventType, payload, headers, config) {
-    if (!config.enabled || !config.command) {
+    // Support both old format (single command string) and new format (command + args)
+    const isNewFormat = config.command && config.args !== undefined;
+
+    if (!config.command) {
       return null;
     }
 
@@ -184,20 +216,37 @@ export class CommandExecutor {
       return null;
     }
 
-    // Substitute variables in command
-    const command = this.substituteVariables(config.command, variables);
+    let command, args, type;
+
+    if (isNewFormat) {
+      // New format: separate command and args
+      command = this.substituteVariables(config.command, variables);
+      args = (config.args || []).map(arg => this.substituteVariables(arg, variables));
+      type = config.type || 'shell';
+    } else {
+      // Old format: single command string (for backward compatibility)
+      const fullCommand = this.substituteVariables(config.command, variables);
+      // Parse command string into command and args
+      const parts = fullCommand.split(' ');
+      command = parts[0];
+      args = parts.slice(1);
+      type = 'shell';
+    }
 
     logger.info('Webhook command prepared', {
       eventType,
-      originalCommand: config.command,
-      substitutedCommand: command,
+      type,
+      command,
+      args,
       variables
     });
 
     // Execute command
     const result = await this.executeCommand(command, {
-      timeout: config.timeout,
-      cwd: config.workingDir
+      type,
+      args,
+      timeout: config.timeout || 300000,
+      cwd: config.cwd || config.workingDir || process.cwd()
     });
 
     return result;

+ 92 - 14
src/config.js

@@ -1,13 +1,16 @@
 /**
- * Configuration module for loading .env settings
+ * Configuration module for loading .env settings and commands.json
  */
 import { readFileSync } from 'fs';
 import { resolve } from 'path';
+import logger from './logger.js';
 
 export class Config {
   constructor() {
     this.env = {};
+    this.commands = {};
     this.loadEnv();
+    this.loadCommands();
   }
 
   /**
@@ -50,6 +53,25 @@ export class Config {
     this.env = { ...this.env, ...process.env };
   }
 
+  /**
+   * Load and parse commands.json file
+   */
+  loadCommands() {
+    try {
+      const commandsPath = resolve(process.cwd(), 'commands.json');
+      const commandsContent = readFileSync(commandsPath, 'utf-8');
+      this.commands = JSON.parse(commandsContent);
+    } catch (error) {
+      if (error.code === 'ENOENT') {
+        // commands.json doesn't exist, which is fine (will use .env config)
+        this.commands = { commands: {} };
+      } else {
+        console.warn('Warning: Could not load commands.json file:', error.message);
+        this.commands = { commands: {} };
+      }
+    }
+  }
+
   /**
    * Get configuration value
    */
@@ -89,30 +111,68 @@ export class Config {
   }
 
   /**
-   * Get command configuration for a specific event type
+   * Get command configurations for a specific event type
+   * Returns array of command configs from commands.json, or legacy config from .env
    */
   getCommandConfig(eventType) {
+    // Check if we have JSON commands for this event type
+    if (this.commands.commands && this.commands.commands[eventType]) {
+      return this.commands.commands[eventType];
+    }
+
+    // Fallback to .env configuration (for backward compatibility)
     const prefix = `WEBHOOK_${eventType.toUpperCase()}`;
+    const enabled = this.getBoolean(`${prefix}_ENABLED`, false);
+    const command = this.get(`${prefix}_COMMAND`, null);
 
-    return {
-      enabled: this.getBoolean(`${prefix}_ENABLED`, false),
-      command: this.get(`${prefix}_COMMAND`, null),
-      filterBranch: this.get(`${prefix}_FILTER_BRANCH`, null),
+    // Only return config if enabled
+    if (!enabled || !command) {
+      return [];
+    }
+
+    // Return as array with single item for consistency
+    return [{
+      name: `${eventType}-legacy`,
+      description: `Legacy .env command for ${eventType}`,
+      type: 'shell',
+      command: command,
+      args: undefined, // Will be parsed from command string
+      cwd: this.get('COMMAND_WORKING_DIR', null),
       timeout: this.getNumber('COMMAND_TIMEOUT', 300000),
-      workingDir: this.get('COMMAND_WORKING_DIR', process.cwd())
-    };
+      filterBranch: this.get(`${prefix}_FILTER_BRANCH`, null)
+    }];
   }
 
   /**
    * Get global command configuration
+   * Returns array of global command configs from commands.json, or legacy config from .env
    */
   getGlobalCommandConfig() {
-    return {
-      enabled: this.getBoolean('WEBHOOK_GLOBAL_ENABLED', false),
-      command: this.get('WEBHOOK_GLOBAL_COMMAND', null),
+    // Check if we have JSON commands for global
+    if (this.commands.commands && this.commands.commands.global) {
+      return this.commands.commands.global;
+    }
+
+    // Fallback to .env configuration (for backward compatibility)
+    const enabled = this.getBoolean('WEBHOOK_GLOBAL_ENABLED', false);
+    const command = this.get('WEBHOOK_GLOBAL_COMMAND', null);
+
+    // Only return config if enabled
+    if (!enabled || !command) {
+      return [];
+    }
+
+    // Return as array with single item for consistency
+    return [{
+      name: 'global-legacy',
+      description: 'Legacy .env global command',
+      type: 'shell',
+      command: command,
+      args: undefined, // Will be parsed from command string
+      cwd: this.get('COMMAND_WORKING_DIR', null),
       timeout: this.getNumber('COMMAND_TIMEOUT', 300000),
-      workingDir: this.get('COMMAND_WORKING_DIR', process.cwd())
-    };
+      filterBranch: null
+    }];
   }
 
   /**
@@ -120,7 +180,25 @@ export class Config {
    */
   getEnabledEvents() {
     const events = ['push', 'pull_request', 'create', 'delete', 'release', 'issues', 'issue_comment'];
-    return events.filter(event => this.getCommandConfig(event).enabled);
+
+    // Check both JSON config and .env config
+    const enabledEvents = [];
+
+    for (const event of events) {
+      // Check JSON config
+      if (this.commands.commands && this.commands.commands[event] && this.commands.commands[event].length > 0) {
+        enabledEvents.push(event);
+        continue;
+      }
+
+      // Check .env config
+      const prefix = `WEBHOOK_${event.toUpperCase()}`;
+      if (this.getBoolean(`${prefix}_ENABLED`, false)) {
+        enabledEvents.push(event);
+      }
+    }
+
+    return enabledEvents;
   }
 }
 

+ 11 - 7
src/index.js

@@ -26,23 +26,27 @@ if (enabledEvents.length > 0) {
 
   // Register event-specific command handlers
   for (const eventType of enabledEvents) {
-    const eventConfig = config.getCommandConfig(eventType);
+    const eventConfigs = config.getCommandConfig(eventType);
 
     handler.on(eventType, async (payload, headers) => {
-      // Enqueue job instead of executing directly
-      jobQueue.enqueue(eventType, payload, headers, eventConfig);
+      // Enqueue each command as a separate job
+      for (const eventConfig of eventConfigs) {
+        jobQueue.enqueue(eventType, payload, headers, eventConfig);
+      }
     });
   }
 }
 
 // Register global command handler if enabled
-const globalConfig = config.getGlobalCommandConfig();
-if (globalConfig.enabled && globalConfig.command) {
+const globalConfigs = config.getGlobalCommandConfig();
+if (globalConfigs.length > 0) {
   logger.info('Global command handler enabled');
 
   handler.onAny(async (eventType, payload, headers) => {
-    // Enqueue global command job
-    jobQueue.enqueue(eventType, payload, headers, globalConfig);
+    // Enqueue each global command as a separate job
+    for (const globalConfig of globalConfigs) {
+      jobQueue.enqueue(eventType, payload, headers, globalConfig);
+    }
   });
 }