happyskills 0.9.1 → 0.10.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 +9 -0
- package/package.json +1 -1
- package/src/api/repos.js +7 -1
- package/src/commands/delete.js +78 -0
- package/src/commands/init.js +25 -13
- package/src/constants.js +3 -1
- package/src/index.js +1 -0
- package/src/integration/cli.test.js +85 -3
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.10.0] - 2026-03-11
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- Add `delete` command to permanently remove a skill from the registry — requires authentication, interactive confirmation (type full `owner/name`), and `-y`/`--yes` in JSON mode; alias `del`
|
|
14
|
+
- Add `-g`/`--global` flag to `init` command for scaffolding skills in the global `~/.claude/skills/` directory
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
- Change `init` to create skills in `.claude/skills/<name>/` instead of the current directory — name argument is now required; `.claude/skills/` is auto-created if missing
|
|
18
|
+
|
|
10
19
|
## [0.9.1] - 2026-03-10
|
|
11
20
|
|
|
12
21
|
### Changed
|
package/package.json
CHANGED
package/src/api/repos.js
CHANGED
|
@@ -52,4 +52,10 @@ const check_updates = (skills) => catch_errors('Check updates failed', async ()
|
|
|
52
52
|
return data
|
|
53
53
|
})
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
const del_repo = (owner, name) => catch_errors(`Failed to delete ${owner}/${name}`, async () => {
|
|
56
|
+
const [errors, data] = await client.del(`/repos/${encodeURIComponent(owner)}/${encodeURIComponent(name)}`)
|
|
57
|
+
if (errors) throw errors[errors.length - 1]
|
|
58
|
+
return data
|
|
59
|
+
})
|
|
60
|
+
|
|
61
|
+
module.exports = { search, resolve_dependencies, clone, push, get_refs, get_repo, check_updates, del_repo }
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
const readline = require('readline')
|
|
2
|
+
const { error: { catch_errors } } = require('puffy-core')
|
|
3
|
+
const { del_repo } = require('../api/repos')
|
|
4
|
+
const { require_token } = require('../auth/token_store')
|
|
5
|
+
const { print_help, print_json, print_success, print_warn } = require('../ui/output')
|
|
6
|
+
const { exit_with_error, UsageError, CliError } = require('../utils/errors')
|
|
7
|
+
const { EXIT_CODES } = require('../constants')
|
|
8
|
+
|
|
9
|
+
const HELP_TEXT = `Usage: happyskills delete <owner/name> [options]
|
|
10
|
+
|
|
11
|
+
Permanently delete a skill from the registry.
|
|
12
|
+
|
|
13
|
+
Arguments:
|
|
14
|
+
owner/name Skill to delete (e.g., acme/deploy-aws)
|
|
15
|
+
|
|
16
|
+
Options:
|
|
17
|
+
-y, --yes Skip confirmation prompt (required with --json)
|
|
18
|
+
--json Output as JSON
|
|
19
|
+
|
|
20
|
+
Aliases: del
|
|
21
|
+
|
|
22
|
+
Examples:
|
|
23
|
+
happyskills delete acme/deploy-aws
|
|
24
|
+
happyskills delete acme/deploy-aws --json -y`
|
|
25
|
+
|
|
26
|
+
const _ask_confirmation = (skill) => new Promise((resolve) => {
|
|
27
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr })
|
|
28
|
+
print_warn(`You are about to permanently delete "${skill}" from the registry.`)
|
|
29
|
+
print_warn('This action cannot be undone.')
|
|
30
|
+
rl.question(`\nType "${skill}" to confirm: `, (answer) => {
|
|
31
|
+
rl.close()
|
|
32
|
+
resolve(answer.trim() === skill)
|
|
33
|
+
})
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const run = (args) => catch_errors('Delete failed', async () => {
|
|
37
|
+
if (args.flags._show_help) {
|
|
38
|
+
print_help(HELP_TEXT)
|
|
39
|
+
return process.exit(EXIT_CODES.SUCCESS)
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const skill = args._[0]
|
|
43
|
+
if (!skill) {
|
|
44
|
+
throw new UsageError('Please specify a skill to delete (e.g., happyskills delete acme/deploy-aws).')
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (!skill.includes('/')) {
|
|
48
|
+
throw new UsageError('Skill must be in owner/name format (e.g., acme/deploy-aws).')
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
await require_token()
|
|
52
|
+
|
|
53
|
+
const [owner, name] = skill.split('/')
|
|
54
|
+
|
|
55
|
+
if (args.flags.json) {
|
|
56
|
+
if (!args.flags.yes) {
|
|
57
|
+
throw new CliError('Confirmation required. Use -y or --yes to confirm deletion in JSON mode.', EXIT_CODES.ERROR)
|
|
58
|
+
}
|
|
59
|
+
} else {
|
|
60
|
+
const confirmed = await _ask_confirmation(skill)
|
|
61
|
+
if (!confirmed) {
|
|
62
|
+
print_warn('Delete cancelled.')
|
|
63
|
+
return process.exit(EXIT_CODES.SUCCESS)
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const [errors, result] = await del_repo(owner, name)
|
|
68
|
+
if (errors) throw errors[errors.length - 1]
|
|
69
|
+
|
|
70
|
+
if (args.flags.json) {
|
|
71
|
+
print_json({ data: { deleted: true, skill, ...(result || {}) } })
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
print_success(`Deleted "${skill}" from the registry.`)
|
|
76
|
+
}).then(([errors]) => { if (errors) { exit_with_error(errors); return } })
|
|
77
|
+
|
|
78
|
+
module.exports = { run }
|
package/src/commands/init.js
CHANGED
|
@@ -1,28 +1,30 @@
|
|
|
1
1
|
const path = require('path')
|
|
2
2
|
const { error: { catch_errors, wrap_errors: e } } = require('puffy-core')
|
|
3
3
|
const { write_manifest } = require('../manifest/writer')
|
|
4
|
-
const { write_file, file_exists } = require('../utils/fs')
|
|
4
|
+
const { write_file, file_exists, ensure_dir } = require('../utils/fs')
|
|
5
5
|
const { print_success, print_error, print_help, print_hint, print_json, code } = require('../ui/output')
|
|
6
|
-
const { exit_with_error, CliError } = require('../utils/errors')
|
|
6
|
+
const { exit_with_error, CliError, UsageError } = require('../utils/errors')
|
|
7
7
|
const { SKILL_JSON, SKILL_MD, EXIT_CODES } = require('../constants')
|
|
8
|
+
const { find_project_root, skills_dir } = require('../config/paths')
|
|
8
9
|
|
|
9
|
-
const HELP_TEXT = `Usage: happyskills init [
|
|
10
|
+
const HELP_TEXT = `Usage: happyskills init <name> [options]
|
|
10
11
|
|
|
11
|
-
Scaffold a new skill in
|
|
12
|
+
Scaffold a new skill in .claude/skills/<name>/.
|
|
12
13
|
|
|
13
14
|
Creates:
|
|
14
|
-
skill.json Skill manifest (name, version, deps)
|
|
15
|
-
SKILL.md Skill instructions for AI agents
|
|
15
|
+
.claude/skills/<name>/skill.json Skill manifest (name, version, deps)
|
|
16
|
+
.claude/skills/<name>/SKILL.md Skill instructions for AI agents
|
|
16
17
|
|
|
17
18
|
Arguments:
|
|
18
|
-
name Skill name (
|
|
19
|
+
name Skill name (required, e.g., my-deploy-skill)
|
|
19
20
|
|
|
20
21
|
Options:
|
|
22
|
+
-g, --global Create in global skills (~/.claude/skills/)
|
|
21
23
|
--json Output as JSON
|
|
22
24
|
|
|
23
25
|
Examples:
|
|
24
|
-
happyskills init
|
|
25
|
-
happyskills init my-deploy-skill`
|
|
26
|
+
happyskills init my-deploy-skill
|
|
27
|
+
happyskills init my-deploy-skill -g`
|
|
26
28
|
|
|
27
29
|
const create_skill_md = (name) => `---
|
|
28
30
|
name: ${name}
|
|
@@ -50,14 +52,24 @@ const run = (args) => catch_errors('Init failed', async () => {
|
|
|
50
52
|
return process.exit(EXIT_CODES.SUCCESS)
|
|
51
53
|
}
|
|
52
54
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
+
const name = args._[0]
|
|
56
|
+
if (!name) {
|
|
57
|
+
throw new UsageError('Please specify a skill name (e.g., happyskills init my-deploy-skill).')
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const is_global = args.flags.global || false
|
|
61
|
+
const project_root = find_project_root()
|
|
62
|
+
const base_skills_dir = skills_dir(is_global, project_root)
|
|
63
|
+
const dir = path.join(base_skills_dir, name)
|
|
55
64
|
|
|
56
65
|
const [, json_exists] = await file_exists(path.join(dir, SKILL_JSON))
|
|
57
66
|
if (json_exists) {
|
|
58
|
-
throw new CliError(`${SKILL_JSON} already exists
|
|
67
|
+
throw new CliError(`${SKILL_JSON} already exists at ${dir}`, EXIT_CODES.ERROR)
|
|
59
68
|
}
|
|
60
69
|
|
|
70
|
+
const [dir_err] = await ensure_dir(dir)
|
|
71
|
+
if (dir_err) throw e('Failed to create skill directory', dir_err)
|
|
72
|
+
|
|
61
73
|
const manifest = create_manifest(name)
|
|
62
74
|
const [write_err] = await write_manifest(dir, manifest)
|
|
63
75
|
if (write_err) throw e('Failed to write manifest', write_err)
|
|
@@ -76,7 +88,7 @@ const run = (args) => catch_errors('Init failed', async () => {
|
|
|
76
88
|
return
|
|
77
89
|
}
|
|
78
90
|
|
|
79
|
-
print_success(`Initialized skill "${name}"`)
|
|
91
|
+
print_success(`Initialized skill "${name}" at ${dir}`)
|
|
80
92
|
console.log(` ${SKILL_JSON} — manifest`)
|
|
81
93
|
console.log(` ${SKILL_MD} — instructions`)
|
|
82
94
|
console.log()
|
package/src/constants.js
CHANGED
|
@@ -29,13 +29,15 @@ const COMMAND_ALIASES = {
|
|
|
29
29
|
r: 'refresh',
|
|
30
30
|
up: 'update',
|
|
31
31
|
pub: 'publish',
|
|
32
|
-
v: 'validate'
|
|
32
|
+
v: 'validate',
|
|
33
|
+
del: 'delete'
|
|
33
34
|
}
|
|
34
35
|
|
|
35
36
|
const COMMANDS = [
|
|
36
37
|
'init',
|
|
37
38
|
'install',
|
|
38
39
|
'uninstall',
|
|
40
|
+
'delete',
|
|
39
41
|
'list',
|
|
40
42
|
'search',
|
|
41
43
|
'check',
|
package/src/index.js
CHANGED
|
@@ -87,6 +87,7 @@ Commands:
|
|
|
87
87
|
init [name] Scaffold SKILL.md + skill.json
|
|
88
88
|
install [owner/skill] Install skill + dependencies (alias: i, add)
|
|
89
89
|
uninstall <owner/skill> Remove skill + prune orphans (alias: rm, remove)
|
|
90
|
+
delete <owner/skill> Delete skill from registry (alias: del)
|
|
90
91
|
list List installed skills (alias: ls)
|
|
91
92
|
search <query> Search the registry (alias: s)
|
|
92
93
|
check [owner/skill] Check for available updates
|
|
@@ -315,9 +315,11 @@ describe('CLI — --json: success responses use { data } envelope', () => {
|
|
|
315
315
|
it('init --json error when skill.json already exists returns { error }', () => {
|
|
316
316
|
const tmp = make_tmp()
|
|
317
317
|
try {
|
|
318
|
-
// Create skill.json first
|
|
319
|
-
|
|
320
|
-
|
|
318
|
+
// Create .claude/skills/test-skill/skill.json first
|
|
319
|
+
const skill_dir = path.join(tmp, '.claude', 'skills', 'test-skill')
|
|
320
|
+
fs.mkdirSync(skill_dir, { recursive: true })
|
|
321
|
+
fs.writeFileSync(path.join(skill_dir, 'skill.json'), '{}')
|
|
322
|
+
const { stdout, code } = run(['init', 'test-skill', '--json'], {}, { cwd: tmp })
|
|
321
323
|
assert.strictEqual(code, 1)
|
|
322
324
|
const out = parse_json_output(stdout, 'init --json duplicate')
|
|
323
325
|
assert.ok('error' in out)
|
|
@@ -606,3 +608,83 @@ describe('CLI — validate command', () => {
|
|
|
606
608
|
}
|
|
607
609
|
})
|
|
608
610
|
})
|
|
611
|
+
|
|
612
|
+
// ─── delete command ───────────────────────────────────────────────────────────
|
|
613
|
+
|
|
614
|
+
describe('CLI — delete command', () => {
|
|
615
|
+
it('delete --help outputs correct help text and exits 0', () => {
|
|
616
|
+
const { stdout, code } = run(['delete', '--help'])
|
|
617
|
+
assert.strictEqual(code, 0)
|
|
618
|
+
assert.ok(stdout.includes('Arguments:'))
|
|
619
|
+
assert.ok(stdout.includes('Options:'))
|
|
620
|
+
assert.ok(stdout.includes('Examples:'))
|
|
621
|
+
assert.ok(stdout.includes('Aliases:'))
|
|
622
|
+
})
|
|
623
|
+
|
|
624
|
+
it('delete without arguments exits with code 2 (usage error)', () => {
|
|
625
|
+
const { code, stderr } = run(['delete'])
|
|
626
|
+
assert.strictEqual(code, 2)
|
|
627
|
+
assert.ok(stderr.includes('specify a skill'))
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
it('delete with no slash exits with code 2 and shows owner/name format message', () => {
|
|
631
|
+
const { code, stderr } = run(['delete', 'deploy-aws'])
|
|
632
|
+
assert.strictEqual(code, 2)
|
|
633
|
+
assert.ok(stderr.includes('owner/name format'))
|
|
634
|
+
})
|
|
635
|
+
|
|
636
|
+
it('del alias resolves to delete (shows correct --help)', () => {
|
|
637
|
+
const { stdout, code } = run(['del', '--help'])
|
|
638
|
+
assert.strictEqual(code, 0)
|
|
639
|
+
assert.ok(stdout.toLowerCase().includes('delete'))
|
|
640
|
+
})
|
|
641
|
+
|
|
642
|
+
it('delete acme/deploy-aws --json without -y exits with code 1 (confirmation required)', () => {
|
|
643
|
+
const tmp_xdg = make_tmp()
|
|
644
|
+
try {
|
|
645
|
+
const creds_dir = path.join(tmp_xdg, 'happyskills')
|
|
646
|
+
fs.mkdirSync(creds_dir, { recursive: true })
|
|
647
|
+
const payload = { email: 'test@example.com', 'cognito:username': 'testuser', sub: 'test-sub-123' }
|
|
648
|
+
const fake_jwt = `eyJhbGciOiJSUzI1NiJ9.${Buffer.from(JSON.stringify(payload)).toString('base64url')}.fakesig`
|
|
649
|
+
fs.writeFileSync(path.join(creds_dir, 'credentials.json'), JSON.stringify({
|
|
650
|
+
id_token: fake_jwt,
|
|
651
|
+
access_token: 'fake-access',
|
|
652
|
+
refresh_token: 'fake-refresh',
|
|
653
|
+
expires_in: 3600,
|
|
654
|
+
stored_at: new Date().toISOString()
|
|
655
|
+
}, null, '\t'))
|
|
656
|
+
const { stdout, code } = run(['delete', 'acme/deploy-aws', '--json'], { XDG_CONFIG_HOME: tmp_xdg })
|
|
657
|
+
assert.strictEqual(code, 1)
|
|
658
|
+
const out = parse_json_output(stdout, 'delete --json no -y')
|
|
659
|
+
assert.ok('error' in out)
|
|
660
|
+
assert.ok(out.error.message.toLowerCase().includes('confirmation'))
|
|
661
|
+
} finally {
|
|
662
|
+
fs.rmSync(tmp_xdg, { recursive: true, force: true })
|
|
663
|
+
}
|
|
664
|
+
})
|
|
665
|
+
|
|
666
|
+
it('delete acme/deploy-aws --json -y exits with code 4 (network error)', () => {
|
|
667
|
+
const tmp_xdg = make_tmp()
|
|
668
|
+
try {
|
|
669
|
+
const creds_dir = path.join(tmp_xdg, 'happyskills')
|
|
670
|
+
fs.mkdirSync(creds_dir, { recursive: true })
|
|
671
|
+
const payload = { email: 'test@example.com', 'cognito:username': 'testuser', sub: 'test-sub-123' }
|
|
672
|
+
const fake_jwt = `eyJhbGciOiJSUzI1NiJ9.${Buffer.from(JSON.stringify(payload)).toString('base64url')}.fakesig`
|
|
673
|
+
fs.writeFileSync(path.join(creds_dir, 'credentials.json'), JSON.stringify({
|
|
674
|
+
id_token: fake_jwt,
|
|
675
|
+
access_token: 'fake-access',
|
|
676
|
+
refresh_token: 'fake-refresh',
|
|
677
|
+
expires_in: 3600,
|
|
678
|
+
stored_at: new Date().toISOString()
|
|
679
|
+
}, null, '\t'))
|
|
680
|
+
const { stdout, code } = run(['delete', 'acme/deploy-aws', '--json', '-y'], { XDG_CONFIG_HOME: tmp_xdg })
|
|
681
|
+
assert.strictEqual(code, 4)
|
|
682
|
+
const out = parse_json_output(stdout, 'delete --json -y network failure')
|
|
683
|
+
assert.ok('error' in out)
|
|
684
|
+
assert.strictEqual(out.error.code, 'NETWORK_ERROR')
|
|
685
|
+
assert.strictEqual(out.error.exit_code, 4)
|
|
686
|
+
} finally {
|
|
687
|
+
fs.rmSync(tmp_xdg, { recursive: true, force: true })
|
|
688
|
+
}
|
|
689
|
+
})
|
|
690
|
+
})
|