@hi-man/himan 0.3.5 → 0.4.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/CHANGELOG.md CHANGED
@@ -6,6 +6,26 @@ The format is based on Keep a Changelog, and this project follows semver for the
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.4.1] - 2026-05-14
10
+
11
+ ### Added
12
+
13
+ - Added `himan doctor` to check local Node/Git availability, Himan home state, current source scanning, effective agents, project lock state, and installed targets.
14
+
15
+ ### Changed
16
+
17
+ - Changed `create` and `dev` to use current project agent target directories for resource authoring, while `publish` now logs stages and can install the published version either into the current project or globally.
18
+ - Changed `himan init` to support quick-start setup with optional `--agent`, `--install type/name[@version],...`, `--mode`, and `--json`.
19
+
20
+ ## [0.4.0] - 2026-05-13
21
+
22
+ ### Added
23
+
24
+ - Added the `github-npm-publish` skill for reusable GitHub Actions npm release workflow guidance.
25
+ - Added `himan resource rename` and top-level `himan rename`, currently marked not recommended, to rename source resources, update metadata/docs, preserve old tags, create a latest-version tag for the new name, and migrate the current project's install targets and lock entry by default.
26
+ - Added `himan source clone` and `himan source sync` for cloning Git sources and syncing latest resource snapshots into another Git source.
27
+ - Added static `analysis` metadata to newly scaffolded skill `himan.yaml` files and a `himan-skill-metadata` skill for generating matching metadata when agents create skills.
28
+
9
29
  ## [0.3.5] - 2026-05-12
10
30
 
11
31
  ### Changed
package/README.md CHANGED
@@ -41,16 +41,22 @@ pnpm dlx @hi-man/himan --help
41
41
  以下示例假设你已有一个可访问的 himan Git source 仓库,仓库中存在 `my-rule` 的资源版本 tag,并且你拥有发布所需的 Git push 权限。
42
42
 
43
43
  ```bash
44
- himan init https://github.com/your-org/your-himan-registry.git
45
- himan list rule
46
- himan agent use codex
47
- himan install rule my-rule
44
+ himan init https://github.com/your-org/your-himan-registry.git \
45
+ --agent codex \
46
+ --install rule/my-rule
47
+ himan doctor
48
+ himan create skill my-skill
49
+ # 编辑并验证项目下 .agents/skills/my-skill/
50
+ himan publish skill my-skill --patch
48
51
  himan dev rule my-rule
49
- # 编辑项目下 .himan/dev/rule/my-rule/
52
+ # 直接编辑项目下 .agents/rules/my-rule/;若只存在全局安装,dev 会先复制到当前项目
50
53
  himan publish rule my-rule --patch
51
54
  ```
52
55
 
53
- - **rule / command / skill**:都支持 `create`、`list`、`history`、`install`、`dev`、`publish`、`uninstall`。
56
+ - `init` `--agent` 会写入当前项目默认 agent;`--install` 可一次选择要安装的资源,格式为 `type/name[@version]`,多个资源用逗号分隔,例如 `rule/my-rule,skill/my-skill@1.0.0`。
57
+ - 也可以只执行 `himan init <git_url>` 跳过 agent 和资源安装,后续再用 `himan agent use ...`、`himan list ...`、`himan install ...` 单独配置。
58
+ - `himan doctor` 会检查本机 Node/Git、Himan home、当前 source、资源扫描、默认 agent、lock 和已安装目标。
59
+ - **rule / command / skill**:都支持 `create`、`rename`、`list`、`history`、`install`、`dev`、`publish`、`uninstall`;其中 `rename` 暂不推荐使用。
54
60
  - 安装后项目目标位置(按 `agents`,默认 `cursor`):
55
61
  - `cursor` -> `.cursor/{rules|commands|skills}/<name>`
56
62
  - `claude-code` -> `.claude/{rules|commands|skills}/<name>`
@@ -61,10 +67,11 @@ himan publish rule my-rule --patch
61
67
  - `claude-code` -> `~/.claude/{rules|commands|skills}/<name>`
62
68
  - `codex` -> `~/.agents/{rules|commands|skills}/<name>`
63
69
  - `openclaw` -> `~/.openclaw/{rules|commands|skills}/<name>`
