| 1 | //! `codewhale integrations …` command surface (dispatched from the outer |
| 2 | //! `codewhale` binary as a passthrough). |
| 3 | |
| 4 | use std::io::IsTerminal; |
| 5 | use std::path::Path; |
| 6 | |
| 7 | use anyhow::{Context, Result}; |
| 8 | |
| 9 | use crate::config::Config; |
| 10 | use crate::integrations::dsh::{ |
| 11 | self, CLI_COMMAND, DetectEnv, DshAdapter, DshIntegrationState, DshPaths, DshPlan, |
| 12 | DshReceiptEvent, DshStatusReport, ProcessRunner, RELATIONSHIP_LABEL, |
| 13 | }; |
| 14 | use crate::{DshIntegrationCommand, IntegrationsCommand}; |
| 15 | |
| 16 | pub(crate) fn run(config: &Config, workspace: &Path, command: IntegrationsCommand) -> Result<()> { |
| 17 | match command { |
| 18 | IntegrationsCommand::Dsh { command } => run_dsh(config, workspace, command), |
| 19 | } |
| 20 | } |
| 21 | |
| 22 | fn detect_now() -> dsh::DshDetection { |
| 23 | dsh::detect::detect(&DetectEnv::from_process(), &ProcessRunner) |
| 24 | } |
| 25 | |
| 26 | fn status_report( |
| 27 | config: &Config, |
| 28 | workspace: &Path, |
| 29 | allow_full_access: bool, |
| 30 | ) -> Result<(DshPaths, DshStatusReport)> { |
| 31 | let paths = DshPaths::from_process()?; |
| 32 | let detection = detect_now(); |
| 33 | let identity = dsh::codewhale_route_identity(config, workspace); |
| 34 | let report = dsh::compute_status( |
| 35 | &paths, |
| 36 | detection, |
| 37 | identity, |
| 38 | allow_full_access, |
| 39 | dsh::bundle_availability_now(), |
| 40 | )?; |
| 41 | Ok((paths, report)) |
| 42 | } |
| 43 | |
| 44 | fn confirm(yes: bool, question: &str) -> Result<()> { |
| 45 | if yes { |
| 46 | return Ok(()); |
| 47 | } |
| 48 | if !std::io::stdin().is_terminal() { |
| 49 | anyhow::bail!("stdin is not a terminal; pass --yes to confirm the disclosed plan"); |
| 50 | } |
| 51 | print!("{question} [y/N] "); |
| 52 | use std::io::Write; |
| 53 | std::io::stdout().flush().ok(); |
| 54 | let mut answer = String::new(); |
| 55 | std::io::stdin() |
| 56 | .read_line(&mut answer) |
| 57 | .context("read confirmation")?; |
| 58 | if !matches!(answer.trim().to_ascii_lowercase().as_str(), "y" | "yes") { |
| 59 | anyhow::bail!("not confirmed; nothing was written"); |
| 60 | } |
| 61 | Ok(()) |
| 62 | } |
| 63 | |
| 64 | fn print_status(report: &DshStatusReport) { |
| 65 | println!("DeepSeek Harness (dsh) — {}", RELATIONSHIP_LABEL); |
| 66 | println!(" state: {}", report.state.label()); |
| 67 | println!(" summary: {}", dsh::status_line(report)); |
| 68 | match report.detection.binary.as_ref() { |
| 69 | Some(bin) => println!(" dsh binary: {}", bin.display()), |
| 70 | None => println!(" dsh binary: not on PATH"), |
| 71 | } |
| 72 | println!( |
| 73 | " dsh version: {} ({})", |
| 74 | report.detection.version.as_deref().unwrap_or("unknown"), |
| 75 | report.detection.compatibility.label() |
| 76 | ); |
| 77 | println!( |
| 78 | " DSH_HOME: {}{}{}", |
| 79 | report.detection.dsh_home.display(), |
| 80 | if report.detection.dsh_home_from_env { |
| 81 | " (from env)" |
| 82 | } else { |
| 83 | "" |
| 84 | }, |
| 85 | if report.detection.dsh_home_exists { |
| 86 | "" |
| 87 | } else { |
| 88 | " (absent)" |
| 89 | } |
| 90 | ); |
| 91 | if !report.detection.profiles.is_empty() { |
| 92 | println!(" dsh profiles: {}", report.detection.profiles.join(", ")); |
| 93 | } |
| 94 | println!( |
| 95 | " dsh credentials file: {}{}", |
| 96 | if report.detection.credentials_present { |
| 97 | "present" |
| 98 | } else { |
| 99 | "absent" |
| 100 | }, |
| 101 | match report.detection.credentials_mode_ok { |
| 102 | Some(true) => " (0600)", |
| 103 | Some(false) => " (mode is not 0600)", |
| 104 | None => "", |
| 105 | } |
| 106 | ); |
| 107 | if !report.shadowing_namespaces.is_empty() { |
| 108 | println!( |
| 109 | " dsh settings.yaml sections that can shadow the overlay: {}", |
| 110 | report.shadowing_namespaces.join(", ") |
| 111 | ); |
| 112 | } |
| 113 | println!( |
| 114 | " dsh plugin path (bundle): {}", |
| 115 | report.bundle_availability.label() |
| 116 | ); |
| 117 | println!(" Codewhale-owned files: {}", report.paths_root.display()); |
| 118 | println!( |
| 119 | " overlay: {}{}", |
| 120 | report.overlay_path.display(), |
| 121 | if report.overlay_present { |
| 122 | "" |
| 123 | } else { |
| 124 | " (absent)" |
| 125 | } |
| 126 | ); |
| 127 | if let Some(record) = report.record.as_ref() { |
| 128 | println!( |
| 129 | " connected: {} · profile {} · {}/{} via {} · permission {}{}", |
| 130 | record.connected_at, |
| 131 | record.profile, |
| 132 | record.identity.source.provider_id, |
| 133 | record.identity.source.model, |
| 134 | record.identity.dsh_provider().unwrap_or("unsupported"), |
| 135 | record.identity.permission_mode.as_str(), |
| 136 | if record.disabled { " · DISABLED" } else { "" } |
| 137 | ); |
| 138 | match record.bundle.as_ref() { |
| 139 | Some(bundle) => println!( |
| 140 | " bundle: installed in DSH profile `{}` ({}) · {} {} · app {} · patch sha256 {}{}", |
| 141 | bundle.profile, |
| 142 | bundle.profile_dir.display(), |
| 143 | bundle.package_name, |
| 144 | bundle.package_version, |
| 145 | bundle.app_bundle.package_name(), |
| 146 | bundle.patch_sha256, |
| 147 | report |
| 148 | .bundle_profile_bundles |
| 149 | .as_ref() |
| 150 | .map(|list| format!(" · profile bundles [{}]", list.join(", "))) |
| 151 | .unwrap_or_else(|| " · profile manifest unreadable".to_string()) |
| 152 | ), |
| 153 | None => println!( |
| 154 | " bundle: not installed · plugin path {} · `{CLI_COMMAND} install-bundle`", |
| 155 | report.bundle_availability.label() |
| 156 | ), |
| 157 | } |
| 158 | if record.skin_enabled { |
| 159 | println!( |
| 160 | " skin: on (Codewhale palette via overrideTokens in the bundle profile; `{CLI_COMMAND} update --skin false` turns it off)" |
| 161 | ); |
| 162 | if record.ocean_enabled { |
| 163 | println!( |
| 164 | " ocean: on (ambient whales + glyph fish canvas behind the DSH web UI; `{CLI_COMMAND} update --ocean false` turns it off; in the browser localStorage[\"{}\"] = \"off\" or window.{}.stop())", |
| 165 | dsh::scene::OCEAN_STORAGE_KEY, |
| 166 | dsh::scene::OCEAN_WINDOW_HANDLE |
| 167 | ); |
| 168 | } else { |
| 169 | println!(" ocean: off (`{CLI_COMMAND} update --ocean true` turns it on)"); |
| 170 | } |
| 171 | } |
| 172 | } |
| 173 | match (&report.current_identity, &report.current_identity_error) { |
| 174 | (Some(now), _) if now.mappable() => println!( |
| 175 | " current Codewhale route: {}/{} · {} · would map via {}", |
| 176 | now.source.provider_id, |
| 177 | now.source.model, |
| 178 | now.source.base_url, |
| 179 | now.dsh_provider().unwrap_or("(not mappable)") |
| 180 | ), |
| 181 | (Some(now), _) => { |
| 182 | if let DshAdapter::Unsupported { reason } = &now.adapter { |
| 183 | println!( |
| 184 | " current Codewhale route: {}/{} · {} · cannot be carried by DSH: {reason}", |
| 185 | now.source.provider_id, now.source.model, now.source.base_url |
| 186 | ); |
| 187 | } |
| 188 | } |
| 189 | (None, Some(error)) => println!(" current Codewhale route: unresolved ({error})"), |
| 190 | (None, None) => {} |
| 191 | } |
| 192 | match &report.state { |
| 193 | DshIntegrationState::Detected { .. } => { |
| 194 | println!(" next: `{CLI_COMMAND} plan` then `{CLI_COMMAND} connect`") |
| 195 | } |
| 196 | DshIntegrationState::StaleConfig { .. } => println!(" next: `{CLI_COMMAND} update`"), |
| 197 | DshIntegrationState::Disabled { .. } => println!(" next: `{CLI_COMMAND} enable`"), |
| 198 | DshIntegrationState::Connected { .. } | DshIntegrationState::StaleVersion { .. } => { |
| 199 | println!( |
| 200 | " next: `{CLI_COMMAND} launch [--profile web|headless|codewhale] [dsh app args]`" |
| 201 | ) |
| 202 | } |
| 203 | _ => {} |
| 204 | } |
| 205 | } |
| 206 | |
| 207 | fn print_plan(plan: &DshPlan, event: &str) { |
| 208 | println!("{RELATIONSHIP_LABEL} — {event} plan (nothing written yet)"); |
| 209 | println!( |
| 210 | " identity: {}/{} → DSH provider {} · endpoint {}", |
| 211 | plan.mapped.source.provider_id, |
| 212 | plan.mapped.source.model, |
| 213 | plan.mapped.dsh_provider().unwrap_or("unsupported"), |
| 214 | plan.mapped.source.base_url |
| 215 | ); |
| 216 | println!( |
| 217 | " reasoning: codewhale={} → dsh={}", |
| 218 | plan.mapped |
| 219 | .source |
| 220 | .reasoning_effort |
| 221 | .as_deref() |
| 222 | .unwrap_or("inherit"), |
| 223 | plan.mapped |
| 224 | .dsh_reasoning_effort |
| 225 | .as_deref() |
| 226 | .unwrap_or("(default)") |
| 227 | ); |
| 228 | println!( |
| 229 | " permission: DSH_PERMISSION_MODE={}", |
| 230 | plan.mapped.permission_mode.as_str() |
| 231 | ); |
| 232 | println!(" workspace: {}", plan.mapped.source.workspace); |
| 233 | println!(" will write: {}", plan.overlay_path.display()); |
| 234 | println!(" will write: {}", plan.receipt_path.display()); |
| 235 | if let Some(skin) = plan.skin_path.as_ref() { |
| 236 | println!(" will write: {} (+ preview html)", skin.display()); |
| 237 | } |
| 238 | println!( |
| 239 | " will NOT write: anything under $DSH_HOME, any API key, OAuth file, or file contents" |
| 240 | ); |
| 241 | println!(" overlay sha256: {}", plan.overlay_sha256); |
| 242 | println!(" launch: {}", plan.launch_command); |
| 243 | for line in &plan.disclosures { |
| 244 | println!(" note: {line}"); |
| 245 | } |
| 246 | println!(" overlay preview:"); |
| 247 | for line in plan.overlay_text.lines() { |
| 248 | println!(" {line}"); |
| 249 | } |
| 250 | } |
| 251 | |
| 252 | fn run_dsh(config: &Config, workspace: &Path, command: DshIntegrationCommand) -> Result<()> { |
| 253 | match command { |
| 254 | DshIntegrationCommand::Status { json } => { |
| 255 | let (_paths, report) = status_report(config, workspace, false)?; |
| 256 | if json { |
| 257 | println!("{}", serde_json::to_string_pretty(&report)?); |
| 258 | } else { |
| 259 | print_status(&report); |
| 260 | } |
| 261 | Ok(()) |
| 262 | } |
| 263 | DshIntegrationCommand::Plan { |
| 264 | json, |
| 265 | profile, |
| 266 | allow_full_access, |
| 267 | skin, |
| 268 | } => { |
| 269 | let (paths, report) = status_report(config, workspace, allow_full_access)?; |
| 270 | let identity = dsh::codewhale_route_identity(config, workspace) |
| 271 | .map_err(|e| anyhow::anyhow!("cannot resolve the current Codewhale route: {e}"))?; |
| 272 | let plan = dsh::plan( |
| 273 | &paths, |
| 274 | &report.detection, |
| 275 | &identity, |
| 276 | &profile, |
| 277 | allow_full_access, |
| 278 | skin, |
| 279 | true, |
| 280 | )?; |
| 281 | if json { |
| 282 | println!("{}", serde_json::to_string_pretty(&plan)?); |
| 283 | } else { |
| 284 | print_plan(&plan, "connect"); |
| 285 | if !report.detection.installed() { |
| 286 | println!( |
| 287 | " warning: dsh is not on PATH; the plan is valid but launch will fail" |
| 288 | ); |
| 289 | } |
| 290 | } |
| 291 | Ok(()) |
| 292 | } |
| 293 | DshIntegrationCommand::Connect { |
| 294 | profile, |
| 295 | allow_full_access, |
| 296 | skin, |
| 297 | yes, |
| 298 | } => { |
| 299 | let (paths, report) = status_report(config, workspace, allow_full_access)?; |
| 300 | ensure_launchable_dsh(&report)?; |
| 301 | if report.record.as_ref().is_some_and(|r| !r.disabled) { |
| 302 | anyhow::bail!( |
| 303 | "DSH is already connected; use `{CLI_COMMAND} update` to rewrite the overlay" |
| 304 | ); |
| 305 | } |
| 306 | let identity = dsh::codewhale_route_identity(config, workspace) |
| 307 | .map_err(|e| anyhow::anyhow!("cannot resolve the current Codewhale route: {e}"))?; |
| 308 | let plan = dsh::plan( |
| 309 | &paths, |
| 310 | &report.detection, |
| 311 | &identity, |
| 312 | &profile, |
| 313 | allow_full_access, |
| 314 | skin, |
| 315 | true, |
| 316 | )?; |
| 317 | print_plan(&plan, "connect"); |
| 318 | confirm(yes, "Write these Codewhale-owned files and connect DSH?")?; |
| 319 | let record = |
| 320 | dsh::apply_plan(&paths, &report.detection, &plan, DshReceiptEvent::Connect)?; |
| 321 | println!( |
| 322 | "connected: {} (sha256 {})", |
| 323 | record.overlay_path.display(), |
| 324 | record.overlay_sha256 |
| 325 | ); |
| 326 | println!("receipt: {}", paths.receipt.display()); |
| 327 | Ok(()) |
| 328 | } |
| 329 | DshIntegrationCommand::Update { |
| 330 | profile, |
| 331 | allow_full_access, |
| 332 | skin, |
| 333 | ocean, |
| 334 | yes, |
| 335 | } => { |
| 336 | let (paths, report) = status_report(config, workspace, allow_full_access)?; |
| 337 | ensure_launchable_dsh(&report)?; |
| 338 | let record = report.record.as_ref().ok_or_else(|| { |
| 339 | anyhow::anyhow!("DSH is not connected; run `{CLI_COMMAND} connect`") |
| 340 | })?; |
| 341 | let profile = profile.unwrap_or_else(|| record.profile.clone()); |
| 342 | let skin = skin.unwrap_or(record.skin_enabled); |
| 343 | let ocean = ocean.unwrap_or(record.ocean_enabled); |
| 344 | let identity = dsh::codewhale_route_identity(config, workspace) |
| 345 | .map_err(|e| anyhow::anyhow!("cannot resolve the current Codewhale route: {e}"))?; |
| 346 | let plan = dsh::plan( |
| 347 | &paths, |
| 348 | &report.detection, |
| 349 | &identity, |
| 350 | &profile, |
| 351 | allow_full_access, |
| 352 | skin, |
| 353 | ocean, |
| 354 | )?; |
| 355 | print_plan(&plan, "update"); |
| 356 | confirm(yes, "Rewrite the Codewhale overlay for DSH?")?; |
| 357 | let record = |
| 358 | dsh::apply_plan(&paths, &report.detection, &plan, DshReceiptEvent::Update)?; |
| 359 | println!( |
| 360 | "updated: {} (sha256 {})", |
| 361 | record.overlay_path.display(), |
| 362 | record.overlay_sha256 |
| 363 | ); |
| 364 | Ok(()) |
| 365 | } |
| 366 | DshIntegrationCommand::Launch { |
| 367 | profile, |
| 368 | dry_run, |
| 369 | args, |
| 370 | } => { |
| 371 | let (_paths, report) = status_report(config, workspace, false)?; |
| 372 | let spec = dsh::launch_spec(&report, profile.as_deref(), &args, workspace)?; |
| 373 | println!("{RELATIONSHIP_LABEL}: {}", spec.display()); |
| 374 | if dry_run { |
| 375 | return Ok(()); |
| 376 | } |
| 377 | let code = dsh::spawn_launch(&spec)?; |
| 378 | if code != 0 { |
| 379 | anyhow::bail!("dsh exited with status {code}"); |
| 380 | } |
| 381 | Ok(()) |
| 382 | } |
| 383 | DshIntegrationCommand::Disable => { |
| 384 | let paths = DshPaths::from_process()?; |
| 385 | let record = dsh::set_disabled(&paths, true)?; |
| 386 | println!( |
| 387 | "disabled: overlay kept at {}; launches refused", |
| 388 | record.overlay_path.display() |
| 389 | ); |
| 390 | Ok(()) |
| 391 | } |
| 392 | DshIntegrationCommand::Enable => { |
| 393 | let paths = DshPaths::from_process()?; |
| 394 | let record = dsh::set_disabled(&paths, false)?; |
| 395 | println!("enabled: {}", record.overlay_path.display()); |
| 396 | Ok(()) |
| 397 | } |
| 398 | DshIntegrationCommand::InstallBundle { app, yes } => { |
| 399 | let app = dsh::DshAppBundle::parse(&app) |
| 400 | .ok_or_else(|| anyhow::anyhow!("--app must be `web` or `headless`, got `{app}`"))?; |
| 401 | let (paths, report) = status_report(config, workspace, false)?; |
| 402 | ensure_launchable_dsh(&report)?; |
| 403 | let record = report.record.as_ref().ok_or_else(|| { |
| 404 | anyhow::anyhow!("DSH is not connected; run `{CLI_COMMAND} connect` first") |
| 405 | })?; |
| 406 | if let dsh::BundleAvailability::NotAvailable { reason } = &report.bundle_availability { |
| 407 | anyhow::bail!("DSH plugin path not available: {reason}"); |
| 408 | } |
| 409 | if matches!(report.state, DshIntegrationState::StaleConfig { .. }) { |
| 410 | anyhow::bail!("overlay is stale; run `{CLI_COMMAND} update` before install-bundle"); |
| 411 | } |
| 412 | let app_source = dsh::bundle::app_bundle_source( |
| 413 | report |
| 414 | .detection |
| 415 | .binary |
| 416 | .as_ref() |
| 417 | .ok_or_else(|| anyhow::anyhow!("dsh binary path is unknown"))?, |
| 418 | app, |
| 419 | )?; |
| 420 | let profile_dir = report |
| 421 | .detection |
| 422 | .dsh_home |
| 423 | .join("profiles") |
| 424 | .join(dsh::bundle::BUNDLE_PROFILE); |
| 425 | println!("{RELATIONSHIP_LABEL} — install-bundle plan (nothing written yet)"); |
| 426 | println!( |
| 427 | " will write (Codewhale-owned): {}/{{package.json,cordis.patch.yml,README.md,NOTICE.md}}", |
| 428 | paths.bundle_dir.display() |
| 429 | ); |
| 430 | println!( |
| 431 | " cordis.patch.yml = the current overlay rows (sha256 {})", |
| 432 | record.overlay_sha256 |
| 433 | ); |
| 434 | println!( |
| 435 | " will run: dsh plugin --profile {} add {}", |
| 436 | dsh::bundle::BUNDLE_PROFILE, |
| 437 | app_source.display() |
| 438 | ); |
| 439 | println!( |
| 440 | " will run: dsh plugin --profile {} add {}", |
| 441 | dsh::bundle::BUNDLE_PROFILE, |
| 442 | paths.bundle_dir.display() |
| 443 | ); |
| 444 | println!( |
| 445 | " dsh will create/modify only its dedicated profile {} (package.json, pnpm-lock.yaml, node_modules links); `web`/`headless` are untouched", |
| 446 | profile_dir.display() |
| 447 | ); |
| 448 | println!(" plugin path: {}", report.bundle_availability.label()); |
| 449 | println!(" no network: both adds are local `link:` paths"); |
| 450 | confirm( |
| 451 | yes, |
| 452 | "Install the Codewhale bundle into DSH profile `codewhale`?", |
| 453 | )?; |
| 454 | let bundle = dsh::install_bundle( |
| 455 | &paths, |
| 456 | &report.detection, |
| 457 | &dsh::ProcessRunner, |
| 458 | &report.bundle_availability, |
| 459 | app, |
| 460 | )?; |
| 461 | println!( |
| 462 | "installed: {} {} into {} (patch sha256 {})", |
| 463 | bundle.package_name, |
| 464 | bundle.package_version, |
| 465 | bundle.profile_dir.display(), |
| 466 | bundle.patch_sha256 |
| 467 | ); |
| 468 | println!( |
| 469 | "launch without --patch: dsh --profile {} (or `{CLI_COMMAND} launch`)", |
| 470 | dsh::bundle::BUNDLE_PROFILE |
| 471 | ); |
| 472 | Ok(()) |
| 473 | } |
| 474 | DshIntegrationCommand::RemoveBundle { yes } => { |
| 475 | let (paths, report) = status_report(config, workspace, false)?; |
| 476 | let bundle = report |
| 477 | .record |
| 478 | .as_ref() |
| 479 | .and_then(|r| r.bundle.as_ref()) |
| 480 | .ok_or_else(|| anyhow::anyhow!("no bundle is installed"))?; |
| 481 | println!( |
| 482 | "remove-bundle will run: dsh plugin --profile {} remove {}", |
| 483 | bundle.profile, bundle.package_name |
| 484 | ); |
| 485 | println!(" then delete only {}", paths.bundle_dir.display()); |
| 486 | println!( |
| 487 | " the DSH profile dir {} and its app bundle link stay in place (DSH-owned)", |
| 488 | bundle.profile_dir.display() |
| 489 | ); |
| 490 | confirm( |
| 491 | yes, |
| 492 | "Remove the Codewhale bundle from DSH profile `codewhale`?", |
| 493 | )?; |
| 494 | let removed = dsh::remove_bundle(&paths, &report.detection, &dsh::ProcessRunner)?; |
| 495 | println!( |
| 496 | "removed bundle; deleted {} Codewhale-owned file(s)", |
| 497 | removed.len() |
| 498 | ); |
| 499 | Ok(()) |
| 500 | } |
| 501 | DshIntegrationCommand::Remove { yes } => { |
| 502 | let paths = DshPaths::from_process()?; |
| 503 | println!( |
| 504 | "remove will delete only Codewhale-owned files under {}:", |
| 505 | paths.root.display() |
| 506 | ); |
| 507 | for path in [&paths.overlay, &paths.skin, &paths.skin_preview] { |
| 508 | if path.exists() { |
| 509 | println!(" {}", path.display()); |
| 510 | } |
| 511 | } |
| 512 | println!( |
| 513 | " (receipt history is kept at {}; $DSH_HOME is never touched)", |
| 514 | paths.receipt.display() |
| 515 | ); |
| 516 | confirm(yes, "Remove the DSH integration files?")?; |
| 517 | let removed = dsh::remove(&paths)?; |
| 518 | println!("removed {} file(s)", removed.len()); |
| 519 | Ok(()) |
| 520 | } |
| 521 | } |
| 522 | } |
| 523 | |
| 524 | fn ensure_launchable_dsh(report: &DshStatusReport) -> Result<()> { |
| 525 | match &report.state { |
| 526 | DshIntegrationState::NotInstalled => { |
| 527 | anyhow::bail!( |
| 528 | "dsh is not on PATH; install the official DeepSeek Harness first (npm i -g @deepseek-ai/dsh)" |
| 529 | ) |
| 530 | } |
| 531 | DshIntegrationState::Offline { reason } => anyhow::bail!("dsh is offline: {reason}"), |
| 532 | DshIntegrationState::Incompatible { reason, .. } => { |
| 533 | anyhow::bail!("installed dsh is incompatible with this integration: {reason}") |
| 534 | } |
| 535 | _ => Ok(()), |
| 536 | } |
| 537 | } |
| 538 |