ADR-064: Generator Output Directory Configuration

Status

Accepted

Date

2025-12-19

Deciders

Context and Problem Statement

Code generators (plugin, model, process, callout) need a consistent way to determine where to write generated files. Currently, when outputDir is not provided, generators default to the current working directory (.), which causes problems:

  1. MCP Server context: Files are created where the MCP server was started, not where the developer expects
  2. n8n automation: Batch jobs create files in the workflow execution directory
  3. Multi-project setups: Developers work on multiple plugins in a structured workspace

We need a configurable default output directory that works across all interfaces (CLI, MCP, Chat API, n8n).

Decision Drivers

Decision Outcome

Implement a hierarchical output directory resolution with configurable defaults:

1. Explicit outputDir parameter (highest priority)
   ↓
2. GITHUB_ROOT/{artifactId} for plugin generator
   ↓
3. GENERATOR_ROOT_DIR default directory
   ↓
4. Current working directory (.)

Configuration

# application.properties
idempiere.hub.generator.root-dir=${GENERATOR_ROOT_DIR:}
idempiere.hub.generator.github-root=${GITHUB_ROOT:}

Environment Variables

Variable Purpose Example
GITHUB_ROOT Parent directory for plugin repositories /Users/dev/github
GENERATOR_ROOT_DIR Default output for all generators /Users/dev/generated

Best Practices

Developer Workstation Setup

# ~/.zshrc or ~/.bashrc
export GITHUB_ROOT="$HOME/github"
export GENERATOR_ROOT_DIR="$HOME/github"

MCP Server Configuration (Claude Desktop)

{
  "mcpServers": {
    "idempiere": {
      "command": "java",
      "args": ["-jar", "/path/to/idempiere-hub-runner.jar", "server", "mcp"],
      "env": {
        "GITHUB_ROOT": "/Users/dev/github",
        "GENERATOR_ROOT_DIR": "/Users/dev/github",
        "IDEMPIERE_DB_HOST": "localhost",
        "IDEMPIERE_DB_PORT": "5433"
      }
    }
  }
}

n8n Workflow Configuration

{
  "nodes": [
    {
      "name": "Generate Plugin",
      "type": "n8n-nodes-base.executeCommand",
      "parameters": {
        "command": "java -jar idempiere-hub-runner.jar generate plugin --artifactId=my-plugin",
        "env": {
          "GITHUB_ROOT": "/opt/n8n/workspaces/plugins",
          "GENERATOR_ROOT_DIR": "/opt/n8n/workspaces/generated"
        }
      }
    }
  ]
}

CI/CD Pipeline (GitHub Actions)

jobs:
  generate:
    runs-on: ubuntu-latest
    env:
      GITHUB_ROOT: ${{ github.workspace }}
      GENERATOR_ROOT_DIR: ${{ github.workspace }}/generated
    steps:
      - uses: actions/checkout@v4
      - name: Generate Plugin
        run: |
          java -jar idempiere-hub-runner.jar generate plugin \
            --artifactId=org.idempiere.myplugin \
            --groupId=org.idempiere

Architecture

Resolution Flow

┌─────────────────────────────────────────────────────────────────────────┐
│  GeneratorToolLogic.generate(generatorName, options, outputDir, dryRun) │
└───────────────────────────────────┬─────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│  GeneratorConfig.resolveOutputDir(outputDir, artifactId)                │
│                                                                         │
│  1. if (outputDir != null) → return outputDir                          │
│  2. if (artifactId != null && GITHUB_ROOT set)                         │
│     → return GITHUB_ROOT/artifactId                                    │
│  3. if (GENERATOR_ROOT_DIR set) → return GENERATOR_ROOT_DIR            │
│  4. return "." (current directory)                                      │
└───────────────────────────────────┬─────────────────────────────────────┘
                                    │
                                    ▼
┌─────────────────────────────────────────────────────────────────────────┐
│  GeneratorTree(resolvedPath)                                            │
│  → Write files to resolved directory                                    │
└─────────────────────────────────────────────────────────────────────────┘

Example Scenarios

Scenario 1: Plugin Generation with GITHUB_ROOT

GITHUB_ROOT=/Users/dev/github
MCP Tool: generatePlugin(artifactId="org.idempiere.rating", ...)
Resolved: /Users/dev/github/org.idempiere.rating/

Scenario 2: Model Generation with GENERATOR_ROOT_DIR

GENERATOR_ROOT_DIR=/Users/dev/generated
MCP Tool: generateModel(tableName="XX_Rating", ...)
Resolved: /Users/dev/generated/

Scenario 3: Explicit outputDir (Always Wins)

CLI: idempiere-hub generate plugin --outputDir=/tmp/test
Resolved: /tmp/test/

Scenario 4: No Config (Backward Compatible)

Resolved: . (current directory)

Implementation

GeneratorConfig Interface

@ConfigMapping(prefix = "idempiere.hub.generator")
public interface GeneratorConfig {

    Optional<String> rootDir();
    Optional<String> githubRoot();

    default String resolveOutputDir(String explicitOutputDir, String artifactId) {
        if (explicitOutputDir != null && !explicitOutputDir.isBlank()) {
            return explicitOutputDir;
        }
        if (artifactId != null && githubRoot().isPresent()) {
            return githubRoot().get() + "/" + artifactId;
        }
        if (rootDir().isPresent() && !rootDir().get().isBlank()) {
            return rootDir().get();
        }
        return ".";
    }
}

GeneratorToolLogic Update

@Inject GeneratorConfig generatorConfig;

public ToolResult generate(String generatorName, Map<String, Object> options,
                            String outputDir, boolean dryRun) {
    String artifactId = options != null ? (String) options.get("artifactId") : null;
    String resolvedOutputDir = generatorConfig.resolveOutputDir(outputDir, artifactId);
    Path basePath = Path.of(resolvedOutputDir);
    // ...
}

Workspace Layout Best Practice

$GITHUB_ROOT/
├── org.idempiere.rating/          # Generated plugin
│   ├── pom.xml
│   ├── src/
│   └── META-INF/
├── org.idempiere.shipping/        # Another plugin
│   ├── pom.xml
│   └── src/
├── idempiere/                     # iDempiere core (for reference)
│   └── ...
└── idempiere-hub/                 # This project
    └── ...

Security Considerations

  1. Path validation: Generators should validate resolved paths are within expected boundaries
  2. No path traversal: Reject ../ in artifactId or outputDir
  3. Permissions: Ensure write permissions before generating

Note: Credentials for VCS access (GitHub tokens, etc.) are out of scope for this ADR. See ADR-065: Credentials and Secrets Management for secure credential handling with AWS Secrets Manager, HashiCorp Vault, etc.

References


Note: This configuration is optional. Without it, generators behave as before (current directory). Setting GITHUB_ROOT enables the workspace-aware plugin generation pattern.

Path: /docs/developers/architecture/idempiere-hub/064-generator-output-directory-configuration