harveyz-skill 0.33.0 → 0.35.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 +20 -0
- package/README.md +31 -2
- package/bin/cli.js +219 -47
- package/lib/bundles.js +12 -4
- package/lib/install-source.js +46 -0
- package/lib/mcp-server.js +2 -2
- package/lib/version-check.js +2 -2
- package/package.json +14 -3
- package/skills/agent-canvas/agent-canvas-control/SKILL.md +236 -0
- package/skills/agent-canvas/capture-requirement/SKILL.md +52 -0
- package/skills/agent-canvas/close-node/SKILL.md +118 -0
- package/skills/agent-canvas/describe-node/SKILL.md +52 -0
- package/skills/agent-canvas/relate-node/SKILL.md +64 -0
- package/skills/agent-canvas/relation-review/SKILL.md +120 -0
- package/skills/coding/analyze-claudemd/SKILL.md +114 -0
- package/skills/coding/analyze-claudemd/references/criteria.md +142 -0
- package/skills/coding/capture-vocab/SKILL.md +20 -74
- package/skills/coding/capture-vocab/references/manage.md +115 -0
- package/skills/coding/capture-vocab/references/project-setup.md +31 -0
- package/skills/coding/capture-vocab/scripts/__pycache__/vocab.cpython-314.pyc +0 -0
- package/skills/coding/capture-vocab/scripts/vocab.py +207 -0
- package/skills/coding/capture-vocab/tests/__pycache__/test_vocab.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/coding/capture-vocab/tests/fixtures/.hskill/capture-vocab/vocab.md +245 -0
- package/skills/coding/capture-vocab/tests/test_vocab.py +159 -0
- package/skills/coding/init-project/SKILL.md +157 -0
- package/skills/coding/init-project/assets/skeleton/CLAUDE.md +17 -0
- package/skills/coding/init-project/assets/skeleton/README.md +12 -0
- package/skills/coding/init-project/assets/skeleton/TODO.md +9 -0
- package/skills/coding/init-project/assets/skeleton/gitignore-code +19 -0
- package/skills/coding/init-project/assets/templates/code.yml +37 -0
- package/skills/coding/init-project/references/check-report.md +55 -0
- package/skills/coding/init-project/references/template-schema.md +63 -0
- package/skills/feed/manage-creators/SKILL.md +10 -2
- package/skills/feed/sync-website/scripts/__pycache__/store_config.cpython-314.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-xtimeline/scripts/__pycache__/store_config.cpython-314.pyc +0 -0
- package/skills/feed/sync-xtimeline/tests/__pycache__/test_store_config.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/feed/sync-ytchannel/scripts/__pycache__/store_config.cpython-314.pyc +0 -0
- package/skills/feed/sync-ytchannel/tests/__pycache__/test_store_config.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/research/clip-url/scripts/__pycache__/store_config.cpython-314.pyc +0 -0
- package/skills/research/clip-url/tests/__pycache__/test_store_config.cpython-314-pytest-9.1.1.pyc +0 -0
- package/skills/research/learn-video/SKILL.md +14 -7
- package/skills/research/learn-video/scripts/__pycache__/archive.cpython-314.pyc +0 -0
- package/skills/research/learn-video/scripts/__pycache__/build_creator_index.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/archive.py +21 -57
- package/skills/research/learn-video/scripts/build_creator_index.py +136 -0
- 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_build_creator_index.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_archive.py +61 -108
- package/skills/research/learn-video/tests/test_build_creator_index.py +192 -0
- package/skills-index.json +57 -8
- package/tools/roster/roster/__init__.py +1 -1
- package/tools/roster/roster/__main__.py +33 -3
- package/tools/roster/roster/__pycache__/__init__.cpython-314.pyc +0 -0
- package/tools/roster/roster/__pycache__/__main__.cpython-314.pyc +0 -0
- package/tools/roster/roster/__pycache__/migrate_schema.cpython-314.pyc +0 -0
- package/tools/roster/roster/__pycache__/registry.cpython-314.pyc +0 -0
- package/tools/roster/roster/__pycache__/urls.cpython-314.pyc +0 -0
- package/tools/roster/roster/migrate_schema.py +52 -0
- package/tools/roster/roster/registry.py +7 -3
- package/tools/roster/roster/urls.py +5 -1
- package/tools/roster/tests/__pycache__/test_cli.cpython-314-pytest-9.1.1.pyc +0 -0
- package/tools/roster/tests/__pycache__/test_config.cpython-314-pytest-9.1.1.pyc +0 -0
- package/tools/roster/tests/__pycache__/test_migrate_schema.cpython-314-pytest-9.1.1.pyc +0 -0
- package/tools/roster/tests/__pycache__/test_registry.cpython-314-pytest-9.1.1.pyc +0 -0
- package/tools/roster/tests/__pycache__/test_registry_merge.cpython-314-pytest-9.1.1.pyc +0 -0
- package/tools/roster/tests/__pycache__/test_state.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/tools/roster/tests/test_cli.py +28 -1
- package/tools/roster/tests/test_config.py +2 -2
- package/tools/roster/tests/test_migrate_schema.py +157 -0
- package/tools/roster/tests/test_registry.py +33 -2
- package/tools/roster/tests/test_registry_merge.py +11 -0
- package/tools/roster/tests/test_state.py +26 -0
- package/tools/roster/tests/test_urls.py +15 -0
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [0.35.0] - 2026-09-21
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
- `learn-video`:新增 `build_creator_index.py`(`build`/`check` 两个子命令)。扫描 vdl 写出的全部 `meta.json`,按归一化后的 YouTube handle(去 `@`、转小写)分组,产出 `<knowledgeRoot>/videos/creators.json`。不读、不写 `registry.json`——"是不是在关注这个人"是判断,判断不落盘在事实旁边,完全交给下游(scholia)现查决定,索引本身永远不含 `watched`/`creator_id`。原子写(临时文件 + rename),构建失败旧索引原样保留;单个 `meta.json` 损坏时归入 `unresolved` 而不是从统计里静默消失;`work` 目录不存在(vdl 的 `WORK_ROOT` 漂了)时报错退出,不会把一个正常的索引悄悄替换成空索引
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- **破坏性变更** `learn-video`:`archive.py` 从"写 `meta.json`"降级为"校验 `meta.json`"。vdl(Video-Learner 仓库)现在自己把 `uploader_id`/`channel_id`/`uploader_url` 三个新字段与统一存储契约的三个必填字段(`source_url`/`title`/`fetched_at`)全部写好,`archive.py` 只剩读一遍 `meta.json` 核对必填字段是否齐全,不再从 vdl 的 sqlite 捞取任何展示字段,也不再写文件。CLI 入参从 `TASK_ID`/`SOURCE_URL`/`TITLE`/`FETCHED_AT` 四个环境变量精简到只剩 `TASK_ID`。version 1.8.2 → 1.9.0
|
|
17
|
+
- **升级路径**:这是 vdl 那一侧先落地的改动的下游收尾。若本机 vdl 版本落后(写出的 `meta.json` 还没有新增字段/契约字段),归档步骤会直接报错停下并列出缺的字段名,不会再像过去那样悄悄补一份缺字段的 `meta.json`。看到这个报错,先确认 Video-Learner 是否需要升级
|
|
18
|
+
|
|
19
|
+
## [0.34.0] - 2026-09-16
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- `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`
|
|
23
|
+
- `agent-canvas` bundle:新增 `agent-canvas-control`、`capture-requirement`、`close-node`、`describe-node`、`relate-node`、`relation-review` 六个 skill,在 Agent Canvas 画布节点里操控节点实体、摆放画布、梳理节点间语义关系
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
- `hskill <任意子命令> --json` 在输出走管道时被截断:Node 写管道是异步的,而每个子命令块以 `process.exit()` 结尾,不等缓冲区排空。`status --json` 的输出刚随 agent-canvas 六个 skill 涨到 66,739 字节、越过 64KiB 管道缓冲区,于是 `hskill status --json | jq` 拿到退出码 0 和一段从字符串中间断掉的 JSON——**静默失败**,退出码还宣称成功。16 处 `--json` 出口统一改走等待写入落地的 `emitJson()`;回归测试逐字节比对管道输出与直接重定向输出,且在负载跌回缓冲区以下时显式 skip 而非假绿
|
|
27
|
+
- `hskill version` / `hskill --version` 在 npm 不在 PATH 上时抛原始堆栈崩溃(cron、受限 PATH、精简镜像):来源追踪让 `readSource()` 每次都经 `globalRoot()` 打一次 `npm root -g`,而它只兜住了 JSON 解析失败。现在 `globalRoot()` 的失败并入同一个 `null`,即回落到缺省的 npm 来源
|
|
28
|
+
- `compareVersions()` 遇到 semver build metadata(`0.33.0+local`)会解析出 `NaN` 导致比较结果无意义,现按规范在比较前截断 `+` 及其后内容
|
|
29
|
+
|
|
10
30
|
## [0.33.0] - 2026-09-15
|
|
11
31
|
|
|
12
32
|
### Added
|
package/README.md
CHANGED
|
@@ -96,14 +96,43 @@ hskill install --bundle dev # 安装整个 bundle
|
|
|
96
96
|
hskill install --skill git-workflow-init # 安装单个 skill
|
|
97
97
|
hskill install --tool hub # 安装 shell 工具
|
|
98
98
|
hskill list # 查看可用 skill
|
|
99
|
-
hskill update #
|
|
99
|
+
hskill update # 更新 hskill 自身(沿用当前来源)
|
|
100
|
+
hskill version # 查看版本与安装来源
|
|
100
101
|
hskill --help # 查看帮助
|
|
101
102
|
```
|
|
102
103
|
|
|
103
104
|
Skills 安装到 `~/.claude/skills/`,shell 工具安装到 `~/.local/bin/`。
|
|
104
105
|
|
|
106
|
+
**从本地仓库安装 hskill 自身**
|
|
107
|
+
|
|
108
|
+
发版前想先用一阵子工作树里的改动,可以把 hskill 装成本地来源:
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
hskill update --local ~/Projects/harveyz-skill # 从本地仓库打包安装
|
|
112
|
+
hskill update # 之后裸跑就一直走本地,不会跳回 npm
|
|
113
|
+
hskill update --npm # 显式切回 npm registry
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
`--local` 走的是 `npm pack` + 安装 tarball,**不是软链**——产出独立快照,并且真实执行 `prepack` 与 `package.json` 的 `files[]` 白名单,等于在发 npm 之前预演一次真实发布,能提前暴露打包问题。
|
|
117
|
+
|
|
118
|
+
两种来源共存于同一个命令,`update` 是**粘性**的:本地装的就从原仓库重新打包,npm 装的就走 registry,**裸 `update` 永远不跨来源**。跨来源只能由 `--local` / `--npm` 显式触发,触发时会打印完整迁移行。
|
|
119
|
+
|
|
120
|
+
安装来源记在全局安装目录里(`.hskill-source.json` + 版本号后缀 `+local`)。因为任何一次 `npm install -g` 都会清空重建该目录,绕过 hskill 手工安装也会自动抹掉痕迹,所以记录的来源不会与实际情况脱节:
|
|
121
|
+
|
|
122
|
+
```console
|
|
123
|
+
$ hskill version
|
|
124
|
+
0.34.0+local
|
|
125
|
+
|
|
126
|
+
source: local /Users/you/Projects/harveyz-skill
|
|
127
|
+
branch: staging commit: 3d4192a
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
本地来源下 `hskill version --check` 比对的是**记录的 commit 与仓库 HEAD**,不是版本号——开发分支上版本号常常几十个提交不动,能动的只有 commit。
|
|
131
|
+
|
|
105
132
|
**本地源码开发**
|
|
106
133
|
|
|
134
|
+
不安装、直接从仓库跑 CLI 来装 skill:
|
|
135
|
+
|
|
107
136
|
```bash
|
|
108
137
|
node bin/cli.js install --skill mermaid-diagram --target claude
|
|
109
138
|
node bin/cli.js install --bundle dev --target claude
|
|
@@ -149,7 +178,7 @@ hskill hooks uninstall check-similar-branch --scope project # 从当前项目
|
|
|
149
178
|
| **hskill_install** | 安装一个 bundle、指定 skill 或 shell 工具 |
|
|
150
179
|
| **hskill_uninstall** | 卸载指定的 skill 或 shell 工具 |
|
|
151
180
|
| **hskill_hooks** | 查看、安装或卸载 hskill git hooks |
|
|
152
|
-
| **hskill_update** |
|
|
181
|
+
| **hskill_update** | 更新 hskill 自身,沿用当前安装来源(npm 或本地仓库);切换来源需用 CLI 的 `--npm` / `--local` |
|
|
153
182
|
|
|
154
183
|
在 MCP host 的配置中注册(假设已通过 `npm install -g harveyz-skill` 全局安装):
|
|
155
184
|
|
package/bin/cli.js
CHANGED
|
@@ -2,14 +2,14 @@
|
|
|
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'
|
|
9
9
|
import { fileURLToPath } from 'url'
|
|
10
10
|
import {
|
|
11
11
|
getAllSkillItems, getAllToolItems, getAllHookItems, checkHookInstalled,
|
|
12
|
-
checkInstalled, checkToolInstalled, scopeSummary,
|
|
12
|
+
checkInstalled, checkToolInstalled, scopeSummary, toSkillItems,
|
|
13
13
|
resolveSkills, resolveSkillsByName, resolveTools, resolveToolsByName,
|
|
14
14
|
TOOL_BUNDLE_CHOICES,
|
|
15
15
|
} from '../lib/bundles.js'
|
|
@@ -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,22 +269,138 @@ 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
|
+
|
|
378
|
+
// Re-require fresh: the self-update above just replaced this install
|
|
379
|
+
// directory's skills-index.json on disk, but bin/cli.js's own top-level
|
|
380
|
+
// import of lib/bundles.js already cached the *old* skillDefs at process
|
|
381
|
+
// start. A plain require() here is the first hit for this path in this
|
|
382
|
+
// process, so it reads what's actually on disk now.
|
|
383
|
+
const { renames = [], skills: freshSkillDefs = [] } = require('../skills-index.json')
|
|
384
|
+
const skillsRoot = path.join(__dirname, '..', 'skills')
|
|
385
|
+
|
|
386
|
+
// Auto-upgrade skills opted into `autoUpdate: true` (skills-index.json),
|
|
387
|
+
// scoped to `user` to match `hskill upgrade`'s default scope.
|
|
388
|
+
const autoUpdateRows = buildSkillRows(null, toSkillItems(freshSkillDefs, skillsRoot))
|
|
389
|
+
.filter(r => r.autoUpdate)
|
|
390
|
+
if (autoUpdateRows.length > 0) {
|
|
391
|
+
const autoTargets = resolveTargets(['all'], 'user')
|
|
392
|
+
const autoSummary = await upgradeOutdatedSkills(autoUpdateRows, autoTargets, 'userDetail')
|
|
393
|
+
const upgraded = [...new Set(Object.values(autoSummary).flatMap(s => s.installed ?? []))]
|
|
394
|
+
const failed = [...new Set(Object.values(autoSummary).flatMap(s => (s.failed ?? []).map(f => f.name)))]
|
|
395
|
+
if (upgraded.length > 0) console.log(chalk.green(` ✔ Auto-updated ${upgraded.length} skill(s): ${upgraded.join(', ')}`))
|
|
396
|
+
if (failed.length > 0) console.log(chalk.yellow(` ⚠ Auto-update failed for: ${failed.join(', ')}`))
|
|
397
|
+
}
|
|
398
|
+
|
|
235
399
|
// Run skill rename migrations
|
|
236
|
-
const { renames = [], skills: skillDefs = [] } = require('../skills-index.json')
|
|
237
400
|
if (renames.length > 0) {
|
|
238
401
|
console.log(chalk.dim(' · Migrating renamed skills…'))
|
|
239
402
|
const targets = SKILL_TARGETS.map(name => ({ name, dir: userSkillDir(name) }))
|
|
240
|
-
const
|
|
241
|
-
const migrationSummary = await migrateRenamedSkills(renames, targets, skillsRoot, skillDefs)
|
|
403
|
+
const migrationSummary = await migrateRenamedSkills(renames, targets, skillsRoot, freshSkillDefs)
|
|
242
404
|
const totalMigrated = Object.values(migrationSummary).reduce((n, s) => n + s.migrated.length, 0)
|
|
243
405
|
const totalFailed = Object.values(migrationSummary).reduce((n, s) => n + s.failed.length, 0)
|
|
244
406
|
if (totalMigrated > 0) console.log(chalk.green(` ✔ Migrated ${totalMigrated} skill(s)`))
|
|
@@ -276,10 +438,10 @@ if (subcommand === 'list') {
|
|
|
276
438
|
const { skills, tools = [] } = require('../skills-index.json')
|
|
277
439
|
const sorted = [...skills].sort((a, b) => a.bundle.localeCompare(b.bundle) || a.path.split('/').pop().localeCompare(b.path.split('/').pop()))
|
|
278
440
|
if (jsonFlag) {
|
|
279
|
-
|
|
441
|
+
await emitJson({
|
|
280
442
|
skills: sorted.map(s => ({ name: s.path.split('/').pop(), path: s.path, bundle: s.bundle, global: s.global ?? false })),
|
|
281
443
|
tools: tools.map(t => t.name),
|
|
282
|
-
}
|
|
444
|
+
})
|
|
283
445
|
process.exit(0)
|
|
284
446
|
}
|
|
285
447
|
const nw = Math.max(...sorted.map(s => s.path.split('/').pop().length), 4)
|
|
@@ -310,10 +472,11 @@ function resolveHookDisplayVersion(inst, sourceVersion) {
|
|
|
310
472
|
}
|
|
311
473
|
|
|
312
474
|
// ── Shared skill scan ─────────────────────────────────────────────────────────
|
|
313
|
-
function buildSkillRows(nameFilter = null) {
|
|
475
|
+
function buildSkillRows(nameFilter = null, sourceItems = null) {
|
|
476
|
+
const allItems = sourceItems ?? getAllSkillItems()
|
|
314
477
|
const items = nameFilter
|
|
315
|
-
?
|
|
316
|
-
:
|
|
478
|
+
? allItems.filter(s => s.skillName === nameFilter)
|
|
479
|
+
: allItems
|
|
317
480
|
return items.map(s => {
|
|
318
481
|
const inst = checkInstalled(s.skillName, s.version ?? '—')
|
|
319
482
|
return {
|
|
@@ -321,6 +484,7 @@ function buildSkillRows(nameFilter = null) {
|
|
|
321
484
|
bundle: s.bundle ?? '—',
|
|
322
485
|
version: s.version ?? '—',
|
|
323
486
|
installScope: s.installScope ?? null,
|
|
487
|
+
autoUpdate: s.autoUpdate ?? false,
|
|
324
488
|
srcPath: s.srcPath,
|
|
325
489
|
userStatus: scopeSummary(inst.user),
|
|
326
490
|
projectStatus: scopeSummary(inst.project),
|
|
@@ -330,6 +494,26 @@ function buildSkillRows(nameFilter = null) {
|
|
|
330
494
|
})
|
|
331
495
|
}
|
|
332
496
|
|
|
497
|
+
// Shared by `upgrade` and the post-`update` auto-upgrade step: for each
|
|
498
|
+
// target, install whichever rows are outdated (status === 'update') on that
|
|
499
|
+
// target's scope. Rows not carrying the given scope's detail are left alone.
|
|
500
|
+
async function upgradeOutdatedSkills(rows, targetList, scopeKey) {
|
|
501
|
+
const summary = {}
|
|
502
|
+
for (const { name: targetName, dir } of targetList) {
|
|
503
|
+
const upgradeList = rows
|
|
504
|
+
.filter(r => r[scopeKey]?.[targetName]?.status === 'update')
|
|
505
|
+
.map(r => ({ skillName: r.name, srcPath: r.srcPath, version: r.version }))
|
|
506
|
+
|
|
507
|
+
if (!upgradeList.length) continue
|
|
508
|
+
|
|
509
|
+
console.log('')
|
|
510
|
+
const result = await installSkills(upgradeList, [{ name: targetName, dir }], true)
|
|
511
|
+
Object.assign(summary, result)
|
|
512
|
+
console.log('')
|
|
513
|
+
}
|
|
514
|
+
return summary
|
|
515
|
+
}
|
|
516
|
+
|
|
333
517
|
// ── Status / Outdated ─────────────────────────────────────────────────────────
|
|
334
518
|
if (subcommand === 'status' || subcommand === 'outdated') {
|
|
335
519
|
const outdatedOnly = subcommand === 'outdated'
|
|
@@ -380,12 +564,12 @@ if (subcommand === 'status' || subcommand === 'outdated') {
|
|
|
380
564
|
return { name: h.name, description: h.description, user: inst.user, project: inst.project }
|
|
381
565
|
})
|
|
382
566
|
if (outdatedOnly) {
|
|
383
|
-
|
|
567
|
+
await emitJson({
|
|
384
568
|
skills: jsonSkills.filter(s => Object.values(s.user).some(v => v.status === 'update') || Object.values(s.project).some(v => v.status === 'update')),
|
|
385
569
|
tools: jsonTools.filter(t => t.status === 'update'),
|
|
386
|
-
}
|
|
570
|
+
})
|
|
387
571
|
} else {
|
|
388
|
-
|
|
572
|
+
await emitJson({ skills: jsonSkills, tools: jsonTools, hooks: jsonHooks })
|
|
389
573
|
}
|
|
390
574
|
process.exit(0)
|
|
391
575
|
}
|
|
@@ -513,17 +697,17 @@ if (subcommand === 'info') {
|
|
|
513
697
|
if (jsonFlag) {
|
|
514
698
|
if (skill) {
|
|
515
699
|
const inst = checkInstalled(skill.skillName, skill.version ?? '—')
|
|
516
|
-
|
|
700
|
+
await emitJson({
|
|
517
701
|
name: skill.skillName, type: 'skill', version: skill.version ?? '—',
|
|
518
702
|
user: Object.fromEntries(targets.map(t => [t, inst.user[t]])),
|
|
519
703
|
project: Object.fromEntries(targets.map(t => [t, inst.project[t]])),
|
|
520
|
-
}
|
|
704
|
+
})
|
|
521
705
|
} else {
|
|
522
706
|
const inst = checkToolInstalled(tool.toolName, tool.srcPath)
|
|
523
|
-
|
|
707
|
+
await emitJson({
|
|
524
708
|
name: tool.toolName, type: 'tool', version: tool.version ?? '—',
|
|
525
709
|
installed: inst,
|
|
526
|
-
}
|
|
710
|
+
})
|
|
527
711
|
}
|
|
528
712
|
process.exit(0)
|
|
529
713
|
}
|
|
@@ -590,7 +774,7 @@ if (subcommand === 'uninstall') {
|
|
|
590
774
|
const { removed, failed } = await uninstallTool(nameToRemove, { yes: yesFlag })
|
|
591
775
|
if (jsonFlag) {
|
|
592
776
|
console.error = originalError
|
|
593
|
-
|
|
777
|
+
await emitJson({ removed: removed.length > 0, failed: failed.length > 0 })
|
|
594
778
|
} else if (removed.length > 0) {
|
|
595
779
|
console.error(chalk.green.bold(`✔ ${nameToRemove} uninstalled`))
|
|
596
780
|
}
|
|
@@ -617,7 +801,7 @@ if (subcommand === 'uninstall') {
|
|
|
617
801
|
}
|
|
618
802
|
if (jsonFlag) {
|
|
619
803
|
console.error = originalError2
|
|
620
|
-
|
|
804
|
+
await emitJson({ removed: anyRemoved, failed: anyFailed })
|
|
621
805
|
} else if (anyRemoved) {
|
|
622
806
|
console.error(chalk.green.bold(`✔ ${nameToRemove} uninstalled`))
|
|
623
807
|
}
|
|
@@ -663,7 +847,7 @@ if (subcommand === 'hooks') {
|
|
|
663
847
|
codex: inst.codex,
|
|
664
848
|
}
|
|
665
849
|
})
|
|
666
|
-
|
|
850
|
+
await emitJson({ hooks: out })
|
|
667
851
|
process.exit(0)
|
|
668
852
|
}
|
|
669
853
|
function hookIcon(s) {
|
|
@@ -724,7 +908,7 @@ if (subcommand === 'hooks') {
|
|
|
724
908
|
const { installed, skipped, failed } = await installHooksForTarget(toInstall, hookTargetArg, hookScopeArg, hookProjectArg, hookForce)
|
|
725
909
|
|
|
726
910
|
if (hookJsonFlag) {
|
|
727
|
-
|
|
911
|
+
await emitJson({ installed, skipped, failed })
|
|
728
912
|
process.exit(failed.length ? 1 : 0)
|
|
729
913
|
} else {
|
|
730
914
|
if (installed.length) console.error(chalk.green.bold(`✔ Hooks installed (${hookScopeArg}):`), installed.join(', '))
|
|
@@ -746,7 +930,7 @@ if (subcommand === 'hooks') {
|
|
|
746
930
|
}
|
|
747
931
|
const { removed } = await uninstallHook(nameToRemove, hookScopeArg, hookProjectArg)
|
|
748
932
|
if (hookJsonFlag) {
|
|
749
|
-
|
|
933
|
+
await emitJson({ removed })
|
|
750
934
|
} else if (!removed) {
|
|
751
935
|
console.log(chalk.dim(` · ${nameToRemove} was not installed in ${hookScopeArg} scope`))
|
|
752
936
|
}
|
|
@@ -781,26 +965,14 @@ if (subcommand === 'upgrade') {
|
|
|
781
965
|
const targetList = resolveTargets(upgradeTargetArg ? [upgradeTargetArg] : ['all'], upgradeScopeArg)
|
|
782
966
|
const scopeKey = upgradeScopeArg + 'Detail' // 'userDetail' or 'projectDetail'
|
|
783
967
|
|
|
784
|
-
const summary =
|
|
785
|
-
for (const { name: targetName, dir } of targetList) {
|
|
786
|
-
const upgradeList = rows
|
|
787
|
-
.filter(r => r[scopeKey]?.[targetName]?.status === 'update')
|
|
788
|
-
.map(r => ({ skillName: r.name, srcPath: r.srcPath, version: r.version }))
|
|
789
|
-
|
|
790
|
-
if (!upgradeList.length) continue
|
|
791
|
-
|
|
792
|
-
console.log('')
|
|
793
|
-
const result = await installSkills(upgradeList, [{ name: targetName, dir }], true)
|
|
794
|
-
Object.assign(summary, result)
|
|
795
|
-
console.log('')
|
|
796
|
-
}
|
|
968
|
+
const summary = await upgradeOutdatedSkills(rows, targetList, scopeKey)
|
|
797
969
|
|
|
798
970
|
const nothingUpgraded = Object.keys(summary).length === 0
|
|
799
971
|
if (jsonFlag) {
|
|
800
972
|
if (nothingUpgraded) {
|
|
801
|
-
|
|
973
|
+
await emitJson({ skills: {}, upToDate: true })
|
|
802
974
|
} else {
|
|
803
|
-
|
|
975
|
+
await emitJson({ skills: summary })
|
|
804
976
|
}
|
|
805
977
|
} else {
|
|
806
978
|
if (nothingUpgraded) {
|
|
@@ -1512,7 +1684,7 @@ try {
|
|
|
1512
1684
|
const out = {}
|
|
1513
1685
|
if (skillSummary !== null) out.skills = skillSummary
|
|
1514
1686
|
if (toolSummary !== null) out.tools = toolSummary
|
|
1515
|
-
|
|
1687
|
+
await emitJson(out)
|
|
1516
1688
|
} else {
|
|
1517
1689
|
printSummary(skillSummary, toolSummary)
|
|
1518
1690
|
}
|
package/lib/bundles.js
CHANGED
|
@@ -101,21 +101,29 @@ export function buildAllChoices() {
|
|
|
101
101
|
return choices
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
//
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
104
|
+
// 把一组原始 skill 定义(来自 skills-index.json 的 skills[])展开为运行时条目。
|
|
105
|
+
// 抽出来是因为 `hskill update` 在自更新落盘后,需要绕开本模块顶部那次一次性
|
|
106
|
+
// require() 缓存的旧 skillDefs,直接用刚写到磁盘的新 skills-index.json 重新展开。
|
|
107
|
+
export function toSkillItems(defs, root) {
|
|
108
|
+
return defs.map(skill => {
|
|
109
|
+
const srcPath = path.join(root, skill.path)
|
|
108
110
|
return {
|
|
109
111
|
kind: 'skill',
|
|
110
112
|
skillName: skill.path.split('/').pop(),
|
|
111
113
|
bundle: skill.bundle,
|
|
112
114
|
installScope: skill.installScope ?? null,
|
|
115
|
+
autoUpdate: skill.autoUpdate ?? false,
|
|
113
116
|
srcPath,
|
|
114
117
|
version: readVersion(srcPath),
|
|
115
118
|
}
|
|
116
119
|
})
|
|
117
120
|
}
|
|
118
121
|
|
|
122
|
+
// 所有 skill 展开为 { kind:'skill', skillName, bundle, srcPath } 列表(对应 all 选项)
|
|
123
|
+
export function getAllSkillItems() {
|
|
124
|
+
return toSkillItems(skillDefs, skillsRoot)
|
|
125
|
+
}
|
|
126
|
+
|
|
119
127
|
// 所有 tool 展开为 { kind:'tool', toolName, srcPath, version } 列表
|
|
120
128
|
export function getAllToolItems() {
|
|
121
129
|
return toolDefs.map(tool => {
|
|
@@ -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/mcp-server.js
CHANGED
|
@@ -152,12 +152,12 @@ export function createServer() {
|
|
|
152
152
|
})
|
|
153
153
|
|
|
154
154
|
server.registerTool('hskill_update', {
|
|
155
|
-
description: 'Update hskill itself
|
|
155
|
+
description: 'Update hskill itself from whichever source the current install came from: the npm registry (the default) or, if it was installed from a local repo, that same repo re-packed. Never switches between the two — use the CLI (`hskill update --npm` / `--local <path>`) to change source. This replaces the globally installed hskill binary and is irreversible except by manually reinstalling a specific version.',
|
|
156
156
|
inputSchema: {},
|
|
157
157
|
}, async () => {
|
|
158
158
|
const { code, stdout, stderr } = await runCli(['update'])
|
|
159
159
|
if (code === 0) {
|
|
160
|
-
return { content: [{ type: 'text', text: 'hskill updated
|
|
160
|
+
return { content: [{ type: 'text', text: stdout.trim() || 'hskill updated.' }] }
|
|
161
161
|
}
|
|
162
162
|
const message = stderr.trim() || stdout.trim() || `update failed with exit code ${code}`
|
|
163
163
|
return { content: [{ type: 'text', text: message }], isError: true }
|
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
|