64
- - 开发态目录:
65
- - `rule` -> `.himan/dev/rule/<name>`
66
- - `command` -> `.himan/dev/command/<name>`
67
- - `skill` -> `.himan/dev/skill/<name>`
70
+ - 创建与开发态目录默认就是当前项目的 agent 目标目录;例如 Codex:
71
+ - `rule` -> `.agents/rules/<name>`
72
+ - `command` -> `.agents/commands/<name>`
73
+ - `skill` -> `.agents/skills/<name>`
74
+ `dev` 修改项目内资源时不再创建 `.himan/dev`,只在资源仅存在于用户级全局目录时复制到当前项目对应 agent 目录。
68
75
  - lock 文件:项目安装 `install <type> <name[@version]>` 会写入 `himan.lock`,记录 source、精确版本、agent 和安装模式;`himan install`(无参数)会按 lock 记录的 source 批量恢复安装,不受当前 default source 切换影响。`--global` 安装不写当前项目的 `himan.lock`。
69
76
  - 安装模式:默认 `--mode copy` 将资源复制到目标 agent 目录;也可用 `--mode link` 使用软链,lock 会记录并复现该模式。
70
77
  - 默认 agent:`agent use <agent>` 默认写当前项目 `.himan/config.json`;加 `--global` 写入 `~/.himan/config.json`。当前项目配置优先于全局配置。
@@ -96,15 +103,47 @@ your-himan-source/
96
103
  - `README.md`:source 仓库入口文档,建议记录资源目录说明、推荐安装方式、默认 agent 策略、常用资源索引和维护约定。
97
104
  - `CHANGELOG.md`:source 仓库级变更记录,建议记录新增、变更、废弃、移除的资源,以及重要版本发布说明。
98
105
  - `rules/`、`commands/`、`skills/`:按资源类型分组;每个子目录是一份 himan 资源。
99
- - `himan.yaml`:可选资源元数据;存在时供 himan 扫描、校验、读取入口和默认 agent
106
+ - `himan.yaml`:可选资源元数据;存在时供 himan 扫描、校验、读取入口和默认 agent;skill 资源可包含 `analysis` 静态分析信息。
100
107
  - `content.md` / `SKILL.md`:资源主入口;没有 `himan.yaml` 时,`rule` / `command` 默认使用 `content.md`,`skill` 默认使用 `SKILL.md`。
101
108
 
109
+ skill 的 `himan.yaml` 推荐包含静态分析 metadata,便于 hooks、日志和后续分析系统关联 skill 内容成本与依赖:
110
+
111
+ ```yaml
112
+ name: my-skill
113
+ type: skill
114
+ version: 0.1.0
115
+ entry: SKILL.md
116
+ description: Do the skill workflow.
117
+ agents:
118
+ - codex
119
+ analysis:
120
+ content:
121
+ tokenizer: approx-char-v1
122
+ tokenEstimator: ceil(chars/4)
123
+ entryTokens: 120
124
+ packageTokens: 180
125
+ contentHash: sha256:...
126
+ measuredAt: "2026-05-13T00:00:00.000Z"
127
+ measuredBy: codex
128
+ dependencies:
129
+ skills: []
130
+ scripts: []
131
+ mcpTools: []
132
+ generation:
133
+ generatedBy: codex
134
+ generatedAt: "2026-05-13T00:00:00.000Z"
135
+ ```
136
+
137
+ `analysis` 是静态构建信息,不记录运行时 token 或执行耗时;`himan create skill` 会为新 skill scaffold 生成基础 `analysis`。
138
+
102
139
  可通过 `himan source init-docs` 为当前 default source 生成根目录文档模板;默认只创建缺失文件,`--force` 会覆盖已有 `README.md` / `CHANGELOG.md`,并把当前 source 中已有的 `rule`、`command`、`skill` 整理进 README 资源索引和 CHANGELOG 初始条目;资源引用会优先带上 Git tag 中的最新 semver 版本;对于尚未补齐 `himan.yaml` 的资源,会按默认入口识别,skill 还会读取 `skills/<name>/SKILL.md` front matter。`--dry-run` 可预览结果。有实际文件变更时,命令会提交并 push 到当前 Git source。
103
140
 
