@hi-man/himan 0.2.2 → 0.3.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 +32 -0
- package/README.md +40 -1
- package/dist/adapters/git/repo-manager.js +21 -3
- package/dist/adapters/resource/resource-scanner.js +73 -18
- package/dist/adapters/source/git-source-adapter.js +403 -10
- package/dist/adapters/source/registry-source-adapter.js +3 -0
- package/dist/cli/builders.js +1 -1
- package/dist/cli/source-commands.js +28 -0
- package/dist/domain/source-docs.js +1 -0
- package/dist/services/index.js +83 -24
- package/docs/development.md +4 -3
- package/docs/error-codes.md +2 -2
- package/docs/mvp/README.md +3 -3
- package/docs/mvp/create-resource.md +77 -11
- package/docs/mvp/impl.md +3 -3
- package/package.json +5 -4
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,38 @@ The format is based on Keep a Changelog, and this project follows semver for the
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.3.1] - 2026-05-07
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- Added the `common-project-changelog` skill to enforce changelog and version history placement rules.
|
|
14
|
+
- Added `scripts/release-changelog.mjs` so package version scripts release `[Unreleased]` changelog entries into the new version section.
|
|
15
|
+
|
|
16
|
+
### Changed
|
|
17
|
+
|
|
18
|
+
- Changed `himan source init-docs --force` to list existing source resources in generated docs.
|
|
19
|
+
- Changed `himan source init-docs` to commit and push generated source docs when files changed.
|
|
20
|
+
- Changed generated source docs to show the latest tagged resource version when one exists.
|
|
21
|
+
- Changed `himan publish` to allow resources without `himan.yaml` when their default entry file exists.
|
|
22
|
+
- Changed resource discovery to infer `rule`, `command`, and `skill` resources from default entry files when `himan.yaml` is absent.
|
|
23
|
+
- Changed `himan publish` to reinstall the published version in copy mode, update the lock file, and remove the resource dev directory.
|
|
24
|
+
- Changed package version scripts to archive `[Unreleased]` changelog entries after version bumps.
|
|
25
|
+
|
|
26
|
+
### Fixed
|
|
27
|
+
|
|
28
|
+
- Fixed `himan source init-docs --force` so existing Codex-style skills with `SKILL.md` front matter are included in generated source docs.
|
|
29
|
+
|
|
30
|
+
## [0.3.0] - 2026-05-07
|
|
31
|
+
|
|
32
|
+
### Added
|
|
33
|
+
|
|
34
|
+
- Added `himan source init-docs` to scaffold source-level `README.md` and `CHANGELOG.md` files.
|
|
35
|
+
- Added automatic source-level README/CHANGELOG maintenance for resource create and publish flows.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- Documented the recommended Git source repository-level `README.md` and `CHANGELOG.md` convention.
|
|
40
|
+
|
|
9
41
|
## [0.2.2] - 2026-05-07
|
|
10
42
|
|
|
11
43
|
### Added
|
package/README.md
CHANGED
|
@@ -66,6 +66,43 @@ himan publish rule my-rule --patch
|
|
|
66
66
|
|
|
67
67
|
版本以 Git tag 为准,格式:`rule/my-rule@1.0.0`。更多设计见 [docs/mvp](./docs/mvp/README.md)。
|
|
68
68
|
|
|
69
|
+
## Source 仓库结构
|
|
70
|
+
|
|
71
|
+
himan Git source 是一个普通 Git 仓库,推荐先维护仓库级 `README.md` 和 `CHANGELOG.md`,用于说明整个资源集合,而不是把说明文档塞进每个 agent 的最终消费目录。
|
|
72
|
+
|
|
73
|
+
```text
|
|
74
|
+
your-himan-source/
|
|
75
|
+
README.md
|
|
76
|
+
CHANGELOG.md
|
|
77
|
+
rules/
|
|
78
|
+
my-rule/
|
|
79
|
+
himan.yaml
|
|
80
|
+
content.md
|
|
81
|
+
commands/
|
|
82
|
+
my-command/
|
|
83
|
+
himan.yaml
|
|
84
|
+
content.md
|
|
85
|
+
skills/
|
|
86
|
+
my-skill/
|
|
87
|
+
himan.yaml
|
|
88
|
+
SKILL.md
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
- `README.md`:source 仓库入口文档,建议记录资源目录说明、推荐安装方式、默认 agent 策略、常用资源索引和维护约定。
|
|
92
|
+
- `CHANGELOG.md`:source 仓库级变更记录,建议记录新增、变更、废弃、移除的资源,以及重要版本发布说明。
|
|
93
|
+
- `rules/`、`commands/`、`skills/`:按资源类型分组;每个子目录是一份 himan 资源。
|
|
94
|
+
- `himan.yaml`:可选资源元数据;存在时供 himan 扫描、校验、读取入口和默认 agent。
|
|
95
|
+
- `content.md` / `SKILL.md`:资源主入口;没有 `himan.yaml` 时,`rule` / `command` 默认使用 `content.md`,`skill` 默认使用 `SKILL.md`。
|
|
96
|
+
|
|
97
|
+
可通过 `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。
|
|
98
|
+
|
|
99
|
+
`himan create` 和 `himan publish` 会自动维护 source 根目录文档:
|
|
100
|
+
|
|
101
|
+
- `README.md`:只更新 `<!-- himan:resources:start -->` 和 `<!-- himan:resources:end -->` 之间的资源索引;如果没有 marker,会在文件末尾追加一个受控资源索引区。
|
|
102
|
+
- `CHANGELOG.md`:向 `[Unreleased]` 下追加资源变更条目;`create` 记录 `Added`,`publish` 记录 `Changed` / published version。
|
|
103
|
+
|
|
104
|
+
仓库根目录的 `README.md` 和 `CHANGELOG.md` 不会被安装到 agent 目录;agent 只消费被安装的具体资源目录。当前安装实现会 materialize 资源目录本身,因此对 Cursor 这类要求特定单文件格式的 agent,资源目录内应避免放入会干扰识别的额外文件。
|
|
105
|
+
|
|
69
106
|
## 常用命令
|
|
70
107
|
|
|
71
108
|
### 1) source(数据源)
|
|
@@ -76,6 +113,7 @@ himan publish rule my-rule --patch
|
|
|
76
113
|
| `source add <name> <git_url>` | 添加命名 Git 源 |
|
|
77
114
|
| `source use <name>` | 切换默认源 |
|
|
78
115
|
| `source list [--json]` | 查看已配置源(标记当前 default) |
|
|
116
|
+
| `source init-docs [--force] [--dry-run] [--json]` | 为当前 default source 生成仓库级 README/CHANGELOG |
|
|
79
117
|
| `source init <git_url>` | 与 `init` 等价,便于统一走 `himan source ...` 入口 |
|
|
80
118
|
|
|
81
119
|
等价独立命令:
|
|
@@ -84,6 +122,7 @@ himan publish rule my-rule --patch
|
|
|
84
122
|
- `himan-source add <name> <git_url>`
|
|
85
123
|
- `himan-source use <name>`
|
|
86
124
|
- `himan-source list [--json]`
|
|
125
|
+
- `himan-source init-docs [--force] [--dry-run] [--json]`
|
|
87
126
|
|
|
88
127
|
### 2) resource(资源)
|
|
89
128
|
|
|
@@ -122,7 +161,7 @@ himan publish rule my-rule --patch
|
|
|
122
161
|
说明:资源与项目相关命令统一使用 `--agent` 指定目标 Agent。
|
|
123
162
|
若未显式传 `--agent`,`create` / `install` 会使用当前项目默认 agent、全局默认 agent、资源 metadata 或内置默认 `cursor` 中最合适的一项;`dev` 会优先使用 lock 中记录的 agent。
|
|
124
163
|
|
|
125
|
-
`publish` 优先使用项目里 `.himan/dev`
|
|
164
|
+
`publish` 优先使用项目里 `.himan/dev` 对应目录,否则用源仓库里对应目录。若资源目录包含 `himan.yaml`,发布前会校验元数据与入口文件;若没有 `himan.yaml`,则按默认入口推断最小元数据并发布,不会强制创建 `himan.yaml`。发布需要可推送的 Git 权限。发布 commit 会包含资源目录以及自动维护的 source 根目录 `README.md` / `CHANGELOG.md`。发布成功后会从新版本 store 以 `copy` 模式重新安装到项目目标、更新 lock,并删除对应 `.himan/dev/<type>/<name>` 开发目录。
|
|
126
165
|
|
|
127
166
|
`--json` 模式下,失败时会输出机器可读错误 JSON(`stderr`)。错误码定义见 [docs/error-codes.md](./docs/error-codes.md)。
|
|
128
167
|
|
|
@@ -48,6 +48,20 @@ export class RepoManager {
|
|
|
48
48
|
}
|
|
49
49
|
async commitTagAndPush(repoDir, message, tag, branch, paths = ["."]) {
|
|
50
50
|
const git = simpleGit(repoDir);
|
|
51
|
+
await this.commitChanges(git, message, paths, true);
|
|
52
|
+
await git.addTag(tag);
|
|
53
|
+
await this.pushCurrentBranch(git, branch);
|
|
54
|
+
await git.pushTags("origin");
|
|
55
|
+
}
|
|
56
|
+
async commitAndPush(repoDir, message, branch, paths = ["."]) {
|
|
57
|
+
const git = simpleGit(repoDir);
|
|
58
|
+
const committed = await this.commitChanges(git, message, paths, false);
|
|
59
|
+
if (!committed)
|
|
60
|
+
return false;
|
|
61
|
+
await this.pushCurrentBranch(git, branch);
|
|
62
|
+
return true;
|
|
63
|
+
}
|
|
64
|
+
async commitChanges(git, message, paths, requireChanges) {
|
|
51
65
|
const pathspecs = paths.length > 0 ? paths : ["."];
|
|
52
66
|
await git.add(pathspecs);
|
|
53
67
|
const stagedFiles = await git.raw([
|
|
@@ -58,14 +72,18 @@ export class RepoManager {
|
|
|
58
72
|
...pathspecs,
|
|
59
73
|
]);
|
|
60
74
|
if (!stagedFiles.trim()) {
|
|
61
|
-
|
|
75
|
+
if (requireChanges) {
|
|
76
|
+
throw new HimanError(errorCodes.PUBLISH_NO_CHANGES, "No changes to publish.");
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
62
79
|
}
|
|
63
80
|
await git.commit(message, pathspecs);
|
|
64
|
-
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
async pushCurrentBranch(git, branch) {
|
|
65
84
|
const currentBranch = (await git.raw(["rev-parse", "--abbrev-ref", "HEAD"])).trim();
|
|
66
85
|
const targetBranch = branch ?? currentBranch;
|
|
67
86
|
await git.push("origin", targetBranch);
|
|
68
|
-
await git.pushTags("origin");
|
|
69
87
|
}
|
|
70
88
|
async exists(targetPath) {
|
|
71
89
|
try {
|
|
@@ -15,28 +15,80 @@ export class ResourceScanner {
|
|
|
15
15
|
const result = [];
|
|
16
16
|
for (const resourceDir of resourceDirs) {
|
|
17
17
|
const yamlPath = path.join(baseDir, resourceDir.name, "himan.yaml");
|
|
18
|
-
if (
|
|
18
|
+
if (await this.exists(yamlPath)) {
|
|
19
|
+
const raw = await fs.readFile(yamlPath, "utf8");
|
|
20
|
+
const parsed = YAML.parse(raw);
|
|
21
|
+
if (!parsed)
|
|
22
|
+
continue;
|
|
23
|
+
if (parsed.type !== type)
|
|
24
|
+
continue;
|
|
25
|
+
if (!parsed.name || !parsed.entry)
|
|
26
|
+
continue;
|
|
27
|
+
result.push({
|
|
28
|
+
name: parsed.name,
|
|
29
|
+
type,
|
|
30
|
+
entry: parsed.entry,
|
|
31
|
+
description: parsed.description,
|
|
32
|
+
agents: Array.isArray(parsed.agents)
|
|
33
|
+
? (parsed.agents ?? [])
|
|
34
|
+
: (parsed.targets ?? []),
|
|
35
|
+
});
|
|
19
36
|
continue;
|
|
20
|
-
|
|
21
|
-
const
|
|
22
|
-
if (
|
|
23
|
-
|
|
24
|
-
if (parsed.type !== type)
|
|
25
|
-
continue;
|
|
26
|
-
if (!parsed.name || !parsed.entry)
|
|
27
|
-
continue;
|
|
28
|
-
result.push({
|
|
29
|
-
name: parsed.name,
|
|
30
|
-
type,
|
|
31
|
-
entry: parsed.entry,
|
|
32
|
-
description: parsed.description,
|
|
33
|
-
agents: Array.isArray(parsed.agents)
|
|
34
|
-
? (parsed.agents ?? [])
|
|
35
|
-
: (parsed.targets ?? []),
|
|
36
|
-
});
|
|
37
|
+
}
|
|
38
|
+
const inferred = await this.inferResourceMeta(path.join(baseDir, resourceDir.name), resourceDir.name, type);
|
|
39
|
+
if (inferred)
|
|
40
|
+
result.push(inferred);
|
|
37
41
|
}
|
|
38
42
|
return result;
|
|
39
43
|
}
|
|
44
|
+
async inferResourceMeta(resourceDir, dirName, type) {
|
|
45
|
+
const entry = this.getDefaultEntry(type);
|
|
46
|
+
const entryPath = path.join(resourceDir, entry);
|
|
47
|
+
if (!(await this.exists(entryPath)))
|
|
48
|
+
return undefined;
|
|
49
|
+
const metadata = type === "skill" ? await this.readSkillFrontMatter(entryPath) : null;
|
|
50
|
+
return {
|
|
51
|
+
name: this.readStringMetadata(metadata, "name") ?? dirName,
|
|
52
|
+
type,
|
|
53
|
+
entry,
|
|
54
|
+
description: this.readStringMetadata(metadata, "description"),
|
|
55
|
+
agents: this.readStringArrayMetadata(metadata, "agents") ??
|
|
56
|
+
this.readStringArrayMetadata(metadata, "targets") ??
|
|
57
|
+
[],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
async readSkillFrontMatter(skillPath) {
|
|
61
|
+
const raw = await fs.readFile(skillPath, "utf8");
|
|
62
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(raw.trimStart());
|
|
63
|
+
if (!match)
|
|
64
|
+
return null;
|
|
65
|
+
try {
|
|
66
|
+
const parsed = YAML.parse(match[1]);
|
|
67
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
68
|
+
? parsed
|
|
69
|
+
: null;
|
|
70
|
+
}
|
|
71
|
+
catch {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
readStringMetadata(metadata, key) {
|
|
76
|
+
const value = metadata?.[key];
|
|
77
|
+
if (typeof value !== "string")
|
|
78
|
+
return undefined;
|
|
79
|
+
const trimmed = value.trim();
|
|
80
|
+
return trimmed ? trimmed : undefined;
|
|
81
|
+
}
|
|
82
|
+
readStringArrayMetadata(metadata, key) {
|
|
83
|
+
const value = metadata?.[key];
|
|
84
|
+
if (!Array.isArray(value))
|
|
85
|
+
return undefined;
|
|
86
|
+
const items = value
|
|
87
|
+
.filter((item) => typeof item === "string")
|
|
88
|
+
.map((item) => item.trim())
|
|
89
|
+
.filter(Boolean);
|
|
90
|
+
return items.length > 0 ? items : undefined;
|
|
91
|
+
}
|
|
40
92
|
async exists(targetPath) {
|
|
41
93
|
try {
|
|
42
94
|
await fs.access(targetPath);
|
|
@@ -53,4 +105,7 @@ export class ResourceScanner {
|
|
|
53
105
|
return "commands";
|
|
54
106
|
return "skills";
|
|
55
107
|
}
|
|
108
|
+
getDefaultEntry(type) {
|
|
109
|
+
return type === "skill" ? "SKILL.md" : "content.md";
|
|
110
|
+
}
|
|
56
111
|
}
|
|
@@ -7,6 +7,9 @@ import { createHash } from "node:crypto";
|
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import YAML from "yaml";
|
|
9
9
|
import { IndexCacheStore } from "../../state/index-cache-store.js";
|
|
10
|
+
const RESOURCE_TYPES = ["rule", "command", "skill"];
|
|
11
|
+
const README_RESOURCES_START = "<!-- himan:resources:start -->";
|
|
12
|
+
const README_RESOURCES_END = "<!-- himan:resources:end -->";
|
|
10
13
|
export class GitSourceAdapter {
|
|
11
14
|
repoManager = new RepoManager();
|
|
12
15
|
scanner = new ResourceScanner();
|
|
@@ -24,7 +27,7 @@ export class GitSourceAdapter {
|
|
|
24
27
|
const repoId = this.sourceConfig?.repoId ?? "default";
|
|
25
28
|
const typeDir = this.getTypeDir(type);
|
|
26
29
|
const baseDir = path.join(repoDir, typeDir);
|
|
27
|
-
const metadataHash = await this.getResourceMetadataHash(baseDir);
|
|
30
|
+
const metadataHash = await this.getResourceMetadataHash(baseDir, type);
|
|
28
31
|
const cached = await this.indexStore.get(repoId, type);
|
|
29
32
|
if (cached && cached.metadataHash === metadataHash) {
|
|
30
33
|
return cached.resources;
|
|
@@ -48,18 +51,27 @@ export class GitSourceAdapter {
|
|
|
48
51
|
async publish(type, name, version, sourceDir) {
|
|
49
52
|
const repoDir = this.getRepoDir();
|
|
50
53
|
const targetDir = path.join(repoDir, `${type}s`, name);
|
|
51
|
-
const
|
|
54
|
+
const metadataResult = await this.validatePublishResource(type, name, sourceDir);
|
|
52
55
|
const sameDir = await this.isSameDirectory(sourceDir, targetDir);
|
|
53
56
|
if (!sameDir) {
|
|
54
57
|
await fs.rm(targetDir, { recursive: true, force: true });
|
|
55
58
|
await fs.mkdir(path.dirname(targetDir), { recursive: true });
|
|
56
59
|
await fs.cp(sourceDir, targetDir, { recursive: true });
|
|
57
60
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
+
if (metadataResult.shouldWriteMetadata) {
|
|
62
|
+
const yamlPath = path.join(targetDir, "himan.yaml");
|
|
63
|
+
const metadata = { ...metadataResult.metadata, version };
|
|
64
|
+
await fs.writeFile(yamlPath, YAML.stringify(metadata), "utf8");
|
|
65
|
+
}
|
|
66
|
+
const docsPaths = await this.maintainSourceDocs(repoDir, {
|
|
67
|
+
section: "Changed",
|
|
68
|
+
line: `- Published \`${type}/${name}@${version}\`.`,
|
|
69
|
+
}, new Map([[this.getResourceVersionOverrideKey(type, name), version]]));
|
|
61
70
|
const tag = `${type}/${name}@${version}`;
|
|
62
|
-
await this.repoManager.commitTagAndPush(repoDir, `publish ${type}/${name}@${version}`, tag, undefined, [
|
|
71
|
+
await this.repoManager.commitTagAndPush(repoDir, `publish ${type}/${name}@${version}`, tag, undefined, [
|
|
72
|
+
path.relative(repoDir, targetDir),
|
|
73
|
+
...docsPaths.map((docPath) => path.relative(repoDir, docPath)),
|
|
74
|
+
]);
|
|
63
75
|
return { version, tag };
|
|
64
76
|
}
|
|
65
77
|
async create(type, name, options) {
|
|
@@ -67,7 +79,8 @@ export class GitSourceAdapter {
|
|
|
67
79
|
const resourceDir = path.join(repoDir, this.getTypeDir(type), name);
|
|
68
80
|
const entry = options.entry ?? this.getDefaultEntry(type);
|
|
69
81
|
const agents = options.agents?.length ? options.agents : ["cursor"];
|
|
70
|
-
|
|
82
|
+
const resourceExists = await this.exists(resourceDir);
|
|
83
|
+
if (resourceExists && !options.force) {
|
|
71
84
|
throw new HimanError(errorCodes.RESOURCE_EXISTS, `Resource already exists: ${type}/${name}`);
|
|
72
85
|
}
|
|
73
86
|
const files = [path.join(resourceDir, "himan.yaml"), path.join(resourceDir, entry)];
|
|
@@ -83,6 +96,12 @@ export class GitSourceAdapter {
|
|
|
83
96
|
agents,
|
|
84
97
|
}), "utf8");
|
|
85
98
|
await fs.writeFile(path.join(resourceDir, entry), this.getDefaultContent(type, name), "utf8");
|
|
99
|
+
await this.maintainSourceDocs(repoDir, {
|
|
100
|
+
section: resourceExists ? "Changed" : "Added",
|
|
101
|
+
line: resourceExists
|
|
102
|
+
? `- Updated \`${type}/${name}\`.`
|
|
103
|
+
: `- Added \`${type}/${name}\`.`,
|
|
104
|
+
});
|
|
86
105
|
}
|
|
87
106
|
return {
|
|
88
107
|
type,
|
|
@@ -92,6 +111,40 @@ export class GitSourceAdapter {
|
|
|
92
111
|
dryRun: Boolean(options.dryRun),
|
|
93
112
|
};
|
|
94
113
|
}
|
|
114
|
+
async initDocs(options = {}) {
|
|
115
|
+
const repoDir = this.getRepoDir();
|
|
116
|
+
const files = [
|
|
117
|
+
{
|
|
118
|
+
path: path.join(repoDir, "README.md"),
|
|
119
|
+
content: await this.buildReadmeContent(repoDir),
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
path: path.join(repoDir, "CHANGELOG.md"),
|
|
123
|
+
content: await this.buildChangelogContent(repoDir),
|
|
124
|
+
},
|
|
125
|
+
];
|
|
126
|
+
const results = [];
|
|
127
|
+
const changedPaths = [];
|
|
128
|
+
for (const file of files) {
|
|
129
|
+
const exists = await this.exists(file.path);
|
|
130
|
+
const action = exists ? (options.force ? "updated" : "skipped") : "created";
|
|
131
|
+
const reason = action === "skipped" ? "file already exists" : undefined;
|
|
132
|
+
results.push({ path: file.path, action, reason });
|
|
133
|
+
if (!options.dryRun && action !== "skipped") {
|
|
134
|
+
await fs.writeFile(file.path, file.content, "utf8");
|
|
135
|
+
changedPaths.push(path.relative(repoDir, file.path));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
const committed = !options.dryRun &&
|
|
139
|
+
changedPaths.length > 0 &&
|
|
140
|
+
(await this.repoManager.commitAndPush(repoDir, "docs: init source docs", undefined, changedPaths));
|
|
141
|
+
return {
|
|
142
|
+
sourceDir: repoDir,
|
|
143
|
+
files: results,
|
|
144
|
+
dryRun: Boolean(options.dryRun),
|
|
145
|
+
committed,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
95
148
|
getRepoDir() {
|
|
96
149
|
if (!this.sourceConfig?.repoDir) {
|
|
97
150
|
throw new HimanError(errorCodes.CONFIG_NOT_FOUND, "Git source is not initialized.");
|
|
@@ -119,7 +172,10 @@ export class GitSourceAdapter {
|
|
|
119
172
|
async validatePublishResource(type, name, resourceDir) {
|
|
120
173
|
const yamlPath = path.join(resourceDir, "himan.yaml");
|
|
121
174
|
if (!(await this.exists(yamlPath))) {
|
|
122
|
-
|
|
175
|
+
return {
|
|
176
|
+
metadata: await this.inferPublishResourceMetadata(type, name, resourceDir),
|
|
177
|
+
shouldWriteMetadata: false,
|
|
178
|
+
};
|
|
123
179
|
}
|
|
124
180
|
const raw = await fs.readFile(yamlPath, "utf8");
|
|
125
181
|
let parsed;
|
|
@@ -165,11 +221,45 @@ export class GitSourceAdapter {
|
|
|
165
221
|
throw this.invalidResourceMetadata(type, name, `Resource entry is not a file: ${entry}`, { yamlPath, entry, entryPath });
|
|
166
222
|
}
|
|
167
223
|
return {
|
|
168
|
-
|
|
224
|
+
metadata: {
|
|
225
|
+
...parsed,
|
|
226
|
+
name,
|
|
227
|
+
type,
|
|
228
|
+
entry,
|
|
229
|
+
},
|
|
230
|
+
shouldWriteMetadata: true,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
async inferPublishResourceMetadata(type, name, resourceDir) {
|
|
234
|
+
const entry = this.getDefaultEntry(type);
|
|
235
|
+
const entryPath = path.join(resourceDir, entry);
|
|
236
|
+
let entryStat;
|
|
237
|
+
try {
|
|
238
|
+
entryStat = await fs.stat(entryPath);
|
|
239
|
+
}
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (!this.isNotFoundError(error)) {
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
throw this.invalidResourceMetadata(type, name, `Missing himan.yaml and default entry file for publish: ${entry}`, { yamlPath: path.join(resourceDir, "himan.yaml"), entry, entryPath });
|
|
245
|
+
}
|
|
246
|
+
if (!entryStat.isFile()) {
|
|
247
|
+
throw this.invalidResourceMetadata(type, name, `Default resource entry is not a file: ${entry}`, { entry, entryPath });
|
|
248
|
+
}
|
|
249
|
+
const frontMatter = type === "skill" ? await this.readSkillFrontMatter(entryPath) : null;
|
|
250
|
+
const metadata = {
|
|
169
251
|
name,
|
|
170
252
|
type,
|
|
171
253
|
entry,
|
|
172
254
|
};
|
|
255
|
+
const description = this.readStringMetadata(frontMatter, "description");
|
|
256
|
+
if (description)
|
|
257
|
+
metadata.description = description;
|
|
258
|
+
const agents = this.readStringArrayMetadata(frontMatter, "agents") ??
|
|
259
|
+
this.readStringArrayMetadata(frontMatter, "targets");
|
|
260
|
+
if (agents)
|
|
261
|
+
metadata.agents = agents;
|
|
262
|
+
return metadata;
|
|
173
263
|
}
|
|
174
264
|
invalidResourceMetadata(type, name, message, details) {
|
|
175
265
|
return new HimanError(errorCodes.INVALID_RESOURCE_METADATA, `Invalid metadata for ${type}/${name}: ${message}`, details);
|
|
@@ -184,7 +274,7 @@ export class GitSourceAdapter {
|
|
|
184
274
|
return "commands";
|
|
185
275
|
return "skills";
|
|
186
276
|
}
|
|
187
|
-
async getResourceMetadataHash(baseDir) {
|
|
277
|
+
async getResourceMetadataHash(baseDir, type) {
|
|
188
278
|
const hash = createHash("sha256");
|
|
189
279
|
hash.update("himan-resource-index-v1");
|
|
190
280
|
if (!(await this.exists(baseDir))) {
|
|
@@ -210,6 +300,18 @@ export class GitSourceAdapter {
|
|
|
210
300
|
throw error;
|
|
211
301
|
}
|
|
212
302
|
hash.update("\0yaml-missing");
|
|
303
|
+
const entryPath = path.join(baseDir, resourceDirName, this.getDefaultEntry(type));
|
|
304
|
+
try {
|
|
305
|
+
const raw = await fs.readFile(entryPath);
|
|
306
|
+
hash.update("\0entry:");
|
|
307
|
+
hash.update(raw);
|
|
308
|
+
}
|
|
309
|
+
catch (entryError) {
|
|
310
|
+
if (!this.isNotFoundError(entryError)) {
|
|
311
|
+
throw entryError;
|
|
312
|
+
}
|
|
313
|
+
hash.update("\0entry-missing");
|
|
314
|
+
}
|
|
213
315
|
}
|
|
214
316
|
}
|
|
215
317
|
return hash.digest("hex");
|
|
@@ -232,4 +334,295 @@ export class GitSourceAdapter {
|
|
|
232
334
|
}
|
|
233
335
|
return `# ${name}\n\nDescribe skill workflow here.\n`;
|
|
234
336
|
}
|
|
337
|
+
async buildReadmeContent(repoDir, versionOverrides = new Map()) {
|
|
338
|
+
const resourceLines = await this.buildResourceIndex(repoDir, versionOverrides);
|
|
339
|
+
const repo = this.sourceConfig?.repo ?? "<git_url>";
|
|
340
|
+
return [
|
|
341
|
+
`# ${this.getSourceTitle()}`,
|
|
342
|
+
"",
|
|
343
|
+
"Himan source repository for reusable agent resources.",
|
|
344
|
+
"",
|
|
345
|
+
"## Resources",
|
|
346
|
+
"",
|
|
347
|
+
README_RESOURCES_START,
|
|
348
|
+
...resourceLines,
|
|
349
|
+
README_RESOURCES_END,
|
|
350
|
+
"",
|
|
351
|
+
"## Usage",
|
|
352
|
+
"",
|
|
353
|
+
"```bash",
|
|
354
|
+
`himan source add team ${repo}`,
|
|
355
|
+
"himan source use team",
|
|
356
|
+
"himan list rule",
|
|
357
|
+
"himan install rule <name>",
|
|
358
|
+
"```",
|
|
359
|
+
"",
|
|
360
|
+
"## Maintenance",
|
|
361
|
+
"",
|
|
362
|
+
"- Add resources with `himan create <type> <name>`.",
|
|
363
|
+
"- Publish resource versions with `himan publish <type> <name>`.",
|
|
364
|
+
"- Record source-level changes in `CHANGELOG.md`.",
|
|
365
|
+
"- Resource versions are tracked by Git tags such as `rule/code-review@1.0.0`.",
|
|
366
|
+
"",
|
|
367
|
+
].join("\n");
|
|
368
|
+
}
|
|
369
|
+
async buildChangelogContent(repoDir) {
|
|
370
|
+
const resourceLines = await this.buildExistingResourceChangelogLines(repoDir);
|
|
371
|
+
return [
|
|
372
|
+
"# Changelog",
|
|
373
|
+
"",
|
|
374
|
+
"All notable source-level resource changes are documented in this file.",
|
|
375
|
+
"",
|
|
376
|
+
"## [Unreleased]",
|
|
377
|
+
"",
|
|
378
|
+
"### Added",
|
|
379
|
+
"",
|
|
380
|
+
"- Initial source README/CHANGELOG scaffold.",
|
|
381
|
+
...resourceLines,
|
|
382
|
+
"",
|
|
383
|
+
].join("\n");
|
|
384
|
+
}
|
|
385
|
+
async buildResourceIndex(repoDir, versionOverrides = new Map()) {
|
|
386
|
+
const sections = [];
|
|
387
|
+
for (const type of RESOURCE_TYPES) {
|
|
388
|
+
const resources = await this.collectResourceDocsItems(repoDir, type);
|
|
389
|
+
sections.push(`### ${this.getTypeLabel(type)}`, "");
|
|
390
|
+
if (resources.length === 0) {
|
|
391
|
+
sections.push(`- No ${type} resources yet.`, "");
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
for (const resource of resources) {
|
|
395
|
+
const ref = await this.getResourceRef(repoDir, resource.type, resource.name, versionOverrides);
|
|
396
|
+
sections.push(`- \`${ref}\`${resource.description ? `: ${resource.description}` : ""}`);
|
|
397
|
+
}
|
|
398
|
+
sections.push("");
|
|
399
|
+
}
|
|
400
|
+
while (sections.at(-1) === "") {
|
|
401
|
+
sections.pop();
|
|
402
|
+
}
|
|
403
|
+
return sections;
|
|
404
|
+
}
|
|
405
|
+
async buildExistingResourceChangelogLines(repoDir) {
|
|
406
|
+
const lines = [];
|
|
407
|
+
for (const type of RESOURCE_TYPES) {
|
|
408
|
+
const resources = await this.collectResourceDocsItems(repoDir, type);
|
|
409
|
+
for (const resource of resources) {
|
|
410
|
+
const ref = await this.getResourceRef(repoDir, resource.type, resource.name);
|
|
411
|
+
lines.push(`- Documented existing resource \`${ref}\`.`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
return lines;
|
|
415
|
+
}
|
|
416
|
+
async collectResourceDocsItems(repoDir, type) {
|
|
417
|
+
const resources = await this.scanner.scanByType(repoDir, type);
|
|
418
|
+
const items = resources.map((resource) => ({
|
|
419
|
+
name: resource.name,
|
|
420
|
+
type: resource.type,
|
|
421
|
+
description: resource.description,
|
|
422
|
+
}));
|
|
423
|
+
const managedNames = new Set(resources.map((resource) => resource.name));
|
|
424
|
+
items.push(...(await this.scanEntryBasedDocsItems(repoDir, type, managedNames)));
|
|
425
|
+
return items.sort((a, b) => a.name.localeCompare(b.name));
|
|
426
|
+
}
|
|
427
|
+
async scanEntryBasedDocsItems(repoDir, type, managedNames) {
|
|
428
|
+
const baseDir = path.join(repoDir, this.getTypeDir(type));
|
|
429
|
+
if (!(await this.exists(baseDir)))
|
|
430
|
+
return [];
|
|
431
|
+
const entries = await fs.readdir(baseDir, { withFileTypes: true });
|
|
432
|
+
const items = [];
|
|
433
|
+
for (const entry of entries) {
|
|
434
|
+
if (!entry.isDirectory())
|
|
435
|
+
continue;
|
|
436
|
+
const resourceEntry = this.getDefaultEntry(type);
|
|
437
|
+
const entryPath = path.join(baseDir, entry.name, resourceEntry);
|
|
438
|
+
if (!(await this.exists(entryPath)))
|
|
439
|
+
continue;
|
|
440
|
+
const metadata = type === "skill" ? await this.readSkillFrontMatter(entryPath) : null;
|
|
441
|
+
const name = this.readStringMetadata(metadata, "name") ?? entry.name;
|
|
442
|
+
if (managedNames.has(name) || managedNames.has(entry.name))
|
|
443
|
+
continue;
|
|
444
|
+
items.push({
|
|
445
|
+
name,
|
|
446
|
+
type,
|
|
447
|
+
description: this.readStringMetadata(metadata, "description"),
|
|
448
|
+
});
|
|
449
|
+
}
|
|
450
|
+
return items;
|
|
451
|
+
}
|
|
452
|
+
async readSkillFrontMatter(skillPath) {
|
|
453
|
+
const raw = await fs.readFile(skillPath, "utf8");
|
|
454
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(raw.trimStart());
|
|
455
|
+
if (!match)
|
|
456
|
+
return null;
|
|
457
|
+
try {
|
|
458
|
+
const parsed = YAML.parse(match[1]);
|
|
459
|
+
return this.isRecord(parsed) ? parsed : null;
|
|
460
|
+
}
|
|
461
|
+
catch {
|
|
462
|
+
return null;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
readStringMetadata(metadata, key) {
|
|
466
|
+
const value = metadata?.[key];
|
|
467
|
+
if (typeof value !== "string")
|
|
468
|
+
return undefined;
|
|
469
|
+
const trimmed = value.trim();
|
|
470
|
+
return trimmed ? trimmed : undefined;
|
|
471
|
+
}
|
|
472
|
+
readStringArrayMetadata(metadata, key) {
|
|
473
|
+
const value = metadata?.[key];
|
|
474
|
+
if (!Array.isArray(value))
|
|
475
|
+
return undefined;
|
|
476
|
+
const items = value
|
|
477
|
+
.filter((item) => typeof item === "string")
|
|
478
|
+
.map((item) => item.trim())
|
|
479
|
+
.filter(Boolean);
|
|
480
|
+
return items.length > 0 ? items : undefined;
|
|
481
|
+
}
|
|
482
|
+
async maintainSourceDocs(repoDir, changelogEntry, versionOverrides = new Map()) {
|
|
483
|
+
const readmePath = await this.updateReadmeResourceIndex(repoDir, versionOverrides);
|
|
484
|
+
const changelogPath = await this.updateChangelog(repoDir, changelogEntry);
|
|
485
|
+
return [readmePath, changelogPath];
|
|
486
|
+
}
|
|
487
|
+
async updateReadmeResourceIndex(repoDir, versionOverrides = new Map()) {
|
|
488
|
+
const readmePath = path.join(repoDir, "README.md");
|
|
489
|
+
if (!(await this.exists(readmePath))) {
|
|
490
|
+
await fs.writeFile(readmePath, await this.buildReadmeContent(repoDir, versionOverrides), "utf8");
|
|
491
|
+
return readmePath;
|
|
492
|
+
}
|
|
493
|
+
const current = await fs.readFile(readmePath, "utf8");
|
|
494
|
+
const resourceSection = [
|
|
495
|
+
README_RESOURCES_START,
|
|
496
|
+
...(await this.buildResourceIndex(repoDir, versionOverrides)),
|
|
497
|
+
README_RESOURCES_END,
|
|
498
|
+
].join("\n");
|
|
499
|
+
const updated = this.replaceOrAppendReadmeResourceSection(current, resourceSection);
|
|
500
|
+
if (updated !== current) {
|
|
501
|
+
await fs.writeFile(readmePath, updated, "utf8");
|
|
502
|
+
}
|
|
503
|
+
return readmePath;
|
|
504
|
+
}
|
|
505
|
+
replaceOrAppendReadmeResourceSection(content, resourceSection) {
|
|
506
|
+
const startIndex = content.indexOf(README_RESOURCES_START);
|
|
507
|
+
const endIndex = content.indexOf(README_RESOURCES_END);
|
|
508
|
+
if (startIndex >= 0 && endIndex > startIndex) {
|
|
509
|
+
const before = content.slice(0, startIndex).replace(/\s*$/, "\n");
|
|
510
|
+
const after = content
|
|
511
|
+
.slice(endIndex + README_RESOURCES_END.length)
|
|
512
|
+
.replace(/^\s*/, "\n\n");
|
|
513
|
+
return `${before}${resourceSection}${after}`.replace(/\s*$/, "\n");
|
|
514
|
+
}
|
|
515
|
+
const base = content.replace(/\s*$/, "");
|
|
516
|
+
return `${base}\n\n## Resources\n\n${resourceSection}\n`;
|
|
517
|
+
}
|
|
518
|
+
async updateChangelog(repoDir, entry) {
|
|
519
|
+
const changelogPath = path.join(repoDir, "CHANGELOG.md");
|
|
520
|
+
const current = (await this.exists(changelogPath))
|
|
521
|
+
? await fs.readFile(changelogPath, "utf8")
|
|
522
|
+
: this.buildChangelogBaseContent();
|
|
523
|
+
const updated = this.insertChangelogEntry(current, entry);
|
|
524
|
+
if (updated !== current) {
|
|
525
|
+
await fs.writeFile(changelogPath, updated, "utf8");
|
|
526
|
+
}
|
|
527
|
+
return changelogPath;
|
|
528
|
+
}
|
|
529
|
+
buildChangelogBaseContent() {
|
|
530
|
+
return [
|
|
531
|
+
"# Changelog",
|
|
532
|
+
"",
|
|
533
|
+
"All notable source-level resource changes are documented in this file.",
|
|
534
|
+
"",
|
|
535
|
+
"## [Unreleased]",
|
|
536
|
+
"",
|
|
537
|
+
].join("\n");
|
|
538
|
+
}
|
|
539
|
+
insertChangelogEntry(content, entry) {
|
|
540
|
+
const lines = content.replace(/\s*$/, "").split("\n");
|
|
541
|
+
let unreleasedIndex = lines.findIndex((line) => line.trim() === "## [Unreleased]");
|
|
542
|
+
if (unreleasedIndex === -1) {
|
|
543
|
+
const firstVersionIndex = lines.findIndex((line) => line.startsWith("## "));
|
|
544
|
+
const insertIndex = firstVersionIndex === -1 ? lines.length : firstVersionIndex;
|
|
545
|
+
lines.splice(insertIndex, 0, "## [Unreleased]", "");
|
|
546
|
+
unreleasedIndex = insertIndex;
|
|
547
|
+
}
|
|
548
|
+
const blockEnd = this.findNextHeadingIndex(lines, unreleasedIndex + 1, "## ");
|
|
549
|
+
const unreleasedLines = lines.slice(unreleasedIndex, blockEnd);
|
|
550
|
+
if (unreleasedLines.includes(entry.line)) {
|
|
551
|
+
return `${lines.join("\n")}\n`;
|
|
552
|
+
}
|
|
553
|
+
const sectionHeading = `### ${entry.section}`;
|
|
554
|
+
const sectionIndex = lines.findIndex((line, index) => index > unreleasedIndex && index < blockEnd && line.trim() === sectionHeading);
|
|
555
|
+
if (sectionIndex >= 0) {
|
|
556
|
+
const insertIndex = lines[sectionIndex + 1] === "" ? sectionIndex + 2 : sectionIndex + 1;
|
|
557
|
+
lines.splice(insertIndex, 0, entry.line);
|
|
558
|
+
return `${lines.join("\n")}\n`;
|
|
559
|
+
}
|
|
560
|
+
const insertIndex = this.findChangelogSectionInsertIndex(lines, unreleasedIndex, blockEnd, entry.section);
|
|
561
|
+
lines.splice(insertIndex, 0, `### ${entry.section}`, "", entry.line, "");
|
|
562
|
+
return `${lines.join("\n").replace(/\s*$/, "")}\n`;
|
|
563
|
+
}
|
|
564
|
+
findNextHeadingIndex(lines, startIndex, headingPrefix) {
|
|
565
|
+
const found = lines.findIndex((line, index) => index >= startIndex && line.startsWith(headingPrefix));
|
|
566
|
+
return found === -1 ? lines.length : found;
|
|
567
|
+
}
|
|
568
|
+
findChangelogSectionInsertIndex(lines, unreleasedIndex, blockEnd, section) {
|
|
569
|
+
const sectionOrder = ["Added", "Changed"];
|
|
570
|
+
const sectionRank = sectionOrder.indexOf(section);
|
|
571
|
+
for (let index = unreleasedIndex + 1; index < blockEnd; index += 1) {
|
|
572
|
+
const line = lines[index].trim();
|
|
573
|
+
if (!line.startsWith("### "))
|
|
574
|
+
continue;
|
|
575
|
+
const foundSection = line.slice(4);
|
|
576
|
+
const foundRank = sectionOrder.indexOf(foundSection);
|
|
577
|
+
if (foundRank > sectionRank) {
|
|
578
|
+
return index;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
return blockEnd;
|
|
582
|
+
}
|
|
583
|
+
async readResourceVersion(repoDir, type, name) {
|
|
584
|
+
const yamlPath = path.join(repoDir, this.getTypeDir(type), name, "himan.yaml");
|
|
585
|
+
try {
|
|
586
|
+
const raw = await fs.readFile(yamlPath, "utf8");
|
|
587
|
+
const parsed = YAML.parse(raw);
|
|
588
|
+
return typeof parsed?.version === "string" ? parsed.version : undefined;
|
|
589
|
+
}
|
|
590
|
+
catch (error) {
|
|
591
|
+
if (!this.isNotFoundError(error)) {
|
|
592
|
+
throw error;
|
|
593
|
+
}
|
|
594
|
+
return undefined;
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
async getResourceRef(repoDir, type, name, versionOverrides = new Map()) {
|
|
598
|
+
const version = versionOverrides.get(this.getResourceVersionOverrideKey(type, name)) ??
|
|
599
|
+
(await this.readLatestTaggedResourceVersion(repoDir, type, name)) ??
|
|
600
|
+
(await this.readResourceVersion(repoDir, type, name));
|
|
601
|
+
return this.formatResourceRef(type, name, version);
|
|
602
|
+
}
|
|
603
|
+
getResourceVersionOverrideKey(type, name) {
|
|
604
|
+
return `${type}/${name}`;
|
|
605
|
+
}
|
|
606
|
+
async readLatestTaggedResourceVersion(repoDir, type, name) {
|
|
607
|
+
const versions = (await this.repoManager.listTags(repoDir, `${type}/${name}@*`))
|
|
608
|
+
.map((tag) => tag.split("@").at(1) ?? "")
|
|
609
|
+
.filter((version) => semver.valid(version))
|
|
610
|
+
.sort(semver.rcompare);
|
|
611
|
+
return versions.at(0);
|
|
612
|
+
}
|
|
613
|
+
formatResourceRef(type, name, version) {
|
|
614
|
+
return version ? `${type}/${name}@${version}` : `${type}/${name}`;
|
|
615
|
+
}
|
|
616
|
+
getSourceTitle() {
|
|
617
|
+
const repo = this.sourceConfig?.repo?.replace(/\/$/, "");
|
|
618
|
+
const repoName = repo?.split(/[/:]/).at(-1)?.replace(/\.git$/, "");
|
|
619
|
+
return repoName ? `${repoName} Himan Source` : "Himan Source";
|
|
620
|
+
}
|
|
621
|
+
getTypeLabel(type) {
|
|
622
|
+
if (type === "rule")
|
|
623
|
+
return "Rules";
|
|
624
|
+
if (type === "command")
|
|
625
|
+
return "Commands";
|
|
626
|
+
return "Skills";
|
|
627
|
+
}
|
|
235
628
|
}
|
|
@@ -18,4 +18,7 @@ export class RegistrySourceAdapter {
|
|
|
18
18
|
async create(_type, _name, _options) {
|
|
19
19
|
throw new HimanError(errorCodes.NOT_IMPLEMENTED, "Registry source is reserved for phase 2.");
|
|
20
20
|
}
|
|
21
|
+
async initDocs(_options) {
|
|
22
|
+
throw new HimanError(errorCodes.NOT_IMPLEMENTED, "Registry source is reserved for phase 2.");
|
|
23
|
+
}
|
|
21
24
|
}
|
package/dist/cli/builders.js
CHANGED
|
@@ -60,7 +60,7 @@ function appendCommandGroupsHelp(program) {
|
|
|
60
60
|
program.addHelpText("after", `
|
|
61
61
|
Command groups:
|
|
62
62
|
source Data source management (git now, registry reserved)
|
|
63
|
-
init, source init, source add, source use, source list
|
|
63
|
+
init, source init, source add, source use, source list, source init-docs
|
|
64
64
|
resource Source resource discovery and metadata
|
|
65
65
|
list, history, create, resource list, resource history, resource create
|
|
66
66
|
project Resource usage lifecycle in current project
|
|
@@ -55,4 +55,32 @@ export function registerSourceCommands(command, services, options) {
|
|
|
55
55
|
}
|
|
56
56
|
});
|
|
57
57
|
});
|
|
58
|
+
command
|
|
59
|
+
.command("init-docs")
|
|
60
|
+
.option("--force", "overwrite existing README.md and CHANGELOG.md")
|
|
61
|
+
.option("--dry-run", "show files without writing")
|
|
62
|
+
.option("--json", "output json format")
|
|
63
|
+
.description("Create source-level README.md and CHANGELOG.md")
|
|
64
|
+
.action(async (options) => {
|
|
65
|
+
await runAction(async () => {
|
|
66
|
+
const result = await services.initSourceDocs({
|
|
67
|
+
force: options.force,
|
|
68
|
+
dryRun: options.dryRun,
|
|
69
|
+
});
|
|
70
|
+
if (options.json) {
|
|
71
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
process.stdout.write(`Source docs ${result.dryRun ? "dry-run" : "initialized"}: ${result.sourceDir}\n`);
|
|
75
|
+
for (const file of result.files) {
|
|
76
|
+
process.stdout.write(`- ${file.action} ${file.path}${file.reason ? ` (${file.reason})` : ""}\n`);
|
|
77
|
+
}
|
|
78
|
+
if (result.committed) {
|
|
79
|
+
process.stdout.write("Committed and pushed source docs changes.\n");
|
|
80
|
+
}
|
|
81
|
+
else if (!result.dryRun) {
|
|
82
|
+
process.stdout.write("No source docs changes to commit.\n");
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
58
86
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/services/index.js
CHANGED
|
@@ -101,6 +101,10 @@ export class ServiceFactory {
|
|
|
101
101
|
isDefault: name === config.sources?.default,
|
|
102
102
|
}));
|
|
103
103
|
}
|
|
104
|
+
async initSourceDocs(options) {
|
|
105
|
+
const source = await this.loadSourceFromConfig();
|
|
106
|
+
return source.initDocs(options);
|
|
107
|
+
}
|
|
104
108
|
async setAgents(agents, scope, projectDir) {
|
|
105
109
|
const normalized = normalizeAgents(agents);
|
|
106
110
|
if (scope === "project") {
|
|
@@ -194,6 +198,7 @@ export class ServiceFactory {
|
|
|
194
198
|
async publish(type, name, releaseType, projectDir) {
|
|
195
199
|
const source = await this.loadSourceFromConfig();
|
|
196
200
|
const sourceDir = await this.resolvePublishSourceDir(type, name, projectDir);
|
|
201
|
+
const existingInstallInfo = await this.tryResolveInstalledResource(projectDir, type, name);
|
|
197
202
|
const history = await source.history(type, name);
|
|
198
203
|
const latest = history[0]?.version ?? "0.0.0";
|
|
199
204
|
const nextVersion = this.versions.nextVersion(latest, releaseType);
|
|
@@ -204,28 +209,31 @@ export class ServiceFactory {
|
|
|
204
209
|
if (!(await this.exists(storePath))) {
|
|
205
210
|
await source.pull(type, name, nextVersion, storePath);
|
|
206
211
|
}
|
|
207
|
-
const agentsFromMeta = normalizeAgents((await this.readResourceMetaFromDir(storePath))?.agents);
|
|
208
212
|
const locked = await this.getLockedResource(projectDir, type, name);
|
|
213
|
+
const resourceMeta = await this.readResourceMetaFromDir(storePath, type);
|
|
214
|
+
const configuredAgents = await this.getConfiguredAgents(projectDir);
|
|
209
215
|
const nextAgents = locked?.agents?.length
|
|
210
216
|
? normalizeAgents(locked.agents)
|
|
211
|
-
:
|
|
212
|
-
|
|
217
|
+
: existingInstallInfo?.agents.length
|
|
218
|
+
? normalizeAgents(existingInstallInfo.agents)
|
|
219
|
+
: configuredAgents ?? normalizeAgents(resourceMeta?.agents);
|
|
220
|
+
const installMode = "copy";
|
|
213
221
|
const linkPaths = getProjectResourcePaths(projectDir, type, name, nextAgents);
|
|
214
222
|
for (const linkPath of linkPaths) {
|
|
215
|
-
|
|
216
|
-
await this.materializeResource(storePath, linkPath, installMode);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
if (locked) {
|
|
220
|
-
const sourceInfo = await this.getLockSourceInfo();
|
|
221
|
-
await this.lockStore.upsertResource(projectDir, sourceInfo, {
|
|
222
|
-
type,
|
|
223
|
-
name,
|
|
224
|
-
version: nextVersion,
|
|
225
|
-
agents: nextAgents,
|
|
226
|
-
mode: installMode,
|
|
227
|
-
});
|
|
223
|
+
await this.materializeResource(storePath, linkPath, installMode);
|
|
228
224
|
}
|
|
225
|
+
const sourceInfo = await this.getLockSourceInfo();
|
|
226
|
+
await this.lockStore.upsertResource(projectDir, sourceInfo, {
|
|
227
|
+
type,
|
|
228
|
+
name,
|
|
229
|
+
version: nextVersion,
|
|
230
|
+
agents: nextAgents,
|
|
231
|
+
mode: installMode,
|
|
232
|
+
});
|
|
233
|
+
await fs.rm(this.getProjectDevPath(projectDir, type, name), {
|
|
234
|
+
recursive: true,
|
|
235
|
+
force: true,
|
|
236
|
+
});
|
|
229
237
|
return { type, name, version: result.version, tag: result.tag };
|
|
230
238
|
}
|
|
231
239
|
async create(type, name, options, projectDir) {
|
|
@@ -270,7 +278,7 @@ export class ServiceFactory {
|
|
|
270
278
|
if (!(await this.exists(storePath))) {
|
|
271
279
|
await source.pull(type, name, resolvedVersion, storePath);
|
|
272
280
|
}
|
|
273
|
-
const resourceMeta = await this.readResourceMetaFromDir(storePath);
|
|
281
|
+
const resourceMeta = await this.readResourceMetaFromDir(storePath, type);
|
|
274
282
|
const effectiveTargets = await this.resolveEffectiveAgents(projectDir, agents, resourceMeta?.agents);
|
|
275
283
|
const linkPaths = getProjectResourcePaths(projectDir, type, name, effectiveTargets);
|
|
276
284
|
for (const linkPath of linkPaths) {
|
|
@@ -424,7 +432,7 @@ export class ServiceFactory {
|
|
|
424
432
|
throw new HimanError(errorCodes.INSTALL_NOT_FOUND, `Installed resource link not found for ${type}/${name}. Run install first.`);
|
|
425
433
|
}
|
|
426
434
|
const installedPath = await fs.realpath(existingCandidates[0].path);
|
|
427
|
-
const resourceMeta = await this.readResourceMetaFromDir(installedPath);
|
|
435
|
+
const resourceMeta = await this.readResourceMetaFromDir(installedPath, type);
|
|
428
436
|
const agentsFromMeta = resourceMeta?.agents?.length
|
|
429
437
|
? normalizeAgents(resourceMeta.agents)
|
|
430
438
|
: undefined;
|
|
@@ -437,6 +445,18 @@ export class ServiceFactory {
|
|
|
437
445
|
mode: "link",
|
|
438
446
|
};
|
|
439
447
|
}
|
|
448
|
+
async tryResolveInstalledResource(projectDir, type, name) {
|
|
449
|
+
try {
|
|
450
|
+
return await this.resolveInstalledResource(projectDir, type, name);
|
|
451
|
+
}
|
|
452
|
+
catch (error) {
|
|
453
|
+
if (error instanceof HimanError &&
|
|
454
|
+
error.code === errorCodes.INSTALL_NOT_FOUND) {
|
|
455
|
+
return undefined;
|
|
456
|
+
}
|
|
457
|
+
throw error;
|
|
458
|
+
}
|
|
459
|
+
}
|
|
440
460
|
async resolveEffectiveAgents(projectDir, explicitAgents, fallbackAgents) {
|
|
441
461
|
if (explicitAgents?.length) {
|
|
442
462
|
return normalizeAgents(explicitAgents);
|
|
@@ -460,15 +480,51 @@ export class ServiceFactory {
|
|
|
460
480
|
}
|
|
461
481
|
return undefined;
|
|
462
482
|
}
|
|
463
|
-
async readResourceMetaFromDir(resourceDir) {
|
|
483
|
+
async readResourceMetaFromDir(resourceDir, type) {
|
|
464
484
|
const yamlPath = path.join(resourceDir, "himan.yaml");
|
|
465
|
-
if (
|
|
485
|
+
if (await this.exists(yamlPath)) {
|
|
486
|
+
const raw = await fs.readFile(yamlPath, "utf8");
|
|
487
|
+
const parsed = YAML.parse(raw) ??
|
|
488
|
+
null;
|
|
489
|
+
if (!parsed)
|
|
490
|
+
return null;
|
|
491
|
+
return { agents: parsed.agents ?? parsed.targets };
|
|
492
|
+
}
|
|
493
|
+
if (type !== "skill")
|
|
466
494
|
return null;
|
|
467
|
-
const
|
|
468
|
-
|
|
469
|
-
if (!parsed)
|
|
495
|
+
const entryPath = path.join(resourceDir, this.getDefaultEntry(type));
|
|
496
|
+
if (!(await this.exists(entryPath)))
|
|
470
497
|
return null;
|
|
471
|
-
|
|
498
|
+
const metadata = await this.readFrontMatter(entryPath);
|
|
499
|
+
return {
|
|
500
|
+
agents: this.readStringArrayMetadata(metadata, "agents") ??
|
|
501
|
+
this.readStringArrayMetadata(metadata, "targets"),
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
async readFrontMatter(filePath) {
|
|
505
|
+
const raw = await fs.readFile(filePath, "utf8");
|
|
506
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(raw.trimStart());
|
|
507
|
+
if (!match)
|
|
508
|
+
return null;
|
|
509
|
+
try {
|
|
510
|
+
const parsed = YAML.parse(match[1]);
|
|
511
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)
|
|
512
|
+
? parsed
|
|
513
|
+
: null;
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
return null;
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
readStringArrayMetadata(metadata, key) {
|
|
520
|
+
const value = metadata?.[key];
|
|
521
|
+
if (!Array.isArray(value))
|
|
522
|
+
return undefined;
|
|
523
|
+
const items = value
|
|
524
|
+
.filter((item) => typeof item === "string")
|
|
525
|
+
.map((item) => item.trim())
|
|
526
|
+
.filter(Boolean);
|
|
527
|
+
return items.length > 0 ? items : undefined;
|
|
472
528
|
}
|
|
473
529
|
async exists(targetPath) {
|
|
474
530
|
try {
|
|
@@ -508,6 +564,9 @@ export class ServiceFactory {
|
|
|
508
564
|
return "commands";
|
|
509
565
|
return "skills";
|
|
510
566
|
}
|
|
567
|
+
getDefaultEntry(type) {
|
|
568
|
+
return type === "skill" ? "SKILL.md" : "content.md";
|
|
569
|
+
}
|
|
511
570
|
validateCreateInput(type, name, options) {
|
|
512
571
|
if (!["rule", "command", "skill"].includes(type)) {
|
|
513
572
|
throw new HimanError(errorCodes.UNSUPPORTED_RESOURCE_TYPE, `Unsupported resource type for create: ${type}`);
|
package/docs/development.md
CHANGED
|
@@ -41,9 +41,9 @@ pnpm test
|
|
|
41
41
|
本地可执行 `pnpm run verify`(类型检查、单测、`build`),确认通过后再提 PR。PR 会自动运行同一组核心校验。
|
|
42
42
|
|
|
43
43
|
2. **更新 `package.json` 中的 `version` 与 `CHANGELOG.md`**
|
|
44
|
-
npm 不允许重复发布同一版本号。合并进 `master` 前,在 PR
|
|
44
|
+
npm 不允许重复发布同一版本号。合并进 `master` 前,在 PR 里把用户可见变更先记录到 [CHANGELOG.md](../CHANGELOG.md) 的 `[Unreleased]`,再把版本改成 registry 上尚未存在的号。
|
|
45
45
|
- 手动改 `version` 字段,或
|
|
46
|
-
-
|
|
46
|
+
- 在分支上执行其一(改版本号并把 `[Unreleased]` 归档到新版本,**不会**发包):`pnpm run version:patch` / `version:minor` / `version:major`(使用 `npm version … --no-git-tag-version`,随后执行 `scripts/release-changelog.mjs`;需自行 `git add` / `commit` 版本和 changelog 变更)。
|
|
47
47
|
Git 标签约定:与 `version` 对应、带前缀 **`v`**(如 `1.2.0` → 标签 `v1.2.0`)。
|
|
48
48
|
|
|
49
49
|
3. **合并到 `master`**
|
|
@@ -66,7 +66,8 @@ pnpm test
|
|
|
66
66
|
| `pnpm run release:dry` | 检查 + `npm publish --dry-run`(演练,不上传) |
|
|
67
67
|
| `pnpm run release:test` | 检查 + 将版本打成 `*-test.*` 预发布号并发布到 **`@test` 标签** |
|
|
68
68
|
| `pnpm run release` | 检查 + 发布 **latest**(维护者本地发包时用;**请写 `pnpm run release`**,勿用裸命令 `pnpm publish`,二者不是同一套流程) |
|
|
69
|
-
| `pnpm run
|
|
69
|
+
| `pnpm run changelog:release` | 把 `CHANGELOG.md` 的 `[Unreleased]` 归档到当前 `package.json` 版本 |
|
|
70
|
+
| `pnpm run version:patch` / `version:minor` / `version:major` | 提升 `package.json` 版本号,并调用 `changelog:release`;不发包 |
|
|
70
71
|
|
|
71
72
|
发测试标签后,安装示例:`npm i @hi-man/himan@test`。
|
|
72
73
|
|
package/docs/error-codes.md
CHANGED
|
@@ -110,8 +110,8 @@
|
|
|
110
110
|
### `E_INVALID_RESOURCE_METADATA`
|
|
111
111
|
|
|
112
112
|
- **含义**:资源元数据不合法,无法发布或读取为有效资源。
|
|
113
|
-
- **常见触发**:`
|
|
114
|
-
-
|
|
113
|
+
- **常见触发**:`himan.yaml` 存在但 `name/type/entry` 不匹配,`entry` 指向的入口文件不存在,或缺少 `himan.yaml` 且默认入口文件也不存在。
|
|
114
|
+
- **建议处理**:如果使用 `himan.yaml`,确认 `name`、`type`、`entry` 与命令参数和文件结构一致;如果暂不使用 `himan.yaml`,确认默认入口文件存在:`rule` / `command` 为 `content.md`,`skill` 为 `SKILL.md`。
|
|
115
115
|
|
|
116
116
|
### `E_PUBLISH_NO_CHANGES`
|
|
117
117
|
|
package/docs/mvp/README.md
CHANGED
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
### 2.2 `list`
|
|
31
31
|
|
|
32
32
|
- `himan list [type]`,`--json` 可选
|
|
33
|
-
-
|
|
33
|
+
- 扫描源仓库中各类型目录;优先读取 `himan.yaml`,缺失时按默认入口文件推断资源,返回名称、描述、目标 agent、入口文件等。
|
|
34
34
|
|
|
35
35
|
### 2.3 `history`
|
|
36
36
|
|
|
@@ -64,7 +64,7 @@
|
|
|
64
64
|
- 发布内容优先取项目 `.himan/dev/<type>/<name>`,否则取源仓库内对应资源目录。
|
|
65
65
|
- 新版本:基于已有 tag 最新 semver 递增;无任何历史时从 `0.0.0` 起算。
|
|
66
66
|
- 写回源仓库、提交、打 tag、推送,并将该版本同步到本地 store。
|
|
67
|
-
-
|
|
67
|
+
- 发布成功后,用新版本 store 以 copy 模式重新安装到项目目标、更新 lock,并删除对应 `.himan/dev/<type>/<name>`。
|
|
68
68
|
|
|
69
69
|
### 2.7 `create`
|
|
70
70
|
|
|
@@ -106,7 +106,7 @@
|
|
|
106
106
|
- `.himan/dev/<type>/<name>`:资源开发态可编辑副本
|
|
107
107
|
|
|
108
108
|
**源仓库内资源布局:**
|
|
109
|
-
- `rules/<name>/`、`commands/<name>/`、`skills/<name
|
|
109
|
+
- `rules/<name>/`、`commands/<name>/`、`skills/<name>/`,可含 `himan.yaml`,并包含约定入口文件(如 `content.md`、`SKILL.md`)。
|
|
110
110
|
|
|
111
111
|
### 3.3 技术依赖(概要)
|
|
112
112
|
|
|
@@ -39,24 +39,90 @@ himan create <type> <name> [options]
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
-
## 3.
|
|
42
|
+
## 3. Source 仓库结构
|
|
43
43
|
|
|
44
|
-
|
|
44
|
+
Git source 仓库推荐先维护仓库级 `README.md` 和 `CHANGELOG.md`,用于说明整个资源集合的使用方式与变更历史。它们位于仓库根目录,不属于任何单个资源,也不会通过 `himan install` 安装到 agent 目录。
|
|
45
|
+
|
|
46
|
+
```text
|
|
47
|
+
repo/
|
|
48
|
+
README.md
|
|
49
|
+
CHANGELOG.md
|
|
50
|
+
rules/
|
|
51
|
+
commands/
|
|
52
|
+
skills/
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
- `README.md`:说明 source 的用途、资源索引、安装示例、默认 agent 策略和维护约定
|
|
56
|
+
- `CHANGELOG.md`:记录 source 级别的新增资源、资源变更、废弃、移除和重要发布说明
|
|
57
|
+
- `rules/`、`commands/`、`skills/`:资源类型根目录,由 himan 扫描
|
|
58
|
+
|
|
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
|
+
|
|
61
|
+
`create` 和 `publish` 会自动维护根目录文档:
|
|
62
|
+
|
|
63
|
+
- `README.md`:只维护 `<!-- himan:resources:start -->` / `<!-- himan:resources:end -->` 标记内的资源索引;如果旧 README 没有标记,则在文件末尾追加受控资源索引区
|
|
64
|
+
- `CHANGELOG.md`:向 `[Unreleased]` 写入资源变更;新增资源写入 `Added`,发布版本写入 `Changed`
|
|
65
|
+
|
|
66
|
+
推荐的 `README.md` 基本结构:
|
|
67
|
+
|
|
68
|
+
```md
|
|
69
|
+
# Team Himan Source
|
|
70
|
+
|
|
71
|
+
## Resources
|
|
72
|
+
|
|
73
|
+
<!-- himan:resources:start -->
|
|
74
|
+
|
|
75
|
+
- rule/code-review: Code review behavior for backend changes.
|
|
76
|
+
- command/create-mr: MR creation workflow.
|
|
77
|
+
- skill/api-debugging: API debugging workflow.
|
|
78
|
+
|
|
79
|
+
<!-- himan:resources:end -->
|
|
80
|
+
|
|
81
|
+
## Usage
|
|
82
|
+
|
|
83
|
+
\`\`\`bash
|
|
84
|
+
himan source add team <git_url>
|
|
85
|
+
himan source use team
|
|
86
|
+
himan install rule code-review
|
|
87
|
+
\`\`\`
|
|
88
|
+
|
|
89
|
+
## Maintenance
|
|
90
|
+
|
|
91
|
+
- Publish resource versions with himan publish.
|
|
92
|
+
- Record source-level changes in CHANGELOG.md.
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
推荐的 `CHANGELOG.md` 基本结构:
|
|
96
|
+
|
|
97
|
+
```md
|
|
98
|
+
# Changelog
|
|
99
|
+
|
|
100
|
+
## [Unreleased]
|
|
101
|
+
|
|
102
|
+
### Added
|
|
103
|
+
|
|
104
|
+
- Added rule/code-review.
|
|
105
|
+
|
|
106
|
+
### Changed
|
|
107
|
+
|
|
108
|
+
- Updated skill/api-debugging usage guidance.
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## 4. 资源目录与元数据
|
|
112
|
+
|
|
113
|
+
`create` 生成资源目录,结构示例:
|
|
45
114
|
|
|
46
115
|
```text
|
|
47
116
|
repo/
|
|
48
117
|
rules/<name>/
|
|
49
|
-
himan.yaml
|
|
50
118
|
content.md
|
|
51
119
|
commands/<name>/
|
|
52
|
-
himan.yaml
|
|
53
120
|
content.md
|
|
54
121
|
skills/<name>/
|
|
55
|
-
himan.yaml
|
|
56
122
|
SKILL.md
|
|
57
123
|
```
|
|
58
124
|
|
|
59
|
-
`himan.yaml` 最小字段示例:
|
|
125
|
+
`himan.yaml` 是推荐但非强制的资源元数据文件。存在时,发布会校验 `name`、`type`、`entry` 与入口文件;缺失时,发布按默认入口推断最小元数据:`rule` / `command` 使用 `content.md`,`skill` 使用 `SKILL.md`。`himan.yaml` 最小字段示例:
|
|
60
126
|
|
|
61
127
|
```yaml
|
|
62
128
|
name: code-review
|
|
@@ -73,7 +139,7 @@ agents:
|
|
|
73
139
|
|
|
74
140
|
---
|
|
75
141
|
|
|
76
|
-
##
|
|
142
|
+
## 5. 流程概要
|
|
77
143
|
|
|
78
144
|
1. 读取本地配置,确认已初始化源
|
|
79
145
|
2. 校验类型与资源名格式
|
|
@@ -86,14 +152,14 @@ agents:
|
|
|
86
152
|
|
|
87
153
|
---
|
|
88
154
|
|
|
89
|
-
##
|
|
155
|
+
## 6. 模板
|
|
90
156
|
|
|
91
157
|
- 当前仅 **basic**:最简结构与提示性说明
|
|
92
158
|
- 后续可扩展更多模板名;自定义模板目录可作为后续增强
|
|
93
159
|
|
|
94
160
|
---
|
|
95
161
|
|
|
96
|
-
##
|
|
162
|
+
## 7. 错误场景(产品语义)
|
|
97
163
|
|
|
98
164
|
- 资源目录已存在
|
|
99
165
|
- 模板不存在(含请求了尚未支持的模板名)
|
|
@@ -103,13 +169,13 @@ agents:
|
|
|
103
169
|
|
|
104
170
|
---
|
|
105
171
|
|
|
106
|
-
##
|
|
172
|
+
## 8. 测试关注点
|
|
107
173
|
|
|
108
174
|
- 名称与类型校验、默认 entry/agents、重复创建与 `--force`、`--dry-run` 不写盘、创建后 `list` 可见
|
|
109
175
|
|
|
110
176
|
---
|
|
111
177
|
|
|
112
|
-
##
|
|
178
|
+
## 9. 与资源工作流衔接
|
|
113
179
|
|
|
114
180
|
```text
|
|
115
181
|
create → edit → publish
|
package/docs/mvp/impl.md
CHANGED
|
@@ -30,8 +30,8 @@
|
|
|
30
30
|
|
|
31
31
|
### 2.2 `list [type]`
|
|
32
32
|
|
|
33
|
-
-
|
|
34
|
-
-
|
|
33
|
+
- 在缓存仓库内按类型扫描子目录,优先读取 `himan.yaml`,缺失时按默认入口文件推断资源
|
|
34
|
+
- 校验已有元数据的类型与必填字段(如 name、entry),不符的目录跳过
|
|
35
35
|
- 支持人类可读与 `--json` 输出
|
|
36
36
|
|
|
37
37
|
### 2.3 `history <type> <name>`
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
- 下一版本:基于历史最新 tag;无历史则从 `0.0.0` 按 patch/minor/major 递增
|
|
59
59
|
- 将内容同步回缓存仓库中的规范路径,更新元数据中的版本字段,提交、打 tag、推送
|
|
60
60
|
- 将新 tag 对应内容拉取到 store 新版本目录
|
|
61
|
-
-
|
|
61
|
+
- 用新版本 store 以 copy 模式重新安装项目内对应类型目标,更新 lock,并删除对应 `.himan/dev/<type>/<name>` 开发目录
|
|
62
62
|
|
|
63
63
|
### 2.7 `create <type> <name>`
|
|
64
64
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hi-man/himan",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.1",
|
|
4
4
|
"description": "Prompt and agent asset management CLI",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -59,9 +59,10 @@
|
|
|
59
59
|
"release": "pnpm run verify && npm publish",
|
|
60
60
|
"release:dry": "pnpm run verify && npm publish --dry-run",
|
|
61
61
|
"release:test": "pnpm run verify && npm version prerelease --preid test --no-git-tag-version && npm publish --tag test",
|
|
62
|
-
"
|
|
63
|
-
"version:
|
|
64
|
-
"version:
|
|
62
|
+
"changelog:release": "node scripts/release-changelog.mjs",
|
|
63
|
+
"version:patch": "npm version patch --no-git-tag-version && pnpm run changelog:release",
|
|
64
|
+
"version:minor": "npm version minor --no-git-tag-version && pnpm run changelog:release",
|
|
65
|
+
"version:major": "npm version major --no-git-tag-version && pnpm run changelog:release"
|
|
65
66
|
},
|
|
66
67
|
"dependencies": {
|
|
67
68
|
"commander": "^14.0.3",
|