@hi-man/himan 0.1.0 → 0.2.2
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 +65 -0
- package/README.md +75 -66
- package/dist/adapters/git/repo-manager.js +41 -6
- package/dist/adapters/resource/resource-scanner.js +3 -1
- package/dist/adapters/source/git-source-adapter.js +103 -17
- package/dist/bin/himan-project.js +7 -0
- package/dist/bin/himan-resource.js +7 -0
- package/dist/bin/himan-source.js +7 -0
- package/dist/bin/himan.js +7 -0
- package/dist/{index.js → bin/shared.js} +3 -5
- package/dist/cli/agent-commands.js +93 -0
- package/dist/cli/builders.js +72 -0
- package/dist/cli/index.js +3 -318
- package/dist/cli/project-commands.js +127 -0
- package/dist/cli/resource-commands.js +104 -0
- package/dist/cli/shared.js +69 -0
- package/dist/cli/source-commands.js +58 -0
- package/dist/services/index.js +261 -70
- package/dist/state/index-cache-store.js +4 -3
- package/dist/state/project-config-store.js +43 -0
- package/dist/state/project-lock-store.js +4 -0
- package/dist/state/state-store.js +5 -1
- package/dist/utils/agent-configs.js +71 -0
- package/dist/utils/errors.js +2 -0
- package/dist/{version.js → utils/version.js} +1 -1
- 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 +129 -0
- package/docs/mvp/impl.md +111 -0
- package/package.json +31 -7
package/dist/services/index.js
CHANGED
|
@@ -1,20 +1,25 @@
|
|
|
1
1
|
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
|
-
import {
|
|
4
|
+
import { ProjectConfigStore } from "../state/project-config-store.js";
|
|
5
|
+
import { ProjectLockStore, } from "../state/project-lock-store.js";
|
|
5
6
|
import { PathResolver } from "../utils/path-resolver.js";
|
|
6
7
|
import { toRepoId } from "../utils/repo-id.js";
|
|
7
8
|
import { HimanError, errorCodes } from "../utils/errors.js";
|
|
9
|
+
import { getProjectResourcePaths, getSupportedAgentNames, normalizeAgents, } from "../utils/agent-configs.js";
|
|
8
10
|
import path from "node:path";
|
|
9
11
|
import { promises as fs } from "node:fs";
|
|
10
12
|
import { VersionResolver } from "../adapters/version/version-resolver.js";
|
|
13
|
+
import YAML from "yaml";
|
|
11
14
|
export class ServiceFactory {
|
|
12
15
|
stateStore = new StateStore();
|
|
16
|
+
projectConfigStore = new ProjectConfigStore();
|
|
13
17
|
lockStore = new ProjectLockStore();
|
|
14
18
|
paths = new PathResolver();
|
|
15
19
|
versions = new VersionResolver();
|
|
16
20
|
async initSource(type, repo) {
|
|
17
21
|
await this.stateStore.ensureBaseDirs();
|
|
22
|
+
const current = await this.stateStore.loadConfig();
|
|
18
23
|
const sourceConfig = this.buildSourceConfig(type, repo);
|
|
19
24
|
const source = this.createSource(type);
|
|
20
25
|
await source.init(sourceConfig);
|
|
@@ -29,6 +34,7 @@ export class ServiceFactory {
|
|
|
29
34
|
default: "default",
|
|
30
35
|
items: { default: stateSource },
|
|
31
36
|
},
|
|
37
|
+
agents: current?.agents,
|
|
32
38
|
});
|
|
33
39
|
return {
|
|
34
40
|
sourceType: type,
|
|
@@ -60,6 +66,7 @@ export class ServiceFactory {
|
|
|
60
66
|
default: defaultName,
|
|
61
67
|
items,
|
|
62
68
|
},
|
|
69
|
+
agents: current?.agents,
|
|
63
70
|
});
|
|
64
71
|
return { name, type, repo: sourceConfig.repo, repoId: sourceConfig.repoId };
|
|
65
72
|
}
|
|
@@ -78,6 +85,7 @@ export class ServiceFactory {
|
|
|
78
85
|
default: name,
|
|
79
86
|
items: config.sources.items,
|
|
80
87
|
},
|
|
88
|
+
agents: config.agents,
|
|
81
89
|
});
|
|
82
90
|
return { name };
|
|
83
91
|
}
|
|
@@ -93,54 +101,95 @@ export class ServiceFactory {
|
|
|
93
101
|
isDefault: name === config.sources?.default,
|
|
94
102
|
}));
|
|
95
103
|
}
|
|
96
|
-
async
|
|
104
|
+
async setAgents(agents, scope, projectDir) {
|
|
105
|
+
const normalized = normalizeAgents(agents);
|
|
106
|
+
if (scope === "project") {
|
|
107
|
+
await this.projectConfigStore.saveAgents(projectDir, normalized);
|
|
108
|
+
return { scope, agents: normalized };
|
|
109
|
+
}
|
|
110
|
+
await this.stateStore.ensureBaseDirs();
|
|
111
|
+
const current = await this.stateStore.loadConfig();
|
|
112
|
+
await this.stateStore.saveConfig({
|
|
113
|
+
...(current ?? {}),
|
|
114
|
+
agents: normalized,
|
|
115
|
+
});
|
|
116
|
+
return { scope, agents: normalized };
|
|
117
|
+
}
|
|
118
|
+
async getAgentSettings(projectDir) {
|
|
119
|
+
const [globalConfig, projectConfig] = await Promise.all([
|
|
120
|
+
this.stateStore.loadConfig(),
|
|
121
|
+
this.projectConfigStore.load(projectDir),
|
|
122
|
+
]);
|
|
123
|
+
const global = globalConfig?.agents?.length
|
|
124
|
+
? normalizeAgents(globalConfig.agents)
|
|
125
|
+
: undefined;
|
|
126
|
+
const project = projectConfig?.agents?.length
|
|
127
|
+
? normalizeAgents(projectConfig.agents)
|
|
128
|
+
: undefined;
|
|
129
|
+
return {
|
|
130
|
+
global,
|
|
131
|
+
project,
|
|
132
|
+
effective: project ?? global ?? normalizeAgents(),
|
|
133
|
+
supported: getSupportedAgentNames(),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
async clearAgents(scope, projectDir) {
|
|
137
|
+
if (scope === "project") {
|
|
138
|
+
await this.projectConfigStore.clearAgents(projectDir);
|
|
139
|
+
return { scope };
|
|
140
|
+
}
|
|
141
|
+
const current = await this.stateStore.loadConfig();
|
|
142
|
+
await this.stateStore.saveConfig({
|
|
143
|
+
...(current ?? {}),
|
|
144
|
+
agents: undefined,
|
|
145
|
+
});
|
|
146
|
+
return { scope };
|
|
147
|
+
}
|
|
148
|
+
async list(type, agents) {
|
|
97
149
|
const source = await this.loadSourceFromConfig();
|
|
98
|
-
|
|
150
|
+
const resources = await source.list(type);
|
|
151
|
+
if (!agents?.length)
|
|
152
|
+
return resources;
|
|
153
|
+
const selected = normalizeAgents(agents);
|
|
154
|
+
return resources.filter((resource) => normalizeAgents(resource.agents).some((agent) => selected.includes(agent)));
|
|
99
155
|
}
|
|
100
156
|
async history(type, name) {
|
|
101
157
|
const source = await this.loadSourceFromConfig();
|
|
102
158
|
return source.history(type, name);
|
|
103
159
|
}
|
|
104
|
-
async install(type, name, version, projectDir) {
|
|
105
|
-
const source = await this.
|
|
106
|
-
|
|
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 };
|
|
160
|
+
async install(type, name, version, projectDir, agents, mode = "link") {
|
|
161
|
+
const { source, sourceInfo } = await this.loadSourceWithInfoFromConfig();
|
|
162
|
+
return this.installWithSource(source, sourceInfo, type, name, version, projectDir, agents, mode);
|
|
124
163
|
}
|
|
125
164
|
async dev(type, name, projectDir) {
|
|
126
|
-
const
|
|
127
|
-
const installedPath =
|
|
165
|
+
const installInfo = await this.resolveInstalledResource(projectDir, type, name);
|
|
166
|
+
const installedPath = installInfo.installedPath;
|
|
128
167
|
const devPath = this.getProjectDevPath(projectDir, type, name);
|
|
129
168
|
if (!(await this.exists(devPath))) {
|
|
130
169
|
await fs.mkdir(path.dirname(devPath), { recursive: true });
|
|
131
170
|
await fs.cp(installedPath, devPath, { recursive: true });
|
|
132
171
|
}
|
|
133
|
-
|
|
134
|
-
|
|
172
|
+
for (const linkPath of installInfo.linkPaths) {
|
|
173
|
+
await this.materializeResource(devPath, linkPath, installInfo.mode);
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
type,
|
|
177
|
+
name,
|
|
178
|
+
devPath,
|
|
179
|
+
linkPath: installInfo.linkPaths[0],
|
|
180
|
+
mode: installInfo.mode,
|
|
181
|
+
};
|
|
135
182
|
}
|
|
136
183
|
async uninstall(type, name, projectDir) {
|
|
137
|
-
const
|
|
138
|
-
if (
|
|
139
|
-
throw new HimanError(errorCodes.INSTALL_NOT_FOUND, `Installed resource link not found
|
|
184
|
+
const installInfo = await this.resolveInstalledResource(projectDir, type, name);
|
|
185
|
+
if (installInfo.linkPaths.length === 0) {
|
|
186
|
+
throw new HimanError(errorCodes.INSTALL_NOT_FOUND, `Installed resource link not found for ${type}/${name}.`);
|
|
187
|
+
}
|
|
188
|
+
for (const linkPath of installInfo.linkPaths) {
|
|
189
|
+
await fs.rm(linkPath, { recursive: true, force: true });
|
|
140
190
|
}
|
|
141
|
-
await fs.rm(linkPath, { recursive: true, force: true });
|
|
142
191
|
await this.lockStore.removeResource(projectDir, { type, name });
|
|
143
|
-
return { type, name, linkPath };
|
|
192
|
+
return { type, name, linkPath: installInfo.linkPaths[0] };
|
|
144
193
|
}
|
|
145
194
|
async publish(type, name, releaseType, projectDir) {
|
|
146
195
|
const source = await this.loadSourceFromConfig();
|
|
@@ -155,33 +204,43 @@ export class ServiceFactory {
|
|
|
155
204
|
if (!(await this.exists(storePath))) {
|
|
156
205
|
await source.pull(type, name, nextVersion, storePath);
|
|
157
206
|
}
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
207
|
+
const agentsFromMeta = normalizeAgents((await this.readResourceMetaFromDir(storePath))?.agents);
|
|
208
|
+
const locked = await this.getLockedResource(projectDir, type, name);
|
|
209
|
+
const nextAgents = locked?.agents?.length
|
|
210
|
+
? normalizeAgents(locked.agents)
|
|
211
|
+
: agentsFromMeta;
|
|
212
|
+
const installMode = this.resolveInstallMode(locked?.mode);
|
|
213
|
+
const linkPaths = getProjectResourcePaths(projectDir, type, name, nextAgents);
|
|
214
|
+
for (const linkPath of linkPaths) {
|
|
215
|
+
if (await this.exists(linkPath)) {
|
|
216
|
+
await this.materializeResource(storePath, linkPath, installMode);
|
|
217
|
+
}
|
|
161
218
|
}
|
|
162
|
-
if (
|
|
219
|
+
if (locked) {
|
|
163
220
|
const sourceInfo = await this.getLockSourceInfo();
|
|
164
221
|
await this.lockStore.upsertResource(projectDir, sourceInfo, {
|
|
165
222
|
type,
|
|
166
223
|
name,
|
|
167
224
|
version: nextVersion,
|
|
225
|
+
agents: nextAgents,
|
|
226
|
+
mode: installMode,
|
|
168
227
|
});
|
|
169
228
|
}
|
|
170
229
|
return { type, name, version: result.version, tag: result.tag };
|
|
171
230
|
}
|
|
172
|
-
async create(type, name, options) {
|
|
231
|
+
async create(type, name, options, projectDir) {
|
|
173
232
|
this.validateCreateInput(type, name, options);
|
|
174
233
|
const source = await this.loadSourceFromConfig();
|
|
175
234
|
return source.create(type, name, {
|
|
176
235
|
description: options.description,
|
|
177
|
-
|
|
236
|
+
agents: await this.resolveEffectiveAgents(projectDir, options.agents),
|
|
178
237
|
entry: options.entry,
|
|
179
238
|
template: options.template ?? "basic",
|
|
180
239
|
force: options.force,
|
|
181
240
|
dryRun: options.dryRun,
|
|
182
241
|
});
|
|
183
242
|
}
|
|
184
|
-
async installFromLock(projectDir) {
|
|
243
|
+
async installFromLock(projectDir, agents, mode) {
|
|
185
244
|
const { lock, state } = await this.lockStore.loadWithState(projectDir);
|
|
186
245
|
if (state === "missing") {
|
|
187
246
|
throw new HimanError(errorCodes.LOCK_NOT_FOUND, `Lock file not found: ${this.lockStore.getLockPath(projectDir)}`);
|
|
@@ -193,21 +252,69 @@ export class ServiceFactory {
|
|
|
193
252
|
throw new HimanError(errorCodes.LOCK_NOT_FOUND, `Lock file has no resources: ${this.lockStore.getLockPath(projectDir)}`);
|
|
194
253
|
}
|
|
195
254
|
const results = [];
|
|
255
|
+
const lockSourceInfo = this.normalizeLockSourceInfo(lock.source);
|
|
256
|
+
const lockedSource = await this.loadSourceFromLock(lockSourceInfo);
|
|
196
257
|
for (const item of lock.resources) {
|
|
197
|
-
const result = await this.
|
|
258
|
+
const result = await this.installWithSource(lockedSource, lockSourceInfo, item.type, item.name, item.version, projectDir, agents ?? item.agents, mode ?? this.resolveInstallMode(item.mode));
|
|
198
259
|
results.push(result);
|
|
199
260
|
}
|
|
200
261
|
return results;
|
|
201
262
|
}
|
|
263
|
+
async installWithSource(source, sourceInfo, type, name, version, projectDir, agents, mode) {
|
|
264
|
+
const history = await source.history(type, name);
|
|
265
|
+
if (history.length === 0) {
|
|
266
|
+
throw new HimanError(errorCodes.RESOURCE_NOT_FOUND, `Resource not found: ${type}/${name}`);
|
|
267
|
+
}
|
|
268
|
+
const resolvedVersion = this.resolveVersion(history, version);
|
|
269
|
+
const storePath = this.getStorePath(type, name, resolvedVersion);
|
|
270
|
+
if (!(await this.exists(storePath))) {
|
|
271
|
+
await source.pull(type, name, resolvedVersion, storePath);
|
|
272
|
+
}
|
|
273
|
+
const resourceMeta = await this.readResourceMetaFromDir(storePath);
|
|
274
|
+
const effectiveTargets = await this.resolveEffectiveAgents(projectDir, agents, resourceMeta?.agents);
|
|
275
|
+
const linkPaths = getProjectResourcePaths(projectDir, type, name, effectiveTargets);
|
|
276
|
+
for (const linkPath of linkPaths) {
|
|
277
|
+
await this.materializeResource(storePath, linkPath, mode);
|
|
278
|
+
}
|
|
279
|
+
await this.lockStore.upsertResource(projectDir, sourceInfo, {
|
|
280
|
+
type,
|
|
281
|
+
name,
|
|
282
|
+
version: resolvedVersion,
|
|
283
|
+
agents: effectiveTargets,
|
|
284
|
+
mode,
|
|
285
|
+
});
|
|
286
|
+
return { type, name, version: resolvedVersion, linkPath: linkPaths[0], mode };
|
|
287
|
+
}
|
|
202
288
|
async loadSourceFromConfig() {
|
|
289
|
+
return (await this.loadSourceWithInfoFromConfig()).source;
|
|
290
|
+
}
|
|
291
|
+
async loadSourceWithInfoFromConfig() {
|
|
292
|
+
const { name, source: stateSource } = await this.getCurrentSourceState();
|
|
293
|
+
const sourceInfo = this.toLockSourceInfo(stateSource, name);
|
|
294
|
+
const source = await this.loadSourceFromInfo(sourceInfo);
|
|
295
|
+
return {
|
|
296
|
+
source,
|
|
297
|
+
sourceInfo,
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
async loadSourceFromLock(sourceInfo) {
|
|
301
|
+
return this.loadSourceFromInfo(sourceInfo);
|
|
302
|
+
}
|
|
303
|
+
async loadSourceFromInfo(sourceInfo) {
|
|
304
|
+
const normalizedSourceInfo = this.normalizeLockSourceInfo(sourceInfo);
|
|
305
|
+
const sourceConfig = this.buildSourceConfig(normalizedSourceInfo.type, normalizedSourceInfo.repo, normalizedSourceInfo.repoId);
|
|
306
|
+
const source = this.createSource(normalizedSourceInfo.type);
|
|
307
|
+
await source.init(sourceConfig);
|
|
308
|
+
return source;
|
|
309
|
+
}
|
|
310
|
+
async getCurrentSourceState() {
|
|
203
311
|
const config = await this.stateStore.loadConfig();
|
|
204
|
-
if (!config) {
|
|
312
|
+
if (!config?.source) {
|
|
205
313
|
throw new HimanError(errorCodes.CONFIG_NOT_FOUND, "Source config not found. Please run `himan init <git_repo>` first.");
|
|
206
314
|
}
|
|
207
|
-
const
|
|
208
|
-
const
|
|
209
|
-
|
|
210
|
-
return source;
|
|
315
|
+
const currentName = config.sources?.default ?? "default";
|
|
316
|
+
const currentSource = config.sources?.items[currentName] ?? config.source;
|
|
317
|
+
return { name: currentName, source: currentSource };
|
|
211
318
|
}
|
|
212
319
|
createSource(type) {
|
|
213
320
|
return type === "registry"
|
|
@@ -215,21 +322,31 @@ export class ServiceFactory {
|
|
|
215
322
|
: new GitSourceAdapter();
|
|
216
323
|
}
|
|
217
324
|
async getLockSourceInfo() {
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
|
|
325
|
+
const { name, source } = await this.getCurrentSourceState();
|
|
326
|
+
return this.toLockSourceInfo(source, name);
|
|
327
|
+
}
|
|
328
|
+
toLockSourceInfo(source, name) {
|
|
329
|
+
return this.normalizeLockSourceInfo({
|
|
330
|
+
name,
|
|
331
|
+
type: source.type,
|
|
332
|
+
repo: source.repo,
|
|
333
|
+
repoId: source.repoId,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
normalizeLockSourceInfo(sourceInfo) {
|
|
337
|
+
if (sourceInfo.type !== "git" || !sourceInfo.repo) {
|
|
338
|
+
return sourceInfo;
|
|
221
339
|
}
|
|
222
340
|
return {
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
repoId: config.source.repoId,
|
|
341
|
+
...sourceInfo,
|
|
342
|
+
repoId: sourceInfo.repoId ?? toRepoId(sourceInfo.repo),
|
|
226
343
|
};
|
|
227
344
|
}
|
|
228
|
-
async
|
|
345
|
+
async getLockedResource(projectDir, type, name) {
|
|
229
346
|
const lock = await this.lockStore.load(projectDir);
|
|
230
347
|
if (!lock)
|
|
231
|
-
return
|
|
232
|
-
return lock.resources.
|
|
348
|
+
return undefined;
|
|
349
|
+
return lock.resources.find((item) => item.type === type && item.name === name);
|
|
233
350
|
}
|
|
234
351
|
buildSourceConfig(type, repo, repoId) {
|
|
235
352
|
if (type === "registry") {
|
|
@@ -258,26 +375,100 @@ export class ServiceFactory {
|
|
|
258
375
|
getStorePath(type, name, version) {
|
|
259
376
|
return path.join(this.paths.getStoreDir(), type, name, version);
|
|
260
377
|
}
|
|
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
378
|
getProjectDevPath(projectDir, type, name) {
|
|
269
379
|
return path.join(projectDir, ".himan", "dev", type, name);
|
|
270
380
|
}
|
|
271
|
-
async
|
|
272
|
-
await fs.mkdir(path.dirname(
|
|
273
|
-
await fs.rm(
|
|
274
|
-
|
|
381
|
+
async materializeResource(sourcePath, targetPath, mode) {
|
|
382
|
+
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
383
|
+
await fs.rm(targetPath, { recursive: true, force: true });
|
|
384
|
+
if (mode === "copy") {
|
|
385
|
+
await fs.cp(sourcePath, targetPath, { recursive: true });
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
await fs.symlink(sourcePath, targetPath, "dir");
|
|
389
|
+
}
|
|
390
|
+
resolveInstallMode(mode) {
|
|
391
|
+
return mode === "copy" ? "copy" : "link";
|
|
392
|
+
}
|
|
393
|
+
async resolveInstalledResource(projectDir, type, name) {
|
|
394
|
+
const locked = await this.getLockedResource(projectDir, type, name);
|
|
395
|
+
const configuredAgents = await this.getConfiguredAgents(projectDir);
|
|
396
|
+
const lockedTargets = locked?.agents?.length
|
|
397
|
+
? normalizeAgents(locked.agents)
|
|
398
|
+
: configuredAgents ?? normalizeAgents();
|
|
399
|
+
const expectedFromLock = getProjectResourcePaths(projectDir, type, name, lockedTargets);
|
|
400
|
+
const existingFromLock = [];
|
|
401
|
+
for (const candidate of expectedFromLock) {
|
|
402
|
+
if (await this.exists(candidate))
|
|
403
|
+
existingFromLock.push(candidate);
|
|
404
|
+
}
|
|
405
|
+
if (existingFromLock.length > 0) {
|
|
406
|
+
const installedPath = await fs.realpath(existingFromLock[0]);
|
|
407
|
+
return {
|
|
408
|
+
installedPath,
|
|
409
|
+
agents: lockedTargets,
|
|
410
|
+
linkPaths: getProjectResourcePaths(projectDir, type, name, lockedTargets),
|
|
411
|
+
mode: this.resolveInstallMode(locked?.mode),
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
const allCandidates = getSupportedAgentNames().map((agent) => ({
|
|
415
|
+
agent,
|
|
416
|
+
path: getProjectResourcePaths(projectDir, type, name, [agent])[0],
|
|
417
|
+
}));
|
|
418
|
+
const existingCandidates = [];
|
|
419
|
+
for (const candidate of allCandidates) {
|
|
420
|
+
if (await this.exists(candidate.path))
|
|
421
|
+
existingCandidates.push(candidate);
|
|
422
|
+
}
|
|
423
|
+
if (existingCandidates.length === 0) {
|
|
424
|
+
throw new HimanError(errorCodes.INSTALL_NOT_FOUND, `Installed resource link not found for ${type}/${name}. Run install first.`);
|
|
425
|
+
}
|
|
426
|
+
const installedPath = await fs.realpath(existingCandidates[0].path);
|
|
427
|
+
const resourceMeta = await this.readResourceMetaFromDir(installedPath);
|
|
428
|
+
const agentsFromMeta = resourceMeta?.agents?.length
|
|
429
|
+
? normalizeAgents(resourceMeta.agents)
|
|
430
|
+
: undefined;
|
|
431
|
+
const existingAgents = normalizeAgents(existingCandidates.map((candidate) => candidate.agent));
|
|
432
|
+
const effectiveAgents = configuredAgents ?? agentsFromMeta ?? existingAgents;
|
|
433
|
+
return {
|
|
434
|
+
installedPath,
|
|
435
|
+
agents: effectiveAgents,
|
|
436
|
+
linkPaths: getProjectResourcePaths(projectDir, type, name, effectiveAgents),
|
|
437
|
+
mode: "link",
|
|
438
|
+
};
|
|
439
|
+
}
|
|
440
|
+
async resolveEffectiveAgents(projectDir, explicitAgents, fallbackAgents) {
|
|
441
|
+
if (explicitAgents?.length) {
|
|
442
|
+
return normalizeAgents(explicitAgents);
|
|
443
|
+
}
|
|
444
|
+
const configuredAgents = await this.getConfiguredAgents(projectDir);
|
|
445
|
+
if (configuredAgents?.length) {
|
|
446
|
+
return configuredAgents;
|
|
447
|
+
}
|
|
448
|
+
return normalizeAgents(fallbackAgents);
|
|
275
449
|
}
|
|
276
|
-
async
|
|
277
|
-
|
|
278
|
-
|
|
450
|
+
async getConfiguredAgents(projectDir) {
|
|
451
|
+
const [globalConfig, projectConfig] = await Promise.all([
|
|
452
|
+
this.stateStore.loadConfig(),
|
|
453
|
+
this.projectConfigStore.load(projectDir),
|
|
454
|
+
]);
|
|
455
|
+
if (projectConfig?.agents?.length) {
|
|
456
|
+
return normalizeAgents(projectConfig.agents);
|
|
279
457
|
}
|
|
280
|
-
|
|
458
|
+
if (globalConfig?.agents?.length) {
|
|
459
|
+
return normalizeAgents(globalConfig.agents);
|
|
460
|
+
}
|
|
461
|
+
return undefined;
|
|
462
|
+
}
|
|
463
|
+
async readResourceMetaFromDir(resourceDir) {
|
|
464
|
+
const yamlPath = path.join(resourceDir, "himan.yaml");
|
|
465
|
+
if (!(await this.exists(yamlPath)))
|
|
466
|
+
return null;
|
|
467
|
+
const raw = await fs.readFile(yamlPath, "utf8");
|
|
468
|
+
const parsed = YAML.parse(raw) ?? null;
|
|
469
|
+
if (!parsed)
|
|
470
|
+
return null;
|
|
471
|
+
return { agents: parsed.agents ?? parsed.targets };
|
|
281
472
|
}
|
|
282
473
|
async exists(targetPath) {
|
|
283
474
|
try {
|
|
@@ -301,7 +492,7 @@ export class ServiceFactory {
|
|
|
301
492
|
}
|
|
302
493
|
async getRepoResourceDir(type, name) {
|
|
303
494
|
const config = await this.stateStore.loadConfig();
|
|
304
|
-
if (!config) {
|
|
495
|
+
if (!config?.source) {
|
|
305
496
|
throw new HimanError(errorCodes.CONFIG_NOT_FOUND, "Source config not found. Please run `himan init <git_repo>` first.");
|
|
306
497
|
}
|
|
307
498
|
const sourceConfig = this.buildSourceConfig(config.source.type, config.source.repo, config.source.repoId);
|
|
@@ -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
|
});
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { promises as fs } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
export class ProjectConfigStore {
|
|
4
|
+
getConfigPath(projectDir) {
|
|
5
|
+
return path.join(projectDir, ".himan", "config.json");
|
|
6
|
+
}
|
|
7
|
+
async load(projectDir) {
|
|
8
|
+
try {
|
|
9
|
+
const raw = await fs.readFile(this.getConfigPath(projectDir), "utf8");
|
|
10
|
+
const parsed = JSON.parse(raw);
|
|
11
|
+
if (parsed.version !== 1)
|
|
12
|
+
return null;
|
|
13
|
+
return parsed;
|
|
14
|
+
}
|
|
15
|
+
catch {
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
async saveAgents(projectDir, agents) {
|
|
20
|
+
const now = new Date().toISOString();
|
|
21
|
+
const existing = await this.load(projectDir);
|
|
22
|
+
const config = {
|
|
23
|
+
version: 1,
|
|
24
|
+
...existing,
|
|
25
|
+
agents,
|
|
26
|
+
updatedAt: now,
|
|
27
|
+
};
|
|
28
|
+
await fs.mkdir(path.dirname(this.getConfigPath(projectDir)), { recursive: true });
|
|
29
|
+
await fs.writeFile(this.getConfigPath(projectDir), JSON.stringify(config, null, 2), "utf8");
|
|
30
|
+
return config;
|
|
31
|
+
}
|
|
32
|
+
async clearAgents(projectDir) {
|
|
33
|
+
const existing = await this.load(projectDir);
|
|
34
|
+
if (!existing)
|
|
35
|
+
return;
|
|
36
|
+
const config = {
|
|
37
|
+
...existing,
|
|
38
|
+
agents: undefined,
|
|
39
|
+
updatedAt: new Date().toISOString(),
|
|
40
|
+
};
|
|
41
|
+
await fs.writeFile(this.getConfigPath(projectDir), JSON.stringify(config, null, 2), "utf8");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -40,6 +40,8 @@ export class ProjectLockStore {
|
|
|
40
40
|
const found = lock.resources.find((item) => item.type === resource.type && item.name === resource.name);
|
|
41
41
|
if (found) {
|
|
42
42
|
found.version = resource.version;
|
|
43
|
+
found.agents = resource.agents;
|
|
44
|
+
found.mode = resource.mode;
|
|
43
45
|
found.updatedAt = now;
|
|
44
46
|
}
|
|
45
47
|
else {
|
|
@@ -47,6 +49,8 @@ export class ProjectLockStore {
|
|
|
47
49
|
type: resource.type,
|
|
48
50
|
name: resource.name,
|
|
49
51
|
version: resource.version,
|
|
52
|
+
agents: resource.agents,
|
|
53
|
+
mode: resource.mode,
|
|
50
54
|
updatedAt: now,
|
|
51
55
|
});
|
|
52
56
|
}
|
|
@@ -35,12 +35,15 @@ export class StateStore {
|
|
|
35
35
|
default: defaultName,
|
|
36
36
|
items: input.sources.items,
|
|
37
37
|
},
|
|
38
|
+
agents: input.agents,
|
|
38
39
|
};
|
|
39
40
|
}
|
|
40
41
|
}
|
|
41
42
|
const fallback = input.source;
|
|
42
43
|
if (!fallback) {
|
|
43
|
-
|
|
44
|
+
return {
|
|
45
|
+
agents: input.agents,
|
|
46
|
+
};
|
|
44
47
|
}
|
|
45
48
|
return {
|
|
46
49
|
source: fallback,
|
|
@@ -50,6 +53,7 @@ export class StateStore {
|
|
|
50
53
|
default: fallback,
|
|
51
54
|
},
|
|
52
55
|
},
|
|
56
|
+
agents: input.agents,
|
|
53
57
|
};
|
|
54
58
|
}
|
|
55
59
|
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
const AGENT_CONFIGS = [
|
|
3
|
+
{
|
|
4
|
+
name: "cursor",
|
|
5
|
+
aliases: ["cursor"],
|
|
6
|
+
baseDir: ".cursor",
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
name: "claude-code",
|
|
10
|
+
aliases: ["claude", "claude-code", "claude code", "claude_code"],
|
|
11
|
+
baseDir: ".claude",
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
name: "codex",
|
|
15
|
+
aliases: ["codex"],
|
|
16
|
+
baseDir: ".agents",
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
name: "openclaw",
|
|
20
|
+
aliases: ["openclaw", "open-claw", "open claw"],
|
|
21
|
+
baseDir: ".openclaw",
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
const DEFAULT_AGENT = "cursor";
|
|
25
|
+
const AGENT_ALIASES = buildAgentAliases();
|
|
26
|
+
function getTypeDir(type) {
|
|
27
|
+
if (type === "rule")
|
|
28
|
+
return "rules";
|
|
29
|
+
if (type === "command")
|
|
30
|
+
return "commands";
|
|
31
|
+
return "skills";
|
|
32
|
+
}
|
|
33
|
+
function getAgentBaseDir(agent) {
|
|
34
|
+
return getAgentConfig(agent).baseDir;
|
|
35
|
+
}
|
|
36
|
+
export function normalizeAgents(agents) {
|
|
37
|
+
const normalized = (agents ?? [])
|
|
38
|
+
.map((item) => normalizeAgent(item))
|
|
39
|
+
.filter((item) => Boolean(item));
|
|
40
|
+
if (normalized.length === 0) {
|
|
41
|
+
return [DEFAULT_AGENT];
|
|
42
|
+
}
|
|
43
|
+
return [...new Set(normalized)];
|
|
44
|
+
}
|
|
45
|
+
export function normalizeAgent(input) {
|
|
46
|
+
return AGENT_ALIASES.get(input.trim().toLowerCase());
|
|
47
|
+
}
|
|
48
|
+
export function getProjectResourcePaths(projectDir, type, name, agents) {
|
|
49
|
+
const typeDir = getTypeDir(type);
|
|
50
|
+
return normalizeAgents(agents).map((agent) => path.join(projectDir, getAgentBaseDir(agent), typeDir, name));
|
|
51
|
+
}
|
|
52
|
+
export function getSupportedAgentNames() {
|
|
53
|
+
return AGENT_CONFIGS.map((config) => config.name);
|
|
54
|
+
}
|
|
55
|
+
function getAgentConfig(agent) {
|
|
56
|
+
const config = AGENT_CONFIGS.find((item) => item.name === agent);
|
|
57
|
+
if (!config) {
|
|
58
|
+
throw new Error(`Unsupported agent config: ${agent}`);
|
|
59
|
+
}
|
|
60
|
+
return config;
|
|
61
|
+
}
|
|
62
|
+
function buildAgentAliases() {
|
|
63
|
+
const aliases = new Map();
|
|
64
|
+
for (const config of AGENT_CONFIGS) {
|
|
65
|
+
aliases.set(config.name, config.name);
|
|
66
|
+
for (const alias of config.aliases) {
|
|
67
|
+
aliases.set(alias, config.name);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return aliases;
|
|
71
|
+
}
|
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
|
};
|
|
@@ -2,7 +2,7 @@ import { readFileSync } from "node:fs";
|
|
|
2
2
|
import { dirname, join } from "node:path";
|
|
3
3
|
import { fileURLToPath } from "node:url";
|
|
4
4
|
const moduleDir = dirname(fileURLToPath(import.meta.url));
|
|
5
|
-
const pkgPath = join(moduleDir, "
|
|
5
|
+
const pkgPath = join(moduleDir, "../../package.json");
|
|
6
6
|
const parsed = JSON.parse(readFileSync(pkgPath, "utf8"));
|
|
7
7
|
if (typeof parsed.version !== "string" || parsed.version.length === 0) {
|
|
8
8
|
throw new Error("Invalid or missing version in package.json");
|