@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.
@@ -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 { ProjectLockStore } from "../state/project-lock-store.js";
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 list(type) {
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
- return source.list(type);
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.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 };
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 linkPath = this.getProjectResourcePath(projectDir, type, name);
127
- const installedPath = await this.readInstalledPath(linkPath);
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
- await this.switchSymlink(devPath, linkPath);
134
- return { type, name, devPath, linkPath };
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 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}.`);
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 linkPath = this.getProjectResourcePath(projectDir, type, name);
159
- if (await this.exists(linkPath)) {
160
- await this.switchSymlink(storePath, linkPath);
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 (await this.isResourceLocked(projectDir, type, name)) {
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
- targets: options.targets,
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.install(item.type, item.name, item.version, projectDir);
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 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;
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 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.");
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
- type: config.source.type,
224
- repo: config.source.repo,
225
- repoId: config.source.repoId,
341
+ ...sourceInfo,
342
+ repoId: sourceInfo.repoId ?? toRepoId(sourceInfo.repo),
226
343
  };
227
344
  }
228
- async isResourceLocked(projectDir, type, name) {
345
+ async getLockedResource(projectDir, type, name) {
229
346
  const lock = await this.lockStore.load(projectDir);
230
347
  if (!lock)
231
- return false;
232
- return lock.resources.some((item) => item.type === type && item.name === name);
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 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");
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 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.`);
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
- return fs.realpath(linkPath);
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, baseDirMtimeMs, resources) {
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.baseDirMtimeMs = baseDirMtimeMs;
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
- baseDirMtimeMs,
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
- throw new Error("Invalid config: source is required.");
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
+ }
@@ -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, "../package.json");
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");