@hi-man/himan 0.4.0 → 0.5.0

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,28 @@ The format is based on Keep a Changelog, and this project follows semver for the
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.0] - 2026-05-14
10
+
11
+ ### Added
12
+
13
+ - Added `himan resource archive` and `himan resource restore` for moving source resources into `archive/<plural>/<name>`, listing archived resources, and explicitly installing archived history with `--include-archived`.
14
+ - Added the `himan-resource-manage` skill for creating, editing, validating, and publishing Himan resources from project agent folders.
15
+
16
+ ### Changed
17
+
18
+ - Changed `himan doctor` to warn when a project lock references resources archived in its recorded source.
19
+
20
+ ## [0.4.1] - 2026-05-14
21
+
22
+ ### Added
23
+
24
+ - Added `himan doctor` to check local Node/Git availability, Himan home state, current source scanning, effective agents, project lock state, and installed targets.
25
+
26
+ ### Changed
27
+
28
+ - 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.
29
+ - Changed `himan init` to support quick-start setup with optional `--agent`, `--install type/name[@version],...`, `--mode`, and `--json`.
30
+
9
31
  ## [0.4.0] - 2026-05-13
10
32
 
11
33
  ### 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`。当前项目配置优先于全局配置。
@@ -91,11 +98,19 @@ your-himan-source/
91
98
  my-skill/
92
99
  himan.yaml
93
100
  SKILL.md
101
+ archive/
102
+ rules/
103
+ old-rule/
104
+ himan.yaml
105
+ content.md
106
+ commands/
107
+ skills/
94
108
  ```
95
109
 
96
110
  - `README.md`:source 仓库入口文档,建议记录资源目录说明、推荐安装方式、默认 agent 策略、常用资源索引和维护约定。
97
111
  - `CHANGELOG.md`:source 仓库级变更记录,建议记录新增、变更、废弃、移除的资源,以及重要版本发布说明。
98
112
  - `rules/`、`commands/`、`skills/`:按资源类型分组;每个子目录是一份 himan 资源。
113
+ - `archive/rules/`、`archive/commands/`、`archive/skills/`:归档资源目录;默认列表、文档索引和 source sync 不把它们视为 active 资源。
99
114
  - `himan.yaml`:可选资源元数据;存在时供 himan 扫描、校验、读取入口和默认 agent;skill 资源可包含 `analysis` 静态分析信息。
100
115
  - `content.md` / `SKILL.md`:资源主入口;没有 `himan.yaml` 时,`rule` / `command` 默认使用 `content.md`,`skill` 默认使用 `SKILL.md`。
101
116
 
@@ -131,10 +146,12 @@ analysis:
131
146
 
132
147
  可通过 `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
148
 
134
- `himan create`、`himan rename` `himan publish` 会自动维护 source 根目录文档:
149
+ `himan create` 默认在当前项目 agent 目标目录创建资源脚手架,供用户直接验证;`himan publish` 会把项目目录中的资源同步回当前 default source,并自动维护 source 根目录文档:
135
150
 
136
151
  - `README.md`:只更新 `<!-- himan:resources:start -->` 和 `<!-- himan:resources:end -->` 之间的资源索引;如果没有 marker,会在文件末尾追加一个受控资源索引区。
137
- - `CHANGELOG.md`:向 `[Unreleased]` 下追加资源变更条目;`create` 记录 `Added`,`rename` / `publish` 记录 `Changed`。
152
+ - `CHANGELOG.md`:`publish` `[Unreleased]` 下追加 `Changed` / published version 条目。
153
+
154
+ `himan rename` 也会维护 source 根目录文档:更新 README 资源索引,并向 CHANGELOG 的 `[Unreleased]` 追加 `Changed` 条目。
138
155
 
139
156
  仓库根目录的 `README.md` 和 `CHANGELOG.md` 不会被安装到 agent 目录;agent 只消费被安装的具体资源目录。当前安装实现会 materialize 资源目录本身,因此对 Cursor 这类要求特定单文件格式的 agent,资源目录内应避免放入会干扰识别的额外文件。
140
157
 
@@ -144,11 +161,13 @@ analysis:
144
161
 
