- 1001
.map_err(|error| format!("capability update failed: {error}"))?; - 1002
- 1003
// Carry forward components the feed did not ship, so the manifest - 1004
// keeps describing the whole install rather than only what moved. - 1005
for existing in installed.components.clone() { - 1006
if !next_components.iter().any(|c| c.name == existing.name) { - 1007
next_components.push(existing); - 1008
} - 1009
} - 1010
installed.version = feed.version()?.to_string(); - 1011
installed.installed_at = manifest::now_rfc3339(); - 1012
installed.components = next_components; - 1013
installed.write(root)?; - 1014
- 1015
// `desktop: false` — the refusal above guarantees this update carried - 1016
// no `vak-desktop`, so there is no frontend requirement to enforce and - 1017
// nothing under Resources to replace. All this call still does is - 1018
// restamp Info.plist with the new version. - 1019
bundle::write_metadata(root, &installed.version, None, false)?; - 1020
Ok(Some(installed.version.clone())) - 1021
} - 1022
- 1023
// -------------------------------------------------------------- uninstall - 1024
- 1025
pub fn run_uninstall(prefix: Option<PathBuf>, yes: bool, purge: bool) -> i32 { - 1026
let root = InstallRoot::resolve(prefix); - 1027
if !confirm( - 1028
&format!( - 1029
"stop services and remove the managed install at {}?", - 1030
root.prefix().display() - 1031
), - 1032
yes, - 1033
) { - 1034
println!("aborted"); - 1035
return 0; - 1036
} - 1037
- 1038
// Only tear down units that actually exec from THIS prefix. - 1039
// - 1040
// Service teardown used to run against every known unit name with the - 1041
// real `Paths::default()`, whatever `--prefix` said — so uninstalling a - 1042
// throwaway prefix unregistered the operator's real services. Running - 1043
// the test suite on a machine with vak installed stopped its gateway, - 1044
// tray, and bridges, silently. A unit that execs a different install - 1045
// belongs to that install. - 1046
let paths = vak_ops::services::Paths::default(); - 1047
let owned: Vec<&str> = vak_ops::services::SERVICES - 1048
.iter() - 1049
.map(|d| d.name) - 1050
.filter(|name| vak_ops::services::unit_belongs_to_prefix(name, root.prefix(), &paths)) - 1051
.collect(); - 1052
if !owned.is_empty() - 1053
&& let Err(e) = - 1054
vak_ops::services::services_uninstall(&owned, &paths, &vak_ops::services::SystemRunner) - 1055
{ - 1056
eprintln!("warning: service teardown incomplete: {e}"); - 1057
} - 1058
- 1059
// Per-bot bridge units are generated from bots.json rather than living - 1060
// in the static table, and are filtered the same way. - 1061
let data_home = vak_config::paths::data_home(); - 1062
let owned_bots: Vec<String> = vak_ops::services::configured_bot_service_names_all(&data_home) - 1063
.into_iter() - 1064
.filter(|name| vak_ops::services::unit_belongs_to_prefix(name, root.prefix(), &paths)) - 1065
.collect(); - 1066
if !owned_bots.is_empty() { - 1067
let refs: Vec<&str> = owned_bots.iter().map(String::as_str).collect(); - 1068
if let Err(e) = - 1069
vak_ops::services::services_uninstall(&refs, &paths, &vak_ops::services::SystemRunner) - 1070
{ - 1071
eprintln!("warning: bridge teardown incomplete: {e}"); - 1072
} - 1073
} - 1074
- 1075
// Report components installed elsewhere, which removing this prefix - 1076
// will not reach. - 1077
if let Ok(m) = Manifest::read(&root) { - 1078
for c in &m.components { - 1079
if !c.path.starts_with(root.prefix()) { - 1080
eprintln!( - 1081
"note: {} lives outside the prefix and was left in place: {}", - 1082
c.name, - 1083
c.path.display() - 1084
); - 1085
} - 1086
} - 1087
} - 1088
- 1089
match std::fs::remove_dir_all(root.prefix()) { - 1090
Ok(()) => println!("removed {}", root.prefix().display()), - 1091
Err(e) if e.kind() == std::io::ErrorKind::NotFound => { - 1092
println!("nothing installed at {}", root.prefix().display()); - 1093
} - 1094
Err(e) => eprintln!("warning: remove {}: {e}", root.prefix().display()), - 1095
} - 1096
- 1097
// The symlink `report_next_steps` tells people to create. Leaving it - 1098
// is how an uninstall leaves a dangling `vak` on PATH that fails with - 1099
// a confusing "no such file" months later. - 1100
remove_dangling_cli_symlink(root.prefix()); - 1101
- 1102
if purge { - 1103
return purge_state(yes); - 1104
} - 1105
println!(); - 1106
println!("configuration and data were kept; `--purge` removes them too"); - 1107
0 - 1108
} - 1109
- 1110
/// Remove any `vak` symlink that points into the prefix we just deleted. - 1111
/// - 1112
/// Only ever a symlink, and only when it resolves into this prefix: a real - 1113
/// binary someone else installed there is theirs, not ours to remove. - 1114
/// - 1115
/// Every directory `report_next_steps` can send someone to is checked, not - 1116
/// just `/usr/local/bin`. That single hardcoded path was the bug: on Apple - 1117
/// Silicon the link lands in `/opt/homebrew/bin`, so an uninstall left a - 1118
/// dangling `vak` on PATH that failed with "no such file or directory" — - 1119
/// which reads like a broken install rather than a removed one. - 1120
fn remove_dangling_cli_symlink(prefix: &Path) { - 1121
for dir in cli_link_dirs() { - 1122
let link = dir.join("vak"); - 1123
let Ok(meta) = std::fs::symlink_metadata(&link) else { - 1124
continue; - 1125
}; - 1126
if !meta.file_type().is_symlink() { - 1127
continue; - 1128
} - 1129
let Ok(target) = std::fs::read_link(&link) else { - 1130
continue; - 1131
}; - 1132
if !target.starts_with(prefix) { - 1133
continue; - 1134
} - 1135
match std::fs::remove_file(&link) { - 1136
Ok(()) => println!("removed the {} symlink", link.display()), - 1137
Err(e) => eprintln!( - 1138
"note: {} points into the removed prefix; remove it with: sudo rm {} ({e})", - 1139
link.display(), - 1140
link.display() - 1141
), - 1142
} - 1143
} - 1144
} - 1145
- 1146
/// Everything the state registry says a purge removes. - 1147
/// - 1148
/// Derived from `vak_core::state`, not from a list kept here, and framed - 1149
/// as a **preserve rule** rather than a delete list: when a new durable - 1150
/// file appears, the safe default is to leave it alone, and the registry - 1151
/// test fails the build until someone declares what should happen to it - 1152
/// (doc 46 D3, Part VII.2). - 1153
fn purge_state(yes: bool) -> i32 { - 1154
use vak_core::state::{OnPurge, Root}; - 1155
- 1156
let mut targets: Vec<(PathBuf, &'static str)> = Vec::new(); - 1157
for root in Root::ALL { - 1158
let base = vak_core::state::root_path(root); - 1159
for entry in vak_core::state::entries_for(root) { - 1160
if entry.on_purge != OnPurge::Remove { - 1161
continue; - 1162
} - 1163
let path = if entry.path.is_empty() { - 1164
base.clone() - 1165
} else { - 1166
base.join(entry.path) - 1167
}; - 1168
if path.exists() { - 1169
targets.push((path, entry.path)); - 1170
} - 1171
} - 1172
} - 1173
- 1174
if targets.is_empty() { - 1175
println!("nothing to purge — no declared state is present"); - 1176
return 0; - 1177
} - 1178
- 1179
// Name what dies. A confirmation that says "delete everything?" is one - 1180
// people answer without reading. - 1181
println!(); - 1182
println!("--purge will permanently delete:"); - 1183
for (path, _) in &targets { - 1184
println!(" {}", path.display()); - 1185
} - 1186
println!(); - 1187
println!("this includes your Shared configuration, your stored keys, every session"); - 1188
println!("ledger, and every installed skill and plugin."); - 1189
println!("project `.vak` directories inside your own repositories are NOT touched."); - 1190
if !confirm("permanently delete all of that?", yes) { - 1191
println!("kept everything; nothing was deleted"); - 1192
return 0; - 1193
} - 1194
- 1195
let mut failed = false; - 1196
for (path, declared) in &targets { - 1197
// A symlinked root would make `remove_dir_all` follow the link and - 1198
// delete somebody else's directory. Refuse rather than guess. - 1199
match std::fs::symlink_metadata(path) { - 1200
Ok(meta) if meta.file_type().is_symlink() => { - 1201
eprintln!( - 1202
"refused: {} is a symlink; remove it by hand if you meant to", - 1203
path.display() - 1204
); - 1205
failed = true; - 1206
continue; - 1207
} - 1208
_ => {} - 1209
} - 1210
let outcome = if path.is_dir() { - 1211
std::fs::remove_dir_all(path) - 1212
} else { - 1213
std::fs::remove_file(path) - 1214
}; - 1215
match outcome { - 1216
Ok(()) => println!(" removed {declared}"), - 1217
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {} - 1218
Err(e) => { - 1219
eprintln!(" warning: {}: {e}", path.display()); - 1220
failed = true; - 1221
} - 1222
} - 1223
} - 1224
- 1225
println!(); - 1226
println!("purged. the next install is a genuine first run."); - 1227
i32::from(failed) - 1228
} - 1229
- 1230
#[cfg(test)] - 1231
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)] - 1232
mod tests { - 1233
use super::*; - 1234
- 1235
#[test] - 1236
fn confirm_refuses_rather_than_assumes_when_not_interactive() { - 1237
// A piped stdin with no --yes must not be read as consent. - 1238
assert!(!confirm("do the thing?", false)); - 1239
assert!(confirm("do the thing?", true)); - 1240
} - 1241
- 1242
#[test] - 1243
fn every_optional_component_is_declared_optional() { - 1244
let required: Vec<&str> = COMPONENTS - 1245
.iter() - 1246
.filter(|c| c.required) - 1247
.map(|c| c.name) - 1248
.collect(); - 1249
assert_eq!( - 1250
required, - 1251
vec!["vak"], - 1252
"only the CLI is required; the rest ride along when built" - 1253
); - 1254
} - 1255
- 1256
#[cfg(unix)] - 1257
fn executable(path: &Path) { - 1258
use std::os::unix::fs::PermissionsExt as _; - 1259
std::fs::write(path, "#!/bin/sh\n").unwrap(); - 1260
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o755)).unwrap(); - 1261
} - 1262
- 1263
#[cfg(unix)] - 1264
#[test] - 1265
fn cli_on_path_follows_a_symlink_into_the_install() { - 1266
let tmp = tempfile::tempdir().unwrap(); - 1267
let bin = tmp.path().join("App/Contents/MacOS"); - 1268
let link_dir = tmp.path().join("local-bin"); - 1269
std::fs::create_dir_all(&bin).unwrap(); - 1270
std::fs::create_dir_all(&link_dir).unwrap(); - 1271
let cli = bin.join("vak"); - 1272
executable(&cli); - 1273
std::os::unix::fs::symlink(&cli, link_dir.join("vak")).unwrap(); - 1274
- 1275
let path = std::env::join_paths([&link_dir]).unwrap(); - 1276
assert_eq!(cli_on_path(Some(&path), &cli), CliOnPath::ThisInstall); - 1277
let path = std::env::join_paths([&bin]).unwrap(); - 1278
assert_eq!(cli_on_path(Some(&path), &cli), CliOnPath::ThisInstall); - 1279
} - 1280
- 1281
#[cfg(unix)] - 1282
#[test] - 1283
fn cli_on_path_reports_an_earlier_different_vak_as_shadowing() { - 1284
let tmp = tempfile::tempdir().unwrap(); - 1285
let bin = tmp.path().join("install"); - 1286
let stale = tmp.path().join("cargo-bin"); - 1287
let not_exec = tmp.path().join("plain"); - 1288
for dir in [&bin, &stale, ¬_exec] { - 1289
std::fs::create_dir_all(dir).unwrap(); - 1290
} - 1291
let cli = bin.join("vak"); - 1292
executable(&cli); - 1293
executable(&stale.join("vak")); - 1294
std::fs::write(not_exec.join("vak"), "not a program").unwrap(); - 1295
- 1296
// A non-executable `vak` is skipped, as a shell would skip it. - 1297
let path = std::env::join_paths([¬_exec, &bin]).unwrap(); - 1298
assert_eq!(cli_on_path(Some(&path), &cli), CliOnPath::ThisInstall); - 1299
let path = std::env::join_paths([&stale, &bin]).unwrap(); - 1300
assert_eq!( - 1301
cli_on_path(Some(&path), &cli), - 1302
CliOnPath::Shadowed(stale.join("vak")) - 1303
); - 1304
} - 1305
- 1306
#[test] - 1307
fn cli_on_path_is_absent_without_a_match() { - 1308
let tmp = tempfile::tempdir().unwrap(); - 1309
let cli = tmp.path().join("vak"); - 1310
let empty = tempfile::tempdir().unwrap(); - 1311
let path = std::env::join_paths([empty.path()]).unwrap(); - 1312
assert_eq!(cli_on_path(Some(&path), &cli), CliOnPath::Absent); - 1313
assert_eq!(cli_on_path(None, &cli), CliOnPath::Absent); - 1314
} - 1315
} - 1316
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.