@hi-man/himan 0.1.0 → 0.2.2

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/.nvmrc ADDED
@@ -0,0 +1 @@
1
+ 22.22.1
package/CHANGELOG.md ADDED
@@ -0,0 +1,65 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented in this file.
4
+
5
+ The format is based on Keep a Changelog, and this project follows semver for the npm package version.
6
+
7
+ ## [Unreleased]
8
+
9
+ ## [0.2.2] - 2026-05-07
10
+
11
+ ### Added
12
+
13
+ - Added a PR verify workflow that runs typecheck, tests, and build before merging to `master`.
14
+
15
+ ## [0.2.1] - 2026-05-07
16
+
17
+ ### Added
18
+
19
+ - Added this changelog to make release notes and user-visible changes explicit.
20
+ - Added npm package metadata for Node.js engine compatibility and package discovery.
21
+ - Added public contributing, security, and code of conduct documents.
22
+
23
+ ### Changed
24
+
25
+ - Clean `dist` before every build so npm packages cannot include stale generated files.
26
+ - Aligned MVP, v1.0, and roadmap docs with current resource type and multi-agent behavior.
27
+ - Added publish preflight checks for resource metadata and entry files, with stable publish error codes.
28
+ - Restored lock-file installs from the source recorded in `himan.lock` instead of the current default source.
29
+ - Updated repository links to `https://github.com/himan-group/himan`.
30
+ - Updated Git source refresh to fast-forward clean cached working trees after fetch while preserving dirty local edits.
31
+ - Updated list cache invalidation to track `himan.yaml` metadata content instead of parent directory mtimes.
32
+ - Moved developer, testing, release, and CI maintenance documentation from README to `docs/development.md`.
33
+ - Improved README installation guidance for npm, one-off execution, local development, and CLI entry points.
34
+ - Included README-linked user documentation in the npm package and changed GitHub workflow links to repository URLs.
35
+
36
+ ## [0.2.0] - 2026-05-06
37
+
38
+ ### Added
39
+
40
+ - Added command groups for `source`, `resource`, `project`, and `agent`, while keeping backward-compatible top-level lifecycle commands.
41
+ - Added dedicated binaries: `himan-source`, `himan-resource`, and `himan-project`.
42
+ - Added default agent configuration commands for project and global scopes.
43
+ - Added multi-agent installation targets for `cursor`, `claude-code`, `codex`, and `openclaw`.
44
+ - Added `command` and `skill` lifecycle support across create, list, history, install, dev, publish, and uninstall flows.
45
+ - Added project `himan.lock` support for reproducible installs.
46
+ - Added copy install mode in addition to symlink mode.
47
+ - Added local index cache support for resource listing.
48
+ - Added repository guidance files for Codex workflows.
49
+
50
+ ### Changed
51
+
52
+ - Split CLI registration into source, resource, project, agent, and shared command modules.
53
+ - Expanded README and planning docs to reflect multi-type resources, lock behavior, multi-agent targets, and source management.
54
+
55
+ ## [0.1.0] - 2026-04-08
56
+
57
+ ### Added
58
+
59
+ - Initial Git-backed CLI for managing prompt and agent assets.
60
+ - Added source initialization, resource listing, history, install, dev, publish, create, and uninstall workflows.
61
+ - Added resource metadata scanning from `himan.yaml`.
62
+ - Added semver tag-based resource versioning.
63
+ - Added local store, repo cache, config, and index state foundations.
64
+ - Added npm publishing and version tag GitHub Actions workflows.
65
+ - Added Vitest coverage for adapters, state, utilities, and CLI integration paths.
package/README.md CHANGED
@@ -2,26 +2,48 @@
2
2
 
3
3
  himan(含义为"Hey, man"),AI Coding 时代的 Prompt / Agent 资产管理系统(CLI + Git source)
4
4
 
5
+ ## 环境要求
6
+
7
+ - Node.js 22.x;本仓库开发环境由 [.nvmrc](./.nvmrc) 固定为 `22.22.1`。
8
+ - Git;Git source 的初始化、版本查询、安装和发布都依赖本机 Git。
9
+
5
10
  ## 安装与运行
