ADR-030: CLI Design Guidelines
Status
Proposed
Date
2025-12-07
Deciders
- Norbert Bede
- Claude (AI Assistant)
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:
- Inconsistent command patterns across subcommands
- Missing dry-run/preview modes for dangerous operations
- Confirmation prompts not standardized by risk level
- Recovery and resumability not considered
Decision Drivers
- Discoverability: Users should be able to explore without documentation
- Safety: Dangerous operations need appropriate guards
- Composability: Commands should work well in pipelines
- Robustness: Handle failures gracefully with recovery options
- Human-first: Optimize for humans, then machines
Considered Options
- Ad-hoc design - Continue organic growth, fix issues as found
- clig.dev adoption - Adopt clig.dev as our CLI design standard
- 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
- All new commands follow clig.dev guidelines
- Existing commands audited and updated
idempiere-cli doctorvalidates CLI compliance
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:
- [ ] Has
--dry-runfor state-changing operations - [ ] Has appropriate confirmation for risk level
- [ ] Supports
--jsonfor machine consumption - [ ] Errors go to stderr with context and suggestions
- [ ] Returns proper exit codes (see ADR-028)
- [ ] Help includes examples and "see also"
- [ ] Idempotent where possible
- [ ] Handles interruption gracefully
Pros and Cons of the Options
Option 1: Ad-hoc Design
Continue organic growth without formal guidelines.
- Good, because no upfront investment
- Bad, because inconsistent user experience
- Bad, because each command reinvents patterns
- Bad, because harder to document
Option 2: clig.dev Adoption (Chosen)
Adopt clig.dev as formal CLI design standard.
- Good, because industry-proven guidelines
- Good, because comprehensive coverage
- Good, because consistent user experience
- Good, because clear reference for contributors
- Neutral, because requires audit of existing commands
Option 3: Custom Guidelines
Create our own CLI design document.
- Good, because tailored to our needs
- Bad, because duplicates existing work
- Bad, because lacks community validation
- Bad, because maintenance burden
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
- Human-first design - Optimize for humans reading and writing
- Simple parts that combine - Unix philosophy
- Consistency across commands - Predictable patterns
- Robustness - Handle errors gracefully
- Empathy - Consider user's mental state
Related ADRs
- ADR-006 - M2M API with
--json - ADR-019 - Command structure
- ADR-026 - Execution modes
- ADR-028 - Error handling and exit codes
References
- Command Line Interface Guidelines - Primary reference
- 12 Factor CLI Apps
- Unix Philosophy
- Picocli User Manual