ADR-030: CLI Design Guidelines

Status

Proposed

Date

2025-12-07

Deciders

Context and Problem Statement

The idempiere-cli has grown organically with commands added for different use cases. We need a consistent design philosophy that follows industry best practices for modern CLI tools. The Command Line Interface Guidelines provides comprehensive guidance that should be adopted as our standard.

Key challenges:

  1. Inconsistent command patterns across subcommands
  2. Missing dry-run/preview modes for dangerous operations
  3. Confirmation prompts not standardized by risk level
  4. Recovery and resumability not considered

Decision Drivers

Considered Options

  1. Ad-hoc design - Continue organic growth, fix issues as found
  2. clig.dev adoption - Adopt clig.dev as our CLI design standard
  3. Custom guidelines - Create our own CLI design document

Decision Outcome

Chosen option: "clig.dev adoption", because it represents industry consensus from CLI authors (Heroku, Stripe, GitHub, etc.) and provides comprehensive, battle-tested guidance.

Confirmation

Implementation Guidelines

1. Dry-Run / Preview Mode

Standard flag: -n, --dry-run

> "Do not run the command, but describe the changes that would occur if the command were run."

# Show what would be created without creating
idempiere-cli dict table add XX_MyTable --columns "S#Name,Q#Qty" --dry-run

# Output:
Would create table: XX_MyTable
Would create columns:
  - Name (VARCHAR, 60)
  - Qty (DECIMAL, 10,2)
Would create AD_Element entries: 2
Would generate migration script: postgres/202501071234_IDEMPIERE-XXX.sql

No changes made (--dry-run mode)

Implementation pattern:

@Option(names = {"-n", "--dry-run"},
        description = "Show what would happen without making changes")
boolean dryRun;

@Override
public Integer call() {
    if (dryRun) {
        output.info("Would create table: " + tableName);
        // ... describe all changes
        output.info("\nNo changes made (--dry-run mode)");
        return ExitCodes.SUCCESS;
    }
    // Actual execution
}

2. Risk-Based Confirmation

Operations should be categorized by risk level:

Risk Level Actions Guard Pattern
Low Read-only queries, listing None
Medium Create new resources --dry-run available
High Modify existing resources Prompt for y/yes or require --force
Severe Delete, drop, destructive Type resource name to confirm

Medium risk example:

$ idempiere-cli dict table add XX_MyTable --apply

This will create:
  - Table: XX_MyTable (AD_Table_ID will be assigned)
  - Window: XX_MyTable
  - Tab: XX_MyTable
  - 5 standard columns

Proceed? [y/N] y

High risk example:

$ idempiere-cli dict column update C_Order.IsActive --default N

WARNING: This will change IsActive default for 1 core table.
         3,456 existing records will be affected.

Proceed? [y/N] _

# Or non-interactive:
$ idempiere-cli dict column update C_Order.IsActive --default N --force

Severe risk example:

$ idempiere-cli dict table drop XX_MyTable --apply

DANGER: This will permanently delete:
  - Table: XX_MyTable
  - 23 columns
  - 1 window, 2 tabs
  - 156 records in database

Type 'XX_MyTable' to confirm deletion: _

Implementation:

public class ConfirmationHelper {

    public boolean confirmMedium(String message) {
        if (forceFlag) return true;
        output.warn(message);
        return prompt("Proceed? [y/N] ", "y", "yes");
    }

    public boolean confirmSevere(String resourceName, String message) {
        output.error("DANGER: " + message);
        String typed = prompt("Type '" + resourceName + "' to confirm: ");
        return resourceName.equals(typed);
    }
}

3. Graceful Interruption

Multi-stage Ctrl+C pattern:

$ idempiere-cli pack out --module XX_* --apply

Exporting XX_MyModule...
[=========>          ] 45%

^C
Gracefully stopping... (press Ctrl+C again to force)

Cleanup:
  - Rolled back partial transaction
  - Removed incomplete export file

Partial results saved to: /tmp/xx_mymodule_partial.xml
Resume with: idempiere-cli pack out --module XX_* --resume /tmp/xx_mymodule_partial.xml

