This is an automated email from the ASF dual-hosted git repository.

shuber pushed a commit to branch unomi-3-dev
in repository https://gitbox.apache.org/repos/asf/unomi.git


The following commit(s) were added to refs/heads/unomi-3-dev by this push:
     new ccc07678e [UNOMI-879] feat(shell): enhance CSV output and table 
formatting for CRUD commands
ccc07678e is described below

commit ccc07678ed56c1ddd6f84ccd115f0808229da2ec
Author: Serge Huber <[email protected]>
AuthorDate: Wed Jan 21 20:19:51 2026 +0100

    [UNOMI-879] feat(shell): enhance CSV output and table formatting for CRUD 
commands
    
    - Add proper CSV output support using Apache Commons CSV library
    - Fix --csv option parsing when placed before list operation arguments
    - Convert cache statistics from plain text to ShellTable format
    - Add CSV output option to cache commands with proper formatting
    - Remove redundant specific completers (CampaignCompleter, 
EventTypeCompleter, etc.)
    - Use unified IdCompleter for all CRUD command ID completion
    - Update UnomiCrudCommand to use CustomObjectMapper for consistency
    - Add buildCsvOutput method to CrudCommand interface and BaseCrudCommand
    - Update integration tests to validate table headers and CSV format
    - Add commons-csv dependency to shell-dev-commands pom.xml
---
 .../apache/unomi/itests/shell/CacheCommandsIT.java | 111 +++++++----
 .../apache/unomi/itests/shell/CrudCommandsIT.java  |  19 ++
 tools/shell-dev-commands/pom.xml                   |   4 +
 .../unomi/shell/dev/actions/UnomiCrudCommand.java  |  65 +++----
 .../unomi/shell/dev/commands/CacheCommands.java    | 109 +++++++++--
 .../commands/actions/ActionTypeCrudCommand.java    |  15 +-
 .../dev/commands/apikeys/ApiKeyCrudCommand.java    |   4 +-
 .../commands/campaigns/CampaignCrudCommand.java    |   4 +-
 .../campaigns/CampaignEventCrudCommand.java        |  14 +-
 .../conditions/ConditionTypeCrudCommand.java       |   4 +-
 .../dev/commands/consents/ConsentCrudCommand.java  |   4 +-
 .../dev/commands/events/EventCrudCommand.java      |   4 +-
 .../shell/dev/commands/goals/GoalCrudCommand.java  |   4 +-
 .../dev/commands/personas/PersonaCrudCommand.java  |   4 +-
 .../commands/profiles/ProfileAliasCrudCommand.java |  15 +-
 .../dev/commands/profiles/ProfileCrudCommand.java  |   4 +-
 .../properties/PropertyTypeCrudCommand.java        |  18 +-
 .../shell/dev/commands/rules/RuleCrudCommand.java  |   4 +-
 .../commands/rules/RuleStatisticsCrudCommand.java  |   4 +-
 .../dev/commands/scopes/ScopeCrudCommand.java      |   4 +-
 .../dev/commands/scoring/ScoringCrudCommand.java   |   4 +-
 .../dev/commands/segments/SegmentCrudCommand.java  |   4 +-
 .../dev/commands/sessions/SessionCrudCommand.java  |  14 +-
 .../dev/commands/tenants/TenantCrudCommand.java    |  74 ++++----
 .../dev/commands/topics/TopicCrudCommand.java      |   4 +-
 .../shell/dev/completers/CampaignCompleter.java    |  37 ----
 .../shell/dev/completers/EventTypeCompleter.java   |  63 -------
 .../unomi/shell/dev/completers/GoalCompleter.java  |  37 ----
 .../shell/dev/completers/IPAddressCompleter.java   |  54 ------
 .../unomi/shell/dev/completers/IdCompleter.java    |  77 +++++---
 .../shell/dev/completers/ProfileCompleter.java     |  70 -------
 .../unomi/shell/dev/completers/RuleCompleter.java  |  69 -------
 .../unomi/shell/dev/completers/ScopeCompleter.java |  37 ----
 .../shell/dev/completers/ScoringCompleter.java     |  37 ----
 .../shell/dev/completers/SegmentCompleter.java     |  57 ------
 .../shell/dev/completers/SessionCompleter.java     |  72 -------
 .../dev/completers/TenantStatusCompleter.java      |  42 -----
 .../unomi/shell/dev/services/BaseCrudCommand.java  | 206 +++++++++++++++------
 .../unomi/shell/dev/services/CrudCommand.java      |  12 ++
 39 files changed, 496 insertions(+), 888 deletions(-)

