1use ratatui::{
2 layout::Constraint,
3 style::{Color, Modifier, Style},
4 widgets::{Block, Cell, Row, Table},
5};
6use vta_sdk::client::{AddWebvhServerRequest, CreateDidWebvhRequest, UpdateWebvhServerRequest};
7use vta_sdk::prelude::*;
8
9use crate::display::{
10 NAME_HEADER, NameBook, book_from_vta, book_from_webvh_servers, did_cell, full_display_pairs,
11 name_cell, resolve_agent_names_into,
12};
13
14use crate::render::{is_full_display, print_full_entry_owned, print_full_list_title, print_widget};
15
16pub async fn cmd_webvh_server_add(
17 client: &VtaClient,
18 id: String,
19 did: String,
20 label: Option<String>,
21) -> Result<(), Box<dyn std::error::Error>> {
22 let req = AddWebvhServerRequest { id, did, label };
23 let record = client.add_webvh_server(req).await?;
24 println!("WebVH server added:");
25 println!(" ID: {}", record.id);
26 println!(" DID: {}", record.did);
27 if let Some(label) = &record.label {
28 println!(" Label: {label}");
29 }
30 Ok(())
31}
32
33pub async fn cmd_webvh_server_list(client: &VtaClient) -> Result<(), Box<dyn std::error::Error>> {
34 let resp = client.list_webvh_servers().await?;
35
36 if resp.servers.is_empty() {
37 println!("No WebVH servers configured.");
38 return Ok(());
39 }
40
41 let mut book = NameBook::new();
45 book_from_webvh_servers(&mut book, &resp.servers);
46
47 if is_full_display() {
48 print_full_list_title("WebVH Servers", resp.servers.len());
49 for s in &resp.servers {
50 let created = s
51 .created_at
52 .with_timezone(&chrono::Local)
53 .format("%Y-%m-%d %H:%M:%S %:z")
54 .to_string();
55 let mut fields = vec![("ID", s.id.clone())];
56 fields.extend(full_display_pairs(&book, &s.did));
57 fields.push(("Created", created));
58 print_full_entry_owned(&fields);
59 }
60 return Ok(());
61 }
62
63 let header_style = Style::default()
64 .fg(Color::White)
65 .add_modifier(Modifier::BOLD);
66 let header = Row::new(vec!["ID", NAME_HEADER, "DID", "Created"])
67 .style(header_style)
68 .bottom_margin(1);
69
70 let rows: Vec<Row> = resp
71 .servers
72 .iter()
73 .map(|s| {
74 let created = s
75 .created_at
76 .with_timezone(&chrono::Local)
77 .format("%Y-%m-%d %H:%M")
78 .to_string();
79
80 Row::new(vec![
81 Cell::from(s.id.clone()),
82 name_cell(&book, &s.did),
83 did_cell(&s.did),
84 Cell::from(created).style(Style::default().fg(Color::DarkGray)),
85 ])
86 })
87 .collect();
88
89 let title = format!(" WebVH Servers ({}) ", resp.servers.len());
90
91 let table = Table::new(
92 rows,
93 [
94 Constraint::Length(16), Constraint::Min(40), Constraint::Min(16), Constraint::Length(18), ],
99 )
100 .header(header)
101 .column_spacing(2)
102 .block(
103 Block::bordered()
104 .title(title)
105 .border_style(Style::default().fg(Color::DarkGray)),
106 );
107
108 let height = resp.servers.len() as u16 + 4;
109 print_widget(table, height);
110
111 Ok(())
112}
113
114pub async fn cmd_webvh_server_update(
115 client: &VtaClient,
116 id: &str,
117 label: Option<String>,
118) -> Result<(), Box<dyn std::error::Error>> {
119 let req = UpdateWebvhServerRequest { label };
120 let record = client.update_webvh_server(id, req).await?;
121 println!("WebVH server updated:");
122 println!(" ID: {}", record.id);
123 println!(" DID: {}", record.did);
124 if let Some(label) = &record.label {
125 println!(" Label: {label}");
126 }
127 Ok(())
128}
129
130pub async fn cmd_webvh_server_remove(
131 client: &VtaClient,
132 id: &str,
133) -> Result<(), Box<dyn std::error::Error>> {
134 client.remove_webvh_server(id).await?;
135 println!("WebVH server removed: {id}");
136 Ok(())
137}
138
139pub async fn cmd_webvh_did_register_server(
153 client: &VtaClient,
154 did: &str,
155 server_id: &str,
156 force: bool,
157 domain: Option<String>,
158) -> Result<(), Box<dyn std::error::Error>> {
159 let result = client
160 .register_did_with_server(did, server_id, force, domain.as_deref())
161 .await?;
162 println!("DID registered with WebVH server.");
163 println!(" DID: {}", result.did);
164 println!(" Server: {}", result.server_id);
165 println!(" Log entries: {}", result.log_entry_count);
166 println!();
167 println!(
168 "Future `pnm services …` mutations will auto-publish to `{}`.",
169 result.server_id
170 );
171 Ok(())
172}
173
174pub async fn cmd_webvh_did_edit(
190 client: &VtaClient,
191 did: &str,
192 flags: super::webvh_edit::EditFlags,
193 no_confirm: bool,
194) -> Result<(), Box<dyn std::error::Error>> {
195 use super::webvh_edit::{
196 build_options_from_flags, confirm_publish, diff_summary, document_id,
197 extract_current_document, extract_latest_version_id, extract_pre_rotation_status,
198 launch_editor, prompt_webvh_params,
199 };
200
201 let record = client.get_did_webvh(did).await?;
204 let context_id = record.context_id.clone();
205 let scid = record.scid.clone();
206
207 let any_flag_set = flags.document_file.is_some()
212 || flags.options_file.is_some()
213 || flags.pre_rotation.is_some()
214 || flags.ttl.is_some()
215 || !flags.watchers.is_empty()
216 || flags.no_watchers
217 || flags.label.is_some();
218
219 let mut body = if any_flag_set {
220 let body = build_options_from_flags(&flags)?;
221 if let Some(edited) = &body.document {
223 let log = client.get_did_webvh_log(did).await?;
224 let log_str = log.log.ok_or_else(|| -> Box<dyn std::error::Error> {
225 "DID has no published log on the VTA — nothing to edit".into()
226 })?;
227 let prior = extract_current_document(&log_str)?;
228 super::webvh_edit::assert_did_id_unchanged(&prior, edited)?;
229 }
230 body
231 } else {
232 let log = client.get_did_webvh_log(did).await?;
237 let log_str = log.log.ok_or_else(|| -> Box<dyn std::error::Error> {
238 "DID has no published log on the VTA — nothing to edit".into()
239 })?;
240 let prior = extract_current_document(&log_str)?;
241 let prior_id = document_id(&prior)?.to_string();
242 let fetched_version_id = extract_latest_version_id(&log_str).ok();
243 let pre_rotation_status = extract_pre_rotation_status(&log_str);
244 eprintln!("Editing DID document for {prior_id}.");
245 if let Some(ref v) = fetched_version_id {
246 eprintln!(" Current versionId: {v}");
247 }
248 eprintln!("Opening $EDITOR — save and exit to continue, or quit without saving to abort.");
249
250 let edited = match launch_editor(&prior)? {
251 Some(doc) => {
252 let summary = diff_summary(&prior, &doc);
253 eprintln!();
254 eprintln!("Document diff:");
255 for line in summary.lines() {
256 eprintln!(" {line}");
257 }
258 eprintln!();
259 Some(doc)
260 }
261 None => {
262 eprintln!("Editor cancelled. No changes will be published.");
263 return Ok(());
264 }
265 };
266
267 let mut body = prompt_webvh_params(edited, Some(&pre_rotation_status))?;
268 body.expected_version_id = fetched_version_id;
271 body
272 };
273 let _ = &mut body;
276
277 confirm_publish(&body, no_confirm)?;
278
279 let result = client.update_did_webvh(&context_id, &scid, body).await?;
280 println!("WebVH DID updated.");
281 println!(" DID: {}", result.did);
282 println!(" New version ID: {}", result.new_version_id);
283 println!(" New SCID: {}", result.new_scid);
284 println!(" Update keys: {}", result.update_keys_count);
285 println!(" Pre-rotation: {}", result.pre_rotation_key_count);
286 crate::commands::services::print_serverless_hint(result.serverless, &result.did);
287 Ok(())
288}
289
290pub async fn cmd_webvh_did_create(
293 client: &VtaClient,
294 req: CreateDidWebvhRequest,
295) -> Result<(), Box<dyn std::error::Error>> {
296 let result = client.create_did_webvh(req).await?;
297 println!("WebVH DID created:");
298 println!(" DID: {}", result.did);
299 println!(" Context: {}", result.context_id);
300 if let Some(ref server_id) = result.server_id {
301 println!(" Server: {}", server_id);
302 }
303 if let Some(ref mnemonic) = result.mnemonic {
304 println!(" Mnemonic: {}", mnemonic);
305 }
306 println!(" SCID: {}", result.scid);
307 println!(" Portable: {}", result.portable);
308 println!(" Signing key: {}", result.signing_key_id);
309 println!(" KA key: {}", result.ka_key_id);
310 println!(" Pre-rotation keys: {}", result.pre_rotation_key_count);
311
312 if let Some(ref did_document) = result.did_document {
313 println!();
314 println!("DID Document:");
315 println!(
316 "{}",
317 serde_json::to_string_pretty(did_document)
318 .unwrap_or_else(|_| format!("{did_document}"))
319 );
320 }
321 if let Some(ref log_entry) = result.log_entry {
322 println!();
323 println!("Log Entry (did.jsonl):");
324 println!("{}", log_entry);
325 println!();
326 println!("To self-host this DID, place the log entry in a file named `did.jsonl`");
327 println!("at the URL path corresponding to your DID URL.");
328 }
329
330 Ok(())
331}
332
333#[allow(clippy::too_many_arguments)]
335pub async fn cmd_webvh_did_create_with_files(
336 client: &VtaClient,
337 context_id: String,
338 server_id: Option<String>,
339 url: Option<String>,
340 path: Option<String>,
341 domain: Option<String>,
342 label: Option<String>,
343 portable: bool,
344 add_mediator_service: bool,
345 services: Option<String>,
346 pre_rotation_count: u32,
347 did_document_path: Option<String>,
348 did_log_path: Option<String>,
349 no_primary: bool,
350 signing_key_id: Option<String>,
351 ka_key_id: Option<String>,
352 template: Option<String>,
353 template_context: Option<String>,
354 template_vars: Vec<(String, String)>,
355) -> Result<(), Box<dyn std::error::Error>> {
356 let did_document = match did_document_path {
357 Some(p) => {
358 let content =
359 std::fs::read_to_string(&p).map_err(|e| format!("failed to read {p}: {e}"))?;
360 Some(
361 serde_json::from_str::<serde_json::Value>(&content)
362 .map_err(|e| format!("invalid JSON in {p}: {e}"))?,
363 )
364 }
365 None => None,
366 };
367 if template.is_some() && (did_document.is_some() || did_log_path.is_some()) {
368 return Err("--template is mutually exclusive with --did-document and --did-log".into());
369 }
370 let did_log = match did_log_path {
371 Some(p) => {
372 Some(std::fs::read_to_string(&p).map_err(|e| format!("failed to read {p}: {e}"))?)
373 }
374 None => None,
375 };
376 let additional_services = services
377 .map(|s| serde_json::from_str::<Vec<serde_json::Value>>(&s))
378 .transpose()
379 .map_err(|e| format!("invalid --services JSON: {e}"))?;
380
381 let template_vars: std::collections::HashMap<String, serde_json::Value> = template_vars
382 .into_iter()
383 .map(|(k, v)| (k, serde_json::Value::String(v)))
384 .collect();
385
386 let req = CreateDidWebvhRequest {
387 context_id,
388 server_id,
389 url,
390 path,
391 path_mode: None,
395 domain,
396 label,
397 portable,
398 add_mediator_service,
399 additional_services,
400 pre_rotation_count,
401 did_document,
402 did_log,
403 set_primary: !no_primary,
404 signing_key_id,
405 ka_key_id,
406 template,
407 template_context,
408 template_vars,
409 };
410 cmd_webvh_did_create(client, req).await
411}
412
413pub async fn cmd_webvh_did_list(
414 client: &VtaClient,
415 context_id: Option<&str>,
416 server_id: Option<&str>,
417) -> Result<(), Box<dyn std::error::Error>> {
418 let resp = client.list_dids_webvh(context_id, server_id).await?;
419
420 if resp.dids.is_empty() {
421 println!("No WebVH DIDs found.");
422 return Ok(());
423 }
424
425 let mut book = book_from_vta(client).await;
426 resolve_agent_names_into(&mut book, resp.dids.iter().map(|d| d.did.as_str())).await;
429
430 if is_full_display() {
431 print_full_list_title("WebVH DIDs", resp.dids.len());
432 for d in &resp.dids {
433 let created = d
434 .created_at
435 .with_timezone(&chrono::Local)
436 .format("%Y-%m-%d %H:%M:%S %:z")
437 .to_string();
438 let portable = if d.portable { "yes" } else { "no" };
439 let mut fields = full_display_pairs(&book, &d.did);
442 fields.push(("Context", d.context_id.clone()));
443 fields.push(("Server", d.server_id.clone()));
444 fields.push(("SCID", d.scid.clone()));
445 fields.push(("Portable", portable.to_string()));
446 fields.push(("Created", created));
447 print_full_entry_owned(&fields);
448 }
449 return Ok(());
450 }
451
452 let header_style = Style::default()
453 .fg(Color::White)
454 .add_modifier(Modifier::BOLD);
455 let show_names = book.names_any(resp.dids.iter().map(|d| d.did.as_str()));
456 let mut header_cells = vec!["DID", "Context", "Server", "Portable", "Created"];
457 if show_names {
458 header_cells.insert(0, NAME_HEADER);
459 }
460 let header = Row::new(header_cells).style(header_style).bottom_margin(1);
461
462 let rows: Vec<Row> = resp
463 .dids
464 .iter()
465 .map(|d| {
466 let portable = if d.portable { "yes" } else { "no" };
467 let created = d
468 .created_at
469 .with_timezone(&chrono::Local)
470 .format("%Y-%m-%d %H:%M")
471 .to_string();
472
473 let mut cells = vec![
474 did_cell(&d.did),
475 Cell::from(d.context_id.clone()),
476 Cell::from(d.server_id.clone()),
477 Cell::from(portable.to_string()),
478 Cell::from(created).style(Style::default().fg(Color::DarkGray)),
479 ];
480 if show_names {
481 cells.insert(0, name_cell(&book, &d.did));
482 }
483 Row::new(cells)
484 })
485 .collect();
486
487 let title = format!(" WebVH DIDs ({}) ", resp.dids.len());
488
489 let table = Table::new(
490 rows,
491 [
492 Constraint::Min(40), Constraint::Length(16), Constraint::Length(16), Constraint::Length(10), Constraint::Length(18), ],
498 )
499 .header(header)
500 .column_spacing(2)
501 .block(
502 Block::bordered()
503 .title(title)
504 .border_style(Style::default().fg(Color::DarkGray)),
505 );
506
507 let height = resp.dids.len() as u16 + 4;
508 print_widget(table, height);
509
510 Ok(())
511}
512
513pub async fn cmd_webvh_did_get(
514 client: &VtaClient,
515 did: &str,
516) -> Result<(), Box<dyn std::error::Error>> {
517 let record = client.get_did_webvh(did).await?;
518 println!("WebVH DID:");
519 println!(" DID: {}", record.did);
520 println!(" Context: {}", record.context_id);
521 println!(" Server: {}", record.server_id);
522 println!(" Mnemonic: {}", record.mnemonic);
523 println!(" SCID: {}", record.scid);
524 println!(" Portable: {}", record.portable);
525 println!(" Log entries: {}", record.log_entry_count);
526 println!(
527 " Created: {}",
528 crate::duration::format_local_datetime(record.created_at)
529 );
530 println!(
531 " Updated: {}",
532 crate::duration::format_local_datetime(record.updated_at)
533 );
534 Ok(())
535}
536
537pub async fn cmd_webvh_did_delete(
538 client: &VtaClient,
539 did: &str,
540) -> Result<(), Box<dyn std::error::Error>> {
541 client.delete_did_webvh(did).await?;
542 println!("WebVH DID deleted: {did}");
543 Ok(())
544}
545
546pub async fn cmd_webvh_did_log(
554 client: &VtaClient,
555 did: &str,
556 out: Option<std::path::PathBuf>,
557) -> Result<(), Box<dyn std::error::Error>> {
558 let resp = client.get_did_webvh_log(did).await?;
559 let log = resp
560 .log
561 .ok_or_else(|| format!("no did.jsonl log stored for {did}"))?;
562
563 match out {
564 Some(path) => {
565 std::fs::write(&path, log.as_bytes())
566 .map_err(|e| format!("write {}: {e}", path.display()))?;
567 eprintln!(
568 "DID log written to {} ({} bytes)",
569 path.display(),
570 log.len()
571 );
572 }
573 None => {
574 print!("{log}");
577 }
578 }
579 Ok(())
580}