@hi-man/himan 0.3.5 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +75 -22
- package/dist/adapters/git/repo-manager.js +246 -0
- package/dist/adapters/resource/resource-analysis.js +42 -0
- package/dist/adapters/source/git-source-adapter.js +188 -9
- package/dist/adapters/source/registry-source-adapter.js +3 -0
- package/dist/cli/builders.js +8 -3
- package/dist/cli/doctor-command.js +30 -0
- package/dist/cli/project-commands.js +17 -3
- package/dist/cli/resource-commands.js +23 -0
- package/dist/cli/source-commands.js +170 -3
- package/dist/domain/doctor.js +1 -0
- package/dist/domain/source-transfer.js +1 -0
- package/dist/services/index.js +665 -36
- package/dist/state/project-lock-store.js +19 -0
- package/docs/mvp/README.md +6 -9
- package/docs/mvp/create-resource.md +11 -11
- package/docs/mvp/impl.md +6 -6
- package/package.json +1 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { RepoManager } from "../git/repo-manager.js";
|
|
2
2
|
import { ResourceScanner } from "../resource/resource-scanner.js";
|
|
3
|
+
import { buildResourceAnalysisMetadata } from "../resource/resource-analysis.js";
|
|
3
4
|
import semver from "semver";
|
|
4
5
|
import { HimanError, errorCodes } from "../../utils/errors.js";
|
|
5
6
|
import { promises as fs } from "node:fs";
|
|
@@ -85,19 +86,13 @@ export class GitSourceAdapter {
|
|
|
85
86
|
if (resourceExists && !options.force) {
|
|
86
87
|
throw new HimanError(errorCodes.RESOURCE_EXISTS, `Resource already exists: ${type}/${name}`);
|
|
87
88
|
}
|
|
89
|
+
const entryContent = this.getDefaultContent(type, name);
|
|
88
90
|
const files = [path.join(resourceDir, "himan.yaml"), path.join(resourceDir, entry)];
|
|
89
91
|
if (!options.dryRun) {
|
|
90
92
|
await fs.rm(resourceDir, { recursive: true, force: true });
|
|
91
93
|
await fs.mkdir(resourceDir, { recursive: true });
|
|
92
|
-
await fs.writeFile(path.join(resourceDir, "himan.yaml"), YAML.stringify({
|
|
93
|
-
|
|
94
|
-
type,
|
|
95
|
-
version: "0.1.0",
|
|
96
|
-
entry,
|
|
97
|
-
description: options.description ?? `${type} resource ${name}`,
|
|
98
|
-
agents,
|
|
99
|
-
}), "utf8");
|
|
100
|
-
await fs.writeFile(path.join(resourceDir, entry), this.getDefaultContent(type, name), "utf8");
|
|
94
|
+
await fs.writeFile(path.join(resourceDir, "himan.yaml"), YAML.stringify(this.buildCreateResourceMetadata(type, name, entry, entryContent, options.description ?? `${type} resource ${name}`, agents)), "utf8");
|
|
95
|
+
await fs.writeFile(path.join(resourceDir, entry), entryContent, "utf8");
|
|
101
96
|
await this.maintainSourceDocs(repoDir, {
|
|
102
97
|
section: resourceExists ? "Changed" : "Added",
|
|
103
98
|
line: resourceExists
|
|
@@ -113,6 +108,73 @@ export class GitSourceAdapter {
|
|
|
113
108
|
dryRun: Boolean(options.dryRun),
|
|
114
109
|
};
|
|
115
110
|
}
|
|
111
|
+
async rename(type, oldName, newName, options = {}) {
|
|
112
|
+
const repoDir = this.getRepoDir();
|
|
113
|
+
const typeDir = this.getTypeDir(type);
|
|
114
|
+
const previousResourceDir = path.join(repoDir, typeDir, oldName);
|
|
115
|
+
const resourceDir = path.join(repoDir, typeDir, newName);
|
|
116
|
+
if (!(await this.exists(previousResourceDir))) {
|
|
117
|
+
throw new HimanError(errorCodes.RESOURCE_NOT_FOUND, `Resource not found: ${type}/${oldName}`);
|
|
118
|
+
}
|
|
119
|
+
await this.ensureRenameTargetAvailable(repoDir, type, newName, resourceDir);
|
|
120
|
+
const history = await this.history(type, oldName);
|
|
121
|
+
const latestVersion = history[0]?.version;
|
|
122
|
+
const tag = latestVersion ? `${type}/${newName}@${latestVersion}` : undefined;
|
|
123
|
+
if (options.dryRun) {
|
|
124
|
+
return {
|
|
125
|
+
type,
|
|
126
|
+
oldName,
|
|
127
|
+
newName,
|
|
128
|
+
previousResourceDir,
|
|
129
|
+
resourceDir,
|
|
130
|
+
latestVersion,
|
|
131
|
+
tag,
|
|
132
|
+
committed: false,
|
|
133
|
+
dryRun: true,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
await fs.mkdir(path.dirname(resourceDir), { recursive: true });
|
|
137
|
+
await fs.rename(previousResourceDir, resourceDir);
|
|
138
|
+
await this.updateRenamedResourceMetadata(resourceDir, type, oldName, newName);
|
|
139
|
+
const versionOverrides = latestVersion
|
|
140
|
+
? new Map([[this.getResourceVersionOverrideKey(type, newName), latestVersion]])
|
|
141
|
+
: new Map();
|
|
142
|
+
const docsPaths = await this.maintainSourceDocs(repoDir, {
|
|
143
|
+
section: "Changed",
|
|
144
|
+
line: `- Renamed \`${type}/${oldName}\` to \`${type}/${newName}\`.`,
|
|
145
|
+
}, versionOverrides);
|
|
146
|
+
const changedPaths = [
|
|
147
|
+
path.relative(repoDir, previousResourceDir),
|
|
148
|
+
path.relative(repoDir, resourceDir),
|
|
149
|
+
...docsPaths.map((docPath) => path.relative(repoDir, docPath)),
|
|
150
|
+
];
|
|
151
|
+
if (tag) {
|
|
152
|
+
await this.repoManager.commitTagAndPush(repoDir, `rename ${type}/${oldName} to ${type}/${newName}`, tag, undefined, changedPaths);
|
|
153
|
+
return {
|
|
154
|
+
type,
|
|
155
|
+
oldName,
|
|
156
|
+
newName,
|
|
157
|
+
previousResourceDir,
|
|
158
|
+
resourceDir,
|
|
159
|
+
latestVersion,
|
|
160
|
+
tag,
|
|
161
|
+
committed: true,
|
|
162
|
+
dryRun: false,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const committed = await this.repoManager.commitAndPush(repoDir, `rename ${type}/${oldName} to ${type}/${newName}`, undefined, changedPaths);
|
|
166
|
+
return {
|
|
167
|
+
type,
|
|
168
|
+
oldName,
|
|
169
|
+
newName,
|
|
170
|
+
previousResourceDir,
|
|
171
|
+
resourceDir,
|
|
172
|
+
latestVersion,
|
|
173
|
+
tag,
|
|
174
|
+
committed,
|
|
175
|
+
dryRun: false,
|
|
176
|
+
};
|
|
177
|
+
}
|
|
116
178
|
async initDocs(options = {}) {
|
|
117
179
|
const repoDir = this.getRepoDir();
|
|
118
180
|
const files = [
|
|
@@ -147,6 +209,13 @@ export class GitSourceAdapter {
|
|
|
147
209
|
committed,
|
|
148
210
|
};
|
|
149
211
|
}
|
|
212
|
+
async cloneTo(targetRepo, options = {}) {
|
|
213
|
+
return this.repoManager.cloneManagedSourceRefs(this.getRepoDir(), targetRepo, options);
|
|
214
|
+
}
|
|
215
|
+
async syncLatestTo(targetRepo, options = {}) {
|
|
216
|
+
const resources = await this.collectLatestVersionedResources();
|
|
217
|
+
return this.repoManager.syncLatestSourceSnapshot(this.getRepoDir(), targetRepo, resources, options);
|
|
218
|
+
}
|
|
150
219
|
getRepoDir() {
|
|
151
220
|
if (!this.sourceConfig?.repoDir) {
|
|
152
221
|
throw new HimanError(errorCodes.CONFIG_NOT_FOUND, "Git source is not initialized.");
|
|
@@ -171,6 +240,65 @@ export class GitSourceAdapter {
|
|
|
171
240
|
return false;
|
|
172
241
|
}
|
|
173
242
|
}
|
|
243
|
+
async ensureRenameTargetAvailable(repoDir, type, newName, resourceDir) {
|
|
244
|
+
if (await this.exists(resourceDir)) {
|
|
245
|
+
throw new HimanError(errorCodes.RESOURCE_EXISTS, `Resource already exists: ${type}/${newName}`);
|
|
246
|
+
}
|
|
247
|
+
const [resources, history] = await Promise.all([
|
|
248
|
+
this.scanner.scanByType(repoDir, type),
|
|
249
|
+
this.history(type, newName),
|
|
250
|
+
]);
|
|
251
|
+
if (resources.some((resource) => resource.name === newName) || history.length > 0) {
|
|
252
|
+
throw new HimanError(errorCodes.RESOURCE_EXISTS, `Resource already exists: ${type}/${newName}`);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async updateRenamedResourceMetadata(resourceDir, type, oldName, newName) {
|
|
256
|
+
const yamlPath = path.join(resourceDir, "himan.yaml");
|
|
257
|
+
if (await this.exists(yamlPath)) {
|
|
258
|
+
const raw = await fs.readFile(yamlPath, "utf8");
|
|
259
|
+
let parsed;
|
|
260
|
+
try {
|
|
261
|
+
parsed = YAML.parse(raw);
|
|
262
|
+
}
|
|
263
|
+
catch (error) {
|
|
264
|
+
throw this.invalidResourceMetadata(type, oldName, "himan.yaml is not valid YAML.", { yamlPath, reason: error instanceof Error ? error.message : String(error) });
|
|
265
|
+
}
|
|
266
|
+
if (!this.isRecord(parsed)) {
|
|
267
|
+
throw this.invalidResourceMetadata(type, oldName, "himan.yaml must be an object.", { yamlPath });
|
|
268
|
+
}
|
|
269
|
+
await fs.writeFile(yamlPath, YAML.stringify({
|
|
270
|
+
...parsed,
|
|
271
|
+
name: newName,
|
|
272
|
+
}), "utf8");
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (type !== "skill")
|
|
276
|
+
return;
|
|
277
|
+
await this.updateSkillFrontMatterName(path.join(resourceDir, this.getDefaultEntry(type)), oldName, newName);
|
|
278
|
+
}
|
|
279
|
+
async updateSkillFrontMatterName(skillPath, oldName, newName) {
|
|
280
|
+
if (!(await this.exists(skillPath)))
|
|
281
|
+
return;
|
|
282
|
+
const raw = await fs.readFile(skillPath, "utf8");
|
|
283
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(raw);
|
|
284
|
+
if (!match)
|
|
285
|
+
return;
|
|
286
|
+
let parsed;
|
|
287
|
+
try {
|
|
288
|
+
parsed = YAML.parse(match[1]);
|
|
289
|
+
}
|
|
290
|
+
catch {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
if (!this.isRecord(parsed) || parsed.name !== oldName)
|
|
294
|
+
return;
|
|
295
|
+
const frontMatter = YAML.stringify({
|
|
296
|
+
...parsed,
|
|
297
|
+
name: newName,
|
|
298
|
+
}).trimEnd();
|
|
299
|
+
const updated = `---\n${frontMatter}\n---\n${raw.slice(match[0].length)}`;
|
|
300
|
+
await fs.writeFile(skillPath, updated, "utf8");
|
|
301
|
+
}
|
|
174
302
|
async validatePublishResource(type, name, resourceDir) {
|
|
175
303
|
const yamlPath = path.join(resourceDir, "himan.yaml");
|
|
176
304
|
if (!(await this.exists(yamlPath))) {
|
|
@@ -406,6 +534,25 @@ export class GitSourceAdapter {
|
|
|
406
534
|
}
|
|
407
535
|
return `# ${name}\n\nDescribe skill workflow here.\n`;
|
|
408
536
|
}
|
|
537
|
+
buildCreateResourceMetadata(type, name, entry, entryContent, description, agents) {
|
|
538
|
+
const metadata = {
|
|
539
|
+
name,
|
|
540
|
+
type,
|
|
541
|
+
version: "0.1.0",
|
|
542
|
+
entry,
|
|
543
|
+
description,
|
|
544
|
+
agents,
|
|
545
|
+
};
|
|
546
|
+
if (type === "skill") {
|
|
547
|
+
metadata.analysis = buildResourceAnalysisMetadata({
|
|
548
|
+
entry,
|
|
549
|
+
entryContent,
|
|
550
|
+
measuredBy: "himan",
|
|
551
|
+
generatedBy: "himan",
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
return metadata;
|
|
555
|
+
}
|
|
409
556
|
async buildReadmeContent(repoDir, versionOverrides = new Map()) {
|
|
410
557
|
const resourceLines = await this.buildResourceIndex(repoDir, versionOverrides);
|
|
411
558
|
const repo = this.sourceConfig?.repo ?? "<git_url>";
|
|
@@ -666,6 +813,38 @@ export class GitSourceAdapter {
|
|
|
666
813
|
return undefined;
|
|
667
814
|
}
|
|
668
815
|
}
|
|
816
|
+
async collectLatestVersionedResources() {
|
|
817
|
+
const repoDir = this.getRepoDir();
|
|
818
|
+
const resources = [];
|
|
819
|
+
for (const type of RESOURCE_TYPES) {
|
|
820
|
+
const scanned = await this.scanner.scanByType(repoDir, type);
|
|
821
|
+
for (const resource of scanned) {
|
|
822
|
+
const history = await this.history(type, resource.name);
|
|
823
|
+
const latest = history[0];
|
|
824
|
+
const metadataVersion = await this.readResourceVersion(repoDir, type, resource.name);
|
|
825
|
+
const version = latest?.version ?? metadataVersion;
|
|
826
|
+
if (!version || !semver.valid(version)) {
|
|
827
|
+
throw new HimanError(errorCodes.VERSION_NOT_FOUND, `Latest version not found for ${type}/${resource.name}.`);
|
|
828
|
+
}
|
|
829
|
+
resources.push({
|
|
830
|
+
type,
|
|
831
|
+
name: resource.name,
|
|
832
|
+
version,
|
|
833
|
+
tag: `${type}/${resource.name}@${version}`,
|
|
834
|
+
sourceRef: latest ? latest.raw : undefined,
|
|
835
|
+
sourcePath: latest
|
|
836
|
+
? undefined
|
|
837
|
+
: path.join(repoDir, this.getTypeDir(type), resource.name),
|
|
838
|
+
});
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return resources.sort((a, b) => {
|
|
842
|
+
const typeOrder = RESOURCE_TYPES.indexOf(a.type) - RESOURCE_TYPES.indexOf(b.type);
|
|
843
|
+
if (typeOrder !== 0)
|
|
844
|
+
return typeOrder;
|
|
845
|
+
return a.name.localeCompare(b.name);
|
|
846
|
+
});
|
|
847
|
+
}
|
|
669
848
|
async getResourceRef(repoDir, type, name, versionOverrides = new Map()) {
|
|
670
849
|
const version = versionOverrides.get(this.getResourceVersionOverrideKey(type, name)) ??
|
|
671
850
|
(await this.readLatestTaggedResourceVersion(repoDir, type, name)) ??
|
|
@@ -18,6 +18,9 @@ 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 rename(_type, _oldName, _newName, _options) {
|
|
22
|
+
throw new HimanError(errorCodes.NOT_IMPLEMENTED, "Registry source is reserved for phase 2.");
|
|
23
|
+
}
|
|
21
24
|
async initDocs(_options) {
|
|
22
25
|
throw new HimanError(errorCodes.NOT_IMPLEMENTED, "Registry source is reserved for phase 2.");
|
|
23
26
|
}
|
package/dist/cli/builders.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { ServiceFactory } from "../services/index.js";
|
|
2
2
|
import { registerAgentCommands } from "./agent-commands.js";
|
|
3
|
+
import { registerDoctorCommand } from "./doctor-command.js";
|
|
3
4
|
import { registerProjectCommands } from "./project-commands.js";
|
|
4
5
|
import { registerResourceCommands } from "./resource-commands.js";
|
|
5
6
|
import { registerInitCommand, registerSourceCommands } from "./source-commands.js";
|
|
@@ -9,6 +10,7 @@ export function buildCli() {
|
|
|
9
10
|
const services = new ServiceFactory();
|
|
10
11
|
appendCommandGroupsHelp(program);
|
|
11
12
|
registerInitCommand(program, services);
|
|
13
|
+
registerDoctorCommand(program, services);
|
|
12
14
|
const sourceCmd = program.command("source").description("Manage source repositories");
|
|
13
15
|
registerSourceCommands(sourceCmd, services, { includeInit: true });
|
|
14
16
|
const resourceCmd = program
|
|
@@ -60,14 +62,17 @@ function appendCommandGroupsHelp(program) {
|
|
|
60
62
|
program.addHelpText("after", `
|
|
61
63
|
Command groups:
|
|
62
64
|
source Data source management (git now, registry reserved)
|
|
63
|
-
init, source init, source add, source use, source list,
|
|
65
|
+
init, source init, source add, source use, source list,
|
|
66
|
+
source init-docs, source clone, source sync
|
|
64
67
|
resource Source resource discovery and metadata
|
|
65
|
-
list, list --installed, history, create,
|
|
66
|
-
resource list, resource history, resource create
|
|
68
|
+
list, list --installed, history, create, rename (not recommended yet),
|
|
69
|
+
resource list, resource history, resource create, resource rename
|
|
67
70
|
project Resource usage lifecycle in current project or user-level agent dirs
|
|
68
71
|
list, install, dev, uninstall, publish,
|
|
69
72
|
project list, project install, project dev, project uninstall, project publish
|
|
70
73
|
agent Default agent configuration
|
|
71
74
|
agent list, agent use, agent current, agent clear
|
|
75
|
+
doctor Runtime and project health checks
|
|
76
|
+
doctor
|
|
72
77
|
`);
|
|
73
78
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { runAction } from "./shared.js";
|
|
2
|
+
export function registerDoctorCommand(command, services) {
|
|
3
|
+
command
|
|
4
|
+
.command("doctor")
|
|
5
|
+
.option("--json", "output json format")
|
|
6
|
+
.description("Check Himan runtime and project health")
|
|
7
|
+
.action(async (options) => {
|
|
8
|
+
await runAction(async () => {
|
|
9
|
+
const result = await services.doctor(process.cwd());
|
|
10
|
+
if (options.json) {
|
|
11
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
12
|
+
}
|
|
13
|
+
else {
|
|
14
|
+
writeDoctorResult(result);
|
|
15
|
+
}
|
|
16
|
+
if (!result.ok) {
|
|
17
|
+
process.exitCode = 1;
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function writeDoctorResult(result) {
|
|
23
|
+
process.stdout.write("Himan doctor\n");
|
|
24
|
+
for (const check of result.checks) {
|
|
25
|
+
process.stdout.write(`${formatCheckStatus(check)} ${check.name}: ${check.message}\n`);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
function formatCheckStatus(check) {
|
|
29
|
+
return `[${check.status}]`;
|
|
30
|
+
}
|
|
@@ -82,7 +82,11 @@ export function registerProjectCommands(command, services, options = {}) {
|
|
|
82
82
|
await runAction(async () => {
|
|
83
83
|
const resourceType = ensureResourceType(type);
|
|
84
84
|
const result = await services.dev(resourceType, name, process.cwd());
|
|
85
|
-
|
|
85
|
+
if (result.sourceScope === "global") {
|
|
86
|
+
process.stdout.write(`Copied global ${result.type}/${result.name} into current project: ${result.devPath}\n`);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
process.stdout.write(`Editing ${result.type}/${result.name} in place: ${result.devPath}\n`);
|
|
86
90
|
});
|
|
87
91
|
});
|
|
88
92
|
command
|
|
@@ -104,13 +108,23 @@ export function registerProjectCommands(command, services, options = {}) {
|
|
|
104
108
|
.option("--patch", "patch release")
|
|
105
109
|
.option("--minor", "minor release")
|
|
106
110
|
.option("--major", "major release")
|
|
111
|
+
.option("--global", "install the published version into user-level agent directories")
|
|
107
112
|
.description("Publish resource (default: --patch)")
|
|
108
113
|
.action(async (type, name, options) => {
|
|
109
114
|
await runAction(async () => {
|
|
110
115
|
const resourceType = ensureResourceType(type);
|
|
111
116
|
const releaseType = resolveReleaseType(options);
|
|
112
|
-
const
|
|
113
|
-
process.stdout.write(
|
|
117
|
+
const installScope = options.global ? "global" : "project";
|
|
118
|
+
process.stdout.write(options.global
|
|
119
|
+
? "Published resource will be installed globally; current project lock will not be updated.\n"
|
|
120
|
+
: "Published resource will be installed into the current project and recorded in himan.lock. Use --global to install globally instead.\n");
|
|
121
|
+
const result = await services.publish(resourceType, name, releaseType, process.cwd(), {
|
|
122
|
+
installScope,
|
|
123
|
+
onProgress: (progress) => {
|
|
124
|
+
process.stdout.write(`[publish:${progress.stage}] ${progress.message}\n`);
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
process.stdout.write(`Published ${result.type}/${result.name}@${result.version} and installed ${result.installScope === "global" ? "globally" : "into current project"}\n`);
|
|
114
128
|
});
|
|
115
129
|
});
|
|
116
130
|
}
|
|
@@ -91,6 +91,29 @@ export function registerResourceCommands(command, services) {
|
|
|
91
91
|
process.stdout.write(`Created ${result.type}/${result.name} at ${result.resourceDir}${result.dryRun ? " (dry-run)" : ""}\n`);
|
|
92
92
|
});
|
|
93
93
|
});
|
|
94
|
+
command
|
|
95
|
+
.command("rename")
|
|
96
|
+
.argument("<type>", "resource type")
|
|
97
|
+
.argument("<old-name>", "current resource name")
|
|
98
|
+
.argument("<new-name>", "new resource name")
|
|
99
|
+
.option("--dry-run", "show rename result without writing")
|
|
100
|
+
.option("--no-project", "do not migrate current project install targets or lock")
|
|
101
|
+
.option("--json", "output json format")
|
|
102
|
+
.description("Rename resource in current default source (not recommended yet)")
|
|
103
|
+
.action(async (type, oldName, newName, options) => {
|
|
104
|
+
await runAction(async () => {
|
|
105
|
+
const resourceType = ensureResourceType(type);
|
|
106
|
+
const result = await services.rename(resourceType, oldName, newName, process.cwd(), {
|
|
107
|
+
dryRun: options.dryRun,
|
|
108
|
+
migrateProject: options.project,
|
|
109
|
+
});
|
|
110
|
+
if (options.json) {
|
|
111
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
process.stdout.write(`Renamed ${result.type}/${result.oldName} to ${result.type}/${result.newName}${result.dryRun ? " (dry-run)" : ""}\n`);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
94
117
|
}
|
|
95
118
|
async function writeInstalledList(services, type, agents, json) {
|
|
96
119
|
if (!type) {
|
|
@@ -1,12 +1,41 @@
|
|
|
1
|
+
import { getSupportedAgentNames, normalizeAgent } from "../utils/agent-configs.js";
|
|
2
|
+
import { HimanError, errorCodes } from "../utils/errors.js";
|
|
1
3
|
import { runAction } from "./shared.js";
|
|
2
4
|
export function registerInitCommand(command, services) {
|
|
3
5
|
command
|
|
4
6
|
.command("init")
|
|
5
7
|
.argument("<git_repo>", "Git repository URL")
|
|
6
|
-
.
|
|
8
|
+
.option("--agent <list>", "set current project default agents, comma separated")
|
|
9
|
+
.option("--install <refs>", "install resource refs after init, comma separated: rule/name[@version]")
|
|
10
|
+
.option("--mode <mode>", "install mode for --install: link or copy")
|
|
11
|
+
.option("--json", "output json format")
|
|
12
|
+
.action(async (gitRepo, options) => {
|
|
7
13
|
await runAction(async () => {
|
|
8
|
-
const
|
|
9
|
-
|
|
14
|
+
const agents = parseAgents(options.agent);
|
|
15
|
+
const installRefs = parseInstallRefs(options.install);
|
|
16
|
+
const mode = parseInstallMode(options.mode);
|
|
17
|
+
if (mode && installRefs.length === 0) {
|
|
18
|
+
throw new HimanError(errorCodes.CLI_USAGE, "Use --mode only with --install.");
|
|
19
|
+
}
|
|
20
|
+
const source = await services.initSource("git", gitRepo);
|
|
21
|
+
const agentResult = agents?.length
|
|
22
|
+
? await services.setAgents(agents, "project", process.cwd())
|
|
23
|
+
: undefined;
|
|
24
|
+
const installed = [];
|
|
25
|
+
for (const ref of installRefs) {
|
|
26
|
+
installed.push(await services.install(ref.type, ref.name, ref.version, process.cwd(), agents, mode));
|
|
27
|
+
}
|
|
28
|
+
if (options.json) {
|
|
29
|
+
process.stdout.write(`${JSON.stringify({ source, agents: agentResult, installed }, null, 2)}\n`);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
process.stdout.write(`Initialized ${source.sourceType} source: ${source.repo}\n`);
|
|
33
|
+
if (agentResult) {
|
|
34
|
+
process.stdout.write(`Using agents (${agentResult.scope}): ${agentResult.agents.join(", ")}\n`);
|
|
35
|
+
}
|
|
36
|
+
for (const item of installed) {
|
|
37
|
+
process.stdout.write(`Installed ${item.type}/${item.name}@${item.version}\n`);
|
|
38
|
+
}
|
|
10
39
|
});
|
|
11
40
|
});
|
|
12
41
|
}
|
|
@@ -35,6 +64,80 @@ export function registerSourceCommands(command, services, options) {
|
|
|
35
64
|
process.stdout.write(`Using source: ${result.name}\n`);
|
|
36
65
|
});
|
|
37
66
|
});
|
|
67
|
+
command
|
|
68
|
+
.command("clone")
|
|
69
|
+
.argument("<from>", "source name or git repository URL")
|
|
70
|
+
.argument("<to>", "target source name or git repository URL")
|
|
71
|
+
.option("--branch <branch>", "source branch to clone")
|
|
72
|
+
.option("--target-branch <branch>", "target branch name")
|
|
73
|
+
.option("--add-source <name>", "add the target git repo as a named source after clone")
|
|
74
|
+
.option("--use", "switch default source to the target source after clone")
|
|
75
|
+
.option("--dry-run", "show refs without pushing")
|
|
76
|
+
.option("--json", "output json format")
|
|
77
|
+
.description("Clone a git source into an empty target git repository")
|
|
78
|
+
.action(async (from, to, options) => {
|
|
79
|
+
await runAction(async () => {
|
|
80
|
+
const result = await services.cloneSource(from, to, {
|
|
81
|
+
branch: options.branch,
|
|
82
|
+
targetBranch: options.targetBranch,
|
|
83
|
+
addSource: options.addSource,
|
|
84
|
+
use: options.use,
|
|
85
|
+
dryRun: options.dryRun,
|
|
86
|
+
});
|
|
87
|
+
if (options.json) {
|
|
88
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
process.stdout.write(`Source clone ${result.dryRun ? "dry-run" : "completed"}: ${result.source.name ?? result.source.repo} -> ${result.target.name ?? result.target.repo}\n`);
|
|
92
|
+
process.stdout.write(`- branch ${result.branch} -> ${result.targetBranch}\n`);
|
|
93
|
+
process.stdout.write(`- resource tags: ${result.tags.length}\n`);
|
|
94
|
+
if (result.addedSource) {
|
|
95
|
+
process.stdout.write(`- added source: ${result.addedSource}\n`);
|
|
96
|
+
}
|
|
97
|
+
if (result.usedSource) {
|
|
98
|
+
process.stdout.write(`- using source: ${result.usedSource}\n`);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
command
|
|
103
|
+
.command("sync")
|
|
104
|
+
.argument("<from>", "source name or git repository URL")
|
|
105
|
+
.argument("<to>", "target source name or git repository URL")
|
|
106
|
+
.option("--target-branch <branch>", "target branch name", "main")
|
|
107
|
+
.option("--add-source <name>", "add the target git repo as a named source after sync")
|
|
108
|
+
.option("--use", "switch default source to the target source after sync")
|
|
109
|
+
.option("--dry-run", "show resources without pushing")
|
|
110
|
+
.option("--json", "output json format")
|
|
111
|
+
.description("Sync latest source resource snapshots into a target git repository")
|
|
112
|
+
.action(async (from, to, options) => {
|
|
113
|
+
await runAction(async () => {
|
|
114
|
+
const result = await services.syncSource(from, to, {
|
|
115
|
+
targetBranch: options.targetBranch,
|
|
116
|
+
addSource: options.addSource,
|
|
117
|
+
use: options.use,
|
|
118
|
+
dryRun: options.dryRun,
|
|
119
|
+
});
|
|
120
|
+
if (options.json) {
|
|
121
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const created = result.resources.filter((resource) => resource.action === "created").length;
|
|
125
|
+
const skipped = result.resources.length - created;
|
|
126
|
+
process.stdout.write(`Source sync ${result.dryRun ? "dry-run" : "completed"}: ${result.source.name ?? result.source.repo} -> ${result.target.name ?? result.target.repo}\n`);
|
|
127
|
+
process.stdout.write(`- target branch: ${result.targetBranch}\n`);
|
|
128
|
+
process.stdout.write(`- resources: ${result.resources.length}\n`);
|
|
129
|
+
process.stdout.write(`- tags created: ${created}\n`);
|
|
130
|
+
if (skipped > 0) {
|
|
131
|
+
process.stdout.write(`- tags skipped: ${skipped}\n`);
|
|
132
|
+
}
|
|
133
|
+
if (result.addedSource) {
|
|
134
|
+
process.stdout.write(`- added source: ${result.addedSource}\n`);
|
|
135
|
+
}
|
|
136
|
+
if (result.usedSource) {
|
|
137
|
+
process.stdout.write(`- using source: ${result.usedSource}\n`);
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
});
|
|
38
141
|
command
|
|
39
142
|
.command("list")
|
|
40
143
|
.option("--json", "output json format")
|
|
@@ -84,3 +187,67 @@ export function registerSourceCommands(command, services, options) {
|
|
|
84
187
|
});
|
|
85
188
|
});
|
|
86
189
|
}
|
|
190
|
+
function ensureResourceType(type) {
|
|
191
|
+
if (type !== "rule" && type !== "command" && type !== "skill") {
|
|
192
|
+
throw new HimanError(errorCodes.UNSUPPORTED_RESOURCE_TYPE, `Unsupported resource type: ${type}`);
|
|
193
|
+
}
|
|
194
|
+
return type;
|
|
195
|
+
}
|
|
196
|
+
function parseInstallRefs(input) {
|
|
197
|
+
if (!input)
|
|
198
|
+
return [];
|
|
199
|
+
const refs = input
|
|
200
|
+
.split(",")
|
|
201
|
+
.map((item) => item.trim())
|
|
202
|
+
.filter(Boolean);
|
|
203
|
+
if (refs.length === 0) {
|
|
204
|
+
throw new HimanError(errorCodes.INVALID_INPUT, "Install list cannot be empty.");
|
|
205
|
+
}
|
|
206
|
+
return refs.map((ref) => {
|
|
207
|
+
const parts = ref.split("/");
|
|
208
|
+
if (parts.length !== 2 || !parts[0] || !parts[1]) {
|
|
209
|
+
throw new HimanError(errorCodes.INVALID_INPUT, `Invalid install ref: ${ref}. Use type/name[@version].`);
|
|
210
|
+
}
|
|
211
|
+
const { name, version } = parseNameVersion(parts[1]);
|
|
212
|
+
if (!name) {
|
|
213
|
+
throw new HimanError(errorCodes.INVALID_INPUT, `Invalid install ref: ${ref}. Use type/name[@version].`);
|
|
214
|
+
}
|
|
215
|
+
return {
|
|
216
|
+
type: ensureResourceType(parts[0]),
|
|
217
|
+
name,
|
|
218
|
+
version,
|
|
219
|
+
};
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
function parseNameVersion(input) {
|
|
223
|
+
const idx = input.lastIndexOf("@");
|
|
224
|
+
if (idx <= 0)
|
|
225
|
+
return { name: input };
|
|
226
|
+
return { name: input.slice(0, idx), version: input.slice(idx + 1) };
|
|
227
|
+
}
|
|
228
|
+
function parseInstallMode(input) {
|
|
229
|
+
if (!input)
|
|
230
|
+
return undefined;
|
|
231
|
+
const normalized = input.trim().toLowerCase();
|
|
232
|
+
if (normalized === "link" || normalized === "copy") {
|
|
233
|
+
return normalized;
|
|
234
|
+
}
|
|
235
|
+
throw new HimanError(errorCodes.INVALID_INPUT, `Unsupported install mode: ${input}. Supported modes: link, copy`);
|
|
236
|
+
}
|
|
237
|
+
function parseAgents(input) {
|
|
238
|
+
if (!input)
|
|
239
|
+
return undefined;
|
|
240
|
+
const agents = input
|
|
241
|
+
.split(",")
|
|
242
|
+
.map((item) => item.trim())
|
|
243
|
+
.filter(Boolean);
|
|
244
|
+
if (agents.length === 0)
|
|
245
|
+
return undefined;
|
|
246
|
+
const supported = getSupportedAgentNames();
|
|
247
|
+
for (const agent of agents) {
|
|
248
|
+
if (!normalizeAgent(agent)) {
|
|
249
|
+
throw new HimanError(errorCodes.INVALID_INPUT, `Unsupported agent: ${agent}. Supported agents: ${supported.join(", ")}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return agents;
|
|
253
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|