104
- `himan create` `himan publish` 会自动维护 source 根目录文档:
141
+ `himan create` 默认在当前项目 agent 目标目录创建资源脚手架,供用户直接验证;`himan publish` 会把项目目录中的资源同步回当前 default source,并自动维护 source 根目录文档:
105
142
 
106
143
  - `README.md`:只更新 `<!-- himan:resources:start -->` 和 `<!-- himan:resources:end -->` 之间的资源索引;如果没有 marker,会在文件末尾追加一个受控资源索引区。
107
- - `CHANGELOG.md`:向 `[Unreleased]` 下追加资源变更条目;`create` 记录 `Added`,`publish` 记录 `Changed` / published version
144
+ - `CHANGELOG.md`:`publish` `[Unreleased]` 下追加 `Changed` / published version 条目。
145
+
146
+ `himan rename` 也会维护 source 根目录文档:更新 README 资源索引,并向 CHANGELOG 的 `[Unreleased]` 追加 `Changed` 条目。
108
147
 
109
148
  仓库根目录的 `README.md` 和 `CHANGELOG.md` 不会被安装到 agent 目录;agent 只消费被安装的具体资源目录。当前安装实现会 materialize 资源目录本身,因此对 Cursor 这类要求特定单文件格式的 agent,资源目录内应避免放入会干扰识别的额外文件。
110
149
 
@@ -114,11 +153,13 @@ your-himan-source/
114
153
 
115
154
  | 命令 | 说明 |
116
155
  | ----------------------------- | ------------------------------------------------ |
117
- | `init <git_url>` | 初始化默认源(当前为 Git)并写入 `~/.himan/config.json` |
156
+ | `init <git_url> [--agent a,b] [--install type/name[@version],...] [--mode link\|copy] [--json]` | 初始化默认源(当前为 Git)并写入 `~/.himan/config.json`;可同时写当前项目默认 agent 并安装选定资源 |
118
157
  | `source add <name> <git_url>` | 添加命名 Git 源 |
119
158
  | `source use <name>` | 切换默认源 |
120
159
  | `source list [--json]` | 查看已配置源(标记当前 default) |
121
160
  | `source init-docs [--force] [--dry-run] [--json]` | 为当前 default source 生成仓库级 README/CHANGELOG |
161
+ | `source clone <from> <to> [--branch b] [--target-branch b] [--add-source name] [--use] [--dry-run] [--json]` | 将 Git source 分支和 himan 管理的资源 tag 复制到空目标 Git 仓库 |
162
+ | `source sync <from> <to> [--target-branch b] [--add-source name] [--use] [--dry-run] [--json]` | 将最新资源快照同步到目标 Git 仓库并创建对应最新 tag |
122
163
  | `source init <git_url>` | 与 `init` 等价,便于统一走 `himan source ...` 入口 |
123
164
 
124
165
  等价独立命令:
@@ -128,6 +169,8 @@ your-himan-source/
128
169
  - `himan-source use <name>`
129
170
  - `himan-source list [--json]`
130
171
  - `himan-source init-docs [--force] [--dry-run] [--json]`
172
+ - `himan-source clone <from> <to> [...]`
173
+ - `himan-source sync <from> <to> [...]`
131
174
 
132
175
  ### 2) resource(资源)
133
176
 
@@ -135,7 +178,8 @@ your-himan-source/
135
178
  | -------------------------------- | ----------------------------------------------------------------------------------- |
136
179
  | `list [type] [--agent a,b] [--brief] [--installed] [--json]` | 默认列出当前 default source 的资源;未传 `type` 时按 `rule`/`command`/`skill` 分组展示全部资源;可按 agent 过滤;默认显示描述,`--brief` 可隐藏描述;`--installed` 改为查看当前项目 `himan.lock` 中的已安装资源 |
137
180
  | `history <type> <name> [--json]` | 按 tag 查看版本历史 |
138
- | `create <type> <name>` | 脚手架;常用选项:`--description`、`--agent a,b`、`--dry-run`、`--force`、`--json` |
181
+ | `create <type> <name>` | 在当前项目 agent 目录创建脚手架;常用选项:`--description`、`--agent a,b`、`--dry-run`、`--force`、`--json` |
182
+ | `rename <type> <old-name> <new-name>` | 暂不推荐使用;重命名当前 default source 中的资源;常用选项:`--dry-run`、`--no-project`、`--json` |
139
183
 