diff --git 
a/itests/src/test/java/org/apache/unomi/itests/shell/CacheCommandsIT.java 
b/itests/src/test/java/org/apache/unomi/itests/shell/CacheCommandsIT.java
index 13e920c34..a305c2068 100644
--- a/itests/src/test/java/org/apache/unomi/itests/shell/CacheCommandsIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/shell/CacheCommandsIT.java
@@ -27,33 +27,37 @@ public class CacheCommandsIT extends ShellCommandsBaseIT {
     @Test
     public void testCacheStats() throws Exception {
         String output = executeCommandAndGetOutput("unomi:cache --stats");
-        assertContainsAny(output, new String[]{"Statistics for type:", 
"Hits:", "Misses:"}, 
-            "Should show statistics for type");
+        // With ShellTable output, check for table headers instead of plain 
text
+        assertContainsAny(output, new String[]{"Type", "Hits", "Misses", "No 
cache statistics available"}, 
+            "Should show statistics table or indicate no stats");
         
-        // If statistics are shown, validate they contain numeric values
-        if (output.contains("Hits:")) {
-            validateNumericValuesInOutput(output, new String[]{"Hits:", 
"Misses:", "Updates:"}, false);
+        // If statistics are shown in table format, validate table structure
+        if (output.contains("Type") && output.contains("Hits")) {
+            validateTableHeaders(output, new String[]{"Type", "Hits", 
"Misses"});
+            // Table should have at least header row
+            String[] lines = output.split("\n");
+            Assert.assertTrue("Should have table output with headers", 
lines.length > 0);
         }
     }
 
     @Test
     public void testCacheStatsWithReset() throws Exception {
         String output = executeCommandAndGetOutput("unomi:cache --stats 
--reset");
-        // Should show statistics and reset confirmation
-        assertContainsAny(output, new String[]{"Statistics have been reset", 
"Statistics for type:"}, 
-            "Should show statistics have been reset");
+        // Should show statistics table and reset confirmation
+        assertContainsAny(output, new String[]{"Statistics have been reset", 
"Type", "Hits"}, 
+            "Should show statistics table and reset confirmation");
         
-        // If no explicit reset message, at least verify stats were shown
+        // If no explicit reset message, at least verify stats table was shown
         if (!output.contains("Statistics have been reset")) {
-            assertContainsAny(output, new String[]{"Statistics for type:", 
"Hits:"}, 
-                "Should show cache statistics");
+            assertContainsAny(output, new String[]{"Type", "Hits", "Misses"}, 
+                "Should show cache statistics table");
         }
     }
 
     @Test
     public void testCacheStatsWithTenant() throws Exception {
         String output = executeCommandAndGetOutput("unomi:cache --stats 
--tenant " + TEST_TENANT_ID);
-        // Should show statistics (may be empty if no cache activity)
+        // Should show statistics table (may be empty if no cache activity)
         // Note: --tenant option sets the tenantId but displayStatistics() 
doesn't filter by tenant,
         // so it shows all statistics. The output may be empty if there are no 
statistics at all.
         // Empty output is valid (means no statistics available)
@@ -63,16 +67,15 @@ public class CacheCommandsIT extends ShellCommandsBaseIT {
         }
         
         assertContainsAny(output, new String[]{
-            "Statistics for type:", 
-            "Hits:", 
-            "No statistics available",
+            "Type", 
+            "Hits", 
+            "No cache statistics available",
             "Cache service not available"
-        }, "Should show cache statistics, indicate no stats, or service 
unavailable");
+        }, "Should show cache statistics table, indicate no stats, or service 
unavailable");
         
-        // If stats are shown, they should be valid
-        if (output.contains("Statistics for type:")) {
-            assertContainsAny(output, new String[]{"Hits:", "Misses:"}, 
-                "Should contain Hits or Misses when stats are shown");
+        // If stats table is shown, validate table structure
+        if (output.contains("Type") && output.contains("Hits")) {
+            validateTableHeaders(output, new String[]{"Type", "Hits", 
"Misses"});
         }
     }
 
@@ -104,34 +107,68 @@ public class CacheCommandsIT extends ShellCommandsBaseIT {
     @Test
     public void testCacheStatsWithType() throws Exception {
         String output = executeCommandAndGetOutput("unomi:cache --stats --type 
profile");
-        // Should show stats for the specific type or indicate no stats
+        // Should show stats table for the specific type or indicate no stats
         assertContainsAny(output, new String[]{
-            "Statistics for type: profile",
+            "profile",
             "No statistics available for type: profile",
-            "Hits:"
-        }, "Should show statistics for profile type or indicate no stats");
+            "Type",
+            "Hits"
+        }, "Should show statistics table for profile type or indicate no 
stats");
         
-        // If stats are shown, verify they're for the correct type
-        if (output.contains("Statistics for type: profile")) {
-            assertContainsAny(output, new String[]{"Hits:", "Misses:"}, 
-                "Should show Hits or Misses for profile type");
+        // If stats table is shown, verify it contains the type and table 
structure
+        if (output.contains("Type") && output.contains("Hits")) {
+            validateTableHeaders(output, new String[]{"Type", "Hits", 
"Misses"});
+            // If profile type is in the table, it should be in a data row
+            if (tableContainsValue(output, "profile")) {
+                Assert.assertTrue("Should show profile type in table", true);
+            }
         }
     }
 
     @Test
     public void testCacheDetailedStats() throws Exception {
         String output = executeCommandAndGetOutput("unomi:cache --stats 
--detailed");
-        // Detailed stats should show additional metrics like efficiency score
+        // Detailed stats should show additional columns like efficiency score 
and error rate
         assertContainsAny(output, new String[]{
-            "Statistics for type:",
-            "Efficiency Score:",
-            "Error Rate:",
-            "Hits:"
-        }, "Should show detailed statistics");
+            "Type",
+            "Efficiency Score",
+            "Error Rate",
+            "Hits"
+        }, "Should show detailed statistics table with additional columns");
         
-        // If detailed stats are shown, verify they contain numeric values 
(allow decimals)
-        if (output.contains("Efficiency Score:") || output.contains("Error 
Rate:")) {
-            validateNumericValuesInOutput(output, new String[]{"Efficiency 
Score:", "Error Rate:"}, true);
+        // If detailed stats table is shown, verify it has the additional 
columns
+        if (output.contains("Type") && output.contains("Hits")) {
+            validateTableHeaders(output, new String[]{"Type", "Hits", 
"Efficiency Score", "Error Rate"});
+        }
+    }
+
+    @Test
+    public void testCacheStatsCsv() throws Exception {
+        String csvOutput = executeCommandAndGetOutput("unomi:cache --stats 
--csv");
+        // CSV should contain commas and have at least header row
+        Assert.assertTrue("Should output CSV format", csvOutput.contains(",") 
|| csvOutput.trim().length() > 0);
+        // CSV should have at least one line (header)
+        String[] lines = csvOutput.split("\n");
+        Assert.assertTrue("CSV output should have at least header line", 
lines.length > 0);
+        // CSV header should contain expected columns
+        if (lines.length > 0) {
+            String header = lines[0];
+            assertContainsAny(header, new String[]{"Type", "Hits", "Misses"}, 
+                "CSV header should contain expected columns");
+        }
+    }
+
+    @Test
+    public void testCacheStatsDetailedCsv() throws Exception {
+        String csvOutput = executeCommandAndGetOutput("unomi:cache --stats 
--detailed --csv");
+        // CSV should contain commas and have detailed columns
+        Assert.assertTrue("Should output CSV format", csvOutput.contains(",") 
|| csvOutput.trim().length() > 0);
+        // CSV header should contain detailed columns
+        String[] lines = csvOutput.split("\n");
+        if (lines.length > 0) {
+            String header = lines[0];
+            assertContainsAny(header, new String[]{"Type", "Hits", "Efficiency 
Score", "Error Rate"}, 
+                "CSV header should contain detailed columns");
         }
     }
 }
diff --git 
a/itests/src/test/java/org/apache/unomi/itests/shell/CrudCommandsIT.java 
b/itests/src/test/java/org/apache/unomi/itests/shell/CrudCommandsIT.java
index 48a8632be..31b179304 100644
--- a/itests/src/test/java/org/apache/unomi/itests/shell/CrudCommandsIT.java
+++ b/itests/src/test/java/org/apache/unomi/itests/shell/CrudCommandsIT.java
@@ -344,6 +344,25 @@ public class CrudCommandsIT extends ShellCommandsBaseIT {
         Assert.assertTrue("CSV output should have at least one line", 
lines.length > 0);
     }
 
+    @Test
+    public void testGoalListCsvBeforeList() throws Exception {
+        // Test --csv option before list operation (fix for option parsing 
issue)
+        String csvOutput = executeCommandAndGetOutput("unomi:crud --csv list 
goal");
+        // CSV should contain commas and have at least one line
+        Assert.assertTrue("Should output CSV format when --csv is before 
list", 
+            csvOutput.contains(",") || csvOutput.trim().length() > 0);
+        // CSV should have at least header line
+        String[] lines = csvOutput.split("\n");
+        Assert.assertTrue("CSV output should have at least header line", 
lines.length > 0);
+        // Verify it's actually CSV (not table format with spaces)
+        if (lines.length > 0) {
+            String firstLine = lines[0];
+            // CSV should have commas, not just spaces
+            Assert.assertTrue("First line should contain commas (CSV format)", 
+                firstLine.contains(",") || firstLine.trim().isEmpty());
+        }
+    }
+
     /**
      * Helper method to test basic CRUD operations for an object type.
      * Reduces code duplication across similar object types.
diff --git a/tools/shell-dev-commands/pom.xml b/tools/shell-dev-commands/pom.xml
index 3519e74e3..e62a63758 100644
--- a/tools/shell-dev-commands/pom.xml
+++ b/tools/shell-dev-commands/pom.xml
@@ -100,6 +100,10 @@
             <artifactId>commons-lang3</artifactId>
             <scope>provided</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-csv</artifactId>
+        </dependency>
 
         <dependency>
             <groupId>junit</groupId>
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/actions/UnomiCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/actions/UnomiCrudCommand.java
index e42d097d5..2ca8433f1 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/actions/UnomiCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/actions/UnomiCrudCommand.java
@@ -18,6 +18,7 @@ package org.apache.unomi.shell.dev.actions;
 
 import com.fasterxml.jackson.databind.ObjectMapper;
 import org.apache.commons.lang3.StringUtils;
+import org.apache.unomi.persistence.spi.CustomObjectMapper;
 import org.apache.karaf.shell.api.action.*;
 import org.apache.karaf.shell.api.action.lifecycle.Init;
 import org.apache.karaf.shell.api.action.lifecycle.Reference;
@@ -48,7 +49,7 @@ public class UnomiCrudCommand implements Action {
 
     private static final Logger LOGGER = 
LoggerFactory.getLogger(UnomiCrudCommand.class.getName());
 
-    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
+    private static final ObjectMapper OBJECT_MAPPER = 
CustomObjectMapper.getObjectMapper();
 
     @Reference
     private BundleContext bundleContext;
@@ -127,15 +128,18 @@ public class UnomiCrudCommand implements Action {
      * Parse list-specific options from the remaining argument list.
      * This implements Option 1: Simple Manual Parsing from the redesign 
proposal.
      * 
+     * Note: If --csv was already set by Karaf's option parser (when placed 
before arguments),
+     * we preserve that value. Otherwise, we parse it from the remaining list.
+     * 
      * @param remaining List of remaining tokens after type (e.g., ["--csv", 
"-n", "50"])
      */
     private void parseListOptions(List<String> remaining) {
-        boolean csv = false;
-        Integer maxEntries = null;
+        // Preserve csv value if already set by Karaf's option parser (when 
--csv comes before arguments)
+        boolean csv = this.csv;
+        Integer maxEntries = this.maxEntries;
         
         if (remaining == null || remaining.isEmpty()) {
-            this.csv = csv;
-            this.maxEntries = maxEntries;
+            // Keep existing values if already set by Karaf
             return;
         }
         
@@ -396,13 +400,13 @@ public class UnomiCrudCommand implements Action {
      * Find and execute the CrudCommand for the given type.
      * 
      * @param console console for output
-     * @return the result of the operation, or null if no handler found
+     * @return true if a handler was found and executed, false otherwise
      * @throws Exception if the operation fails
      */
-    private Object findAndExecuteCommand(PrintStream console) throws Exception 
{
+    private boolean findAndExecuteCommand(PrintStream console) throws 
Exception {
         ServiceReference<?>[] refs = 
bundleContext.getAllServiceReferences(CrudCommand.class.getName(), null);
         if (refs == null) {
-            return null;
+            return false;
         }
         
         String operationLower = operation.toLowerCase();
@@ -410,13 +414,14 @@ public class UnomiCrudCommand implements Action {
             CrudCommand cmd = (CrudCommand) bundleContext.getService(ref);
             if (cmd.getObjectType().equals(type)) {
                 try {
-                    return executeOperation(cmd, operationLower, console);
+                    executeOperation(cmd, operationLower, console);
+                    return true; // Handler found and executed
                 } finally {
                     bundleContext.ungetService(ref);
                 }
             }
         }
-        return null;
+        return false; // No handler found
     }
 
     @Override
@@ -427,11 +432,11 @@ public class UnomiCrudCommand implements Action {
             return null;
         }
         
-        Object result = findAndExecuteCommand(console);
-        if (result == null) {
+        boolean handlerFound = findAndExecuteCommand(console);
+        if (!handlerFound) {
             console.println("No handler found for object type: " + type);
         }
-        return result;
+        return null;
     }
 
     /**
@@ -541,16 +546,7 @@ public class UnomiCrudCommand implements Action {
         String id = remaining.get(0);
         String jsonOrUrl = remaining.get(1);
         
-        if (StringUtils.isBlank(id)) {
-            console.println("Error: ID is required for update operation");
-            return null;
-        }
-        
-        if (StringUtils.isBlank(jsonOrUrl)) {
-            console.println("Error: JSON string or URL is required for update 
operation");
-            return null;
-        }
-        
+        // hasMinimumRemainingArgs already ensures both id and jsonOrUrl are 
non-blank
         Map<String, Object> updateProps = 
parsePropertiesWithErrorHandling(jsonOrUrl, console);
         if (updateProps == null) {
             return null;
@@ -586,22 +582,27 @@ public class UnomiCrudCommand implements Action {
         // Parse list-specific options from remaining argument
         parseListOptions(remaining);
         
-        ShellTable table = new ShellTable();
-        if (csv) {
-            table.noHeaders().separator(",");
-        }
         String[] headers = cmd.getHeaders();
         if (headers == null || headers.length == 0) {
             console.println("Error: No headers available for " + type);
             return null;
         }
-        for (String header : headers) {
-            table.column(header);
-        }
+        
         // Ensure limit is positive (default to 100 if null or invalid)
         int limit = (maxEntries != null && maxEntries > 0) ? maxEntries : 100;
-        cmd.buildRows(table, limit);
-        table.print(console, !csv);
+        
+        if (csv) {
+            // Generate proper CSV output using Apache Commons CSV
+            cmd.buildCsvOutput(console, headers, limit);
+        } else {
+            // Generate table output
+            ShellTable table = new ShellTable();
+            for (String header : headers) {
+                table.column(header);
+            }
+            cmd.buildRows(table, limit);
+            table.print(console, true);
+        }
         return null;
     }
 }
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/CacheCommands.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/CacheCommands.java
index b82869891..110990e51 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/CacheCommands.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/CacheCommands.java
@@ -16,12 +16,13 @@
  */
 package org.apache.unomi.shell.dev.commands;
 
-import org.apache.karaf.shell.api.action.Action;
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVPrinter;
 import org.apache.karaf.shell.api.action.Command;
 import org.apache.karaf.shell.api.action.Option;
 import org.apache.karaf.shell.api.action.lifecycle.Reference;
 import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.karaf.shell.api.console.Session;
+import org.apache.karaf.shell.support.table.ShellTable;
 
 import org.apache.unomi.api.services.ExecutionContextManager;
 import org.apache.unomi.api.services.cache.MultiTypeCacheService;
@@ -32,7 +33,9 @@ import 
org.apache.unomi.shell.dev.commands.TenantContextHelper;
 
 import java.io.PrintStream;
 import java.io.Serializable;
+import java.util.ArrayList;
 import java.util.Date;
+import java.util.List;
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
@@ -73,6 +76,9 @@ public class CacheCommands extends BaseSimpleCommand {
     @Option(name = "--watch", description = "Watch cache statistics (refresh 
interval in seconds)", required = false)
     private int watchInterval = 0;
 
+    @Option(name = "--csv", description = "Output statistics in CSV format", 
required = false)
+    private boolean csv = false;
+
     @Option(name = "--id", description = "Specific entry ID to view or 
remove", required = false)
     private String entryId;
 
@@ -234,18 +240,20 @@ public class CacheCommands extends BaseSimpleCommand {
         CacheStatistics stats = cacheService.getStatistics();
         Map<String, TypeStatistics> allStats = stats.getAllStats();
 
+        if (allStats.isEmpty()) {
+            println("No cache statistics available");
+            return;
+        }
+
         if (type != null) {
             TypeStatistics typeStats = allStats.get(type);
             if (typeStats == null) {
                 println("No statistics available for type: " + type);
                 return;
             }
-            printTypeStats(type, typeStats);
+            displayStatisticsTable(Map.of(type, typeStats));
         } else {
-            for (Map.Entry<String, TypeStatistics> entry : 
allStats.entrySet()) {
-                printTypeStats(entry.getKey(), entry.getValue());
-                println("---");
-            }
+            displayStatisticsTable(allStats);
         }
 
         if (reset) {
@@ -254,27 +262,90 @@ public class CacheCommands extends BaseSimpleCommand {
         }
     }
 
-    private void printTypeStats(String type, TypeStatistics stats) {
-        println("Statistics for type: " + type);
-        println("  Hits: " + stats.getHits());
-        println("  Misses: " + stats.getMisses());
-        println("  Updates: " + stats.getUpdates());
-        println("  Validation Failures: " + stats.getValidationFailures());
-        println("  Indexing Errors: " + stats.getIndexingErrors());
+    private void displayStatisticsTable(Map<String, TypeStatistics> allStats) {
+        PrintStream console = getConsole();
+        
+        // Build headers
+        List<String> headers = new ArrayList<>();
+        headers.add("Type");
+        headers.add("Hits");
+        headers.add("Misses");
+        headers.add("Updates");
+        headers.add("Validation Failures");
+        headers.add("Indexing Errors");
+        headers.add("Hit Ratio (%)");
+        headers.add("Miss Ratio (%)");
+        if (detailed) {
+            headers.add("Efficiency Score");
+            headers.add("Error Rate (%)");
+        }
+
+        if (csv) {
+            // Generate CSV output
+            try {
+                CSVFormat csvFormat = CSVFormat.DEFAULT;
+                CSVPrinter printer = csvFormat.print(console);
+                
+                // Print header
+                printer.printRecord(headers.toArray());
+                
+                // Print data rows
+                for (Map.Entry<String, TypeStatistics> entry : 
allStats.entrySet()) {
+                    List<String> row = buildStatisticsRow(entry.getKey(), 
entry.getValue());
+                    printer.printRecord(row.toArray());
+                }
+                
+                printer.close();
+            } catch (Exception e) {
+                console.println("Error generating CSV output: " + 
e.getMessage());
+            }
+        } else {
+            // Generate table output
+            ShellTable table = new ShellTable();
+            for (String header : headers) {
+                table.column(header);
+            }
+            
+            for (Map.Entry<String, TypeStatistics> entry : 
allStats.entrySet()) {
+                List<String> row = buildStatisticsRow(entry.getKey(), 
entry.getValue());
+                table.addRow().addContent(row.toArray());
+            }
+            
+            table.print(console);
+        }
+    }
+
+    private List<String> buildStatisticsRow(String type, TypeStatistics stats) 
{
+        List<String> row = new ArrayList<>();
+        row.add(type);
+        row.add(String.valueOf(stats.getHits()));
+        row.add(String.valueOf(stats.getMisses()));
+        row.add(String.valueOf(stats.getUpdates()));
+        row.add(String.valueOf(stats.getValidationFailures()));
+        row.add(String.valueOf(stats.getIndexingErrors()));
 
         long total = stats.getHits() + stats.getMisses();
         if (total > 0) {
             double hitRatio = (double) stats.getHits() / total * 100;
             double missRatio = (double) stats.getMisses() / total * 100;
-            printf("  Hit Ratio: %.2f%%\n", hitRatio);
-            printf("  Miss Ratio: %.2f%%\n", missRatio);
+            row.add(String.format("%.2f", hitRatio));
+            row.add(String.format("%.2f", missRatio));
 
             if (detailed) {
-                printf("  Efficiency Score: %.2f\n", 
calculateEfficiencyScore(stats));
-                printf("  Error Rate: %.2f%%\n",
-                    (double)(stats.getValidationFailures() + 
stats.getIndexingErrors()) / total * 100);
+                row.add(String.format("%.2f", 
calculateEfficiencyScore(stats)));
+                double errorRate = (double)(stats.getValidationFailures() + 
stats.getIndexingErrors()) / total * 100;
+                row.add(String.format("%.2f", errorRate));
+            }
+        } else {
+            row.add("0.00");
+            row.add("0.00");
+            if (detailed) {
+                row.add("0.00");
+                row.add("0.00");
             }
         }
+
+        return row;
     }
 
     private double calculateEfficiencyScore(TypeStatistics stats) {
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/actions/ActionTypeCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/actions/ActionTypeCrudCommand.java
index 3c52088f2..319464aef 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/actions/ActionTypeCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/actions/ActionTypeCrudCommand.java
@@ -68,16 +68,7 @@ public class ActionTypeCrudCommand extends BaseCrudCommand {
     @Override
     protected PartialList<?> getItems(Query query) {
         List<ActionType> actionTypes = new 
ArrayList<>(definitionsService.getAllActionTypes());
-
-        // Apply query limit
-        Integer offset = query.getOffset();
-        Integer limit = query.getLimit();
-        int start = offset == null ? 0 : offset;
-        int size = limit == null ? actionTypes.size() : limit;
-        int end = Math.min(start + size, actionTypes.size());
-
-        List<ActionType> pagedActionTypes = actionTypes.subList(start, end);
-        return new PartialList<>(pagedActionTypes, start, 
pagedActionTypes.size(), actionTypes.size(), PartialList.Relation.EQUAL);
+        return paginateList(actionTypes, query);
     }
 
     @Override
@@ -146,9 +137,7 @@ public class ActionTypeCrudCommand extends BaseCrudCommand {
 
     @Override
     public List<String> completePropertyNames(String prefix) {
-        return PROPERTY_NAMES.stream()
-                .filter(name -> name.startsWith(prefix))
-                .collect(Collectors.toList());
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
     }
 
     @Override
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/apikeys/ApiKeyCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/apikeys/ApiKeyCrudCommand.java
index f9b2ec8c6..d91446ec8 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/apikeys/ApiKeyCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/apikeys/ApiKeyCrudCommand.java
@@ -186,9 +186,7 @@ public class ApiKeyCrudCommand extends BaseCrudCommand {
 
     @Override
     public List<String> completePropertyNames(String prefix) {
-        return PROPERTY_NAMES.stream()
-                .filter(name -> name.startsWith(prefix))
-                .collect(Collectors.toList());
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
     }
 
     @Override
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignCrudCommand.java
index 42e589f55..caa609f47 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignCrudCommand.java
@@ -125,9 +125,7 @@ public class CampaignCrudCommand extends BaseCrudCommand {
 
     @Override
     public List<String> completePropertyNames(String prefix) {
-        return PROPERTY_NAMES.stream()
-            .filter(name -> name.startsWith(prefix))
-            .collect(Collectors.toList());
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
     }
 
     @Override
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignEventCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignEventCrudCommand.java
index 16bd1352f..8096e9dcb 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignEventCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/campaigns/CampaignEventCrudCommand.java
@@ -108,11 +108,6 @@ public class CampaignEventCrudCommand extends 
BaseCrudCommand {
 
     @Override
     public void update(String id, Map<String, Object> properties) {
-        // First check if the event exists
-        if (read(id) == null) {
-            return;
-        }
-
         CampaignEvent updatedEvent = OBJECT_MAPPER.convertValue(properties, 
CampaignEvent.class);
         updatedEvent.setItemId(id);
         goalsService.setCampaignEvent(updatedEvent);
@@ -120,17 +115,12 @@ public class CampaignEventCrudCommand extends 
BaseCrudCommand {
 
     @Override
     public void delete(String id) {
-        // First check if the event exists
-        if (read(id) != null) {
-            goalsService.removeCampaignEvent(id);
-        }
+        goalsService.removeCampaignEvent(id);
     }
 
     @Override
     public List<String> completePropertyNames(String prefix) {
-        return PROPERTY_NAMES.stream()
-            .filter(name -> name.startsWith(prefix))
-            .collect(Collectors.toList());
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
     }
 
     @Override
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/conditions/ConditionTypeCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/conditions/ConditionTypeCrudCommand.java
index ef2f10367..b98560290 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/conditions/ConditionTypeCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/conditions/ConditionTypeCrudCommand.java
@@ -124,9 +124,7 @@ public class ConditionTypeCrudCommand extends 
BaseCrudCommand {
 
     @Override
     public List<String> completePropertyNames(String prefix) {
-        return PROPERTY_NAMES.stream()
-                .filter(name -> name.startsWith(prefix))
-                .collect(Collectors.toList());
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
     }
 
     @Override
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/consents/ConsentCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/consents/ConsentCrudCommand.java
index cf9c089bb..ed59c4288 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/consents/ConsentCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/consents/ConsentCrudCommand.java
@@ -224,9 +224,7 @@ public class ConsentCrudCommand extends BaseCrudCommand {
 
     @Override
     public List<String> completePropertyNames(String prefix) {
-        return PROPERTY_NAMES.stream()
-            .filter(name -> name.startsWith(prefix))
-            .collect(Collectors.toList());
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
     }
 
     @Override
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/events/EventCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/events/EventCrudCommand.java
index 33551ca28..697867bd3 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/events/EventCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/events/EventCrudCommand.java
@@ -151,9 +151,7 @@ public class EventCrudCommand extends BaseCrudCommand {
 
     @Override
     public List<String> completePropertyNames(String prefix) {
-        return PROPERTY_NAMES.stream()
-                .filter(name -> name.startsWith(prefix))
-                .collect(Collectors.toList());
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
     }
 
     @Override
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/goals/GoalCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/goals/GoalCrudCommand.java
index 43ca2f92a..38a7e77ef 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/goals/GoalCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/goals/GoalCrudCommand.java
@@ -119,9 +119,7 @@ public class GoalCrudCommand extends BaseCrudCommand {
 
     @Override
     public List<String> completePropertyNames(String prefix) {
-        return PROPERTY_NAMES.stream()
-            .filter(name -> name.startsWith(prefix))
-            .collect(Collectors.toList());
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
     }
 
     @Override
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/personas/PersonaCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/personas/PersonaCrudCommand.java
index 85d3871e4..45e0b8845 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/personas/PersonaCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/personas/PersonaCrudCommand.java
@@ -140,9 +140,7 @@ public class PersonaCrudCommand extends BaseCrudCommand {
 
     @Override
     public List<String> completePropertyNames(String prefix) {
-        return PROPERTY_NAMES.stream()
-                .filter(name -> name.startsWith(prefix))
-                .collect(Collectors.toList());
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
     }
 
     @Override
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileAliasCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileAliasCrudCommand.java
index d3d896bb9..64b27af14 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileAliasCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileAliasCrudCommand.java
@@ -77,8 +77,8 @@ public class ProfileAliasCrudCommand extends BaseCrudCommand {
     @Override
     public Map<String, Object> read(String id) {
         // Since there's no direct method to get a single alias, we'll need to 
search for it
-        // We'll use findProfileAliases with a small limit since we know the ID
-        PartialList<ProfileAlias> aliases = 
profileService.findProfileAliases(null, 0, 1, null);
+        // Search with a reasonable limit to find the alias by ID
+        PartialList<ProfileAlias> aliases = 
profileService.findProfileAliases(null, 0, 100, null);
         ProfileAlias alias = aliases.getList().stream()
             .filter(a -> a.getItemId().equals(id))
             .findFirst()
@@ -108,13 +108,14 @@ public class ProfileAliasCrudCommand extends 
BaseCrudCommand {
 
     @Override
     public void update(String id, Map<String, Object> properties) {
-        // First check if the alias exists
-        if (read(id) == null) {
+        // Get the existing alias (check if it exists and get its data in one 
call)
+        Map<String, Object> aliasData = read(id);
+        if (aliasData == null) {
             return;
         }
 
         // Remove the old alias and add the new one
-        ProfileAlias oldAlias = OBJECT_MAPPER.convertValue(read(id), 
ProfileAlias.class);
+        ProfileAlias oldAlias = OBJECT_MAPPER.convertValue(aliasData, 
ProfileAlias.class);
         profileService.removeAliasFromProfile(oldAlias.getProfileID(), 
oldAlias.getItemId(), oldAlias.getClientID());
 
         ProfileAlias updatedAlias = OBJECT_MAPPER.convertValue(properties, 
ProfileAlias.class);
@@ -135,9 +136,7 @@ public class ProfileAliasCrudCommand extends 
BaseCrudCommand {
 
     @Override
     public List<String> completePropertyNames(String prefix) {
-        return PROPERTY_NAMES.stream()
-            .filter(name -> name.startsWith(prefix))
-            .collect(Collectors.toList());
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
     }
 
     @Override
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileCrudCommand.java
index dc6f96d8b..3dc1c6f0c 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/profiles/ProfileCrudCommand.java
@@ -144,9 +144,7 @@ public class ProfileCrudCommand extends BaseCrudCommand {
 
     @Override
     public List<String> completePropertyNames(String prefix) {
-        return PROPERTY_NAMES.stream()
-            .filter(name -> name.startsWith(prefix))
-            .collect(Collectors.toList());
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
     }
 
     @Override
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/properties/PropertyTypeCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/properties/PropertyTypeCrudCommand.java
index f8eaa5a23..55b31301f 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/properties/PropertyTypeCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/properties/PropertyTypeCrudCommand.java
@@ -70,7 +70,7 @@ public class PropertyTypeCrudCommand extends BaseCrudCommand {
     }
 
     @Override
-    public PartialList<?> getItems(Query query) {
+    protected PartialList<?> getItems(Query query) {
         Map<String, Collection<PropertyType>> propertyTypesByTarget = 
profileService.getTargetPropertyTypes();
         List<PropertyType> propertyTypes = new ArrayList<>();
         
@@ -79,17 +79,7 @@ public class PropertyTypeCrudCommand extends BaseCrudCommand 
{
             propertyTypes.addAll(typeCollection);
         }
         
-        Integer start = query.getOffset();
-        Integer size = query.getLimit();
-        if (start == null) {
-            start = 0;
-        }
-        if (size == null) {
-            size = 50;
-        }
-        int end = Math.min(start + size, propertyTypes.size());
-        List<PropertyType> pagedPropertyTypes = propertyTypes.subList(start, 
end);
-        return new PartialList<>(pagedPropertyTypes, start, 
pagedPropertyTypes.size(), propertyTypes.size(), PartialList.Relation.EQUAL);
+        return paginateList(propertyTypes, query);
     }
 
     @Override
@@ -164,9 +154,7 @@ public class PropertyTypeCrudCommand extends 
BaseCrudCommand {
 
     @Override
     public List<String> completePropertyNames(String prefix) {
-        return PROPERTY_NAMES.stream()
-                .filter(name -> name.startsWith(prefix))
-                .collect(Collectors.toList());
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
     }
 
     @Override
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleCrudCommand.java
index 52def49f6..64d523926 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleCrudCommand.java
@@ -163,9 +163,7 @@ public class RuleCrudCommand extends BaseCrudCommand {
 
     @Override
     public List<String> completePropertyNames(String prefix) {
-        return PROPERTY_NAMES.stream()
-                .filter(name -> name.startsWith(prefix))
-                .collect(Collectors.toList());
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
     }
 
     @Override
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleStatisticsCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleStatisticsCrudCommand.java
index 9e0c072ee..8a0507bdd 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleStatisticsCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/rules/RuleStatisticsCrudCommand.java
@@ -111,9 +111,7 @@ public class RuleStatisticsCrudCommand extends 
BaseCrudCommand {
 
     @Override
     public List<String> completePropertyNames(String prefix) {
-        return PROPERTY_NAMES.stream()
-            .filter(name -> name.startsWith(prefix))
-            .collect(Collectors.toList());
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
     }
 
     @Override
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scopes/ScopeCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scopes/ScopeCrudCommand.java
index bcb9f7fee..10bef1fca 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scopes/ScopeCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scopes/ScopeCrudCommand.java
@@ -133,9 +133,7 @@ public class ScopeCrudCommand extends BaseCrudCommand {
 
     @Override
     public List<String> completePropertyNames(String prefix) {
-        return PROPERTY_NAMES.stream()
-                .filter(name -> name.startsWith(prefix))
-                .collect(Collectors.toList());
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
     }
 
 }
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scoring/ScoringCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scoring/ScoringCrudCommand.java
index 4a3a30970..78fd511dd 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scoring/ScoringCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/scoring/ScoringCrudCommand.java
@@ -118,9 +118,7 @@ public class ScoringCrudCommand extends BaseCrudCommand {
 
     @Override
     public List<String> completePropertyNames(String prefix) {
-        return PROPERTY_NAMES.stream()
-                .filter(name -> name.startsWith(prefix))
-                .collect(Collectors.toList());
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
     }
 
     @Override
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/segments/SegmentCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/segments/SegmentCrudCommand.java
index 5b93b0636..85e16aecc 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/segments/SegmentCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/segments/SegmentCrudCommand.java
@@ -148,9 +148,7 @@ public class SegmentCrudCommand extends BaseCrudCommand {
 
     @Override
     public List<String> completePropertyNames(String prefix) {
-        return PROPERTY_NAMES.stream()
-                .filter(name -> name.startsWith(prefix))
-                .collect(Collectors.toList());
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
     }
 
     @Override
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/sessions/SessionCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/sessions/SessionCrudCommand.java
index 2a79be13e..a185163d1 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/sessions/SessionCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/sessions/SessionCrudCommand.java
@@ -92,11 +92,6 @@ public class SessionCrudCommand extends BaseCrudCommand {
 
     @Override
     public void update(String id, Map<String, Object> properties) {
-        // First check if the session exists
-        if (read(id) == null) {
-            return;
-        }
-
         Session updatedSession = OBJECT_MAPPER.convertValue(properties, 
Session.class);
         updatedSession.setItemId(id);
         profileService.saveSession(updatedSession);
@@ -104,17 +99,12 @@ public class SessionCrudCommand extends BaseCrudCommand {
 
     @Override
     public void delete(String id) {
-        // First check if the session exists
-        if (read(id) != null) {
-            profileService.deleteSession(id);
-        }
+        profileService.deleteSession(id);
     }
 
     @Override
     public List<String> completePropertyNames(String prefix) {
-        return PROPERTY_NAMES.stream()
-            .filter(name -> name.startsWith(prefix))
-            .collect(Collectors.toList());
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
     }
 
     @Override
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/tenants/TenantCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/tenants/TenantCrudCommand.java
index c099e42a5..cd809bacf 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/tenants/TenantCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/tenants/TenantCrudCommand.java
@@ -21,8 +21,6 @@ import org.apache.karaf.shell.support.table.ShellTable;
 
 import java.io.PrintStream;
 import org.apache.unomi.api.PartialList;
-import org.apache.unomi.api.conditions.Condition;
-import org.apache.unomi.api.conditions.ConditionType;
 import org.apache.unomi.api.query.Query;
 import org.apache.unomi.api.tenants.*;
 import org.apache.unomi.common.DataTable;
@@ -31,6 +29,8 @@ import org.apache.unomi.shell.dev.services.BaseCrudCommand;
 import org.apache.unomi.shell.dev.services.CrudCommand;
 import org.osgi.service.component.annotations.Component;
 import org.osgi.service.component.annotations.Reference;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.util.*;
 import java.util.stream.Collectors;
@@ -41,6 +41,7 @@ import java.util.stream.Collectors;
 @Component(service = CrudCommand.class, immediate = true)
 public class TenantCrudCommand extends BaseCrudCommand {
 
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(TenantCrudCommand.class.getName());
     private static final ObjectMapper OBJECT_MAPPER = new CustomObjectMapper();
     private static final List<String> PROPERTY_NAMES = List.of(
         "itemId", "name", "description", "status", "creationDate", 
"lastModificationDate", "resourceQuota", "properties", 
"restrictedEventPermissions", "authorizedIPs"
@@ -73,25 +74,25 @@ public class TenantCrudCommand extends BaseCrudCommand {
      */
     @Override
     protected DataTable buildDataTable() {
-        Query query = new Query();
-        query.setLimit(maxEntries);
-        Condition matchAllCondition = new 
Condition(definitionsService.getConditionType("matchAllCondition"));
-        query.setCondition(matchAllCondition);
-        query.setSortby(getSortBy());
+        PrintStream console = getConsole();
+        try {
+            Query query = buildQuery(maxEntries);
+            PartialList<?> items = getItems(query);
+            
+            printPaginationWarning(items, console);
 
-        PartialList<?> items = getItems(query);
-        if (items.getList().size() != items.getTotalSize()) {
-            PrintStream console = getConsole();
-            console.println("WARNING : Only the first " + items.getPageSize() 
+ " items have been retrieved, there are " + items.getTotalSize() + " items 
registered in total. Use the maxEntries parameter to retrieve more items");
-        }
+            DataTable dataTable = new DataTable();
+            for (Object item : items.getList()) {
+                Comparable[] rowData = buildRow(item);
+                dataTable.addRow(rowData);
+            }
 
-        DataTable dataTable = new DataTable();
-        for (Object item : items.getList()) {
-            Comparable[] rowData = buildRow(item);
-            dataTable.addRow(rowData);
+            return dataTable;
+        } catch (Exception e) {
+            LOGGER.error("Error building data table", e);
+            console.println("Error: " + e.getMessage());
+            return new DataTable();
         }
-
-        return dataTable;
     }
 
     /**
@@ -99,29 +100,20 @@ public class TenantCrudCommand extends BaseCrudCommand {
      */
     @Override
     public void buildRows(ShellTable table, int maxEntries) {
-        Query query = new Query();
-        query.setLimit(maxEntries);
         PrintStream console = getConsole();
-        if (definitionsService == null) {
-            console.println("Error: No definitions service available, unable 
to build rows");
-            return;
-        }
-        ConditionType matchAllConditionType = 
definitionsService.getConditionType("matchAllCondition");
-        if (matchAllConditionType == null) {
-            console.println("Error: No matchAllCondition available, unable to 
build rows");
-        }
-        Condition matchAllCondition = new Condition(matchAllConditionType);
-        query.setCondition(matchAllCondition);
-        query.setSortby(getSortBy());
-
-        PartialList<?> items = getItems(query);
-        if (items.getList().size() != items.getTotalSize()) {
-            console.println("WARNING : Only the first " + items.getPageSize() 
+ " items have been retrieved, there are " + items.getTotalSize() + " items 
registered in total. Use the maxEntries parameter to retrieve more items");
-        }
+        try {
+            Query query = buildQuery(maxEntries);
+            PartialList<?> items = getItems(query);
+            
+            printPaginationWarning(items, console);
 
-        for (Object item : items.getList()) {
-            Comparable[] rowData = buildRow(item);
-            table.addRow().addContent(rowData);
+            for (Object item : items.getList()) {
+                Comparable[] rowData = buildRow(item);
+                table.addRow().addContent(rowData);
+            }
+        } catch (Exception e) {
+            console.println("Error: " + e.getMessage());
+            LOGGER.error("Error building rows", e);
         }
     }
 
@@ -247,9 +239,7 @@ public class TenantCrudCommand extends BaseCrudCommand {
 
     @Override
     public List<String> completePropertyNames(String prefix) {
-        return PROPERTY_NAMES.stream()
-            .filter(name -> name.startsWith(prefix))
-            .collect(Collectors.toList());
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
     }
 
     @Override
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/topics/TopicCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/topics/TopicCrudCommand.java
index fda6e6b5b..f97bed28d 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/topics/TopicCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/commands/topics/TopicCrudCommand.java
@@ -115,8 +115,6 @@ public class TopicCrudCommand extends BaseCrudCommand {
 
     @Override
     public List<String> completePropertyNames(String prefix) {
-        return PROPERTY_NAMES.stream()
-                .filter(name -> name.startsWith(prefix))
-                .collect(Collectors.toList());
+        return filterPropertyNames(PROPERTY_NAMES, prefix);
     }
 }
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/CampaignCompleter.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/CampaignCompleter.java
deleted file mode 100644
index 3b65ea1bb..000000000
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/CampaignCompleter.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.unomi.shell.dev.completers;
-
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.campaigns.Campaign;
-
-/**
- * Completer for Campaign IDs
- */
-@Service
-public class CampaignCompleter extends BaseCompleter<Campaign> {
-
-    @Override
-    protected Class<Campaign> getItemType() {
-        return Campaign.class;
-    }
-
-    @Override
-    protected String getSortBy() {
-        return "metadata.name";
-    }
-}
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/EventTypeCompleter.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/EventTypeCompleter.java
deleted file mode 100644
index 086238ef0..000000000
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/EventTypeCompleter.java
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.unomi.shell.dev.completers;
-
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.karaf.shell.api.console.CommandLine;
-import org.apache.karaf.shell.api.console.Completer;
-import org.apache.karaf.shell.api.console.Session;
-import org.apache.karaf.shell.support.completers.StringsCompleter;
-import org.apache.unomi.api.services.EventService;
-
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-@Service
-public class EventTypeCompleter implements Completer {
-
-    @Reference
-    private EventService eventService;
-
-    @Override
-    public int complete(Session session, CommandLine commandLine, List<String> 
candidates) {
-        StringsCompleter delegate = new StringsCompleter();
-
-        // Add common event types
-        Set<String> eventTypes = new HashSet<>();
-        eventTypes.add("view");
-        eventTypes.add("login");
-        eventTypes.add("sessionCreated");
-        eventTypes.add("profileUpdated");
-        eventTypes.add("sessionReassigned");
-        eventTypes.add("updateProperties");
-        eventTypes.add("formSubmitted");
-        eventTypes.add("click");
-        eventTypes.add("download");
-        eventTypes.add("search");
-        eventTypes.add("videoStarted");
-        eventTypes.add("videoCompleted");
-
-        // Add event types to completer
-        for (String eventType : eventTypes) {
-            delegate.getStrings().add(eventType);
-        }
-
-        return delegate.complete(session, commandLine, candidates);
-    }
-}
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/GoalCompleter.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/GoalCompleter.java
deleted file mode 100644
index 145946081..000000000
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/GoalCompleter.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.unomi.shell.dev.completers;
-
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.goals.Goal;
-
-/**
- * Completer for Goal IDs
- */
-@Service
-public class GoalCompleter extends BaseCompleter<Goal> {
-
-    @Override
-    protected Class<Goal> getItemType() {
-        return Goal.class;
-    }
-
-    @Override
-    protected String getSortBy() {
-        return "metadata.name";
-    }
-}
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/IPAddressCompleter.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/IPAddressCompleter.java
deleted file mode 100644
index bd16c5d45..000000000
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/IPAddressCompleter.java
+++ /dev/null
@@ -1,54 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.unomi.shell.dev.completers;
-
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.karaf.shell.api.console.CommandLine;
-import org.apache.karaf.shell.api.console.Completer;
-import org.apache.karaf.shell.api.console.Session;
-import org.apache.karaf.shell.support.completers.StringsCompleter;
-
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-@Service
-public class IPAddressCompleter implements Completer {
-
-    @Override
-    public int complete(Session session, CommandLine commandLine, List<String> 
candidates) {
-        StringsCompleter delegate = new StringsCompleter();
-
-        // Add common IP addresses and ranges
-        Set<String> ipAddresses = new HashSet<>();
-        ipAddresses.add("127.0.0.1");
-        ipAddresses.add("localhost");
-        ipAddresses.add("0.0.0.0");
-        ipAddresses.add("::1");
-        ipAddresses.add("192.168.0.0/16");
-        ipAddresses.add("10.0.0.0/8");
-        ipAddresses.add("172.16.0.0/12");
-        ipAddresses.add("fc00::/7");
-
-        // Add IP addresses to completer
-        for (String ip : ipAddresses) {
-            delegate.getStrings().add(ip);
-        }
-
-        return delegate.complete(session, commandLine, candidates);
-    }
-}
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/IdCompleter.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/IdCompleter.java
index 1790f9267..71f6df412 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/IdCompleter.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/IdCompleter.java
@@ -46,15 +46,16 @@ public class IdCompleter implements Completer {
 
     @Override
     public int complete(Session session, CommandLine commandLine, List<String> 
candidates) {
-        // Get the operation and type from the command line (index 0 = 
operation, index 1 = type)
+        // Get the operation and type from the command line
+        // args[0] = "crud" (command name), args[1] = operation, args[2] = 
type, args[3+] = remaining
         String operation = null;
         String type = null;
         String[] args = commandLine.getArguments();
-        if (args.length > 0) {
-            operation = args[0];
-        }
         if (args.length > 1) {
-            type = args[1];
+            operation = args[1];
+        }
+        if (args.length > 2) {
+            type = args[2];
         }
         if (type == null) {
             return -1;
@@ -70,13 +71,26 @@ public class IdCompleter implements Completer {
             }
         }
 
-        // For update operation, check if we're completing the first argument 
(ID) or second (JSON)
-        // remaining[0] = ID, remaining[1] = JSON
-        // If args.length > 2, we're completing remaining[1] (JSON), so don't 
complete IDs
-        if (operation != null && "update".equals(operation.toLowerCase()) && 
args.length > 2) {
+        // Determine which argument we're completing based on args.length
+        // args[0] = "crud" (command name)
+        // args[1] = operation
+        // args[2] = type
+        // args[3+] = remaining (multi-valued)
+        // For read/delete: remaining[0] = ID (complete when args.length == 3, 
i.e., we're at remaining[0])
+        // For update: remaining[0] = ID, remaining[1] = JSON
+        //   - Complete IDs when args.length == 3 (completing remaining[0], 
which is the ID)
+        //   - Don't complete IDs when args.length >= 4 (completing 
remaining[1], which is JSON)
+        
+        // For update operation, if args.length >= 4, we're past the ID 
argument
+        // and are completing the JSON part, so don't complete IDs
+        if (operation != null && "update".equals(operation.toLowerCase()) && 
args.length >= 4) {
             // We're past the ID argument, so we're completing JSON - don't 
complete IDs
             return -1;
         }
+        
+        // For read/delete/update with args.length == 3, we're completing the 
ID (remaining[0])
+        // The completer is attached to the "remaining" argument, so it will 
be called
+        // when we're at that position
 
         // Find the CrudCommand for this type
         try {
@@ -88,21 +102,7 @@ public class IdCompleter implements Completer {
                         if (cmd.getObjectType().equals(type)) {
                             // Get the prefix from what the user has typed so 
far
                             // StringsCompleter will handle the final 
matching, but we need prefix for server-side filtering
-                            String prefix = "";
-                            String buffer = commandLine.getBuffer();
-                            
-                            if (buffer != null && !buffer.trim().isEmpty()) {
-                                // Get the last word from the buffer (the 
current value being typed)
-                                String trimmed = buffer.trim();
-                                int lastSpace = trimmed.lastIndexOf(' ');
-                                if (lastSpace >= 0 && lastSpace < 
trimmed.length() - 1) {
-                                    prefix = trimmed.substring(lastSpace + 1);
-                                    // Skip if it looks like an option
-                                    if (prefix.startsWith("-")) {
-                                        prefix = "";
-                                    }
-                                }
-                            }
+                            String prefix = 
extractPrefixFromBuffer(commandLine);
                             
                             List<String> completions = cmd.completeId(prefix);
 
@@ -124,4 +124,33 @@ public class IdCompleter implements Completer {
 
         return -1;
     }
+
+    /**
+     * Extract the prefix from the command line buffer for completion.
+     * This extracts the last word being typed, skipping options that start 
with '-'.
+     *
+     * @param commandLine the command line
+     * @return the prefix to use for filtering completions, or empty string if 
none
+     */
+    private String extractPrefixFromBuffer(CommandLine commandLine) {
+        String buffer = commandLine.getBuffer();
+        if (buffer == null || buffer.trim().isEmpty()) {
+            return "";
+        }
+        
+        // Get the last word from the buffer (the current value being typed)
+        String trimmed = buffer.trim();
+        int lastSpace = trimmed.lastIndexOf(' ');
+        if (lastSpace < 0 || lastSpace >= trimmed.length() - 1) {
+            return "";
+        }
+        
+        String prefix = trimmed.substring(lastSpace + 1);
+        // Skip if it looks like an option
+        if (prefix.startsWith("-")) {
+            return "";
+        }
+        
+        return prefix;
+    }
 }
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/ProfileCompleter.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/ProfileCompleter.java
deleted file mode 100644
index 60cd251d1..000000000
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/ProfileCompleter.java
+++ /dev/null
@@ -1,70 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.unomi.shell.dev.completers;
-
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.karaf.shell.api.console.CommandLine;
-import org.apache.karaf.shell.api.console.Completer;
-import org.apache.karaf.shell.api.console.Session;
-import org.apache.karaf.shell.support.completers.StringsCompleter;
-import org.apache.unomi.api.PartialList;
-import org.apache.unomi.api.Profile;
-import org.apache.unomi.api.conditions.Condition;
-import org.apache.unomi.api.query.Query;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.api.services.ProfileService;
-
-import java.util.List;
-
-@Service
-public class ProfileCompleter implements Completer {
-
-    private static final int DEFAULT_LIMIT = 50;
-
-    @Reference
-    private ProfileService profileService;
-
-    @Reference
-    private DefinitionsService definitionsService;
-
-    @Override
-    public int complete(Session session, CommandLine commandLine, List<String> 
candidates) {
-        StringsCompleter delegate = new StringsCompleter();
-
-        try {
-            // Create query matching the profile-list command
-            Query query = new Query();
-            
query.setSortby("systemProperties.lastUpdated:desc,properties.lastVisit:desc");
-            query.setLimit(DEFAULT_LIMIT);
-            Condition matchAllCondition = new 
Condition(definitionsService.getConditionType("matchAllCondition"));
-            query.setCondition(matchAllCondition);
-
-            // Get the latest profiles
-            PartialList<Profile> profiles = profileService.search(query, 
Profile.class);
-
-            // Add profile IDs to completer
-            for (Profile profile : profiles.getList()) {
-                delegate.getStrings().add(profile.getItemId());
-            }
-        } catch (Exception e) {
-            // Log error or handle exception
-        }
-
-        return delegate.complete(session, commandLine, candidates);
-    }
-}
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/RuleCompleter.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/RuleCompleter.java
deleted file mode 100644
index b30da0d0c..000000000
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/RuleCompleter.java
+++ /dev/null
@@ -1,69 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.unomi.shell.dev.completers;
-
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.karaf.shell.api.console.CommandLine;
-import org.apache.karaf.shell.api.console.Completer;
-import org.apache.karaf.shell.api.console.Session;
-import org.apache.karaf.shell.support.completers.StringsCompleter;
-import org.apache.unomi.api.Metadata;
-import org.apache.unomi.api.PartialList;
-import org.apache.unomi.api.conditions.Condition;
-import org.apache.unomi.api.query.Query;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.api.services.RulesService;
-
-import java.util.List;
-
-@Service
-public class RuleCompleter implements Completer {
-
-    private static final int DEFAULT_LIMIT = 50;
-
-    @Reference
-    private RulesService rulesService;
-
-    @Reference
-    private DefinitionsService definitionsService;
-
-    @Override
-    public int complete(Session session, CommandLine commandLine, List<String> 
candidates) {
-        StringsCompleter delegate = new StringsCompleter();
-
-        try {
-            // Create query matching the rule-list command
-            Query query = new Query();
-            query.setLimit(DEFAULT_LIMIT);
-            Condition matchAllCondition = new 
Condition(definitionsService.getConditionType("matchAllCondition"));
-            query.setCondition(matchAllCondition);
-
-            // Get all rules
-            PartialList<Metadata> rules = rulesService.getRuleMetadatas(query);
-
-            // Add rule IDs to completer
-            for (Metadata rule : rules.getList()) {
-                delegate.getStrings().add(rule.getId());
-            }
-        } catch (Exception e) {
-            // Log error or handle exception
-        }
-
-        return delegate.complete(session, commandLine, candidates);
-    }
-}
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/ScopeCompleter.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/ScopeCompleter.java
deleted file mode 100644
index 391763399..000000000
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/ScopeCompleter.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.unomi.shell.dev.completers;
-
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.Scope;
-
-/**
- * Completer for Scope IDs
- */
-@Service
-public class ScopeCompleter extends BaseCompleter<Scope> {
-
-    @Override
-    protected Class<Scope> getItemType() {
-        return Scope.class;
-    }
-
-    @Override
-    protected String getSortBy() {
-        return "metadata.name";
-    }
-}
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/ScoringCompleter.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/ScoringCompleter.java
deleted file mode 100644
index 7d9d310f2..000000000
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/ScoringCompleter.java
+++ /dev/null
@@ -1,37 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.unomi.shell.dev.completers;
-
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.unomi.api.segments.Scoring;
-
-/**
- * Completer for Scoring IDs
- */
-@Service
-public class ScoringCompleter extends BaseCompleter<Scoring> {
-
-    @Override
-    protected Class<Scoring> getItemType() {
-        return Scoring.class;
-    }
-
-    @Override
-    protected String getSortBy() {
-        return "metadata.name";
-    }
-}
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/SegmentCompleter.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/SegmentCompleter.java
deleted file mode 100644
index e5a47988c..000000000
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/SegmentCompleter.java
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.unomi.shell.dev.completers;
-
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.karaf.shell.api.console.CommandLine;
-import org.apache.karaf.shell.api.console.Completer;
-import org.apache.karaf.shell.api.console.Session;
-import org.apache.karaf.shell.support.completers.StringsCompleter;
-import org.apache.unomi.api.Metadata;
-import org.apache.unomi.api.PartialList;
-import org.apache.unomi.api.services.SegmentService;
-import org.osgi.service.component.annotations.Reference;
-
-@Service
-public class SegmentCompleter implements Completer {
-
-    private static final int DEFAULT_LIMIT = 50;
-
-    private SegmentService segmentService;
-
-    @Reference
-    public void setSegmentService(SegmentService segmentService) {
-        this.segmentService = segmentService;
-    }
-
-    @Override
-    public int complete(Session session, CommandLine commandLine, 
java.util.List<String> candidates) {
-        StringsCompleter delegate = new StringsCompleter();
-
-        try {
-            // Get segments sorted by name
-            PartialList<Metadata> segments = 
segmentService.getSegmentMetadatas(0, DEFAULT_LIMIT, "name:asc");
-            for (Metadata segment : segments.getList()) {
-                delegate.getStrings().add(segment.getId());
-            }
-        } catch (Exception e) {
-            // Log error or handle exception
-        }
-
-        return delegate.complete(session, commandLine, candidates);
-    }
-}
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/SessionCompleter.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/SessionCompleter.java
deleted file mode 100644
index ffa218b3d..000000000
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/SessionCompleter.java
+++ /dev/null
@@ -1,72 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.unomi.shell.dev.completers;
-
-import org.apache.karaf.shell.api.action.lifecycle.Reference;
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.karaf.shell.api.console.CommandLine;
-import org.apache.karaf.shell.api.console.Completer;
-import org.apache.karaf.shell.api.console.Session;
-import org.apache.karaf.shell.support.completers.StringsCompleter;
-import org.apache.unomi.api.PartialList;
-import org.apache.unomi.api.conditions.Condition;
-import org.apache.unomi.api.query.Query;
-import org.apache.unomi.api.services.DefinitionsService;
-import org.apache.unomi.api.services.ProfileService;
-
-import java.io.PrintStream;
-import java.util.List;
-
-@Service
-public class SessionCompleter implements Completer {
-
-    private static final int DEFAULT_LIMIT = 50;
-
-    @Reference
-    private ProfileService profileService;
-
-    @Reference
-    private DefinitionsService definitionsService;
-
-    @Override
-    public int complete(Session session, CommandLine commandLine, List<String> 
candidates) {
-        StringsCompleter delegate = new StringsCompleter();
-
-        try {
-            // Create query matching the session-list command
-            Query query = new Query();
-            query.setSortby("lastEventDate:desc");
-            query.setLimit(DEFAULT_LIMIT);
-            Condition matchAllCondition = new 
Condition(definitionsService.getConditionType("matchAllCondition"));
-            query.setCondition(matchAllCondition);
-
-            // Get the latest sessions
-            PartialList<org.apache.unomi.api.Session> sessions = 
profileService.searchSessions(query);
-
-            // Add session IDs to completer
-            for (org.apache.unomi.api.Session s : sessions.getList()) {
-                delegate.getStrings().add(s.getItemId());
-            }
-        } catch (Exception e) {
-            // Note: Printing during completion can interfere with completion, 
but using console for consistency
-            PrintStream console = session.getConsole();
-            console.println("Error: Error getting sessions: " + 
e.getMessage());
-        }
-
-        return delegate.complete(session, commandLine, candidates);
-    }
-}
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/TenantStatusCompleter.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/TenantStatusCompleter.java
deleted file mode 100644
index a63f73bb5..000000000
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/completers/TenantStatusCompleter.java
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Licensed to the Apache Software Foundation (ASF) under one or more
- * contributor license agreements.  See the NOTICE file distributed with
- * this work for additional information regarding copyright ownership.
- * The ASF licenses this file to You under the Apache License, Version 2.0
- * (the "License"); you may not use this file except in compliance with
- * the License.  You may obtain a copy of the License at
- *
- *      http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package org.apache.unomi.shell.dev.completers;
-
-import org.apache.karaf.shell.api.action.lifecycle.Service;
-import org.apache.karaf.shell.api.console.CommandLine;
-import org.apache.karaf.shell.api.console.Completer;
-import org.apache.karaf.shell.api.console.Session;
-import org.apache.karaf.shell.support.completers.StringsCompleter;
-import org.apache.unomi.api.tenants.TenantStatus;
-
-import java.util.List;
-
-@Service
-public class TenantStatusCompleter implements Completer {
-
-    @Override
-    public int complete(Session session, CommandLine commandLine, List<String> 
candidates) {
-        StringsCompleter delegate = new StringsCompleter();
-
-        // Add all tenant status values
-        for (TenantStatus status : TenantStatus.values()) {
-            delegate.getStrings().add(status.name());
-        }
-
-        return delegate.complete(session, commandLine, candidates);
-    }
-}
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/BaseCrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/BaseCrudCommand.java
index 997e17ef6..4260a6086 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/BaseCrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/BaseCrudCommand.java
@@ -16,6 +16,8 @@
  */
 package org.apache.unomi.shell.dev.services;
 
+import org.apache.commons.csv.CSVFormat;
+import org.apache.commons.csv.CSVPrinter;
 import org.apache.karaf.shell.api.action.Argument;
 import org.apache.karaf.shell.api.action.Option;
 import org.apache.karaf.shell.support.table.ShellTable;
@@ -39,6 +41,7 @@ import java.lang.reflect.Field;
 import java.lang.reflect.Method;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.stream.Collectors;
 
 /**
  * Base class for CRUD command implementations that provides common 
functionality
@@ -59,34 +62,25 @@ public abstract class BaseCrudCommand extends 
ListCommandSupport implements Crud
 
     @Override
     protected DataTable buildDataTable() {
-        Query query = new Query();
-        query.setLimit(maxEntries);
-        Condition matchAllCondition = new 
Condition(definitionsService.getConditionType("matchAllCondition"));
-        query.setCondition(matchAllCondition);
-        query.setSortby(getSortBy());
-
-        PartialList<?> items = getItems(query);
-        if (items.getList().size() != items.getTotalSize()) {
-            PrintStream console = getConsole();
-            console.println("WARNING : Only the first " + items.getPageSize() 
+ " items have been retrieved, there are " + items.getTotalSize() + " items 
registered in total. Use the maxEntries parameter to retrieve more items");
-        }
-
-        DataTable dataTable = new DataTable();
-        for (Object item : items.getList()) {
-            Comparable[] rowData = buildRow(item);
-
-            // Get tenant ID from the item if possible
-            String tenantId = getTenantIdFromItem(item);
+        PrintStream console = getConsole();
+        try {
+            Query query = buildQuery(maxEntries);
+            PartialList<?> items = getItems(query);
+            
+            printPaginationWarning(items, console);
 
-            // Create a new array with tenantId as the first element
-            Comparable[] rowWithTenant = new Comparable[rowData.length + 1];
-            rowWithTenant[0] = tenantId;
-            System.arraycopy(rowData, 0, rowWithTenant, 1, rowData.length);
+            DataTable dataTable = new DataTable();
+            for (Object item : items.getList()) {
+                Comparable[] rowWithTenant = buildRowWithTenant(item);
+                dataTable.addRow(rowWithTenant);
+            }
 
-            dataTable.addRow(rowWithTenant);
+            return dataTable;
+        } catch (Exception e) {
+            LOGGER.error("Error building data table", e);
+            console.println("Error: " + e.getMessage());
+            return new DataTable();
         }
-
-        return dataTable;
     }
 
     /**
@@ -147,43 +141,118 @@ public abstract class BaseCrudCommand extends 
ListCommandSupport implements Crud
      */
     protected abstract String[] getHeadersWithoutTenant();
 
-    @Override
-    public void buildRows(ShellTable table, int maxEntries) {
+    /**
+     * Build a query with matchAllCondition and sort criteria.
+     * This is a common helper method used by buildDataTable(), buildRows(), 
buildCsvOutput(), and completeId().
+     *
+     * @param limit maximum number of entries
+     * @return the configured query
+     * @throws Exception if definitions service is not available or 
matchAllCondition cannot be found
+     */
+    protected Query buildQuery(int limit) throws Exception {
         Query query = new Query();
-        query.setLimit(maxEntries);
-        PrintStream console = getConsole();
+        query.setLimit(limit);
+        
         if (definitionsService == null) {
-            console.println("Error: No definitions service available, unable 
to build rows");
-            LOGGER.error("Definition service is not available, unable to build 
rows");
-            return;
+            throw new Exception("Definitions service is not available");
         }
+        
         ConditionType matchAllConditionType = 
definitionsService.getConditionType("matchAllCondition");
         if (matchAllConditionType == null) {
-            console.println("Error: No matchAllCondition available, unable to 
build rows");
-            LOGGER.error("No matchAllCondition available, unable to build 
rows");
+            throw new Exception("No matchAllCondition available");
         }
+        
         Condition matchAllCondition = new Condition(matchAllConditionType);
         query.setCondition(matchAllCondition);
         query.setSortby(getSortBy());
+        
+        return query;
+    }
 
-        PartialList<?> items = getItems(query);
+    /**
+     * Build a row array with tenant ID prepended as the first element.
+     * This is a common helper method used by buildDataTable(), buildRows(), 
and buildCsvOutput().
+     *
+     * @param item the item to build a row from
+     * @return array with tenant ID as first element, followed by row data
+     */
+    protected Comparable[] buildRowWithTenant(Object item) {
+        Comparable[] rowData = buildRow(item);
+        String tenantId = getTenantIdFromItem(item);
+        
+        // Create a new array with tenantId as the first element
+        Comparable[] rowWithTenant = new Comparable[rowData.length + 1];
+        rowWithTenant[0] = tenantId;
+        System.arraycopy(rowData, 0, rowWithTenant, 1, rowData.length);
+        
+        return rowWithTenant;
+    }
+
+    /**
+     * Print pagination warning if not all items were retrieved.
+     * This is a common helper method used by buildDataTable() and buildRows().
+     *
+     * @param items the partial list of items
+     * @param console console for output
+     */
+    protected void printPaginationWarning(PartialList<?> items, PrintStream 
console) {
         if (items.getList().size() != items.getTotalSize()) {
             console.println("WARNING : Only the first " + items.getPageSize() 
+ " items have been retrieved, there are " + items.getTotalSize() + " items 
registered in total. Use the maxEntries parameter to retrieve more items");
         }
+    }
 
-        for (Object item : items.getList()) {
-            Comparable[] rowData = buildRow(item);
-
-            // Get tenant ID from the item if possible
-            String tenantId = getTenantIdFromItem(item);
+    @Override
+    public void buildRows(ShellTable table, int maxEntries) {
+        PrintStream console = getConsole();
+        try {
+            Query query = buildQuery(maxEntries);
+            PartialList<?> items = getItems(query);
+            
+            printPaginationWarning(items, console);
 
-            // Create a new array with tenantId as the first element
-            Comparable[] rowWithTenant = new Comparable[rowData.length + 1];
-            rowWithTenant[0] = tenantId;
-            System.arraycopy(rowData, 0, rowWithTenant, 1, rowData.length);
+            for (Object item : items.getList()) {
+                Comparable[] rowWithTenant = buildRowWithTenant(item);
+                table.addRow().addContent(rowWithTenant);
+            }
+        } catch (Exception e) {
+            console.println("Error: " + e.getMessage());
+            LOGGER.error("Error building rows", e);
+        }
+    }
 
-            table.addRow().addContent(rowWithTenant);
+    /**
+     * Generate CSV output directly using commons-csv API.
+     * This method uses the same logic as buildRows() but outputs as CSV.
+     *
+     * @param console console for output
+     * @param headers column headers
+     * @param limit maximum number of entries
+     * @throws Exception if generation fails
+     */
+    public void buildCsvOutput(PrintStream console, String[] headers, int 
limit) throws Exception {
+        Query query = buildQuery(limit);
+        PartialList<?> items = getItems(query);
+        
+        // Generate CSV directly using commons-csv
+        CSVFormat csvFormat = CSVFormat.DEFAULT;
+        CSVPrinter printer = csvFormat.print(console);
+        
+        // Print header
+        printer.printRecord((Object[]) headers);
+        
+        // Print data rows
+        for (Object item : items.getList()) {
+            Comparable[] rowWithTenant = buildRowWithTenant(item);
+            
+            // Convert to List<String> for CSV printer
+            List<String> row = new ArrayList<>();
+            for (Comparable<?> cell : rowWithTenant) {
+                row.add(cell != null ? cell.toString() : "");
+            }
+            printer.printRecord(row.toArray());
         }
+        
+        printer.close();
     }
 
     /**
@@ -217,19 +286,9 @@ public abstract class BaseCrudCommand extends 
ListCommandSupport implements Crud
      */
     @Override
     public List<String> completeId(String prefix) {
-        // Create a query with increased limit to provide more completions
-        Query query = new Query();
-        query.setLimit(50); // Higher limit for completions
-
-        if (definitionsService == null) {
-            LOGGER.error("Definition service is not available, unable to 
complete IDs");
-            return List.of();
-        }
-
         try {
-            Condition matchAllCondition = new 
Condition(definitionsService.getConditionType("matchAllCondition"));
-            query.setCondition(matchAllCondition);
-            query.setSortby(getSortBy());
+            // Create a query with increased limit to provide more completions
+            Query query = buildQuery(50); // Higher limit for completions
 
             // Get items using the appropriate service method
             PartialList<?> items = getItems(query);
@@ -264,6 +323,39 @@ public abstract class BaseCrudCommand extends 
ListCommandSupport implements Crud
         return System.out;
     }
 
+    /**
+     * Apply pagination to a list of items based on query parameters.
+     * This is a helper method for implementations that need to paginate 
in-memory lists.
+     *
+     * @param items the full list of items
+     * @param query the query with offset and limit parameters
+     * @param <T> the type of items in the list
+     * @return a PartialList with paginated results
+     */
+    protected <T> PartialList<T> paginateList(List<T> items, Query query) {
+        Integer offset = query.getOffset();
+        Integer limit = query.getLimit();
+        int start = offset == null ? 0 : offset;
+        int size = limit == null ? items.size() : limit;
+        int end = Math.min(start + size, items.size());
+        
+        List<T> pagedItems = items.subList(start, end);
+        return new PartialList<>(pagedItems, start, pagedItems.size(), 
items.size(), PartialList.Relation.EQUAL);
+    }
+
+    /**
+     * Filter property names by prefix. This is a helper method for 
completePropertyNames implementations.
+     *
+     * @param propertyNames the list of property names to filter
+     * @param prefix the prefix to filter by
+     * @return filtered list of property names
+     */
+    protected List<String> filterPropertyNames(List<String> propertyNames, 
String prefix) {
+        return propertyNames.stream()
+                .filter(name -> name.startsWith(prefix))
+                .collect(Collectors.toList());
+    }
+
     /**
      * Extract the ID from an item. This method attempts to extract the ID 
using common patterns.
      * Subclasses can override this method to provide specialized ID 
extraction for specific item types.
diff --git 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/CrudCommand.java
 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/CrudCommand.java
index a9ed548b6..4c088e906 100644
--- 
a/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/CrudCommand.java
+++ 
b/tools/shell-dev-commands/src/main/java/org/apache/unomi/shell/dev/services/CrudCommand.java
@@ -18,6 +18,7 @@ package org.apache.unomi.shell.dev.services;
 
 import org.apache.karaf.shell.support.table.ShellTable;
 
+import java.io.PrintStream;
 import java.util.List;
 import java.util.Map;
 
@@ -191,4 +192,15 @@ public interface CrudCommand {
      * @param maxEntries maximum number of entries to include
      */
     void buildRows(ShellTable table, int maxEntries);
+
+    /**
+     * Generate CSV output directly using commons-csv API.
+     * This method uses the same logic as buildRows() but outputs as CSV.
+     *
+     * @param console console for output
+     * @param headers column headers
+     * @param limit maximum number of entries
+     * @throws Exception if generation fails
+     */
+    void buildCsvOutput(PrintStream console, String[] headers, int limit) 
throws Exception;
 }

Reply via email to