6
11
 
12
+ ### 使用 npm 包
13
+
14
+ 全局安装后可直接使用 `himan`:
15
+
7
16
  ```bash
8
- pnpm install
9
- pnpm run build
17
+ npm install -g @hi-man/himan
18
+ himan --help
10
19
  ```
11
20
 
12
- 之后任选其一执行命令:
21
+ 也可以一次性运行主入口:
22
+
23
+ ```bash
24
+ npx @hi-man/himan --help
25
+ pnpm dlx @hi-man/himan --help
26
+ ```
27
+
28
+ ### 命令入口
29
+
30
+ 包内提供四个 CLI 入口:
13
31
 
14
- - 已全局安装本包:`himan <子命令>`
15
- - 本地开发:`pnpm run dev -- <子命令>`
16
- - 或直接:`node dist/index.js <子命令>`
32
+ - `himan <子命令>`(主入口)
33
+ - `himan-source <子命令>`(source 相关)
34
+ - `himan-resource <子命令>`(resource/project 相关)
35
+ - `himan-project <子命令>`(project 相关)
17
36
 
18
- 下文用 `himan` 代指上述入口。
37
+ 下文默认使用 `himan` 主入口;三个专用入口在对应章节单独列出。
19
38
 
20
39
  ## 一分钟上手
21
40
 
41
+ 以下示例假设你已有一个可访问的 himan Git source 仓库,仓库中存在 `my-rule` 的资源版本 tag,并且你拥有发布所需的 Git push 权限。
42
+
22
43
  ```bash
23
44
  himan init https://github.com/your-org/your-himan-registry.git
24
45
  himan list rule
46
+ himan agent use codex
25
47
  himan install rule my-rule
26
48
  himan dev rule my-rule
27
49
  # 编辑项目下 .himan/dev/rule/my-rule/
@@ -29,15 +51,18 @@ himan publish rule my-rule --patch
29
51
  ```
30
52
 
31
53
  - **rule / command / skill**:都支持 `create`、`list`、`history`、`install`、`dev`、`publish`、`uninstall`。
32
- - 安装后项目链接位置:
33
- - `rule` -> `.cursor/rules/<name>`
34
- - `command` -> `.cursor/commands/<name>`
35
- - `skill` -> `.cursor/skills/<name>`
54
+ - 安装后项目目标位置(按 `agents`,默认 `cursor`):
55
+ - `cursor` -> `.cursor/{rules|commands|skills}/<name>`
56
+ - `claude-code` -> `.claude/{rules|commands|skills}/<name>`
57
+ - `codex` -> `.agents/{rules|commands|skills}/<name>`
58
+ - `openclaw` -> `.openclaw/{rules|commands|skills}/<name>`
36
59
  - 开发态目录:
37
60
  - `rule` -> `.himan/dev/rule/<name>`
38
61
  - `command` -> `.himan/dev/command/<name>`
39
62
  - `skill` -> `.himan/dev/skill/<name>`
40
- - lock 文件:`install <type> <name[@version]>` 会写入 `himan.lock`;`himan install`(无参数)会按 lock 批量恢复安装。
63
+ - lock 文件:`install <type> <name[@version]>` 会写入 `himan.lock`,记录 source、精确版本、agent 和安装模式;`himan install`(无参数)会按 lock 记录的 source 批量恢复安装,不受当前 default source 切换影响。
64
+ - 安装模式:默认 `--mode link` 使用软链;也可用 `--mode copy` 将资源复制到目标 agent 目录,lock 会记录并复现该模式。
65
+ - 默认 agent:`agent use <agent>` 默认写当前项目 `.himan/config.json`;加 `--global` 写入 `~/.himan/config.json`。当前项目配置优先于全局配置。
41
66
 
42
67
  版本以 Git tag 为准,格式:`rule/my-rule@1.0.0`。更多设计见 [docs/mvp](./docs/mvp/README.md)。
43
68
 
@@ -51,33 +76,62 @@ himan publish rule my-rule --patch
51
76
  | `source add <name> <git_url>` | 添加命名 Git 源 |
