@hi-man/himan 0.4.0 → 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,17 @@ 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
+
9
20
  ## [0.4.0] - 2026-05-13
10
21
 
11
22
  ### Added
package/README.md CHANGED
@@ -41,15 +41,21 @@ 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
 
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 和已安装目标。
53
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>`
@@ -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`。当前项目配置优先于全局配置。
@@ -131,10 +138,12 @@ analysis:
131
138
 
132
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。
133
140
 
134
- `himan create`、`himan rename` `himan publish` 会自动维护 source 根目录文档:
141
+ `himan create` 默认在当前项目 agent 目标目录创建资源脚手架,供用户直接验证;`himan publish` 会把项目目录中的资源同步回当前 default source,并自动维护 source 根目录文档:
135
142
 
136
143
  - `README.md`:只更新 `<!-- himan:resources:start -->` 和 `<!-- himan:resources:end -->` 之间的资源索引;如果没有 marker,会在文件末尾追加一个受控资源索引区。
137
- - `CHANGELOG.md`:向 `[Unreleased]` 下追加资源变更条目;`create` 记录 `Added`,`rename` / `publish` 记录 `Changed`。
144
+ - `CHANGELOG.md`:`publish` `[Unreleased]` 下追加 `Changed` / published version 条目。
145
+
146
+ `himan rename` 也会维护 source 根目录文档:更新 README 资源索引,并向 CHANGELOG 的 `[Unreleased]` 追加 `Changed` 条目。
138
147
 
139
148
  仓库根目录的 `README.md` 和 `CHANGELOG.md` 不会被安装到 agent 目录;agent 只消费被安装的具体资源目录。当前安装实现会 materialize 资源目录本身,因此对 Cursor 这类要求特定单文件格式的 agent,资源目录内应避免放入会干扰识别的额外文件。
140
149
 
@@ -144,11 +153,13 @@ analysis:
144
153
 
145
154
  | 命令 | 说明 |
146
155
  | ----------------------------- | ------------------------------------------------ |
147
- | `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 并安装选定资源 |
148
157
  | `source add <name> <git_url>` | 添加命名 Git 源 |
149
158
  | `source use <name>` | 切换默认源 |
150
159
  | `source list [--json]` | 查看已配置源(标记当前 default) |
151
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 |
152
163
  | `source init <git_url>` | 与 `init` 等价,便于统一走 `himan source ...` 入口 |
153
164
 
154
165
  等价独立命令:
@@ -158,6 +169,8 @@ analysis:
158
169
  - `himan-source use <name>`
159
170
  - `himan-source list [--json]`
160
171
  - `himan-source init-docs [--force] [--dry-run] [--json]`
172
+ - `himan-source clone <from> <to> [...]`
173
+ - `himan-source sync <from> <to> [...]`
161
174
 
162
175
  ### 2) resource(资源)
163
176
 
@@ -165,7 +178,7 @@ analysis:
165
178
  | -------------------------------- | ----------------------------------------------------------------------------------- |
166
179
  | `list [type] [--agent a,b] [--brief] [--installed] [--json]` | 默认列出当前 default source 的资源;未传 `type` 时按 `rule`/`command`/`skill` 分组展示全部资源;可按 agent 过滤;默认显示描述,`--brief` 可隐藏描述;`--installed` 改为查看当前项目 `himan.lock` 中的已安装资源 |
167
180
  | `history <type> <name> [--json]` | 按 tag 查看版本历史 |
168
- | `create <type> <name>` | 脚手架;常用选项:`--description`、`--agent a,b`、`--dry-run`、`--force`、`--json` |
181
+ | `create <type> <name>` | 在当前项目 agent 目录创建脚手架;常用选项:`--description`、`--agent a,b`、`--dry-run`、`--force`、`--json` |
169
182
  | `rename <type> <old-name> <new-name>` | 暂不推荐使用;重命名当前 default source 中的资源;常用选项:`--dry-run`、`--no-project`、`--json` |
170
183
 
171
184
  ### 3) project(当前项目)
@@ -174,9 +187,9 @@ analysis:
174
187
  | --------------------------------- | --------------------------------------------------------- |
175
188
  | `list [type] [--agent a,b] [--json]` | 查看当前项目 `himan.lock` 中记录的已安装资源;未传 `type` 时按 `rule`/`command`/`skill` 分组展示 |
176
189
  | `install [type] [name[@version]] [--global] [--agent a,b] [--mode link\|copy]` | 有参数时从当前 default source 安装指定资源;**无参数**时按 `himan.lock` 记录的 source 批量安装;加 `--global` 时安装到用户级 agent 目录且不写项目 lock;可覆盖安装目标 agent 或安装模式 |
177
- | `dev <type> <name>` | 切换到开发态,并按安装模式将项目目标指向或复制自 `.himan/dev/...` |
190
+ | `dev <type> <name>` | 切换到开发态;项目资源原地编辑,全局资源先复制到当前项目目标目录 |
178
191
  | `uninstall <type> <name>` | 从项目移除安装目标,并同步删除 `himan.lock` 条目 |
179
- | `publish <type> <name>` | 默认 `--patch`;可选 `--minor` / `--major`(勿同时使用多个) |
192
+ | `publish <type> <name> [--global]` | 默认 `--patch`;可选 `--minor` / `--major`(勿同时使用多个);发布后默认安装到当前项目并更新 lock,`--global` 安装到用户级目录 |
180
193
 
181
194
  ### 4) agent(默认 Agent)
182
195
 
@@ -187,6 +200,12 @@ analysis:
187
200
  | `agent current [--json]` | 查看当前项目、全局和最终生效的默认 agent |
188
201
  | `agent clear [--project\|--global] [--json]` | 清除当前项目或全局默认 agent;默认 `--project` |
189
202
 
203
+ ### 5) doctor(可用性检查)
204
+
205
+ | 命令 | 说明 |
206
+ |------|------|
207
+ | `doctor [--json]` | 检查 Node/Git、Himan home、当前 source、资源扫描、默认 agent、项目 lock 和已安装目标;存在 error 时以非零状态退出 |
208
+
190
209
  也可使用分组命令(与上面等价):
191
210
 
192
211
  - `himan resource list|history|create|rename ...`
@@ -194,11 +213,12 @@ analysis:
194
213
  - `himan project list|install|dev|uninstall|publish ...`
195
214
  - `himan-project list|install|dev|uninstall|publish ...`
196
215
  - `himan agent list|use|current|clear ...`
216
+ - `himan doctor ...`
197
217
 
198
218
  说明:资源与项目相关命令统一使用 `--agent` 指定目标 Agent。
199
- 若未显式传 `--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 目录。
200
220
 
