harveyz-skill 0.32.0 → 0.34.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 +24 -0
- package/bin/cli.js +170 -27
- package/lib/install-source.js +46 -0
- package/lib/version-check.js +2 -2
- package/package.json +7 -1
- package/skills/agent-canvas/agent-canvas-control/SKILL.md +235 -0
- package/skills/agent-canvas/capture-requirement/SKILL.md +51 -0
- package/skills/agent-canvas/close-node/SKILL.md +117 -0
- package/skills/agent-canvas/describe-node/SKILL.md +47 -0
- package/skills/agent-canvas/relate-node/SKILL.md +63 -0
- package/skills/agent-canvas/relation-review/SKILL.md +120 -0
- package/skills/coding/handoff/SKILL.md +122 -17
- package/skills/coding/handoff/assets/handoff-template.md +9 -3
- package/skills/coding/handoff/evals/evals.json +1 -1
- package/skills/coding/handoff/scripts/validate-handoff.sh +10 -0
- package/skills/coding/handoff/tests/validate-handoff.bats +65 -0
- package/skills/feed/sync-website/SKILL.md +14 -3
- package/skills/feed/sync-website/scripts/__pycache__/archive_articles.cpython-314.pyc +0 -0
- package/skills/feed/sync-website/scripts/__pycache__/articles_client.cpython-314.pyc +0 -0
- package/skills/feed/sync-website/scripts/__pycache__/browser_fetch_cli.cpython-314.pyc +0 -0
- package/skills/feed/sync-website/scripts/__pycache__/browser_fetch_locate.cpython-314.pyc +0 -0
- package/skills/feed/sync-website/scripts/__pycache__/calibration_gate.cpython-314.pyc +0 -0
- package/skills/feed/sync-website/scripts/__pycache__/config.cpython-314.pyc +0 -0
- package/skills/feed/sync-website/scripts/__pycache__/cursor.cpython-314.pyc +0 -0
- package/skills/feed/sync-website/scripts/__pycache__/digest.cpython-314.pyc +0 -0
- package/skills/feed/sync-website/scripts/__pycache__/fetch_new_articles.cpython-314.pyc +0 -0
- package/skills/feed/sync-website/scripts/__pycache__/roster_client.cpython-314.pyc +0 -0
- package/skills/feed/sync-website/scripts/__pycache__/roster_locate.cpython-314.pyc +0 -0
- package/skills/feed/sync-website/scripts/__pycache__/store_config.cpython-314.pyc +0 -0
- package/skills/feed/sync-website/scripts/store_config.py +84 -2
- package/skills/feed/sync-website/tests/__pycache__/conftest.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/feed/sync-website/tests/__pycache__/test_archive_articles.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/feed/sync-website/tests/__pycache__/test_articles_client.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/feed/sync-website/tests/__pycache__/test_browser_fetch_locate.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/feed/sync-website/tests/__pycache__/test_calibration_gate.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/feed/sync-website/tests/__pycache__/test_cursor.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/feed/sync-website/tests/__pycache__/test_digest.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/feed/sync-website/tests/__pycache__/test_fetch_new_articles.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/feed/sync-website/tests/__pycache__/test_roster_client.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/feed/sync-website/tests/__pycache__/test_store_config.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/feed/sync-website/tests/test_store_config.py +101 -0
- package/skills/feed/sync-xtimeline/SKILL.md +6 -2
- package/skills/feed/sync-xtimeline/scripts/__pycache__/store_config.cpython-314.pyc +0 -0
- package/skills/feed/sync-xtimeline/scripts/store_config.py +84 -2
- package/skills/feed/sync-xtimeline/tests/__pycache__/test_store_config.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/feed/sync-xtimeline/tests/test_store_config.py +101 -0
- package/skills/feed/sync-ytchannel/SKILL.md +6 -2
- package/skills/feed/sync-ytchannel/scripts/__pycache__/store_config.cpython-314.pyc +0 -0
- package/skills/feed/sync-ytchannel/scripts/store_config.py +84 -2
- package/skills/feed/sync-ytchannel/tests/__pycache__/test_store_config.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/feed/sync-ytchannel/tests/test_store_config.py +101 -0
- package/skills/mint/init-skill/SKILL.md +1 -1
- package/skills/mint/learn-skill/SKILL.md +1 -1
- package/skills/research/clip-url/SKILL.md +10 -3
- package/skills/research/clip-url/scripts/__pycache__/store_config.cpython-314.pyc +0 -0
- package/skills/research/clip-url/scripts/store_config.py +84 -2
- package/skills/research/clip-url/tests/__pycache__/test_store_config.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/research/clip-url/tests/test_store_config.py +101 -0
- package/skills/research/learn-video/SKILL.md +8 -6
- package/skills/research/learn-video/scripts/__pycache__/archive.cpython-314.pyc +0 -0
- package/skills/research/learn-video/scripts/__pycache__/store_config.cpython-314.pyc +0 -0
- package/skills/research/learn-video/scripts/store_config.py +84 -2
- package/skills/research/learn-video/tests/__pycache__/test_archive.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/research/learn-video/tests/__pycache__/test_store_config.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/research/learn-video/tests/test_store_config.py +101 -0
- package/skills/research/survey-skillrepo/SKILL.md +1 -1
- package/skills-index.json +44 -19
- package/tools/browser-fetch/browser_fetch/__pycache__/cli.cpython-314.pyc +0 -0
- package/tools/browser-fetch/browser_fetch/__pycache__/core.cpython-314.pyc +0 -0
- package/tools/browser-fetch/browser_fetch/__pycache__/extractors.cpython-314.pyc +0 -0
- package/tools/browser-fetch/browser_fetch/__pycache__/normalize.cpython-314.pyc +0 -0
- package/tools/browser-fetch/browser_fetch/__pycache__/site_rules.cpython-314.pyc +0 -0
- package/tools/browser-fetch/tests/__pycache__/conftest.cpython-314-pytest-9.1.1.pyc +0 -0
- package/tools/browser-fetch/tests/__pycache__/test_cli_articles.cpython-314-pytest-9.1.1.pyc +0 -0
- package/tools/browser-fetch/tests/__pycache__/test_cli_articles_rule.cpython-314-pytest-9.1.1.pyc +0 -0
- package/tools/browser-fetch/tests/__pycache__/test_cli_articles_transform.cpython-314-pytest-9.1.1.pyc +0 -0
- package/tools/browser-fetch/tests/__pycache__/test_extractors_articles.cpython-314-pytest-9.1.1.pyc +0 -0
- package/tools/browser-fetch/tests/__pycache__/test_normalize.cpython-314-pytest-9.1.1.pyc +0 -0
- package/tools/browser-fetch/tests/__pycache__/test_site_rules.cpython-314-pytest-9.1.1.pyc +0 -0
- package/tools/browser-fetch/tests/__pycache__/test_site_rules_dirs.cpython-314-pytest-9.1.1.pyc +0 -0
- package/tools/roster/roster/__pycache__/urls.cpython-314.pyc +0 -0
- package/tools/roster/tests/__pycache__/test_cli.cpython-314-pytest-9.1.1.pyc +0 -0
- package/tools/roster/tests/__pycache__/test_urls.cpython-314-pytest-9.1.1.pyc +0 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,30 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.34.0] - 2026-09-16
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `hskill` 双来源安装与粘性更新:`hskill update --local <路径>` 从本地仓库 `npm pack` 出 tarball 再全局安装(真实拷贝,走 `prepack` 与 `files[]` 白名单,等于在发 npm 前预演一次真实发布),`hskill update --npm` 切回 registry,裸 `hskill update` 沿用当前来源不跨轨道。此前 `update` 是一行硬编码的 `npm install -g harveyz-skill@latest`,手工装的本地版会被它静默替换掉且事后无从察觉。来源痕迹(`.hskill-source.json` + 版本号后缀 `+local`)只写在全局安装目录内,因而任何一次 `npm install -g`——包括绕过 hskill 的手工安装——都会自动抹掉它,"记录的来源"与"实际的来源"不可能分叉。`hskill version` 展示来源与 branch/commit,`version --check` 在本地来源下比对 commit 而非版本号(开发分支上版本号常常不动)。设计见 `docs/superpowers/specs/2026-09-15-hskill-install-source-design.md`
|
|
14
|
+
- `agent-canvas` bundle:新增 `agent-canvas-control`、`capture-requirement`、`close-node`、`describe-node`、`relate-node`、`relation-review` 六个 skill,在 Agent Canvas 画布节点里操控节点实体、摆放画布、梳理节点间语义关系
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- `hskill <任意子命令> --json` 在输出走管道时被截断:Node 写管道是异步的,而每个子命令块以 `process.exit()` 结尾,不等缓冲区排空。`status --json` 的输出刚随 agent-canvas 六个 skill 涨到 66,739 字节、越过 64KiB 管道缓冲区,于是 `hskill status --json | jq` 拿到退出码 0 和一段从字符串中间断掉的 JSON——**静默失败**,退出码还宣称成功。16 处 `--json` 出口统一改走等待写入落地的 `emitJson()`;回归测试逐字节比对管道输出与直接重定向输出,且在负载跌回缓冲区以下时显式 skip 而非假绿
|
|
18
|
+
- `hskill version` / `hskill --version` 在 npm 不在 PATH 上时抛原始堆栈崩溃(cron、受限 PATH、精简镜像):来源追踪让 `readSource()` 每次都经 `globalRoot()` 打一次 `npm root -g`,而它只兜住了 JSON 解析失败。现在 `globalRoot()` 的失败并入同一个 `null`,即回落到缺省的 npm 来源
|
|
19
|
+
- `compareVersions()` 遇到 semver build metadata(`0.33.0+local`)会解析出 `NaN` 导致比较结果无意义,现按规范在比较前截断 `+` 及其后内容
|
|
20
|
+
|
|
21
|
+
## [0.33.0] - 2026-09-15
|
|
22
|
+
|
|
23
|
+
### Added
|
|
24
|
+
- `store_config.py`(`learn-video`/`clip-url`/`sync-xtimeline`/`sync-ytchannel`/`sync-website` 共用):新增 `check-downstream`,核对统一存储根的产物与 vdl / scholia 两个下游读取方是否同步,五份 SKILL.md 的前置检查步骤接入该核对
|
|
25
|
+
|
|
26
|
+
### Changed
|
|
27
|
+
- `handoff`(1.8.0 → 1.11.0):新增 Phase 2.5 接手方自测闸门 + 打回复修回路(此前「接手方完工 → 待验收」这个状态转移毫无前置条件);建工作区改为显式给基线分支并 `EnterWorktree(path:)` 进去,三方指令统一,修复了两次因裸 git 命令落到主工作树而夹带他人未合并提交的翻车;关闭 `using-git-worktrees` 前置调用(它会另起一个 `.worktrees/` 下的第二工作区,与 handoff 自己的工作区约定冲突);平台专属指令(`EnterWorktree` 等 Claude Code harness 特性)拆进独立「平台适配」章节,避免 pi/hermes/Codex 上的接手方读到卡住
|
|
28
|
+
- `publish-skill` 审计:`init-skill`(1.2.0→1.2.1)、`learn-skill`(2.0.0→2.0.1)、`research/survey-skillrepo`(2.0.1→2.0.2)——内容已变但 version 未 bump,属于 F8 硬性检查项,现已按 patch 补齐;另有 6 个 skill(`handoff`/`sync-website`/`sync-xtimeline`/`sync-ytchannel`/`clip-url`/`learn-video`)此前 version 已 bump 但 `skills-index.json` 里的 `contentHash` 记录没跟上,已同步
|
|
29
|
+
|
|
30
|
+
### Fixed
|
|
31
|
+
- `learn-video` / `clip-url` / `sync-xtimeline` / `sync-ytchannel` / `sync-website` 共用的 `knowledgeRoot` 默认建议路径从 `~/Documents/knowledge` 改为 `~/knowledge`:前者在无 Full Disk Access 的 agent 执行环境下写入会静默失败,且此前没有护栏阻止 agent 擅自把配置改到别的路径绕过,导致产物在用户不知情的情况下分裂到两个根目录;五份 SKILL.md 加"写入失败必须如实报告并停下问用户"的护栏指令
|
|
32
|
+
- `tests/install.bats` / `tests/interactive.bats`:`survey-skillrepo` 测试夹具的硬编码版本号随上面的 F8 审计一并更新为 `2.0.2`,此前的 bump 一度导致 3 个断言失败
|
|
33
|
+
|
|
10
34
|
## [0.32.0] - 2026-09-07
|
|
11
35
|
|
|
12
36
|
### Added
|
package/bin/cli.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { select, input, confirm } from '@inquirer/prompts'
|
|
3
3
|
import chalk from 'chalk'
|
|
4
4
|
import { execSync, spawnSync } from 'child_process'
|
|
5
|
-
import { existsSync, writeFileSync, unlinkSync } from 'fs'
|
|
5
|
+
import { existsSync, readFileSync, writeFileSync, unlinkSync, mkdtempSync } from 'fs'
|
|
6
6
|
import { createRequire } from 'module'
|
|
7
7
|
import os from 'os'
|
|
8
8
|
import path from 'path'
|
|
@@ -24,6 +24,16 @@ const args = process.argv.slice(2)
|
|
|
24
24
|
const subcommand = args[0]
|
|
25
25
|
const jsonFlag = args.includes('--json')
|
|
26
26
|
|
|
27
|
+
// Writing to a pipe is asynchronous in Node, and process.exit() throws away
|
|
28
|
+
// whatever is still buffered. Anything past the 64KiB pipe buffer was being cut
|
|
29
|
+
// mid-string, so `hskill status --json | jq` got exit code 0 and unparseable
|
|
30
|
+
// JSON. Every --json emitter goes through here, which resolves only once the
|
|
31
|
+
// write has actually landed.
|
|
32
|
+
async function emitJson(value) {
|
|
33
|
+
const text = JSON.stringify(value, null, 2) + '\n'
|
|
34
|
+
await new Promise(resolve => process.stdout.write(text, resolve))
|
|
35
|
+
}
|
|
36
|
+
|
|
27
37
|
// ── Help ─────────────────────────────────────────────────────────────────────
|
|
28
38
|
function printHelp() {
|
|
29
39
|
console.log(`
|
|
@@ -50,10 +60,12 @@ function printHelp() {
|
|
|
50
60
|
hskill uninstall <tool> uninstall a shell tool and clean up all files
|
|
51
61
|
hskill uninstall <tool> --yes skip all confirmations (incl. config files)
|
|
52
62
|
hskill uninstall <skill> --scope <s> --target <t> uninstall a skill
|
|
53
|
-
hskill update update hskill
|
|
63
|
+
hskill update update hskill (sticky: refreshes from whichever source is installed)
|
|
64
|
+
hskill update --local <path> switch to / refresh a local repo (npm pack + install)
|
|
65
|
+
hskill update --npm switch back to npm registry
|
|
54
66
|
hskill mcp start an MCP server (stdio) exposing hskill's tools to MCP-capable agent hosts
|
|
55
|
-
hskill version show version
|
|
56
|
-
hskill version --check compare
|
|
67
|
+
hskill version show version (adds source/branch/commit lines when installed from a local repo)
|
|
68
|
+
hskill version --check compare against npm registry, or against the local source repo's HEAD
|
|
57
69
|
hskill --help show this help
|
|
58
70
|
|
|
59
71
|
${chalk.cyan('Examples:')}
|
|
@@ -74,7 +86,7 @@ function printHelp() {
|
|
|
74
86
|
|
|
75
87
|
if (args[0] === '--help' || args[0] === '-h') {
|
|
76
88
|
if (jsonFlag || args.includes('--json')) {
|
|
77
|
-
|
|
89
|
+
await emitJson({
|
|
78
90
|
name: 'hskill',
|
|
79
91
|
version,
|
|
80
92
|
description: 'Skill manager for Claude Code, Cursor, Codex, OpenClaw, Hermes, OpenCode, and Pi',
|
|
@@ -138,7 +150,11 @@ if (args[0] === '--help' || args[0] === '-h') {
|
|
|
138
150
|
},
|
|
139
151
|
{
|
|
140
152
|
name: 'update',
|
|
141
|
-
description: 'Update hskill to
|
|
153
|
+
description: 'Update hskill; sticky to whichever source (npm or local repo) is currently installed',
|
|
154
|
+
flags: [
|
|
155
|
+
{ name: '--local', arg: '<path>', description: 'Switch to / refresh a local repo source (npm pack + install -g)' },
|
|
156
|
+
{ name: '--npm', description: 'Switch back to the npm registry source' },
|
|
157
|
+
],
|
|
142
158
|
},
|
|
143
159
|
{
|
|
144
160
|
name: 'mcp',
|
|
@@ -149,7 +165,7 @@ if (args[0] === '--help' || args[0] === '-h') {
|
|
|
149
165
|
description: 'Print version and exit',
|
|
150
166
|
},
|
|
151
167
|
],
|
|
152
|
-
}
|
|
168
|
+
})
|
|
153
169
|
process.exit(0)
|
|
154
170
|
}
|
|
155
171
|
printHelp()
|
|
@@ -157,12 +173,37 @@ if (args[0] === '--help' || args[0] === '-h') {
|
|
|
157
173
|
}
|
|
158
174
|
|
|
159
175
|
if (args[0] === '--version' || args[0] === '-v' || subcommand === 'version') {
|
|
176
|
+
const { readSource, gitInfo } = await import('../lib/install-source.js')
|
|
177
|
+
const source = readSource()
|
|
178
|
+
|
|
160
179
|
if (subcommand === 'version' && args.includes('--check')) {
|
|
180
|
+
if (source) {
|
|
181
|
+
let current
|
|
182
|
+
try {
|
|
183
|
+
current = gitInfo(source.repo)
|
|
184
|
+
} catch (err) {
|
|
185
|
+
console.error(chalk.red(` ✗ Could not read local source repo: ${err.message}`))
|
|
186
|
+
process.exit(1)
|
|
187
|
+
}
|
|
188
|
+
const upToDate = current.commit === source.commit && !current.dirty
|
|
189
|
+
if (jsonFlag) {
|
|
190
|
+
await emitJson({ source: 'local', repo: source.repo, installedCommit: source.commit, currentCommit: current.commit, dirty: current.dirty, upToDate })
|
|
191
|
+
} else if (upToDate) {
|
|
192
|
+
console.log(chalk.green(` ✔ hskill is up to date with local source (${source.repo}@${current.commit})`))
|
|
193
|
+
} else if (current.commit !== source.commit) {
|
|
194
|
+
console.log(chalk.yellow(` ⚠ local source has new commits: ${source.commit} → ${current.commit}`))
|
|
195
|
+
console.log(chalk.dim(' Run: hskill update'))
|
|
196
|
+
} else {
|
|
197
|
+
console.log(chalk.yellow(' ⚠ local source has uncommitted changes not yet packed'))
|
|
198
|
+
console.log(chalk.dim(' Run: hskill update'))
|
|
199
|
+
}
|
|
200
|
+
process.exit(0)
|
|
201
|
+
}
|
|
161
202
|
try {
|
|
162
203
|
const { checkNpmVersion } = await import('../lib/version-check.js')
|
|
163
204
|
const { current, latest, upToDate } = await checkNpmVersion('harveyz-skill', version)
|
|
164
205
|
if (jsonFlag) {
|
|
165
|
-
|
|
206
|
+
await emitJson({ current, latest, upToDate })
|
|
166
207
|
} else if (upToDate) {
|
|
167
208
|
console.log(chalk.green(` ✔ hskill v${current} is up to date`))
|
|
168
209
|
} else {
|
|
@@ -176,6 +217,11 @@ if (args[0] === '--version' || args[0] === '-v' || subcommand === 'version') {
|
|
|
176
217
|
process.exit(0)
|
|
177
218
|
}
|
|
178
219
|
console.log(version)
|
|
220
|
+
if (source) {
|
|
221
|
+
console.log('')
|
|
222
|
+
console.log(`source: local ${source.repo}`)
|
|
223
|
+
console.log(`branch: ${source.branch} commit: ${source.commit}${source.dirty ? ' (dirty)' : ''}`)
|
|
224
|
+
}
|
|
179
225
|
process.exit(0)
|
|
180
226
|
}
|
|
181
227
|
|
|
@@ -223,15 +269,112 @@ async function checkArchivedInstalls() {
|
|
|
223
269
|
}
|
|
224
270
|
|
|
225
271
|
// ── Update ───────────────────────────────────────────────────────────────────
|
|
226
|
-
|
|
272
|
+
// Sticky: whichever source is currently installed (recorded in .hskill-source.json
|
|
273
|
+
// inside the global install dir) is what a bare `update` refreshes from. `--local`
|
|
274
|
+
// and `--npm` explicitly switch the source. See docs/superpowers/specs/2026-09-15-hskill-install-source-design.md
|
|
275
|
+
async function updateToNpm(priorSource) {
|
|
227
276
|
console.log(chalk.dim(' · Updating hskill…'))
|
|
228
277
|
try {
|
|
229
278
|
execSync('npm install -g harveyz-skill@latest', { stdio: 'inherit' })
|
|
230
|
-
console.log(chalk.green(' ✔ hskill updated'))
|
|
231
279
|
} catch {
|
|
232
280
|
console.error(chalk.red(' ✗ Update failed. Try: npm install -g harveyz-skill@latest'))
|
|
233
281
|
process.exit(1)
|
|
234
282
|
}
|
|
283
|
+
if (priorSource) {
|
|
284
|
+
const { globalRoot } = await import('../lib/install-source.js')
|
|
285
|
+
const newVersion = JSON.parse(readFileSync(path.join(globalRoot(), 'harveyz-skill', 'package.json'), 'utf8')).version
|
|
286
|
+
console.log(` ${priorSource.version}+local (${priorSource.branch}@${priorSource.commit}) → ${newVersion} (npm)`)
|
|
287
|
+
}
|
|
288
|
+
console.log(chalk.green(' ✔ hskill updated'))
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
async function updateToLocal(repoPath, priorSource) {
|
|
292
|
+
if (!existsSync(repoPath)) {
|
|
293
|
+
console.error(chalk.red(` ✗ 本地来源仓库不存在:${repoPath}`))
|
|
294
|
+
console.error(chalk.dim(' 改用 npm: hskill update --npm'))
|
|
295
|
+
console.error(chalk.dim(` 指向新路径: hskill update --local <新路径>`))
|
|
296
|
+
process.exit(1)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const { gitInfo, writeSource, globalRoot } = await import('../lib/install-source.js')
|
|
300
|
+
// git info must be collected before `npm pack` — its `prepack` hook rewrites
|
|
301
|
+
// the tracked .npmignore, which would otherwise poison the dirty check.
|
|
302
|
+
const info = gitInfo(repoPath)
|
|
303
|
+
const origVersion = JSON.parse(readFileSync(path.join(repoPath, 'package.json'), 'utf8')).version
|
|
304
|
+
|
|
305
|
+
console.log(chalk.dim(' · Packing local repo…'))
|
|
306
|
+
const tmpDir = mkdtempSync(path.join(os.tmpdir(), 'hskill-pack-'))
|
|
307
|
+
const packResult = spawnSync('npm', ['pack', '--pack-destination', tmpDir], { cwd: repoPath, encoding: 'utf8' })
|
|
308
|
+
if (packResult.status !== 0) {
|
|
309
|
+
console.error(chalk.red(' ✗ npm pack failed'))
|
|
310
|
+
process.exit(1)
|
|
311
|
+
}
|
|
312
|
+
const tarballName = packResult.stdout.trim().split('\n').pop()
|
|
313
|
+
|
|
314
|
+
console.log(chalk.dim(' · Installing packed tarball…'))
|
|
315
|
+
const installResult = spawnSync('npm', ['install', '-g', path.join(tmpDir, tarballName)], { stdio: 'inherit' })
|
|
316
|
+
if (installResult.status !== 0) {
|
|
317
|
+
console.error(chalk.red(' ✗ Install failed'))
|
|
318
|
+
process.exit(1)
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
const installedPkgPath = path.join(globalRoot(), 'harveyz-skill', 'package.json')
|
|
322
|
+
const installedPkg = JSON.parse(readFileSync(installedPkgPath, 'utf8'))
|
|
323
|
+
installedPkg.version = `${origVersion}+local`
|
|
324
|
+
writeFileSync(installedPkgPath, JSON.stringify(installedPkg, null, 2) + '\n')
|
|
325
|
+
|
|
326
|
+
writeSource({
|
|
327
|
+
repo: repoPath,
|
|
328
|
+
branch: info.branch,
|
|
329
|
+
commit: info.commit,
|
|
330
|
+
dirty: info.dirty,
|
|
331
|
+
version: origVersion,
|
|
332
|
+
installedAt: new Date().toISOString(),
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
if (priorSource === null) {
|
|
336
|
+
console.log(` ${version} (npm) → ${origVersion}+local (${info.branch}@${info.commit})`)
|
|
337
|
+
} else if (priorSource.repo !== repoPath) {
|
|
338
|
+
console.log(` ${priorSource.version}+local (${priorSource.branch}@${priorSource.commit}) → ${origVersion}+local (${info.branch}@${info.commit})`)
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const npmignoreStatus = execSync('git status --porcelain -- .npmignore', { cwd: repoPath, encoding: 'utf8' }).trim()
|
|
342
|
+
if (npmignoreStatus) {
|
|
343
|
+
console.log(chalk.dim(' · .npmignore changed by prepack — left as-is, review with `git diff .npmignore`'))
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
console.log(chalk.green(' ✔ hskill updated'))
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (subcommand === 'update') {
|
|
350
|
+
const updateArgs = args.slice(1)
|
|
351
|
+
const localIdx = updateArgs.indexOf('--local')
|
|
352
|
+
const npmFlag = updateArgs.includes('--npm')
|
|
353
|
+
const localGiven = localIdx !== -1
|
|
354
|
+
const localPath = localGiven ? updateArgs[localIdx + 1] : undefined
|
|
355
|
+
|
|
356
|
+
if (localGiven && npmFlag) {
|
|
357
|
+
console.error(chalk.red(' ✗ --local and --npm are mutually exclusive'))
|
|
358
|
+
process.exit(1)
|
|
359
|
+
}
|
|
360
|
+
if (localGiven && (!localPath || localPath.startsWith('--'))) {
|
|
361
|
+
console.error(chalk.red(' ✗ --local requires a path: hskill update --local <path>'))
|
|
362
|
+
process.exit(1)
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
const { readSource } = await import('../lib/install-source.js')
|
|
366
|
+
const priorSource = readSource()
|
|
367
|
+
|
|
368
|
+
if (npmFlag) {
|
|
369
|
+
await updateToNpm(priorSource)
|
|
370
|
+
} else if (localGiven) {
|
|
371
|
+
await updateToLocal(localPath, priorSource)
|
|
372
|
+
} else if (priorSource) {
|
|
373
|
+
await updateToLocal(priorSource.repo, priorSource)
|
|
374
|
+
} else {
|
|
375
|
+
await updateToNpm(priorSource)
|
|
376
|
+
}
|
|
377
|
+
|
|
235
378
|
// Run skill rename migrations
|
|
236
379
|
const { renames = [], skills: skillDefs = [] } = require('../skills-index.json')
|
|
237
380
|
if (renames.length > 0) {
|
|
@@ -276,10 +419,10 @@ if (subcommand === 'list') {
|
|
|
276
419
|
const { skills, tools = [] } = require('../skills-index.json')
|
|
277
420
|
const sorted = [...skills].sort((a, b) => a.bundle.localeCompare(b.bundle) || a.path.split('/').pop().localeCompare(b.path.split('/').pop()))
|
|
278
421
|
if (jsonFlag) {
|
|
279
|
-
|
|
422
|
+
await emitJson({
|
|
280
423
|
skills: sorted.map(s => ({ name: s.path.split('/').pop(), path: s.path, bundle: s.bundle, global: s.global ?? false })),
|
|
281
424
|
tools: tools.map(t => t.name),
|
|
282
|
-
}
|
|
425
|
+
})
|
|
283
426
|
process.exit(0)
|
|
284
427
|
}
|
|
285
428
|
const nw = Math.max(...sorted.map(s => s.path.split('/').pop().length), 4)
|
|
@@ -380,12 +523,12 @@ if (subcommand === 'status' || subcommand === 'outdated') {
|
|
|
380
523
|
return { name: h.name, description: h.description, user: inst.user, project: inst.project }
|
|
381
524
|
})
|
|
382
525
|
if (outdatedOnly) {
|
|
383
|
-
|
|
526
|
+
await emitJson({
|
|
384
527
|
skills: jsonSkills.filter(s => Object.values(s.user).some(v => v.status === 'update') || Object.values(s.project).some(v => v.status === 'update')),
|
|
385
528
|
tools: jsonTools.filter(t => t.status === 'update'),
|
|
386
|
-
}
|
|
529
|
+
})
|
|
387
530
|
} else {
|
|
388
|
-
|
|
531
|
+
await emitJson({ skills: jsonSkills, tools: jsonTools, hooks: jsonHooks })
|
|
389
532
|
}
|
|
390
533
|
process.exit(0)
|
|
391
534
|
}
|
|
@@ -513,17 +656,17 @@ if (subcommand === 'info') {
|
|
|
513
656
|
if (jsonFlag) {
|
|
514
657
|
if (skill) {
|
|
515
658
|
const inst = checkInstalled(skill.skillName, skill.version ?? '—')
|
|
516
|
-
|
|
659
|
+
await emitJson({
|
|
517
660
|
name: skill.skillName, type: 'skill', version: skill.version ?? '—',
|
|
518
661
|
user: Object.fromEntries(targets.map(t => [t, inst.user[t]])),
|
|
519
662
|
project: Object.fromEntries(targets.map(t => [t, inst.project[t]])),
|
|
520
|
-
}
|
|
663
|
+
})
|
|
521
664
|
} else {
|
|
522
665
|
const inst = checkToolInstalled(tool.toolName, tool.srcPath)
|
|
523
|
-
|
|
666
|
+
await emitJson({
|
|
524
667
|
name: tool.toolName, type: 'tool', version: tool.version ?? '—',
|
|
525
668
|
installed: inst,
|
|
526
|
-
}
|
|
669
|
+
})
|
|
527
670
|
}
|
|
528
671
|
process.exit(0)
|
|
529
672
|
}
|
|
@@ -590,7 +733,7 @@ if (subcommand === 'uninstall') {
|
|
|
590
733
|
const { removed, failed } = await uninstallTool(nameToRemove, { yes: yesFlag })
|
|
591
734
|
if (jsonFlag) {
|
|
592
735
|
console.error = originalError
|
|
593
|
-
|
|
736
|
+
await emitJson({ removed: removed.length > 0, failed: failed.length > 0 })
|
|
594
737
|
} else if (removed.length > 0) {
|
|
595
738
|
console.error(chalk.green.bold(`✔ ${nameToRemove} uninstalled`))
|
|
596
739
|
}
|
|
@@ -617,7 +760,7 @@ if (subcommand === 'uninstall') {
|
|
|
617
760
|
}
|
|
618
761
|
if (jsonFlag) {
|
|
619
762
|
console.error = originalError2
|
|
620
|
-
|
|
763
|
+
await emitJson({ removed: anyRemoved, failed: anyFailed })
|
|
621
764
|
} else if (anyRemoved) {
|
|
622
765
|
console.error(chalk.green.bold(`✔ ${nameToRemove} uninstalled`))
|
|
623
766
|
}
|
|
@@ -663,7 +806,7 @@ if (subcommand === 'hooks') {
|
|
|
663
806
|
codex: inst.codex,
|
|
664
807
|
}
|
|
665
808
|
})
|
|
666
|
-
|
|
809
|
+
await emitJson({ hooks: out })
|
|
667
810
|
process.exit(0)
|
|
668
811
|
}
|
|
669
812
|
function hookIcon(s) {
|
|
@@ -724,7 +867,7 @@ if (subcommand === 'hooks') {
|
|
|
724
867
|
const { installed, skipped, failed } = await installHooksForTarget(toInstall, hookTargetArg, hookScopeArg, hookProjectArg, hookForce)
|
|
725
868
|
|
|
726
869
|
if (hookJsonFlag) {
|
|
727
|
-
|
|
870
|
+
await emitJson({ installed, skipped, failed })
|
|
728
871
|
process.exit(failed.length ? 1 : 0)
|
|
729
872
|
} else {
|
|
730
873
|
if (installed.length) console.error(chalk.green.bold(`✔ Hooks installed (${hookScopeArg}):`), installed.join(', '))
|
|
@@ -746,7 +889,7 @@ if (subcommand === 'hooks') {
|
|
|
746
889
|
}
|
|
747
890
|
const { removed } = await uninstallHook(nameToRemove, hookScopeArg, hookProjectArg)
|
|
748
891
|
if (hookJsonFlag) {
|
|
749
|
-
|
|
892
|
+
await emitJson({ removed })
|
|
750
893
|
} else if (!removed) {
|
|
751
894
|
console.log(chalk.dim(` · ${nameToRemove} was not installed in ${hookScopeArg} scope`))
|
|
752
895
|
}
|
|
@@ -798,9 +941,9 @@ if (subcommand === 'upgrade') {
|
|
|
798
941
|
const nothingUpgraded = Object.keys(summary).length === 0
|
|
799
942
|
if (jsonFlag) {
|
|
800
943
|
if (nothingUpgraded) {
|
|
801
|
-
|
|
944
|
+
await emitJson({ skills: {}, upToDate: true })
|
|
802
945
|
} else {
|
|
803
|
-
|
|
946
|
+
await emitJson({ skills: summary })
|
|
804
947
|
}
|
|
805
948
|
} else {
|
|
806
949
|
if (nothingUpgraded) {
|
|
@@ -1512,7 +1655,7 @@ try {
|
|
|
1512
1655
|
const out = {}
|
|
1513
1656
|
if (skillSummary !== null) out.skills = skillSummary
|
|
1514
1657
|
if (toolSummary !== null) out.tools = toolSummary
|
|
1515
|
-
|
|
1658
|
+
await emitJson(out)
|
|
1516
1659
|
} else {
|
|
1517
1660
|
printSummary(skillSummary, toolSummary)
|
|
1518
1661
|
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { execSync } from 'child_process'
|
|
2
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
|
|
5
|
+
let cachedGlobalRoot = null
|
|
6
|
+
|
|
7
|
+
export function globalRoot() {
|
|
8
|
+
if (process.env.HSKILL_GLOBAL_ROOT) return process.env.HSKILL_GLOBAL_ROOT
|
|
9
|
+
// stdio 'pipe' keeps npm's own stderr off ours: callers that tolerate failure
|
|
10
|
+
// (readSource) would otherwise leak "npm: command not found" into clean output.
|
|
11
|
+
if (!cachedGlobalRoot) cachedGlobalRoot = execSync('npm root -g', { encoding: 'utf8', stdio: 'pipe' }).trim()
|
|
12
|
+
return cachedGlobalRoot
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function sourceFilePath() {
|
|
16
|
+
return path.join(globalRoot(), 'harveyz-skill', '.hskill-source.json')
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// null means "npm source" — the default. An unreadable trace file and an
|
|
20
|
+
// unreachable npm both collapse into it: `version` must keep working where npm
|
|
21
|
+
// is off PATH, and a source we cannot read is not a source we can update from.
|
|
22
|
+
export function readSource() {
|
|
23
|
+
let file
|
|
24
|
+
try {
|
|
25
|
+
file = sourceFilePath()
|
|
26
|
+
} catch {
|
|
27
|
+
return null
|
|
28
|
+
}
|
|
29
|
+
if (!existsSync(file)) return null
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(readFileSync(file, 'utf8'))
|
|
32
|
+
} catch {
|
|
33
|
+
return null
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function writeSource(info) {
|
|
38
|
+
writeFileSync(sourceFilePath(), JSON.stringify(info, null, 2) + '\n')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function gitInfo(repo) {
|
|
42
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: repo, encoding: 'utf8' }).trim()
|
|
43
|
+
const commit = execSync('git rev-parse --short HEAD', { cwd: repo, encoding: 'utf8' }).trim()
|
|
44
|
+
const dirty = execSync('git status --porcelain', { cwd: repo, encoding: 'utf8' }).trim().length > 0
|
|
45
|
+
return { branch, commit, dirty }
|
|
46
|
+
}
|
package/lib/version-check.js
CHANGED
|
@@ -2,8 +2,8 @@ const DEFAULT_REGISTRY = 'https://registry.npmjs.org'
|
|
|
2
2
|
const REGISTRY_TIMEOUT_MS = 5000
|
|
3
3
|
|
|
4
4
|
export function compareVersions(a, b) {
|
|
5
|
-
const pa = a.split('.').map(Number)
|
|
6
|
-
const pb = b.split('.').map(Number)
|
|
5
|
+
const pa = a.split('+')[0].split('.').map(Number)
|
|
6
|
+
const pb = b.split('+')[0].split('.').map(Number)
|
|
7
7
|
for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
|
|
8
8
|
const diff = (pa[i] || 0) - (pb[i] || 0)
|
|
9
9
|
if (diff !== 0) return diff
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "harveyz-skill",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.34.0",
|
|
4
4
|
"description": "Skill manager for Claude Code, Cursor, Codex, OpenClaw, Hermes, OpenCode, and Pi",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -62,6 +62,12 @@
|
|
|
62
62
|
"skills/coding/handoff/",
|
|
63
63
|
"skills/feed/manage-creators/",
|
|
64
64
|
"skills/feed/capture-opinion/",
|
|
65
|
+
"skills/agent-canvas/agent-canvas-control/",
|
|
66
|
+
"skills/agent-canvas/describe-node/",
|
|
67
|
+
"skills/agent-canvas/relate-node/",
|
|
68
|
+
"skills/agent-canvas/capture-requirement/",
|
|
69
|
+
"skills/agent-canvas/relation-review/",
|
|
70
|
+
"skills/agent-canvas/close-node/",
|
|
65
71
|
"tools/hub/",
|
|
66
72
|
"tools/sync-agent/",
|
|
67
73
|
"tools/browser-fetch/",
|