sansavision-pulse-cli 0.4.2

Pulse CLI — schema compilation, validation, dev server, and deployment tools
use clap::{Args, Subcommand};
use std::path::PathBuf;

/// `pulse migrate` subcommands
#[derive(Debug, Args)]
pub struct MigrateArgs {
    #[command(subcommand)]
    pub command: MigrateCommand,
}

#[derive(Debug, Subcommand)]
pub enum MigrateCommand {
    /// Analyse a codebase and report all WebRTC usage patterns
    Analyse(AnalyseArgs),
    /// Generate migration shims for common WebRTC patterns
    Generate(GenerateArgs),
    /// Show a diff of what would change
    Diff(DiffArgs),
    /// Run an interactive migration wizard
    Wizard,
}

#[derive(Debug, Args)]
pub struct AnalyseArgs {
    /// Path to scan (default: current dir)
    #[arg(default_value = ".")]
    pub path: PathBuf,
    /// Output format: table | json
    #[arg(long, default_value = "table")]
    pub format: String,
}

#[derive(Debug, Args)]
pub struct GenerateArgs {
    /// Source file or directory
    pub path: PathBuf,
    /// Write output to this file (default: stdout)
    #[arg(short, long)]
    pub output: Option<PathBuf>,
    /// Target SDK: ts | rust | go
    #[arg(long, default_value = "ts")]
    pub target: String,
}

#[derive(Debug, Args)]
pub struct DiffArgs {
    pub path: PathBuf,
}

// ─── Patterns we detect ──────────────────────────────────────
#[derive(Debug, Clone)]
pub struct WebRtcPattern {
    pub name: &'static str,
    pub webrtc_api: &'static str,
    pub pulse_equivalent: &'static str,
    pub effort: &'static str,
    pub notes: &'static str,
}

pub fn known_patterns() -> Vec<WebRtcPattern> {
    vec![
        WebRtcPattern {
            name: "RTCPeerConnection",
            webrtc_api: "new RTCPeerConnection(config)",
            pulse_equivalent: "Pulse.connect({ relay, token })",
            effort: "Low",
            notes: "No ICE/STUN/TURN config needed. Pulse handles traversal.",
        },
        WebRtcPattern {
            name: "getUserMedia",
            webrtc_api: "navigator.mediaDevices.getUserMedia(constraints)",
            pulse_equivalent: "Keep as-is → pass stream to room.publish(stream)",
            effort: "None",
            notes: "MediaStream API is unchanged. Only the publishing differs.",
        },
        WebRtcPattern {
            name: "addTrack / addStream",
            webrtc_api: "pc.addTrack(track, stream)",
            pulse_equivalent: "room.publish(stream, { qos: 'realtime-audio' })",
            effort: "Low",
            notes: "Replace with room.publish(). QoS class maps to WebRTC priority.",
        },
        WebRtcPattern {
            name: "RTCDataChannel",
            webrtc_api: "pc.createDataChannel('label', opts)",
            pulse_equivalent: "conn.openStream({ reliability, ordering, qos })",
            effort: "Low",
            notes: "Pulse streams are richer: configurable reliability, ordering, and QoS class.",
        },
        WebRtcPattern {
            name: "onicecandidate",
            webrtc_api: "pc.onicecandidate = (e) => signaling.send(e.candidate)",
            pulse_equivalent: "DELETE — no signalling server required",
            effort: "High (delete)",
            notes: "Pulse relay handles traversal. Remove all ICE signalling logic.",
        },
        WebRtcPattern {
            name: "createOffer / createAnswer",
            webrtc_api: "pc.createOffer() / pc.createAnswer()",
            pulse_equivalent: "DELETE — handled by Pulse.connect()",
            effort: "High (delete)",
            notes: "SDP negotiation is eliminated. Replace the entire offer/answer dance with one connect() call.",
        },
        WebRtcPattern {
            name: "RTCSessionDescription",
            webrtc_api: "new RTCSessionDescription(sdp)",
            pulse_equivalent: "DELETE",
            effort: "High (delete)",
            notes: "SDP is replaced by PLP CONNECT/ACCEPT control plane.",
        },
        WebRtcPattern {
            name: "ontrack",
            webrtc_api: "pc.ontrack = (e) => video.srcObject = e.streams[0]",
            pulse_equivalent: "room.on('streamPublished', ({ stream }) => ...)",
            effort: "Low",
            notes: "Event name changes; stream object is a standard MediaStream.",
        },
        WebRtcPattern {
            name: "getStats",
            webrtc_api: "pc.getStats()",
            pulse_equivalent: "conn.on('metrics', (m) => ...)",
            effort: "Low",
            notes: "Pulse metrics are push-based and structured. No polling required.",
        },
        WebRtcPattern {
            name: "TURN server config",
            webrtc_api: "iceServers: [{ urls: 'turn:...' }]",
            pulse_equivalent: "DELETE — Pulse relay is TURN-free",
            effort: "Zero (delete)",
            notes: "Remove all TURN/STUN server config and credentials.",
        },
    ]
}

