use clap::{Args, Subcommand};
use std::path::PathBuf;
#[derive(Debug, Args)]
pub struct MigrateArgs {
#[command(subcommand)]
pub command: MigrateCommand,
}
#[derive(Debug, Subcommand)]
pub enum MigrateCommand {
Analyse(AnalyseArgs),
Generate(GenerateArgs),
Diff(DiffArgs),
Wizard,
}
#[derive(Debug, Args)]
pub struct AnalyseArgs {
#[arg(default_value = ".")]
pub path: PathBuf,
#[arg(long, default_value = "table")]
pub format: String,
}
#[derive(Debug, Args)]
pub struct GenerateArgs {
pub path: PathBuf,
#[arg(short, long)]
pub output: Option<PathBuf>,
#[arg(long, default_value = "ts")]
pub target: String,
}
#[derive(Debug, Args)]
pub struct DiffArgs {
pub path: PathBuf,
}
#[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());
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");
}