@hi-man/himan 0.2.0 → 0.3.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/.nvmrc +1 -0
- package/CHANGELOG.md +76 -0
- package/README.md +69 -64
- package/dist/adapters/git/repo-manager.js +41 -6
- package/dist/adapters/source/git-source-adapter.js +343 -16
- package/dist/adapters/source/registry-source-adapter.js +3 -0
- package/dist/cli/builders.js +1 -1
- package/dist/cli/source-commands.js +22 -0
- package/dist/domain/source-docs.js +1 -0
- package/dist/services/index.js +76 -37
- package/dist/state/index-cache-store.js +4 -3
- package/dist/utils/errors.js +2 -0
- package/docs/development.md +83 -0
- package/docs/error-codes.md +132 -0
- package/docs/mvp/README.md +143 -0
- package/docs/mvp/create-resource.md +198 -0
- package/docs/mvp/impl.md +111 -0
- package/package.json +26 -5
|
@@ -3,9 +3,13 @@ import { ResourceScanner } from "../resource/resource-scanner.js";
|
|
|
3
3
|
import semver from "semver";
|
|
4
4
|
import { HimanError, errorCodes } from "../../utils/errors.js";
|
|
5
5
|
import { promises as fs } from "node:fs";
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
6
7
|
import path from "node:path";
|
|
7
8
|
import YAML from "yaml";
|
|
8
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 -->";
|
|
9
13
|
export class GitSourceAdapter {
|
|
10
14
|
repoManager = new RepoManager();
|
|
11
15
|
scanner = new ResourceScanner();
|
|
@@ -23,13 +27,13 @@ export class GitSourceAdapter {
|
|
|
23
27
|
const repoId = this.sourceConfig?.repoId ?? "default";
|
|
24
28
|
const typeDir = this.getTypeDir(type);
|
|
25
29
|
const baseDir = path.join(repoDir, typeDir);
|
|
26
|
-
const
|
|
30
|
+
const metadataHash = await this.getResourceMetadataHash(baseDir);
|
|
27
31
|
const cached = await this.indexStore.get(repoId, type);
|
|
28
|
-
if (cached && cached.
|
|
32
|
+
if (cached && cached.metadataHash === metadataHash) {
|
|
29
33
|
return cached.resources;
|
|
30
34
|
}
|
|
31
35
|
const scanned = await this.scanner.scanByType(repoDir, type);
|
|
32
|
-
await this.indexStore.upsert(repoId, type,
|
|
36
|
+
await this.indexStore.upsert(repoId, type, metadataHash, scanned);
|
|
33
37
|
return scanned;
|
|
34
38
|
}
|
|
35
39
|
async history(type, name) {
|
|
@@ -47,6 +51,7 @@ export class GitSourceAdapter {
|
|
|
47
51
|
async publish(type, name, version, sourceDir) {
|
|
48
52
|
const repoDir = this.getRepoDir();
|
|
49
53
|
const targetDir = path.join(repoDir, `${type}s`, name);
|
|
54
|
+
const metadata = await this.validatePublishResource(type, name, sourceDir);
|
|
50
55
|
const sameDir = await this.isSameDirectory(sourceDir, targetDir);
|
|
51
56
|
if (!sameDir) {
|
|
52
57
|
await fs.rm(targetDir, { recursive: true, force: true });
|
|
@@ -54,14 +59,17 @@ export class GitSourceAdapter {
|
|
|
54
59
|
await fs.cp(sourceDir, targetDir, { recursive: true });
|
|
55
60
|
}
|
|
56
61
|
const yamlPath = path.join(targetDir, "himan.yaml");
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
62
|
+
metadata.version = version;
|
|
63
|
+
await fs.writeFile(yamlPath, YAML.stringify(metadata), "utf8");
|
|
64
|
+
const docsPaths = await this.maintainSourceDocs(repoDir, {
|
|
65
|
+
section: "Changed",
|
|
66
|
+
line: `- Published \`${type}/${name}@${version}\`.`,
|
|
67
|
+
});
|
|
63
68
|
const tag = `${type}/${name}@${version}`;
|
|
64
|
-
await this.repoManager.commitTagAndPush(repoDir, `publish ${type}/${name}@${version}`, tag
|
|
69
|
+
await this.repoManager.commitTagAndPush(repoDir, `publish ${type}/${name}@${version}`, tag, undefined, [
|
|
70
|
+
path.relative(repoDir, targetDir),
|
|
71
|
+
...docsPaths.map((docPath) => path.relative(repoDir, docPath)),
|
|
72
|
+
]);
|
|
65
73
|
return { version, tag };
|
|
66
74
|
}
|
|
67
75
|
async create(type, name, options) {
|
|
@@ -69,7 +77,8 @@ export class GitSourceAdapter {
|
|
|
69
77
|
const resourceDir = path.join(repoDir, this.getTypeDir(type), name);
|
|
70
78
|
const entry = options.entry ?? this.getDefaultEntry(type);
|
|
71
79
|
const agents = options.agents?.length ? options.agents : ["cursor"];
|
|
72
|
-
|
|
80
|
+
const resourceExists = await this.exists(resourceDir);
|
|
81
|
+
if (resourceExists && !options.force) {
|
|
73
82
|
throw new HimanError(errorCodes.RESOURCE_EXISTS, `Resource already exists: ${type}/${name}`);
|
|
74
83
|
}
|
|
75
84
|
const files = [path.join(resourceDir, "himan.yaml"), path.join(resourceDir, entry)];
|
|
@@ -85,6 +94,12 @@ export class GitSourceAdapter {
|
|
|
85
94
|
agents,
|
|
86
95
|
}), "utf8");
|
|
87
96
|
await fs.writeFile(path.join(resourceDir, entry), this.getDefaultContent(type, name), "utf8");
|
|
97
|
+
await this.maintainSourceDocs(repoDir, {
|
|
98
|
+
section: resourceExists ? "Changed" : "Added",
|
|
99
|
+
line: resourceExists
|
|
100
|
+
? `- Updated \`${type}/${name}\`.`
|
|
101
|
+
: `- Added \`${type}/${name}\`.`,
|
|
102
|
+
});
|
|
88
103
|
}
|
|
89
104
|
return {
|
|
90
105
|
type,
|
|
@@ -94,6 +109,34 @@ export class GitSourceAdapter {
|
|
|
94
109
|
dryRun: Boolean(options.dryRun),
|
|
95
110
|
};
|
|
96
111
|
}
|
|
112
|
+
async initDocs(options = {}) {
|
|
113
|
+
const repoDir = this.getRepoDir();
|
|
114
|
+
const files = [
|
|
115
|
+
{
|
|
116
|
+
path: path.join(repoDir, "README.md"),
|
|
117
|
+
content: await this.buildReadmeContent(repoDir),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
path: path.join(repoDir, "CHANGELOG.md"),
|
|
121
|
+
content: this.buildChangelogContent(),
|
|
122
|
+
},
|
|
123
|
+
];
|
|
124
|
+
const results = [];
|
|
125
|
+
for (const file of files) {
|
|
126
|
+
const exists = await this.exists(file.path);
|
|
127
|
+
const action = exists ? (options.force ? "updated" : "skipped") : "created";
|
|
128
|
+
const reason = action === "skipped" ? "file already exists" : undefined;
|
|
129
|
+
results.push({ path: file.path, action, reason });
|
|
130
|
+
if (!options.dryRun && action !== "skipped") {
|
|
131
|
+
await fs.writeFile(file.path, file.content, "utf8");
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
sourceDir: repoDir,
|
|
136
|
+
files: results,
|
|
137
|
+
dryRun: Boolean(options.dryRun),
|
|
138
|
+
};
|
|
139
|
+
}
|
|
97
140
|
getRepoDir() {
|
|
98
141
|
if (!this.sourceConfig?.repoDir) {
|
|
99
142
|
throw new HimanError(errorCodes.CONFIG_NOT_FOUND, "Git source is not initialized.");
|
|
@@ -118,14 +161,66 @@ export class GitSourceAdapter {
|
|
|
118
161
|
return false;
|
|
119
162
|
}
|
|
120
163
|
}
|
|
121
|
-
async
|
|
164
|
+
async validatePublishResource(type, name, resourceDir) {
|
|
165
|
+
const yamlPath = path.join(resourceDir, "himan.yaml");
|
|
166
|
+
if (!(await this.exists(yamlPath))) {
|
|
167
|
+
throw this.invalidResourceMetadata(type, name, "Missing himan.yaml for publish.", { yamlPath });
|
|
168
|
+
}
|
|
169
|
+
const raw = await fs.readFile(yamlPath, "utf8");
|
|
170
|
+
let parsed;
|
|
122
171
|
try {
|
|
123
|
-
|
|
124
|
-
return stat.mtimeMs;
|
|
172
|
+
parsed = YAML.parse(raw);
|
|
125
173
|
}
|
|
126
|
-
catch {
|
|
127
|
-
|
|
174
|
+
catch (error) {
|
|
175
|
+
throw this.invalidResourceMetadata(type, name, "himan.yaml is not valid YAML.", { yamlPath, reason: error instanceof Error ? error.message : String(error) });
|
|
176
|
+
}
|
|
177
|
+
if (!this.isRecord(parsed)) {
|
|
178
|
+
throw this.invalidResourceMetadata(type, name, "himan.yaml must be an object.", { yamlPath });
|
|
179
|
+
}
|
|
180
|
+
if (parsed.name !== name) {
|
|
181
|
+
throw this.invalidResourceMetadata(type, name, `himan.yaml name must be "${name}".`, { yamlPath, actual: parsed.name });
|
|
182
|
+
}
|
|
183
|
+
if (parsed.type !== type) {
|
|
184
|
+
throw this.invalidResourceMetadata(type, name, `himan.yaml type must be "${type}".`, { yamlPath, actual: parsed.type });
|
|
185
|
+
}
|
|
186
|
+
if (typeof parsed.entry !== "string" || parsed.entry.trim().length === 0) {
|
|
187
|
+
throw this.invalidResourceMetadata(type, name, "himan.yaml entry is required.", { yamlPath });
|
|
188
|
+
}
|
|
189
|
+
const entry = parsed.entry.trim();
|
|
190
|
+
const entryPath = path.resolve(resourceDir, entry);
|
|
191
|
+
const resourceRoot = path.resolve(resourceDir);
|
|
192
|
+
const relativeEntryPath = path.relative(resourceRoot, entryPath);
|
|
193
|
+
if (path.isAbsolute(entry) ||
|
|
194
|
+
relativeEntryPath === "" ||
|
|
195
|
+
relativeEntryPath.startsWith("..") ||
|
|
196
|
+
path.isAbsolute(relativeEntryPath)) {
|
|
197
|
+
throw this.invalidResourceMetadata(type, name, "himan.yaml entry must point to a file inside the resource directory.", { yamlPath, entry });
|
|
128
198
|
}
|
|
199
|
+
let entryStat;
|
|
200
|
+
try {
|
|
201
|
+
entryStat = await fs.stat(entryPath);
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (!this.isNotFoundError(error)) {
|
|
205
|
+
throw error;
|
|
206
|
+
}
|
|
207
|
+
throw this.invalidResourceMetadata(type, name, `Resource entry file not found: ${entry}`, { yamlPath, entry, entryPath });
|
|
208
|
+
}
|
|
209
|
+
if (!entryStat.isFile()) {
|
|
210
|
+
throw this.invalidResourceMetadata(type, name, `Resource entry is not a file: ${entry}`, { yamlPath, entry, entryPath });
|
|
211
|
+
}
|
|
212
|
+
return {
|
|
213
|
+
...parsed,
|
|
214
|
+
name,
|
|
215
|
+
type,
|
|
216
|
+
entry,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
invalidResourceMetadata(type, name, message, details) {
|
|
220
|
+
return new HimanError(errorCodes.INVALID_RESOURCE_METADATA, `Invalid metadata for ${type}/${name}: ${message}`, details);
|
|
221
|
+
}
|
|
222
|
+
isRecord(value) {
|
|
223
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
129
224
|
}
|
|
130
225
|
getTypeDir(type) {
|
|
131
226
|
if (type === "rule")
|
|
@@ -134,6 +229,42 @@ export class GitSourceAdapter {
|
|
|
134
229
|
return "commands";
|
|
135
230
|
return "skills";
|
|
136
231
|
}
|
|
232
|
+
async getResourceMetadataHash(baseDir) {
|
|
233
|
+
const hash = createHash("sha256");
|
|
234
|
+
hash.update("himan-resource-index-v1");
|
|
235
|
+
if (!(await this.exists(baseDir))) {
|
|
236
|
+
hash.update("\0missing");
|
|
237
|
+
return hash.digest("hex");
|
|
238
|
+
}
|
|
239
|
+
const entries = await fs.readdir(baseDir, { withFileTypes: true });
|
|
240
|
+
const resourceDirNames = entries
|
|
241
|
+
.filter((entry) => entry.isDirectory())
|
|
242
|
+
.map((entry) => entry.name)
|
|
243
|
+
.sort();
|
|
244
|
+
for (const resourceDirName of resourceDirNames) {
|
|
245
|
+
hash.update("\0dir:");
|
|
246
|
+
hash.update(resourceDirName);
|
|
247
|
+
const yamlPath = path.join(baseDir, resourceDirName, "himan.yaml");
|
|
248
|
+
try {
|
|
249
|
+
const raw = await fs.readFile(yamlPath);
|
|
250
|
+
hash.update("\0yaml:");
|
|
251
|
+
hash.update(raw);
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
if (!this.isNotFoundError(error)) {
|
|
255
|
+
throw error;
|
|
256
|
+
}
|
|
257
|
+
hash.update("\0yaml-missing");
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
return hash.digest("hex");
|
|
261
|
+
}
|
|
262
|
+
isNotFoundError(error) {
|
|
263
|
+
return (typeof error === "object" &&
|
|
264
|
+
error !== null &&
|
|
265
|
+
"code" in error &&
|
|
266
|
+
error.code === "ENOENT");
|
|
267
|
+
}
|
|
137
268
|
getDefaultEntry(type) {
|
|
138
269
|
return type === "skill" ? "SKILL.md" : "content.md";
|
|
139
270
|
}
|
|
@@ -146,4 +277,200 @@ export class GitSourceAdapter {
|
|
|
146
277
|
}
|
|
147
278
|
return `# ${name}\n\nDescribe skill workflow here.\n`;
|
|
148
279
|
}
|
|
280
|
+
async buildReadmeContent(repoDir) {
|
|
281
|
+
const resourceLines = await this.buildResourceIndex(repoDir);
|
|
282
|
+
const repo = this.sourceConfig?.repo ?? "<git_url>";
|
|
283
|
+
return [
|
|
284
|
+
`# ${this.getSourceTitle()}`,
|
|
285
|
+
"",
|
|
286
|
+
"Himan source repository for reusable agent resources.",
|
|
287
|
+
"",
|
|
288
|
+
"## Resources",
|
|
289
|
+
"",
|
|
290
|
+
README_RESOURCES_START,
|
|
291
|
+
...resourceLines,
|
|
292
|
+
README_RESOURCES_END,
|
|
293
|
+
"",
|
|
294
|
+
"## Usage",
|
|
295
|
+
"",
|
|
296
|
+
"```bash",
|
|
297
|
+
`himan source add team ${repo}`,
|
|
298
|
+
"himan source use team",
|
|
299
|
+
"himan list rule",
|
|
300
|
+
"himan install rule <name>",
|
|
301
|
+
"```",
|
|
302
|
+
"",
|
|
303
|
+
"## Maintenance",
|
|
304
|
+
"",
|
|
305
|
+
"- Add resources with `himan create <type> <name>`.",
|
|
306
|
+
"- Publish resource versions with `himan publish <type> <name>`.",
|
|
307
|
+
"- Record source-level changes in `CHANGELOG.md`.",
|
|
308
|
+
"- Resource versions are tracked by Git tags such as `rule/code-review@1.0.0`.",
|
|
309
|
+
"",
|
|
310
|
+
].join("\n");
|
|
311
|
+
}
|
|
312
|
+
buildChangelogContent() {
|
|
313
|
+
return [
|
|
314
|
+
"# Changelog",
|
|
315
|
+
"",
|
|
316
|
+
"All notable source-level resource changes are documented in this file.",
|
|
317
|
+
"",
|
|
318
|
+
"## [Unreleased]",
|
|
319
|
+
"",
|
|
320
|
+
"### Added",
|
|
321
|
+
"",
|
|
322
|
+
"- Initial source README/CHANGELOG scaffold.",
|
|
323
|
+
"",
|
|
324
|
+
].join("\n");
|
|
325
|
+
}
|
|
326
|
+
async buildResourceIndex(repoDir) {
|
|
327
|
+
const sections = [];
|
|
328
|
+
for (const type of RESOURCE_TYPES) {
|
|
329
|
+
const resources = (await this.scanner.scanByType(repoDir, type)).sort((a, b) => a.name.localeCompare(b.name));
|
|
330
|
+
sections.push(`### ${this.getTypeLabel(type)}`, "");
|
|
331
|
+
if (resources.length === 0) {
|
|
332
|
+
sections.push(`- No ${type} resources yet.`, "");
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
for (const resource of resources) {
|
|
336
|
+
const version = await this.readResourceVersion(repoDir, resource.type, resource.name);
|
|
337
|
+
const ref = version
|
|
338
|
+
? `${resource.type}/${resource.name}@${version}`
|
|
339
|
+
: `${resource.type}/${resource.name}`;
|
|
340
|
+
sections.push(`- \`${ref}\`${resource.description ? `: ${resource.description}` : ""}`);
|
|
341
|
+
}
|
|
342
|
+
sections.push("");
|
|
343
|
+
}
|
|
344
|
+
while (sections.at(-1) === "") {
|
|
345
|
+
sections.pop();
|
|
346
|
+
}
|
|
347
|
+
return sections;
|
|
348
|
+
}
|
|
349
|
+
async maintainSourceDocs(repoDir, changelogEntry) {
|
|
350
|
+
const readmePath = await this.updateReadmeResourceIndex(repoDir);
|
|
351
|
+
const changelogPath = await this.updateChangelog(repoDir, changelogEntry);
|
|
352
|
+
return [readmePath, changelogPath];
|
|
353
|
+
}
|
|
354
|
+
async updateReadmeResourceIndex(repoDir) {
|
|
355
|
+
const readmePath = path.join(repoDir, "README.md");
|
|
356
|
+
if (!(await this.exists(readmePath))) {
|
|
357
|
+
await fs.writeFile(readmePath, await this.buildReadmeContent(repoDir), "utf8");
|
|
358
|
+
return readmePath;
|
|
359
|
+
}
|
|
360
|
+
const current = await fs.readFile(readmePath, "utf8");
|
|
361
|
+
const resourceSection = [
|
|
362
|
+
README_RESOURCES_START,
|
|
363
|
+
...(await this.buildResourceIndex(repoDir)),
|
|
364
|
+
README_RESOURCES_END,
|
|
365
|
+
].join("\n");
|
|
366
|
+
const updated = this.replaceOrAppendReadmeResourceSection(current, resourceSection);
|
|
367
|
+
if (updated !== current) {
|
|
368
|
+
await fs.writeFile(readmePath, updated, "utf8");
|
|
369
|
+
}
|
|
370
|
+
return readmePath;
|
|
371
|
+
}
|
|
372
|
+
replaceOrAppendReadmeResourceSection(content, resourceSection) {
|
|
373
|
+
const startIndex = content.indexOf(README_RESOURCES_START);
|
|
374
|
+
const endIndex = content.indexOf(README_RESOURCES_END);
|
|
375
|
+
if (startIndex >= 0 && endIndex > startIndex) {
|
|
376
|
+
const before = content.slice(0, startIndex).replace(/\s*$/, "\n");
|
|
377
|
+
const after = content
|
|
378
|
+
.slice(endIndex + README_RESOURCES_END.length)
|
|
379
|
+
.replace(/^\s*/, "\n\n");
|
|
380
|
+
return `${before}${resourceSection}${after}`.replace(/\s*$/, "\n");
|
|
381
|
+
}
|
|
382
|
+
const base = content.replace(/\s*$/, "");
|
|
383
|
+
return `${base}\n\n## Resources\n\n${resourceSection}\n`;
|
|
384
|
+
}
|
|
385
|
+
async updateChangelog(repoDir, entry) {
|
|
386
|
+
const changelogPath = path.join(repoDir, "CHANGELOG.md");
|
|
387
|
+
const current = (await this.exists(changelogPath))
|
|
388
|
+
? await fs.readFile(changelogPath, "utf8")
|
|
389
|
+
: this.buildChangelogBaseContent();
|
|
390
|
+
const updated = this.insertChangelogEntry(current, entry);
|
|
391
|
+
if (updated !== current) {
|
|
392
|
+
await fs.writeFile(changelogPath, updated, "utf8");
|
|
393
|
+
}
|
|
394
|
+
return changelogPath;
|
|
395
|
+
}
|
|
396
|
+
buildChangelogBaseContent() {
|
|
397
|
+
return [
|
|
398
|
+
"# Changelog",
|
|
399
|
+
"",
|
|
400
|
+
"All notable source-level resource changes are documented in this file.",
|
|
401
|
+
"",
|
|
402
|
+
"## [Unreleased]",
|
|
403
|
+
"",
|
|
404
|
+
].join("\n");
|
|
405
|
+
}
|
|
406
|
+
insertChangelogEntry(content, entry) {
|
|
407
|
+
const lines = content.replace(/\s*$/, "").split("\n");
|
|
408
|
+
let unreleasedIndex = lines.findIndex((line) => line.trim() === "## [Unreleased]");
|
|
409
|
+
if (unreleasedIndex === -1) {
|
|
410
|
+
const firstVersionIndex = lines.findIndex((line) => line.startsWith("## "));
|
|
411
|
+
const insertIndex = firstVersionIndex === -1 ? lines.length : firstVersionIndex;
|
|
412
|
+
lines.splice(insertIndex, 0, "## [Unreleased]", "");
|
|
413
|
+
unreleasedIndex = insertIndex;
|
|
414
|
+
}
|
|
415
|
+
const blockEnd = this.findNextHeadingIndex(lines, unreleasedIndex + 1, "## ");
|
|
416
|
+
const unreleasedLines = lines.slice(unreleasedIndex, blockEnd);
|
|
417
|
+
if (unreleasedLines.includes(entry.line)) {
|
|
418
|
+
return `${lines.join("\n")}\n`;
|
|
419
|
+
}
|
|
420
|
+
const sectionHeading = `### ${entry.section}`;
|
|
421
|
+
const sectionIndex = lines.findIndex((line, index) => index > unreleasedIndex && index < blockEnd && line.trim() === sectionHeading);
|
|
422
|
+
if (sectionIndex >= 0) {
|
|
423
|
+
const insertIndex = lines[sectionIndex + 1] === "" ? sectionIndex + 2 : sectionIndex + 1;
|
|
424
|
+
lines.splice(insertIndex, 0, entry.line);
|
|
425
|
+
return `${lines.join("\n")}\n`;
|
|
426
|
+
}
|
|
427
|
+
const insertIndex = this.findChangelogSectionInsertIndex(lines, unreleasedIndex, blockEnd, entry.section);
|
|
428
|
+
lines.splice(insertIndex, 0, `### ${entry.section}`, "", entry.line, "");
|
|
429
|
+
return `${lines.join("\n").replace(/\s*$/, "")}\n`;
|
|
430
|
+
}
|
|
431
|
+
findNextHeadingIndex(lines, startIndex, headingPrefix) {
|
|
432
|
+
const found = lines.findIndex((line, index) => index >= startIndex && line.startsWith(headingPrefix));
|
|
433
|
+
return found === -1 ? lines.length : found;
|
|
434
|
+
}
|
|
435
|
+
findChangelogSectionInsertIndex(lines, unreleasedIndex, blockEnd, section) {
|
|
436
|
+
const sectionOrder = ["Added", "Changed"];
|
|
437
|
+
const sectionRank = sectionOrder.indexOf(section);
|
|
438
|
+
for (let index = unreleasedIndex + 1; index < blockEnd; index += 1) {
|
|
439
|
+
const line = lines[index].trim();
|
|
440
|
+
if (!line.startsWith("### "))
|
|
441
|
+
continue;
|
|
442
|
+
const foundSection = line.slice(4);
|
|
443
|
+
const foundRank = sectionOrder.indexOf(foundSection);
|
|
444
|
+
if (foundRank > sectionRank) {
|
|
445
|
+
return index;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return blockEnd;
|
|
449
|
+
}
|
|
450
|
+
async readResourceVersion(repoDir, type, name) {
|
|
451
|
+
const yamlPath = path.join(repoDir, this.getTypeDir(type), name, "himan.yaml");
|
|
452
|
+
try {
|
|
453
|
+
const raw = await fs.readFile(yamlPath, "utf8");
|
|
454
|
+
const parsed = YAML.parse(raw);
|
|
455
|
+
return typeof parsed?.version === "string" ? parsed.version : undefined;
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
if (!this.isNotFoundError(error)) {
|
|
459
|
+
throw error;
|
|
460
|
+
}
|
|
461
|
+
return undefined;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
getSourceTitle() {
|
|
465
|
+
const repo = this.sourceConfig?.repo?.replace(/\/$/, "");
|
|
466
|
+
const repoName = repo?.split(/[/:]/).at(-1)?.replace(/\.git$/, "");
|
|
467
|
+
return repoName ? `${repoName} Himan Source` : "Himan Source";
|
|
468
|
+
}
|
|
469
|
+
getTypeLabel(type) {
|
|
470
|
+
if (type === "rule")
|
|
471
|
+
return "Rules";
|
|
472
|
+
if (type === "command")
|
|
473
|
+
return "Commands";
|
|
474
|
+
return "Skills";
|
|
475
|
+
}
|
|
149
476
|
}
|
|
@@ -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,26 @@ 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
|
+
});
|
|
79
|
+
});
|
|
58
80
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/services/index.js
CHANGED
|
@@ -2,7 +2,7 @@ import { GitSourceAdapter } from "../adapters/source/git-source-adapter.js";
|
|
|
2
2
|
import { RegistrySourceAdapter } from "../adapters/source/registry-source-adapter.js";
|
|
3
3
|
import { StateStore } from "../state/state-store.js";
|
|
4
4
|
import { ProjectConfigStore } from "../state/project-config-store.js";
|
|
5
|
-
import { ProjectLockStore } from "../state/project-lock-store.js";
|
|
5
|
+
import { ProjectLockStore, } from "../state/project-lock-store.js";
|
|
6
6
|
import { PathResolver } from "../utils/path-resolver.js";
|
|
7
7
|
import { toRepoId } from "../utils/repo-id.js";
|
|
8
8
|
import { HimanError, errorCodes } from "../utils/errors.js";
|
|
@@ -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") {
|
|
@@ -158,31 +162,8 @@ export class ServiceFactory {
|
|
|
158
162
|
return source.history(type, name);
|
|
159
163
|
}
|
|
160
164
|
async install(type, name, version, projectDir, agents, mode = "link") {
|
|
161
|
-
const source = await this.
|
|
162
|
-
|
|
163
|
-
const history = await source.history(type, name);
|
|
164
|
-
if (history.length === 0) {
|
|
165
|
-
throw new HimanError(errorCodes.RESOURCE_NOT_FOUND, `Resource not found: ${type}/${name}`);
|
|
166
|
-
}
|
|
167
|
-
const resolvedVersion = this.resolveVersion(history, version);
|
|
168
|
-
const storePath = this.getStorePath(type, name, resolvedVersion);
|
|
169
|
-
if (!(await this.exists(storePath))) {
|
|
170
|
-
await source.pull(type, name, resolvedVersion, storePath);
|
|
171
|
-
}
|
|
172
|
-
const resourceMeta = await this.readResourceMetaFromDir(storePath);
|
|
173
|
-
const effectiveTargets = await this.resolveEffectiveAgents(projectDir, agents, resourceMeta?.agents);
|
|
174
|
-
const linkPaths = getProjectResourcePaths(projectDir, type, name, effectiveTargets);
|
|
175
|
-
for (const linkPath of linkPaths) {
|
|
176
|
-
await this.materializeResource(storePath, linkPath, mode);
|
|
177
|
-
}
|
|
178
|
-
await this.lockStore.upsertResource(projectDir, sourceInfo, {
|
|
179
|
-
type,
|
|
180
|
-
name,
|
|
181
|
-
version: resolvedVersion,
|
|
182
|
-
agents: effectiveTargets,
|
|
183
|
-
mode,
|
|
184
|
-
});
|
|
185
|
-
return { type, name, version: resolvedVersion, linkPath: linkPaths[0], mode };
|
|
165
|
+
const { source, sourceInfo } = await this.loadSourceWithInfoFromConfig();
|
|
166
|
+
return this.installWithSource(source, sourceInfo, type, name, version, projectDir, agents, mode);
|
|
186
167
|
}
|
|
187
168
|
async dev(type, name, projectDir) {
|
|
188
169
|
const installInfo = await this.resolveInstalledResource(projectDir, type, name);
|
|
@@ -275,21 +256,69 @@ export class ServiceFactory {
|
|
|
275
256
|
throw new HimanError(errorCodes.LOCK_NOT_FOUND, `Lock file has no resources: ${this.lockStore.getLockPath(projectDir)}`);
|
|
276
257
|
}
|
|
277
258
|
const results = [];
|
|
259
|
+
const lockSourceInfo = this.normalizeLockSourceInfo(lock.source);
|
|
260
|
+
const lockedSource = await this.loadSourceFromLock(lockSourceInfo);
|
|
278
261
|
for (const item of lock.resources) {
|
|
279
|
-
const result = await this.
|
|
262
|
+
const result = await this.installWithSource(lockedSource, lockSourceInfo, item.type, item.name, item.version, projectDir, agents ?? item.agents, mode ?? this.resolveInstallMode(item.mode));
|
|
280
263
|
results.push(result);
|
|
281
264
|
}
|
|
282
265
|
return results;
|
|
283
266
|
}
|
|
267
|
+
async installWithSource(source, sourceInfo, type, name, version, projectDir, agents, mode) {
|
|
268
|
+
const history = await source.history(type, name);
|
|
269
|
+
if (history.length === 0) {
|
|
270
|
+
throw new HimanError(errorCodes.RESOURCE_NOT_FOUND, `Resource not found: ${type}/${name}`);
|
|
271
|
+
}
|
|
272
|
+
const resolvedVersion = this.resolveVersion(history, version);
|
|
273
|
+
const storePath = this.getStorePath(type, name, resolvedVersion);
|
|
274
|
+
if (!(await this.exists(storePath))) {
|
|
275
|
+
await source.pull(type, name, resolvedVersion, storePath);
|
|
276
|
+
}
|
|
277
|
+
const resourceMeta = await this.readResourceMetaFromDir(storePath);
|
|
278
|
+
const effectiveTargets = await this.resolveEffectiveAgents(projectDir, agents, resourceMeta?.agents);
|
|
279
|
+
const linkPaths = getProjectResourcePaths(projectDir, type, name, effectiveTargets);
|
|
280
|
+
for (const linkPath of linkPaths) {
|
|
281
|
+
await this.materializeResource(storePath, linkPath, mode);
|
|
282
|
+
}
|
|
283
|
+
await this.lockStore.upsertResource(projectDir, sourceInfo, {
|
|
284
|
+
type,
|
|
285
|
+
name,
|
|
286
|
+
version: resolvedVersion,
|
|
287
|
+
agents: effectiveTargets,
|
|
288
|
+
mode,
|
|
289
|
+
});
|
|
290
|
+
return { type, name, version: resolvedVersion, linkPath: linkPaths[0], mode };
|
|
291
|
+
}
|
|
284
292
|
async loadSourceFromConfig() {
|
|
293
|
+
return (await this.loadSourceWithInfoFromConfig()).source;
|
|
294
|
+
}
|
|
295
|
+
async loadSourceWithInfoFromConfig() {
|
|
296
|
+
const { name, source: stateSource } = await this.getCurrentSourceState();
|
|
297
|
+
const sourceInfo = this.toLockSourceInfo(stateSource, name);
|
|
298
|
+
const source = await this.loadSourceFromInfo(sourceInfo);
|
|
299
|
+
return {
|
|
300
|
+
source,
|
|
301
|
+
sourceInfo,
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
async loadSourceFromLock(sourceInfo) {
|
|
305
|
+
return this.loadSourceFromInfo(sourceInfo);
|
|
306
|
+
}
|
|
307
|
+
async loadSourceFromInfo(sourceInfo) {
|
|
308
|
+
const normalizedSourceInfo = this.normalizeLockSourceInfo(sourceInfo);
|
|
309
|
+
const sourceConfig = this.buildSourceConfig(normalizedSourceInfo.type, normalizedSourceInfo.repo, normalizedSourceInfo.repoId);
|
|
310
|
+
const source = this.createSource(normalizedSourceInfo.type);
|
|
311
|
+
await source.init(sourceConfig);
|
|
312
|
+
return source;
|
|
313
|
+
}
|
|
314
|
+
async getCurrentSourceState() {
|
|
285
315
|
const config = await this.stateStore.loadConfig();
|
|
286
316
|
if (!config?.source) {
|
|
287
317
|
throw new HimanError(errorCodes.CONFIG_NOT_FOUND, "Source config not found. Please run `himan init <git_repo>` first.");
|
|
288
318
|
}
|
|
289
|
-
const
|
|
290
|
-
const
|
|
291
|
-
|
|
292
|
-
return source;
|
|
319
|
+
const currentName = config.sources?.default ?? "default";
|
|
320
|
+
const currentSource = config.sources?.items[currentName] ?? config.source;
|
|
321
|
+
return { name: currentName, source: currentSource };
|
|
293
322
|
}
|
|
294
323
|
createSource(type) {
|
|
295
324
|
return type === "registry"
|
|
@@ -297,14 +326,24 @@ export class ServiceFactory {
|
|
|
297
326
|
: new GitSourceAdapter();
|
|
298
327
|
}
|
|
299
328
|
async getLockSourceInfo() {
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
329
|
+
const { name, source } = await this.getCurrentSourceState();
|
|
330
|
+
return this.toLockSourceInfo(source, name);
|
|
331
|
+
}
|
|
332
|
+
toLockSourceInfo(source, name) {
|
|
333
|
+
return this.normalizeLockSourceInfo({
|
|
334
|
+
name,
|
|
335
|
+
type: source.type,
|
|
336
|
+
repo: source.repo,
|
|
337
|
+
repoId: source.repoId,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
normalizeLockSourceInfo(sourceInfo) {
|
|
341
|
+
if (sourceInfo.type !== "git" || !sourceInfo.repo) {
|
|
342
|
+
return sourceInfo;
|
|
303
343
|
}
|
|
304
344
|
return {
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
repoId: config.source.repoId,
|
|
345
|
+
...sourceInfo,
|
|
346
|
+
repoId: sourceInfo.repoId ?? toRepoId(sourceInfo.repo),
|
|
308
347
|
};
|
|
309
348
|
}
|
|
310
349
|
async getLockedResource(projectDir, type, name) {
|
|
@@ -12,12 +12,13 @@ export class IndexCacheStore {
|
|
|
12
12
|
return null;
|
|
13
13
|
return data.entries.find((item) => item.repoId === repoId && item.type === type) ?? null;
|
|
14
14
|
}
|
|
15
|
-
async upsert(repoId, type,
|
|
15
|
+
async upsert(repoId, type, metadataHash, resources) {
|
|
16
16
|
const now = new Date().toISOString();
|
|
17
17
|
const file = (await this.load()) ?? { version: 1, entries: [] };
|
|
18
18
|
const found = file.entries.find((item) => item.repoId === repoId && item.type === type);
|
|
19
19
|
if (found) {
|
|
20
|
-
found.
|
|
20
|
+
found.metadataHash = metadataHash;
|
|
21
|
+
delete found.baseDirMtimeMs;
|
|
21
22
|
found.resources = resources;
|
|
22
23
|
found.updatedAt = now;
|
|
23
24
|
}
|
|
@@ -25,7 +26,7 @@ export class IndexCacheStore {
|
|
|
25
26
|
file.entries.push({
|
|
26
27
|
repoId,
|
|
27
28
|
type,
|
|
28
|
-
|
|
29
|
+
metadataHash,
|
|
29
30
|
resources,
|
|
30
31
|
updatedAt: now,
|
|
31
32
|
});
|
package/dist/utils/errors.js
CHANGED
|
@@ -21,5 +21,7 @@ export const errorCodes = {
|
|
|
21
21
|
RESOURCE_EXISTS: "E_RESOURCE_EXISTS",
|
|
22
22
|
TEMPLATE_NOT_FOUND: "E_TEMPLATE_NOT_FOUND",
|
|
23
23
|
INVALID_RESOURCE_NAME: "E_INVALID_RESOURCE_NAME",
|
|
24
|
+
INVALID_RESOURCE_METADATA: "E_INVALID_RESOURCE_METADATA",
|
|
25
|
+
PUBLISH_NO_CHANGES: "E_PUBLISH_NO_CHANGES",
|
|
24
26
|
UNSUPPORTED_RESOURCE_TYPE: "E_UNSUPPORTED_RESOURCE_TYPE",
|
|
25
27
|
};
|