52
77
  | `source use <name>` | 切换默认源 |
53
78
  | `source list [--json]` | 查看已配置源(标记当前 default) |
79
+ | `source init <git_url>` | 与 `init` 等价,便于统一走 `himan source ...` 入口 |
80
+
81
+ 等价独立命令:
82
+
83
+ - `himan-source init <git_url>`
84
+ - `himan-source add <name> <git_url>`
85
+ - `himan-source use <name>`
86
+ - `himan-source list [--json]`
54
87
 
55
88
  ### 2) resource(资源)
56
89
 
57
90
  | 命令 | 说明 |
58
91
  | -------------------------------- | ----------------------------------------------------------------------------------- |
59
- | `list [type] [--json]` | 列出当前 default source 的资源;`type` `rule` / `command` / `skill`,默认 `rule` |
92
+ | `list [type] [--agent a,b] [--json]` | 列出当前 default source 的资源;可按 agent 过滤;`type` 默认 `rule` |
60
93
  | `history <type> <name> [--json]` | 按 tag 查看版本历史 |
61
- | `create <type> <name>` | 脚手架;常用选项:`--description`、`--target a,b`、`--dry-run`、`--force`、`--json` |
94
+ | `create <type> <name>` | 脚手架;常用选项:`--description`、`--agent a,b`、`--dry-run`、`--force`、`--json` |
62
95
 
63
96
  ### 3) project(当前项目)
64
97
 
65
98
  | 命令 | 说明 |
66
99
  | --------------------------------- | --------------------------------------------------------- |
67
- | `install [type] [name[@version]]` | 有参数时安装指定资源;**无参数**时按 `himan.lock` 批量安装 |
68
- | `dev <type> <name>` | 切换到开发态并把项目链接指向 `.himan/dev/...` |
69
- | `uninstall <type> <name>` | 从项目移除安装链接,并同步删除 `himan.lock` 条目 |
100
+ | `install [type] [name[@version]] [--agent a,b] [--mode link\|copy]` | 有参数时从当前 default source 安装指定资源;**无参数**时按 `himan.lock` 记录的 source 批量安装;可覆盖安装目标 agent 或安装模式 |
101
+ | `dev <type> <name>` | 切换到开发态,并按安装模式将项目目标指向或复制自 `.himan/dev/...` |
102
+ | `uninstall <type> <name>` | 从项目移除安装目标,并同步删除 `himan.lock` 条目 |
70
103
  | `publish <type> <name>` | 默认 `--patch`;可选 `--minor` / `--major`(勿同时使用多个) |
71
104
 
72
- `publish` 优先使用项目里 `.himan/dev` 对应目录,否则用源仓库里对应目录。需要可推送的 Git 权限。若该资源已在 lock 中,发布后会同步更新 lock 版本。
105
+ ### 4) agent(默认 Agent)
106
+
107
+ | 命令 | 说明 |
108
+ |------|------|
109
+ | `agent list [--json]` | 查看支持的 agent |
110
+ | `agent use <agent[,agent]> [--project\|--global] [--json]` | 设置当前项目或全局默认 agent;默认 `--project` |
111
+ | `agent current [--json]` | 查看当前项目、全局和最终生效的默认 agent |
112
+ | `agent clear [--project\|--global] [--json]` | 清除当前项目或全局默认 agent;默认 `--project` |
113
+
114
+ 也可使用分组命令(与上面等价):
115
+
116
+ - `himan resource list|history|create ...`
117
+ - `himan-resource list|history|create ...`(兼容保留:也可执行 install/dev/uninstall/publish)
118
+ - `himan project install|dev|uninstall|publish ...`
119
+ - `himan-project install|dev|uninstall|publish ...`
120
+ - `himan agent list|use|current|clear ...`
121
+
122
+ 说明:资源与项目相关命令统一使用 `--agent` 指定目标 Agent。
123
+ 若未显式传 `--agent`,`create` / `install` 会使用当前项目默认 agent、全局默认 agent、资源 metadata 或内置默认 `cursor` 中最合适的一项;`dev` 会优先使用 lock 中记录的 agent。
124
+
125
+ `publish` 优先使用项目里 `.himan/dev` 对应目录,否则用源仓库里对应目录。发布前会校验 `himan.yaml` 与入口文件;需要可推送的 Git 权限。若该资源已在 lock 中,发布后会同步更新 lock 版本。
73
126
 
