happyskills 0.4.3 → 0.5.0
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 +17 -0
- package/package.json +1 -1
- package/src/api/repos.js +7 -2
- package/src/commands/search.js +41 -14
- package/src/config/paths.js +1 -24
- package/src/config/paths.test.js +12 -42
- package/src/index.js +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.5.0] - 2026-03-05
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Add `--mine` flag to `search` command to browse skills across all user's workspaces
|
|
14
|
+
- Add `--personal` flag to `search` command to browse skills in the user's personal workspace only
|
|
15
|
+
- Add `-w` / `--workspace <slug>` flag to `search` command to search within specific workspace(s)
|
|
16
|
+
- Add `--tags <tags>` flag to `search` command to filter results by tags
|
|
17
|
+
- Add `scope` and `visibility` fields to `search` JSON output
|
|
18
|
+
|
|
19
|
+
### Changed
|
|
20
|
+
- Change `search` query argument from required to optional when `--mine`, `--personal`, or `--workspace` is provided (browse mode)
|
|
21
|
+
|
|
22
|
+
## [0.4.4] - 2026-03-05
|
|
23
|
+
|
|
24
|
+
### Fixed
|
|
25
|
+
- Fix all commands (`install`, `list`, `setup`, `update`, `uninstall`, `check`, `bump`, `convert`) inheriting skills from a parent directory; `find_project_root()` now always uses the current working directory and never walks up the directory tree
|
|
26
|
+
|
|
10
27
|
## [0.4.3] - 2026-03-05
|
|
11
28
|
|
|
12
29
|
### Fixed
|
package/package.json
CHANGED
package/src/api/repos.js
CHANGED
|
@@ -2,10 +2,15 @@ const { error: { catch_errors } } = require('puffy-core')
|
|
|
2
2
|
const client = require('./client')
|
|
3
3
|
|
|
4
4
|
const search = (query, options = {}) => catch_errors('Search failed', async () => {
|
|
5
|
-
const params = new URLSearchParams(
|
|
5
|
+
const params = new URLSearchParams()
|
|
6
|
+
if (query) params.set('q', query)
|
|
6
7
|
if (options.limit) params.set('limit', options.limit)
|
|
7
8
|
if (options.offset) params.set('offset', options.offset)
|
|
8
|
-
|
|
9
|
+
if (options.scope) params.set('scope', options.scope)
|
|
10
|
+
if (options.workspace) params.set('workspace', options.workspace)
|
|
11
|
+
if (options.tags) params.set('tags', options.tags)
|
|
12
|
+
const needs_auth = (options.scope && options.scope !== 'public') || options.workspace
|
|
13
|
+
const [errors, data] = await client.get(`/repos/search?${params}`, { auth: needs_auth || false })
|
|
9
14
|
if (errors) throw errors[errors.length - 1]
|
|
10
15
|
return data
|
|
11
16
|
})
|
package/src/commands/search.js
CHANGED
|
@@ -1,24 +1,31 @@
|
|
|
1
1
|
const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
|
|
2
2
|
const repos_api = require('../api/repos')
|
|
3
3
|
const { print_help, print_table, print_json, print_info } = require('../ui/output')
|
|
4
|
-
const { exit_with_error, UsageError } = require('../utils/errors')
|
|
4
|
+
const { exit_with_error, UsageError, AuthError } = require('../utils/errors')
|
|
5
5
|
const { EXIT_CODES } = require('../constants')
|
|
6
|
+
const { load_token } = require('../auth/token_store')
|
|
6
7
|
|
|
7
|
-
const HELP_TEXT = `Usage: happyskills search
|
|
8
|
+
const HELP_TEXT = `Usage: happyskills search [query] [options]
|
|
8
9
|
|
|
9
10
|
Search the skill registry.
|
|
10
11
|
|
|
11
12
|
Arguments:
|
|
12
|
-
query
|
|
13
|
+
query Search term (optional with --mine, --personal, or --workspace)
|
|
13
14
|
|
|
14
15
|
Options:
|
|
15
|
-
--
|
|
16
|
+
-w, --workspace <slug> Search within specific workspace(s) (comma-separated)
|
|
17
|
+
--mine Search across all your workspaces
|
|
18
|
+
--personal Search only your personal workspace
|
|
19
|
+
--tags <tags> Filter by tags (comma-separated)
|
|
20
|
+
--json Output as JSON
|
|
16
21
|
|
|
17
22
|
Aliases: s
|
|
18
23
|
|
|
19
24
|
Examples:
|
|
20
25
|
happyskills search deploy
|
|
21
|
-
happyskills
|
|
26
|
+
happyskills search --mine
|
|
27
|
+
happyskills search deploy --workspace acme
|
|
28
|
+
happyskills s --personal --json`
|
|
22
29
|
|
|
23
30
|
const run = (args) => catch_errors('Search failed', async () => {
|
|
24
31
|
if (args.flags._show_help) {
|
|
@@ -26,32 +33,52 @@ const run = (args) => catch_errors('Search failed', async () => {
|
|
|
26
33
|
return process.exit(EXIT_CODES.SUCCESS)
|
|
27
34
|
}
|
|
28
35
|
|
|
29
|
-
const query = args._.join(' ')
|
|
30
|
-
|
|
31
|
-
|
|
36
|
+
const query = args._.join(' ') || null
|
|
37
|
+
const { mine, personal, workspace, tags } = args.flags
|
|
38
|
+
const has_scope_flag = mine || personal || workspace
|
|
39
|
+
|
|
40
|
+
if (!query && !has_scope_flag && !tags) {
|
|
41
|
+
throw new UsageError('Please provide a search query or use --mine, --personal, or --workspace.')
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const scope = mine ? 'mine' : personal ? 'personal' : undefined
|
|
45
|
+
|
|
46
|
+
if (has_scope_flag) {
|
|
47
|
+
const [, token_data] = await load_token()
|
|
48
|
+
if (!token_data) {
|
|
49
|
+
throw new AuthError('Authentication required. Run `happyskills login` first.')
|
|
50
|
+
}
|
|
32
51
|
}
|
|
33
52
|
|
|
34
|
-
const
|
|
53
|
+
const options = {}
|
|
54
|
+
if (scope) options.scope = scope
|
|
55
|
+
if (workspace) options.workspace = workspace
|
|
56
|
+
if (tags) options.tags = tags
|
|
57
|
+
|
|
58
|
+
const [errors, results] = await repos_api.search(query, options)
|
|
35
59
|
if (errors) throw e('Search failed', errors)
|
|
36
60
|
|
|
37
61
|
const items = Array.isArray(results) ? results : (results?.repos || results?.items || [])
|
|
62
|
+
const effective_scope = scope || (has_scope_flag ? undefined : undefined)
|
|
38
63
|
|
|
39
64
|
if (items.length === 0) {
|
|
40
65
|
if (args.flags.json) {
|
|
41
|
-
print_json({ data: { query, results: [], count: 0 } })
|
|
66
|
+
print_json({ data: { query, scope: effective_scope || 'all', results: [], count: 0 } })
|
|
42
67
|
return
|
|
43
68
|
}
|
|
44
|
-
|
|
69
|
+
const context = query ? ` for "${query}"` : ''
|
|
70
|
+
print_info(`No skills found${context}.`)
|
|
45
71
|
return
|
|
46
72
|
}
|
|
47
73
|
|
|
48
74
|
if (args.flags.json) {
|
|
49
|
-
const
|
|
75
|
+
const mapped = items.map(item => ({
|
|
50
76
|
skill: `${item.owner || item.workspace_slug}/${item.name}`,
|
|
51
77
|
description: item.description || '',
|
|
52
|
-
version: item.latest_version || item.version || '-'
|
|
78
|
+
version: item.latest_version || item.version || '-',
|
|
79
|
+
visibility: item.visibility || 'public'
|
|
53
80
|
}))
|
|
54
|
-
print_json({ data: { query, results, count:
|
|
81
|
+
print_json({ data: { query, scope: effective_scope || 'all', results: mapped, count: mapped.length } })
|
|
55
82
|
return
|
|
56
83
|
}
|
|
57
84
|
|
package/src/config/paths.js
CHANGED
|
@@ -34,30 +34,7 @@ const lock_file_path = (project_root = process.cwd()) => path.join(project_root,
|
|
|
34
34
|
|
|
35
35
|
const skill_install_dir = (base_skills_dir, name) => path.join(base_skills_dir, name)
|
|
36
36
|
|
|
37
|
-
const find_project_root = (start_dir = process.cwd()) =>
|
|
38
|
-
let dir = path.resolve(start_dir)
|
|
39
|
-
while (true) {
|
|
40
|
-
// Never treat the home directory as a project root —
|
|
41
|
-
// ~/.claude/skills is the global skills dir, not a project,
|
|
42
|
-
// and ~/skills-lock.json is an artifact of old buggy behaviour.
|
|
43
|
-
if (dir !== home_dir) {
|
|
44
|
-
const skills = path.join(dir, '.claude', 'skills')
|
|
45
|
-
const lock = path.join(dir, 'skills-lock.json')
|
|
46
|
-
try {
|
|
47
|
-
fs.statSync(skills)
|
|
48
|
-
return dir
|
|
49
|
-
} catch {}
|
|
50
|
-
try {
|
|
51
|
-
fs.statSync(lock)
|
|
52
|
-
return dir
|
|
53
|
-
} catch {}
|
|
54
|
-
}
|
|
55
|
-
const parent = path.dirname(dir)
|
|
56
|
-
if (parent === dir) break
|
|
57
|
-
dir = parent
|
|
58
|
-
}
|
|
59
|
-
return start_dir
|
|
60
|
-
}
|
|
37
|
+
const find_project_root = (start_dir = process.cwd()) => path.resolve(start_dir)
|
|
61
38
|
|
|
62
39
|
module.exports = {
|
|
63
40
|
home_dir,
|
package/src/config/paths.test.js
CHANGED
|
@@ -100,77 +100,47 @@ describe('paths', () => {
|
|
|
100
100
|
})
|
|
101
101
|
|
|
102
102
|
describe('find_project_root', () => {
|
|
103
|
-
it('
|
|
103
|
+
it('returns the given directory as-is', () => {
|
|
104
104
|
const tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'hs-test-')))
|
|
105
105
|
try {
|
|
106
|
-
fs.mkdirSync(path.join(tmp, '.claude', 'skills'), { recursive: true })
|
|
107
106
|
assert.strictEqual(paths.find_project_root(tmp), tmp)
|
|
108
107
|
} finally {
|
|
109
108
|
fs.rmSync(tmp, { recursive: true })
|
|
110
109
|
}
|
|
111
110
|
})
|
|
112
111
|
|
|
113
|
-
it('
|
|
114
|
-
const tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'hs-test-')))
|
|
115
|
-
try {
|
|
116
|
-
fs.writeFileSync(path.join(tmp, 'skills-lock.json'), '{}')
|
|
117
|
-
assert.strictEqual(paths.find_project_root(tmp), tmp)
|
|
118
|
-
} finally {
|
|
119
|
-
fs.rmSync(tmp, { recursive: true })
|
|
120
|
-
}
|
|
121
|
-
})
|
|
122
|
-
|
|
123
|
-
it('walks up to find project root from nested dir', () => {
|
|
112
|
+
it('does not walk up to a parent with .claude/skills', () => {
|
|
124
113
|
const tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'hs-test-')))
|
|
125
114
|
try {
|
|
126
115
|
fs.mkdirSync(path.join(tmp, '.claude', 'skills'), { recursive: true })
|
|
127
|
-
const sub = path.join(tmp, '
|
|
116
|
+
const sub = path.join(tmp, 'sub')
|
|
128
117
|
fs.mkdirSync(sub, { recursive: true })
|
|
129
|
-
|
|
118
|
+
// should return sub, not tmp
|
|
119
|
+
assert.strictEqual(paths.find_project_root(sub), sub)
|
|
130
120
|
} finally {
|
|
131
121
|
fs.rmSync(tmp, { recursive: true })
|
|
132
122
|
}
|
|
133
123
|
})
|
|
134
124
|
|
|
135
|
-
it('
|
|
125
|
+
it('does not walk up to a parent with skills-lock.json', () => {
|
|
136
126
|
const tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'hs-test-')))
|
|
137
127
|
try {
|
|
128
|
+
fs.writeFileSync(path.join(tmp, 'skills-lock.json'), '{}')
|
|
138
129
|
const sub = path.join(tmp, 'sub')
|
|
139
130
|
fs.mkdirSync(sub, { recursive: true })
|
|
131
|
+
// should return sub, not tmp
|
|
140
132
|
assert.strictEqual(paths.find_project_root(sub), sub)
|
|
141
133
|
} finally {
|
|
142
134
|
fs.rmSync(tmp, { recursive: true })
|
|
143
135
|
}
|
|
144
136
|
})
|
|
145
137
|
|
|
146
|
-
it('
|
|
147
|
-
|
|
148
|
-
let global_exists = false
|
|
149
|
-
try { fs.statSync(global_skills); global_exists = true } catch {}
|
|
150
|
-
if (!global_exists) return // skip if global dir not present on this machine
|
|
151
|
-
|
|
152
|
-
const tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'hs-test-')))
|
|
153
|
-
try {
|
|
154
|
-
// tmp has no project markers — should fall back to tmp, not home dir
|
|
155
|
-
assert.strictEqual(paths.find_project_root(tmp), tmp)
|
|
156
|
-
} finally {
|
|
157
|
-
fs.rmSync(tmp, { recursive: true })
|
|
158
|
-
}
|
|
138
|
+
it('returns home dir only when explicitly called with it', () => {
|
|
139
|
+
assert.strictEqual(paths.find_project_root(os.homedir()), os.homedir())
|
|
159
140
|
})
|
|
160
141
|
|
|
161
|
-
it('
|
|
162
|
-
|
|
163
|
-
// even if a stale skills-lock.json exists there from old buggy behaviour.
|
|
164
|
-
// We verify this by running from a temp dir that has no project markers
|
|
165
|
-
// of its own — the result must be the temp dir, not the home dir.
|
|
166
|
-
const tmp = fs.realpathSync(fs.mkdtempSync(path.join(os.tmpdir(), 'hs-test-')))
|
|
167
|
-
try {
|
|
168
|
-
const result = paths.find_project_root(tmp)
|
|
169
|
-
assert.notStrictEqual(result, os.homedir())
|
|
170
|
-
assert.strictEqual(result, tmp)
|
|
171
|
-
} finally {
|
|
172
|
-
fs.rmSync(tmp, { recursive: true })
|
|
173
|
-
}
|
|
142
|
+
it('defaults to cwd when no argument given', () => {
|
|
143
|
+
assert.strictEqual(paths.find_project_root(), path.resolve(process.cwd()))
|
|
174
144
|
})
|
|
175
145
|
})
|
|
176
146
|
})
|