happyskills 0.1.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.
Files changed (52) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/LICENSE +116 -0
  3. package/README.md +76 -0
  4. package/bin/happyskills.js +5 -0
  5. package/package.json +37 -0
  6. package/src/api/auth.js +40 -0
  7. package/src/api/client.js +65 -0
  8. package/src/api/repos.js +44 -0
  9. package/src/api/workspaces.js +10 -0
  10. package/src/auth/token_store.js +69 -0
  11. package/src/commands/check.js +108 -0
  12. package/src/commands/fork.js +101 -0
  13. package/src/commands/init.js +74 -0
  14. package/src/commands/install.js +68 -0
  15. package/src/commands/list.js +61 -0
  16. package/src/commands/login.js +97 -0
  17. package/src/commands/logout.js +26 -0
  18. package/src/commands/publish.js +152 -0
  19. package/src/commands/search.js +58 -0
  20. package/src/commands/uninstall.js +48 -0
  21. package/src/commands/update.js +73 -0
  22. package/src/commands/whoami.js +66 -0
  23. package/src/config/index.js +16 -0
  24. package/src/config/paths.js +41 -0
  25. package/src/config/paths.test.js +100 -0
  26. package/src/constants.js +59 -0
  27. package/src/engine/downloader.js +10 -0
  28. package/src/engine/extractor.js +35 -0
  29. package/src/engine/extractor.test.js +37 -0
  30. package/src/engine/installer.js +150 -0
  31. package/src/engine/resolver.js +36 -0
  32. package/src/engine/system_deps.js +58 -0
  33. package/src/engine/uninstaller.js +73 -0
  34. package/src/engine/uninstaller.test.js +98 -0
  35. package/src/index.js +118 -0
  36. package/src/index.test.js +63 -0
  37. package/src/lock/integrity.js +54 -0
  38. package/src/lock/reader.js +29 -0
  39. package/src/lock/writer.js +31 -0
  40. package/src/lock/writer.test.js +74 -0
  41. package/src/manifest/reader.js +13 -0
  42. package/src/manifest/validator.js +44 -0
  43. package/src/manifest/validator.test.js +101 -0
  44. package/src/manifest/writer.js +12 -0
  45. package/src/ui/colors.js +17 -0
  46. package/src/ui/output.js +48 -0
  47. package/src/ui/spinner.js +59 -0
  48. package/src/utils/errors.js +69 -0
  49. package/src/utils/errors.test.js +96 -0
  50. package/src/utils/fs.js +49 -0
  51. package/src/utils/semver.js +27 -0
  52. package/src/utils/semver.test.js +101 -0