140
184
  ### 3) project(当前项目)
141
185
 
@@ -143,9 +187,9 @@ your-himan-source/
143
187
  | --------------------------------- | --------------------------------------------------------- |
144
188
  | `list [type] [--agent a,b] [--json]` | 查看当前项目 `himan.lock` 中记录的已安装资源;未传 `type` 时按 `rule`/`command`/`skill` 分组展示 |
145
189
  | `install [type] [name[@version]] [--global] [--agent a,b] [--mode link\|copy]` | 有参数时从当前 default source 安装指定资源;**无参数**时按 `himan.lock` 记录的 source 批量安装;加 `--global` 时安装到用户级 agent 目录且不写项目 lock;可覆盖安装目标 agent 或安装模式 |
146
- | `dev <type> <name>` | 切换到开发态,并按安装模式将项目目标指向或复制自 `.himan/dev/...` |
190
+ | `dev <type> <name>` | 切换到开发态;项目资源原地编辑,全局资源先复制到当前项目目标目录 |
147
191
  | `uninstall <type> <name>` | 从项目移除安装目标,并同步删除 `himan.lock` 条目 |
148
- | `publish <type> <name>` | 默认 `--patch`;可选 `--minor` / `--major`(勿同时使用多个) |
192
+ | `publish <type> <name> [--global]` | 默认 `--patch`;可选 `--minor` / `--major`(勿同时使用多个);发布后默认安装到当前项目并更新 lock,`--global` 安装到用户级目录 |
149
193
 
150
194
  ### 4) agent(默认 Agent)
151
195
 
@@ -156,22 +200,31 @@ your-himan-source/
156
200
  | `agent current [--json]` | 查看当前项目、全局和最终生效的默认 agent |
157
201
  | `agent clear [--project\|--global] [--json]` | 清除当前项目或全局默认 agent;默认 `--project` |
158
202
 
203
+ ### 5) doctor(可用性检查)
204
+
205
+ | 命令 | 说明 |
206
+ |------|------|
207
+ | `doctor [--json]` | 检查 Node/Git、Himan home、当前 source、资源扫描、默认 agent、项目 lock 和已安装目标;存在 error 时以非零状态退出 |
208
+
159
209
  也可使用分组命令(与上面等价):
160
210
 
161
- - `himan resource list|history|create ...`
162
- - `himan-resource list|history|create ...`(兼容保留:也可执行 install/dev/uninstall/publish)
211
+ - `himan resource list|history|create|rename ...`
212
+ - `himan-resource list|history|create|rename ...`(兼容保留:也可执行 install/dev/uninstall/publish)
163
213
  - `himan project list|install|dev|uninstall|publish ...`
164
214
  - `himan-project list|install|dev|uninstall|publish ...`
165
215
  - `himan agent list|use|current|clear ...`
216
+ - `himan doctor ...`
166
217
 
167
218
  说明:资源与项目相关命令统一使用 `--agent` 指定目标 Agent。
168
- 若未显式传 `--agent`,`create` / `install` 会使用当前项目默认 agent、全局默认 agent、资源 metadata 或内置默认 `cursor` 中最合适的一项;`dev` 会优先使用 lock 中记录的 agent。`install --global` 会优先复用当前项目 lock 里该资源的 agent,未命中时再使用默认 install 解析顺序,但目标根目录是用户 home 下对应 agent 目录。
219
+ 若未显式传 `--agent`,`create` / `install` 会使用当前项目默认 agent、全局默认 agent、资源 metadata 或内置默认 `cursor` 中最合适的一项;`dev` 会优先使用当前项目已有安装位置,找不到时再从用户级全局安装位置复制到当前项目。`install --global` 会优先复用当前项目 lock 里该资源的 agent,未命中时再使用默认 install 解析顺序,但目标根目录是用户 home 下对应 agent 目录。
220
+
221
+ `publish` 会展示 prepare、resolve-version、publish-source、sync-store、install、cleanup、done 等阶段日志。发布源优先使用旧版 `.himan/dev` 目录,其次使用当前项目 agent 目标目录,最后回退到 source 仓库对应资源目录。若资源目录包含 `himan.yaml`,发布前会校验元数据与入口文件;若没有 `himan.yaml`,则按默认入口推断最小元数据并发布,不会强制创建 `himan.yaml`。若待发布资源内容与最新已发布版本一致,则以 `E_PUBLISH_NO_CHANGES` 终止发布。发布需要可推送的 Git 权限。发布 commit 会包含资源目录以及自动维护的 source 根目录 `README.md` / `CHANGELOG.md`。发布成功后会从新版本 store 以 `copy` 模式重新安装;默认安装到当前项目并更新 `himan.lock`,传 `--global` 时安装到用户级目录且不写当前项目 lock。
169
222
 
