1use std::collections::{BTreeMap, BTreeSet, VecDeque};
11use std::io::{BufRead, IsTerminal, Write};
12use std::path::{Path, PathBuf};
13
14use bynk_project::lock::{LOCK_FILE, Lock, LockedNpm, lock_root, parse_lock};
15
16#[derive(Debug, Clone, PartialEq, Eq)]
18pub enum Mode {
19 Ask,
21 Package(String),
23 All,
25 List,
27}
28
29pub fn run(project: &Path, mode: &Mode) -> Result<(), String> {
31 let (path, mut lock) = read(project)?;
32 let pending = pending_rows(&lock);
33 if *mode == Mode::List {
34 list(&lock, &pending);
35 return Ok(());
36 }
37 let chosen: Vec<usize> = match mode {
38 Mode::All => pending.clone(),
39 Mode::Package(name) => {
40 let rows: Vec<usize> = pending
41 .iter()
42 .copied()
43 .filter(|&i| lock.npm[i].package == *name)
44 .collect();
45 if rows.is_empty() {
46 return Err(if lock.npm.iter().any(|n| n.package == *name) {
47 format!("the npm package `{name}` has nothing awaiting approval")
48 } else {
49 format!("no dependency's adapter requires the npm package `{name}`")
50 });
51 }
52 for &i in &rows {
53 describe(&lock, &lock.npm[i]);
54 }
55 rows
56 }
57 Mode::Ask => {
58 if pending.is_empty() {
59 eprintln!("bynk: no npm packages await approval");
60 return Ok(());
61 }
62 if !std::io::stdin().is_terminal() {
63 return Err(
64 "`bynk approve` asks on a terminal: name a package (`bynk approve <package>`), or use `--all`"
65 .to_string(),
66 );
67 }
68 ask(&lock, &pending)
69 }
70 Mode::List => unreachable!(),
71 };
72 approve(&path, &mut lock, &chosen)
73}
74
75pub fn after_write(project: &Path, pending: &[LockedNpm]) {
79 if pending.is_empty() {
80 return;
81 }
82 let Ok((path, mut lock)) = read(project) else {
83 return;
84 };
85 let rows: Vec<usize> = pending_rows(&lock)
86 .into_iter()
87 .filter(|&i| {
88 pending
89 .iter()
90 .any(|p| p.package == lock.npm[i].package && p.range == lock.npm[i].range)
91 })
92 .collect();
93 if rows.is_empty() {
94 return;
95 }
96 if std::io::stdin().is_terminal() {
97 eprintln!(
98 "bynk: {} npm package{} from fetched packages await{} approval",
99 rows.len(),
100 if rows.len() == 1 { "" } else { "s" },
101 if rows.len() == 1 { "s" } else { "" }
102 );
103 let chosen = ask(&lock, &rows);
104 if let Err(e) = approve(&path, &mut lock, &chosen) {
105 eprintln!("bynk: {e}");
106 }
107 return;
108 }
109 eprintln!("bynk: npm packages awaiting approval — a build reaching them fails until approved:");
110 for &i in &rows {
111 describe(&lock, &lock.npm[i]);
112 }
113 eprintln!(
114 "bynk: review them, then run `bynk approve <package>` (or `bynk approve` on a terminal)"
115 );
116}
117
118fn read(project: &Path) -> Result<(PathBuf, Lock), String> {
119 let root = lock_root(project);
120 let path = root.join(LOCK_FILE);
121 let text = std::fs::read_to_string(&path).map_err(|_| {
122 format!(
123 "there is no `{}` here — run `bynk fetch` first",
124 path.display()
125 )
126 })?;
127 let lock = parse_lock(&text).map_err(|e| format!("`{}`: {}", path.display(), e.0))?;
128 Ok((path, lock))
129}
130
131fn pending_rows(lock: &Lock) -> Vec<usize> {
134 let fetched = |by: &str| {
135 let name = by.split(':').next().unwrap_or_default();
136 lock.package(name)
137 .is_none_or(|p| !p.source.starts_with("path+"))
138 };
139 lock.npm
140 .iter()
141 .enumerate()
142 .filter(|(_, n)| !n.approved && n.required_by.iter().any(|by| fetched(by)))
143 .map(|(i, _)| i)
144 .collect()
145}
146
147fn list(lock: &Lock, pending: &[usize]) {
148 if lock.npm.is_empty() {
149 eprintln!("bynk: no dependency's adapter requires an npm package");
150 return;
151 }
152 for (i, row) in lock.npm.iter().enumerate() {
153 let state = if row.approved {
154 "approved"
155 } else if pending.contains(&i) {
156 "pending"
157 } else {
158 "own"
159 };
160 println!(
161 "{state:<8} {} {} (required by {})",
162 row.package,
163 row.range,
164 row.required_by.join(", ")
165 );
166 }
167}
168
169fn describe(lock: &Lock, row: &LockedNpm) {
171 eprintln!(" {} {}", row.package, row.range);
172 for by in &row.required_by {
173 let package = by.split(':').next().unwrap_or_default();
174 let mut chain = vec!["this project".to_string()];
175 chain.extend(chain_to(lock, package));
176 chain.push(by.clone());
177 eprintln!(" required through {}", chain.join(" → "));
178 }
179 eprintln!(" https://www.npmjs.com/package/{}", row.package);
180}
181
182fn ask(lock: &Lock, rows: &[usize]) -> Vec<usize> {
184 let stdin = std::io::stdin();
185 let mut lines = stdin.lock().lines();
186 let mut chosen = Vec::new();
187 for &i in rows {
188 describe(lock, &lock.npm[i]);
189 eprint!(
190 "Approve `{}` {}? [y/N] ",
191 lock.npm[i].package, lock.npm[i].range
192 );
193 let _ = std::io::stderr().flush();
194 let answer = lines.next().and_then(Result::ok).unwrap_or_default();
195 if matches!(answer.trim().to_ascii_lowercase().as_str(), "y" | "yes") {
196 chosen.push(i);
197 }
198 }
199 chosen
200}
201
202fn approve(path: &Path, lock: &mut Lock, rows: &[usize]) -> Result<(), String> {
203 if rows.is_empty() {
204 eprintln!("bynk: nothing approved");
205 return Ok(());
206 }
207 for &i in rows {
208 lock.npm[i].approved = true;
209 }
210 crate::fetch::write_atomically(path, &lock.render())
211 .map_err(|e| format!("could not write `{}`: {e}", path.display()))?;
212 for &i in rows {
213 eprintln!(
214 "bynk: approved npm package `{}` {}",
215 lock.npm[i].package, lock.npm[i].range
216 );
217 }
218 Ok(())
219}
220
221fn chain_to(lock: &Lock, target: &str) -> Vec<String> {
227 let depended: BTreeSet<&str> = lock
228 .packages
229 .iter()
230 .flat_map(|p| p.dependencies.iter().map(String::as_str))
231 .collect();
232 let mut parent: BTreeMap<&str, Option<&str>> = BTreeMap::new();
233 let mut queue = VecDeque::new();
234 for p in &lock.packages {
235 if !depended.contains(p.name.as_str()) {
236 parent.insert(&p.name, None);
237 queue.push_back(p.name.as_str());
238 }
239 }
240 while let Some(name) = queue.pop_front() {
241 let Some(p) = lock.package(name) else {
242 continue;
243 };
244 for d in &p.dependencies {
245 if !parent.contains_key(d.as_str()) {
246 parent.insert(d, Some(name));
247 queue.push_back(d);
248 }
249 }
250 }
251 let mut chain = Vec::new();
252 let mut at = parent.contains_key(target).then_some(target);
253 while let Some(name) = at {
254 chain.push(name.to_string());
255 at = parent.get(name).copied().flatten();
256 if chain.len() > lock.packages.len() {
257 break;
258 }
259 }
260 if chain.is_empty() {
261 chain.push(target.to_string());
262 }
263 chain.reverse();
264 chain
265}