74
127
  `--json` 模式下,失败时会输出机器可读错误 JSON(`stderr`)。错误码定义见 [docs/error-codes.md](./docs/error-codes.md)。
75
128
 
76
- 多源说明:当前是「**多来源可配置,单来源生效**」模型。业务命令(`list/install/history/dev/publish`)只作用于当前 default source;切换后再执行命令。
129
+ 多源说明:当前是「**多来源可配置,单来源生效**」模型。显式资源命令(`list/install <type> .../history/dev/publish`)作用于当前 default source;`himan install` 无参数恢复时使用 `himan.lock` 中记录的 source。
77
130
 
78
131
  ## 当前范围
79
132
 
80
133
  - 源:**仅 Git**(`init`)。Registry 适配器已预留,尚未实现。
134
+ - 包定位:当前仅承诺 CLI 使用,不提供稳定的 Node.js 程序化 API。
81
135
 
82
136
  ## FAQ
83
137
 
@@ -95,51 +149,6 @@ himan source list
95
149
  **Q: `list` 和 `source list` 有什么区别?**
96
150
  A: `source list` 查看「我配置了哪些来源」;`list` 查看「当前 default source 里有哪些资源」。
97
151
 
98
- ## 开发与测试
99
-
100
- ```bash
101
- pnpm test
102
- ```
103
-
104
- ## 发布 npm 包(维护者)
105
-
106
- ### 流程概览
107
-
108
- 1. **在分支上完成开发与合并前检查**
109
- 本地可执行 `pnpm run verify`(类型检查、单测、`build`),确认通过后再提 PR。
110
-
111
- 2. **更新 `package.json` 中的 `version`**
112
- npm 不允许重复发布同一版本号。合并进 `master` 前,在 PR 里把版本改成 registry 上尚未存在的号。
113
- - 手动改 `version` 字段,或
114
- - 在分支上执行其一(只改版本号,**不会**发包):`pnpm run version:patch` / `version:minor` / `version:major`(使用 `npm version … --no-git-tag-version`,需自行 `git add` / `commit` 版本变更)。
115
- Git 标签约定:与 `version` 对应、带前缀 **`v`**(如 `1.2.0` → 标签 `v1.2.0`)。
116
-
117
- 3. **合并到 `master`**
118
- 推送合并后的 `master` 会触发 GitHub Actions 工作流 [`.github/workflows/publish-npm.yml`](.github/workflows/publish-npm.yml):安装依赖后执行 **`pnpm run release`**(即再次 `verify` + `npm publish`)。
119
- 需在仓库 **Settings → Secrets → Actions** 中配置 **`NPM_TOKEN`**(npm 侧「Access Tokens」,建议 Automation / 具备发包权限的 granular token)。
120
-
121
- 4. **手动从 CI 再发一次(可选)**
122
- 在 GitHub **Actions → Publish to npm → Run workflow** 可手动运行同一流程(例如在修复密钥后重试)。
123
-
124
- ### 本地命令(与 CI 中的 `pnpm run release` 一致)
125
-
126
- | 命令 | 作用 |
127
- |------|------|
128
- | `pnpm run verify` | 仅检查(类型 / 测试 / 构建) |
129
- | `pnpm run release:dry` | 检查 + `npm publish --dry-run`(演练,不上传) |
130
- | `pnpm run release:test` | 检查 + 将版本打成 `*-test.*` 预发布号并发布到 **`@test` 标签** |
131
- | `pnpm run release` | 检查 + 发布 **latest**(维护者本地发包时用;**请写 `pnpm run release`**,勿用裸命令 `pnpm publish`,二者不是同一套流程) |
132
- | `pnpm run version:patch` / `version:minor` / `version:major` | 仅提升 `package.json` 版本号,不发包 |
133
-
134
- 发测试标签后,安装示例:`npm i @hi-man/himan@test`。
135
-
136
- ### CI:合并前校验与合并后打 Git 标签
137
-
138
- | 工作流 | 文件 | 说明 |
139
- |--------|------|------|
140
- | **PR version tag check** | [`.github/workflows/pr-master-version-tag.yml`](.github/workflows/pr-master-version-tag.yml) | 目标分支为 `master` 的 PR:读取 **PR 头提交**上的 `package.json` 的 `version`,若远端已存在同名标签 **`v{version}`**,则 **检查失败**(用于在合并前拦截重复版本)。 |
141
- | **Tag version on master** | [`.github/workflows/push-master-version-tag.yml`](.github/workflows/push-master-version-tag.yml) | 向 `master` **推送**后(含合并 PR):在 **当前推送提交**上创建并推送注释标签 **`v{version}`**。若标签已存在、创建或 `git push` 失败,仅输出 **告警**(`::warning::`),**工作流仍成功**,不撤销已发生的 merge;请按日志提示在本机补打标签并 `git push origin v{x.y.z}`。 |
142
-
143
- **启用「合并前拦截」**:在 GitHub **Settings → Branches** 中为 `master` 配置分支保护,勾选 **Require status checks to pass before merging**,并勾选必选检查 **`PR version tag check / version-tag-available`**(名称以仓库里 Actions 界面为准)。
152
+ ## 开发与维护
144
153
 