170
- `publish` 优先使用项目里 `.himan/dev` 对应目录,否则用源仓库里对应目录。若资源目录包含 `himan.yaml`,发布前会校验元数据与入口文件;若没有 `himan.yaml`,则按默认入口推断最小元数据并发布,不会强制创建 `himan.yaml`。若待发布资源内容与最新已发布版本一致,则以 `E_PUBLISH_NO_CHANGES` 终止发布。发布需要可推送的 Git 权限。发布 commit 会包含资源目录以及自动维护的 source 根目录 `README.md` / `CHANGELOG.md`。发布成功后会从新版本 store 以 `copy` 模式重新安装到项目目标、更新 lock,并删除对应 `.himan/dev/<type>/<name>` 开发目录。
223
+ `rename` 暂不推荐使用。该命令会移动 source 仓库里的资源目录并更新资源 metadata 名称、README 资源索引和 CHANGELOG。已有发布 tag 不会被改写;若旧资源已有历史版本,rename 会为新名字创建一个指向当前最新版本的 tag。默认会迁移当前项目中对应的安装目标、`.himan/dev` 副本和 lock 条目;传 `--no-project` 时只改 source。对于 skill,命令只自动更新 metadata / front matter 中的精确 `name` 字段,不会自动替换 `SKILL.md` 正文中的旧名称引用。
171
224
 
172
225
  `--json` 模式下,失败时会输出机器可读错误 JSON(`stderr`)。错误码定义见 [docs/error-codes.md](./docs/error-codes.md)。
173
226
 
174
- 多源说明:当前是「**多来源可配置,单来源生效**」模型。显式资源命令(`list/install <type> .../history/dev/publish`)作用于当前 default source;`himan install` 无参数恢复时使用 `himan.lock` 中记录的 source。
227
+ 多源说明:当前是「**多来源可配置,单来源生效**」模型。显式资源命令(`list/install <type> .../history/dev/publish/rename`)作用于当前 default source;`himan install` 无参数恢复时使用 `himan.lock` 中记录的 source。
175
228
 
176
229
  ## 当前范围
177
230
 
@@ -1,7 +1,9 @@
1
1
  import { promises as fs } from "node:fs";
2
+ import os from "node:os";
2
3
  import path from "node:path";
3
4
  import { simpleGit } from "simple-git";
4
5
  import { HimanError, errorCodes } from "../../utils/errors.js";
