返回 CodeWhale
kimi_import.rs
根目录 / crates / tui / src / commands / groups / plugins / kimi_import.rs
1 //! Explicit local import bridge for Kimi-managed plugins.
2 //!
3 //! Listing is read-only and only considers immediate, canonical child
4 //! directories of `~/.kimi-code/plugins/managed`. Import requires the exact
5 //! content hash shown by listing, then routes the local directory through the
6 //! ordinary reviewed installer. The resulting Codewhale plugin still starts
7 //! disabled and untrusted; this module never launches or probes an external
8 //! Kimi application, daemon, MCP binary, or permission grant.
9 //!
10 //! FEAT-020: the managed-directory scan runs host-side in the TUI adapter;
11 //! the handler consumes the portable `PluginManagedScan` and renders it.
12
13 use std::fmt::Write as _;
14 use std::path::Path;
15
16 use codewhale_command_contract::facets::{CommandPluginContext, CommandPresentationContext};
17
18 use crate::commands::CommandResult;
19
20 const LIST_COMMAND: &str = "/plugin import kimi [list]";
21 const APPROVE_COMMAND: &str = "/plugin import kimi approve <name> <content-hash>";
22
23 pub(super) fn usage(presentation: &mut dyn CommandPresentationContext) -> String {
24 presentation
25 .translate(
26 "plugin_kimi_usage",
27 &[
28 ("list_command", LIST_COMMAND),
29 ("approve_command", APPROVE_COMMAND),
30 ],
31 )
32 .unwrap_or_default()
33 }
34
35 pub(super) fn dispatch(
36 presentation: &mut dyn CommandPresentationContext,
37 plugin: &mut dyn CommandPluginContext,
38 words: &[&str],
39 home_override: Option<&Path>,
40 ) -> CommandResult {
41 match words {
42 [] | ["list"] => list(presentation, plugin, home_override),
43 ["approve", name, content_hash] => {
44 approve(presentation, plugin, name, content_hash, home_override)
45 }
46 _ => CommandResult::error(usage(presentation)),
47 }
48 }
49
50 fn list(
51 presentation: &mut dyn CommandPresentationContext,
52 plugin: &dyn CommandPluginContext,
53 home_override: Option<&Path>,
54 ) -> CommandResult {
55 let scan = match plugin.managed_scan(home_override) {
56 Ok(scan) => scan,
57 Err(error) => return CommandResult::error(error),
58 };
59 let root = escape_review_path(&scan.root);
60 let mut output = presentation
61 .translate("plugin_kimi_managed_root_heading", &[("root", &root)])
62 .unwrap_or_default();
63 output.push('\n');
64 if scan.candidates.is_empty() {
65 output.push_str(" ");
66 output.push_str(
67 &presentation
68 .translate("plugin_kimi_none_found", &[])
69 .unwrap_or_default(),
70 );
71 output.push('\n');
72 }
73 for candidate in &scan.candidates {
74 let name = escape_review_text(&candidate.name);
75 let version = escape_review_text(&candidate.version);
76 let license = candidate
77 .license
78 .as_deref()
79 .map(escape_review_text)
80 .unwrap_or_else(|| {
81 presentation
82 .translate("plugin_kimi_license_unspecified", &[])
83 .unwrap_or_default()
84 });
85 let applicability = presentation
86 .translate(
87 if candidate.applicable {
88 "plugin_kimi_applicable"
89 } else {
90 "plugin_kimi_not_applicable"
91 },
92 &[],
93 )
94 .unwrap_or_default();
95 let inventory = escape_review_text(&candidate.inventory);
96 let summary = presentation
97 .translate(
98 "plugin_kimi_candidate_summary",
99 &[
100 ("name", &name),
101 ("version", &version),
102 ("license", &license),
103 ("applicability", &applicability),
104 ("inventory", &inventory),
105 ],
106 )
107 .unwrap_or_default();
108 let _ = writeln!(output, "\n{summary}");
109
110 let path = escape_review_path(&candidate.canonical_path);
111 let approve_command = format!(
112 "/plugin import kimi approve {} {}",
113 candidate.name, candidate.content_hash
114 );
115 let details = presentation
116 .translate(
117 "plugin_kimi_candidate_details",
118 &[
119 ("path", &path),
120 ("content_hash", &candidate.content_hash),
121 ("capability_hash", &candidate.capability_hash),
122 ("approve_command", &approve_command),
123 ],
124 )
125 .unwrap_or_default();
126 let _ = writeln!(output, "{details}");
127 }
128 if !scan.rejected.is_empty() {
129 output.push('\n');
130 output.push_str(
131 &presentation
132 .translate("plugin_kimi_rejected_heading", &[])
133 .unwrap_or_default(),
134 );
135 output.push('\n');
136 for rejection in &scan.rejected {
137 let _ = writeln!(output, " - {rejection}");
138 }
139 }
140 output.push('\n');
141 output.push_str(
142 &presentation
143 .translate("plugin_kimi_inspection_footer", &[])
144 .unwrap_or_default(),
145 );
146 CommandResult::message(output)
147 }
148
149 fn approve(
150 presentation: &mut dyn CommandPresentationContext,
151 plugin: &mut dyn CommandPluginContext,
152 name: &str,
153 expected_hash: &str,
154 home_override: Option<&Path>,
155 ) -> CommandResult {
156 let scan = match plugin.managed_scan(home_override) {
157 Ok(scan) => scan,
158 Err(error) => return CommandResult::error(error),
159 };
160 let Some(candidate) = scan
161 .candidates
162 .into_iter()
163 .find(|candidate| candidate.name == name)
164 else {
165 let name = escape_review_text(name);
166 return CommandResult::error(
167 presentation
168 .translate(
169 "plugin_kimi_candidate_missing",
170 &[("name", &name), ("list_command", "/plugin import kimi")],
171 )
172 .unwrap_or_default(),
173 );
174 };
175 if candidate.content_hash != expected_hash {
176 let name = escape_review_text(name);
177 let expected = escape_review_text(expected_hash);
178 return CommandResult::error(
179 presentation
180 .translate(
181 "plugin_kimi_candidate_changed",
182 &[
183 ("name", &name),
184 ("expected", &expected),
185 ("actual", &candidate.content_hash),
186 ("list_command", "/plugin import kimi"),
187 ],
188 )
189 .unwrap_or_default(),
190 );
191 }
192
193 // The facet revalidates and copies the source through the ordinary local
194 // installer; its result is always rediscovered disabled/untrusted and
195 // presents the post-copy authority review before any activation.
196 super::install_bundle_with_expected_hash(
197 presentation,
198 plugin,
199 &candidate.canonical_path,
200 expected_hash,
201 )
202 }
203
204 pub(super) fn escape_review_path(path: &Path) -> String {
205 super::escape_review_path(path)
206 }
207
208 pub(super) fn escape_review_text(value: &str) -> String {
209 super::escape_review_text(value)
210 }
211
211 lines RUST