145
162
  | 命令 | 说明 |
146
163
  | ----------------------------- | ------------------------------------------------ |
147
- | `init <git_url>` | 初始化默认源(当前为 Git)并写入 `~/.himan/config.json` |
164
+ | `init <git_url> [--agent a,b] [--install type/name[@version],...] [--mode link\|copy] [--json]` | 初始化默认源(当前为 Git)并写入 `~/.himan/config.json`;可同时写当前项目默认 agent 并安装选定资源 |
148
165
  | `source add <name> <git_url>` | 添加命名 Git 源 |
149
166
  | `source use <name>` | 切换默认源 |
150
167
  | `source list [--json]` | 查看已配置源(标记当前 default) |
151
168
  | `source init-docs [--force] [--dry-run] [--json]` | 为当前 default source 生成仓库级 README/CHANGELOG |
169
+ | `source clone <from> <to> [--branch b] [--target-branch b] [--add-source name] [--use] [--dry-run] [--json]` | 将 Git source 分支和 himan 管理的资源 tag 复制到空目标 Git 仓库 |
170
+ | `source sync <from> <to> [--target-branch b] [--add-source name] [--use] [--dry-run] [--json]` | 将最新资源快照同步到目标 Git 仓库并创建对应最新 tag |
152
171
  | `source init <git_url>` | 与 `init` 等价,便于统一走 `himan source ...` 入口 |
153
172
 
154
173
  等价独立命令:
@@ -158,14 +177,18 @@ analysis:
158
177
  - `himan-source use <name>`
159
178
  - `himan-source list [--json]`
160
179
  - `himan-source init-docs [--force] [--dry-run] [--json]`
180
+ - `himan-source clone <from> <to> [...]`
181
+ - `himan-source sync <from> <to> [...]`
161
182
 
162
183
  ### 2) resource(资源)
163
184
 
164
185
  | 命令 | 说明 |
165
186
  | -------------------------------- | ----------------------------------------------------------------------------------- |
166
- | `list [type] [--agent a,b] [--brief] [--installed] [--json]` | 默认列出当前 default source 的资源;未传 `type` 时按 `rule`/`command`/`skill` 分组展示全部资源;可按 agent 过滤;默认显示描述,`--brief` 可隐藏描述;`--installed` 改为查看当前项目 `himan.lock` 中的已安装资源 |
187
+ | `list [type] [--agent a,b] [--brief] [--installed] [--archived] [--include-archived] [--json]` | 默认列出当前 default source active 资源;未传 `type` 时按 `rule`/`command`/`skill` 分组展示全部资源;可按 agent 过滤;默认显示描述,`--brief` 可隐藏描述;`--installed` 改为查看当前项目 `himan.lock` 中的已安装资源;`--archived` 只看归档资源,`--include-archived` 同时展示 active 和归档资源 |
167
188
  | `history <type> <name> [--json]` | 按 tag 查看版本历史 |
168
- | `create <type> <name>` | 脚手架;常用选项:`--description`、`--agent a,b`、`--dry-run`、`--force`、`--json` |
189
+ | `create <type> <name>` | 在当前项目 agent 目录创建脚手架;常用选项:`--description`、`--agent a,b`、`--dry-run`、`--force`、`--json` |
190
+ | `archive <type> <name>` | 将当前 default source 中的资源移动到 `archive/<plural>/<name>`;常用选项:`--reason`、`--dry-run`、`--json` |
191
+ | `restore <type> <name>` | 将归档资源恢复回 active 类型目录;常用选项:`--dry-run`、`--json` |
169
192
  | `rename <type> <old-name> <new-name>` | 暂不推荐使用;重命名当前 default source 中的资源;常用选项:`--dry-run`、`--no-project`、`--json` |
170
193
 
171
194
  ### 3) project(当前项目)
@@ -173,10 +196,10 @@ analysis:
173
196
  | 命令 | 说明 |
174
197
  | --------------------------------- | --------------------------------------------------------- |
175
198
  | `list [type] [--agent a,b] [--json]` | 查看当前项目 `himan.lock` 中记录的已安装资源;未传 `type` 时按 `rule`/`command`/`skill` 分组展示 |
