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.
- package/CHANGELOG.md +21 -0
- package/LICENSE +116 -0
- package/README.md +76 -0
- package/bin/happyskills.js +5 -0
- package/package.json +37 -0
- package/src/api/auth.js +40 -0
- package/src/api/client.js +65 -0
- package/src/api/repos.js +44 -0
- package/src/api/workspaces.js +10 -0
- package/src/auth/token_store.js +69 -0
- package/src/commands/check.js +108 -0
- package/src/commands/fork.js +101 -0
- package/src/commands/init.js +74 -0
- package/src/commands/install.js +68 -0
- package/src/commands/list.js +61 -0
- package/src/commands/login.js +97 -0
- package/src/commands/logout.js +26 -0
- package/src/commands/publish.js +152 -0
- package/src/commands/search.js +58 -0
- package/src/commands/uninstall.js +48 -0
- package/src/commands/update.js +73 -0
- package/src/commands/whoami.js +66 -0
- package/src/config/index.js +16 -0
- package/src/config/paths.js +41 -0
- package/src/config/paths.test.js +100 -0
- package/src/constants.js +59 -0
- package/src/engine/downloader.js +10 -0
- package/src/engine/extractor.js +35 -0
- package/src/engine/extractor.test.js +37 -0
- package/src/engine/installer.js +150 -0
- package/src/engine/resolver.js +36 -0
- package/src/engine/system_deps.js +58 -0
- package/src/engine/uninstaller.js +73 -0
- package/src/engine/uninstaller.test.js +98 -0
- package/src/index.js +118 -0
- package/src/index.test.js +63 -0
- package/src/lock/integrity.js +54 -0
- package/src/lock/reader.js +29 -0
- package/src/lock/writer.js +31 -0
- package/src/lock/writer.test.js +74 -0
- package/src/manifest/reader.js +13 -0
- package/src/manifest/validator.js +44 -0
- package/src/manifest/validator.test.js +101 -0
- package/src/manifest/writer.js +12 -0
- package/src/ui/colors.js +17 -0
- package/src/ui/output.js +48 -0
- package/src/ui/spinner.js +59 -0
- package/src/utils/errors.js +69 -0
- package/src/utils/errors.test.js +96 -0
- package/src/utils/fs.js +49 -0
- package/src/utils/semver.js +27 -0
- 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)
|
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
|
+
}
|
package/src/api/auth.js
ADDED
|
@@ -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 }
|
package/src/api/repos.js
ADDED
|
@@ -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 }
|