Implementation:

private volatile boolean interrupted = false;
private volatile boolean forceQuit = false;

public void setupInterruptHandler() {
    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
        if (!interrupted) {
            interrupted = true;
            output.warn("\nGracefully stopping... (press Ctrl+C again to force)");
            cleanup();
        } else {
            forceQuit = true;
            System.exit(130);  // 128 + SIGINT(2)
        }
    }));
}

4. Recovery and Resumability

> "If the program fails for some transient reason, you should be able to hit <up> and <enter> and it should pick up from where it left off."

Idempotent operations:

# Running twice should be safe
$ idempiere-cli dict table add XX_MyTable --columns "S#Name"
Created table: XX_MyTable

$ idempiere-cli dict table add XX_MyTable --columns "S#Name"
Table XX_MyTable already exists (no changes made)

Resume from checkpoint:

$ idempiere-cli pack out --module XX_* --apply
ERROR: Connection lost after exporting 3 of 5 tables

Progress saved to: ~/.idempiere-cli/checkpoint/packout_20250107.json
Resume with: idempiere-cli pack out --resume

$ idempiere-cli pack out --resume
Resuming from checkpoint (3/5 complete)
Exporting XX_Table4...

5. Command Structure Guidelines

Verb-Noun pattern:

# Good: verb first
idempiere-cli dict table add
idempiere-cli dict table list
idempiere-cli dict table drop

# Avoid: noun first
idempiere-cli dict add-table  # inconsistent

Progressive disclosure:

# Simple case - minimal options
idempiere-cli dict table add XX_MyTable

# Complex case - full control
idempiere-cli dict table add XX_MyTable \
  --columns "S#Name,Q#Qty,A#Amount" \
  --window "My Window" \
  --entity-type "U" \
  --access-level "3" \
  --apply

6. Output Guidelines

Default to human-readable:

$ idempiere-cli dict table list --pattern "C_Order%"

┌────────────┬─────────────────────┬────────┐
│ Table      │ Description         │ Type   │
├────────────┼─────────────────────┼────────┤
│ C_Order    │ Sales Order         │ D      │
│ C_OrderLine│ Sales Order Line    │ D      │
└────────────┴─────────────────────┴────────┘

2 tables found

Machine-readable with --json:

$ idempiere-cli dict table list --pattern "C_Order%" --json

{"tables":[{"name":"C_Order","description":"Sales Order","entityType":"D"},{"name":"C_OrderLine","description":"Sales Order Line","entityType":"D"}],"count":2}

Errors to stderr:

// Good
ConsoleOutput.error("Table not found: " + name);

// Bad
System.out.println("ERROR: Table not found: " + name);

7. Help System

Command-level help:

$ idempiere-cli dict table add --help

Usage: idempiere-cli dict table add [OPTIONS] <tableName>

Create a new table in Application Dictionary

Arguments:
  <tableName>    Table name (use XX_ prefix for custom tables)

Options:
  -c, --columns=<spec>    Column definitions using prefix notation
                          (use 'idempiere-cli dict column-help' for syntax)
  -n, --dry-run           Show what would be created without making changes
  -f, --force             Skip confirmation prompts
  --apply                 Apply changes to database (default: generate only)

Examples:
  # Create simple table with standard columns
  idempiere-cli dict table add XX_MyTable

  # Create table with custom columns
  idempiere-cli dict table add XX_MyTable --columns "S#Name,Q#Qty,A#Amount"

  # Preview without applying
  idempiere-cli dict table add XX_MyTable --dry-run

See also:
  idempiere-cli dict table list    List existing tables
  idempiere-cli dict column add    Add columns to existing table
  https://wiki.idempiere.org/en/Table_and_Column

Contextual suggestions:

$ idempiere-cli dict tabl add XX_Test

Unknown command: 'tabl'

Did you mean?
  idempiere-cli dict table add XX_Test

See 'idempiere-cli dict --help' for available commands.

8. Standard Flags

Adopt clig.dev standard flags consistently:

