Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions crates/hk-core/src/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -927,9 +927,28 @@ impl Store {
)",
)?;

// 3. CLI children inherit pack from their parent
conn.execute_batch(
"UPDATE extensions SET pack = (
SELECT p.pack FROM extensions p
WHERE p.id = extensions.cli_parent_id AND p.pack IS NOT NULL
)
WHERE pack IS NULL
AND cli_parent_id IS NOT NULL
AND EXISTS (
SELECT 1 FROM extensions p
WHERE p.id = extensions.cli_parent_id AND p.pack IS NOT NULL
)",
)?;

Ok(())
}

/// Public wrapper so callers can re-run pack backfill after setting install_meta.
pub fn run_backfill_packs(&self) -> Result<(), HkError> {
Self::backfill_packs(&self.conn)
}

pub fn insert_audit_result(&self, result: &AuditResult) -> Result<(), HkError> {
self.conn.execute(
"INSERT INTO audit_results (extension_id, findings_json, trust_score, audited_at)
Expand Down
66 changes: 57 additions & 9 deletions crates/hk-desktop/src/commands/extensions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -523,13 +523,68 @@ pub async fn scan_and_sync(state: State<'_, AppState>) -> Result<usize, HkError>
let store = state.store.clone();
let adapters = state.adapters.clone();
tauri::async_runtime::spawn_blocking(move || {
// Scan filesystem WITHOUT holding the lock — this is the slow part
let extensions = scanner::scan_all(&adapters);
let count = extensions.len();

// Lock briefly for a single transactional write (fast — one fsync total)
let store = store.lock();

// Remember existing skill IDs so we only match NEW ones after sync
let pre_ids: std::collections::HashSet<String> = store
.list_extensions(Some(ExtensionKind::Skill), None)
.unwrap_or_default()
.into_iter()
.map(|e| e.id)
.collect();

store.sync_extensions(&extensions)?;

// Match only newly added skills without install_meta against marketplace
let unlinked: Vec<(String, String)> = store
.list_extensions(Some(ExtensionKind::Skill), None)?
.into_iter()
.filter(|e| e.install_meta.is_none() && !pre_ids.contains(&e.id))
.map(|e| (e.id, e.name))
.collect();

if !unlinked.is_empty() {
let unique_names: std::collections::HashSet<&str> =
unlinked.iter().map(|(_, n)| n.as_str()).collect();
let mut matched: std::collections::HashMap<String, (String, String, Option<String>)> =
std::collections::HashMap::new();
for name in &unique_names {
if let Ok(results) = hk_core::marketplace::search_skills(name, 5) {
let exact: Vec<_> = results.iter().filter(|r| r.name.eq_ignore_ascii_case(name)).collect();
if exact.len() == 1 {
let item = exact[0];
let git_url = hk_core::marketplace::git_url_for_source(&item.source);
let remote_rev = manager::get_remote_head(&git_url).ok();
matched.insert(name.to_string(), (git_url, item.skill_id.clone(), remote_rev));
}
}
}
if !matched.is_empty() {
let now = chrono::Utc::now();
for (id, name) in &unlinked {
if let Some((git_url, skill_id, remote_rev)) = matched.get(name.as_str()) {
let meta = InstallMeta {
install_type: "marketplace".into(),
url: Some(format!("{}/{}", git_url.trim_end_matches(".git"), skill_id)),
url_resolved: Some(git_url.clone()),
branch: None,
subpath: if skill_id.is_empty() { None } else { Some(skill_id.clone()) },
revision: remote_rev.clone(),
remote_revision: remote_rev.clone(),
checked_at: Some(now),
check_error: None,
};
let _ = store.set_install_meta(id, &meta);
}
}
// Re-run backfill to extract pack from newly set URLs
let _ = store.run_backfill_packs();
}
}

Ok(count)
})
.await
Expand Down Expand Up @@ -628,7 +683,6 @@ pub async fn check_updates(
let unique_names: std::collections::HashSet<&str> =
unlinked.iter().map(|(_, name)| name.as_str()).collect();

// For each unique name, search marketplace and resolve remote revision
let mut matched: std::collections::HashMap<String, (String, String, Option<String>)> =
std::collections::HashMap::new();
for name in &unique_names {
Expand All @@ -640,8 +694,6 @@ pub async fn check_updates(
if exact.len() == 1 {
let item = exact[0];
let git_url = hk_core::marketplace::git_url_for_source(&item.source);
// Get current remote HEAD as baseline — so we don't falsely
// show "update available" when we don't know the local version
let remote_rev = manager::get_remote_head(&git_url).ok();
matched.insert(
name.to_string(),
Expand All @@ -656,8 +708,6 @@ pub async fn check_updates(
let now = chrono::Utc::now();
for (id, name) in &unlinked {
if let Some((git_url, skill_id, remote_rev)) = matched.get(name.as_str()) {
// Set revision = remote_rev as baseline: "assume local is at this version"
// Next check_updates will detect if remote moved past this point
let meta = InstallMeta {
install_type: "marketplace".into(),
url: Some(format!(
Expand All @@ -680,8 +730,6 @@ pub async fn check_updates(
if let Err(e) = store.set_install_meta(id, &meta) {
eprintln!("[hk] warning: {e}");
}
// Don't push to updatable — we already know the status (up-to-date)
// and don't need to call get_remote_head again
}
}
}
Expand Down
Loading
Loading