@hi-man/himan 0.4.1 → 0.5.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,23 @@ The format is based on Keep a Changelog, and this project follows semver for the
6
6
 
7
7
  ## [Unreleased]
8
8
 
9
+ ## [0.5.1] - 2026-05-14
10
+
11
+ ### Fixed
12
+
13
+ - Fixed `himan-skill-metadata` so newly generated skill metadata starts at version `0.0.1`.
14
+
15
+ ## [0.5.0] - 2026-05-14
16
+
17
+ ### Added
18
+
19
+ - 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`.
20
+ - Added the `himan-resource-manage` skill for creating, editing, validating, and publishing Himan resources from project agent folders.
21
+
22
+ ### Changed
23
+
24
+ - Changed `himan doctor` to warn when a project lock references resources archived in its recorded source.
25
+
9
26
  ## [0.4.1] - 2026-05-14
10
27
 
11
28
  ### Added
package/README.md CHANGED
@@ -98,11 +98,19 @@ your-himan-source/
98
98
  my-skill/
99
99
  himan.yaml
100
100
  SKILL.md
101
+ archive/
102
+ rules/
103
+ old-rule/
104
+ himan.yaml
105
+ content.md
106
+ commands/
107
+ skills/
101
108
  ```
102
109
 
103
110
  - `README.md`:source 仓库入口文档,建议记录资源目录说明、推荐安装方式、默认 agent 策略、常用资源索引和维护约定。
104
111
  - `CHANGELOG.md`:source 仓库级变更记录,建议记录新增、变更、废弃、移除的资源,以及重要版本发布说明。
105
112
  - `rules/`、`commands/`、`skills/`:按资源类型分组;每个子目录是一份 himan 资源。
113
+ - `archive/rules/`、`archive/commands/`、`archive/skills/`:归档资源目录;默认列表、文档索引和 source sync 不把它们视为 active 资源。
106
114
  - `himan.yaml`:可选资源元数据;存在时供 himan 扫描、校验、读取入口和默认 agent;skill 资源可包含 `analysis` 静态分析信息。
107
115
  - `content.md` / `SKILL.md`:资源主入口;没有 `himan.yaml` 时,`rule` / `command` 默认使用 `content.md`,`skill` 默认使用 `SKILL.md`。
108
116
 
@@ -176,9 +184,11 @@ analysis:
176
184
 
177
185
  | 命令 | 说明 |
178
186
  | -------------------------------- | ----------------------------------------------------------------------------------- |
179
- | `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 和归档资源 |
180
188
  | `history <type> <name> [--json]` | 按 tag 查看版本历史 |
181
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` |
182
192
  | `rename <type> <old-name> <new-name>` | 暂不推荐使用;重命名当前 default source 中的资源;常用选项:`--dry-run`、`--no-project`、`--json` |
183
193
 
184
194
  ### 3) project(当前项目)
@@ -186,7 +196,7 @@ analysis:
186
196
  | 命令 | 说明 |
187
197
  | --------------------------------- | --------------------------------------------------------- |
188
198
  | `list [type] [--agent a,b] [--json]` | 查看当前项目 `himan.lock` 中记录的已安装资源;未传 `type` 时按 `rule`/`command`/`skill` 分组展示 |
189
- | `install [type] [name[@version]] [--global] [--agent a,b] [--mode link\|copy]` | 有参数时从当前 default source 安装指定资源;**无参数**时按 `himan.lock` 记录的 source 批量安装;加 `--global` 时安装到用户级 agent 目录且不写项目 lock;可覆盖安装目标 agent 或安装模式 |
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` 才能按历史版本安装 |
190
200
  | `dev <type> <name>` | 切换到开发态;项目资源原地编辑,全局资源先复制到当前项目目标目录 |
191
201
  | `uninstall <type> <name>` | 从项目移除安装目标,并同步删除 `himan.lock` 条目 |
192
202
  | `publish <type> <name> [--global]` | 默认 `--patch`;可选 `--minor` / `--major`(勿同时使用多个);发布后默认安装到当前项目并更新 lock,`--global` 安装到用户级目录 |
@@ -204,12 +214,12 @@ analysis:
204
214
 
205
215
  | 命令 | 说明 |
206
216
  |------|------|
207
- | `doctor [--json]` | 检查 Node/Git、Himan home、当前 source、资源扫描、默认 agent、项目 lock 和已安装目标;存在 error 时以非零状态退出 |
217
+ | `doctor [--json]` | 检查 Node/Git、Himan home、当前 source、资源扫描、默认 agent、项目 lock、归档资源引用和已安装目标;存在 error 时以非零状态退出 |
208
218
 
209
219
  也可使用分组命令(与上面等价):
210
220
 
211
- - `himan resource list|history|create|rename ...`
212
- - `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)
213
223
  - `himan project list|install|dev|uninstall|publish ...`
