diff --git a/mateclaw-server/src/main/java/vip/mate/cli/ExportCommand.java b/mateclaw-server/src/main/java/vip/mate/cli/ExportCommand.java new file mode 100644 index 00000000..cb6b9b81 --- /dev/null +++ b/mateclaw-server/src/main/java/vip/mate/cli/ExportCommand.java @@ -0,0 +1,80 @@ +package vip.mate.cli; + +import org.springframework.stereotype.Component; +import vip.mate.operational.service.OperationalDataExportService; + +import java.io.IOException; +import java.time.LocalDate; + +/** + * {@code --cli.command=export} — generate a 9-sheet operational data report. + * + *
Writes the ZIP bytes to stdout so the caller can redirect:
+ *{@code
+ * java -jar app.jar --cli.command=export \
+ * --cli.start=2026-01-01 --cli.end=2026-06-30 > report.zip
+ * }
+ */
+@Component
+public class ExportCommand implements MateClawCli.CliCommand {
+
+ private final OperationalDataExportService exportService;
+
+ public ExportCommand(OperationalDataExportService exportService) {
+ this.exportService = exportService;
+ }
+
+ @Override public String name() { return "export"; }
+ @Override public String description() { return "Generate the operational data report (9-sheet Excel, written to stdout)"; }
+
+ @Override public String usage() {
+ return """
+ \s
+ export — generate the operational data report; ZIP bytes are written to stdout
+ \s
+ Required:
+ --cli.start=YYYY-MM-DD start date (inclusive)
+ --cli.end=YYYY-MM-DD end date (inclusive)
+ \s
+ Optional:
+ --cli.dry-run dry-run mode
+ \s
+ Example:
+ java -jar app.jar --cli.command=export \\
+ \s --cli.start=2026-01-01 --cli.end=2026-06-30 > report.zip
+ \s""";
+ }
+
+ @Override
+ public void execute(MateClawCli.CliContext ctx) {
+ LocalDate start = ctx.requireDate("cli.start");
+ LocalDate end = ctx.requireDate("cli.end");
+
+ if (ctx.isDryRun()) {
+ ctx.header("Export dry-run");
+ ctx.info("Date range", start + " ~ " + end);
+ ctx.info("Result", "ZIP bytes would be written to stdout (not executed)");
+ ctx.done("Dry-run complete");
+ ctx.exit(0);
+ return;
+ }
+
+ byte[] zip = exportService.exportBackendBytes(start, end);
+
+ try {
+ // Diagnostics go to stderr so stdout stays a clean binary stream for redirection.
+ System.err.println("=== Operational data export ===");
+ System.err.printf(" Date range : %s ~ %s%n", start, end);
+ System.err.printf(" Size : %d KB%n", zip.length / 1024);
+ System.err.printf(" File name : ops_data_%s_%s.zip%n", start, end);
+ System.err.println("\n=== Writing to stdout (redirect: ... > report.zip) ===");
+ System.out.write(zip);
+ System.out.flush();
+ System.err.println("=== Export complete ===");
+ } catch (IOException e) {
+ ctx.error("Failed to write to stdout: " + e.getMessage());
+ }
+
+ ctx.exit(0);
+ }
+}
diff --git a/mateclaw-server/src/main/java/vip/mate/cli/MateClawCli.java b/mateclaw-server/src/main/java/vip/mate/cli/MateClawCli.java
new file mode 100644
index 00000000..7acdd517
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/cli/MateClawCli.java
@@ -0,0 +1,178 @@
+package vip.mate.cli;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.ApplicationArguments;
+import org.springframework.boot.ApplicationRunner;
+import org.springframework.boot.ExitCodeGenerator;
+import org.springframework.boot.SpringApplication;
+import org.springframework.context.ApplicationContext;
+import org.springframework.core.annotation.Order;
+import org.springframework.stereotype.Component;
+
+import java.time.LocalDate;
+import java.time.format.DateTimeParseException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+/**
+ * MateClaw CLI framework — single-file core containing:
+ * {@code
+ * java -jar app.jar --cli.command=help
+ * java -jar app.jar --cli.command=export \
+ * --cli.start=2026-01-01 --cli.end=2026-06-30 > report.zip
+ * }
+ *
+ * {@code
+ * @Component
+ * public class MyCommand implements MateClawCli.CliCommand {
+ * public String name() { return "mycmd"; }
+ * public String description() { return "does something"; }
+ * public String usage() { return " --cli.x=...\n example: ..."; }
+ * public void execute(MateClawCli.CliContext ctx) {
+ * ctx.exit(0);
+ * }
+ * }
+ * }
+ */
+public final class MateClawCli { private MateClawCli() { /* namespace */ }
+
+ // ═══ CliCommand — interface for pluggable commands ═══
+
+ public interface CliCommand {
+ String name();
+ String description();
+ default String usage() { return ""; }
+ void execute(CliContext ctx);
+ }
+
+ // ═══ CliContext — argument parsing & output ═══
+
+ public static class CliContext {
+ private static final Logger log = LoggerFactory.getLogger(CliContext.class);
+ private final ApplicationArguments args;
+ private final ApplicationContext springCtx;
+ private final boolean dryRun;
+
+ public CliContext(ApplicationArguments args, ApplicationContext springCtx) {
+ this.args = args;
+ this.springCtx = springCtx;
+ this.dryRun = args.getOptionNames().contains("cli.dry-run");
+ }
+
+ /** Read optional string param (null if absent). */
+ public String arg(String key) {
+ var vals = args.getOptionValues(key);
+ return vals != null && !vals.isEmpty() ? vals.get(0) : null;
+ }
+
+ /** Read required string param. Absent = error + exit. */
+ public String requireArg(String key) {
+ String val = arg(key);
+ if (val == null || val.isBlank()) error("Missing required parameter: --" + key);
+ return val;
+ }
+
+ /** Read required date param (YYYY-MM-DD). Bad format = error + exit. */
+ public LocalDate requireDate(String key) {
+ String raw = requireArg(key);
+ try { return LocalDate.parse(raw); }
+ catch (DateTimeParseException e) { error("Invalid date format: --" + key + "=" + raw + " (expected YYYY-MM-DD)"); return null; }
+ }
+
+ /** True when {@code --cli.dry-run} was passed. */
+ public boolean isDryRun() { return dryRun; }
+
+ // —— output ——————————————————————————————————————————
+ public void header(String title) { System.out.println(); System.out.println("=== " + title + " ==="); }
+ public void info(String key, Object val) { System.out.printf(" %-12s : %s%n", key, val); }
+ public void done(String msg) { System.out.println(); System.out.println("=== " + msg + " ==="); System.out.println(); }
+ public void warn(String msg) { log.warn(msg); System.err.println("[WARN] " + msg); }
+ public void error(String msg) { log.error(msg); System.err.println("[ERROR] " + msg); exit(1); }
+
+ public void exit(int code) {
+ System.out.flush(); System.err.flush();
+ try { Thread.sleep(200); } catch (InterruptedException ignored) {}
+ SpringApplication.exit(springCtx, (ExitCodeGenerator) () -> code);
+ System.exit(code);
+ }
+ }
+
+ // ═══ CliRunner — auto-discovery ApplicationRunner ═══
+
+ @Component
+ @Order(9999)
+ public static class CliRunner implements ApplicationRunner {
+ private static final Logger log = LoggerFactory.getLogger(CliRunner.class);
+ private final ApplicationContext springCtx;
+ private final Map