176
- | `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/...` |
199
+ | `install [type] [name[@version]] [--global] [--agent a,b] [--mode link\|copy] [--include-archived]` | 有参数时从当前 default source 安装指定资源;**无参数**时按 `himan.lock` 记录的 source 批量安装;加 `--global` 时安装到用户级 agent 目录且不写项目 lock;可覆盖安装目标 agent 或安装模式;归档资源必须显式传 `--include-archived` 才能按历史版本安装 |
200
+ | `dev <type> <name>` | 切换到开发态;项目资源原地编辑,全局资源先复制到当前项目目标目录 |
178
201
  | `uninstall <type> <name>` | 从项目移除安装目标,并同步删除 `himan.lock` 条目 |
179
- | `publish <type> <name>` | 默认 `--patch`;可选 `--minor` / `--major`(勿同时使用多个) |
202
+ | `publish <type> <name> [--global]` | 默认 `--patch`;可选 `--minor` / `--major`(勿同时使用多个);发布后默认安装到当前项目并更新 lock,`--global` 安装到用户级目录 |
180
203
 
181
204
  ### 4) agent(默认 Agent)
182
205
 
@@ -187,21 +210,30 @@ analysis:
187
210
  | `agent current [--json]` | 查看当前项目、全局和最终生效的默认 agent |
188
211
  | `agent clear [--project\|--global] [--json]` | 清除当前项目或全局默认 agent;默认 `--project` |
189
212
 
213
+ ### 5) doctor(可用性检查)
214
+
215
+ | 命令 | 说明 |
216
+ |------|------|
217
+ | `doctor [--json]` | 检查 Node/Git、Himan home、当前 source、资源扫描、默认 agent、项目 lock、归档资源引用和已安装目标;存在 error 时以非零状态退出 |
218
+
190
219
  也可使用分组命令(与上面等价):
191
220
 
192
- - `himan resource list|history|create|rename ...`
193
- - `himan-resource list|history|create|rename ...`(兼容保留:也可执行 install/dev/uninstall/publish)
221
+ - `himan resource list|history|create|archive|restore|rename ...`
222
+ - `himan-resource list|history|create|archive|restore|rename ...`(兼容保留:也可执行 install/dev/uninstall/publish)
194
223
  - `himan project list|install|dev|uninstall|publish ...`
195
224
  - `himan-project list|install|dev|uninstall|publish ...`
196
225
  - `himan agent list|use|current|clear ...`
226
+ - `himan doctor ...`
197
227
 
198
228
  说明:资源与项目相关命令统一使用 `--agent` 指定目标 Agent。
199
- 若未显式传 `--agent`,`create` / `install` 会使用当前项目默认 agent、全局默认 agent、资源 metadata 或内置默认 `cursor` 中最合适的一项;`dev` 会优先使用 lock 中记录的 agent。`install --global` 会优先复用当前项目 lock 里该资源的 agent,未命中时再使用默认 install 解析顺序,但目标根目录是用户 home 下对应 agent 目录。
229
+ 若未显式传 `--agent`,`create` / `install` 会使用当前项目默认 agent、全局默认 agent、资源 metadata 或内置默认 `cursor` 中最合适的一项;`dev` 会优先使用当前项目已有安装位置,找不到时再从用户级全局安装位置复制到当前项目。`install --global` 会优先复用当前项目 lock 里该资源的 agent,未命中时再使用默认 install 解析顺序,但目标根目录是用户 home 下对应 agent 目录。
200
230
 
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>` 开发目录。
231
+ `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
232
 
203
233
  `rename` 暂不推荐使用。该命令会移动 source 仓库里的资源目录并更新资源 metadata 名称、README 资源索引和 CHANGELOG。已有发布 tag 不会被改写;若旧资源已有历史版本,rename 会为新名字创建一个指向当前最新版本的 tag。默认会迁移当前项目中对应的安装目标、`.himan/dev` 副本和 lock 条目;传 `--no-project` 时只改 source。对于 skill,命令只自动更新 metadata / front matter 中的精确 `name` 字段,不会自动替换 `SKILL.md` 正文中的旧名称引用。