214
224
  - `himan-project list|install|dev|uninstall|publish ...`
215
225
  - `himan agent list|use|current|clear ...`
@@ -222,6 +232,8 @@ analysis:
222
232
 
223
233
  `rename` 暂不推荐使用。该命令会移动 source 仓库里的资源目录并更新资源 metadata 名称、README 资源索引和 CHANGELOG。已有发布 tag 不会被改写;若旧资源已有历史版本,rename 会为新名字创建一个指向当前最新版本的 tag。默认会迁移当前项目中对应的安装目标、`.himan/dev` 副本和 lock 条目;传 `--no-project` 时只改 source。对于 skill,命令只自动更新 metadata / front matter 中的精确 `name` 字段,不会自动替换 `SKILL.md` 正文中的旧名称引用。
224
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
+
225
237
  `--json` 模式下,失败时会输出机器可读错误 JSON(`stderr`)。错误码定义见 [docs/error-codes.md](./docs/error-codes.md)。
226
238
 
227
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
  }
@@ -65,10 +65,12 @@ Command groups:
65
65
  init, source init, source add, source use, source list,
66
66
  source init-docs, source clone, source sync
67
67
  resource Source resource discovery and metadata
68
- list, list --installed, history, create, rename (not recommended yet),
69
- 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
70
72
  project Resource usage lifecycle in current project or user-level agent dirs
71
- list, install, dev, uninstall, publish,
73
+ list, install, install --include-archived, dev, uninstall, publish,
72
74
  project list, project install, project dev, project uninstall, project publish
73
75
  agent Default agent configuration
74
76
  agent list, agent use, agent current, agent clear
@@ -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
  });