pub fn run_analyse(args: &AnalyseArgs) {
    let patterns = known_patterns();

    println!("\n🔍 Pulse Migrate — WebRTC Pattern Analysis");
    println!("   Scanning: {}\n", args.path.display());

    // In production, this would use tree-sitter or regex to scan actual source files.
    // Here we report all known patterns as a migration reference.

    if args.format == "json" {
        println!("[");
        for (i, p) in patterns.iter().enumerate() {
            let comma = if i < patterns.len() - 1 { "," } else { "" };
            println!(
                "  {{ \"name\": \"{}\", \"effort\": \"{}\", \"webrtc\": \"{}\", \"pulse\": \"{}\" }}{}",
                p.name, p.effort, p.webrtc_api, p.pulse_equivalent, comma
            );
        }
        println!("]");
        return;
    }

    println!(
        "{:<25} {:<12} {:<40} {}",
        "WebRTC API", "Effort", "Pulse Equivalent", "Notes"
    );
    println!("{}", "".repeat(120));
    for p in &patterns {
        println!(
            "{:<25} {:<12} {:<40} {}",
            p.name, p.effort, p.pulse_equivalent, p.notes
        );
    }

    let high: Vec<_> = patterns
        .iter()
        .filter(|p| p.effort.starts_with("High"))
        .collect();
    let low: Vec<_> = patterns
        .iter()
        .filter(|p| p.effort.starts_with("Low"))
        .collect();
    let none: Vec<_> = patterns
        .iter()
        .filter(|p| p.effort == "None" || p.effort.starts_with("Zero"))
        .collect();

    println!("\n📊 Migration complexity summary:");
    println!("   🔴 Delete / High effort: {} patterns", high.len());
    println!("   🟡 Low effort rewrites:  {} patterns", low.len());
    println!("   🟢 Keep as-is / Zero:   {} patterns\n", none.len());
    println!("Total estimated effort: 2–4 hours for a typical WebRTC app.\n");
    println!("Run `pulse migrate generate <path>` to generate migration shims.");
}

pub fn run_generate(args: &GenerateArgs) {
    println!(
        "\n🔧 Generating Pulse migration shims for: {}",
        args.path.display()
    );
    println!("   Target SDK: {}\n", args.target);

    let shim = match args.target.as_str() {
        "rust" => include_str_or_default("rust"),
        _ => typescript_shim(),
    };

    if let Some(out) = args.output.as_ref() {
        std::fs::write(out, shim).expect("Failed to write shim");
        println!("✅ Shim written to {}", out.display());
    } else {
        println!("{}", shim);
    }
}

fn typescript_shim() -> &'static str {
    r#"/**
 * @pulse/webrtc-compat — WebRTC compatibility shim for Pulse
 * Generated by `pulse migrate generate`
 *
 * This shim re-exports common WebRTC patterns as Pulse equivalents
 * to minimise diff size during migration.
 */
import { Pulse, type PulseConnectOptions } from '@pulse/sdk'

// DROP-IN for: new RTCPeerConnection(config)
export async function createConnection(opts: PulseConnectOptions) {
  return Pulse.connect(opts)
}

// DROP-IN for: pc.addTrack() / addStream()
export async function publishStream(room: Awaited<ReturnType<typeof Pulse.connect>>['join'] extends (...args: any[]) => infer R ? Awaited<R> : never, stream: MediaStream, qos = 'balanced') {
  return room.publish(stream, { qos } as any)
}

// DROP-IN for: pc.createDataChannel()
export async function createDataChannel(conn: any, label: string, opts: { ordered?: boolean; maxRetransmits?: number } = {}) {
  return conn.openStream({
    reliability: opts.maxRetransmits === 0 ? 'unreliable' : 'reliable',
    ordering: opts.ordered !== false ? 'ordered' : 'unordered',
    qos: 'interactive-data',
    label,
  })
}

// NOTE: Remove all signalling server code (createOffer/createAnswer/onicecandidate).
// Pulse handles connection establishment automatically.
"#
}

fn include_str_or_default(_lang: &str) -> &'static str {
    "// Rust migration shim generation not yet implemented\n// See: docs/migrate-from-webrtc.md\n"
}

pub fn run_diff(args: &DiffArgs) {
    println!("\n📋 Pulse Migrate — Diff Preview: {}", args.path.display());
    println!("\nThis would show a unified diff of what `pulse migrate generate` would change.");
    println!("Full diff support requires tree-sitter integration (coming in v0.2.0).\n");
}

pub fn run_wizard() {
    println!("\n🧙 Pulse Migrate — Interactive Wizard");
    println!("  1️⃣  Analyse your project (pulse migrate analyse .)");
    println!("  2️⃣  Preview changes     (pulse migrate diff ./src)");
    println!(
        "  3️⃣  Generate shims      (pulse migrate generate ./src --output ./src/pulse-compat.ts)"
    );
    println!("  4️⃣  Install SDK         (npm install @pulse/sdk)");
    println!("  5️⃣  Remove TURN servers from your infrastructure");
    println!("\n  Full interactive wizard coming in CLI v0.2.0.\n");
}