happyskills 0.22.0 → 0.22.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.22.1] - 2026-03-30
11
+
12
+ ### Fixed
13
+ - Fix sequential `detect_status` bottleneck in `status`, `refresh`, and `update` — local modification detection now runs in parallel via `Promise.all` instead of awaiting each skill sequentially
14
+
10
15
  ## [0.22.0] - 2026-03-30
11
16
 
12
17
  ### Added
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "happyskills",
3
- "version": "0.22.0",
3
+ "version": "0.22.1",
4
4
  "description": "Package manager for AI agent skills",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "author": "Nicolas Dao <nic@cloudlesslabs.com> (https://cloudlesslabs.com)",
@@ -126,16 +126,18 @@ const run = (args) => catch_errors('Refresh failed', async () => {
126
126
  }
127
127
  }
128
128
 
129
- // 5. Detect local modifications before updating
129
+ // 5. Detect local modifications for all outdated skills in parallel
130
130
  const base_dir = skills_dir(is_global, project_root)
131
- const skipped = []
132
- const safe_to_update = []
133
-
134
- for (const r of outdated) {
131
+ const detections = await Promise.all(outdated.map(r => {
135
132
  const lock_entry = skills[r.skill]
136
133
  const short_name = r.skill.split('/')[1] || r.skill
137
134
  const dir = skill_install_dir(base_dir, short_name)
138
- const [, det] = await detect_status(lock_entry, dir)
135
+ return detect_status(lock_entry, dir).then(([, det]) => ({ r, det }))
136
+ }))
137
+
138
+ const skipped = []
139
+ const safe_to_update = []
140
+ for (const { r, det } of detections) {
139
141
  if (det?.local_modified) {
140
142
  skipped.push({ skill: r.skill, reason: 'local_modifications', suggestion: `happyskills pull ${r.skill}` })
141
143
  } else {
@@ -69,18 +69,18 @@ const run = (args) => catch_errors('Status failed', async () => {
69
69
 
70
70
  const base_dir = skills_dir(is_global, project_root)
71
71
 
72
- // Detect local modifications for each skill
73
- const results = []
74
- for (const [name, data] of entries) {
75
- if (!data) {
76
- results.push({ skill: name, status: 'not_found', local_modified: false, remote_updated: false })
77
- continue
78
- }
72
+ // Detect local modifications for all skills in parallel
73
+ const detections = await Promise.all(entries.map(([name, data]) => {
74
+ if (!data) return { name, data, det: null }
79
75
  const short_name = name.split('/')[1] || name
80
76
  const dir = skill_install_dir(base_dir, short_name)
81
- const [, det] = await detect_status(data, dir)
77
+ return detect_status(data, dir).then(([, det]) => ({ name, data, det }))
78
+ }))
79
+
80
+ const results = detections.map(({ name, data, det }) => {
81
+ if (!data) return { skill: name, status: 'not_found', local_modified: false, remote_updated: false }
82
82
  const has_conflicts = (data.conflict_files || []).length > 0
83
- results.push({
83
+ return {
84
84
  skill: name,
85
85
  base_version: data.version || null,
86
86
  base_commit: data.base_commit || null,
@@ -91,8 +91,8 @@ const run = (args) => catch_errors('Status failed', async () => {
91
91
  remote_commit: null,
92
92
  conflict_files: data.conflict_files || [],
93
93
  status: has_conflicts ? 'conflicts' : 'clean'
94
- })
95
- }
94
+ }
95
+ })
96
96
 
97
97
  // Check remote for updates
98
98
  const skill_names = results.filter(r => r.status !== 'not_found').map(r => r.skill)
@@ -72,16 +72,24 @@ const run = (args) => catch_errors('Update failed', async () => {
72
72
  const base_dir = skills_dir(is_global, project_root)
73
73
  const force = args.flags.force || false
74
74
 
75
- for (const [name, data] of to_update) {
76
- // Check for local modifications before overwriting
77
- if (!force && data) {
75
+ // Detect local modifications for all skills in parallel
76
+ const modified_set = new Set()
77
+ if (!force) {
78
+ const detections = await Promise.all(to_update.map(([name, data]) => {
79
+ if (!data) return { name, modified: false }
78
80
  const short_name = name.split('/')[1] || name
79
81
  const dir = skill_install_dir(base_dir, short_name)
80
- const [, det] = await detect_status(data, dir)
81
- if (det?.local_modified) {
82
- print_warn(`${name} has local modifications. Use --force to discard, or 'happyskills pull' to merge.`)
83
- continue
84
- }
82
+ return detect_status(data, dir).then(([, det]) => ({ name, modified: det?.local_modified || false }))
83
+ }))
84
+ for (const { name, modified } of detections) {
85
+ if (modified) modified_set.add(name)
86
+ }
87
+ }
88
+
89
+ for (const [name, data] of to_update) {
90
+ if (modified_set.has(name)) {
91
+ print_warn(`${name} has local modifications. Use --force to discard, or 'happyskills pull' to merge.`)
92
+ continue
85
93
  }
86
94
 
87
95
  const before_version = data?.version || null