# Wasot CLI Migration TODO

**Current:** Subcommand-based (`wasot to-xml file.wasot`)
**Target:** Flag-based with optional subcommands (`wasot file.wasot -t xml`)

---

## Quick Reference

```
# Current
wasot pretty file.wasot
wasot to-xml file.wasot
wasot to-jsonl file.wasot
wasot to-binary file.wasot
wasot convert --from wasot --to xml -i file.wasot

# Target
wasot file.wasot                 # normalize
wasot file.wasot -t xml          # convert
wasot -t jsonl < file.wasot      # stdin
wasot check file.wasot           # validate
wasot info file.wasot            # stats
wasot --select "p" file.wasot    # filter
```

---

## Phase 1: Clap Restructure

**Goal:** Make `wasot file.wasot` work without subcommand.

### 1.1 GlobalOptions + Optional Subcommand

```rust
#[derive(Parser)]
#[command(name = "wasot", version, about = "Transform Wasot documents")]
#[command(args_conflicts_with_subcommands = true)]
struct Cli {
    #[command(flatten)]
    opts: GlobalOpts,

    #[command(subcommand)]
    cmd: Option<Cmd>,
}

#[derive(Args)]
struct GlobalOpts {
    #[arg(short = 'f', long, value_enum)]
    from: Option<Format>,

    #[arg(short = 't', long, value_enum)]
    to: Option<Format>,

    #[arg(short = 'o', long)]
    output: Option<String>,

    #[arg(short = 'q', long)]
    quiet: bool,

    #[arg(short = 'v', long)]
    verbose: bool,

    #[arg(long, value_enum, default_value = "auto")]
    color: ColorChoice,

    #[arg(long)]
    select: Option<String>,

    /// Input files (stdin if empty)
    files: Vec<String>,
}

#[derive(Subcommand)]
enum Cmd {
    Check { files: Vec<String> },
    Info { file: Option<String> },
    // Keep legacy commands hidden
    #[command(hide = true)]
    Pretty { file_path: Option<String>, #[arg(short, long)] output: Option<String> },
    // ... other legacy commands
}
```

**Tasks:**
- [ ] Add `GlobalOpts` struct
- [ ] Change `command: Commands` → `cmd: Option<Cmd>`
- [ ] Handle `None` in main() → default convert
- [ ] Add `ColorChoice` enum

### 1.2 Main Dispatch

```rust
fn main() {
    #[cfg(unix)]
    unsafe { libc::signal(libc::SIGPIPE, libc::SIG_DFL); }

    let cli = Cli::parse();

    let result = match cli.cmd {
        None => process_default(&cli.opts),
        Some(Cmd::Check { files }) => process_check(&files),
        Some(Cmd::Info { file }) => process_info(file.as_deref()),
        Some(Cmd::Pretty { file_path, output }) => process_pretty(file_path.as_deref(), output.as_deref()),
        // ... legacy commands
    };

    if let Err(e) = result {
        eprintln!("wasot: {e}");
        std::process::exit(exit_code(&e));
    }
}
```

**Tasks:**
- [ ] Add SIGPIPE handler (add `libc` to Cargo.toml)
- [ ] Implement `process_default()` that uses GlobalOpts
- [ ] Add `exit_code()` function (1=parse, 2=io, 3=args)

---

## Phase 2: Format Detection

**Goal:** Auto-detect input format from extension and content.

### 2.1 Unified Detection

```rust
fn detect_format(path: Option<&str>, data: &[u8]) -> Format {
    // 1. Try extension
    if let Some(p) = path {
        match Path::new(p).extension().and_then(|e| e.to_str()) {
            Some("wasot") => return Format::Wasot,
            Some("xml") => return Format::Xml,
            Some("jsonl") => return Format::Jsonl,
            Some("wasotb") => return Format::Binary,
            _ => {}
        }
    }

    // 2. Try content sniffing
    if let Ok(text) = std::str::from_utf8(&data[..data.len().min(256)]) {
        if let Some(c) = text.chars().find(|c| !c.is_whitespace()) {
            return match c {
                '.' | '#' => Format::Wasot,
                '<' => Format::Xml,
                '{' | '[' => Format::Jsonl,
                c if c.is_alphabetic() => Format::Wasot,
                _ => Format::Binary,
            };
        }
    }

    Format::Binary
}
```