204
234
 
235
+ `archive` 是 source 级软下线操作:它把资源目录移动到 `archive/<plural>/<name>`,从默认 `list`、source README 资源索引和 `source sync` 的 active 快照中移除,并在 `CHANGELOG.md` 记录归档事件;已有 Git tag、本地 store、项目安装目录和 `himan.lock` 不会被删除。直接安装归档资源会返回 `E_RESOURCE_ARCHIVED`,需要显式加 `--include-archived`;无参数 `himan install` 仍会按 lock 恢复已记录的归档资源。`doctor` 会对 lock 中引用归档资源的项目给出 warning。`restore` 会把资源从 archive 目录移回 active 类型目录。
236
+
205
237
  `--json` 模式下,失败时会输出机器可读错误 JSON(`stderr`)。错误码定义见 [docs/error-codes.md](./docs/error-codes.md)。
206
238
 
207
239
  多源说明:当前是「**多来源可配置,单来源生效**」模型。显式资源命令(`list/install <type> .../history/dev/publish/rename`)作用于当前 default source;`himan install` 无参数恢复时使用 `himan.lock` 中记录的 source。
@@ -5,8 +5,10 @@ export class ResourceScanner {
5
5
  async scanRules(repoDir) {
6
6
  return this.scanByType(repoDir, "rule");
7
7
  }
8
- async scanByType(repoDir, type) {
9
- const baseDir = path.join(repoDir, this.getTypeDir(type));
8
+ async scanByType(repoDir, type, options = {}) {
9
+ const baseDir = options.archived
10
+ ? path.join(repoDir, "archive", this.getTypeDir(type))
11
+ : path.join(repoDir, this.getTypeDir(type));
10
12
  const hasBaseDir = await this.exists(baseDir);
11
13
  if (!hasBaseDir)
12
14
  return [];
@@ -32,16 +34,23 @@ export class ResourceScanner {
32
34
  agents: Array.isArray(parsed.agents)
33
35
  ? (parsed.agents ?? [])
34
36
  : (parsed.targets ?? []),
37
+ ...(options.archived
38
+ ? {
39
+ archived: true,
40
+ archivedAt: this.readStringMetadata(parsed, "archivedAt"),
41
+ archiveReason: this.readStringMetadata(parsed, "archiveReason"),
42
+ }
43
+ : {}),
35
44
  });
36
45
  continue;
37
46
  }
38
- const inferred = await this.inferResourceMeta(path.join(baseDir, resourceDir.name), resourceDir.name, type);
47
+ const inferred = await this.inferResourceMeta(path.join(baseDir, resourceDir.name), resourceDir.name, type, options);
39
48
  if (inferred)
40
49
  result.push(inferred);
41
50
  }
42
51
  return result;
43
52
  }