Flag Short Purpose
--help -h Show help
--version -V Show version
--verbose -v Increase verbosity
--quiet -q Suppress output
--dry-run -n Preview without changes
--force -f Skip confirmations
--json Machine-readable output
--no-color Disable colored output

Command Audit Checklist

For each command, verify:

Pros and Cons of the Options

Option 1: Ad-hoc Design

Continue organic growth without formal guidelines.

Option 2: clig.dev Adoption (Chosen)

Adopt clig.dev as formal CLI design standard.

Option 3: Custom Guidelines

Create our own CLI design document.

More Information

WizardFlow Implementation

The org.idempiere.cli.wizard package provides Terraform/CloudFormation-style guided wizards:

┌─────────────────────────────────────────────────────────────────┐
│              TERRAFORM-STYLE WIZARD FLOW                         │
├─────────────────────────────────────────────────────────────────┤
│                                                                  │
│  1. COLLECT (questions, checkboxes, selections)                 │
│  2. PLAN (generate execution plan)                              │
│  3. SUMMARIZE (show what will be created/updated/deleted)       │
│  4. APPLY (execute after confirmation)                          │
│                                                                  │
└─────────────────────────────────────────────────────────────────┘

Key Classes:

Class Purpose
WizardFlow<T> Main wizard builder/runner
ExecutionPlan<T> Terraform-style plan with resource changes
ResourceChange Individual CREATE/UPDATE/DELETE operation
WizardResult Execution result with status and data
CheckboxOption Multi-select option

Usage Example:

WizardFlow.<TableContext>create(prompt)
    .title("Create iDempiere Table")
    .dryRun(dryRun)
    .ask("tableName", "Table name (XX_ prefix):", null)
    .ask("description", "Description:", null)
    .askCheckboxes("templates", "Include column templates:", List.of(
        CheckboxOption.selected("audit", "Standard audit columns"),
        CheckboxOption.selected("uuid", "UUID column"),
        CheckboxOption.of("document", "Document columns")
    ))
    .askEnum("entityType", "Entity type:", ENTITY_TYPES)
    .onPlan(answers -> createPlan(answers))
    .onApply(plan -> applyPlan(plan))
    .run();

Input Sources:

All produce the same ExecutionPlan:

Source Description
Interactive wizard --guided flag, user answers questions
YAML template Pre-defined configuration file
AI-generated Plan created from natural language
Direct CLI Arguments provided on command line

Example Output:

═══════════════════════════════════════════
              CREATE TABLE
═══════════════════════════════════════════

? Table name (XX_ prefix): XX_EmployeeCert
? Description: Employee Training Certifications
? Include column templates:
  (enter numbers to toggle, press Enter to confirm)
  1. [x] Standard audit columns
  2. [x] UUID column
  3. [ ] Document columns
  4. [ ] Accounting columns
Toggle (e.g., 1,3) or Enter to continue:

═══════════════════════════════════════════
                 SUMMARY
═══════════════════════════════════════════

Configuration:
  tableName: XX_EmployeeCert
  description: Employee Training Certifications
  templates: [audit, uuid]
  entityType: U
  accessLevel: 3

Execution Plan:
  + AD_Table.XX_EmployeeCert
  + AD_Column.XX_EmployeeCert_ID
  + AD_Column.XX_EmployeeCert_UU
  + AD_Column.AD_Client_ID
  + AD_Column.AD_Org_ID
  + AD_Column.IsActive
  + AD_Column.Created
  + AD_Column.CreatedBy
  + AD_Column.Updated
  + AD_Column.UpdatedBy
  + PostgreSQL Table.XX_EmployeeCert

Plan: 11 to add, 0 to change, 0 to destroy

? Apply these changes? [Y/n] y

Applying changes...

✓ Table created successfully!
  Table: XX_EmployeeCert
  AD_Table_ID: 1000123
  Columns: 10

clig.dev Key Principles

  1. Human-first design - Optimize for humans reading and writing
  2. Simple parts that combine - Unix philosophy
  3. Consistency across commands - Predictable patterns
  4. Robustness - Handle errors gracefully
  5. Empathy - Consider user's mental state

References

Path: /docs/developers/architecture/idempiere-hub/030-cli-design-guidelines