@hi-man/himan 0.1.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/LICENSE +201 -0
- package/README.md +145 -0
- package/dist/adapters/git/repo-manager.js +70 -0
- package/dist/adapters/resource/resource-scanner.js +54 -0
- package/dist/adapters/source/git-source-adapter.js +149 -0
- package/dist/adapters/source/registry-source-adapter.js +21 -0
- package/dist/adapters/source/resource-source-adapter.js +1 -0
- package/dist/adapters/version/version-resolver.js +13 -0
- package/dist/cli/index.js +318 -0
- package/dist/domain/resource.js +1 -0
- package/dist/index.js +17 -0
- package/dist/services/index.js +331 -0
- package/dist/state/index-cache-store.js +48 -0
- package/dist/state/project-lock-store.js +72 -0
- package/dist/state/state-store.js +55 -0
- package/dist/utils/errors.js +25 -0
- package/dist/utils/path-resolver.js +16 -0
- package/dist/utils/repo-id.js +4 -0
- package/dist/version.js +10 -0
- package/package.json +55 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import { Command, CommanderError } from "commander";
|
|
2
|
+
import { ServiceFactory } from "../services/index.js";
|
|
3
|
+
import { errorCodes, HimanError } from "../utils/errors.js";
|
|
4
|
+
import { PACKAGE_VERSION } from "../version.js";
|
|
5
|
+
export function buildCli() {
|
|
6
|
+
const program = new Command();
|
|
7
|
+
const services = new ServiceFactory();
|
|
8
|
+
program.exitOverride();
|
|
9
|
+
program.configureOutput({
|
|
10
|
+
writeOut: (str) => {
|
|
11
|
+
process.stdout.write(str);
|
|
12
|
+
},
|
|
13
|
+
writeErr: () => {
|
|
14
|
+
// Parse/usage errors are unified by writeCliError().
|
|
15
|
+
},
|
|
16
|
+
});
|
|
17
|
+
program
|
|
18
|
+
.name("himan")
|
|
19
|
+
.description("Prompt and agent asset management CLI")
|
|
20
|
+
.version(PACKAGE_VERSION);
|
|
21
|
+
appendCommandGroupsHelp(program);
|
|
22
|
+
program
|
|
23
|
+
.command("init")
|
|
24
|
+
.argument("<git_repo>", "Git repository URL")
|
|
25
|
+
.action(async (gitRepo) => {
|
|
26
|
+
await runAction(async () => {
|
|
27
|
+
const result = await services.initSource("git", gitRepo);
|
|
28
|
+
process.stdout.write(`Initialized ${result.sourceType} source: ${result.repo}\n`);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
const sourceCmd = program.command("source").description("Manage source repositories");
|
|
32
|
+
sourceCmd
|
|
33
|
+
.command("add")
|
|
34
|
+
.argument("<name>", "source name (kebab-case)")
|
|
35
|
+
.argument("<git_repo>", "Git repository URL")
|
|
36
|
+
.description("Add a named git source")
|
|
37
|
+
.action(async (name, gitRepo) => {
|
|
38
|
+
await runAction(async () => {
|
|
39
|
+
const result = await services.addSource(name, "git", gitRepo);
|
|
40
|
+
process.stdout.write(`Added source ${result.name}: ${result.type}${result.repo ? ` ${result.repo}` : ""}\n`);
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
sourceCmd
|
|
44
|
+
.command("use")
|
|
45
|
+
.argument("<name>", "source name")
|
|
46
|
+
.description("Switch default source")
|
|
47
|
+
.action(async (name) => {
|
|
48
|
+
await runAction(async () => {
|
|
49
|
+
const result = await services.useSource(name);
|
|
50
|
+
process.stdout.write(`Using source: ${result.name}\n`);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
sourceCmd
|
|
54
|
+
.command("list")
|
|
55
|
+
.option("--json", "output json format")
|
|
56
|
+
.description("List configured sources and current default")
|
|
57
|
+
.action(async (options) => {
|
|
58
|
+
await runAction(async () => {
|
|
59
|
+
const sources = await services.listSources();
|
|
60
|
+
if (options.json) {
|
|
61
|
+
process.stdout.write(`${JSON.stringify(sources, null, 2)}\n`);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (sources.length === 0) {
|
|
65
|
+
process.stdout.write("No sources configured.\n");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
for (const source of sources) {
|
|
69
|
+
process.stdout.write(`- ${source.name}${source.isDefault ? " (default)" : ""}: ${source.type}${source.repo ? ` ${source.repo}` : ""}\n`);
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
program
|
|
74
|
+
.command("list")
|
|
75
|
+
.argument("[type]", "resource type", "rule")
|
|
76
|
+
.option("--json", "output json format")
|
|
77
|
+
.description("List resources from current default source")
|
|
78
|
+
.action(async (type, options) => {
|
|
79
|
+
await runAction(async () => {
|
|
80
|
+
const resourceType = ensureResourceType(type);
|
|
81
|
+
const resources = await services.list(resourceType);
|
|
82
|
+
if (options.json) {
|
|
83
|
+
process.stdout.write(`${JSON.stringify(resources, null, 2)}\n`);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
if (resources.length === 0) {
|
|
87
|
+
process.stdout.write("No resources found.\n");
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
for (const resource of resources) {
|
|
91
|
+
process.stdout.write(`- ${resource.type}/${resource.name}${resource.description ? `: ${resource.description}` : ""}\n`);
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
program
|
|
96
|
+
.command("history")
|
|
97
|
+
.argument("<type>", "resource type")
|
|
98
|
+
.argument("<name>", "resource name")
|
|
99
|
+
.option("--json", "output json format")
|
|
100
|
+
.description("Show resource history")
|
|
101
|
+
.action(async (type, name, options) => {
|
|
102
|
+
await runAction(async () => {
|
|
103
|
+
const resourceType = ensureResourceType(type);
|
|
104
|
+
const versions = await services.history(resourceType, name);
|
|
105
|
+
if (options.json) {
|
|
106
|
+
process.stdout.write(`${JSON.stringify(versions, null, 2)}\n`);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
if (versions.length === 0) {
|
|
110
|
+
process.stdout.write(`No history found for ${resourceType}/${name}.\n`);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
for (const version of versions) {
|
|
114
|
+
process.stdout.write(`- ${version.raw}\n`);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
program
|
|
119
|
+
.command("install")
|
|
120
|
+
.argument("[type]", "resource type")
|
|
121
|
+
.argument("[name[@version]]", "resource name with optional @version")
|
|
122
|
+
.description("Install resource, or install from himan.lock")
|
|
123
|
+
.action(async (type, nameVersion) => {
|
|
124
|
+
await runAction(async () => {
|
|
125
|
+
if (!type && !nameVersion) {
|
|
126
|
+
const results = await services.installFromLock(process.cwd());
|
|
127
|
+
if (results.length === 0) {
|
|
128
|
+
process.stdout.write("No resources in lock file.\n");
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
for (const item of results) {
|
|
132
|
+
process.stdout.write(`Installed ${item.type}/${item.name}@${item.version}\n`);
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
if (!type || !nameVersion) {
|
|
137
|
+
throw new Error("Install usage:\n"
|
|
138
|
+
+ " - himan install # install from himan.lock\n"
|
|
139
|
+
+ " - himan install <type> <name[@version]> # install single resource");
|
|
140
|
+
}
|
|
141
|
+
const resourceType = ensureResourceType(type);
|
|
142
|
+
const { name, version } = parseNameVersion(nameVersion);
|
|
143
|
+
const result = await services.install(resourceType, name, version, process.cwd());
|
|
144
|
+
process.stdout.write(`Installed ${result.type}/${result.name}@${result.version}\n`);
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
program
|
|
148
|
+
.command("dev")
|
|
149
|
+
.argument("<type>", "resource type")
|
|
150
|
+
.argument("<name>", "resource name")
|
|
151
|
+
.description("Switch resource to development mode")
|
|
152
|
+
.action(async (type, name) => {
|
|
153
|
+
await runAction(async () => {
|
|
154
|
+
const resourceType = ensureResourceType(type);
|
|
155
|
+
const result = await services.dev(resourceType, name, process.cwd());
|
|
156
|
+
process.stdout.write(`Switched ${result.type}/${result.name} to dev mode: ${result.devPath}\n`);
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
program
|
|
160
|
+
.command("uninstall")
|
|
161
|
+
.argument("<type>", "resource type")
|
|
162
|
+
.argument("<name>", "resource name")
|
|
163
|
+
.description("Uninstall resource from project and lock")
|
|
164
|
+
.action(async (type, name) => {
|
|
165
|
+
await runAction(async () => {
|
|
166
|
+
const resourceType = ensureResourceType(type);
|
|
167
|
+
const result = await services.uninstall(resourceType, name, process.cwd());
|
|
168
|
+
process.stdout.write(`Uninstalled ${result.type}/${result.name}\n`);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
program
|
|
172
|
+
.command("publish")
|
|
173
|
+
.argument("<type>", "resource type")
|
|
174
|
+
.argument("<name>", "resource name")
|
|
175
|
+
.option("--patch", "patch release")
|
|
176
|
+
.option("--minor", "minor release")
|
|
177
|
+
.option("--major", "major release")
|
|
178
|
+
.description("Publish resource (default: --patch)")
|
|
179
|
+
.action(async (type, name, options) => {
|
|
180
|
+
await runAction(async () => {
|
|
181
|
+
const resourceType = ensureResourceType(type);
|
|
182
|
+
const releaseType = resolveReleaseType(options);
|
|
183
|
+
const result = await services.publish(resourceType, name, releaseType, process.cwd());
|
|
184
|
+
process.stdout.write(`Published ${result.type}/${result.name}@${result.version}\n`);
|
|
185
|
+
});
|
|
186
|
+
});
|
|
187
|
+
program
|
|
188
|
+
.command("create")
|
|
189
|
+
.argument("<type>", "resource type")
|
|
190
|
+
.argument("<name>", "resource name")
|
|
191
|
+
.option("--description <text>", "resource description")
|
|
192
|
+
.option("--target <list>", "targets list, comma separated")
|
|
193
|
+
.option("--entry <file>", "entry file name")
|
|
194
|
+
.option("--template <name>", "template name", "basic")
|
|
195
|
+
.option("--force", "overwrite existing resource")
|
|
196
|
+
.option("--dry-run", "show files without writing")
|
|
197
|
+
.option("--json", "output json format")
|
|
198
|
+
.description("Create resource scaffold")
|
|
199
|
+
.action(async (type, name, options) => {
|
|
200
|
+
await runAction(async () => {
|
|
201
|
+
const resourceType = ensureCreateResourceType(type);
|
|
202
|
+
const result = await services.create(resourceType, name, {
|
|
203
|
+
description: options.description,
|
|
204
|
+
targets: parseTargets(options.target),
|
|
205
|
+
entry: options.entry,
|
|
206
|
+
template: options.template,
|
|
207
|
+
force: options.force,
|
|
208
|
+
dryRun: options.dryRun,
|
|
209
|
+
});
|
|
210
|
+
if (options.json) {
|
|
211
|
+
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
process.stdout.write(`Created ${result.type}/${result.name} at ${result.resourceDir}${result.dryRun ? " (dry-run)" : ""}\n`);
|
|
215
|
+
});
|
|
216
|
+
});
|
|
217
|
+
return program;
|
|
218
|
+
}
|
|
219
|
+
function ensureResourceType(type) {
|
|
220
|
+
if (type !== "rule" && type !== "command" && type !== "skill") {
|
|
221
|
+
throw new Error(`Unsupported resource type: ${type}`);
|
|
222
|
+
}
|
|
223
|
+
return type;
|
|
224
|
+
}
|
|
225
|
+
function parseNameVersion(input) {
|
|
226
|
+
const idx = input.lastIndexOf("@");
|
|
227
|
+
if (idx <= 0)
|
|
228
|
+
return { name: input };
|
|
229
|
+
return { name: input.slice(0, idx), version: input.slice(idx + 1) };
|
|
230
|
+
}
|
|
231
|
+
async function runAction(action) {
|
|
232
|
+
try {
|
|
233
|
+
await action();
|
|
234
|
+
}
|
|
235
|
+
catch (error) {
|
|
236
|
+
writeCliError(error);
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
export function writeCliError(error) {
|
|
241
|
+
const payload = toCliErrorPayload(error);
|
|
242
|
+
if (shouldOutputJsonError()) {
|
|
243
|
+
process.stderr.write(`${JSON.stringify(payload, null, 2)}\n`);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
process.stderr.write(`[${payload.error.code}] ${payload.error.message}\n`);
|
|
247
|
+
}
|
|
248
|
+
function toCliErrorPayload(error) {
|
|
249
|
+
if (error instanceof CommanderError) {
|
|
250
|
+
return {
|
|
251
|
+
ok: false,
|
|
252
|
+
error: {
|
|
253
|
+
code: errorCodes.CLI_USAGE,
|
|
254
|
+
message: error.message,
|
|
255
|
+
details: {
|
|
256
|
+
commanderCode: error.code,
|
|
257
|
+
exitCode: error.exitCode,
|
|
258
|
+
},
|
|
259
|
+
},
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
if (error instanceof HimanError) {
|
|
263
|
+
return {
|
|
264
|
+
ok: false,
|
|
265
|
+
error: {
|
|
266
|
+
code: error.code,
|
|
267
|
+
message: error.message,
|
|
268
|
+
details: error.details,
|
|
269
|
+
},
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
ok: false,
|
|
274
|
+
error: {
|
|
275
|
+
code: "E_UNKNOWN",
|
|
276
|
+
message: error instanceof Error ? error.message : String(error),
|
|
277
|
+
},
|
|
278
|
+
};
|
|
279
|
+
}
|
|
280
|
+
function shouldOutputJsonError() {
|
|
281
|
+
return process.argv.includes("--json");
|
|
282
|
+
}
|
|
283
|
+
function appendCommandGroupsHelp(program) {
|
|
284
|
+
program.addHelpText("after", `
|
|
285
|
+
Command groups:
|
|
286
|
+
source Data source management (git now, registry reserved)
|
|
287
|
+
init, source add, source use, source list
|
|
288
|
+
resource Source resource discovery and metadata
|
|
289
|
+
list, history, create
|
|
290
|
+
project Resource usage lifecycle in current project
|
|
291
|
+
install, dev, uninstall, publish
|
|
292
|
+
`);
|
|
293
|
+
}
|
|
294
|
+
function resolveReleaseType(options) {
|
|
295
|
+
const selected = [
|
|
296
|
+
options.patch ? "patch" : undefined,
|
|
297
|
+
options.minor ? "minor" : undefined,
|
|
298
|
+
options.major ? "major" : undefined,
|
|
299
|
+
].filter(Boolean);
|
|
300
|
+
if (selected.length > 1) {
|
|
301
|
+
throw new Error("Use only one of --patch, --minor or --major.");
|
|
302
|
+
}
|
|
303
|
+
return selected[0] ?? "patch";
|
|
304
|
+
}
|
|
305
|
+
function ensureCreateResourceType(type) {
|
|
306
|
+
if (type !== "rule" && type !== "command" && type !== "skill") {
|
|
307
|
+
throw new Error(`Unsupported resource type: ${type}`);
|
|
308
|
+
}
|
|
309
|
+
return type;
|
|
310
|
+
}
|
|
311
|
+
function parseTargets(input) {
|
|
312
|
+
if (!input)
|
|
313
|
+
return undefined;
|
|
314
|
+
return input
|
|
315
|
+
.split(",")
|
|
316
|
+
.map((item) => item.trim())
|
|
317
|
+
.filter(Boolean);
|
|
318
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { CommanderError } from "commander";
|
|
3
|
+
import { buildCli, writeCliError } from "./cli/index.js";
|
|
4
|
+
async function main() {
|
|
5
|
+
const program = buildCli();
|
|
6
|
+
try {
|
|
7
|
+
await program.parseAsync(process.argv);
|
|
8
|
+
}
|
|
9
|
+
catch (error) {
|
|
10
|
+
if (error instanceof CommanderError && error.exitCode === 0) {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
writeCliError(error);
|
|
14
|
+
process.exitCode = error instanceof CommanderError ? error.exitCode : 1;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
void main();
|
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { GitSourceAdapter } from "../adapters/source/git-source-adapter.js";
|
|
2
|
+
import { RegistrySourceAdapter } from "../adapters/source/registry-source-adapter.js";
|
|
3
|
+
import { StateStore } from "../state/state-store.js";
|
|
4
|
+
import { ProjectLockStore } from "../state/project-lock-store.js";
|
|
5
|
+
import { PathResolver } from "../utils/path-resolver.js";
|
|
6
|
+
import { toRepoId } from "../utils/repo-id.js";
|
|
7
|
+
import { HimanError, errorCodes } from "../utils/errors.js";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
import { promises as fs } from "node:fs";
|
|
10
|
+
import { VersionResolver } from "../adapters/version/version-resolver.js";
|
|
11
|
+
export class ServiceFactory {
|
|
12
|
+
stateStore = new StateStore();
|
|
13
|
+
lockStore = new ProjectLockStore();
|
|
14
|
+
paths = new PathResolver();
|
|
15
|
+
versions = new VersionResolver();
|
|
16
|
+
async initSource(type, repo) {
|
|
17
|
+
await this.stateStore.ensureBaseDirs();
|
|
18
|
+
const sourceConfig = this.buildSourceConfig(type, repo);
|
|
19
|
+
const source = this.createSource(type);
|
|
20
|
+
await source.init(sourceConfig);
|
|
21
|
+
const stateSource = {
|
|
22
|
+
type,
|
|
23
|
+
repo: sourceConfig.repo,
|
|
24
|
+
repoId: sourceConfig.repoId,
|
|
25
|
+
};
|
|
26
|
+
await this.stateStore.saveConfig({
|
|
27
|
+
source: stateSource,
|
|
28
|
+
sources: {
|
|
29
|
+
default: "default",
|
|
30
|
+
items: { default: stateSource },
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
return {
|
|
34
|
+
sourceType: type,
|
|
35
|
+
repo: sourceConfig.repo,
|
|
36
|
+
repoId: sourceConfig.repoId,
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async addSource(name, type, repo) {
|
|
40
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
|
|
41
|
+
throw new HimanError(errorCodes.INVALID_INPUT, `Invalid source name: ${name}`);
|
|
42
|
+
}
|
|
43
|
+
await this.stateStore.ensureBaseDirs();
|
|
44
|
+
const sourceConfig = this.buildSourceConfig(type, repo);
|
|
45
|
+
const source = this.createSource(type);
|
|
46
|
+
await source.init(sourceConfig);
|
|
47
|
+
const stateSource = {
|
|
48
|
+
type,
|
|
49
|
+
repo: sourceConfig.repo,
|
|
50
|
+
repoId: sourceConfig.repoId,
|
|
51
|
+
};
|
|
52
|
+
const current = await this.stateStore.loadConfig();
|
|
53
|
+
const items = { ...(current?.sources?.items ?? {}) };
|
|
54
|
+
items[name] = stateSource;
|
|
55
|
+
const defaultName = current?.sources?.default ?? name;
|
|
56
|
+
const defaultSource = items[defaultName] ?? stateSource;
|
|
57
|
+
await this.stateStore.saveConfig({
|
|
58
|
+
source: defaultSource,
|
|
59
|
+
sources: {
|
|
60
|
+
default: defaultName,
|
|
61
|
+
items,
|
|
62
|
+
},
|
|
63
|
+
});
|
|
64
|
+
return { name, type, repo: sourceConfig.repo, repoId: sourceConfig.repoId };
|
|
65
|
+
}
|
|
66
|
+
async useSource(name) {
|
|
67
|
+
const config = await this.stateStore.loadConfig();
|
|
68
|
+
if (!config?.sources) {
|
|
69
|
+
throw new HimanError(errorCodes.CONFIG_NOT_FOUND, "No source configured.");
|
|
70
|
+
}
|
|
71
|
+
const target = config.sources.items[name];
|
|
72
|
+
if (!target) {
|
|
73
|
+
throw new HimanError(errorCodes.RESOURCE_NOT_FOUND, `Source not found: ${name}`);
|
|
74
|
+
}
|
|
75
|
+
await this.stateStore.saveConfig({
|
|
76
|
+
source: target,
|
|
77
|
+
sources: {
|
|
78
|
+
default: name,
|
|
79
|
+
items: config.sources.items,
|
|
80
|
+
},
|
|
81
|
+
});
|
|
82
|
+
return { name };
|
|
83
|
+
}
|
|
84
|
+
async listSources() {
|
|
85
|
+
const config = await this.stateStore.loadConfig();
|
|
86
|
+
if (!config?.sources)
|
|
87
|
+
return [];
|
|
88
|
+
return Object.entries(config.sources.items).map(([name, source]) => ({
|
|
89
|
+
name,
|
|
90
|
+
type: source.type,
|
|
91
|
+
repo: source.repo,
|
|
92
|
+
repoId: source.repoId,
|
|
93
|
+
isDefault: name === config.sources?.default,
|
|
94
|
+
}));
|
|
95
|
+
}
|
|
96
|
+
async list(type) {
|
|
97
|
+
const source = await this.loadSourceFromConfig();
|
|
98
|
+
return source.list(type);
|
|
99
|
+
}
|
|
100
|
+
async history(type, name) {
|
|
101
|
+
const source = await this.loadSourceFromConfig();
|
|
102
|
+
return source.history(type, name);
|
|
103
|
+
}
|
|
104
|
+
async install(type, name, version, projectDir) {
|
|
105
|
+
const source = await this.loadSourceFromConfig();
|
|
106
|
+
const sourceInfo = await this.getLockSourceInfo();
|
|
107
|
+
const history = await source.history(type, name);
|
|
108
|
+
if (history.length === 0) {
|
|
109
|
+
throw new HimanError(errorCodes.RESOURCE_NOT_FOUND, `Resource not found: ${type}/${name}`);
|
|
110
|
+
}
|
|
111
|
+
const resolvedVersion = this.resolveVersion(history, version);
|
|
112
|
+
const storePath = this.getStorePath(type, name, resolvedVersion);
|
|
113
|
+
const linkPath = this.getProjectResourcePath(projectDir, type, name);
|
|
114
|
+
if (!(await this.exists(storePath))) {
|
|
115
|
+
await source.pull(type, name, resolvedVersion, storePath);
|
|
116
|
+
}
|
|
117
|
+
await this.switchSymlink(storePath, linkPath);
|
|
118
|
+
await this.lockStore.upsertResource(projectDir, sourceInfo, {
|
|
119
|
+
type,
|
|
120
|
+
name,
|
|
121
|
+
version: resolvedVersion,
|
|
122
|
+
});
|
|
123
|
+
return { type, name, version: resolvedVersion, linkPath };
|
|
124
|
+
}
|
|
125
|
+
async dev(type, name, projectDir) {
|
|
126
|
+
const linkPath = this.getProjectResourcePath(projectDir, type, name);
|
|
127
|
+
const installedPath = await this.readInstalledPath(linkPath);
|
|
128
|
+
const devPath = this.getProjectDevPath(projectDir, type, name);
|
|
129
|
+
if (!(await this.exists(devPath))) {
|
|
130
|
+
await fs.mkdir(path.dirname(devPath), { recursive: true });
|
|
131
|
+
await fs.cp(installedPath, devPath, { recursive: true });
|
|
132
|
+
}
|
|
133
|
+
await this.switchSymlink(devPath, linkPath);
|
|
134
|
+
return { type, name, devPath, linkPath };
|
|
135
|
+
}
|
|
136
|
+
async uninstall(type, name, projectDir) {
|
|
137
|
+
const linkPath = this.getProjectResourcePath(projectDir, type, name);
|
|
138
|
+
if (!(await this.exists(linkPath))) {
|
|
139
|
+
throw new HimanError(errorCodes.INSTALL_NOT_FOUND, `Installed resource link not found: ${linkPath}.`);
|
|
140
|
+
}
|
|
141
|
+
await fs.rm(linkPath, { recursive: true, force: true });
|
|
142
|
+
await this.lockStore.removeResource(projectDir, { type, name });
|
|
143
|
+
return { type, name, linkPath };
|
|
144
|
+
}
|
|
145
|
+
async publish(type, name, releaseType, projectDir) {
|
|
146
|
+
const source = await this.loadSourceFromConfig();
|
|
147
|
+
const sourceDir = await this.resolvePublishSourceDir(type, name, projectDir);
|
|
148
|
+
const history = await source.history(type, name);
|
|
149
|
+
const latest = history[0]?.version ?? "0.0.0";
|
|
150
|
+
const nextVersion = this.versions.nextVersion(latest, releaseType);
|
|
151
|
+
const result = await source.publish(type, name, nextVersion, sourceDir, {
|
|
152
|
+
releaseType,
|
|
153
|
+
});
|
|
154
|
+
const storePath = this.getStorePath(type, name, nextVersion);
|
|
155
|
+
if (!(await this.exists(storePath))) {
|
|
156
|
+
await source.pull(type, name, nextVersion, storePath);
|
|
157
|
+
}
|
|
158
|
+
const linkPath = this.getProjectResourcePath(projectDir, type, name);
|
|
159
|
+
if (await this.exists(linkPath)) {
|
|
160
|
+
await this.switchSymlink(storePath, linkPath);
|
|
161
|
+
}
|
|
162
|
+
if (await this.isResourceLocked(projectDir, type, name)) {
|
|
163
|
+
const sourceInfo = await this.getLockSourceInfo();
|
|
164
|
+
await this.lockStore.upsertResource(projectDir, sourceInfo, {
|
|
165
|
+
type,
|
|
166
|
+
name,
|
|
167
|
+
version: nextVersion,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return { type, name, version: result.version, tag: result.tag };
|
|
171
|
+
}
|
|
172
|
+
async create(type, name, options) {
|
|
173
|
+
this.validateCreateInput(type, name, options);
|
|
174
|
+
const source = await this.loadSourceFromConfig();
|
|
175
|
+
return source.create(type, name, {
|
|
176
|
+
description: options.description,
|
|
177
|
+
targets: options.targets,
|
|
178
|
+
entry: options.entry,
|
|
179
|
+
template: options.template ?? "basic",
|
|
180
|
+
force: options.force,
|
|
181
|
+
dryRun: options.dryRun,
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
async installFromLock(projectDir) {
|
|
185
|
+
const { lock, state } = await this.lockStore.loadWithState(projectDir);
|
|
186
|
+
if (state === "missing") {
|
|
187
|
+
throw new HimanError(errorCodes.LOCK_NOT_FOUND, `Lock file not found: ${this.lockStore.getLockPath(projectDir)}`);
|
|
188
|
+
}
|
|
189
|
+
if (state === "invalid" || !lock) {
|
|
190
|
+
throw new HimanError(errorCodes.LOCK_INVALID, `Lock file is invalid: ${this.lockStore.getLockPath(projectDir)}`);
|
|
191
|
+
}
|
|
192
|
+
if (lock.resources.length === 0) {
|
|
193
|
+
throw new HimanError(errorCodes.LOCK_NOT_FOUND, `Lock file has no resources: ${this.lockStore.getLockPath(projectDir)}`);
|
|
194
|
+
}
|
|
195
|
+
const results = [];
|
|
196
|
+
for (const item of lock.resources) {
|
|
197
|
+
const result = await this.install(item.type, item.name, item.version, projectDir);
|
|
198
|
+
results.push(result);
|
|
199
|
+
}
|
|
200
|
+
return results;
|
|
201
|
+
}
|
|
202
|
+
async loadSourceFromConfig() {
|
|
203
|
+
const config = await this.stateStore.loadConfig();
|
|
204
|
+
if (!config) {
|
|
205
|
+
throw new HimanError(errorCodes.CONFIG_NOT_FOUND, "Source config not found. Please run `himan init <git_repo>` first.");
|
|
206
|
+
}
|
|
207
|
+
const sourceConfig = this.buildSourceConfig(config.source.type, config.source.repo, config.source.repoId);
|
|
208
|
+
const source = this.createSource(config.source.type);
|
|
209
|
+
await source.init(sourceConfig);
|
|
210
|
+
return source;
|
|
211
|
+
}
|
|
212
|
+
createSource(type) {
|
|
213
|
+
return type === "registry"
|
|
214
|
+
? new RegistrySourceAdapter()
|
|
215
|
+
: new GitSourceAdapter();
|
|
216
|
+
}
|
|
217
|
+
async getLockSourceInfo() {
|
|
218
|
+
const config = await this.stateStore.loadConfig();
|
|
219
|
+
if (!config) {
|
|
220
|
+
throw new HimanError(errorCodes.CONFIG_NOT_FOUND, "Source config not found. Please run `himan init <git_repo>` first.");
|
|
221
|
+
}
|
|
222
|
+
return {
|
|
223
|
+
type: config.source.type,
|
|
224
|
+
repo: config.source.repo,
|
|
225
|
+
repoId: config.source.repoId,
|
|
226
|
+
};
|
|
227
|
+
}
|
|
228
|
+
async isResourceLocked(projectDir, type, name) {
|
|
229
|
+
const lock = await this.lockStore.load(projectDir);
|
|
230
|
+
if (!lock)
|
|
231
|
+
return false;
|
|
232
|
+
return lock.resources.some((item) => item.type === type && item.name === name);
|
|
233
|
+
}
|
|
234
|
+
buildSourceConfig(type, repo, repoId) {
|
|
235
|
+
if (type === "registry") {
|
|
236
|
+
return { type };
|
|
237
|
+
}
|
|
238
|
+
if (!repo) {
|
|
239
|
+
throw new HimanError(errorCodes.INVALID_INPUT, "Git repo is required for git source.");
|
|
240
|
+
}
|
|
241
|
+
const effectiveRepoId = repoId ?? toRepoId(repo);
|
|
242
|
+
return {
|
|
243
|
+
type,
|
|
244
|
+
repo,
|
|
245
|
+
repoId: effectiveRepoId,
|
|
246
|
+
repoDir: path.join(this.paths.getReposDir(), effectiveRepoId),
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
resolveVersion(history, version) {
|
|
250
|
+
if (!version)
|
|
251
|
+
return history[0].version;
|
|
252
|
+
const found = history.find((item) => item.version === version);
|
|
253
|
+
if (!found) {
|
|
254
|
+
throw new HimanError(errorCodes.VERSION_NOT_FOUND, `Version not found: ${version}`);
|
|
255
|
+
}
|
|
256
|
+
return found.version;
|
|
257
|
+
}
|
|
258
|
+
getStorePath(type, name, version) {
|
|
259
|
+
return path.join(this.paths.getStoreDir(), type, name, version);
|
|
260
|
+
}
|
|
261
|
+
getProjectResourcePath(projectDir, type, name) {
|
|
262
|
+
if (type === "rule")
|
|
263
|
+
return path.join(projectDir, ".cursor", "rules", name);
|
|
264
|
+
if (type === "command")
|
|
265
|
+
return path.join(projectDir, ".cursor", "commands", name);
|
|
266
|
+
return path.join(projectDir, ".cursor", "skills", name);
|
|
267
|
+
}
|
|
268
|
+
getProjectDevPath(projectDir, type, name) {
|
|
269
|
+
return path.join(projectDir, ".himan", "dev", type, name);
|
|
270
|
+
}
|
|
271
|
+
async switchSymlink(targetPath, linkPath) {
|
|
272
|
+
await fs.mkdir(path.dirname(linkPath), { recursive: true });
|
|
273
|
+
await fs.rm(linkPath, { recursive: true, force: true });
|
|
274
|
+
await fs.symlink(targetPath, linkPath, "dir");
|
|
275
|
+
}
|
|
276
|
+
async readInstalledPath(linkPath) {
|
|
277
|
+
if (!(await this.exists(linkPath))) {
|
|
278
|
+
throw new HimanError(errorCodes.INSTALL_NOT_FOUND, `Installed resource link not found: ${linkPath}. Run install first.`);
|
|
279
|
+
}
|
|
280
|
+
return fs.realpath(linkPath);
|
|
281
|
+
}
|
|
282
|
+
async exists(targetPath) {
|
|
283
|
+
try {
|
|
284
|
+
await fs.access(targetPath);
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
catch {
|
|
288
|
+
return false;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
async resolvePublishSourceDir(type, name, projectDir) {
|
|
292
|
+
const devPath = this.getProjectDevPath(projectDir, type, name);
|
|
293
|
+
if (await this.exists(devPath)) {
|
|
294
|
+
return devPath;
|
|
295
|
+
}
|
|
296
|
+
const repoResourceDir = await this.getRepoResourceDir(type, name);
|
|
297
|
+
if (await this.exists(repoResourceDir)) {
|
|
298
|
+
return repoResourceDir;
|
|
299
|
+
}
|
|
300
|
+
throw new HimanError(errorCodes.RESOURCE_NOT_FOUND, `No publish source found for ${type}/${name}. Create resource or switch to dev mode first.`);
|
|
301
|
+
}
|
|
302
|
+
async getRepoResourceDir(type, name) {
|
|
303
|
+
const config = await this.stateStore.loadConfig();
|
|
304
|
+
if (!config) {
|
|
305
|
+
throw new HimanError(errorCodes.CONFIG_NOT_FOUND, "Source config not found. Please run `himan init <git_repo>` first.");
|
|
306
|
+
}
|
|
307
|
+
const sourceConfig = this.buildSourceConfig(config.source.type, config.source.repo, config.source.repoId);
|
|
308
|
+
if (!sourceConfig.repoDir) {
|
|
309
|
+
throw new HimanError(errorCodes.INVALID_INPUT, "Current source does not support repo directory publish.");
|
|
310
|
+
}
|
|
311
|
+
return path.join(sourceConfig.repoDir, `${this.getTypeDir(type)}`, name);
|
|
312
|
+
}
|
|
313
|
+
getTypeDir(type) {
|
|
314
|
+
if (type === "rule")
|
|
315
|
+
return "rules";
|
|
316
|
+
if (type === "command")
|
|
317
|
+
return "commands";
|
|
318
|
+
return "skills";
|
|
319
|
+
}
|
|
320
|
+
validateCreateInput(type, name, options) {
|
|
321
|
+
if (!["rule", "command", "skill"].includes(type)) {
|
|
322
|
+
throw new HimanError(errorCodes.UNSUPPORTED_RESOURCE_TYPE, `Unsupported resource type for create: ${type}`);
|
|
323
|
+
}
|
|
324
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
|
|
325
|
+
throw new HimanError(errorCodes.INVALID_RESOURCE_NAME, `Invalid resource name: ${name}. Use kebab-case only.`);
|
|
326
|
+
}
|
|
327
|
+
if (options.template && options.template !== "basic") {
|
|
328
|
+
throw new HimanError(errorCodes.TEMPLATE_NOT_FOUND, `Template not found: ${options.template}`);
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
}
|