@hi-man/himan 0.3.2 → 0.3.4

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,27 @@ 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.4] - 2026-05-11
10
+
11
+ ### Changed
12
+
13
+ - Changed project guidance to require changelog updates for user-visible CLI behavior changes.
14
+
15
+ ### Fixed
16
+
17
+ - Fixed `himan publish` so publishing stops with `E_PUBLISH_NO_CHANGES` when the resource content matches the latest published version.
18
+
19
+ ## [0.3.3] - 2026-05-11
20
+
21
+ ### Added
22
+
23
+ - Added `himan project list` and `himan list --installed` to show resources recorded in the current project's `himan.lock`.
24
+
25
+ ### Changed
26
+
27
+ - Changed `himan list` without a resource type to group all source resources by `rule`, `command`, and `skill`.
28
+ - Added `himan list --brief` to hide resource descriptions in concise list output.
29
+
9
30
  ## [0.3.2] - 2026-05-08
10
31
 
11
32
  ### Added
package/README.md CHANGED
@@ -133,7 +133,7 @@ your-himan-source/
133
133
 
134
134
  | 命令 | 说明 |
135
135
  | -------------------------------- | ----------------------------------------------------------------------------------- |
136
- | `list [type] [--agent a,b] [--json]` | 列出当前 default source 的资源;可按 agent 过滤;`type` 默认 `rule` |
136
+ | `list [type] [--agent a,b] [--brief] [--installed] [--json]` | 默认列出当前 default source 的资源;未传 `type` 时按 `rule`/`command`/`skill` 分组展示全部资源;可按 agent 过滤;默认显示描述,`--brief` 可隐藏描述;`--installed` 改为查看当前项目 `himan.lock` 中的已安装资源 |
137
137
  | `history <type> <name> [--json]` | 按 tag 查看版本历史 |
138
138
  | `create <type> <name>` | 脚手架;常用选项:`--description`、`--agent a,b`、`--dry-run`、`--force`、`--json` |
139
139
 
@@ -141,6 +141,7 @@ your-himan-source/
141
141
 
142
142
  | 命令 | 说明 |
143
143
  | --------------------------------- | --------------------------------------------------------- |
144
+ | `list [type] [--agent a,b] [--json]` | 查看当前项目 `himan.lock` 中记录的已安装资源;未传 `type` 时按 `rule`/`command`/`skill` 分组展示 |
144
145
  | `install [type] [name[@version]] [--global] [--agent a,b] [--mode link\|copy]` | 有参数时从当前 default source 安装指定资源;**无参数**时按 `himan.lock` 记录的 source 批量安装;加 `--global` 时安装到用户级 agent 目录且不写项目 lock;可覆盖安装目标 agent 或安装模式 |
145
146
  | `dev <type> <name>` | 切换到开发态,并按安装模式将项目目标指向或复制自 `.himan/dev/...` |
146
147
  | `uninstall <type> <name>` | 从项目移除安装目标,并同步删除 `himan.lock` 条目 |
@@ -159,14 +160,14 @@ your-himan-source/
159
160
 
160
161
  - `himan resource list|history|create ...`
161
162
  - `himan-resource list|history|create ...`(兼容保留:也可执行 install/dev/uninstall/publish)
162
- - `himan project install|dev|uninstall|publish ...`
163
- - `himan-project install|dev|uninstall|publish ...`
163
+ - `himan project list|install|dev|uninstall|publish ...`
164
+ - `himan-project list|install|dev|uninstall|publish ...`
164
165
  - `himan agent list|use|current|clear ...`
165
166
 
166
167
  说明:资源与项目相关命令统一使用 `--agent` 指定目标 Agent。
167
168
  若未显式传 `--agent`,`create` / `install` 会使用当前项目默认 agent、全局默认 agent、资源 metadata 或内置默认 `cursor` 中最合适的一项;`dev` 会优先使用 lock 中记录的 agent。`install --global` 会优先复用当前项目 lock 里该资源的 agent,未命中时再使用默认 install 解析顺序,但目标根目录是用户 home 下对应 agent 目录。
168
169
 
169
- `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>` 开发目录。
170
171
 
171
172
  `--json` 模式下,失败时会输出机器可读错误 JSON(`stderr`)。错误码定义见 [docs/error-codes.md](./docs/error-codes.md)。