package/CHANGELOG.md ADDED
@@ -0,0 +1,21 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-02-28
11
+
12
+ ### Added
13
+ - Initial release of the `happyskills` CLI
14
+ - `install` command for downloading and resolving skill dependencies
15
+ - `search` command for discovering skills from the registry
16
+ - `info` command for viewing skill metadata
17
+ - `init` command for scaffolding new skill projects
18
+ - `publish` command for publishing skills to the registry
19
+ - `login` / `logout` commands for authentication
20
+ - Semantic version resolution with conflict detection
21
+ - Lockfile support (`happyskills-lock.json`)
package/LICENSE ADDED
@@ -0,0 +1,116 @@
1
+ HAPPYSKILLS PROPRIETARY LICENSE
2
+
3
+ Copyright (c) 2026 Cloudless Consulting Pty Ltd (ABN 29 636 444 463)
4
+ All rights reserved.
5
+
6
+ https://cloudlesslabs.com
7
+
8
+ TERMS AND CONDITIONS
9
+
10
+ 1. DEFINITIONS
11
+
12
+ "Software" refers to the HappySkills CLI tool, including all source code,
13
+ object code, documentation, and associated files distributed as the
14
+ "happyskills" package.
15
+
16
+ "Licensor" refers to Cloudless Consulting Pty Ltd.
17
+
18
+ "You" refers to any individual or entity that accesses, installs, or uses
19
+ the Software.
20
+
21
+ 2. GRANT OF LICENSE
22
+
23
+ Subject to the terms of this license, the Licensor grants You a limited,
24
+ non-exclusive, non-transferable, non-sublicensable, revocable license to:
25
+
26
+ (a) Install and use the Software solely for the purpose of interacting
27
+ with the HappySkills registry and platform services; and
28
+
29
+ (b) Use the Software in accordance with its intended purpose as a
30
+ command-line package manager for AI agent skills.
31
+
32
+ 3. RESTRICTIONS
33
+
34
+ You may NOT, without the prior written consent of the Licensor:
35
+
36
+ (a) Copy, modify, adapt, translate, or create derivative works based on
37
+ the Software or any part thereof;
38
+
39
+ (b) Reverse engineer, decompile, disassemble, or otherwise attempt to
40
+ derive the source code, algorithms, or data structures of the
41
+ Software, except to the extent that such activity is expressly
42
+ permitted by applicable law notwithstanding this limitation;
43
+
44
+ (c) Distribute, sublicense, lease, rent, sell, or otherwise transfer the
45
+ Software or any rights therein to any third party;
46
+
47
+ (d) Remove, alter, or obscure any proprietary notices, labels, or marks
48
+ on the Software;
49
+
50
+ (e) Use the Software to build a competing product or service;
51
+
52
+ (f) Use the Software in any manner that infringes the intellectual
53
+ property rights of the Licensor or any third party; or
54
+
55
+ (g) Use any portion of the Software's source code in any other software
56
+ or project, whether open source or proprietary.
57
+
58
+ 4. INTELLECTUAL PROPERTY
59
+
60
+ The Software and all copies thereof are proprietary to and the exclusive
61
+ property of the Licensor. The Software is protected by copyright laws,
62
+ international treaty provisions, and other intellectual property laws.
63
+ The Licensor retains all right, title, and interest in and to the
64
+ Software, including all intellectual property rights therein.
65
+
66
+ No title to or ownership of the Software or any intellectual property
67
+ rights therein is transferred to You by this license.
68
+
69
+ 5. NO WARRANTY
70
+
71
+ THE SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
72
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
73
+ FITNESS FOR A PARTICULAR PURPOSE, AND NONINFRINGEMENT. IN NO EVENT SHALL
74
+ THE LICENSOR BE LIABLE FOR ANY CLAIM, DAMAGES, OR OTHER LIABILITY,
75
+ WHETHER IN AN ACTION OF CONTRACT, TORT, OR OTHERWISE, ARISING FROM, OUT
76
+ OF, OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
77
+ THE SOFTWARE.
78
+
79
+ 6. LIMITATION OF LIABILITY
80
+
81
+ TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW, IN NO EVENT SHALL THE
82
+ LICENSOR BE LIABLE FOR ANY INDIRECT, INCIDENTAL, SPECIAL, CONSEQUENTIAL,
83
+ OR PUNITIVE DAMAGES, OR ANY LOSS OF PROFITS, REVENUE, DATA, OR USE,
84
+ ARISING OUT OF OR RELATED TO THE SOFTWARE, EVEN IF THE LICENSOR HAS BEEN
85
+ ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
86
+
87
+ 7. TERMINATION
88
+
89
+ This license is effective until terminated. It will terminate automatically
90
+ without notice if You fail to comply with any term of this license. Upon
91
+ termination, You must destroy all copies of the Software in Your
92
+ possession or control.
93
+
94
+ The Licensor may also terminate this license at any time for any reason
95
+ upon notice to You.
96
+
97
+ 8. GOVERNING LAW
98
+
99
+ This license shall be governed by and construed in accordance with the
100
+ laws of New South Wales, Australia, without regard to its conflict of law
101
+ provisions. Any disputes arising under this license shall be subject to
102
+ the exclusive jurisdiction of the courts of New South Wales, Australia.
103
+
104
+ 9. ENTIRE AGREEMENT
105
+
106
+ This license constitutes the entire agreement between You and the Licensor
107
+ regarding the Software and supersedes all prior agreements and
108
+ understandings, whether written or oral.
109
+
110
+ 10. CONTACT
111
+
112
+ For licensing inquiries, permissions, or questions:
113
+
114
+ Cloudless Consulting Pty Ltd
115
+ ABN 29 636 444 463
116
+ https://cloudlesslabs.com
package/README.md ADDED
@@ -0,0 +1,76 @@
1
+ # HappySkills CLI
2
+
3
+ Package manager for AI agent skills. Install, manage, version, and publish skills from your terminal — think npm for AI skills.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install -g happyskills
9
+ ```
10
+
11
+ Requires Node.js 22 or later.
12
+
13
+ ## Quick Start
14
+
15
+ ```bash
16
+ # Search the registry
17
+ happyskills search deploy
18
+
19
+ # Install a skill (with transitive dependencies)
20
+ happyskills install acme/deploy-aws
21
+
22
+ # Scaffold a new skill
23
+ happyskills init my-skill
24
+
25
+ # Publish to the registry
26
+ happyskills login
27
+ happyskills publish
28
+ ```
29
+
30
+ ## Commands
31
+
32
+ | Command | Description |
33
+ |---------|-------------|
34
+ | `init [name]` | Scaffold `SKILL.md` + `skill.json` in the current directory |
35
+ | `install [owner/skill]` | Install a skill and its dependencies. Without arguments, installs from `skill.json` |
36
+ | `uninstall <owner/skill>` | Remove a skill and prune orphaned dependencies |
37
+ | `list` | List installed skills from the lock file |
38
+ | `search <query>` | Search the skill registry |
39
+ | `check [owner/skill]` | Check for available updates |
40
+ | `update [owner/skill]` | Upgrade to latest compatible versions |
41
+ | `publish` | Push the current skill to the registry |
42
+ | `fork <owner/skill>` | Fork a skill to your workspace |
43
+ | `login` | Authenticate with the registry |
44
+ | `logout` | Clear stored credentials |
45
+ | `whoami` | Show the currently authenticated user |
46
+
47
+ ## Command Aliases
48
+
49
+ | Alias | Command |
50
+ |-------|---------|
51
+ | `i`, `add` | `install` |
52
+ | `rm`, `remove` | `uninstall` |
53
+ | `ls` | `list` |
54
+ | `s` | `search` |
55
+ | `up` | `update` |
56
+ | `pub` | `publish` |
57
+
58
+ ## Global Flags
59
+
60
+ | Flag | Description |
61
+ |------|-------------|
62
+ | `--help` | Show help for a command |
63
+ | `--version` | Show CLI version |
64
+ | `-g`, `--global` | Use global scope (`~/.claude/skills/`) |
65
+ | `-y`, `--yes` | Skip confirmation prompts |
66
+ | `--json` | Output as JSON (supported by `list`, `search`, `check`, `whoami`) |
67
+
68
+ ## License
69
+
70
+ See [LICENSE](./LICENSE) for details.
71
+
72
+ ## Links
73
+
74
+ - [Full documentation](https://github.com/cloudlesslabs/skillsbang/tree/master/cli)
75
+ - [Report an issue](https://github.com/cloudlesslabs/skillsbang/issues)
76
+ - [HappySkills website](https://happyskills.ai)
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env node
2
+
3
+ try { require('dotenv').config({ path: require('path').join(__dirname, '..', '.env') }) } catch {}
4
+ const { run } = require('../src/index')
5
+ run(process.argv.slice(2))
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "happyskills",
3
+ "version": "0.1.0",
4
+ "description": "Package manager for AI agent skills",
5
+ "license": "SEE LICENSE IN LICENSE",
6
+ "author": "Nicolas Dao <nic@cloudlesslabs.com> (https://cloudlesslabs.com)",
7
+ "bin": {
8
+ "happyskills": "./bin/happyskills.js"
9
+ },
10
+ "main": "src/index.js",
11
+ "repository": {
12
+ "type": "git",
13
+ "url": "https://github.com/cloudlesslabs/skillsbang.git",
14
+ "directory": "cli"
15
+ },
16
+ "homepage": "https://happyskills.ai",
17
+ "bugs": {
18
+ "url": "https://github.com/cloudlesslabs/skillsbang/issues"
19
+ },
20
+ "keywords": ["cli", "skills", "package-manager", "ai", "claude", "agent", "dependency-resolution"],
21
+ "files": ["bin/", "src/", "LICENSE", "CHANGELOG.md"],
22
+ "scripts": {
23
+ "dev": "node bin/happyskills.js",
24
+ "test": "node --test src/**/*.test.js",
25
+ "release": "./scripts/release.sh"
26
+ },
27
+ "engines": {
28
+ "node": ">=22.0.0"
29
+ },
30
+ "dependencies": {
31
+ "puffy-core": "^1.3.1",
32
+ "semver": "^7.6.0"
33
+ },
34
+ "devDependencies": {
35
+ "dotenv": "^17.2.4"
36
+ }
37
+ }
@@ -0,0 +1,40 @@
1
+ const { error: { catch_errors } } = require('puffy-core')
2
+ const client = require('./client')
3
+
4
+ const login = (email, password) => catch_errors('Login failed', async () => {
5
+ const [errors, data] = await client.post('/auth/login', { email, password }, { auth: false })
6
+ if (errors) throw errors[0]
7
+ return data
8
+ })
9
+
10
+ const signup = (email, password) => catch_errors('Signup failed', async () => {
11
+ const [errors, data] = await client.post('/auth/signup', { email, password }, { auth: false })
12
+ if (errors) throw errors[0]
13
+ return data
14
+ })
15
+
16
+ const confirm = (email, code) => catch_errors('Confirmation failed', async () => {
17
+ const [errors, data] = await client.post('/auth/confirm', { email, code }, { auth: false })
18
+ if (errors) throw errors[0]
19
+ return data
20
+ })
21
+
22
+ const resend_code = (email) => catch_errors('Resend code failed', async () => {
23
+ const [errors, data] = await client.post('/auth/resend-code', { email }, { auth: false })
24
+ if (errors) throw errors[0]
25
+ return data
26
+ })
27
+
28
+ const device_start = () => catch_errors('Device auth start failed', async () => {
29
+ const [errors, data] = await client.post('/auth/device/start', {}, { auth: false })
30
+ if (errors) throw errors[0]
31
+ return data
32
+ })
33
+
34
+ const device_token = (device_code) => catch_errors('Device auth token failed', async () => {
35
+ const [errors, data] = await client.post('/auth/device/token', { device_code }, { auth: false })
36
+ if (errors) throw errors[0]
37
+ return data
38
+ })
39
+
40
+ module.exports = { login, signup, confirm, resend_code, device_start, device_token }
@@ -0,0 +1,65 @@
1
+ const { createHash } = require('crypto')
2
+ const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
3
+ const { API_URL } = require('../constants')
4
+ const { ApiError, NetworkError, AuthError } = require('../utils/errors')
5
+ const { load_token } = require('../auth/token_store')
6
+
7
+ const get_base_url = () => process.env.HAPPYSKILLS_API_URL || API_URL
8
+
9
+ const request = (method, path, options = {}) => catch_errors(`API ${method} ${path} failed`, async () => {
10
+ const { body, auth = true, raw_response = false, headers: extra_headers = {} } = options
11
+ const url = `${get_base_url()}${path}`
12
+ const headers = { ...extra_headers }
13
+
14
+ if (auth) {
15
+ const [, token_data] = await load_token()
16
+ if (token_data) {
17
+ headers['Authorization'] = `Bearer ${token_data.id_token}`
18
+ }
19
+ }
20
+
21
+ const body_str = body ? (typeof body === 'string' ? body : JSON.stringify(body)) : undefined
22
+
23
+ if (body && !raw_response) {
24
+ headers['Content-Type'] = 'application/json'
25
+ }
26
+
27
+ if (body_str) {
28
+ headers['x-amz-content-sha256'] = createHash('sha256').update(body_str).digest('hex')
29
+ }
30
+
31
+ let res
32
+ try {
33
+ res = await fetch(url, {
34
+ method,
35
+ headers,
36
+ body: body_str
37
+ })
38
+ } catch (err) {
39
+ throw new NetworkError(`Failed to connect to API: ${err.message}`)
40
+ }
41
+
42
+ if (raw_response) return res
43
+
44
+ const data = await res.json().catch(() => null)
45
+
46
+ if (!res.ok) {
47
+ const err_code = data?.error?.code || 'UNKNOWN'
48
+ const err_msg = data?.error?.message || `Request failed with status ${res.status}`
49
+
50
+ if (res.status === 401) {
51
+ throw new AuthError(err_msg)
52
+ }
53
+
54
+ throw new ApiError(err_msg, res.status, err_code)
55
+ }
56
+
57
+ return data?.data !== undefined ? data.data : data
58
+ })
59
+
60
+ const get = (path, options) => request('GET', path, options)
61
+ const post = (path, body, options) => request('POST', path, { ...options, body })
62
+ const put = (path, body, options) => request('PUT', path, { ...options, body })
63
+ const del = (path, options) => request('DELETE', path, options)
64
+
65
+ module.exports = { request, get, post, put, del, get_base_url }
@@ -0,0 +1,44 @@
1
+ const { error: { catch_errors } } = require('puffy-core')
2
+ const client = require('./client')
3
+
4
+ const search = (query, options = {}) => catch_errors('Search failed', async () => {
5
+ const params = new URLSearchParams({ q: query })
6
+ if (options.limit) params.set('limit', options.limit)
7
+ if (options.offset) params.set('offset', options.offset)
8
+ const [errors, data] = await client.get(`/repos/search?${params}`, { auth: false })
9
+ if (errors) throw errors[0]
10
+ return data
11
+ })
12
+
13
+ const resolve_dependencies = (skill, version, installed = {}) => catch_errors('Dependency resolution failed', async () => {
14
+ const [errors, data] = await client.post('/repos:resolve-dependencies', { skill, version, installed })
15
+ if (errors) throw errors[0]
16
+ return data
17
+ })
18
+
19
+ const clone = (owner, repo, ref) => catch_errors(`Clone ${owner}/${repo} failed`, async () => {
20
+ const params = ref ? `?ref=${encodeURIComponent(ref)}` : ''
21
+ const [errors, data] = await client.get(`/repos/${owner}/${repo}/clone${params}`)
22
+ if (errors) throw errors[0]
23
+ return data
24
+ })
25
+
26
+ const push = (owner, repo, body) => catch_errors(`Push to ${owner}/${repo} failed`, async () => {
27
+ const [errors, data] = await client.post(`/repos/${owner}/${repo}/push`, body)
28
+ if (errors) throw errors[0]
29
+ return data
30
+ })
31
+
32
+ const get_refs = (owner, repo) => catch_errors(`Get refs for ${owner}/${repo} failed`, async () => {
33
+ const [errors, data] = await client.get(`/repos/${owner}/${repo}/refs`)
34
+ if (errors) throw errors[0]
35
+ return data
36
+ })
37
+
38
+ const get_repo = (owner, repo) => catch_errors(`Get repo ${owner}/${repo} failed`, async () => {
39
+ const [errors, data] = await client.get(`/repos/${owner}/${repo}`, { auth: false })
40
+ if (errors) throw errors[0]
41
+ return data
42
+ })
43
+
44
+ module.exports = { search, resolve_dependencies, clone, push, get_refs, get_repo }
@@ -0,0 +1,10 @@
1
+ const { error: { catch_errors } } = require('puffy-core')
2
+ const client = require('./client')
3
+
4
+ const list_workspaces = () => catch_errors('List workspaces failed', async () => {
5
+ const [errors, data] = await client.get('/workspaces')
6
+ if (errors) throw errors[0]
7
+ return data
8
+ })
9
+
10
+ module.exports = { list_workspaces }
@@ -0,0 +1,69 @@
1
+ const fs = require('fs')
2
+ const path = require('path')
3
+ const { error: { catch_errors } } = require('puffy-core')
4
+ const { credentials_path, config_dir } = require('../config/paths')
5
+
6
+ const save_token = (token_data) => catch_errors('Failed to save token', async () => {
7
+ const creds_path = credentials_path()
8
+ const dir = config_dir()
9
+ await fs.promises.mkdir(dir, { recursive: true })
10
+ const data = {
11
+ ...token_data,
12
+ stored_at: new Date().toISOString()
13
+ }
14
+ await fs.promises.writeFile(creds_path, JSON.stringify(data, null, '\t') + '\n', {
15
+ encoding: 'utf-8',
16
+ mode: 0o600
17
+ })
18
+ })
19
+
20
+ const load_token = () => catch_errors('Failed to load token', async () => {
21
+ const creds_path = credentials_path()
22
+ try {
23
+ await fs.promises.access(creds_path)
24
+ } catch {
25
+ return null
26
+ }
27
+
28
+ const stat = await fs.promises.stat(creds_path)
29
+ const mode = stat.mode & 0o777
30
+ if (mode !== 0o600) {
31
+ const { print_warn } = require('../ui/output')
32
+ print_warn(`Credentials file has permissive permissions (${mode.toString(8)}). Expected 600.`)
33
+ }
34
+
35
+ const content = await fs.promises.readFile(creds_path, 'utf-8')
36
+ const data = JSON.parse(content)
37
+
38
+ if (data.stored_at && data.expires_in) {
39
+ const stored = new Date(data.stored_at).getTime()
40
+ const now = Date.now()
41
+ const elapsed_sec = (now - stored) / 1000
42
+ if (elapsed_sec >= data.expires_in) {
43
+ await clear_token()
44
+ return null
45
+ }
46
+ }
47
+
48
+ return data
49
+ })
50
+
51
+ const clear_token = () => catch_errors('Failed to clear token', async () => {
52
+ const creds_path = credentials_path()
53
+ try {
54
+ await fs.promises.unlink(creds_path)
55
+ } catch {
56
+ // File may not exist
57
+ }
58
+ })
59
+
60
+ const require_token = async () => {
61
+ const [, data] = await load_token()
62
+ if (!data) {
63
+ const { AuthError } = require('../utils/errors')
64
+ throw new AuthError()
65
+ }
66
+ return data
67
+ }
68
+
69
+ module.exports = { save_token, load_token, clear_token, require_token }
@@ -0,0 +1,108 @@
1
+ const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
2
+ const { read_lock, get_all_locked_skills } = require('../lock/reader')
3
+ const repos_api = require('../api/repos')
4
+ const { satisfies, gt } = require('../utils/semver')
5
+ const { print_help, print_table, print_json, print_info, print_success } = require('../ui/output')
6
+ const { green, yellow, red } = require('../ui/colors')
7
+ const { exit_with_error } = require('../utils/errors')
8
+ const { EXIT_CODES } = require('../constants')
9
+
10
+ const HELP_TEXT = `Usage: happyskills check [owner/skill] [options]
11
+
12
+ Check for available updates.
13
+
14
+ Arguments:
15
+ owner/skill Check specific skill (optional, checks all if omitted)
16
+
17
+ Options:
18
+ --json Output as JSON
19
+
20
+ Examples:
21
+ happyskills check
22
+ happyskills check acme/deploy-aws
23
+ happyskills check --json`
24
+
25
+ const run = (args) => catch_errors('Check failed', async () => {
26
+ if (args.flags._show_help) {
27
+ print_help(HELP_TEXT)
28
+ return process.exit(EXIT_CODES.SUCCESS)
29
+ }
30
+
31
+ const project_root = process.cwd()
32
+ const [, lock_data] = await read_lock(project_root)
33
+ const skills = get_all_locked_skills(lock_data)
34
+ const entries = Object.entries(skills)
35
+
36
+ if (entries.length === 0) {
37
+ print_info('No skills installed.')
38
+ return
39
+ }
40
+
41
+ const target_skill = args._[0]
42
+ const to_check = target_skill
43
+ ? entries.filter(([name]) => name === target_skill)
44
+ : entries.filter(([, data]) => data.requested_by?.includes('__root__'))
45
+
46
+ if (to_check.length === 0) {
47
+ print_info(target_skill ? `${target_skill} is not installed.` : 'No directly installed skills found.')
48
+ return
49
+ }
50
+
51
+ const results = []
52
+ for (const [name, data] of to_check) {
53
+ const [owner, repo] = name.split('/')
54
+ const [errors, refs] = await repos_api.get_refs(owner, repo)
55
+ if (errors) {
56
+ results.push({ skill: name, installed: data.version, latest: 'error', status: 'error' })
57
+ continue
58
+ }
59
+
60
+ const versions = (refs || [])
61
+ .map(r => r.name?.replace('refs/tags/v', ''))
62
+ .filter(Boolean)
63
+
64
+ const latest = versions.sort((a, b) => gt(a, b) ? -1 : 1)[0]
65
+
66
+ if (!latest) {
67
+ results.push({ skill: name, installed: data.version, latest: '-', status: 'unknown' })
68
+ } else if (latest === data.version) {
69
+ results.push({ skill: name, installed: data.version, latest, status: 'up-to-date' })
70
+ } else if (gt(latest, data.version)) {
71
+ results.push({ skill: name, installed: data.version, latest, status: 'outdated' })
72
+ } else {
73
+ results.push({ skill: name, installed: data.version, latest, status: 'up-to-date' })
74
+ }
75
+ }
76
+
77
+ if (args.flags.json) {
78
+ print_json(results)
79
+ return
80
+ }
81
+
82
+ const status_colors = {
83
+ 'up-to-date': green,
84
+ 'outdated': yellow,
85
+ 'error': red,
86
+ 'unknown': (s) => s
87
+ }
88
+
89
+ const rows = results.map(r => [
90
+ r.skill,
91
+ r.installed,
92
+ r.latest,
93
+ (status_colors[r.status] || ((s) => s))(r.status)
94
+ ])
95
+
96
+ print_table(['Skill', 'Installed', 'Latest', 'Status'], rows)
97
+
98
+ const outdated = results.filter(r => r.status === 'outdated')
99
+ if (outdated.length > 0) {
100
+ console.log()
101
+ print_info(`Run 'happyskills update' to upgrade ${outdated.length} skill(s).`)
102
+ } else if (results.every(r => r.status === 'up-to-date')) {
103
+ console.log()
104
+ print_success('All skills are up to date.')
105
+ }
106
+ }).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
107
+
108
+ module.exports = { run }