harveyz-skill 0.1.2 → 0.2.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/bin/cli.js +116 -44
- package/lib/bundles.js +97 -22
- package/lib/installer.js +126 -5
- package/lib/targets.js +1 -0
- package/lib/vars.js +34 -0
- package/package.json +7 -3
- package/skills/harness/diataxis-docs/SKILL.md +128 -0
- package/skills/harness/diataxis-docs/references/custom-categories.md +32 -0
- package/skills/harness/diataxis-docs/references/index-template.md +29 -0
- package/skills/web-fetch/article-fetcher/SKILL.md +625 -0
- package/skills/web-fetch/article-fetcher/references/article_utils.py +225 -0
- package/skills/web-fetch/article-fetcher/references/file-format.md +73 -0
- package/skills/web-fetch/article-fetcher/scripts/import_reading.py +75 -0
- package/skills/web-fetch/article-fetcher/scripts/init_db.py +33 -0
- package/skills/web-fetch/article-fetcher/scripts/url-index.db +0 -0
- package/skills/web-fetch/article-fetcher/scripts/url-index.db.bak +0 -0
- package/skills/web-fetch/article-fetcher/vars.json +12 -0
- package/skills/writing/mermaid-diagram/SKILL.md +220 -0
- package/skills/writing/mermaid-diagram/references/color-templates.md +75 -0
- package/skills/writing/mermaid-diagram/references/flowchart.md +88 -0
- package/skills/writing/mermaid-diagram/references/gantt-timeline.md +62 -0
- package/skills/writing/mermaid-diagram/references/other-charts.md +55 -0
- package/skills/writing/mermaid-diagram/references/sequence.md +143 -0
- package/skills/writing/mermaid-diagram/references/statediagram.md +54 -0
- package/skills/writing/mermaid-diagram/scripts/check_mermaid.py +84 -0
- package/skills-index.json +32 -0
- package/tools/p-launch/p-launch.sh +275 -0
- package/tools/p-launch/tests/e2e.bats +164 -0
- package/tools/p-launch/tests/unit.bats +151 -0
- package/tools/p-launch/vars.json +9 -0
- package/tools/p-launch/zshrc.snippet +5 -0
- package/bundles.json +0 -24
package/bin/cli.js
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { checkbox } from '@inquirer/prompts'
|
|
3
3
|
import chalk from 'chalk'
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
buildAllChoices, getAllSkillItems, getAllToolItems,
|
|
6
|
+
resolveSkills, resolveTools,
|
|
7
|
+
TOOL_BUNDLE_CHOICES,
|
|
8
|
+
} from '../lib/bundles.js'
|
|
5
9
|
import { TARGET_CHOICES, resolveTargets, TARGETS } from '../lib/targets.js'
|
|
6
|
-
import { installSkills } from '../lib/installer.js'
|
|
10
|
+
import { installSkills, installTools } from '../lib/installer.js'
|
|
7
11
|
|
|
8
12
|
const args = process.argv.slice(2)
|
|
9
13
|
const forceFlag = args.includes('--force')
|
|
@@ -12,70 +16,138 @@ const targetIdx = args.indexOf('--target')
|
|
|
12
16
|
const bundleArg = bundleIdx !== -1 ? args[bundleIdx + 1] : undefined
|
|
13
17
|
const targetArg = targetIdx !== -1 ? args[targetIdx + 1] : undefined
|
|
14
18
|
|
|
15
|
-
// list 子命令
|
|
16
19
|
if (args[0] === 'list') {
|
|
17
20
|
const { createRequire } = await import('module')
|
|
18
21
|
const require = createRequire(import.meta.url)
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
if (
|
|
22
|
+
const { bundleMeta, skills, toolBundleMeta = {}, tools = [] } = require('../skills-index.json')
|
|
23
|
+
const byBundle = {}
|
|
24
|
+
for (const s of skills) {
|
|
25
|
+
if (!byBundle[s.bundle]) byBundle[s.bundle] = []
|
|
26
|
+
byBundle[s.bundle].push(s.path)
|
|
27
|
+
}
|
|
28
|
+
for (const [name, paths] of Object.entries(byBundle)) {
|
|
29
|
+
console.log(chalk.bold(name) + ' — ' + (bundleMeta[name] ?? name))
|
|
30
|
+
for (const p of paths) console.log(' ' + p)
|
|
31
|
+
}
|
|
32
|
+
if (tools.length > 0) {
|
|
33
|
+
console.log('')
|
|
34
|
+
console.log(chalk.bold('shell tools:'))
|
|
35
|
+
for (const t of tools) console.log(' ' + t.name)
|
|
23
36
|
}
|
|
24
37
|
process.exit(0)
|
|
25
38
|
}
|
|
26
39
|
|
|
40
|
+
const TOOL_BUNDLE_VALUES = new Set(TOOL_BUNDLE_CHOICES.map(c => c.value))
|
|
41
|
+
|
|
42
|
+
function buildInteractiveChoices() {
|
|
43
|
+
const toolItems = getAllToolItems()
|
|
44
|
+
return [
|
|
45
|
+
...buildAllChoices(),
|
|
46
|
+
...(toolItems.length > 0
|
|
47
|
+
? [
|
|
48
|
+
{ type: 'separator', separator: '── shell tools ──' },
|
|
49
|
+
...toolItems.map(t => ({ name: t.toolName, value: t })),
|
|
50
|
+
]
|
|
51
|
+
: []),
|
|
52
|
+
{ type: 'separator', separator: '────────────────' },
|
|
53
|
+
{ name: 'all skills', value: 'all' },
|
|
54
|
+
]
|
|
55
|
+
}
|
|
56
|
+
|
|
27
57
|
try {
|
|
28
|
-
|
|
29
|
-
let
|
|
58
|
+
let skillItems = []
|
|
59
|
+
let toolItems = []
|
|
60
|
+
|
|
30
61
|
if (bundleArg) {
|
|
31
|
-
|
|
62
|
+
const bundles = bundleArg.split(',').map(s => s.trim()).filter(Boolean)
|
|
63
|
+
const skillBundles = bundles.filter(b => !TOOL_BUNDLE_VALUES.has(b))
|
|
64
|
+
const toolBundles = bundles.filter(b => TOOL_BUNDLE_VALUES.has(b))
|
|
65
|
+
if (skillBundles.length) skillItems = resolveSkills(skillBundles).map(s => ({ kind: 'skill', ...s }))
|
|
66
|
+
if (toolBundles.length) toolItems = resolveTools(toolBundles).map(t => ({ kind: 'tool', ...t }))
|
|
32
67
|
} else {
|
|
33
|
-
|
|
34
|
-
message: '
|
|
35
|
-
choices:
|
|
68
|
+
const selected = await checkbox({
|
|
69
|
+
message: 'Select items to install (space to select, enter to confirm):',
|
|
70
|
+
choices: buildInteractiveChoices(),
|
|
36
71
|
})
|
|
37
|
-
}
|
|
38
72
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
73
|
+
if (!selected.length) {
|
|
74
|
+
console.log(chalk.dim(' · Nothing selected, exiting'))
|
|
75
|
+
process.exit(0)
|
|
76
|
+
}
|
|
43
77
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
],
|
|
78
|
+
const hasAll = selected.includes('all')
|
|
79
|
+
const items = selected.filter(s => s !== 'all')
|
|
80
|
+
|
|
81
|
+
const selectedSkills = hasAll ? getAllSkillItems() : items.filter(s => s.kind === 'skill')
|
|
82
|
+
toolItems = items.filter(s => s.kind === 'tool')
|
|
83
|
+
|
|
84
|
+
const seen = new Set()
|
|
85
|
+
skillItems = selectedSkills.filter(s => {
|
|
86
|
+
if (seen.has(s.skillName)) return false
|
|
87
|
+
seen.add(s.skillName); return true
|
|
55
88
|
})
|
|
56
89
|
}
|
|
57
90
|
|
|
58
|
-
if (!
|
|
59
|
-
console.log(chalk.
|
|
60
|
-
process.exit(
|
|
91
|
+
if (!skillItems.length && !toolItems.length) {
|
|
92
|
+
console.log(chalk.dim(' · Nothing selected, exiting'))
|
|
93
|
+
process.exit(0)
|
|
61
94
|
}
|
|
62
95
|
|
|
63
|
-
|
|
64
|
-
|
|
96
|
+
// ── Install skills ───────────────────────────────────────────────────────────
|
|
97
|
+
if (skillItems.length > 0) {
|
|
98
|
+
let selectedTargets
|
|
99
|
+
if (targetArg) {
|
|
100
|
+
selectedTargets = targetArg === 'all' ? Object.keys(TARGETS) : [targetArg]
|
|
101
|
+
} else {
|
|
102
|
+
selectedTargets = await checkbox({
|
|
103
|
+
message: 'Install to which tools (space to select, enter to confirm):',
|
|
104
|
+
choices: [
|
|
105
|
+
...TARGET_CHOICES,
|
|
106
|
+
{ name: 'all — all tools', value: 'all' },
|
|
107
|
+
],
|
|
108
|
+
})
|
|
109
|
+
}
|
|
65
110
|
|
|
66
|
-
|
|
67
|
-
|
|
111
|
+
if (selectedTargets.length > 0) {
|
|
112
|
+
const targets = resolveTargets(selectedTargets)
|
|
113
|
+
console.log('')
|
|
114
|
+
const summary = await installSkills(skillItems, targets, forceFlag)
|
|
115
|
+
console.log('')
|
|
116
|
+
if (Object.keys(summary).length === 0) {
|
|
117
|
+
console.log(chalk.dim(' · No skills installed'))
|
|
118
|
+
} else {
|
|
119
|
+
console.log(chalk.green.bold('✔ Skills installed:'))
|
|
120
|
+
for (const [target, names] of Object.entries(summary)) {
|
|
121
|
+
console.log(` ${chalk.bold(target)} ← ${names.join(', ')}`)
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
68
126
|
|
|
69
|
-
|
|
70
|
-
if (
|
|
71
|
-
console.log(
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
127
|
+
// ── Install shell tools ──────────────────────────────────────────────────────
|
|
128
|
+
if (toolItems.length > 0) {
|
|
129
|
+
console.log('')
|
|
130
|
+
const installed = await installTools(
|
|
131
|
+
toolItems.map(t => ({ toolName: t.toolName, srcPath: t.srcPath })),
|
|
132
|
+
TARGETS.shell,
|
|
133
|
+
forceFlag,
|
|
134
|
+
)
|
|
135
|
+
console.log('')
|
|
136
|
+
if (installed.length === 0) {
|
|
137
|
+
console.log(chalk.dim(' · No shell tools installed'))
|
|
138
|
+
} else {
|
|
139
|
+
console.log(chalk.green.bold('✔ Shell tools installed:'))
|
|
140
|
+
for (const name of installed) {
|
|
141
|
+
console.log(` ${chalk.bold('~/.local/bin')} ← ${name}`)
|
|
142
|
+
}
|
|
143
|
+
console.log('')
|
|
144
|
+
console.log(chalk.yellow.bold(' ⚡ Reload your shell to apply changes:'))
|
|
145
|
+
console.log('')
|
|
146
|
+
console.log(` ${chalk.bold.cyan('source ~/.zshrc')}`)
|
|
147
|
+
console.log('')
|
|
76
148
|
}
|
|
77
149
|
}
|
|
78
150
|
} catch (err) {
|
|
79
|
-
console.error(chalk.red('
|
|
151
|
+
console.error(chalk.red(' ✗ ' + err.message))
|
|
80
152
|
process.exit(1)
|
|
81
153
|
}
|
package/lib/bundles.js
CHANGED
|
@@ -5,34 +5,109 @@ import { fileURLToPath } from 'url'
|
|
|
5
5
|
const require = createRequire(import.meta.url)
|
|
6
6
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
7
7
|
|
|
8
|
-
const
|
|
9
|
-
const
|
|
8
|
+
const { bundleMeta, skills: skillDefs, toolBundleMeta = {}, tools: toolDefs = [] } = require('../skills-index.json')
|
|
9
|
+
const repoRoot = path.join(__dirname, '..')
|
|
10
|
+
const skillsRoot = path.join(repoRoot, 'skills')
|
|
10
11
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
// ── Skills ──────────────────────────────────────────────────────────────────
|
|
13
|
+
const bundleSkills = {}
|
|
14
|
+
for (const skill of skillDefs) {
|
|
15
|
+
if (!bundleSkills[skill.bundle]) bundleSkills[skill.bundle] = []
|
|
16
|
+
bundleSkills[skill.bundle].push(skill)
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const namedBundles = Object.keys(bundleSkills)
|
|
20
|
+
|
|
21
|
+
export const BUNDLE_CHOICES = [
|
|
22
|
+
...namedBundles.map(name => ({
|
|
23
|
+
name: `${name.padEnd(16)} — ${bundleMeta[name] ?? name}`,
|
|
24
|
+
value: name,
|
|
25
|
+
})),
|
|
26
|
+
{ name: `${'all'.padEnd(16)} — 全部 skill`, value: 'all' },
|
|
27
|
+
]
|
|
15
28
|
|
|
16
|
-
// 返回选中 bundles 对应的去重 skill 路径列表 { skillName, srcPath }
|
|
17
29
|
export function resolveSkills(selectedBundles) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
const expanded = selectedBundles.includes('all') ? namedBundles : selectedBundles
|
|
31
|
+
|
|
32
|
+
const seen = new Set()
|
|
33
|
+
const result = []
|
|
34
|
+
for (const bundleName of expanded) {
|
|
35
|
+
const skills = bundleSkills[bundleName]
|
|
36
|
+
if (!skills) throw new Error(`Unknown bundle: "${bundleName}"`)
|
|
37
|
+
for (const skill of skills) {
|
|
38
|
+
if (seen.has(skill.path)) continue
|
|
39
|
+
seen.add(skill.path)
|
|
40
|
+
const skillName = skill.path.split('/').pop()
|
|
41
|
+
result.push({ skillName, srcPath: path.join(skillsRoot, skill.path) })
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return result
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// 单个 skill 粒度的选项列表(用于交互式选择)
|
|
48
|
+
// 每个值是 { kind:'skill', skillName, srcPath } 或特殊字符串 'all'
|
|
49
|
+
export function buildAllChoices() {
|
|
50
|
+
const choices = []
|
|
51
|
+
|
|
52
|
+
// ── skill ── 分组
|
|
53
|
+
for (const [bundleName, skills] of Object.entries(bundleSkills)) {
|
|
54
|
+
choices.push({ type: 'separator', separator: `── ${bundleName} ──` })
|
|
55
|
+
for (const skill of skills) {
|
|
56
|
+
const skillName = skill.path.split('/').pop()
|
|
57
|
+
choices.push({
|
|
58
|
+
name: skillName,
|
|
59
|
+
value: { kind: 'skill', skillName, srcPath: path.join(skillsRoot, skill.path) },
|
|
60
|
+
})
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return choices
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 所有 skill 展开为 { kind:'skill', skillName, srcPath } 列表(对应 all 选项)
|
|
68
|
+
export function getAllSkillItems() {
|
|
69
|
+
return skillDefs.map(skill => ({
|
|
70
|
+
kind: 'skill',
|
|
71
|
+
skillName: skill.path.split('/').pop(),
|
|
72
|
+
srcPath: path.join(skillsRoot, skill.path),
|
|
73
|
+
}))
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 所有 tool 展开为 { kind:'tool', toolName, srcPath } 列表
|
|
77
|
+
export function getAllToolItems() {
|
|
78
|
+
return toolDefs.map(tool => ({
|
|
79
|
+
kind: 'tool',
|
|
80
|
+
toolName: tool.name,
|
|
81
|
+
srcPath: path.join(repoRoot, 'tools', tool.name),
|
|
82
|
+
}))
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// ── Tools ────────────────────────────────────────────────────────────────────
|
|
86
|
+
const bundleTools = {}
|
|
87
|
+
for (const tool of toolDefs) {
|
|
88
|
+
if (!bundleTools[tool.bundle]) bundleTools[tool.bundle] = []
|
|
89
|
+
bundleTools[tool.bundle].push(tool)
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const namedToolBundles = Object.keys(bundleTools)
|
|
93
|
+
|
|
94
|
+
export const TOOL_BUNDLE_CHOICES = namedToolBundles.map(name => ({
|
|
95
|
+
name: `${name.padEnd(16)} — ${toolBundleMeta[name] ?? name}`,
|
|
96
|
+
value: name,
|
|
97
|
+
}))
|
|
98
|
+
|
|
99
|
+
export function resolveTools(selectedBundles) {
|
|
100
|
+
const expanded = selectedBundles.includes('all') ? namedToolBundles : selectedBundles
|
|
25
101
|
|
|
26
102
|
const seen = new Set()
|
|
27
103
|
const result = []
|
|
28
|
-
for (const
|
|
29
|
-
const
|
|
30
|
-
if (!
|
|
31
|
-
for (const
|
|
32
|
-
if (seen.has(
|
|
33
|
-
seen.add(
|
|
34
|
-
|
|
35
|
-
result.push({ skillName, srcPath: path.join(skillsRoot, rel) })
|
|
104
|
+
for (const bundleName of expanded) {
|
|
105
|
+
const tools = bundleTools[bundleName]
|
|
106
|
+
if (!tools) throw new Error(`Unknown tool bundle: "${bundleName}"`)
|
|
107
|
+
for (const tool of tools) {
|
|
108
|
+
if (seen.has(tool.name)) continue
|
|
109
|
+
seen.add(tool.name)
|
|
110
|
+
result.push({ toolName: tool.name, srcPath: path.join(repoRoot, 'tools', tool.name) })
|
|
36
111
|
}
|
|
37
112
|
}
|
|
38
113
|
return result
|
package/lib/installer.js
CHANGED
|
@@ -1,7 +1,117 @@
|
|
|
1
1
|
import fs from 'fs-extra'
|
|
2
|
+
import os from 'os'
|
|
2
3
|
import path from 'path'
|
|
3
4
|
import { confirm } from '@inquirer/prompts'
|
|
4
5
|
import chalk from 'chalk'
|
|
6
|
+
import { loadVarDefs, buildAutoVars, resolveVars, substituteVars } from './vars.js'
|
|
7
|
+
|
|
8
|
+
const BINARY_EXTS = new Set(['.db', '.pyc', '.jpg', '.jpeg', '.png', '.gif', '.webp', '.pdf'])
|
|
9
|
+
const SKIP_NAMES = new Set(['vars.json', '__pycache__', '.DS_Store'])
|
|
10
|
+
|
|
11
|
+
async function copyDir(srcDir, destDir, varsMap) {
|
|
12
|
+
await fs.ensureDir(destDir)
|
|
13
|
+
const entries = await fs.readdir(srcDir, { withFileTypes: true })
|
|
14
|
+
for (const entry of entries) {
|
|
15
|
+
if (SKIP_NAMES.has(entry.name)) continue
|
|
16
|
+
const src = path.join(srcDir, entry.name)
|
|
17
|
+
const dest = path.join(destDir, entry.name)
|
|
18
|
+
if (entry.isDirectory()) {
|
|
19
|
+
await copyDir(src, dest, varsMap)
|
|
20
|
+
} else if (Object.keys(varsMap).length > 0 && !BINARY_EXTS.has(path.extname(entry.name).toLowerCase())) {
|
|
21
|
+
const content = await fs.readFile(src, 'utf-8').catch(e => { throw new Error(`Failed to read ${src}: ${e.message}`) })
|
|
22
|
+
await fs.writeFile(dest, substituteVars(content, varsMap), 'utf-8')
|
|
23
|
+
} else {
|
|
24
|
+
await fs.copy(src, dest, { overwrite: true })
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// tools: [{ toolName, srcPath }]
|
|
30
|
+
// targetDir: string — path to ~/.local/bin
|
|
31
|
+
// force: boolean
|
|
32
|
+
export async function installTools(tools, targetDir, force = false) {
|
|
33
|
+
const exists = await fs.pathExists(targetDir)
|
|
34
|
+
if (!exists) {
|
|
35
|
+
console.log(chalk.dim(` · Creating directory: ${targetDir}`))
|
|
36
|
+
await fs.ensureDir(targetDir)
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const installed = []
|
|
40
|
+
for (const { toolName, srcPath } of tools) {
|
|
41
|
+
const scriptSrc = path.join(srcPath, `${toolName}.sh`)
|
|
42
|
+
const destPath = path.join(targetDir, toolName)
|
|
43
|
+
|
|
44
|
+
if (!await fs.pathExists(scriptSrc)) {
|
|
45
|
+
console.log(chalk.red(` ✗ Script not found: ${scriptSrc}`))
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const destExists = await fs.pathExists(destPath)
|
|
50
|
+
if (destExists && !force) {
|
|
51
|
+
const ok = await confirm({ message: `${destPath} already exists. Overwrite?`, default: true })
|
|
52
|
+
if (!ok) {
|
|
53
|
+
console.log(chalk.dim(` · Skipped ${toolName}`))
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
try {
|
|
59
|
+
const varDefs = await loadVarDefs(srcPath)
|
|
60
|
+
let varsMap = {}
|
|
61
|
+
if (varDefs.length > 0) {
|
|
62
|
+
console.log(chalk.bold(`\n Configure ${toolName}:`))
|
|
63
|
+
varsMap = await resolveVars(varDefs, buildAutoVars())
|
|
64
|
+
for (const def of varDefs) {
|
|
65
|
+
if (def.configFile && def.configKey) {
|
|
66
|
+
await _writeToolConfigVar(def, varsMap[def.name])
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
const content = await fs.readFile(scriptSrc, 'utf-8')
|
|
71
|
+
await fs.writeFile(destPath, substituteVars(content, varsMap), { encoding: 'utf-8', mode: 0o755 })
|
|
72
|
+
installed.push(toolName)
|
|
73
|
+
await _patchZshrc(srcPath, toolName)
|
|
74
|
+
} catch (err) {
|
|
75
|
+
console.log(chalk.red(` ✗ Failed to install ${toolName}: ${err.message}`))
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return installed
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function _writeToolConfigVar(def, value) {
|
|
83
|
+
const configPath = def.configFile.replace(/^~/, os.homedir())
|
|
84
|
+
await fs.ensureDir(path.dirname(configPath))
|
|
85
|
+
await fs.writeFile(configPath, `${def.configKey}=("${value}")\n`, 'utf-8')
|
|
86
|
+
console.log(chalk.green(` ✓ Config written to ${def.configFile}`))
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
async function _patchZshrc(srcPath, toolName) {
|
|
90
|
+
const snippetPath = path.join(srcPath, 'zshrc.snippet')
|
|
91
|
+
if (!await fs.pathExists(snippetPath)) return
|
|
92
|
+
|
|
93
|
+
const zshrcPath = path.join(os.homedir(), '.zshrc')
|
|
94
|
+
const marker = `# >>> ${toolName}`
|
|
95
|
+
|
|
96
|
+
const existing = await fs.pathExists(zshrcPath)
|
|
97
|
+
? await fs.readFile(zshrcPath, 'utf-8')
|
|
98
|
+
: ''
|
|
99
|
+
|
|
100
|
+
if (existing.includes(marker)) {
|
|
101
|
+
console.log(chalk.dim(` · ~/.zshrc already has ${toolName} config, skipping`))
|
|
102
|
+
return
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const ok = await confirm({
|
|
106
|
+
message: `Add ${toolName} PATH and alias to ~/.zshrc?`,
|
|
107
|
+
default: true,
|
|
108
|
+
})
|
|
109
|
+
if (!ok) return
|
|
110
|
+
|
|
111
|
+
const snippet = await fs.readFile(snippetPath, 'utf-8')
|
|
112
|
+
await fs.appendFile(zshrcPath, snippet, 'utf-8')
|
|
113
|
+
console.log(chalk.green(` ✓ Written to ~/.zshrc`))
|
|
114
|
+
}
|
|
5
115
|
|
|
6
116
|
// skills: [{ skillName, srcPath }]
|
|
7
117
|
// targets: [{ name, dir }]
|
|
@@ -12,7 +122,7 @@ export async function installSkills(skills, targets, force = false) {
|
|
|
12
122
|
for (const { name: targetName, dir: targetDir } of targets) {
|
|
13
123
|
const exists = await fs.pathExists(targetDir)
|
|
14
124
|
if (!exists) {
|
|
15
|
-
console.log(chalk.
|
|
125
|
+
console.log(chalk.dim(` · Skipping ${targetName} (directory not found: ${targetDir})`))
|
|
16
126
|
continue
|
|
17
127
|
}
|
|
18
128
|
|
|
@@ -22,18 +132,29 @@ export async function installSkills(skills, targets, force = false) {
|
|
|
22
132
|
const destExists = await fs.pathExists(destPath)
|
|
23
133
|
|
|
24
134
|
if (destExists && !force) {
|
|
25
|
-
const ok = await confirm({ message: `${targetName}/${skillName}
|
|
135
|
+
const ok = await confirm({ message: `${targetName}/${skillName} already exists. Overwrite?`, default: false })
|
|
26
136
|
if (!ok) {
|
|
27
|
-
console.log(chalk.
|
|
137
|
+
console.log(chalk.dim(` · Skipped ${targetName}/${skillName}`))
|
|
28
138
|
continue
|
|
29
139
|
}
|
|
30
140
|
}
|
|
31
141
|
|
|
142
|
+
if (!await fs.pathExists(srcPath)) {
|
|
143
|
+
console.log(chalk.red(` ✗ Source not found: ${srcPath}`))
|
|
144
|
+
continue
|
|
145
|
+
}
|
|
146
|
+
|
|
32
147
|
try {
|
|
33
|
-
await
|
|
148
|
+
const varDefs = await loadVarDefs(srcPath)
|
|
149
|
+
let varsMap = {}
|
|
150
|
+
if (varDefs.length > 0) {
|
|
151
|
+
console.log(chalk.bold(`\n Configure ${skillName}:`))
|
|
152
|
+
varsMap = await resolveVars(varDefs, buildAutoVars())
|
|
153
|
+
}
|
|
154
|
+
await copyDir(srcPath, destPath, varsMap)
|
|
34
155
|
installed.push(skillName)
|
|
35
156
|
} catch (err) {
|
|
36
|
-
console.log(chalk.red(`
|
|
157
|
+
console.log(chalk.red(` ✗ Failed to copy ${targetName}/${skillName}: ${err.message}`))
|
|
37
158
|
}
|
|
38
159
|
}
|
|
39
160
|
|
package/lib/targets.js
CHANGED
|
@@ -5,6 +5,7 @@ export const TARGETS = {
|
|
|
5
5
|
claude: path.join(os.homedir(), '.claude', 'skills'),
|
|
6
6
|
cursor: path.join(os.homedir(), '.cursor', 'skills'),
|
|
7
7
|
codex: path.join(os.homedir(), '.codex', 'skills'),
|
|
8
|
+
shell: path.join(os.homedir(), '.local', 'bin'),
|
|
8
9
|
}
|
|
9
10
|
|
|
10
11
|
export const TARGET_CHOICES = Object.entries(TARGETS).map(([name, dir]) => ({
|
package/lib/vars.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// lib/vars.js
|
|
2
|
+
import fs from 'fs-extra'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import os from 'os'
|
|
5
|
+
import { input } from '@inquirer/prompts'
|
|
6
|
+
|
|
7
|
+
export function buildAutoVars() {
|
|
8
|
+
return { HOME: os.homedir() }
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export async function loadVarDefs(skillSrcPath) {
|
|
12
|
+
const varsFile = path.join(skillSrcPath, 'vars.json')
|
|
13
|
+
if (!await fs.pathExists(varsFile)) return []
|
|
14
|
+
const defs = await fs.readJson(varsFile)
|
|
15
|
+
if (!Array.isArray(defs)) throw new Error(`vars.json must be an array (got ${typeof defs})`)
|
|
16
|
+
return defs
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function substituteVars(text, varsMap) {
|
|
20
|
+
return text.replace(/\{\{(\w+)\}\}/g, (_, name) => varsMap[name] ?? `{{${name}}}`)
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export async function resolveVars(varDefs = [], autoVars) {
|
|
24
|
+
const result = { ...autoVars }
|
|
25
|
+
for (const def of varDefs) {
|
|
26
|
+
const defaultVal = substituteVars(def.default ?? '', autoVars)
|
|
27
|
+
const value = await input({
|
|
28
|
+
message: `${def.description}:`,
|
|
29
|
+
default: defaultVal,
|
|
30
|
+
})
|
|
31
|
+
result[def.name] = value
|
|
32
|
+
}
|
|
33
|
+
return result
|
|
34
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harveyz-skill",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "One-click skill installer for Claude Code, Cursor, and Codex",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -13,9 +13,10 @@
|
|
|
13
13
|
"bin/",
|
|
14
14
|
"lib/",
|
|
15
15
|
"bundles.json",
|
|
16
|
+
"skills-index.json",
|
|
16
17
|
"skills/analysis/skill-analyzer/CHANGELOG.md",
|
|
17
18
|
"skills/analysis/skill-analyzer/SKILL.md",
|
|
18
|
-
"skills/
|
|
19
|
+
"skills/harness/diataxis-docs/",
|
|
19
20
|
"skills/harness/full-stack-debug-env/",
|
|
20
21
|
"skills/superpowers-fork/brainstorming/",
|
|
21
22
|
"skills/superpowers-fork/executing-plans/",
|
|
@@ -23,7 +24,10 @@
|
|
|
23
24
|
"skills/superpowers-fork/using-git-worktrees/",
|
|
24
25
|
"skills/superpowers-fork/writing-plans/",
|
|
25
26
|
"skills/task/pm-task-dispatch/",
|
|
26
|
-
"skills/task/task-close/"
|
|
27
|
+
"skills/task/task-close/",
|
|
28
|
+
"skills/web-fetch/article-fetcher/",
|
|
29
|
+
"skills/writing/mermaid-diagram/",
|
|
30
|
+
"tools/p-launch/"
|
|
27
31
|
],
|
|
28
32
|
"engines": {
|
|
29
33
|
"node": ">=18"
|