172
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
  }
@@ -25,7 +25,7 @@ export function buildCli() {
25
25
  registerAgentCommands(agentCmd, services);
26
26
  // Backward compatible top-level resource lifecycle commands.
27
27
  registerResourceCommands(program, services);
28
- registerProjectCommands(program, services);
28
+ registerProjectCommands(program, services, { includeList: false });
29
29
  return program;
30
30
  }
31
31
  export function buildSourceCli() {
@@ -43,7 +43,7 @@ export function buildResourceCli() {
43
43
  .description("Manage default agent configuration");
44
44
  registerAgentCommands(agentCmd, services);
45
45
  // Backward compatible: keep project lifecycle commands in himan-resource.
46
- registerProjectCommands(program, services);
46
+ registerProjectCommands(program, services, { includeList: false });
47
47
  return program;
48
48
  }
49
49
  export function buildProjectCli() {
@@ -62,10 +62,11 @@ Command groups:
62
62
  source Data source management (git now, registry reserved)
63
63
  init, source init, source add, source use, source list, source init-docs
64
64
  resource Source resource discovery and metadata
65
- list, history, create, resource list, resource history, resource create
65
+ list, list --installed, history, create,
66
+ resource list, resource history, resource create
66
67
  project Resource usage lifecycle in current project or user-level agent dirs
67
- install, dev, uninstall, publish,
68
- project install, project dev, project uninstall, project publish
68
+ list, install, dev, uninstall, publish,
69
+ project list, project install, project dev, project uninstall, project publish
69
70
  agent Default agent configuration
70
71
  agent list, agent use, agent current, agent clear
71
72
  `);
@@ -0,0 +1,46 @@
1
+ const RESOURCE_TYPES = ["rule", "command", "skill"];
2
+ export async function listInstalledResourceGroups(services, projectDir, agents) {
3
+ const resources = await services.listInstalled(projectDir, undefined, agents);
4
+ return groupInstalledResources(resources);
5
+ }
6
+ export function groupInstalledResources(resources) {
7
+ return {
8
+ rule: resources.filter((resource) => resource.type === "rule"),
9
+ command: resources.filter((resource) => resource.type === "command"),
10
+ skill: resources.filter((resource) => resource.type === "skill"),
11
+ };
12
+ }
13
+ export function writeInstalledResourceGroups(groups) {
14
+ const hasResources = RESOURCE_TYPES.some((type) => groups[type].length > 0);
15
+ if (!hasResources) {
16
+ process.stdout.write("No installed resources found.\n");
17
+ return;
18
+ }
19
+ for (const type of RESOURCE_TYPES) {
20
+ const resources = groups[type];
21
+ if (resources.length === 0)
22
+ continue;
23
+ process.stdout.write(`${formatGroupTitle(type)}:\n`);
24
+ writeInstalledResources(resources);
25
+ }
26
+ }
27
+ export function writeInstalledResources(resources) {
28
+ if (resources.length === 0) {
29
+ process.stdout.write("No installed resources found.\n");
30
+ return;
31
+ }
32
+ for (const resource of resources) {
33
+ process.stdout.write(`- ${formatInstalledResource(resource)}\n`);
34
+ }
35
+ }
36
+ function formatInstalledResource(resource) {
37
+ const agents = resource.agents.length > 0 ? ` [${resource.agents.join(", ")}]` : "";
38
+ return `${resource.type}/${resource.name}@${resource.version}${agents} (${resource.mode})`;
39
+ }
40
+ function formatGroupTitle(type) {
41
+ if (type === "rule")
42
+ return "Rules";
43
+ if (type === "command")
44
+ return "Commands";
45
+ return "Skills";
46
+ }
@@ -1,7 +1,37 @@
1
1
  import { HimanError, errorCodes } from "../utils/errors.js";
2
2
  import { getSupportedAgentNames, normalizeAgent } from "../utils/agent-configs.js";
3
+ import { listInstalledResourceGroups, writeInstalledResourceGroups, writeInstalledResources, } from "./installed-resource-list.js";
3
4
  import { runAction } from "./shared.js";
4
- export function registerProjectCommands(command, services) {
5
+ export function registerProjectCommands(command, services, options = {}) {
6
+ if (options.includeList !== false) {
7
+ command
8
+ .command("list")
9
+ .argument("[type]", "resource type")
10
+ .option("--agent <list>", "agent list filter, comma separated")
11
+ .option("--json", "output json format")
12
+ .description("List resources installed in current project")
13
+ .action(async (type, commandOptions) => {
14
+ await runAction(async () => {
15
+ const agents = parseAgents(commandOptions.agent);
16
+ if (!type) {
17
+ const groups = await listInstalledResourceGroups(services, process.cwd(), agents);
18
+ if (commandOptions.json) {
19
+ process.stdout.write(`${JSON.stringify(groups, null, 2)}\n`);
20
+ return;
21
+ }
22
+ writeInstalledResourceGroups(groups);
23
+ return;
24
+ }
25
+ const resourceType = ensureResourceType(type);
26
+ const resources = await services.listInstalled(process.cwd(), resourceType, agents);
27
+ if (commandOptions.json) {
28
+ process.stdout.write(`${JSON.stringify(resources, null, 2)}\n`);
29
+ return;
30
+ }
31
+ writeInstalledResources(resources);
32
+ });
33
+ });
34
+ }
5
35
  command
6
36
  .command("install")
7
37
  .argument("[type]", "resource type")
@@ -1,28 +1,41 @@
1
1
  import { HimanError, errorCodes } from "../utils/errors.js";
2
2
  import { getSupportedAgentNames, normalizeAgent } from "../utils/agent-configs.js";
3
+ import { listInstalledResourceGroups, writeInstalledResourceGroups, writeInstalledResources, } from "./installed-resource-list.js";
3
4
  import { runAction } from "./shared.js";
5
+ const RESOURCE_TYPES = ["rule", "command", "skill"];
4
6
  export function registerResourceCommands(command, services) {
5
7
  command
6
8
  .command("list")
7
- .argument("[type]", "resource type", "rule")
9
+ .argument("[type]", "resource type")
8
10
  .option("--agent <list>", "agent list filter, comma separated")
11
+ .option("--brief", "hide resource descriptions")
12
+ .option("--installed", "list resources installed in current project")
9
13
  .option("--json", "output json format")
10
- .description("List resources from current default source")
14
+ .description("List resources from current default source or project installs")
11
15
  .action(async (type, options) => {
12
16
  await runAction(async () => {
13
- const resourceType = ensureResourceType(type);
14
- const resources = await services.list(resourceType, parseAgents(options.agent));
15
- if (options.json) {
16
- process.stdout.write(`${JSON.stringify(resources, null, 2)}\n`);
17
+ const agents = parseAgents(options.agent);
18
+ const showDescription = !options.brief;
19
+ if (options.installed) {
20
+ await writeInstalledList(services, type, agents, Boolean(options.json));
17
21
  return;
18
22
  }
19
- if (resources.length === 0) {
20
- process.stdout.write("No resources found.\n");
23
+ if (!type) {
24
+ const groups = await listGroupedResources(services, agents);
25
+ if (options.json) {
26
+ process.stdout.write(`${JSON.stringify(formatResourceGroups(groups, showDescription), null, 2)}\n`);
27
+ return;
28
+ }
29
+ writeGroupedResources(groups, showDescription);
21
30
  return;
22
31
  }
23
- for (const resource of resources) {
24
- process.stdout.write(`- ${resource.type}/${resource.name}${resource.description ? `: ${resource.description}` : ""}\n`);
32
+ const resourceType = ensureResourceType(type);
33
+ const resources = await services.list(resourceType, agents);
34
+ if (options.json) {
35
+ process.stdout.write(`${JSON.stringify(formatResources(resources, showDescription), null, 2)}\n`);
36
+ return;
25
37
  }
38
+ writeResourceList(resources, showDescription);
26
39
  });
27
40
  });
28
41
  command
@@ -79,12 +92,82 @@ export function registerResourceCommands(command, services) {
79
92
  });
80
93
  });
81
94
  }
95
+ async function writeInstalledList(services, type, agents, json) {
96
+ if (!type) {
97
+ const groups = await listInstalledResourceGroups(services, process.cwd(), agents);
98
+ if (json) {
99
+ process.stdout.write(`${JSON.stringify(groups, null, 2)}\n`);
100
+ return;
101
+ }
102
+ writeInstalledResourceGroups(groups);
103
+ return;
104
+ }
105
+ const resourceType = ensureResourceType(type);
106
+ const resources = await services.listInstalled(process.cwd(), resourceType, agents);
107
+ if (json) {
108
+ process.stdout.write(`${JSON.stringify(resources, null, 2)}\n`);
109
+ return;
110
+ }
111
+ writeInstalledResources(resources);
112
+ }
82
113
  function ensureResourceType(type) {
83
114
  if (type !== "rule" && type !== "command" && type !== "skill") {
84
115
  throw new HimanError(errorCodes.UNSUPPORTED_RESOURCE_TYPE, `Unsupported resource type: ${type}`);
85
116
  }
86
117
  return type;
87
118
  }
119
+ async function listGroupedResources(services, agents) {
120
+ return {
121
+ rule: await services.list("rule", agents),
122
+ command: await services.list("command", agents),
123
+ skill: await services.list("skill", agents),
124
+ };
125
+ }
126
+ function formatResourceGroups(groups, showDescription) {
127
+ return {
128
+ rule: formatResources(groups.rule, showDescription),
129
+ command: formatResources(groups.command, showDescription),
130
+ skill: formatResources(groups.skill, showDescription),
131
+ };
132
+ }
133
+ function formatResources(resources, showDescription) {
134
+ if (showDescription)
135
+ return resources;
136
+ return resources.map((resource) => {
137
+ const { description: _description, ...withoutDescription } = resource;
138
+ return withoutDescription;
139
+ });
140
+ }
141
+ function writeGroupedResources(groups, showDescription) {
142
+ const hasResources = RESOURCE_TYPES.some((type) => groups[type].length > 0);
143
+ if (!hasResources) {
144
+ process.stdout.write("No resources found.\n");
145
+ return;
146
+ }
147
+ for (const type of RESOURCE_TYPES) {
148
+ const resources = groups[type];
149
+ if (resources.length === 0)
150
+ continue;
151
+ process.stdout.write(`${formatGroupTitle(type)}:\n`);
152
+ writeResourceList(resources, showDescription);
153
+ }
154
+ }
155
+ function writeResourceList(resources, showDescription) {
156
+ if (resources.length === 0) {
157
+ process.stdout.write("No resources found.\n");
158
+ return;
159
+ }
160
+ for (const resource of resources) {
161
+ process.stdout.write(`- ${resource.type}/${resource.name}${showDescription && resource.description ? `: ${resource.description}` : ""}\n`);
162
+ }
163
+ }
164
+ function formatGroupTitle(type) {
165
+ if (type === "rule")
166
+ return "Rules";
167
+ if (type === "command")
168
+ return "Commands";
169
+ return "Skills";
170
+ }
88
171
  function parseAgents(input) {
89
172
  if (!input)
90
173
  return undefined;
@@ -157,6 +157,28 @@ export class ServiceFactory {
157
157
  const selected = normalizeAgents(agents);
158
158
  return resources.filter((resource) => normalizeAgents(resource.agents).some((agent) => selected.includes(agent)));
159
159
  }
160
+ async listInstalled(projectDir, type, agents) {
161
+ const { lock, state } = await this.lockStore.loadWithState(projectDir);
162
+ if (state === "invalid") {
163
+ throw new HimanError(errorCodes.LOCK_INVALID, `Lock file is invalid: ${this.lockStore.getLockPath(projectDir)}`);
164
+ }
165
+ if (state === "missing" || !lock) {
166
+ return [];
167
+ }
168
+ const selectedAgents = agents?.length ? normalizeAgents(agents) : undefined;
169
+ return lock.resources
170
+ .filter((resource) => !type || resource.type === type)
171
+ .map((resource) => ({
172
+ type: resource.type,
173
+ name: resource.name,
174
+ version: resource.version,
175
+ agents: normalizeAgents(resource.agents),
176
+ mode: this.resolveInstallMode(resource.mode),
177
+ updatedAt: resource.updatedAt,
178
+ }))
179
+ .filter((resource) => !selectedAgents ||
180
+ resource.agents.some((agent) => selectedAgents.includes(agent)));
181
+ }
160
182
  async history(type, name) {
161
183
  const source = await this.loadSourceFromConfig();
162
184
  return source.history(type, name);
@@ -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`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hi-man/himan",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "Prompt and agent asset management CLI",
5
5
  "keywords": [
6
6
  "ai",