返回 CodeWhale
presets.rs
根目录 / crates / tui / src / plugins / install / dsh / presets.rs
1 //! Raw roster import joins existing Native entries; no writable root or agent store.
2 use super::*;
3
4 pub(super) const AGENT_PRESETS: &str = "@deepseek-ai/dsh-agent-presets";
5 pub(crate) const MARKER: &str = "// codewhale-native-preset-v1 ";
6
7 /// Exact contained package lookup. No upward node_modules walk, exports glob,
8 /// install script, require fallback or package-selected resolver is executed.
9 pub(super) fn contained_module(package: &Package, name: &str) -> Result<PathBuf> {
10 if name.starts_with("./") {
11 return package.contained(Path::new(name));
12 }
13 let parts: Vec<_> = name.split('/').collect();
14 let count = if name.starts_with('@') { 2 } else { 1 };
15 require(
16 parts.len() >= count
17 && parts
18 .iter()
19 .all(|p| !p.is_empty() && !matches!(*p, "." | ".."))
20 && !name.chars().any(|c| c <= ' ' || matches!(c, '\\' | ':')),
21 "Bare module must name one contained package export.",
22 )?;
23 let identity = parts[..count].join("/");
24 let root = format!("node_modules/{identity}");
25 let manifest = package.contained(Path::new(&format!("{root}/package.json")))?;
26 let data = parse_json(&utf8(read_file(&manifest, MAX_DOCUMENT)?)?)?;
27 require(
28 data.get("name").and_then(Value::str) == Some(identity.as_str())
29 && data.get("type").and_then(Value::str) == Some("module"),
30 "Bare module requires an exact contained ESM package identity.",
31 )?;
32 fn target(value: &Value) -> Result<Option<&str>> {
33 match value {
34 Value::Str(text) => Ok(Some(text)),
35 Value::Map(items) => {
36 for (key, child) in items {
37 if matches!(key.as_str(), "node" | "import" | "default")
38 && let Some(picked) = target(child)?
39 {
40 return Ok(Some(picked));
41 }
42 }
43 Ok(None)
44 }
45 _ => refuse(
46 "Null/array/non-string package targets are refused; a blocked export must not fall through to another condition.",
47 ),
48 }
49 }
50 let subpath = if parts.len() == count {
51 ".".into()
52 } else {
53 format!("./{}", parts[count..].join("/"))
54 };
55 let selected = match data.get("exports") {
56 Some(Value::Map(items)) if items.iter().any(|(key, _)| key.starts_with('.')) => match items.iter().find(|(key,_)|key==&subpath) { Some((_,value))=>target(value)?,None=>None },
57 Some(value) if subpath == "." => target(value)?,
58 Some(_) => None,
59 None if subpath == "." => Some(data.get("main").and_then(Value::str).unwrap_or("./index.js")),
60 None => Some(subpath.as_str()),
61 }.ok_or_else(|| anyhow::Error::msg("Bare package export is not admitted; conditional arrays/globs/require-only exports are refused."))?;
62 let selected = selected
63 .strip_prefix("./")
64 .filter(|path| plain_relative(path))
65 .ok_or_else(|| anyhow::Error::msg("Package export must be one plain contained path."))?;
66 package.contained(Path::new(&format!("{root}/{selected}")))
67 }
68
69 pub(super) fn has_roster(entries: &[Value]) -> bool {
70 entries.iter().any(|row| {
71 row.get("name").and_then(Value::str) == Some(AGENT_PRESETS)
72 || row
73 .get("config")
74 .is_some_and(|children| matches!(children, Value::Seq(rows) if has_roster(rows)))
75 })
76 }
77
78 /// Runs the same nonexecuting reviewer as ordinary composition import, over
79 /// contained documents and directories already covered by the source inventory.
80 pub(super) fn prepare(
81 package: &Package,
82 document: &Json,
83 output: &mut OutputFiles,
84 ) -> Result<Vec<String>> {
85 let mut directories = Vec::new();
86 let mut documents = Vec::new();
87 walk_files(&package.root, |path, is_dir| {
88 let relative = relative_slash(path, &package.root);
89 if is_dir {
90 directories.push(relative);
91 return Ok(());
92 }
93 if !matches!(
94 path.file_name().and_then(|p| p.to_str()),
95 Some("package.json" | "preset.yml" | "agent.cordis.yml")
96 ) {
97 return Ok(());
98 }
99 let source = utf8(read_file(
100 &package.contained(Path::new(&relative))?,
101 MAX_DOCUMENT,
102 )?)?;
103 documents
104 .push(json!({"path":relative,"sha256":sha256_hex(source.as_bytes()),"source":source}));
105 Ok(())
106 })?;
107 let review = crate::extension_host::composition_review::review(&json!({"kind":"agent-presets","composition":document,"documents":documents,"directories":directories})).map_err(anyhow::Error::msg)?;
108 let catalog = review
109 .get("catalog")
110 .filter(|c| c.get("version") == Some(&json!(1)))
111 .ok_or_else(|| anyhow::Error::msg("Preset reviewer omitted its catalog."))?;
112 let prepared = review
113 .get("presets")
114 .and_then(Json::as_array)
115 .filter(|p| !p.is_empty() && p.len() <= 64)
116 .ok_or_else(|| anyhow::Error::msg("Preset reviewer returned no usable bounded entries."))?;
117 let mut catalog = catalog.clone();
118 let mut entries = Vec::new();
119 for item in prepared {
120 let mut metadata = item
121 .get("metadata")
122 .cloned()
123 .ok_or_else(|| anyhow::Error::msg("Preset reviewer omitted metadata."))?;
124 let id = metadata
125 .get("id")
126 .and_then(Json::as_str)
127 .filter(|id| id.len() <= 64 && pattern(r"^[a-z0-9][a-z0-9-]*$").is_match(id))
128 .ok_or_else(|| anyhow::Error::msg("Preset reviewer returned an invalid id."))?;
129 let entry = format!("native/presets/{id}.mjs");
130 let config = format!("native/presets/{id}.json");
131 // The first line is inert data; Rust binds these display/default facts
132 // to the exact existing EntryRef bytes instead of another metadata store.
133 let source = format!(
134 "{MARKER}{}\nimport {{ mountReviewedPreset }} from '@codewhale/dsh-composition';\nimport data from './{id}.json' with {{ type: 'json' }};\nexport async function apply(ctx) {{ await mountReviewedPreset(ctx, new URL('../../source/', import.meta.url).href, data.composition, data.catalog, data.selected); }}\n",
135 serde_json::to_string(&metadata)?
136 );
137 require(
138 source.len() <= 16 * 1024,
139 "Preset entry metadata is too large.",
140 )?;
141 let digest = sha256_hex(source.as_bytes());
142 metadata["entry"] = json!({"path":entry,"sha256":digest});
143 let rows = catalog
144 .get_mut("presets")
145 .and_then(Json::as_array_mut)
146 .ok_or_else(|| anyhow::Error::msg("Preset catalog omitted rows."))?;
147 let row = rows
148 .iter_mut()
149 .find(|p| p.get("id") == metadata.get("id"))
150 .ok_or_else(|| anyhow::Error::msg("Preset catalog/entry mismatch."))?;
151 *row = metadata;
152 output.add(entry.clone(), source.into_bytes())?;
153 entries.push((entry, config, item.clone()));
154 }
155 output.add(
156 "native/presets.json".into(),
157 serde_json::to_vec_pretty(&catalog)?,
158 )?;
159 let mut paths = Vec::new();
160 for (entry, config, selected) in entries {
161 output.add(
162 config,
163 serde_json::to_vec_pretty(
164 &json!({"catalog":catalog,"selected":selected,"composition":document}),
165 )?,
166 )?;
167 paths.push(entry);
168 }
169 Ok(paths)
170 }
171
171 lines RUST