@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.
@@ -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
- name,
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
  }
@@ -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, source init-docs
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
- process.stdout.write(`Switched ${result.type}/${result.name} to dev mode: ${result.devPath}\n`);
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 result = await services.publish(resourceType, name, releaseType, process.cwd());
113
- process.stdout.write(`Published ${result.type}/${result.name}@${result.version}\n`);
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
- .action(async (gitRepo) => {
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 result = await services.initSource("git", gitRepo);
9
- process.stdout.write(`Initialized ${result.sourceType} source: ${result.repo}\n`);
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 {};