@@ -10,18 +10,30 @@ export function registerResourceCommands(command, services) {
10
10
  .option("--agent <list>", "agent list filter, comma separated")
11
11
  .option("--brief", "hide resource descriptions")
12
12
  .option("--installed", "list resources installed in current project")
13
+ .option("--archived", "list archived resources only")
14
+ .option("--include-archived", "include archived resources in source list")
13
15
  .option("--json", "output json format")
14
16
  .description("List resources from current default source or project installs")
15
17
  .action(async (type, options) => {
16
18
  await runAction(async () => {
17
19
  const agents = parseAgents(options.agent);
18
20
  const showDescription = !options.brief;
21
+ if (options.installed && (options.archived || options.includeArchived)) {
22
+ throw new HimanError(errorCodes.CLI_USAGE, "--archived and --include-archived only apply to source resource lists.");
23
+ }
24
+ if (options.archived && options.includeArchived) {
25
+ throw new HimanError(errorCodes.CLI_USAGE, "Use only one of --archived or --include-archived.");
26
+ }
27
+ const listOptions = {
28
+ archived: Boolean(options.archived),
29
+ includeArchived: Boolean(options.includeArchived),
30
+ };
19
31
  if (options.installed) {
20
32
  await writeInstalledList(services, type, agents, Boolean(options.json));
21
33
  return;
22
34
  }
23
35
  if (!type) {
24
- const groups = await listGroupedResources(services, agents);
36
+ const groups = await listGroupedResources(services, agents, listOptions);
25
37
  if (options.json) {
26
38
  process.stdout.write(`${JSON.stringify(formatResourceGroups(groups, showDescription), null, 2)}\n`);
27
39
  return;
@@ -30,7 +42,7 @@ export function registerResourceCommands(command, services) {
30
42
  return;
31
43
  }
32
44
  const resourceType = ensureResourceType(type);
33
- const resources = await services.list(resourceType, agents);
45
+ const resources = await services.list(resourceType, agents, listOptions);
34
46
  if (options.json) {
35
47
  process.stdout.write(`${JSON.stringify(formatResources(resources, showDescription), null, 2)}\n`);
36
48
  return;
@@ -91,6 +103,48 @@ export function registerResourceCommands(command, services) {
91
103
  process.stdout.write(`Created ${result.type}/${result.name} at ${result.resourceDir}${result.dryRun ? " (dry-run)" : ""}\n`);
92
104
  });
93
105
  });
106
+ command
107
+ .command("archive")
108
+ .argument("<type>", "resource type")
109
+ .argument("<name>", "resource name")
110
+ .option("--reason <text>", "archive reason")
111
+ .option("--dry-run", "show archive result without writing")
112
+ .option("--json", "output json format")
113
+ .description("Archive resource in current default source")
114
+ .action(async (type, name, options) => {
115
+ await runAction(async () => {
116
+ const resourceType = ensureResourceType(type);
117
+ const result = await services.archive(resourceType, name, {
118
+ reason: options.reason,
119
+ dryRun: options.dryRun,
120
+ });
121
+ if (options.json) {
122
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
123
+ return;
124
+ }
125
+ process.stdout.write(`Archived ${result.type}/${result.name}${result.dryRun ? " (dry-run)" : ""}\n`);
126
+ });
127
+ });
128
+ command
129
+ .command("restore")
130
+ .argument("<type>", "resource type")
131
+ .argument("<name>", "resource name")
132
+ .option("--dry-run", "show restore result without writing")
133
+ .option("--json", "output json format")
134
+ .description("Restore archived resource into current default source")
135
+ .action(async (type, name, options) => {
136
+ await runAction(async () => {
137
+ const resourceType = ensureResourceType(type);
138
+ const result = await services.restore(resourceType, name, {
139
+ dryRun: options.dryRun,
140
+ });
141
+ if (options.json) {
142
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
143
+ return;
144
+ }
145
+ process.stdout.write(`Restored ${result.type}/${result.name}${result.dryRun ? " (dry-run)" : ""}\n`);
146
+ });
147
+ });
94
148
  command
95
149
  .command("rename")
96
150
  .argument("<type>", "resource type")
@@ -139,11 +193,11 @@ function ensureResourceType(type) {
139
193
  }
140
194
  return type;
141
195
  }
142
- async function listGroupedResources(services, agents) {
196
+ async function listGroupedResources(services, agents, options = {}) {
143
197
  return {
144
- rule: await services.list("rule", agents),
145
- command: await services.list("command", agents),
146
- skill: await services.list("skill", agents),
198
+ rule: await services.list("rule", agents, options),
199
+ command: await services.list("command", agents, options),
200
+ skill: await services.list("skill", agents, options),
147
201
  };
148
202
  }
149
203
  function formatResourceGroups(groups, showDescription) {
@@ -181,7 +235,8 @@ function writeResourceList(resources, showDescription) {
181
235
  return;
182
236
  }
183
237
  for (const resource of resources) {
184
- process.stdout.write(`- ${resource.type}/${resource.name}${showDescription && resource.description ? `: ${resource.description}` : ""}\n`);
238
+ const archived = resource.archived ? " [archived]" : "";
239
+ process.stdout.write(`- ${resource.type}/${resource.name}${archived}${showDescription && resource.description ? `: ${resource.description}` : ""}\n`);
185
240
  }
186
241
  }
187
242
  function formatGroupTitle(type) {
@@ -213,6 +213,7 @@ export class ServiceFactory {
213
213
  checks.push(await this.checkAgents(projectDir));
214
214
  const lockCheck = await this.checkProjectLock(projectDir);
215
215
  checks.push(lockCheck.check);
216
+ checks.push(await this.checkProjectArchiveStatus(lockCheck.lock));
216
217
  checks.push(await this.checkProjectTargets(projectDir, lockCheck.lock));
217
218
  return {
218
219
  ok: !checks.some((check) => check.status === "error"),
@@ -362,6 +363,44 @@ export class ServiceFactory {
362
363
  lock,
363
364
  };
364
365
  }
366
+ async checkProjectArchiveStatus(lock) {
367
+ if (!lock || lock.resources.length === 0) {
368
+ return {
369
+ name: "archive",
370
+ status: "ok",
371
+ message: "No locked resources to check for archive status.",
372
+ };
373
+ }
374
+ try {
375
+ const source = await this.loadSourceFromLock(this.normalizeLockSourceInfo(lock.source));
376
+ const archived = [];
377
+ for (const resource of lock.resources) {
378
+ if (await source.isArchived(resource.type, resource.name)) {
379
+ archived.push(`${resource.type}/${resource.name}@${resource.version}`);
380
+ }
381
+ }
382
+ if (archived.length > 0) {
383
+ return {
384
+ name: "archive",
385
+ status: "warn",
386
+ message: `${archived.length} locked resources are archived in the current source.`,
387
+ details: { resources: archived },
388
+ };
389
+ }
390
+ return {
391
+ name: "archive",
392
+ status: "ok",
393
+ message: "No locked resources are archived in the current source.",
394
+ };
395
+ }
396
+ catch (error) {
397
+ return {
398
+ name: "archive",
399
+ status: "warn",
400
+ message: `Cannot check archived resources. ${error instanceof Error ? error.message : String(error)}`,
401
+ };
402
+ }
403
+ }
365
404
  async checkProjectTargets(projectDir, lock) {
366
405
  if (!lock || lock.resources.length === 0) {
367
406
  return {
@@ -397,14 +436,27 @@ export class ServiceFactory {
397
436
  message: "All locked project targets exist.",
398
437
  };
399
438
  }
400
- async list(type, agents) {
439
+ async list(type, agents, options = {}) {
401
440
  const source = await this.loadSourceFromConfig();
402
- const resources = await source.list(type);
441
+ const resources = await source.list(type, options);
403
442
  if (!agents?.length)
404
443
  return resources;
405
444
  const selected = normalizeAgents(agents);
406
445
  return resources.filter((resource) => normalizeAgents(resource.agents).some((agent) => selected.includes(agent)));
407
446
  }
447
+ async archive(type, name, options = {}) {
448
+ this.validateResourceIdentity(type, name, "archive");
449
+ const source = await this.loadSourceFromConfig();
450
+ return source.archive(type, name, {
451
+ ...options,
452
+ reason: options.reason?.trim(),
453
+ });
454
+ }
455
+ async restore(type, name, options = {}) {
456
+ this.validateResourceIdentity(type, name, "restore");
457
+ const source = await this.loadSourceFromConfig();
458
+ return source.restore(type, name, options);
459
+ }
408
460
  async listInstalled(projectDir, type, agents) {
409
461
  const { lock, state } = await this.lockStore.loadWithState(projectDir);
410
462
  if (state === "invalid") {
@@ -431,13 +483,13 @@ export class ServiceFactory {
431
483
  const source = await this.loadSourceFromConfig();
432
484
  return source.history(type, name);
433
485
  }
434
- async install(type, name, version, projectDir, agents, mode = "copy") {
486
+ async install(type, name, version, projectDir, agents, mode = "copy", options = {}) {
435
487
  const { source, sourceInfo } = await this.loadSourceWithInfoFromConfig();
436
- return this.installWithSource(source, sourceInfo, type, name, version, projectDir, agents, mode);
488
+ return this.installWithSource(source, sourceInfo, type, name, version, projectDir, agents, mode, "project", options);
437
489
  }
438
- async installGlobal(type, name, version, projectDir, agents, mode = "copy") {
490
+ async installGlobal(type, name, version, projectDir, agents, mode = "copy", options = {}) {
439
491
  const source = await this.loadSourceFromConfig();
440
- return this.installWithSource(source, undefined, type, name, version, projectDir, agents, mode, "global");
492
+ return this.installWithSource(source, undefined, type, name, version, projectDir, agents, mode, "global", options);
441
493
  }
442
494
  async dev(type, name, projectDir) {
443
495
  const projectTarget = await this.tryResolveProjectResourceTarget(projectDir, type, name);
@@ -484,6 +536,9 @@ export class ServiceFactory {
484
536
  const installScope = options.installScope ?? "project";
485
537
  this.reportPublishProgress(options, "prepare", `Preparing ${type}/${name}.`);
486
538
  const source = await this.loadSourceFromConfig();
539
+ if (await source.isArchived(type, name)) {
540
+ throw new HimanError(errorCodes.RESOURCE_ARCHIVED, `Resource is archived: ${type}/${name}. Restore it before publishing a new version.`);
541
+ }
487
542
  const sourceDir = await this.resolvePublishSourceDir(type, name, projectDir);
488
543
  const existingInstallInfo = await this.tryResolveInstalledResource(projectDir, type, name);
489
544
  const existingGlobalInstallInfo = await this.tryResolveGlobalResourceTarget(projectDir, type, name);
@@ -632,12 +687,16 @@ export class ServiceFactory {
632
687
  const lockSourceInfo = this.normalizeLockSourceInfo(lock.source);
633
688
  const lockedSource = await this.loadSourceFromLock(lockSourceInfo);
634
689
  for (const item of lock.resources) {
635
- const result = await this.installWithSource(lockedSource, lockSourceInfo, item.type, item.name, item.version, projectDir, agents ?? item.agents, mode ?? this.resolveInstallMode(item.mode), "project");
690
+ const result = await this.installWithSource(lockedSource, lockSourceInfo, item.type, item.name, item.version, projectDir, agents ?? item.agents, mode ?? this.resolveInstallMode(item.mode), "project", { includeArchived: true });
636
691
  results.push(result);
637
692
  }
638
693
  return results;
639
694
  }
640
- async installWithSource(source, sourceInfo, type, name, version, projectDir, agents, mode, scope = "project") {
695
+ async installWithSource(source, sourceInfo, type, name, version, projectDir, agents, mode, scope = "project", options = {}) {
696
+ const archived = await source.isArchived(type, name);
697
+ if (archived && !options.includeArchived) {
698
+ throw new HimanError(errorCodes.RESOURCE_ARCHIVED, `Resource is archived: ${type}/${name}. Use --include-archived to install an archived version explicitly.`);
699
+ }
641
700
  const history = await source.history(type, name);
642
701
  if (history.length === 0) {
643
702
  throw new HimanError(errorCodes.RESOURCE_NOT_FOUND, `Resource not found: ${type}/${name}`);
@@ -1241,6 +1300,14 @@ export class ServiceFactory {
1241
1300
  throw new HimanError(errorCodes.TEMPLATE_NOT_FOUND, `Template not found: ${options.template}`);
1242
1301
  }
1243
1302
  }
1303
+ validateResourceIdentity(type, name, action) {
1304
+ if (!["rule", "command", "skill"].includes(type)) {
1305
+ throw new HimanError(errorCodes.UNSUPPORTED_RESOURCE_TYPE, `Unsupported resource type for ${action}: ${type}`);
1306
+ }
1307
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
1308
+ throw new HimanError(errorCodes.INVALID_RESOURCE_NAME, `Invalid resource name: ${name}. Use kebab-case only.`);
1309
+ }
1310
+ }
1244
1311
  validateRenameInput(type, oldName, newName) {
1245
1312
  if (!["rule", "command", "skill"].includes(type)) {
1246
1313
  throw new HimanError(errorCodes.UNSUPPORTED_RESOURCE_TYPE, `Unsupported resource type for rename: ${type}`);
@@ -13,6 +13,7 @@ export const errorCodes = {
13
13
  NOT_IMPLEMENTED: "E_NOT_IMPLEMENTED",
14
14
  INVALID_INPUT: "E_INVALID_INPUT",
15
15
  RESOURCE_NOT_FOUND: "E_RESOURCE_NOT_FOUND",
16
+ RESOURCE_ARCHIVED: "E_RESOURCE_ARCHIVED",
16
17
  VERSION_NOT_FOUND: "E_VERSION_NOT_FOUND",
17
18
  INSTALL_NOT_FOUND: "E_INSTALL_NOT_FOUND",
18
19
  LOCK_NOT_FOUND: "E_LOCK_NOT_FOUND",
@@ -58,6 +58,12 @@
58
58
  - **常见触发**:安装不存在的资源、切换到不存在的 source 名称、发布目标资源不存在。
59
59
  - **建议处理**:先执行 `himan list <type>` / `himan source list` 确认名称。
60
60
 
61
+ ### `E_RESOURCE_ARCHIVED`
62
+
63
+ - **含义**:资源已归档,默认不允许作为 active 资源继续使用。
64
+ - **常见触发**:直接安装或发布已归档资源,或重复归档已经在 `archive/<plural>/<name>` 下的资源。
65
+ - **建议处理**:如需继续维护,先执行 `himan resource restore <type> <name>`;如只需安装历史版本,显式传 `--include-archived`。
66
+
61
67
  ### `E_VERSION_NOT_FOUND`
62
68
 
63
69
  - **含义**:指定版本不存在。
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hi-man/himan",
3
- "version": "0.4.1",
3
+ "version": "0.5.1",
4
4
  "description": "Prompt and agent asset management CLI",
5
5
  "keywords": [
6
6
  "ai",