返回 CodeWhale
dsh_import.rs
根目录 / crates / tui / src / commands / groups / plugins / dsh_import.rs
1 //! `/plugin import dsh`: review, then install, a DeepSeek Harness bundle
2 //! package through the ordinary reviewed installer.
3 //!
4 //! Review converts the package into scratch and shows what converts, what is
5 //! skipped (each a manual port), the authority the bundle will request, and
6 //! the content hash. Approval installs only that exact converted bundle; it
7 //! lands disabled and untrusted like every other install, and the existing
8 //! trust and enable commands take it from there.
9
10 use std::fmt::Write as _;
11 use std::path::Path;
12
13 use codewhale_command_contract::facets::{CommandPluginContext, CommandPresentationContext};
14
15 use super::{escape_review_path, escape_review_text};
16 use crate::commands::CommandResult;
17
18 pub(super) const USAGE: &str =
19 "/plugin import dsh <package-dir>\n/plugin import dsh approve <package-dir> <content-hash>";
20
21 pub(super) fn dispatch(
22 presentation: &mut dyn CommandPresentationContext,
23 plugin: &mut dyn CommandPluginContext,
24 words: &[&str],
25 ) -> CommandResult {
26 match words {
27 ["approve", path @ .., hash] if !path.is_empty() => {
28 approve(presentation, plugin, &path.join(" "), hash)
29 }
30 path if !path.is_empty() && path[0] != "approve" => review(plugin, &path.join(" ")),
31 _ => CommandResult::error(format!("Usage:\n{USAGE}")),
32 }
33 }
34
35 fn list_line(output: &mut String, label: &str, items: &[String]) {
36 let shown = if items.is_empty() {
37 "none".to_string()
38 } else {
39 items
40 .iter()
41 .map(|item| escape_review_text(item))
42 .collect::<Vec<_>>()
43 .join(", ")
44 };
45 let _ = writeln!(output, " {label}: {shown}");
46 }
47
48 fn review(plugin: &dyn CommandPluginContext, path: &str) -> CommandResult {
49 let preview = match plugin.dsh_preview(Path::new(path)) {
50 Ok(preview) => preview,
51 Err(error) => return CommandResult::error(format!("DSH import refused: {error}")),
52 };
53 let mut output = format!(
54 "DeepSeek Harness package {}{} → plugin '{}'\n Source: {}\n",
55 escape_review_text(preview.source_package.as_deref().unwrap_or("unnamed")),
56 preview
57 .source_version
58 .as_deref()
59 .map(|version| format!("@{}", escape_review_text(version)))
60 .unwrap_or_default(),
61 preview.plugin_name,
62 escape_review_path(&preview.package_path),
63 );
64 list_line(&mut output, "Skills", &preview.skills);
65 list_line(&mut output, "Remote MCP servers", &preview.remote_servers);
66 list_line(
67 &mut output,
68 "Local MCP servers (run with your user's authority)",
69 &preview.local_servers,
70 );
71 list_line(
72 &mut output,
73 "Network hosts it will request",
74 &preview.network_hosts,
75 );
76 if preview.requires_node {
77 output.push_str(" Requires: node on PATH\n");
78 }
79 if preview.manual_ports.is_empty() {
80 output.push_str(" Skipped: none\n");
81 } else {
82 let _ = writeln!(
83 output,
84 " Skipped ({}; each needs a manual port and is not imported):",
85 preview.manual_ports.len()
86 );
87 for line in &preview.manual_ports {
88 let _ = writeln!(output, " - {}", escape_review_text(line));
89 }
90 }
91 let _ = writeln!(
92 output,
93 " Content hash: {}\n\nNothing was installed. The full conversion receipt ships in the bundle as CONVERSION.md.\nTo import exactly this bundle (it lands disabled and untrusted):\n /plugin import dsh approve {} {}",
94 preview.content_hash,
95 preview.package_path.display(),
96 preview.content_hash,
97 );
98 CommandResult::message(output)
99 }
100
101 fn approve(
102 presentation: &mut dyn CommandPresentationContext,
103 plugin: &mut dyn CommandPluginContext,
104 path: &str,
105 expected_hash: &str,
106 ) -> CommandResult {
107 // The installer re-converts and refuses unless the staged bytes match the
108 // reviewed hash, so a package edited after review installs nothing.
109 match plugin.install(&format!("dsh:{path}"), Some(expected_hash)) {
110 Ok(receipt) => {
111 super::render_install_receipt(presentation, plugin, receipt, Some(expected_hash))
112 }
113 Err(error) => super::action_error(presentation, &format!("DSH import failed: {error}")),
114 }
115 }
116
116 lines RUST