145
- 说明:来自 fork PR 同样会跑上述 PR 检查;打标签工作流需要 **Actions 对仓库有写权限**(工作流内已设 `contents: write`)。若组织策略禁止 `GITHUB_TOKEN` 写标签,推送标签会失败,需按告警手动推送。
154
+ 源码开发、测试和 npm 发包流程见 [docs/development.md](./docs/development.md)。
@@ -1,6 +1,7 @@
1
1
  import { promises as fs } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { simpleGit } from "simple-git";
4
+ import { HimanError, errorCodes } from "../../utils/errors.js";
4
5
  export class RepoManager {
5
6
  async cloneOrFetch(repo, targetDir) {
6
7
  const gitDir = path.join(targetDir, ".git");
@@ -12,6 +13,7 @@ export class RepoManager {
12
13
  }
13
14
  const git = simpleGit(targetDir);
14
15
  await git.fetch(["--tags", "--prune"]);
16
+ await this.fastForwardCleanWorkingTree(git);
15
17
  }
16
18
  async listTags(repoDir, pattern) {
17
19
  const git = simpleGit(repoDir);
@@ -44,14 +46,21 @@ export class RepoManager {
44
46
  await fs.writeFile(destination, content, "utf8");
45
47
  }
46
48
  }
47
- async commitTagAndPush(repoDir, message, tag, branch) {
49
+ async commitTagAndPush(repoDir, message, tag, branch, paths = ["."]) {
48
50
  const git = simpleGit(repoDir);
49
- await git.add(["."]);
50
- const status = await git.status();
51
- if (status.isClean()) {
52
- throw new Error("No changes to publish.");
51
+ const pathspecs = paths.length > 0 ? paths : ["."];
52
+ await git.add(pathspecs);
53
+ const stagedFiles = await git.raw([
54
+ "diff",
55
+ "--cached",
56
+ "--name-only",
57
+ "--",
58
+ ...pathspecs,
59
+ ]);
60
+ if (!stagedFiles.trim()) {
61
+ throw new HimanError(errorCodes.PUBLISH_NO_CHANGES, "No changes to publish.");
53
62
  }
54
- await git.commit(message);
63
+ await git.commit(message, pathspecs);
55
64
  await git.addTag(tag);
56
65
  const currentBranch = (await git.raw(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
57
66
  const targetBranch = branch ?? currentBranch;
@@ -67,4 +76,30 @@ export class RepoManager {
67
76
  return false;
68
77
  }
69
78
  }
79
+ async fastForwardCleanWorkingTree(git) {
80
+ const status = await git.status();
81
+ if (!status.isClean()) {
82
+ return;
83
+ }
84
+ const upstream = await this.getCurrentUpstream(git);
85
+ if (!upstream) {
86
+ return;
87
+ }
88
+ await git.raw(["merge", "--ff-only", upstream]);
89
+ }
90
+ async getCurrentUpstream(git) {
91
+ try {
92
+ const upstream = await git.raw([
93
+ "rev-parse",
94
+ "--abbrev-ref",
95
+ "--symbolic-full-name",
96
+ "@{u}",
97
+ ]);
98
+ const trimmed = upstream.trim();
99
+ return trimmed.length > 0 ? trimmed : undefined;
100
+ }
101
+ catch {
102
+ return undefined;
103
+ }
104
+ }
70
105
  }
@@ -30,7 +30,9 @@ export class ResourceScanner {
30
30
  type,
31
31
  entry: parsed.entry,
32
32
  description: parsed.description,
33
- targets: parsed.targets,
33
+ agents: Array.isArray(parsed.agents)
34
+ ? (parsed.agents ?? [])
35
+ : (parsed.targets ?? []),
34
36
  });
35
37
  }
36
38
  return result;
@@ -3,6 +3,7 @@ import { ResourceScanner } from "../resource/resource-scanner.js";
3
3
  import semver from "semver";
4
4
  import { HimanError, errorCodes } from "../../utils/errors.js";
5
5
  import { promises as fs } from "node:fs";
6
+ import { createHash } from "node:crypto";
6
7
  import path from "node:path";
7
8
  import YAML from "yaml";
8
9
  import { IndexCacheStore } from "../../state/index-cache-store.js";
@@ -23,13 +24,13 @@ export class GitSourceAdapter {
23
24
  const repoId = this.sourceConfig?.repoId ?? "default";
24
25
  const typeDir = this.getTypeDir(type);
25
26
  const baseDir = path.join(repoDir, typeDir);
26
- const baseDirMtimeMs = await this.getMtimeMs(baseDir);
27
+ const metadataHash = await this.getResourceMetadataHash(baseDir);
27
28
  const cached = await this.indexStore.get(repoId, type);
28
- if (cached && cached.baseDirMtimeMs === baseDirMtimeMs) {
29
+ if (cached && cached.metadataHash === metadataHash) {
29
30
  return cached.resources;
30
31
  }
31
32
  const scanned = await this.scanner.scanByType(repoDir, type);
32
- await this.indexStore.upsert(repoId, type, baseDirMtimeMs, scanned);
33
+ await this.indexStore.upsert(repoId, type, metadataHash, scanned);
33
34
  return scanned;
34
35
  }
35
36
  async history(type, name) {
@@ -47,6 +48,7 @@ export class GitSourceAdapter {
47
48
  async publish(type, name, version, sourceDir) {
48
49
  const repoDir = this.getRepoDir();
49
50
  const targetDir = path.join(repoDir, `${type}s`, name);
51
+ const metadata = await this.validatePublishResource(type, name, sourceDir);
50
52
  const sameDir = await this.isSameDirectory(sourceDir, targetDir);
51
53
  if (!sameDir) {
52
54
  await fs.rm(targetDir, { recursive: true, force: true });
@@ -54,21 +56,17 @@ export class GitSourceAdapter {
54
56
  await fs.cp(sourceDir, targetDir, { recursive: true });
55
57
  }
56
58
  const yamlPath = path.join(targetDir, "himan.yaml");
57
- if (await this.exists(yamlPath)) {
58
- const raw = await fs.readFile(yamlPath, "utf8");
59
- const parsed = YAML.parse(raw);
60
- parsed.version = version;
61
- await fs.writeFile(yamlPath, YAML.stringify(parsed), "utf8");
62
- }
59
+ metadata.version = version;
60
+ await fs.writeFile(yamlPath, YAML.stringify(metadata), "utf8");
63
61
  const tag = `${type}/${name}@${version}`;
64
- await this.repoManager.commitTagAndPush(repoDir, `publish ${type}/${name}@${version}`, tag);
62
+ await this.repoManager.commitTagAndPush(repoDir, `publish ${type}/${name}@${version}`, tag, undefined, [path.relative(repoDir, targetDir)]);
65
63
  return { version, tag };
66
64
  }
67
65
  async create(type, name, options) {
68
66
  const repoDir = this.getRepoDir();
69
67
  const resourceDir = path.join(repoDir, this.getTypeDir(type), name);
70
68
  const entry = options.entry ?? this.getDefaultEntry(type);
71
- const targets = options.targets?.length ? options.targets : ["cursor"];
69
+ const agents = options.agents?.length ? options.agents : ["cursor"];
72
70
  if ((await this.exists(resourceDir)) && !options.force) {
73
71
  throw new HimanError(errorCodes.RESOURCE_EXISTS, `Resource already exists: ${type}/${name}`);
74
72
  }
@@ -82,7 +80,7 @@ export class GitSourceAdapter {
82
80
  version: "0.1.0",
83
81
  entry,
84
82
  description: options.description ?? `${type} resource ${name}`,
85
- targets,
83
+ agents,
86
84
  }), "utf8");
87
85
  await fs.writeFile(path.join(resourceDir, entry), this.getDefaultContent(type, name), "utf8");
88
86
  }
@@ -118,14 +116,66 @@ export class GitSourceAdapter {
118
116
  return false;
119
117
  }
120
118
  }
121
- async getMtimeMs(targetPath) {
119
+ async validatePublishResource(type, name, resourceDir) {
120
+ const yamlPath = path.join(resourceDir, "himan.yaml");
121
+ if (!(await this.exists(yamlPath))) {
122
+ throw this.invalidResourceMetadata(type, name, "Missing himan.yaml for publish.", { yamlPath });
123
+ }
124
+ const raw = await fs.readFile(yamlPath, "utf8");
125
+ let parsed;
122
126
  try {
123
- const stat = await fs.stat(targetPath);
124
- return stat.mtimeMs;
127
+ parsed = YAML.parse(raw);
125
128
  }
126
- catch {
127
- return 0;
129
+ catch (error) {
130
+ throw this.invalidResourceMetadata(type, name, "himan.yaml is not valid YAML.", { yamlPath, reason: error instanceof Error ? error.message : String(error) });
131
+ }
132
+ if (!this.isRecord(parsed)) {
133
+ throw this.invalidResourceMetadata(type, name, "himan.yaml must be an object.", { yamlPath });
134
+ }
135
+ if (parsed.name !== name) {
136
+ throw this.invalidResourceMetadata(type, name, `himan.yaml name must be "${name}".`, { yamlPath, actual: parsed.name });
137
+ }
138
+ if (parsed.type !== type) {
139
+ throw this.invalidResourceMetadata(type, name, `himan.yaml type must be "${type}".`, { yamlPath, actual: parsed.type });
140
+ }
141
+ if (typeof parsed.entry !== "string" || parsed.entry.trim().length === 0) {
142
+ throw this.invalidResourceMetadata(type, name, "himan.yaml entry is required.", { yamlPath });
143
+ }
144
+ const entry = parsed.entry.trim();
145
+ const entryPath = path.resolve(resourceDir, entry);
146
+ const resourceRoot = path.resolve(resourceDir);
147
+ const relativeEntryPath = path.relative(resourceRoot, entryPath);
148
+ if (path.isAbsolute(entry) ||
149
+ relativeEntryPath === "" ||
150
+ relativeEntryPath.startsWith("..") ||
151
+ path.isAbsolute(relativeEntryPath)) {
152
+ throw this.invalidResourceMetadata(type, name, "himan.yaml entry must point to a file inside the resource directory.", { yamlPath, entry });
153
+ }
154
+ let entryStat;
155
+ try {
156
+ entryStat = await fs.stat(entryPath);
157
+ }
158
+ catch (error) {
159
+ if (!this.isNotFoundError(error)) {
160
+ throw error;
161
+ }
162
+ throw this.invalidResourceMetadata(type, name, `Resource entry file not found: ${entry}`, { yamlPath, entry, entryPath });
128
163
  }
164
+ if (!entryStat.isFile()) {
165
+ throw this.invalidResourceMetadata(type, name, `Resource entry is not a file: ${entry}`, { yamlPath, entry, entryPath });
166
+ }
167
+ return {
168
+ ...parsed,
169
+ name,
170
+ type,
171
+ entry,
172
+ };
173
+ }
174
+ invalidResourceMetadata(type, name, message, details) {
175
+ return new HimanError(errorCodes.INVALID_RESOURCE_METADATA, `Invalid metadata for ${type}/${name}: ${message}`, details);
176
+ }
177
+ isRecord(value) {
178
+ return typeof value === "object" && value !== null && !Array.isArray(value);
129
179
  }
130
180
  getTypeDir(type) {
131
181
  if (type === "rule")
@@ -134,6 +184,42 @@ export class GitSourceAdapter {
134
184
  return "commands";
135
185
  return "skills";
136
186
  }
187
+ async getResourceMetadataHash(baseDir) {
188
+ const hash = createHash("sha256");
189
+ hash.update("himan-resource-index-v1");
190
+ if (!(await this.exists(baseDir))) {
191
+ hash.update("\0missing");
192
+ return hash.digest("hex");
193
+ }
194
+ const entries = await fs.readdir(baseDir, { withFileTypes: true });
195
+ const resourceDirNames = entries
196
+ .filter((entry) => entry.isDirectory())
197
+ .map((entry) => entry.name)
198
+ .sort();
199
+ for (const resourceDirName of resourceDirNames) {
200
+ hash.update("\0dir:");
201
+ hash.update(resourceDirName);
202
+ const yamlPath = path.join(baseDir, resourceDirName, "himan.yaml");
203
+ try {
204
+ const raw = await fs.readFile(yamlPath);
205
+ hash.update("\0yaml:");
206
+ hash.update(raw);
207
+ }
208
+ catch (error) {
209
+ if (!this.isNotFoundError(error)) {
210
+ throw error;
211
+ }
212
+ hash.update("\0yaml-missing");
213
+ }
214
+ }
215
+ return hash.digest("hex");
216
+ }
217
+ isNotFoundError(error) {
218
+ return (typeof error === "object" &&
219
+ error !== null &&
220
+ "code" in error &&
221
+ error.code === "ENOENT");
222
+ }
137
223
  getDefaultEntry(type) {
138
224
  return type === "skill" ? "SKILL.md" : "content.md";
139
225
  }
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { buildProjectCli } from "../cli/index.js";
3
+ import { runCliMain } from "./shared.js";
4
+ async function main() {
5
+ await runCliMain(buildProjectCli);
6
+ }
7
+ void main();
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { buildResourceCli } from "../cli/index.js";
3
+ import { runCliMain } from "./shared.js";
4
+ async function main() {
5
+ await runCliMain(buildResourceCli);
6
+ }
7
+ void main();
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { buildSourceCli } from "../cli/index.js";
3
+ import { runCliMain } from "./shared.js";
4
+ async function main() {
5
+ await runCliMain(buildSourceCli);
6
+ }
7
+ void main();
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env node
2
+ import { buildCli } from "../cli/index.js";
3
+ import { runCliMain } from "./shared.js";
4
+ async function main() {
5
+ await runCliMain(buildCli);
6
+ }
7
+ void main();
@@ -1,8 +1,7 @@
1
- #!/usr/bin/env node
2
1
  import { CommanderError } from "commander";
3
- import { buildCli, writeCliError } from "./cli/index.js";
4
- async function main() {
5
- const program = buildCli();
2
+ import { writeCliError } from "../cli/index.js";
3
+ export async function runCliMain(buildProgram) {
4
+ const program = buildProgram();
6
5
  try {
7
6
  await program.parseAsync(process.argv);
8
7
  }
@@ -14,4 +13,3 @@ async function main() {
14
13
  process.exitCode = error instanceof CommanderError ? error.exitCode : 1;
15
14
  }
16
15
  }
17
- void main();