@hi-man/himan 0.3.3 → 0.3.5

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,10 +6,23 @@ The format is based on Keep a Changelog, and this project follows semver for the
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.3.5] - 2026-05-12
10
+
11
+ ### Changed
12
+
13
+ - Changed the CLI version shortcut from `-V` to `-v`; `--version` remains supported.
14
+ - Changed the default install mode to `copy`; pass `--mode link` to install resources as symlinks.
15
+
16
+ ## [0.3.4] - 2026-05-11
17
+
9
18
  ### Changed
10
19
 
11
20
  - Changed project guidance to require changelog updates for user-visible CLI behavior changes.
12
21
 
22
+ ### Fixed
23
+
24
+ - Fixed `himan publish` so publishing stops with `E_PUBLISH_NO_CHANGES` when the resource content matches the latest published version.
25
+
13
26
  ## [0.3.3] - 2026-05-11
14
27
 
15
28
  ### Added
package/README.md CHANGED
@@ -66,7 +66,7 @@ himan publish rule my-rule --patch
66
66
  - `command` -> `.himan/dev/command/<name>`
67
67
  - `skill` -> `.himan/dev/skill/<name>`
68
68
  - lock 文件:项目安装 `install <type> <name[@version]>` 会写入 `himan.lock`,记录 source、精确版本、agent 和安装模式;`himan install`(无参数)会按 lock 记录的 source 批量恢复安装,不受当前 default source 切换影响。`--global` 安装不写当前项目的 `himan.lock`。
69
- - 安装模式:默认 `--mode link` 使用软链;也可用 `--mode copy` 将资源复制到目标 agent 目录,lock 会记录并复现该模式。
69
+ - 安装模式:默认 `--mode copy` 将资源复制到目标 agent 目录;也可用 `--mode link` 使用软链,lock 会记录并复现该模式。
70
70
  - 默认 agent:`agent use <agent>` 默认写当前项目 `.himan/config.json`;加 `--global` 写入 `~/.himan/config.json`。当前项目配置优先于全局配置。
71
71
 
72
72
  版本以 Git tag 为准,格式:`rule/my-rule@1.0.0`。更多设计见 [docs/mvp](./docs/mvp/README.md)。
@@ -167,7 +167,7 @@ your-himan-source/
167
167
  说明:资源与项目相关命令统一使用 `--agent` 指定目标 Agent。
168
168
  若未显式传 `--agent`,`create` / `install` 会使用当前项目默认 agent、全局默认 agent、资源 metadata 或内置默认 `cursor` 中最合适的一项;`dev` 会优先使用 lock 中记录的 agent。`install --global` 会优先复用当前项目 lock 里该资源的 agent,未命中时再使用默认 install 解析顺序,但目标根目录是用户 home 下对应 agent 目录。
169
169
 