201
- `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>` 开发目录。
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。
202
222
 
203
223
  `rename` 暂不推荐使用。该命令会移动 source 仓库里的资源目录并更新资源 metadata 名称、README 资源索引和 CHANGELOG。已有发布 tag 不会被改写;若旧资源已有历史版本,rename 会为新名字创建一个指向当前最新版本的 tag。默认会迁移当前项目中对应的安装目标、`.himan/dev` 副本和 lock 条目;传 `--no-project` 时只改 source。对于 skill,命令只自动更新 metadata / front matter 中的精确 `name` 字段,不会自动替换 `SKILL.md` 正文中的旧名称引用。
204
224
 
@@ -1,5 +1,6 @@
1
1
  import { ServiceFactory } from "../services/index.js";
2
2
  import { registerAgentCommands } from "./agent-commands.js";
3
+ import { registerDoctorCommand } from "./doctor-command.js";
3
4
  import { registerProjectCommands } from "./project-commands.js";
4
5
  import { registerResourceCommands } from "./resource-commands.js";
5
6
  import { registerInitCommand, registerSourceCommands } from "./source-commands.js";
@@ -9,6 +10,7 @@ export function buildCli() {
9
10
  const services = new ServiceFactory();
10
11
  appendCommandGroupsHelp(program);
11
12
  registerInitCommand(program, services);
13
+ registerDoctorCommand(program, services);
12
14
  const sourceCmd = program.command("source").description("Manage source repositories");
13
15
  registerSourceCommands(sourceCmd, services, { includeInit: true });
14
16
  const resourceCmd = program
@@ -70,5 +72,7 @@ Command groups:
70
72
  project list, project install, project dev, project uninstall, project publish
71
73
  agent Default agent configuration
72
74
  agent list, agent use, agent current, agent clear
75
+ doctor Runtime and project health checks
76
+ doctor
73
77
  `);
74
78
  }
@@ -0,0 +1,30 @@
1
+ import { runAction } from "./shared.js";
2
+ export function registerDoctorCommand(command, services) {
3
+ command
4
+ .command("doctor")
5
+ .option("--json", "output json format")
6
+ .description("Check Himan runtime and project health")
7
+ .action(async (options) => {
8
+ await runAction(async () => {
9
+ const result = await services.doctor(process.cwd());
10
+ if (options.json) {
11
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
12
+ }
13
+ else {
14
+ writeDoctorResult(result);
15
+ }
16
+ if (!result.ok) {
17
+ process.exitCode = 1;
18
+ }
19
+ });
20
+ });
21
+ }
22
+ function writeDoctorResult(result) {
23
+ process.stdout.write("Himan doctor\n");
24
+ for (const check of result.checks) {
25
+ process.stdout.write(`${formatCheckStatus(check)} ${check.name}: ${check.message}\n`);
26
+ }
27
+ }
28
+ function formatCheckStatus(check) {
29
+ return `[${check.status}]`;
30
+ }
@@ -82,7 +82,11 @@ export function registerProjectCommands(command, services, options = {}) {
82
82
  await runAction(async () => {
83
83
  const resourceType = ensureResourceType(type);
84
84
  const result = await services.dev(resourceType, name, process.cwd());
85
- process.stdout.write(`Switched ${result.type}/${result.name} to dev mode: ${result.devPath}\n`);
85
+ if (result.sourceScope === "global") {
86
+ process.stdout.write(`Copied global ${result.type}/${result.name} into current project: ${result.devPath}\n`);
87
+ return;
88
+ }
89
+ process.stdout.write(`Editing ${result.type}/${result.name} in place: ${result.devPath}\n`);
86
90
  });
87
91
  });
88
92
  command
@@ -104,13 +108,23 @@ export function registerProjectCommands(command, services, options = {}) {
104
108
  .option("--patch", "patch release")
105
109
  .option("--minor", "minor release")
106
110
  .option("--major", "major release")
111
+ .option("--global", "install the published version into user-level agent directories")
107
112
  .description("Publish resource (default: --patch)")
108
113
  .action(async (type, name, options) => {
109
114
  await runAction(async () => {
110
115
  const resourceType = ensureResourceType(type);
111
116
  const releaseType = resolveReleaseType(options);
112
- const result = await services.publish(resourceType, name, releaseType, process.cwd());
113
- process.stdout.write(`Published ${result.type}/${result.name}@${result.version}\n`);
117
+ const installScope = options.global ? "global" : "project";
118
+ process.stdout.write(options.global
119
+ ? "Published resource will be installed globally; current project lock will not be updated.\n"
120
+ : "Published resource will be installed into the current project and recorded in himan.lock. Use --global to install globally instead.\n");
121
+ const result = await services.publish(resourceType, name, releaseType, process.cwd(), {
122
+ installScope,
123
+ onProgress: (progress) => {
124
+ process.stdout.write(`[publish:${progress.stage}] ${progress.message}\n`);
125
+ },
126
+ });
127
+ process.stdout.write(`Published ${result.type}/${result.name}@${result.version} and installed ${result.installScope === "global" ? "globally" : "into current project"}\n`);
114
128
  });
115
129
  });
116
130
  }
@@ -1,12 +1,41 @@
1
+ import { getSupportedAgentNames, normalizeAgent } from "../utils/agent-configs.js";
2
+ import { HimanError, errorCodes } from "../utils/errors.js";
1
3
  import { runAction } from "./shared.js";
2
4
  export function registerInitCommand(command, services) {
3
5
  command
4
6
  .command("init")
5
7
  .argument("<git_repo>", "Git repository URL")
6
- .action(async (gitRepo) => {
8
+ .option("--agent <list>", "set current project default agents, comma separated")
9
+ .option("--install <refs>", "install resource refs after init, comma separated: rule/name[@version]")
10
+ .option("--mode <mode>", "install mode for --install: link or copy")
11
+ .option("--json", "output json format")
12
+ .action(async (gitRepo, options) => {
7
13
  await runAction(async () => {
8
- const result = await services.initSource("git", gitRepo);
9
- process.stdout.write(`Initialized ${result.sourceType} source: ${result.repo}\n`);
14
+ const agents = parseAgents(options.agent);
15
+ const installRefs = parseInstallRefs(options.install);
16
+ const mode = parseInstallMode(options.mode);
17
+ if (mode && installRefs.length === 0) {
18
+ throw new HimanError(errorCodes.CLI_USAGE, "Use --mode only with --install.");
19
+ }
20
+ const source = await services.initSource("git", gitRepo);
21
+ const agentResult = agents?.length
22
+ ? await services.setAgents(agents, "project", process.cwd())
23
+ : undefined;
24
+ const installed = [];
25
+ for (const ref of installRefs) {
26
+ installed.push(await services.install(ref.type, ref.name, ref.version, process.cwd(), agents, mode));
27
+ }
28
+ if (options.json) {
29
+ process.stdout.write(`${JSON.stringify({ source, agents: agentResult, installed }, null, 2)}\n`);
30
+ return;
31
+ }
32
+ process.stdout.write(`Initialized ${source.sourceType} source: ${source.repo}\n`);
33
+ if (agentResult) {
34
+ process.stdout.write(`Using agents (${agentResult.scope}): ${agentResult.agents.join(", ")}\n`);
35
+ }
36
+ for (const item of installed) {
37
+ process.stdout.write(`Installed ${item.type}/${item.name}@${item.version}\n`);
38
+ }
10
39
  });
11
40
  });
12
41
  }
@@ -158,3 +187,67 @@ export function registerSourceCommands(command, services, options) {
158
187
  });
159
188
  });
160
189
  }
190
+ function ensureResourceType(type) {
191
+ if (type !== "rule" && type !== "command" && type !== "skill") {
192
+ throw new HimanError(errorCodes.UNSUPPORTED_RESOURCE_TYPE, `Unsupported resource type: ${type}`);
193
+ }
194
+ return type;
195
+ }
196
+ function parseInstallRefs(input) {
197
+ if (!input)
198
+ return [];
199
+ const refs = input
200
+ .split(",")
201
+ .map((item) => item.trim())
202
+ .filter(Boolean);
203
+ if (refs.length === 0) {
204
+ throw new HimanError(errorCodes.INVALID_INPUT, "Install list cannot be empty.");
205
+ }
206
+ return refs.map((ref) => {
207
+ const parts = ref.split("/");
208
+ if (parts.length !== 2 || !parts[0] || !parts[1]) {
209
+ throw new HimanError(errorCodes.INVALID_INPUT, `Invalid install ref: ${ref}. Use type/name[@version].`);
210
+ }
211
+ const { name, version } = parseNameVersion(parts[1]);
212
+ if (!name) {
213
+ throw new HimanError(errorCodes.INVALID_INPUT, `Invalid install ref: ${ref}. Use type/name[@version].`);
214
+ }
215
+ return {
216
+ type: ensureResourceType(parts[0]),
217
+ name,
218
+ version,
219
+ };
220
+ });
221
+ }
222
+ function parseNameVersion(input) {
223
+ const idx = input.lastIndexOf("@");
224
+ if (idx <= 0)
225
+ return { name: input };
226
+ return { name: input.slice(0, idx), version: input.slice(idx + 1) };
227
+ }
228
+ function parseInstallMode(input) {
229
+ if (!input)
230
+ return undefined;
231
+ const normalized = input.trim().toLowerCase();
232
+ if (normalized === "link" || normalized === "copy") {
233
+ return normalized;
234
+ }
235
+ throw new HimanError(errorCodes.INVALID_INPUT, `Unsupported install mode: ${input}. Supported modes: link, copy`);
236
+ }
237
+ function parseAgents(input) {
238
+ if (!input)
239
+ return undefined;
240
+ const agents = input
241
+ .split(",")
242
+ .map((item) => item.trim())
243
+ .filter(Boolean);
244
+ if (agents.length === 0)
245
+ return undefined;
246
+ const supported = getSupportedAgentNames();
247
+ for (const agent of agents) {
248
+ if (!normalizeAgent(agent)) {
249
+ throw new HimanError(errorCodes.INVALID_INPUT, `Unsupported agent: ${agent}. Supported agents: ${supported.join(", ")}`);
250
+ }
251
+ }
252
+ return agents;
253
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -8,9 +8,13 @@ import { toRepoId } from "../utils/repo-id.js";
8
8
  import { HimanError, errorCodes } from "../utils/errors.js";
9
9
  import { getGlobalResourcePaths, getProjectResourcePaths, getSupportedAgentNames, normalizeAgents, } from "../utils/agent-configs.js";
10
10
  import path from "node:path";
11
+ import { execFile } from "node:child_process";
11
12
  import { promises as fs } from "node:fs";
13
+ import { promisify } from "node:util";
12
14
  import { VersionResolver } from "../adapters/version/version-resolver.js";
13
15
  import YAML from "yaml";
16
+ const execFileAsync = promisify(execFile);
17
+ const RESOURCE_TYPES = ["rule", "command", "skill"];
14
18
  export class ServiceFactory {
15
19
  stateStore = new StateStore();
16
20
  projectConfigStore = new ProjectConfigStore();
@@ -176,6 +180,45 @@ export class ServiceFactory {
176
180
  supported: getSupportedAgentNames(),
177
181
  };
178
182
  }
183
+ async doctor(projectDir) {
184
+ const checks = [];
185
+ checks.push(this.checkNodeVersion());
186
+ checks.push(await this.checkGit());
187
+ checks.push(await this.checkHomeState());
188
+ const config = await this.stateStore.loadConfig();
189
+ if (!config?.source) {
190
+ checks.push({
191
+ name: "source",
192
+ status: "error",
193
+ message: "No source configured. Run `himan init <git_repo>` first.",
194
+ details: { configPath: this.stateStore.getConfigPath() },
195
+ });
196
+ }
197
+ else {
198
+ const currentName = config.sources?.default ?? "default";
199
+ const currentSource = config.sources?.items[currentName] ?? config.source;
200
+ checks.push({
201
+ name: "source",
202
+ status: "ok",
203
+ message: `Using ${currentSource.type} source ${currentName}.`,
204
+ details: {
205
+ name: currentName,
206
+ type: currentSource.type,
207
+ repo: currentSource.repo,
208
+ repoId: currentSource.repoId,
209
+ },
210
+ });
211
+ checks.push(await this.checkSourceResources());
212
+ }
213
+ checks.push(await this.checkAgents(projectDir));
214
+ const lockCheck = await this.checkProjectLock(projectDir);
215
+ checks.push(lockCheck.check);
216
+ checks.push(await this.checkProjectTargets(projectDir, lockCheck.lock));
217
+ return {
218
+ ok: !checks.some((check) => check.status === "error"),
219
+ checks,
220
+ };
221
+ }
179
222
  async clearAgents(scope, projectDir) {
180
223
  if (scope === "project") {
181
224
  await this.projectConfigStore.clearAgents(projectDir);
@@ -188,6 +231,172 @@ export class ServiceFactory {
188
231
  });
189
232
  return { scope };
190
233
  }
234
+ checkNodeVersion() {
235
+ const version = process.versions.node;
236
+ const major = Number(version.split(".")[0]);
237
+ if (major === 22) {
238
+ return {
239
+ name: "node",
240
+ status: "ok",
241
+ message: `Node.js ${version}.`,
242
+ };
243
+ }
244
+ return {
245
+ name: "node",
246
+ status: "error",
247
+ message: `Node.js ${version} is unsupported. Use Node.js 22.x.`,
248
+ };
249
+ }
250
+ async checkGit() {
251
+ try {
252
+ const result = await execFileAsync("git", ["--version"]);
253
+ return {
254
+ name: "git",
255
+ status: "ok",
256
+ message: result.stdout.trim() || "Git is available.",
257
+ };
258
+ }
259
+ catch (error) {
260
+ return this.errorCheck("git", "Git is not available on PATH.", error);
261
+ }
262
+ }
263
+ async checkHomeState() {
264
+ const root = this.paths.getHimanRoot();
265
+ const reposDir = this.paths.getReposDir();
266
+ const storeDir = this.paths.getStoreDir();
267
+ const missing = [];
268
+ if (!(await this.exists(root)))
269
+ missing.push(root);
270
+ if (!(await this.exists(reposDir)))
271
+ missing.push(reposDir);
272
+ if (!(await this.exists(storeDir)))
273
+ missing.push(storeDir);
274
+ if (missing.length > 0) {
275
+ return {
276
+ name: "home",
277
+ status: "warn",
278
+ message: "Himan home directories are not fully initialized yet.",
279
+ details: { root, reposDir, storeDir, missing },
280
+ };
281
+ }
282
+ return {
283
+ name: "home",
284
+ status: "ok",
285
+ message: `Himan home is initialized at ${root}.`,
286
+ details: { root, reposDir, storeDir },
287
+ };
288
+ }
289
+ async checkSourceResources() {
290
+ try {
291
+ const source = await this.loadSourceFromConfig();
292
+ const entries = await Promise.all(RESOURCE_TYPES.map(async (type) => [type, await source.list(type)]));
293
+ const counts = Object.fromEntries(entries.map(([type, resources]) => [type, resources.length]));
294
+ const total = RESOURCE_TYPES.reduce((sum, type) => sum + counts[type], 0);
295
+ return {
296
+ name: "resources",
297
+ status: "ok",
298
+ message: `Scanned ${total} resources from current source.`,
299
+ details: { counts },
300
+ };
301
+ }
302
+ catch (error) {
303
+ return this.errorCheck("resources", "Cannot scan current source resources.", error);
304
+ }
305
+ }
306
+ async checkAgents(projectDir) {
307
+ try {
308
+ const settings = await this.getAgentSettings(projectDir);
309
+ const scope = settings.project ? "project" : settings.global ? "global" : "default";
310
+ return {
311
+ name: "agents",
312
+ status: "ok",
313
+ message: `Effective agents: ${settings.effective.join(", ")} (${scope}).`,
314
+ details: settings,
315
+ };
316
+ }
317
+ catch (error) {
318
+ return this.errorCheck("agents", "Cannot resolve effective agents.", error);
319
+ }
320
+ }
321
+ async checkProjectLock(projectDir) {
322
+ const lockPath = this.lockStore.getLockPath(projectDir);
323
+ const { lock, state } = await this.lockStore.loadWithState(projectDir);
324
+ if (state === "missing") {
325
+ return {
326
+ check: {
327
+ name: "lock",
328
+ status: "ok",
329
+ message: "No himan.lock found for this project yet.",
330
+ details: { lockPath },
331
+ },
332
+ };
333
+ }
334
+ if (state === "invalid" || !lock) {
335
+ return {
336
+ check: {
337
+ name: "lock",
338
+ status: "error",
339
+ message: `Lock file is invalid: ${lockPath}`,
340
+ details: { lockPath },
341
+ },
342
+ };
343
+ }
344
+ if (lock.resources.length === 0) {
345
+ return {
346
+ check: {
347
+ name: "lock",
348
+ status: "warn",
349
+ message: "himan.lock has no resources.",
350
+ details: { lockPath },
351
+ },
352
+ lock,
353
+ };
354
+ }
355
+ return {
356
+ check: {
357
+ name: "lock",
358
+ status: "ok",
359
+ message: `himan.lock tracks ${lock.resources.length} resources.`,
360
+ details: { lockPath, source: lock.source },
361
+ },
362
+ lock,
363
+ };
364
+ }
365
+ async checkProjectTargets(projectDir, lock) {
366
+ if (!lock || lock.resources.length === 0) {
367
+ return {
368
+ name: "targets",
369
+ status: "ok",
370
+ message: "No locked project targets to verify.",
371
+ };
372
+ }
373
+ const missing = [];
374
+ for (const resource of lock.resources) {
375
+ const agents = normalizeAgents(resource.agents);
376
+ const targets = getProjectResourcePaths(projectDir, resource.type, resource.name, agents);
377
+ for (const targetPath of targets) {
378
+ if (!(await this.exists(targetPath))) {
379
+ missing.push({
380
+ resource: `${resource.type}/${resource.name}@${resource.version}`,
381
+ path: targetPath,
382
+ });
383
+ }
384
+ }
385
+ }
386
+ if (missing.length > 0) {
387
+ return {
388
+ name: "targets",
389
+ status: "warn",
390
+ message: `Missing ${missing.length} installed targets. Run \`himan install\` to restore them.`,
391
+ details: { missing },
392
+ };
393
+ }
394
+ return {
395
+ name: "targets",
396
+ status: "ok",
397
+ message: "All locked project targets exist.",
398
+ };
399
+ }
191
400
  async list(type, agents) {
192
401
  const source = await this.loadSourceFromConfig();
193
402
  const resources = await source.list(type);
@@ -231,22 +440,33 @@ export class ServiceFactory {
231
440
  return this.installWithSource(source, undefined, type, name, version, projectDir, agents, mode, "global");
232
441
  }
233
442
  async dev(type, name, projectDir) {
234
- const installInfo = await this.resolveInstalledResource(projectDir, type, name);
235
- const installedPath = installInfo.installedPath;
236
- const devPath = this.getProjectDevPath(projectDir, type, name);
237
- if (!(await this.exists(devPath))) {
238
- await fs.mkdir(path.dirname(devPath), { recursive: true });
239
- await fs.cp(installedPath, devPath, { recursive: true });
443
+ const projectTarget = await this.tryResolveProjectResourceTarget(projectDir, type, name);
444
+ if (projectTarget) {
445
+ return {
446
+ type,
447
+ name,
448
+ devPath: projectTarget.resourcePath,
449
+ linkPath: projectTarget.linkPaths[0],
450
+ mode: projectTarget.mode,
451
+ sourceScope: "project",
452
+ };
240
453
  }
241
- for (const linkPath of installInfo.linkPaths) {
242
- await this.materializeResource(devPath, linkPath, installInfo.mode);
454
+ const globalTarget = await this.tryResolveGlobalResourceTarget(projectDir, type, name);
455
+ if (!globalTarget) {
456
+ throw new HimanError(errorCodes.INSTALL_NOT_FOUND, `Installed resource link not found for ${type}/${name}. Run install first.`);
457
+ }
458
+ const projectLinkPaths = getProjectResourcePaths(projectDir, type, name, globalTarget.agents);
459
+ for (const [index, linkPath] of projectLinkPaths.entries()) {
460
+ const sourcePath = globalTarget.linkPaths[index] ?? globalTarget.resourcePath;
461
+ await this.materializeResource(sourcePath, linkPath, "copy");
243
462
  }
244
463
  return {
245
464
  type,
246
465
  name,
247
- devPath,
248
- linkPath: installInfo.linkPaths[0],
249
- mode: installInfo.mode,
466
+ devPath: projectLinkPaths[0],
467
+ linkPath: projectLinkPaths[0],
468
+ mode: "copy",
469
+ sourceScope: "global",
250
470
  };
251
471
  }
252
472
  async uninstall(type, name, projectDir) {
@@ -260,58 +480,117 @@ export class ServiceFactory {
260
480
  await this.lockStore.removeResource(projectDir, { type, name });
261
481
  return { type, name, linkPath: installInfo.linkPaths[0] };
262
482
  }
263
- async publish(type, name, releaseType, projectDir) {
483
+ async publish(type, name, releaseType, projectDir, options = {}) {
484
+ const installScope = options.installScope ?? "project";
485
+ this.reportPublishProgress(options, "prepare", `Preparing ${type}/${name}.`);
264
486
  const source = await this.loadSourceFromConfig();
265
487
  const sourceDir = await this.resolvePublishSourceDir(type, name, projectDir);
266
488
  const existingInstallInfo = await this.tryResolveInstalledResource(projectDir, type, name);
489
+ const existingGlobalInstallInfo = await this.tryResolveGlobalResourceTarget(projectDir, type, name);
267
490
  const history = await source.history(type, name);
268
491
  const latest = history[0]?.version ?? "0.0.0";
269
492
  const nextVersion = this.versions.nextVersion(latest, releaseType);
493
+ this.reportPublishProgress(options, "resolve-version", `Resolved ${releaseType} version ${nextVersion}.`);
494
+ this.reportPublishProgress(options, "publish-source", `Publishing ${type}/${name}@${nextVersion} to the Git source.`);
270
495
  const result = await source.publish(type, name, nextVersion, sourceDir, {
271
496
  releaseType,
272
497
  });
273
498
  const storePath = this.getStorePath(type, name, nextVersion);
499
+ this.reportPublishProgress(options, "sync-store", `Syncing ${type}/${name}@${nextVersion} into the local store.`);
274
500
  if (!(await this.exists(storePath))) {
275
501
  await source.pull(type, name, nextVersion, storePath);
276
502
  }
277
503
  const locked = await this.getLockedResource(projectDir, type, name);
278
504
  const resourceMeta = await this.readResourceMetaFromDir(storePath, type);
279
505
  const configuredAgents = await this.getConfiguredAgents(projectDir);
280
- const nextAgents = locked?.agents?.length
281
- ? normalizeAgents(locked.agents)
282
- : existingInstallInfo?.agents.length
283
- ? normalizeAgents(existingInstallInfo.agents)
284
- : configuredAgents ?? normalizeAgents(resourceMeta?.agents);
285
506
  const installMode = "copy";
286
- const linkPaths = getProjectResourcePaths(projectDir, type, name, nextAgents);
507
+ const nextAgents = installScope === "global"
508
+ ? existingGlobalInstallInfo?.agents.length
509
+ ? normalizeAgents(existingGlobalInstallInfo.agents)
510
+ : existingInstallInfo?.agents.length
511
+ ? normalizeAgents(existingInstallInfo.agents)
512
+ : locked?.agents?.length
513
+ ? normalizeAgents(locked.agents)
514
+ : configuredAgents ?? normalizeAgents(resourceMeta?.agents)
515
+ : locked?.agents?.length
516
+ ? normalizeAgents(locked.agents)
517
+ : existingInstallInfo?.agents.length
518
+ ? normalizeAgents(existingInstallInfo.agents)
519
+ : configuredAgents ?? normalizeAgents(resourceMeta?.agents);
520
+ const linkPaths = installScope === "global"
521
+ ? getGlobalResourcePaths(this.paths.getHomeDir(), type, name, nextAgents)
522
+ : getProjectResourcePaths(projectDir, type, name, nextAgents);
523
+ this.reportPublishProgress(options, "install", installScope === "global"
524
+ ? `Installing published version globally for ${nextAgents.join(", ")}.`
525
+ : `Installing published version into the current project for ${nextAgents.join(", ")}.`);
287
526
  for (const linkPath of linkPaths) {
288
527
  await this.materializeResource(storePath, linkPath, installMode);
289
528
  }
290
- const sourceInfo = await this.getLockSourceInfo();
291
- await this.lockStore.upsertResource(projectDir, sourceInfo, {
292
- type,
293
- name,
294
- version: nextVersion,
295
- agents: nextAgents,
296
- mode: installMode,
297
- });
529
+ if (installScope === "project") {
530
+ const sourceInfo = await this.getLockSourceInfo();
531
+ await this.lockStore.upsertResource(projectDir, sourceInfo, {
532
+ type,
533
+ name,
534
+ version: nextVersion,
535
+ agents: nextAgents,
536
+ mode: installMode,
537
+ });
538
+ }
539
+ this.reportPublishProgress(options, "cleanup", `Cleaning up legacy dev copy if present.`);
298
540
  await fs.rm(this.getProjectDevPath(projectDir, type, name), {
299
541
  recursive: true,
300
542
  force: true,
301
543
  });
302
- return { type, name, version: result.version, tag: result.tag };
544
+ this.reportPublishProgress(options, "done", `Published ${type}/${name}@${result.version}.`);
545
+ return {
546
+ type,
547
+ name,
548
+ version: result.version,
549
+ tag: result.tag,
550
+ installScope,
551
+ linkPath: linkPaths[0],
552
+ };
303
553
  }
304
554
  async create(type, name, options, projectDir) {
305
555
  this.validateCreateInput(type, name, options);
306
- const source = await this.loadSourceFromConfig();
307
- return source.create(type, name, {
308
- description: options.description,
309
- agents: await this.resolveEffectiveAgents(projectDir, options.agents),
310
- entry: options.entry,
311
- template: options.template ?? "basic",
312
- force: options.force,
313
- dryRun: options.dryRun,
314
- });
556
+ await this.loadSourceFromConfig();
557
+ const agents = await this.resolveEffectiveAgents(projectDir, options.agents);
558
+ const resourcePaths = getProjectResourcePaths(projectDir, type, name, agents);
559
+ const entry = options.entry ?? this.getDefaultEntry(type);
560
+ const files = resourcePaths.flatMap((resourcePath) => [
561
+ path.join(resourcePath, "himan.yaml"),
562
+ path.join(resourcePath, entry),
563
+ ]);
564
+ const existingPaths = [];
565
+ for (const resourcePath of resourcePaths) {
566
+ if (await this.exists(resourcePath))
567
+ existingPaths.push(resourcePath);
568
+ }
569
+ if (existingPaths.length > 0 && !options.force) {
570
+ throw new HimanError(errorCodes.RESOURCE_EXISTS, `Resource already exists: ${type}/${name}`, { paths: existingPaths });
571
+ }
572
+ if (!options.dryRun) {
573
+ for (const resourcePath of resourcePaths) {
574
+ await fs.rm(resourcePath, { recursive: true, force: true });
575
+ await fs.mkdir(resourcePath, { recursive: true });
576
+ await fs.writeFile(path.join(resourcePath, "himan.yaml"), YAML.stringify({
577
+ name,
578
+ type,
579
+ version: "0.1.0",
580
+ entry,
581
+ description: options.description ?? `${type} resource ${name}`,
582
+ agents,
583
+ }), "utf8");
584
+ await fs.writeFile(path.join(resourcePath, entry), this.getDefaultContent(type, name), "utf8");
585
+ }
586
+ }
587
+ return {
588
+ type,
589
+ name,
590
+ resourceDir: resourcePaths[0],
591
+ files,
592
+ dryRun: Boolean(options.dryRun),
593
+ };
315
594
  }
316
595
  async rename(type, oldName, newName, projectDir, options = {}) {
317
596
  this.validateRenameInput(type, oldName, newName);
@@ -636,6 +915,80 @@ export class ServiceFactory {
636
915
  resolveInstallMode(mode) {
637
916
  return mode === "link" ? "link" : "copy";
638
917
  }
918
+ reportPublishProgress(options, stage, message) {
919
+ options.onProgress?.({ stage, message });
920
+ }
921
+ async tryResolveProjectResourceTarget(projectDir, type, name) {
922
+ const locked = await this.getLockedResource(projectDir, type, name);
923
+ const configuredAgents = await this.getConfiguredAgents(projectDir);
924
+ if (locked?.agents?.length || configuredAgents?.length) {
925
+ const agents = locked?.agents?.length
926
+ ? normalizeAgents(locked.agents)
927
+ : (configuredAgents ?? normalizeAgents());
928
+ const linkPaths = getProjectResourcePaths(projectDir, type, name, agents);
929
+ const existingLinkPath = await this.findFirstExistingPath(linkPaths);
930
+ if (existingLinkPath) {
931
+ return {
932
+ resourcePath: existingLinkPath,
933
+ linkPaths,
934
+ agents,
935
+ mode: this.resolveInstallMode(locked?.mode ?? (await this.readPathMode(existingLinkPath))),
936
+ };
937
+ }
938
+ }
939
+ const existingCandidates = await this.findExistingAgentPaths(projectDir, type, name, "project");
940
+ if (existingCandidates.length === 0) {
941
+ return undefined;
942
+ }
943
+ const existingAgents = normalizeAgents(existingCandidates.map((candidate) => candidate.agent));
944
+ const linkPaths = getProjectResourcePaths(projectDir, type, name, existingAgents);
945
+ return {
946
+ resourcePath: existingCandidates[0].path,
947
+ linkPaths,
948
+ agents: existingAgents,
949
+ mode: await this.readPathMode(existingCandidates[0].path),
950
+ };
951
+ }
952
+ async tryResolveGlobalResourceTarget(projectDir, type, name) {
953
+ const existingCandidates = await this.findExistingAgentPaths(projectDir, type, name, "global");
954
+ if (existingCandidates.length === 0) {
955
+ return undefined;
956
+ }
957
+ const agents = normalizeAgents(existingCandidates.map((candidate) => candidate.agent));
958
+ const linkPaths = getGlobalResourcePaths(this.paths.getHomeDir(), type, name, agents);
959
+ return {
960
+ resourcePath: existingCandidates[0].path,
961
+ linkPaths,
962
+ agents,
963
+ mode: await this.readPathMode(existingCandidates[0].path),
964
+ };
965
+ }
966
+ async findExistingAgentPaths(projectDir, type, name, scope) {
967
+ const rootDir = scope === "global" ? this.paths.getHomeDir() : projectDir;
968
+ const candidates = getSupportedAgentNames().map((agent) => ({
969
+ agent,
970
+ path: scope === "global"
971
+ ? getGlobalResourcePaths(rootDir, type, name, [agent])[0]
972
+ : getProjectResourcePaths(rootDir, type, name, [agent])[0],
973
+ }));
974
+ const existingCandidates = [];
975
+ for (const candidate of candidates) {
976
+ if (await this.exists(candidate.path))
977
+ existingCandidates.push(candidate);
978
+ }
979
+ return existingCandidates;
980
+ }
981
+ async findFirstExistingPath(paths) {
982
+ for (const targetPath of paths) {
983
+ if (await this.exists(targetPath))
984
+ return targetPath;
985
+ }
986
+ return undefined;
987
+ }
988
+ async readPathMode(targetPath) {
989
+ const stat = await fs.lstat(targetPath);
990
+ return stat.isSymbolicLink() ? "link" : "copy";
991
+ }
639
992
  async resolveInstalledResource(projectDir, type, name) {
640
993
  const locked = await this.getLockedResource(projectDir, type, name);
641
994
  const configuredAgents = await this.getConfiguredAgents(projectDir);
@@ -816,6 +1169,13 @@ export class ServiceFactory {
816
1169
  .filter(Boolean);
817
1170
  return items.length > 0 ? items : undefined;
818
1171
  }
1172
+ errorCheck(name, message, error) {
1173
+ return {
1174
+ name,
1175
+ status: "error",
1176
+ message: `${message} ${error instanceof Error ? error.message : String(error)}`,
1177
+ };
1178
+ }
819
1179
  async exists(targetPath) {
820
1180
  try {
821
1181
  await fs.access(targetPath);
@@ -830,6 +1190,10 @@ export class ServiceFactory {
830
1190
  if (await this.exists(devPath)) {
831
1191
  return devPath;
832
1192
  }
1193
+ const projectTarget = await this.tryResolveProjectResourceTarget(projectDir, type, name);
1194
+ if (projectTarget) {
1195
+ return projectTarget.resourcePath;
1196
+ }
833
1197
  const repoResourceDir = await this.getRepoResourceDir(type, name);
834
1198
  if (await this.exists(repoResourceDir)) {
835
1199
  return repoResourceDir;
@@ -857,6 +1221,15 @@ export class ServiceFactory {
857
1221
  getDefaultEntry(type) {
858
1222
  return type === "skill" ? "SKILL.md" : "content.md";
859
1223
  }
1224
+ getDefaultContent(type, name) {
1225
+ if (type === "rule") {
1226
+ return `# ${name}\n\nDescribe rule instructions here.\n`;
1227
+ }
1228
+ if (type === "command") {
1229
+ return `# ${name}\n\nDescribe command behavior here.\n`;
1230
+ }
1231
+ return `# ${name}\n\nDescribe skill workflow here.\n`;
1232
+ }
860
1233
  validateCreateInput(type, name, options) {
861
1234
  if (!["rule", "command", "skill"].includes(type)) {
862
1235
  throw new HimanError(errorCodes.UNSUPPORTED_RESOURCE_TYPE, `Unsupported resource type for create: ${type}`);
@@ -53,23 +53,20 @@
53
53
  ### 2.5 `dev`
54
54
 
55
55
  - `himan dev <type> <name>`,`type` 支持 `rule|command|skill`;需先 `install`。
56
- - 将当前安装内容复制到项目开发目录(已存在则默认不覆盖),再按安装模式更新项目目标:
57
- - `rule`:`.himan/dev/rule/<name>`
58
- - `command`:`.himan/dev/command/<name>`
59
- - `skill`:`.himan/dev/skill/<name>`
56
+ - 项目内资源直接在当前 agent 目标目录原地修改;若资源只存在于用户级全局目录,则复制到当前项目对应 agent 目标目录。
60
57
 
61
58
  ### 2.6 `publish`
62
59
 
63
- - `himan publish <type> <name> --patch|--minor|--major`(默认 patch,三选一)
64
- - 发布内容优先取项目 `.himan/dev/<type>/<name>`,否则取源仓库内对应资源目录。
60
+ - `himan publish <type> <name> --patch|--minor|--major [--global]`(默认 patch,三选一)
61
+ - 发布内容优先取旧版项目 `.himan/dev/<type>/<name>`,其次取当前项目 agent 目标目录,否则取源仓库内对应资源目录。
65
62
  - 新版本:基于已有 tag 最新 semver 递增;无任何历史时从 `0.0.0` 起算。
66
63
  - 写回源仓库、提交、打 tag、推送,并将该版本同步到本地 store。
67
- - 发布成功后,用新版本 store 以 copy 模式重新安装到项目目标、更新 lock,并删除对应 `.himan/dev/<type>/<name>`。
64
+ - 发布过程展示阶段日志。发布成功后,用新版本 store 以 copy 模式重新安装;默认安装到项目目标并更新 lock,`--global` 安装到用户级目录且不写项目 lock。
68
65
 
69
66
  ### 2.7 `create`
70
67
 
71
68
  - `himan create <type> <name>` 及常用选项(描述、目标 agent、dry-run、force、json 等)
72
- - 生成 `rule` / `command` / `skill` 标准目录与 `himan.yaml`、入口模板
69
+ - 在当前项目 agent 目标目录生成 `rule` / `command` / `skill` 标准目录与 `himan.yaml`、入口模板
73
70
  - 与 `publish` 衔接:`create → 编辑 → publish`
74
71
 
75
72
  ### 2.8 `agent`
@@ -103,7 +100,7 @@
103
100
  **项目目录:**
104
101
  - `.cursor` / `.claude` / `.agents` / `.openclaw`:按 agent 和资源类型保存运行态目标(软链或副本)
105
102
  - `.himan/config.json`:项目默认 agent 配置
106
- - `.himan/dev/<type>/<name>`:资源开发态可编辑副本
103
+ - 当前项目 agent 目标目录:资源创建和开发态可编辑位置;旧版 `.himan/dev/<type>/<name>` 仍可作为 publish 输入
107
104
 
108
105
  **源仓库内资源布局:**
109
106
  - `rules/<name>/`、`commands/<name>/`、`skills/<name>/`,可含 `himan.yaml`,并包含约定入口文件(如 `content.md`、`SKILL.md`)。
@@ -58,10 +58,10 @@ repo/
58
58
 
59
59
  可用 `himan source init-docs` 生成根目录文档模板。命令默认只创建缺失的 `README.md` / `CHANGELOG.md`;已有文件会保留,除非显式传 `--force`。`--force` 覆盖文档时会扫描当前 source 中已有的 `rule`、`command`、`skill`,写入 README 资源索引,并在 CHANGELOG 初始条目中记录已整理的资源。资源引用会优先使用 Git tag 中的最新 semver 版本,找不到 tag 时再回退到 `himan.yaml` 的 `version`。对于尚未补齐 `himan.yaml` 的资源,文档整理会按默认入口识别资源;其中 skill 会额外读取 `skills/<name>/SKILL.md` front matter 中的 `name` 和 `description`。`--dry-run` 只返回将执行的创建、覆盖或跳过动作,不写盘。有实际文件变更时,命令会提交并 push 到当前 Git source。
60
60
 
61
- `create` `publish` 会自动维护根目录文档:
61
+ `create` 默认只在当前项目 agent 目标目录生成可验证资源;`publish` 会把项目目录中的资源同步回 source 仓库,并自动维护根目录文档:
62
62
 
63
63
  - `README.md`:只维护 `<!-- himan:resources:start -->` / `<!-- himan:resources:end -->` 标记内的资源索引;如果旧 README 没有标记,则在文件末尾追加受控资源索引区
64
- - `CHANGELOG.md`:向 `[Unreleased]` 写入资源变更;新增资源写入 `Added`,发布版本写入 `Changed`
64
+ - `CHANGELOG.md`:向 `[Unreleased]` 写入发布版本的 `Changed` 条目
65
65
 
66
66
  推荐的 `README.md` 基本结构:
67
67
 
@@ -110,15 +110,15 @@ himan install rule code-review
110
110
 
111
111
  ## 4. 资源目录与元数据
112
112
 
113
- `create` 生成资源目录,结构示例:
113
+ `create` 生成当前项目 agent 目标目录,结构示例:
114
114
 
115
115
  ```text
116
- repo/
117
- rules/<name>/
116
+ project/
117
+ .cursor/rules/<name>/
118
118
  content.md
119
- commands/<name>/
119
+ .cursor/commands/<name>/
120
120
  content.md
121
- skills/<name>/
121
+ .agents/skills/<name>/
122
122
  SKILL.md
123
123
  ```
124
124
 
@@ -143,7 +143,7 @@ agents:
143
143
 
144
144
  1. 读取本地配置,确认已初始化源
145
145
  2. 校验类型与资源名格式
146
- 3. 解析目标路径 `rules|commands|skills/<name>`
146
+ 3. 解析当前项目 agent 目标路径,例如 `.agents/skills/<name>`
147
147
  4. 目录已存在且无 `--force` → 报错
148
148
  5. 生成 `himan.yaml` 与入口模板(`--dry-run` 则不落盘)
149
149
  6. 终端或 `--json` 输出结果;下一步由用户编辑再 `publish`
@@ -178,17 +178,17 @@ agents:
178
178
  ## 9. 与资源工作流衔接
179
179
 
180
180
  ```text
181
- create → edit → publish
181
+ create → edit in project agent folder → publish
182
182
  ```
183
183
 
184
- `create` 会在当前 Git source 缓存仓库中生成资源目录;用户编辑该目录后执行 `publish`。资源已有发布版本并安装到项目后,可再进入 `dev` 工作流:
184
+ `create` 会在当前项目 agent 目标目录中生成资源目录;用户编辑并验证该目录后执行 `publish`。资源已有发布版本并安装到项目后,可再进入 `dev` 工作流,直接在项目 agent 目标目录原地修改:
185
185
 
186
186
  ```bash
187
187
  himan create rule code-review --description "enforce standards"
188
188
  himan publish rule code-review --patch
189
189
  himan install rule code-review
190
190
  himan dev rule code-review
191
- # 编辑 .himan/dev/rule/code-review/
191
+ # 编辑 .cursor/rules/code-review/ 或当前默认 agent 对应目录
192
192
  himan publish rule code-review --patch
193
193
  ```
194
194
 
package/docs/mvp/impl.md CHANGED
@@ -14,7 +14,7 @@
14
14
  **原则:**
15
15
 
16
16
  - 本地 store 按版本存放,已存在的版本目录不被覆盖(安装时复用缓存)
17
- - 开发目录 `.himan/dev` 与运行态 agent 目录(`.cursor` / `.claude` / `.agents` / `.openclaw`)分离
17
+ - 创建和开发默认直接使用运行态 agent 目录(`.cursor` / `.claude` / `.agents` / `.openclaw`),旧版 `.himan/dev` 目录仅作为兼容发布输入
18
18
  - 正式发布版本以 **Git Tag** 为唯一事实来源;`himan.yaml` 中的 version 在发布时会与 tag 对齐
19
19
 
20
20
  ---
@@ -49,21 +49,21 @@
49
49
  ### 2.5 `dev <type> <name>`
50
50
 
51
51
  - 命令层接受 `rule|command|skill`;依赖已安装(能解析当前安装目标)
52
- - 将当前安装内容复制到 `.himan/dev/<type>/<name>`(目录已存在则默认不覆盖)
53
- - 按安装模式将项目目标更新为 dev 目录的软链或副本
52
+ - 当前项目已存在资源时直接返回项目 agent 目标目录,用户原地修改
53
+ - 当前项目不存在但用户级全局目录存在时,将全局资源复制到当前项目对应 agent 目标目录
54
54
 
55
55
  ### 2.6 `publish <type> <name>`
56
56
 
57
- - 发布源:优先项目 `.himan/dev/<type>/<name>`,否则缓存仓库内该资源目录
57
+ - 发布源:优先旧版项目 `.himan/dev/<type>/<name>`,其次当前项目 agent 目标目录,否则缓存仓库内该资源目录
58
58
  - 下一版本:基于历史最新 tag;无历史则从 `0.0.0` 按 patch/minor/major 递增
59
59
  - 将内容同步回缓存仓库中的规范路径,更新元数据中的版本字段,提交、打 tag、推送
60
60
  - 将新 tag 对应内容拉取到 store 新版本目录
61
- - 用新版本 store 以 copy 模式重新安装项目内对应类型目标,更新 lock,并删除对应 `.himan/dev/<type>/<name>` 开发目录
61
+ - 发布过程展示阶段日志;用新版本 store 以 copy 模式重新安装。默认安装到项目内对应类型目标并更新 lock,`--global` 安装到用户级目标且不写项目 lock
62
62
 
63
63
  ### 2.7 `create <type> <name>`
64
64
 
65
65
  - 校验类型与资源命名规则
66
- - 在缓存仓库中创建 `rules|commands|skills/<name>` 及 `himan.yaml`、入口模板
66
+ - 在当前项目 agent 目标目录创建 `rules|commands|skills/<name>` 及 `himan.yaml`、入口模板
67
67
  - 支持覆盖、试运行、JSON 输出;创建后不自动发布
68
68
 
69
69
  ### 2.8 `agent`
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hi-man/himan",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "Prompt and agent asset management CLI",
5
5
  "keywords": [
6
6
  "ai",