**Tasks:**
- [ ] Add `detect_format()` function
- [ ] Replace `is_likely_binary()` calls
- [ ] Add output format detection from `-o` extension
- [ ] Use `.wasotb` extension (not `.bin`)

---

## Phase 3: New Subcommands

### 3.1 Check

```rust
fn process_check(files: &[String]) -> Result<(), Error> {
    let mut all_valid = true;

    for path in files.iter().chain(stdin_if_empty(files)) {
        match validate_file(&path) {
            Ok(()) => {}
            Err(e) => {
                eprintln!("{path}: {e}");
                all_valid = false;
            }
        }
    }

    if all_valid { Ok(()) } else { Err(Error::ValidationFailed) }
}
```

**Tasks:**
- [ ] Add `Check` subcommand
- [ ] Implement validation (parse without output)
- [ ] Support multiple files
- [ ] Support stdin with `-`

### 3.2 Info

```rust
fn process_info(path: Option<&str>) -> Result<(), Error> {
    let stats = collect_stats(path)?;
    println!("blocks: {}", stats.total);
    for (name, count) in stats.by_type.iter() {
        println!("  {name}: {count}");
    }
    println!("depth: {}", stats.max_depth);
    println!("lines: {}", stats.lines);
    println!("bytes: {}", stats.bytes);
    Ok(())
}
```

**Tasks:**
- [ ] Add `Info` subcommand
- [ ] Count blocks by type during parse
- [ ] Track max nesting depth
- [ ] Output YAML-like format

---

## Phase 4: Selector (--select)

**Depends on:** Phases 1-2

```rust
fn process_default(opts: &GlobalOpts) -> Result<(), Error> {
    let input = read_input(&opts.files)?;
    let format = opts.from.unwrap_or_else(|| detect_format(...));

    let emitter = create_emitter(format, &input)?;
    let receiver = emitter.subscribe();

    // Apply selector filter if specified
    let filtered = match &opts.select {
        Some(sel) => filter_by_selector(receiver, sel)?,
        None => receiver,
    };

    // Output in requested format
    let out_format = opts.to.unwrap_or(Format::Wasot);
    write_output(filtered, out_format, opts.output.as_deref())
}
```

**Tasks:**
- [x] Import `SelectorConsumer` from lib
- [x] Add selector parsing in main binary
- [x] Combine with output format (`--select` + `-t jsonl`)

---

## Phase 5: Polish

### Exit Codes

```rust
mod exit_codes {
    pub const SUCCESS: i32 = 0;
    pub const INVALID_INPUT: i32 = 1;
    pub const IO_ERROR: i32 = 2;
    pub const INVALID_ARGS: i32 = 3;
}
```

### Help Text

```rust
#[command(
    long_about = "Wasot is a filter. Like jq for JSON, it transforms structured documents.",
    after_help = "EXAMPLES:
    wasot doc.wasot              Normalize
    wasot doc.wasot -t xml       Convert to XML
    wasot check *.wasot          Validate
    cat file.wasotb | wasot      Auto-detect binary"
)]
```

### Color/Quiet/Verbose

```rust
fn should_color(choice: ColorChoice) -> bool {
    match choice {
        ColorChoice::Always => true,
        ColorChoice::Never => false,
        ColorChoice::Auto => {
            std::io::stderr().is_terminal()
                && std::env::var_os("NO_COLOR").is_none()
        }
    }
}
```

**Tasks:**
- [ ] Define exit code constants
- [ ] Add help examples
- [ ] Implement `--quiet` (suppress warnings)
- [ ] Implement `--verbose` (debug to stderr)
- [ ] Implement `--color` (with NO_COLOR support)