170
- `publish` 优先使用项目里 `.himan/dev` 对应目录,否则用源仓库里对应目录。若资源目录包含 `himan.yaml`,发布前会校验元数据与入口文件;若没有 `himan.yaml`,则按默认入口推断最小元数据并发布,不会强制创建 `himan.yaml`。发布需要可推送的 Git 权限。发布 commit 会包含资源目录以及自动维护的 source 根目录 `README.md` / `CHANGELOG.md`。发布成功后会从新版本 store 以 `copy` 模式重新安装到项目目标、更新 lock,并删除对应 `.himan/dev/<type>/<name>` 开发目录。
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>` 开发目录。
171
171
 
172
172
  `--json` 模式下,失败时会输出机器可读错误 JSON(`stderr`)。错误码定义见 [docs/error-codes.md](./docs/error-codes.md)。
173
173
 
@@ -4,6 +4,7 @@ import semver from "semver";
4
4
  import { HimanError, errorCodes } from "../../utils/errors.js";
5
5
  import { promises as fs } from "node:fs";
6
6
  import { createHash } from "node:crypto";
7
+ import os from "node:os";
7
8
  import path from "node:path";
8
9
  import YAML from "yaml";
9
10
  import { IndexCacheStore } from "../../state/index-cache-store.js";
@@ -52,6 +53,7 @@ export class GitSourceAdapter {
52
53
  const repoDir = this.getRepoDir();
53
54
  const targetDir = path.join(repoDir, `${type}s`, name);
54
55
  const metadataResult = await this.validatePublishResource(type, name, sourceDir);
56
+ await this.ensurePublishHasContentChanges(type, name, sourceDir);
55
57
  const sameDir = await this.isSameDirectory(sourceDir, targetDir);
56
58
  if (!sameDir) {
57
59
  await fs.rm(targetDir, { recursive: true, force: true });
@@ -261,6 +263,76 @@ export class GitSourceAdapter {
261
263
  metadata.agents = agents;
262
264
  return metadata;
263
265
  }
266
+ async ensurePublishHasContentChanges(type, name, sourceDir) {
267
+ const latest = (await this.history(type, name))[0];
268
+ if (!latest)
269
+ return;
270
+ const previousDir = await fs.mkdtemp(path.join(os.tmpdir(), "himan-publish-"));
271
+ try {
272
+ await this.repoManager.archiveResource(this.getRepoDir(), latest.raw, `${type}s/${name}`, previousDir);
273
+ const [nextSnapshot, previousSnapshot] = await Promise.all([
274
+ this.readComparableResourceSnapshot(sourceDir),
275
+ this.readComparableResourceSnapshot(previousDir),
276
+ ]);
277
+ if (this.resourceSnapshotsEqual(nextSnapshot, previousSnapshot)) {
278
+ throw new HimanError(errorCodes.PUBLISH_NO_CHANGES, `No changes to publish for ${type}/${name}.`);
279
+ }
280
+ }
281
+ finally {
282
+ await fs.rm(previousDir, { recursive: true, force: true });
283
+ }
284
+ }
285
+ async readComparableResourceSnapshot(resourceDir) {
286
+ const files = await this.listResourceFiles(resourceDir);
287
+ const snapshot = new Map();
288
+ for (const file of files) {
289
+ const relative = this.toPosixPath(path.relative(resourceDir, file));
290
+ const content = await fs.readFile(file, "utf8");
291
+ snapshot.set(relative, relative === "himan.yaml"
292
+ ? this.normalizeComparableResourceMetadata(content)
293
+ : content);
294
+ }
295
+ return snapshot;
296
+ }
297
+ async listResourceFiles(resourceDir) {
298
+ const result = [];
299
+ const entries = await fs.readdir(resourceDir, { withFileTypes: true });
300
+ for (const entry of entries) {
301
+ const fullPath = path.join(resourceDir, entry.name);
302
+ if (entry.isDirectory()) {
303
+ result.push(...(await this.listResourceFiles(fullPath)));
304
+ }
305
+ else if (entry.isFile()) {
306
+ result.push(fullPath);
307
+ }
308
+ }
309
+ return result.sort((a, b) => a.localeCompare(b));
310
+ }
311
+ normalizeComparableResourceMetadata(content) {
312
+ try {
313
+ const parsed = YAML.parse(content);
314
+ if (!this.isRecord(parsed))
315
+ return content;
316
+ const normalized = { ...parsed };
317
+ delete normalized.version;
318
+ return YAML.stringify(normalized);
319
+ }
320
+ catch {
321
+ return content;
322
+ }
323
+ }
324
+ resourceSnapshotsEqual(a, b) {
325
+ if (a.size !== b.size)
326
+ return false;
327
+ for (const [file, content] of a) {
328
+ if (b.get(file) !== content)
329
+ return false;
330
+ }
331
+ return true;
332
+ }
333
+ toPosixPath(filePath) {
334
+ return filePath.split(path.sep).join("/");
335
+ }
264
336
  invalidResourceMetadata(type, name, message, details) {
265
337
  return new HimanError(errorCodes.INVALID_RESOURCE_METADATA, `Invalid metadata for ${type}/${name}: ${message}`, details);
266
338
  }
@@ -12,7 +12,7 @@ export function createBaseProgram(name, description) {
12
12
  // Parse/usage errors are unified by writeCliError().
13
13
  },
14
14
  });
15
- program.name(name).description(description).version(PACKAGE_VERSION);
15
+ program.name(name).description(description).version(PACKAGE_VERSION, "-v, --version");
16
16
  return program;
17
17
  }
18
18
  export async function runAction(action) {
@@ -183,11 +183,11 @@ export class ServiceFactory {
183
183
  const source = await this.loadSourceFromConfig();
184
184
  return source.history(type, name);
185
185
  }
186
- async install(type, name, version, projectDir, agents, mode = "link") {
186
+ async install(type, name, version, projectDir, agents, mode = "copy") {
187
187
  const { source, sourceInfo } = await this.loadSourceWithInfoFromConfig();
188
188
  return this.installWithSource(source, sourceInfo, type, name, version, projectDir, agents, mode);
189
189
  }
190
- async installGlobal(type, name, version, projectDir, agents, mode = "link") {
190
+ async installGlobal(type, name, version, projectDir, agents, mode = "copy") {
191
191
  const source = await this.loadSourceFromConfig();
192
192
  return this.installWithSource(source, undefined, type, name, version, projectDir, agents, mode, "global");
193
193
  }
@@ -431,7 +431,7 @@ export class ServiceFactory {
431
431
  await fs.symlink(sourcePath, targetPath, "dir");
432
432
  }
433
433
  resolveInstallMode(mode) {
434
- return mode === "copy" ? "copy" : "link";
434
+ return mode === "link" ? "link" : "copy";
435
435
  }
436
436
  async resolveInstalledResource(projectDir, type, name) {
437
437
  const locked = await this.getLockedResource(projectDir, type, name);
@@ -13,7 +13,7 @@
13
13
  ```bash