6
+ const MANAGED_TAG_PATTERNS = ["rule/*@*", "command/*@*", "skill/*@*"];
5
7
  export class RepoManager {
6
8
  async cloneOrFetch(repo, targetDir) {
7
9
  const gitDir = path.join(targetDir, ".git");
@@ -61,6 +63,124 @@ export class RepoManager {
61
63
  await this.pushCurrentBranch(git, branch);
62
64
  return true;
63
65
  }
66
+ async cloneManagedSourceRefs(sourceRepoDir, targetRepo, options = {}) {
67
+ const sourceGit = simpleGit(sourceRepoDir);
68
+ await sourceGit.fetch(["--tags", "--prune"]);
69
+ const branch = options.branch ?? (await this.getCurrentBranch(sourceGit));
70
+ const sourceBranchRef = await this.resolveSourceBranchRef(sourceGit, branch);
71
+ const targetBranch = options.targetBranch ?? branch;
72
+ const targetRefs = await this.listRemoteRefs(targetRepo);
73
+ if (targetRefs.length > 0) {
74
+ throw new HimanError(errorCodes.RESOURCE_EXISTS, "Target source repository is not empty.", { targetRepo, refs: targetRefs.map((item) => item.ref) });
75
+ }
76
+ const tags = await this.listManagedResourceTags(sourceRepoDir);
77
+ if (options.dryRun) {
78
+ return {
79
+ branch,
80
+ targetBranch,
81
+ tags,
82
+ dryRun: true,
83
+ pushed: false,
84
+ };
85
+ }
86
+ const refspecs = [
87
+ `${sourceBranchRef}:refs/heads/${targetBranch}`,
88
+ ...tags.map((tag) => `refs/tags/${tag}:refs/tags/${tag}`),
89
+ ];
90
+ await sourceGit.raw(["push", "--atomic", targetRepo, ...refspecs]);
91
+ return {
92
+ branch,
93
+ targetBranch,
94
+ tags,
95
+ dryRun: false,
96
+ pushed: true,
97
+ };
98
+ }
99
+ async syncLatestSourceSnapshot(sourceRepoDir, targetRepo, resources, options = {}) {
100
+ if (resources.length === 0) {
101
+ throw new HimanError(errorCodes.RESOURCE_NOT_FOUND, "No versioned resources found to sync.");
102
+ }
103
+ const targetBranch = options.targetBranch ?? "main";
104
+ const targetRefs = await this.listRemoteRefs(targetRepo);
105
+ const existingTagRefs = new Set(targetRefs
106
+ .filter((item) => item.ref.startsWith("refs/tags/"))
107
+ .map((item) => item.ref.slice("refs/tags/".length)));
108
+ const targetDir = await fs.mkdtemp(path.join(os.tmpdir(), "himan-source-sync-"));
109
+ const snapshotsDir = await fs.mkdtemp(path.join(os.tmpdir(), "himan-source-snapshots-"));
110
+ try {
111
+ const targetGit = await this.prepareTargetWorktree(targetRepo, targetDir, targetBranch, targetRefs);
112
+ const results = [];
113
+ for (const resource of resources) {
114
+ const sourceSnapshotDir = path.join(snapshotsDir, resource.type, resource.name);
115
+ await this.materializeSourceResourceSnapshot(sourceRepoDir, resource, sourceSnapshotDir);
116
+ if (existingTagRefs.has(resource.tag)) {
117
+ await this.ensureExistingTagMatchesResource(targetDir, resource, sourceSnapshotDir);
118
+ results.push({
119
+ type: resource.type,
120
+ name: resource.name,
121
+ version: resource.version,
122
+ tag: resource.tag,
123
+ action: "skipped",
124
+ });
125
+ }
126
+ else {
127
+ results.push({
128
+ type: resource.type,
129
+ name: resource.name,
130
+ version: resource.version,
131
+ tag: resource.tag,
132
+ action: "created",
133
+ });
134
+ }
135
+ if (!options.dryRun) {
136
+ const targetResourceDir = path.join(targetDir, this.getResourcePath(resource));
137
+ await fs.rm(targetResourceDir, { recursive: true, force: true });
138
+ await fs.mkdir(path.dirname(targetResourceDir), { recursive: true });
139
+ await fs.cp(sourceSnapshotDir, targetResourceDir, { recursive: true });
140
+ }
141
+ }
142
+ if (options.dryRun) {
143
+ return {
144
+ targetBranch,
145
+ resources: results,
146
+ dryRun: true,
147
+ committed: false,
148
+ pushed: false,
149
+ };
150
+ }
151
+ const changedPaths = [
152
+ ...new Set(resources.map((resource) => `${resource.type}s`)),
153
+ ];
154
+ const committed = await this.commitChanges(targetGit, "sync latest himan source resources", changedPaths, false);
155
+ const createdTags = results
156
+ .filter((result) => result.action === "created")
157
+ .map((result) => result.tag);
158
+ for (const tag of createdTags) {
159
+ await targetGit.addTag(tag);
160
+ }
161
+ const shouldPush = committed || createdTags.length > 0;
162
+ if (shouldPush) {
163
+ await targetGit.raw([
164
+ "push",
165
+ "--atomic",
166
+ "origin",
167
+ `${targetBranch}:refs/heads/${targetBranch}`,
168
+ ...createdTags.map((tag) => `refs/tags/${tag}:refs/tags/${tag}`),
169
+ ]);
170
+ }
171
+ return {
172
+ targetBranch,
173
+ resources: results,
174
+ dryRun: false,
175
+ committed,
176
+ pushed: shouldPush,
177
+ };
178
+ }
179
+ finally {
180
+ await fs.rm(targetDir, { recursive: true, force: true });
181
+ await fs.rm(snapshotsDir, { recursive: true, force: true });
182
+ }
183
+ }
64
184
  async commitChanges(git, message, paths, requireChanges) {
65
185
  const pathspecs = paths.length > 0 ? paths : ["."];
66
186
  await git.add(pathspecs);
@@ -80,6 +200,132 @@ export class RepoManager {
80
200
  await git.commit(message, pathspecs);
81
201
  return true;
82
202
  }
203
+ async prepareTargetWorktree(targetRepo, targetDir, targetBranch, targetRefs) {
204
+ const hasTargetBranch = targetRefs.some((item) => item.ref === `refs/heads/${targetBranch}`);
205
+ if (hasTargetBranch) {
206
+ await simpleGit().clone(targetRepo, targetDir, ["--branch", targetBranch]);
207
+ const git = simpleGit(targetDir);
208
+ await git.fetch(["--tags", "--prune"]);
209
+ return git;
210
+ }
211
+ const git = simpleGit(targetDir);
212
+ await git.raw(["init", `--initial-branch=${targetBranch}`]);
213
+ await git.addRemote("origin", targetRepo);
214
+ if (targetRefs.some((item) => item.ref.startsWith("refs/tags/"))) {
215
+ await git.raw(["fetch", "origin", "+refs/tags/*:refs/tags/*"]);
216
+ }
217
+ return git;
218
+ }
219
+ async materializeSourceResourceSnapshot(sourceRepoDir, resource, targetDir) {
220
+ if (resource.sourceRef) {
221
+ await this.archiveResource(sourceRepoDir, resource.sourceRef, this.getResourcePath(resource), targetDir);
222
+ return;
223
+ }
224
+ if (!resource.sourcePath) {
225
+ throw new HimanError(errorCodes.VERSION_NOT_FOUND, `Version source not found for ${resource.type}/${resource.name}@${resource.version}.`);
226
+ }
227
+ await fs.rm(targetDir, { recursive: true, force: true });
228
+ await fs.mkdir(path.dirname(targetDir), { recursive: true });
229
+ await fs.cp(resource.sourcePath, targetDir, { recursive: true });
230
+ }
231
+ async ensureExistingTagMatchesResource(targetRepoDir, resource, sourceSnapshotDir) {
232
+ const previousDir = await fs.mkdtemp(path.join(os.tmpdir(), "himan-source-tag-"));
233
+ try {
234
+ await this.archiveResource(targetRepoDir, resource.tag, this.getResourcePath(resource), previousDir);
235
+ const [sourceSnapshot, previousSnapshot] = await Promise.all([
236
+ this.readDirectorySnapshot(sourceSnapshotDir),
237
+ this.readDirectorySnapshot(previousDir),
238
+ ]);
239
+ if (!this.snapshotsEqual(sourceSnapshot, previousSnapshot)) {
240
+ throw new HimanError(errorCodes.RESOURCE_EXISTS, `Target tag already exists with different content: ${resource.tag}`);
241
+ }
242
+ }
243
+ finally {
244
+ await fs.rm(previousDir, { recursive: true, force: true });
245
+ }
246
+ }
247
+ async listManagedResourceTags(repoDir) {
248
+ const tags = new Set();
249
+ for (const pattern of MANAGED_TAG_PATTERNS) {
250
+ for (const tag of await this.listTags(repoDir, pattern)) {
251
+ tags.add(tag);
252
+ }
253
+ }
254
+ return [...tags].sort((a, b) => a.localeCompare(b));
255
+ }
256
+ async listRemoteRefs(repo) {
257
+ const output = await simpleGit().raw(["ls-remote", "--heads", "--tags", repo]);
258
+ return output
259
+ .split("\n")
260
+ .map((line) => line.trim())
261
+ .filter(Boolean)
262
+ .map((line) => {
263
+ const [hash, ref] = line.split(/\s+/);
264
+ return { hash, ref };
265
+ })
266
+ .filter((item) => item.hash && item.ref && !item.ref.endsWith("^{}"));
267
+ }
268
+ async getCurrentBranch(git) {
269
+ const branch = (await git.raw(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
270
+ if (!branch || branch === "HEAD") {
271
+ throw new HimanError(errorCodes.INVALID_INPUT, "Source repository is not on a named branch.");
272
+ }
273
+ return branch;
274
+ }
275
+ async resolveSourceBranchRef(git, branch) {
276
+ if (await this.hasGitRef(git, `refs/heads/${branch}`)) {
277
+ return branch;
278
+ }
279
+ const remoteRef = `refs/remotes/origin/${branch}`;
280
+ if (await this.hasGitRef(git, remoteRef)) {
281
+ return remoteRef;
282
+ }
283
+ throw new HimanError(errorCodes.INVALID_INPUT, `Source branch not found: ${branch}`);
284
+ }
285
+ async hasGitRef(git, ref) {
286
+ try {
287
+ await git.raw(["rev-parse", "--verify", ref]);
288
+ return true;
289
+ }
290
+ catch {
291
+ return false;
292
+ }
293
+ }
294
+ async readDirectorySnapshot(targetDir) {
295
+ const files = await this.listFiles(targetDir);
296
+ const snapshot = new Map();
297
+ for (const file of files) {
298
+ const relative = path.relative(targetDir, file).split(path.sep).join("/");
299
+ snapshot.set(relative, await fs.readFile(file, "utf8"));
300
+ }
301
+ return snapshot;
302
+ }
303
+ async listFiles(targetDir) {
304
+ const entries = await fs.readdir(targetDir, { withFileTypes: true });
305
+ const files = [];
306
+ for (const entry of entries) {
307
+ const fullPath = path.join(targetDir, entry.name);
308
+ if (entry.isDirectory()) {
309
+ files.push(...(await this.listFiles(fullPath)));
310
+ }
311
+ else if (entry.isFile()) {
312
+ files.push(fullPath);
313
+ }
314
+ }
315
+ return files.sort((a, b) => a.localeCompare(b));
316
+ }
317
+ snapshotsEqual(a, b) {
318
+ if (a.size !== b.size)
319
+ return false;
320
+ for (const [file, content] of a) {
321
+ if (b.get(file) !== content)
322
+ return false;
323
+ }
324
+ return true;
325
+ }
326
+ getResourcePath(resource) {
327
+ return `${resource.type}s/${resource.name}`;
328
+ }
83
329
  async pushCurrentBranch(git, branch) {
84
330
  const currentBranch = (await git.raw(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
85
331
  const targetBranch = branch ?? currentBranch;
@@ -0,0 +1,42 @@
1
+ import { createHash } from "node:crypto";
2
+ const TOKENIZER = "approx-char-v1";
3
+ const TOKEN_ESTIMATOR = "ceil(chars/4)";
4
+ export function buildResourceAnalysisMetadata(input) {
5
+ const measuredAt = input.measuredAt ?? new Date();
6
+ const packageFiles = input.packageFiles?.length
7
+ ? input.packageFiles
8
+ : [{ path: input.entry, content: input.entryContent }];
9
+ return {
10
+ content: {
11
+ tokenizer: TOKENIZER,
12
+ tokenEstimator: TOKEN_ESTIMATOR,
13
+ entryTokens: estimateTokens(input.entryContent),
14
+ packageTokens: estimateTokens(packageFiles.map((file) => file.content).join("\n")),
15
+ contentHash: hashPackageFiles(packageFiles),
16
+ measuredAt: measuredAt.toISOString(),
17
+ measuredBy: input.measuredBy,
18
+ },
19
+ dependencies: {
20
+ skills: [],
21
+ scripts: [],
22
+ mcpTools: [],
23
+ },
24
+ generation: {
25
+ generatedBy: input.generatedBy,
26
+ generatedAt: measuredAt.toISOString(),
27
+ },
28
+ };
29
+ }
30
+ function estimateTokens(content) {
31
+ return Math.ceil(content.length / 4);
32
+ }
33
+ function hashPackageFiles(files) {
34
+ const hash = createHash("sha256");
35
+ for (const file of [...files].sort((a, b) => a.path.localeCompare(b.path))) {
36
+ hash.update(file.path);
37
+ hash.update("\0");
38
+ hash.update(file.content);
39
+ hash.update("\0");
40
+ }
41
+ return `sha256:${hash.digest("hex")}`;
42
+ }