refactor: Refactored some bundle const locations
This commit is contained in:
+32
-2
@@ -1,7 +1,7 @@
|
|||||||
use super::install_remote::{
|
use super::install_remote::{
|
||||||
canonical_source_url, owner_qualifier, repo_name_slug, validate_bundle_name,
|
canonical_source_url, owner_qualifier, repo_name_slug, validate_bundle_name,
|
||||||
};
|
};
|
||||||
use super::paths;
|
use super::{paths, request_context};
|
||||||
use crate::config::AssetCategory;
|
use crate::config::AssetCategory;
|
||||||
use crate::function::write_file_atomic;
|
use crate::function::write_file_atomic;
|
||||||
use crate::utils::IS_STDOUT_TERMINAL;
|
use crate::utils::IS_STDOUT_TERMINAL;
|
||||||
@@ -137,6 +137,7 @@ impl BundleStore {
|
|||||||
bundles: BTreeMap::new(),
|
bundles: BTreeMap::new(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = fs::read_to_string(&path)
|
let content = fs::read_to_string(&path)
|
||||||
.with_context(|| format!("failed to read {}", path.display()))?;
|
.with_context(|| format!("failed to read {}", path.display()))?;
|
||||||
let contents: StoreContents = serde_yaml::from_str(&content).with_context(|| {
|
let contents: StoreContents = serde_yaml::from_str(&content).with_context(|| {
|
||||||
@@ -147,6 +148,7 @@ impl BundleStore {
|
|||||||
path.display()
|
path.display()
|
||||||
)
|
)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
if contents.version > STORE_VERSION {
|
if contents.version > STORE_VERSION {
|
||||||
bail!(
|
bail!(
|
||||||
"{} has store version {}, but this coyote build supports up to \
|
"{} has store version {}, but this coyote build supports up to \
|
||||||
@@ -155,6 +157,7 @@ impl BundleStore {
|
|||||||
contents.version
|
contents.version
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
path,
|
path,
|
||||||
bundles: contents.bundles,
|
bundles: contents.bundles,
|
||||||
@@ -167,10 +170,12 @@ impl BundleStore {
|
|||||||
bundles: &self.bundles,
|
bundles: &self.bundles,
|
||||||
})
|
})
|
||||||
.context("failed to serialize the installed-bundles store")?;
|
.context("failed to serialize the installed-bundles store")?;
|
||||||
|
|
||||||
if let Some(parent) = self.path.parent() {
|
if let Some(parent) = self.path.parent() {
|
||||||
fs::create_dir_all(parent)
|
fs::create_dir_all(parent)
|
||||||
.with_context(|| format!("failed to create directory {}", parent.display()))?;
|
.with_context(|| format!("failed to create directory {}", parent.display()))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
write_file_atomic(&self.path, &content, None)
|
write_file_atomic(&self.path, &content, None)
|
||||||
.with_context(|| format!("failed to write {}", self.path.display()))
|
.with_context(|| format!("failed to write {}", self.path.display()))
|
||||||
}
|
}
|
||||||
@@ -244,12 +249,14 @@ impl BundleStore {
|
|||||||
}
|
}
|
||||||
None => "is reserved for an asset category".to_string(),
|
None => "is reserved for an asset category".to_string(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if base.contains('/') {
|
if base.contains('/') {
|
||||||
bail!(
|
bail!(
|
||||||
"bundle name '{base}' {reason} and cannot be qualified further; \
|
"bundle name '{base}' {reason} and cannot be qualified further; \
|
||||||
uninstall it or pick a different manifest name"
|
uninstall it or pick a different manifest name"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let owner = owner_qualifier(url)
|
let owner = owner_qualifier(url)
|
||||||
.map(|owner| sanitize_name_segment(&owner))
|
.map(|owner| sanitize_name_segment(&owner))
|
||||||
.filter(|owner| !owner.is_empty());
|
.filter(|owner| !owner.is_empty());
|
||||||
@@ -260,6 +267,7 @@ impl BundleStore {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
let qualified = format!("{owner}/{base}");
|
let qualified = format!("{owner}/{base}");
|
||||||
|
|
||||||
if let Some(source) = self.source_of_other_bundle(&qualified, &canonical) {
|
if let Some(source) = self.source_of_other_bundle(&qualified, &canonical) {
|
||||||
bail!(
|
bail!(
|
||||||
"bundle name '{base}' {reason}, and '{qualified}' is already \
|
"bundle name '{base}' {reason}, and '{qualified}' is already \
|
||||||
@@ -267,6 +275,7 @@ impl BundleStore {
|
|||||||
uninstall one or pick a different manifest name"
|
uninstall one or pick a different manifest name"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(other_source) = &collision
|
if let Some(other_source) = &collision
|
||||||
&& manifest_name.is_some()
|
&& manifest_name.is_some()
|
||||||
&& *IS_STDOUT_TERMINAL
|
&& *IS_STDOUT_TERMINAL
|
||||||
@@ -357,6 +366,7 @@ impl BundleStore {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -366,7 +376,9 @@ impl BundleStore {
|
|||||||
.bundles
|
.bundles
|
||||||
.get_mut(name)
|
.get_mut(name)
|
||||||
.expect("bundle existence checked above");
|
.expect("bundle existence checked above");
|
||||||
|
|
||||||
record.updated_at = Some(Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true));
|
record.updated_at = Some(Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true));
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -384,8 +396,10 @@ impl BundleStore {
|
|||||||
.bundles
|
.bundles
|
||||||
.get_mut(name)
|
.get_mut(name)
|
||||||
.expect("bundle existence checked above");
|
.expect("bundle existence checked above");
|
||||||
|
|
||||||
record.commit = commit.to_string();
|
record.commit = commit.to_string();
|
||||||
record.version = version;
|
record.version = version;
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -395,7 +409,9 @@ impl BundleStore {
|
|||||||
.bundles
|
.bundles
|
||||||
.get_mut(bundle)
|
.get_mut(bundle)
|
||||||
.expect("bundle existence checked above");
|
.expect("bundle existence checked above");
|
||||||
|
|
||||||
record.files.retain(|owned| owned.path != path);
|
record.files.retain(|owned| owned.path != path);
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -405,9 +421,11 @@ impl BundleStore {
|
|||||||
.bundles
|
.bundles
|
||||||
.get_mut(bundle)
|
.get_mut(bundle)
|
||||||
.expect("bundle existence checked above");
|
.expect("bundle existence checked above");
|
||||||
|
|
||||||
record
|
record
|
||||||
.mcp_servers
|
.mcp_servers
|
||||||
.retain(|owned| owned.effective_key() != effective_key);
|
.retain(|owned| owned.effective_key() != effective_key);
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -431,8 +449,10 @@ impl BundleStore {
|
|||||||
.bundles
|
.bundles
|
||||||
.get_mut(bundle)
|
.get_mut(bundle)
|
||||||
.expect("bundle existence checked above");
|
.expect("bundle existence checked above");
|
||||||
|
|
||||||
record.files.retain(|owned| owned.path != file.path);
|
record.files.retain(|owned| owned.path != file.path);
|
||||||
record.files.push(file);
|
record.files.push(file);
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -457,22 +477,27 @@ impl BundleStore {
|
|||||||
if owned.effective_key() != key {
|
if owned.effective_key() != key {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if owned.action == McpAction::Replaced {
|
if owned.action == McpAction::Replaced {
|
||||||
prior_user_origin = true;
|
prior_user_origin = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
false
|
false
|
||||||
});
|
});
|
||||||
previously_owned |= record.mcp_servers.len() != before;
|
previously_owned |= record.mcp_servers.len() != before;
|
||||||
}
|
}
|
||||||
|
|
||||||
if previously_owned && !prior_user_origin && entry.action == McpAction::Replaced {
|
if previously_owned && !prior_user_origin && entry.action == McpAction::Replaced {
|
||||||
entry.action = McpAction::Transferred;
|
entry.action = McpAction::Transferred;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.bundles
|
self.bundles
|
||||||
.get_mut(bundle)
|
.get_mut(bundle)
|
||||||
.expect("bundle existence checked above")
|
.expect("bundle existence checked above")
|
||||||
.mcp_servers
|
.mcp_servers
|
||||||
.push(entry);
|
.push(entry);
|
||||||
}
|
}
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -487,6 +512,7 @@ impl BundleStore {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -523,15 +549,19 @@ impl DriftSummary {
|
|||||||
return "-".to_string();
|
return "-".to_string();
|
||||||
}
|
}
|
||||||
let mut parts = Vec::new();
|
let mut parts = Vec::new();
|
||||||
|
|
||||||
if self.intact > 0 {
|
if self.intact > 0 {
|
||||||
parts.push(format!("{} intact", self.intact));
|
parts.push(format!("{} intact", self.intact));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.modified > 0 {
|
if self.modified > 0 {
|
||||||
parts.push(format!("{} modified locally", self.modified));
|
parts.push(format!("{} modified locally", self.modified));
|
||||||
}
|
}
|
||||||
|
|
||||||
if self.missing > 0 {
|
if self.missing > 0 {
|
||||||
parts.push(format!("{} missing", self.missing));
|
parts.push(format!("{} missing", self.missing));
|
||||||
}
|
}
|
||||||
|
|
||||||
parts.join(", ")
|
parts.join(", ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -600,7 +630,7 @@ pub fn list_installed_bundles() -> Result<()> {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut table = super::request_context::asset_table(&[
|
let mut table = request_context::asset_table(&[
|
||||||
"name",
|
"name",
|
||||||
"version",
|
"version",
|
||||||
"source",
|
"source",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use super::bundles::{
|
|||||||
BundleStore, FileAction, FileRecord, InstallMetadata, McpAction, McpServerRecord, hash_bytes,
|
BundleStore, FileAction, FileRecord, InstallMetadata, McpAction, McpServerRecord, hash_bytes,
|
||||||
hash_file,
|
hash_file,
|
||||||
};
|
};
|
||||||
use crate::config::{AssetCategory, InstallFilter, paths};
|
use crate::config::{AssetCategory, BUNDLE_MANIFEST_FILE, InstallFilter, paths};
|
||||||
#[cfg(not(windows))]
|
#[cfg(not(windows))]
|
||||||
use crate::function::Language;
|
use crate::function::Language;
|
||||||
use crate::mcp::{McpServer, McpServersConfig};
|
use crate::mcp::{McpServer, McpServersConfig};
|
||||||
@@ -17,8 +17,8 @@ use inquire::{Confirm, Select};
|
|||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use std::collections::{BTreeSet, HashMap, HashSet};
|
use std::collections::{BTreeSet, HashMap, HashSet};
|
||||||
use std::ffi::{OsStr, OsString};
|
use std::ffi::{OsStr, OsString};
|
||||||
use std::fs;
|
|
||||||
use std::path::{Component, Path, PathBuf};
|
use std::path::{Component, Path, PathBuf};
|
||||||
|
use std::{fs, iter};
|
||||||
|
|
||||||
pub fn install_remote(git_url: &str, filter: Option<InstallFilter>, force: bool) -> Result<()> {
|
pub fn install_remote(git_url: &str, filter: Option<InstallFilter>, force: bool) -> Result<()> {
|
||||||
let (url, reference) = parse_url_with_ref(git_url)?;
|
let (url, reference) = parse_url_with_ref(git_url)?;
|
||||||
@@ -123,16 +123,21 @@ fn classify_install_target(value: &str, installed_names: &[String]) -> InstallTa
|
|||||||
if let Some(category) = AssetCategory::parse(value) {
|
if let Some(category) = AssetCategory::parse(value) {
|
||||||
return InstallTarget::Category(category);
|
return InstallTarget::Category(category);
|
||||||
}
|
}
|
||||||
|
|
||||||
let name = strip_ref_suffix(value);
|
let name = strip_ref_suffix(value);
|
||||||
|
|
||||||
if installed_names.iter().any(|installed| installed == name) {
|
if installed_names.iter().any(|installed| installed == name) {
|
||||||
return InstallTarget::InstalledBundle;
|
return InstallTarget::InstalledBundle;
|
||||||
}
|
}
|
||||||
|
|
||||||
if looks_like_remote_source(value) {
|
if looks_like_remote_source(value) {
|
||||||
return InstallTarget::RemoteSource;
|
return InstallTarget::RemoteSource;
|
||||||
}
|
}
|
||||||
|
|
||||||
if is_repo_shorthand(value) {
|
if is_repo_shorthand(value) {
|
||||||
return InstallTarget::Shorthand;
|
return InstallTarget::Shorthand;
|
||||||
}
|
}
|
||||||
|
|
||||||
InstallTarget::Unknown
|
InstallTarget::Unknown
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,6 +150,7 @@ fn looks_like_remote_source(value: &str) -> bool {
|
|||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
match value.split_once(':') {
|
match value.split_once(':') {
|
||||||
Some((host, _)) => {
|
Some((host, _)) => {
|
||||||
!host.is_empty() && !host.contains('/') && !host.chars().any(char::is_whitespace)
|
!host.is_empty() && !host.contains('/') && !host.chars().any(char::is_whitespace)
|
||||||
@@ -165,6 +171,7 @@ fn is_repo_shorthand(value: &str) -> bool {
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut segments = path.split('/');
|
let mut segments = path.split('/');
|
||||||
segments.clone().count() >= 2 && segments.all(|segment| !segment.is_empty())
|
segments.clone().count() >= 2 && segments.all(|segment| !segment.is_empty())
|
||||||
}
|
}
|
||||||
@@ -176,9 +183,11 @@ fn expand_repo_shorthand(value: &str, git_host: Option<&str>) -> Result<String>
|
|||||||
.or_else(|| raw.strip_prefix("http://"))
|
.or_else(|| raw.strip_prefix("http://"))
|
||||||
.unwrap_or(raw)
|
.unwrap_or(raw)
|
||||||
.trim_matches('/');
|
.trim_matches('/');
|
||||||
|
|
||||||
if host.is_empty() || host.contains(['/', '#', '?']) || host.chars().any(char::is_whitespace) {
|
if host.is_empty() || host.contains(['/', '#', '?']) || host.chars().any(char::is_whitespace) {
|
||||||
bail!("invalid --git-host '{raw}': expected a bare host like git.somedomain.com");
|
bail!("invalid --git-host '{raw}': expected a bare host like git.somedomain.com");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(format!("https://{host}/{value}"))
|
Ok(format!("https://{host}/{value}"))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -195,6 +204,7 @@ pub fn install_or_update(
|
|||||||
'{value}' is not one"
|
'{value}' is not one"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let url = expand_repo_shorthand(value, Some(host))?;
|
let url = expand_repo_shorthand(value, Some(host))?;
|
||||||
println!("Resolved '{value}' to '{url}'");
|
println!("Resolved '{value}' to '{url}'");
|
||||||
return install_remote(&url, filter, force);
|
return install_remote(&url, filter, force);
|
||||||
@@ -239,9 +249,11 @@ pub fn install_or_update(
|
|||||||
let hint = "a remote source must be a git URL, an <owner>/<repo> shorthand \
|
let hint = "a remote source must be a git URL, an <owner>/<repo> shorthand \
|
||||||
(expanded against --git-host, default github.com), an scp-style \
|
(expanded against --git-host, default github.com), an scp-style \
|
||||||
host:path, or an explicit local path (./dir, /abs, ~)";
|
host:path, or an explicit local path (./dir, /abs, ~)";
|
||||||
|
|
||||||
if installed.is_empty() {
|
if installed.is_empty() {
|
||||||
bail!("no bundle named '{value}' is installed; none are installed ({hint})");
|
bail!("no bundle named '{value}' is installed; none are installed ({hint})");
|
||||||
}
|
}
|
||||||
|
|
||||||
bail!(
|
bail!(
|
||||||
"no bundle named '{value}' is installed; installed bundles: {} ({hint})",
|
"no bundle named '{value}' is installed; installed bundles: {} ({hint})",
|
||||||
installed.join(", ")
|
installed.join(", ")
|
||||||
@@ -372,13 +384,16 @@ fn reclassify_owned_unmodified(
|
|||||||
if planned.kind != PlannedKind::Conflict {
|
if planned.kind != PlannedKind::Conflict {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(recorded) = owned.get(provenance_path(&planned.dst).as_str()) else {
|
let Some(recorded) = owned.get(provenance_path(&planned.dst).as_str()) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
|
|
||||||
if hash_file(&planned.dst)? == *recorded {
|
if hash_file(&planned.dst)? == *recorded {
|
||||||
planned.kind = PlannedKind::Refresh;
|
planned.kind = PlannedKind::Refresh;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(plan)
|
Ok(plan)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -398,6 +413,7 @@ fn owned_unmodified_mcp_keys(
|
|||||||
.with_context(|| format!("failed to read local mcp.json at {}", local_path.display()))?;
|
.with_context(|| format!("failed to read local mcp.json at {}", local_path.display()))?;
|
||||||
let config: McpServersConfig = serde_json::from_str(&content)
|
let config: McpServersConfig = serde_json::from_str(&content)
|
||||||
.with_context(|| format!("failed to parse local mcp.json at {}", local_path.display()))?;
|
.with_context(|| format!("failed to parse local mcp.json at {}", local_path.display()))?;
|
||||||
|
|
||||||
for server in &record.mcp_servers {
|
for server in &record.mcp_servers {
|
||||||
let Some(recorded_hash) = server.sha256.as_deref() else {
|
let Some(recorded_hash) = server.sha256.as_deref() else {
|
||||||
continue;
|
continue;
|
||||||
@@ -408,10 +424,12 @@ fn owned_unmodified_mcp_keys(
|
|||||||
};
|
};
|
||||||
let serialized = serde_json::to_string(entry)
|
let serialized = serde_json::to_string(entry)
|
||||||
.with_context(|| format!("failed to serialize MCP server '{key}'"))?;
|
.with_context(|| format!("failed to serialize MCP server '{key}'"))?;
|
||||||
|
|
||||||
if hash_bytes(serialized.as_bytes()) == recorded_hash {
|
if hash_bytes(serialized.as_bytes()) == recorded_hash {
|
||||||
keys.insert(key.to_string());
|
keys.insert(key.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(keys)
|
Ok(keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -453,15 +471,18 @@ fn handle_obsolete_files(
|
|||||||
eprintln!("skipping suspicious recorded path {path}; keeping its record");
|
eprintln!("skipping suspicious recorded path {path}; keeping its record");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let full = config_dir.join(&path);
|
let full = config_dir.join(&path);
|
||||||
if !full.exists() {
|
if !full.exists() {
|
||||||
println!("dropped record for obsolete file {path} (already absent locally)");
|
println!("dropped record for obsolete file {path} (already absent locally)");
|
||||||
store.remove_file_record(bundle, &path)?;
|
store.remove_file_record(bundle, &path)?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let action = resolve_obsolete(&path, &mut sticky)?;
|
let action = resolve_obsolete(&path, &mut sticky)?;
|
||||||
apply_obsolete_action(store, bundle, &path, &full, &config_dir, action)?;
|
apply_obsolete_action(store, bundle, &path, &full, &config_dir, action)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,6 +490,7 @@ fn resolve_obsolete(path: &str, sticky: &mut Option<ObsoleteAction>) -> Result<O
|
|||||||
if let Some(action) = *sticky {
|
if let Some(action) = *sticky {
|
||||||
return Ok(action);
|
return Ok(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
if !*IS_STDOUT_TERMINAL {
|
if !*IS_STDOUT_TERMINAL {
|
||||||
return Ok(ObsoleteAction::Keep);
|
return Ok(ObsoleteAction::Keep);
|
||||||
}
|
}
|
||||||
@@ -513,6 +535,7 @@ fn apply_obsolete_action(
|
|||||||
println!("deleted obsolete file {path}");
|
println!("deleted obsolete file {path}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -548,6 +571,7 @@ pub fn uninstall_bundle(spec: &str, assume_yes: bool) -> Result<()> {
|
|||||||
if installed.is_empty() {
|
if installed.is_empty() {
|
||||||
bail!("no bundle named '{spec}' is installed; none are installed");
|
bail!("no bundle named '{spec}' is installed; none are installed");
|
||||||
}
|
}
|
||||||
|
|
||||||
bail!(
|
bail!(
|
||||||
"no bundle named '{spec}' is installed; installed bundles: {}",
|
"no bundle named '{spec}' is installed; installed bundles: {}",
|
||||||
installed.join(", ")
|
installed.join(", ")
|
||||||
@@ -587,12 +611,14 @@ pub fn uninstall_bundle(spec: &str, assume_yes: bool) -> Result<()> {
|
|||||||
&paths::config_dir(),
|
&paths::config_dir(),
|
||||||
assume_yes,
|
assume_yes,
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
if files.tools_seen {
|
if files.tools_seen {
|
||||||
println!(
|
println!(
|
||||||
"Note: compiled tool binaries remain in {} until the next --build-tools prune.",
|
"Note: compiled tool binaries remain in {} until the next --build-tools prune.",
|
||||||
paths::functions_bin_dir().display()
|
paths::functions_bin_dir().display()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mcp = uninstall_mcp_entries(
|
let mcp = uninstall_mcp_entries(
|
||||||
&mut store,
|
&mut store,
|
||||||
&name,
|
&name,
|
||||||
@@ -605,6 +631,7 @@ pub fn uninstall_bundle(spec: &str, assume_yes: bool) -> Result<()> {
|
|||||||
.get(&name)
|
.get(&name)
|
||||||
.map(|record| record.files.is_empty() && record.mcp_servers.is_empty())
|
.map(|record| record.files.is_empty() && record.mcp_servers.is_empty())
|
||||||
.unwrap_or(true);
|
.unwrap_or(true);
|
||||||
|
|
||||||
if empty {
|
if empty {
|
||||||
store.remove_bundle(&name)?;
|
store.remove_bundle(&name)?;
|
||||||
println!("\nUninstalled bundle '{name}' and removed its record.");
|
println!("\nUninstalled bundle '{name}' and removed its record.");
|
||||||
@@ -614,6 +641,7 @@ pub fn uninstall_bundle(spec: &str, assume_yes: bool) -> Result<()> {
|
|||||||
items, so re-running --uninstall offers them again."
|
items, so re-running --uninstall offers them again."
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
println!(
|
println!(
|
||||||
" files: deleted={} kept={} missing={} failed={}",
|
" files: deleted={} kept={} missing={} failed={}",
|
||||||
files.deleted, files.kept, files.missing, files.failed
|
files.deleted, files.kept, files.missing, files.failed
|
||||||
@@ -623,12 +651,15 @@ pub fn uninstall_bundle(spec: &str, assume_yes: bool) -> Result<()> {
|
|||||||
mcp.removed.len(),
|
mcp.removed.len(),
|
||||||
mcp.kept.len()
|
mcp.kept.len()
|
||||||
);
|
);
|
||||||
|
|
||||||
if !mcp.removed.is_empty() {
|
if !mcp.removed.is_empty() {
|
||||||
println!(" - removed servers: {}", mcp.removed.join(", "));
|
println!(" - removed servers: {}", mcp.removed.join(", "));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !mcp.kept.is_empty() {
|
if !mcp.kept.is_empty() {
|
||||||
println!(" = kept servers: {}", mcp.kept.join(", "));
|
println!(" = kept servers: {}", mcp.kept.join(", "));
|
||||||
}
|
}
|
||||||
|
|
||||||
if !mcp.secrets.is_empty() {
|
if !mcp.secrets.is_empty() {
|
||||||
println!(
|
println!(
|
||||||
" ~ vault secrets referenced by this bundle's servers \
|
" ~ vault secrets referenced by this bundle's servers \
|
||||||
@@ -636,6 +667,7 @@ pub fn uninstall_bundle(spec: &str, assume_yes: bool) -> Result<()> {
|
|||||||
mcp.secrets.join(", ")
|
mcp.secrets.join(", ")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -645,6 +677,7 @@ fn select_uninstall_candidate(store: &BundleStore, spec: &str) -> Result<Option<
|
|||||||
if !is_repo_shorthand(spec) {
|
if !is_repo_shorthand(spec) {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let needle = format!("/{}", canonical_source_url(spec));
|
let needle = format!("/{}", canonical_source_url(spec));
|
||||||
let candidates: Vec<(String, String)> = store
|
let candidates: Vec<(String, String)> = store
|
||||||
.iter()
|
.iter()
|
||||||
@@ -660,6 +693,7 @@ fn select_uninstall_candidate(store: &BundleStore, spec: &str) -> Result<Option<
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|(name, source)| format!("{name} ({source})"))
|
.map(|(name, source)| format!("{name} ({source})"))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
if !*IS_STDOUT_TERMINAL {
|
if !*IS_STDOUT_TERMINAL {
|
||||||
bail!(
|
bail!(
|
||||||
"'{spec}' matches multiple installed bundles: {}; re-run with the \
|
"'{spec}' matches multiple installed bundles: {}; re-run with the \
|
||||||
@@ -667,16 +701,19 @@ fn select_uninstall_candidate(store: &BundleStore, spec: &str) -> Result<Option<
|
|||||||
described.join(", ")
|
described.join(", ")
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
let picked = Select::new(
|
let picked = Select::new(
|
||||||
&format!("Multiple bundles match '{spec}'; which one should be uninstalled?"),
|
&format!("Multiple bundles match '{spec}'; which one should be uninstalled?"),
|
||||||
described.clone(),
|
described.clone(),
|
||||||
)
|
)
|
||||||
.prompt()
|
.prompt()
|
||||||
.with_context(|| "failed to read uninstall selection")?;
|
.with_context(|| "failed to read uninstall selection")?;
|
||||||
|
|
||||||
let index = described
|
let index = described
|
||||||
.iter()
|
.iter()
|
||||||
.position(|option| option == &picked)
|
.position(|option| option == &picked)
|
||||||
.expect("selection came from the presented options");
|
.expect("selection came from the presented options");
|
||||||
|
|
||||||
Ok(Some(candidates[index].0.clone()))
|
Ok(Some(candidates[index].0.clone()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -711,6 +748,7 @@ fn uninstall_owned_files(
|
|||||||
summary.failed += 1;
|
summary.failed += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let full = config_dir.join(Path::new(&file.path));
|
let full = config_dir.join(Path::new(&file.path));
|
||||||
if !full.exists() {
|
if !full.exists() {
|
||||||
println!(
|
println!(
|
||||||
@@ -721,11 +759,14 @@ fn uninstall_owned_files(
|
|||||||
summary.missing += 1;
|
summary.missing += 1;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
summary.tools_seen |= file.category == "functions/tools";
|
summary.tools_seen |= file.category == "functions/tools";
|
||||||
|
|
||||||
if matches!(hash_file(&full), Ok(hash) if hash == file.sha256) {
|
if matches!(hash_file(&full), Ok(hash) if hash == file.sha256) {
|
||||||
delete_owned_file(store, bundle, &file.path, &full, config_dir, &mut summary)?;
|
delete_owned_file(store, bundle, &file.path, &full, config_dir, &mut summary)?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let prompt = format!("File {} was modified locally after install", file.path);
|
let prompt = format!("File {} was modified locally after install", file.path);
|
||||||
let action = resolve_uninstall_action(&prompt, assume_yes, &mut sticky)?;
|
let action = resolve_uninstall_action(&prompt, assume_yes, &mut sticky)?;
|
||||||
apply_uninstall_file_action(
|
apply_uninstall_file_action(
|
||||||
@@ -738,6 +779,7 @@ fn uninstall_owned_files(
|
|||||||
&mut summary,
|
&mut summary,
|
||||||
)?;
|
)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(summary)
|
Ok(summary)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -759,6 +801,7 @@ fn apply_uninstall_file_action(
|
|||||||
delete_owned_file(store, bundle, path, full, config_dir, summary)?;
|
delete_owned_file(store, bundle, path, full, config_dir, summary)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -797,9 +840,11 @@ fn prune_empty_dirs(full: &Path, config_dir: &Path) {
|
|||||||
if dir == config_dir || !dir.starts_with(config_dir) {
|
if dir == config_dir || !dir.starts_with(config_dir) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if fs::remove_dir(dir).is_err() {
|
if fs::remove_dir(dir).is_err() {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
current = dir.parent();
|
current = dir.parent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -812,9 +857,11 @@ fn resolve_uninstall_action(
|
|||||||
if assume_yes || !*IS_STDOUT_TERMINAL {
|
if assume_yes || !*IS_STDOUT_TERMINAL {
|
||||||
return Ok(ObsoleteAction::Keep);
|
return Ok(ObsoleteAction::Keep);
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(action) = *sticky {
|
if let Some(action) = *sticky {
|
||||||
return Ok(action);
|
return Ok(action);
|
||||||
}
|
}
|
||||||
|
|
||||||
let choice = Select::new(prompt, vec!["keep", "delete", "keep-all", "delete-all"])
|
let choice = Select::new(prompt, vec!["keep", "delete", "keep-all", "delete-all"])
|
||||||
.prompt()
|
.prompt()
|
||||||
.with_context(|| "failed to read uninstall choice")?;
|
.with_context(|| "failed to read uninstall choice")?;
|
||||||
@@ -847,6 +894,7 @@ fn uninstall_mcp_entries(
|
|||||||
if servers.is_empty() {
|
if servers.is_empty() {
|
||||||
return Ok(summary);
|
return Ok(summary);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut config = if mcp_path.exists() {
|
let mut config = if mcp_path.exists() {
|
||||||
let content = fs::read_to_string(mcp_path)
|
let content = fs::read_to_string(mcp_path)
|
||||||
.with_context(|| format!("failed to read {}", mcp_path.display()))?;
|
.with_context(|| format!("failed to read {}", mcp_path.display()))?;
|
||||||
@@ -887,6 +935,7 @@ fn uninstall_mcp_entries(
|
|||||||
summary.kept.push(key);
|
summary.kept.push(key);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
let Some(entry) = config.as_ref().and_then(|cfg| cfg.mcp_servers.get(&key)) else {
|
let Some(entry) = config.as_ref().and_then(|cfg| cfg.mcp_servers.get(&key)) else {
|
||||||
println!("dropped record for absent server '{key}'");
|
println!("dropped record for absent server '{key}'");
|
||||||
released.push(key);
|
released.push(key);
|
||||||
@@ -934,6 +983,7 @@ fn uninstall_mcp_entries(
|
|||||||
for key in &released {
|
for key in &released {
|
||||||
store.remove_mcp_record(bundle, key)?;
|
store.remove_mcp_record(bundle, key)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(summary)
|
Ok(summary)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1155,8 +1205,6 @@ fn scan_remote_layout(root: &Path) -> Result<RemoteLayout> {
|
|||||||
Ok(layout)
|
Ok(layout)
|
||||||
}
|
}
|
||||||
|
|
||||||
const BUNDLE_MANIFEST_FILE: &str = "coyote-bundle.yaml";
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
pub(crate) struct BundleManifest {
|
pub(crate) struct BundleManifest {
|
||||||
pub(crate) name: String,
|
pub(crate) name: String,
|
||||||
@@ -1196,6 +1244,7 @@ fn register_bundle(
|
|||||||
homepage: manifest.and_then(|m| m.homepage.clone()),
|
homepage: manifest.and_then(|m| m.homepage.clone()),
|
||||||
},
|
},
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
Ok(resolved.name)
|
Ok(resolved.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1204,12 +1253,14 @@ fn parse_bundle_manifest(root: &Path) -> Result<Option<BundleManifest>> {
|
|||||||
if !path.is_file() {
|
if !path.is_file() {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
let content = fs::read_to_string(&path)
|
let content = fs::read_to_string(&path)
|
||||||
.with_context(|| format!("failed to read bundle manifest at {}", path.display()))?;
|
.with_context(|| format!("failed to read bundle manifest at {}", path.display()))?;
|
||||||
let manifest: BundleManifest = serde_yaml::from_str(&content)
|
let manifest: BundleManifest = serde_yaml::from_str(&content)
|
||||||
.with_context(|| format!("invalid bundle manifest at {}", path.display()))?;
|
.with_context(|| format!("invalid bundle manifest at {}", path.display()))?;
|
||||||
validate_bundle_name(&manifest.name)
|
validate_bundle_name(&manifest.name)
|
||||||
.with_context(|| format!("invalid bundle name in manifest at {}", path.display()))?;
|
.with_context(|| format!("invalid bundle name in manifest at {}", path.display()))?;
|
||||||
|
|
||||||
Ok(Some(manifest))
|
Ok(Some(manifest))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1224,7 +1275,8 @@ pub(crate) fn validate_bundle_name(name: &str) -> Result<()> {
|
|||||||
(as the owner qualifier separator)"
|
(as the owner qualifier separator)"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
for part in owner.into_iter().chain(std::iter::once(base)) {
|
|
||||||
|
for part in owner.into_iter().chain(iter::once(base)) {
|
||||||
if part.is_empty() {
|
if part.is_empty() {
|
||||||
bail!("Invalid bundle name '{name}': name segments cannot be empty");
|
bail!("Invalid bundle name '{name}': name segments cannot be empty");
|
||||||
}
|
}
|
||||||
@@ -1238,6 +1290,7 @@ pub(crate) fn validate_bundle_name(name: &str) -> Result<()> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1285,6 +1338,7 @@ pub(crate) fn owner_qualifier(url: &str) -> Option<String> {
|
|||||||
if segments.len() >= 2 {
|
if segments.len() >= 2 {
|
||||||
return Some(segments[segments.len() - 2].to_string());
|
return Some(segments[segments.len() - 2].to_string());
|
||||||
}
|
}
|
||||||
|
|
||||||
let sanitized = sanitize_host(&host);
|
let sanitized = sanitize_host(&host);
|
||||||
(!sanitized.is_empty()).then_some(sanitized)
|
(!sanitized.is_empty()).then_some(sanitized)
|
||||||
}
|
}
|
||||||
@@ -1595,6 +1649,7 @@ fn read_full(file: &mut fs::File, buf: &mut [u8]) -> std::io::Result<usize> {
|
|||||||
}
|
}
|
||||||
filled += n;
|
filled += n;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(filled)
|
Ok(filled)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1619,6 +1674,7 @@ fn print_plan_summary(plan: &InstallPlan) {
|
|||||||
if refresh > 0 {
|
if refresh > 0 {
|
||||||
line.push_str(&format!(" refresh={refresh}"));
|
line.push_str(&format!(" refresh={refresh}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
println!("{line}");
|
println!("{line}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1764,9 +1820,11 @@ fn record_mcp_merge(store: &mut BundleStore, bundle: &str, report: &McpMergeRepo
|
|||||||
sha256: report.entry_hashes.get(renamed_to).cloned(),
|
sha256: report.entry_hashes.get(renamed_to).cloned(),
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
if entries.is_empty() {
|
if entries.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
store.record_mcp_servers(bundle, entries)
|
store.record_mcp_servers(bundle, entries)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -160,6 +160,7 @@ const SBX_KIT_HASH_FILE: &str = "kit.sha256";
|
|||||||
const SBX_MIXIN_FILE_NAME: &str = "sbx-mixin.yaml";
|
const SBX_MIXIN_FILE_NAME: &str = "sbx-mixin.yaml";
|
||||||
pub(crate) const VAULT_DATA_FILE_NAME: &str = "vault.yml";
|
pub(crate) const VAULT_DATA_FILE_NAME: &str = "vault.yml";
|
||||||
const INSTALLED_BUNDLES_FILE_NAME: &str = "installed-bundles.yaml";
|
const INSTALLED_BUNDLES_FILE_NAME: &str = "installed-bundles.yaml";
|
||||||
|
const BUNDLE_MANIFEST_FILE: &str = "coyote-bundle.yaml";
|
||||||
const SBX_MIXIN_KITS_DIR_NAME: &str = "sbx-mixin-kits";
|
const SBX_MIXIN_KITS_DIR_NAME: &str = "sbx-mixin-kits";
|
||||||
const GIT_DIR_NAME: &str = ".git";
|
const GIT_DIR_NAME: &str = ".git";
|
||||||
const GITIGNORE_FILE_NAME: &str = ".gitignore";
|
const GITIGNORE_FILE_NAME: &str = ".gitignore";
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ use super::{
|
|||||||
Input, InstallFilter, LEFT_PROMPT, LastMessage, MESSAGES_FILE_NAME, MacroAllowlistLevel,
|
Input, InstallFilter, LEFT_PROMPT, LastMessage, MESSAGES_FILE_NAME, MacroAllowlistLevel,
|
||||||
MacroPolicy, MacroSource, MacroState, RESERVED_MACRO_NAMES, RIGHT_PROMPT, ResolvedMacro, Role,
|
MacroPolicy, MacroSource, MacroState, RESERVED_MACRO_NAMES, RIGHT_PROMPT, ResolvedMacro, Role,
|
||||||
RoleLike, SESSIONS_DIR_NAME, SUMMARIZATION_PROMPT, SUMMARY_CONTEXT_PROMPT, StateFlags,
|
RoleLike, SESSIONS_DIR_NAME, SUMMARIZATION_PROMPT, SUMMARY_CONTEXT_PROMPT, StateFlags,
|
||||||
TEMP_ROLE_NAME, TEMP_SESSION_NAME, WorkingMode, ensure_parent_exists,
|
TEMP_ROLE_NAME, TEMP_SESSION_NAME, WorkingMode, bundles, ensure_parent_exists,
|
||||||
list_agents_with_descriptions, memory, paths,
|
list_agents_with_descriptions, memory, paths,
|
||||||
};
|
};
|
||||||
use super::{MessageContentToolCalls, prompts};
|
use super::{MessageContentToolCalls, prompts};
|
||||||
@@ -36,6 +36,7 @@ use crate::utils::{
|
|||||||
};
|
};
|
||||||
use comfy_table::{ContentArrangement, Table, presets::UTF8_FULL};
|
use comfy_table::{ContentArrangement, Table, presets::UTF8_FULL};
|
||||||
|
|
||||||
|
use super::install_remote::DEFAULT_GIT_HOST;
|
||||||
use super::instructions;
|
use super::instructions;
|
||||||
use super::memory::{
|
use super::memory::{
|
||||||
DEFAULT_MEMORY_CAP_WITH_TOOLS, DEFAULT_MEMORY_CAP_WITHOUT_TOOLS, MemoryStore, WorkspaceMemory,
|
DEFAULT_MEMORY_CAP_WITH_TOOLS, DEFAULT_MEMORY_CAP_WITHOUT_TOOLS, MemoryStore, WorkspaceMemory,
|
||||||
@@ -2803,7 +2804,7 @@ impl RequestContext {
|
|||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
"bundles" => super::bundles::list_installed_bundles(),
|
"bundles" => bundles::list_installed_bundles(),
|
||||||
_ => bail!(
|
_ => bail!(
|
||||||
"Unknown kind '{kind}'. Valid kinds: roles, sessions, agents, rags, macros, skills, tools, mcp-servers, bundles"
|
"Unknown kind '{kind}'. Valid kinds: roles, sessions, agents, rags, macros, skills, tools, mcp-servers, bundles"
|
||||||
),
|
),
|
||||||
@@ -3288,6 +3289,7 @@ impl RequestContext {
|
|||||||
"--yes".to_string(),
|
"--yes".to_string(),
|
||||||
Some("Skip the uninstall confirmation".to_string()),
|
Some("Skip the uninstall confirmation".to_string()),
|
||||||
));
|
));
|
||||||
|
|
||||||
values
|
values
|
||||||
}
|
}
|
||||||
".macro" => {
|
".macro" => {
|
||||||
@@ -3503,9 +3505,7 @@ impl RequestContext {
|
|||||||
InstallFilter::NAMES.iter().map(|s| s.to_string()).collect(),
|
InstallFilter::NAMES.iter().map(|s| s.to_string()).collect(),
|
||||||
);
|
);
|
||||||
} else if prev == "--git-host" {
|
} else if prev == "--git-host" {
|
||||||
values = super::map_completion_values(vec![
|
values = super::map_completion_values(vec![DEFAULT_GIT_HOST.to_string()]);
|
||||||
super::install_remote::DEFAULT_GIT_HOST.to_string(),
|
|
||||||
]);
|
|
||||||
} else {
|
} else {
|
||||||
let has_filter = args.iter().enumerate().any(|(i, a)| {
|
let has_filter = args.iter().enumerate().any(|(i, a)| {
|
||||||
a.starts_with("--filter=") || (*a == "--filter" && i < args.len() - 1)
|
a.starts_with("--filter=") || (*a == "--filter" && i < args.len() - 1)
|
||||||
@@ -4981,7 +4981,7 @@ mod tests {
|
|||||||
store
|
store
|
||||||
.upsert_bundle(
|
.upsert_bundle(
|
||||||
"omc",
|
"omc",
|
||||||
crate::config::bundles::InstallMetadata {
|
bundles::InstallMetadata {
|
||||||
source: "https://github.com/x/omc".to_string(),
|
source: "https://github.com/x/omc".to_string(),
|
||||||
git_ref: None,
|
git_ref: None,
|
||||||
commit: "abc123".to_string(),
|
commit: "abc123".to_string(),
|
||||||
@@ -5009,7 +5009,7 @@ mod tests {
|
|||||||
store
|
store
|
||||||
.upsert_bundle(
|
.upsert_bundle(
|
||||||
"omc",
|
"omc",
|
||||||
crate::config::bundles::InstallMetadata {
|
bundles::InstallMetadata {
|
||||||
source: "https://github.com/x/omc".to_string(),
|
source: "https://github.com/x/omc".to_string(),
|
||||||
git_ref: None,
|
git_ref: None,
|
||||||
commit: "abc123".to_string(),
|
commit: "abc123".to_string(),
|
||||||
|
|||||||
+2
-2
@@ -165,9 +165,9 @@ pub(crate) fn write_file_atomic(
|
|||||||
std::process::id(),
|
std::process::id(),
|
||||||
TMP_COUNTER.fetch_add(1, Ordering::Relaxed)
|
TMP_COUNTER.fetch_add(1, Ordering::Relaxed)
|
||||||
));
|
));
|
||||||
let write_synced = || -> std::io::Result<()> {
|
let write_synced = || -> io::Result<()> {
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
let mut file = fs::File::create(&tmp)?;
|
let mut file = File::create(&tmp)?;
|
||||||
file.write_all(content.as_bytes())?;
|
file.write_all(content.as_bytes())?;
|
||||||
file.sync_all()
|
file.sync_all()
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user