14
14
  pnpm install
15
15
  pnpm run build
16
- pnpm run dev -- --help
16
+ pnpm cli --help
17
17
  node dist/bin/himan.js --help
18
18
  ```
19
19
 
@@ -28,6 +28,7 @@ pnpm test
28
28
  | 命令 | 作用 |
29
29
  |------|------|
30
30
  | `pnpm run clean` | 删除 `dist/` |
31
+ | `pnpm cli <subcommand>` | 从源码运行主 CLI |
31
32
  | `pnpm run build` | 清理并编译 TypeScript 到 `dist/` |
32
33
  | `pnpm run typecheck` | 运行 TypeScript 类型检查,不输出文件 |
33
34
  | `pnpm test` | 运行 Vitest 一次 |
@@ -116,7 +116,7 @@
116
116
  ### `E_PUBLISH_NO_CHANGES`
117
117
 
118
118
  - **含义**:发布时没有可提交的资源变更。
119
- - **常见触发**:重复发布已写入相同内容和版本的资源目录。
119
+ - **常见触发**:重复发布与最新已发布版本内容一致的资源目录;`himan.yaml` 中仅版本字段不同也会视为无内容变化。
120
120
  - **建议处理**:确认资源内容或元数据已经变更,再重新执行 `publish`。
121
121
 
122
122
  ### `E_UNSUPPORTED_RESOURCE_TYPE`
@@ -43,7 +43,7 @@
43
43
  - 也支持 `himan install`(无参数)按 `himan.lock` 批量复现安装。
44
44
  - 未指定版本则安装该资源最新 tag 对应版本。
45
45
  - 若本地 store 中已有该版本缓存,则复用、不重新从 Git 导出;否则导出到 store。
46
- - 在项目下按安装模式创建目标(默认 `--mode link` 软链;`--mode copy` 复制)。
46
+ - 在项目下按安装模式创建目标(默认 `--mode copy` 复制;`--mode link` 软链)。
47
47
  - 目标路径由 agent 和资源类型共同决定:
48
48
  - `cursor` -> `.cursor/{rules|commands|skills}/<name>`
49
49
  - `claude-code` -> `.claude/{rules|commands|skills}/<name>`
@@ -91,7 +91,7 @@
91
91
  - **领域**:资源类型、版本、路径约定
92
92
  - **适配**:Git 实现 + Registry 预留;扫描与解析元数据;版本计算;配置与全局路径
93
93
 
94
- **原则:** store 按版本目录追加、不覆盖已有缓存;开发目录与项目安装目标分离;项目侧默认以软链引用资源,也支持复制。
94
+ **原则:** store 按版本目录追加、不覆盖已有缓存;开发目录与项目安装目标分离;项目侧默认复制资源,也支持软链引用。
95
95
 
96
96
  ### 3.2 目录与数据
97
97
 
package/docs/mvp/impl.md CHANGED
@@ -44,7 +44,7 @@
44
44
  - 命令层接受 `rule|command|skill`
45
45
  - 无版本则取该资源历史中的最新 semver
46
46
  - 若本地 store 已有该版本目录则不再从 Git 导出;否则从对应 tag 导出资源树到 store
47
- - 在项目中按 agent 和资源类型创建/更新安装目标,例如 `cursor -> .cursor/{rules|commands|skills}`、`codex -> .agents/{rules|commands|skills}`;默认软链到 store 中该版本,也可通过 `--mode copy` 复制内容
47
+ - 在项目中按 agent 和资源类型创建/更新安装目标,例如 `cursor -> .cursor/{rules|commands|skills}`、`codex -> .agents/{rules|commands|skills}`;默认复制 store 中该版本,也可通过 `--mode link` 软链引用
48
48
 
49
49
  ### 2.5 `dev <type> <name>`
50
50
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hi-man/himan",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Prompt and agent asset management CLI",
5
5
  "keywords": [
6
6
  "ai",
@@ -51,7 +51,7 @@
51
51
  "clean": "rm -rf dist",
52
52
  "prebuild": "rm -rf dist",
53
53
  "build": "tsc -p tsconfig.json",
54
- "dev": "tsx src/bin/himan.ts",
54
+ "cli": "tsx src/bin/himan.ts",
55
55
  "typecheck": "tsc -p tsconfig.json --noEmit",
56
56
  "test": "vitest run",
57
57
  "test:watch": "vitest",