---

## Phase 6: Deprecation

**Timeline:** Warn now, hide in 0.2, remove in 1.0

```rust
#[command(hide = true)]
Pretty { ... }  // Add deprecation warning in handler
```

**Tasks:**
- [x] Add `#[command(hide = true)]` to legacy commands
- [ ] Print warning: `wasot: 'pretty' is deprecated, use 'wasot FILE'`
- [ ] Document migration in CHANGELOG

---

## Implementation Order

| # | Task | Effort | Status |
|---|------|--------|--------|
| 1 | GlobalOpts + optional subcommand | M | ✅ Done |
| 2 | SIGPIPE handler | S | ✅ Done |
| 3 | `detect_format()` with whitespace skip | S | ✅ Done |
| 4 | Default command (`wasot file.wasot`) | M | ✅ Done |
| 5 | Exit codes | S | ✅ Done |
| 6 | `check` subcommand | M | ✅ Done |
| 7 | `info` subcommand | M | ✅ Done |
| 8 | Help text + examples | S | ✅ Done (in Task 1) |
| 9 | `--select` integration | L | ✅ Done |
| 10 | `--quiet`, `--verbose`, `--color` | S | Pending |
| 11 | Deprecation warnings | S | ⚠️ Partial (hidden) |

**Suggested batches:**
- **Batch A (structure):** 1, 2, 3, 4, 5, 8 → ✅ COMPLETE
- **Batch B (commands):** 6, 7 → ✅ COMPLETE
- **Batch C (advanced):** 9, 10, 11 → selectors, polish, cleanup

---

## Testing Checklist

```bash
# After Batch A ✅ VERIFIED
wasot --help                     # ✅ Shows new structure with global opts
wasot examples/demo.wasot        # ✅ Default normalize works
wasot examples/demo.wasot -t xml # ✅ Format flag works
wasot nonexistent.wasot          # ✅ Exit code 2 (IO error)

# After Batch B ✅ VERIFIED
wasot check examples/*.wasot     # ✅ Check works (21 files validated)
wasot info examples/demo.wasot   # ✅ Info works (shows stats)

# After Batch C (select) ✅ VERIFIED
wasot --select "p" examples/demo.wasot           # ✅ Select works (wasot output)
wasot --select "p" examples/demo.wasot -t jsonl  # ✅ Select + jsonl format
wasot --select "footer" examples/demo.wasot -t xml  # ✅ Select + xml format
wasot --select "code" examples/demo.wasot -t binary -o /tmp/test.wasotb  # ✅ Select + binary

# Pending
wasot pretty demo.wasot          # Warns about deprecation
```

---

## Files Modified

| File | Changes |
|------|---------|
| `src/main.rs` | CLI restructure, new commands |
| `Cargo.toml` | Add `libc` dependency |
| `tests/cli_integration.rs` | New integration tests |

---

## Diff Preview: Phase 1 Changes

```diff
 #[derive(Parser)]
-#[command(name = "wasot")]
-#[command(about = "Wasot compiler and format converter")]
-#[command(version = "0.1.0")]
+#[command(name = "wasot", version, about = "Transform Wasot documents")]
+#[command(args_conflicts_with_subcommands = true)]
+#[command(after_help = "EXAMPLES:\n    wasot file.wasot\n    wasot file.wasot -t xml")]
 struct Cli {
+    #[command(flatten)]
+    opts: GlobalOpts,
+
     #[command(subcommand)]
-    command: Commands,
+    cmd: Option<Cmd>,
 }

+#[derive(Args)]
+struct GlobalOpts {
+    #[arg(short = 'f', long, value_enum)]
+    from: Option<Format>,
+
+    #[arg(short = 't', long, value_enum)]
+    to: Option<Format>,
+
+    #[arg(short = 'o', long)]
+    output: Option<String>,
+
+    files: Vec<String>,
+}
```