44
- async inferResourceMeta(resourceDir, dirName, type) {
53
+ async inferResourceMeta(resourceDir, dirName, type, options) {
45
54
  const entry = this.getDefaultEntry(type);
46
55
  const entryPath = path.join(resourceDir, entry);
47
56
  if (!(await this.exists(entryPath)))
@@ -55,6 +64,7 @@ export class ResourceScanner {
55
64
  agents: this.readStringArrayMetadata(metadata, "agents") ??
56
65
  this.readStringArrayMetadata(metadata, "targets") ??
57
66
  [],
67
+ ...(options.archived ? { archived: true } : {}),
58
68
  };
59
69
  }
60
70
  async readSkillFrontMatter(skillPath) {
@@ -24,19 +24,31 @@ export class GitSourceAdapter {
24
24
  this.sourceConfig = sourceConfig;
25
25
  await this.repoManager.cloneOrFetch(sourceConfig.repo, sourceConfig.repoDir);
26
26
  }
27
- async list(type) {
27
+ async list(type, options = {}) {
28
28
  const repoDir = this.getRepoDir();
29
+ if (options.archived) {
30
+ return this.scanner.scanByType(repoDir, type, { archived: true });
31
+ }
29
32
  const repoId = this.sourceConfig?.repoId ?? "default";
30
33
  const typeDir = this.getTypeDir(type);
31
34
  const baseDir = path.join(repoDir, typeDir);
32
35
  const metadataHash = await this.getResourceMetadataHash(baseDir, type);
33
36
  const cached = await this.indexStore.get(repoId, type);
37
+ let active;
34
38
  if (cached && cached.metadataHash === metadataHash) {
35
- return cached.resources;
39
+ active = cached.resources;
40
+ }
41
+ else {
42
+ active = await this.scanner.scanByType(repoDir, type);
43
+ await this.indexStore.upsert(repoId, type, metadataHash, active);
44
+ }
45
+ if (!options.includeArchived) {
46
+ return active;
36
47
  }
37
- const scanned = await this.scanner.scanByType(repoDir, type);
38
- await this.indexStore.upsert(repoId, type, metadataHash, scanned);
39
- return scanned;
48
+ const archived = await this.scanner.scanByType(repoDir, type, {
49
+ archived: true,
50
+ });
51
+ return [...active, ...archived].sort((a, b) => a.name.localeCompare(b.name));
40
52
  }
41
53
  async history(type, name) {
42
54
  const tags = await this.repoManager.listTags(this.getRepoDir(), `${type}/${name}@*`);
@@ -46,6 +58,9 @@ export class GitSourceAdapter {
46
58
  .sort((a, b) => semver.rcompare(a.version, b.version));
47
59
  return versions;
48
60
  }
61
+ async isArchived(type, name) {
62
+ return this.exists(path.join(this.getRepoDir(), "archive", this.getTypeDir(type), name));
63
+ }
49
64
  async pull(type, name, version, targetDir) {
50
65
  const tag = `${type}/${name}@${version}`;
51
66
  await this.repoManager.archiveResource(this.getRepoDir(), tag, `${type}s/${name}`, targetDir);
@@ -175,6 +190,101 @@ export class GitSourceAdapter {
175
190
  dryRun: false,
176
191
  };
177
192
  }
193
+ async archive(type, name, options = {}) {
194
+ const repoDir = this.getRepoDir();
195
+ const typeDir = this.getTypeDir(type);
196
+ const previousResourceDir = path.join(repoDir, typeDir, name);
197
+ const archiveDir = path.join(repoDir, "archive", typeDir, name);
198
+ const archiveReason = this.normalizeArchiveReason(options.reason);
199
+ const archivedAt = new Date().toISOString();
200
+ if (!(await this.exists(previousResourceDir))) {
201
+ if (await this.exists(archiveDir)) {
202
+ throw new HimanError(errorCodes.RESOURCE_ARCHIVED, `Resource already archived: ${type}/${name}`);
203
+ }
204
+ throw new HimanError(errorCodes.RESOURCE_NOT_FOUND, `Resource not found: ${type}/${name}`);
205
+ }
206
+ if (await this.exists(archiveDir)) {
207
+ throw new HimanError(errorCodes.RESOURCE_EXISTS, `Archived resource already exists: ${type}/${name}`);
208
+ }
209
+ if (options.dryRun) {
210
+ return {
211
+ type,
212
+ name,
213
+ previousResourceDir,
214
+ archiveDir,
215
+ archivedAt,
216
+ archiveReason,
217
+ committed: false,
218
+ dryRun: true,
219
+ };
220
+ }
221
+ await this.markArchivedResourceMetadata(previousResourceDir, type, name, archivedAt, archiveReason);
222
+ await fs.mkdir(path.dirname(archiveDir), { recursive: true });
223
+ await fs.rename(previousResourceDir, archiveDir);
224
+ const docsPaths = await this.maintainSourceDocs(repoDir, {
225
+ section: "Deprecated",
226
+ line: archiveReason
227
+ ? `- Archived \`${type}/${name}\`: ${archiveReason}.`
228
+ : `- Archived \`${type}/${name}\`.`,
229
+ });
230
+ const committed = await this.repoManager.commitAndPush(repoDir, `archive ${type}/${name}`, undefined, [
231
+ path.relative(repoDir, previousResourceDir),
232
+ path.relative(repoDir, archiveDir),
233
+ ...docsPaths.map((docPath) => path.relative(repoDir, docPath)),
234
+ ]);
235
+ return {
236
+ type,
237
+ name,
238
+ previousResourceDir,
239
+ archiveDir,
240
+ archivedAt,
241
+ archiveReason,
242
+ committed,
243
+ dryRun: false,
244
+ };
245
+ }
246
+ async restore(type, name, options = {}) {
247
+ const repoDir = this.getRepoDir();
248
+ const typeDir = this.getTypeDir(type);
249
+ const previousArchiveDir = path.join(repoDir, "archive", typeDir, name);
250
+ const resourceDir = path.join(repoDir, typeDir, name);
251
+ if (!(await this.exists(previousArchiveDir))) {
252
+ throw new HimanError(errorCodes.RESOURCE_NOT_FOUND, `Archived resource not found: ${type}/${name}`);
253
+ }
254
+ if (await this.exists(resourceDir)) {
255
+ throw new HimanError(errorCodes.RESOURCE_EXISTS, `Resource already exists: ${type}/${name}`);
256
+ }
257
+ if (options.dryRun) {
258
+ return {
259
+ type,
260
+ name,
261
+ previousArchiveDir,
262
+ resourceDir,
263
+ committed: false,
264
+ dryRun: true,
265
+ };
266
+ }
267
+ await this.clearArchivedResourceMetadata(previousArchiveDir, type, name);
268
+ await fs.mkdir(path.dirname(resourceDir), { recursive: true });
269
+ await fs.rename(previousArchiveDir, resourceDir);
270
+ const docsPaths = await this.maintainSourceDocs(repoDir, {
271
+ section: "Changed",
272
+ line: `- Restored \`${type}/${name}\` from archive.`,
273
+ });
274
+ const committed = await this.repoManager.commitAndPush(repoDir, `restore ${type}/${name}`, undefined, [
275
+ path.relative(repoDir, previousArchiveDir),
276
+ path.relative(repoDir, resourceDir),
277
+ ...docsPaths.map((docPath) => path.relative(repoDir, docPath)),
278
+ ]);
279
+ return {
280
+ type,
281
+ name,
282
+ previousArchiveDir,
283
+ resourceDir,
284
+ committed,
285
+ dryRun: false,
286
+ };
287
+ }
178
288
  async initDocs(options = {}) {
179
289
  const repoDir = this.getRepoDir();
180
290
  const files = [
@@ -299,6 +409,44 @@ export class GitSourceAdapter {
299
409
  const updated = `---\n${frontMatter}\n---\n${raw.slice(match[0].length)}`;
300
410
  await fs.writeFile(skillPath, updated, "utf8");
301
411
  }
412
+ normalizeArchiveReason(reason) {
413
+ const trimmed = reason?.trim();
414
+ return trimmed ? trimmed : undefined;
415
+ }
416
+ async markArchivedResourceMetadata(resourceDir, type, name, archivedAt, archiveReason) {
417
+ const yamlPath = path.join(resourceDir, "himan.yaml");
418
+ if (!(await this.exists(yamlPath)))
419
+ return;
420
+ const parsed = await this.readResourceYamlObject(yamlPath, type, name);
421
+ await fs.writeFile(yamlPath, YAML.stringify({
422
+ ...parsed,
423
+ archived: true,
424
+ archivedAt,
425
+ ...(archiveReason ? { archiveReason } : {}),
426
+ }), "utf8");
427
+ }
428
+ async clearArchivedResourceMetadata(resourceDir, type, name) {
429
+ const yamlPath = path.join(resourceDir, "himan.yaml");
430
+ if (!(await this.exists(yamlPath)))
431
+ return;
432
+ const parsed = await this.readResourceYamlObject(yamlPath, type, name);
433
+ const { archived: _archived, archivedAt: _archivedAt, archiveReason: _archiveReason, ...rest } = parsed;
434
+ await fs.writeFile(yamlPath, YAML.stringify(rest), "utf8");
435
+ }
436
+ async readResourceYamlObject(yamlPath, type, name) {
437
+ const raw = await fs.readFile(yamlPath, "utf8");
438
+ let parsed;
439
+ try {
440
+ parsed = YAML.parse(raw);
441
+ }
442
+ catch (error) {
443
+ throw this.invalidResourceMetadata(type, name, "himan.yaml is not valid YAML.", { yamlPath, reason: error instanceof Error ? error.message : String(error) });
444
+ }
445
+ if (!this.isRecord(parsed)) {
446
+ throw this.invalidResourceMetadata(type, name, "himan.yaml must be an object.", { yamlPath });
447
+ }
448
+ return parsed;
449
+ }
302
450
  async validatePublishResource(type, name, resourceDir) {
303
451
  const yamlPath = path.join(resourceDir, "himan.yaml");
304
452
  if (!(await this.exists(yamlPath))) {
@@ -785,7 +933,12 @@ export class GitSourceAdapter {
785
933
  return found === -1 ? lines.length : found;
786
934
  }
787
935
  findChangelogSectionInsertIndex(lines, unreleasedIndex, blockEnd, section) {
788
- const sectionOrder = ["Added", "Changed"];
936
+ const sectionOrder = [
937
+ "Added",
938
+ "Changed",
939
+ "Deprecated",
940
+ "Removed",
941
+ ];
789
942
  const sectionRank = sectionOrder.indexOf(section);
790
943
  for (let index = unreleasedIndex + 1; index < blockEnd; index += 1) {
791
944
  const line = lines[index].trim();
@@ -3,12 +3,15 @@ export class RegistrySourceAdapter {
3
3
  async init(_sourceConfig) {
4
4
  throw new HimanError(errorCodes.NOT_IMPLEMENTED, "Registry source is reserved for phase 2.");
5
5
  }
6
- async list(_type) {
6
+ async list(_type, _options) {
7
7
  throw new HimanError(errorCodes.NOT_IMPLEMENTED, "Registry source is reserved for phase 2.");
8
8
  }
9
9
  async history(_type, _name) {
10
10
  throw new HimanError(errorCodes.NOT_IMPLEMENTED, "Registry source is reserved for phase 2.");
11
11
  }
12
+ async isArchived(_type, _name) {
13
+ throw new HimanError(errorCodes.NOT_IMPLEMENTED, "Registry source is reserved for phase 2.");
14
+ }
12
15
  async pull(_type, _name, _version, _targetDir) {
13
16
  throw new HimanError(errorCodes.NOT_IMPLEMENTED, "Registry source is reserved for phase 2.");
14
17
  }
@@ -21,6 +24,12 @@ export class RegistrySourceAdapter {
21
24
  async rename(_type, _oldName, _newName, _options) {
22
25
  throw new HimanError(errorCodes.NOT_IMPLEMENTED, "Registry source is reserved for phase 2.");
23
26
  }
27
+ async archive(_type, _name, _options) {
28
+ throw new HimanError(errorCodes.NOT_IMPLEMENTED, "Registry source is reserved for phase 2.");
29
+ }
30
+ async restore(_type, _name, _options) {
31
+ throw new HimanError(errorCodes.NOT_IMPLEMENTED, "Registry source is reserved for phase 2.");
32
+ }
24
33
  async initDocs(_options) {
25
34
  throw new HimanError(errorCodes.NOT_IMPLEMENTED, "Registry source is reserved for phase 2.");
26
35
  }
@@ -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
@@ -63,12 +65,16 @@ Command groups:
63
65
  init, source init, source add, source use, source list,
64
66
  source init-docs, source clone, source sync
65
67
  resource Source resource discovery and metadata
66
- list, list --installed, history, create, rename (not recommended yet),
67
- resource list, resource history, resource create, resource rename
68
+ list, list --archived, list --installed, history, create,
69
+ archive, restore, rename (not recommended yet),
70
+ resource list, resource history, resource create,
71
+ resource archive, resource restore, resource rename
68
72
  project Resource usage lifecycle in current project or user-level agent dirs
69
- list, install, dev, uninstall, publish,
73
+ list, install, install --include-archived, dev, uninstall, publish,
70
74
  project list, project install, project dev, project uninstall, project publish
71
75
  agent Default agent configuration
72
76
  agent list, agent use, agent current, agent clear
77
+ doctor Runtime and project health checks
78
+ doctor
73
79
  `);
74
80
  }
@@ -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
+ }
@@ -39,12 +39,16 @@ export function registerProjectCommands(command, services, options = {}) {
39
39
  .option("--agent <list>", "install target agents, comma separated")
40
40
  .option("--mode <mode>", "install mode: link or copy")
41
41
  .option("--global", "install into user-level agent directories")
42
+ .option("--include-archived", "allow installing an archived resource explicitly")
42
43
  .description("Install resource, or install from himan.lock")
43
44
  .action(async (type, nameVersion, options) => {
44
45
  await runAction(async () => {
45
46
  const agents = parseAgents(options.agent);
46
47
  const mode = parseInstallMode(options.mode);
47
48
  if (!type && !nameVersion) {
49
+ if (options.includeArchived) {
50
+ throw new HimanError(errorCodes.CLI_USAGE, "--include-archived only applies to single-resource install.");
51
+ }
48
52
  if (options.global) {
49
53
  throw new HimanError(errorCodes.CLI_USAGE, "Global install requires a resource:\n"
50
54
  + " - himan install <type> <name[@version]> --global [--mode link|copy]");
@@ -68,8 +72,8 @@ export function registerProjectCommands(command, services, options = {}) {
68
72
  const resourceType = ensureResourceType(type);
69
73
  const { name, version } = parseNameVersion(nameVersion);
70
74
  const result = options.global
71
- ? await services.installGlobal(resourceType, name, version, process.cwd(), agents, mode)
72
- : await services.install(resourceType, name, version, process.cwd(), agents, mode);
75
+ ? await services.installGlobal(resourceType, name, version, process.cwd(), agents, mode, { includeArchived: options.includeArchived })
76
+ : await services.install(resourceType, name, version, process.cwd(), agents, mode, { includeArchived: options.includeArchived });
73
77
  process.stdout.write(`Installed ${options.global ? "global " : ""}${result.type}/${result.name}@${result.version}\n`);
74
78
  });
75
79
  });
@@ -82,7 +86,11 @@ export function registerProjectCommands(command, services, options = {}) {
82
86
  await runAction(async () => {
83
87
  const resourceType = ensureResourceType(type);
84
88
  const result = await services.dev(resourceType, name, process.cwd());
85
- process.stdout.write(`Switched ${result.type}/${result.name} to dev mode: ${result.devPath}\n`);
89
+ if (result.sourceScope === "global") {
90
+ process.stdout.write(`Copied global ${result.type}/${result.name} into current project: ${result.devPath}\n`);
91
+ return;
92
+ }
93
+ process.stdout.write(`Editing ${result.type}/${result.name} in place: ${result.devPath}\n`);
86
94
  });
87
95
  });
88
96
  command
@@ -104,13 +112,23 @@ export function registerProjectCommands(command, services, options = {}) {
104
112
  .option("--patch", "patch release")
105
113
  .option("--minor", "minor release")
106
114
  .option("--major", "major release")
115
+ .option("--global", "install the published version into user-level agent directories")
107
116
  .description("Publish resource (default: --patch)")
108
117
  .action(async (type, name, options) => {
109
118
  await runAction(async () => {
110
119
  const resourceType = ensureResourceType(type);
111
120
  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`);
121
+ const installScope = options.global ? "global" : "project";
122
+ process.stdout.write(options.global
123
+ ? "Published resource will be installed globally; current project lock will not be updated.\n"
124
+ : "Published resource will be installed into the current project and recorded in himan.lock. Use --global to install globally instead.\n");
125
+ const result = await services.publish(resourceType, name, releaseType, process.cwd(), {
126
+ installScope,
127
+ onProgress: (progress) => {
128
+ process.stdout.write(`[publish:${progress.stage}] ${progress.message}\n`);
129
+ },
130
+ });
131
+ process.stdout.write(`Published ${result.type}/${result.name}@${result.version} and installed ${result.installScope === "global" ? "globally" : "into current project"}\n`);
114
132
  });
115
133
  });
116
134
  }