@hi-man/himan 0.3.5 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +75 -22
- package/dist/adapters/git/repo-manager.js +246 -0
- package/dist/adapters/resource/resource-analysis.js +42 -0
- package/dist/adapters/source/git-source-adapter.js +188 -9
- package/dist/adapters/source/registry-source-adapter.js +3 -0
- package/dist/cli/builders.js +8 -3
- package/dist/cli/doctor-command.js +30 -0
- package/dist/cli/project-commands.js +17 -3
- package/dist/cli/resource-commands.js +23 -0
- package/dist/cli/source-commands.js +170 -3
- package/dist/domain/doctor.js +1 -0
- package/dist/domain/source-transfer.js +1 -0
- package/dist/services/index.js +665 -36
- package/dist/state/project-lock-store.js +19 -0
- package/docs/mvp/README.md +6 -9
- package/docs/mvp/create-resource.md +11 -11
- package/docs/mvp/impl.md +6 -6
- package/package.json +1 -1
package/dist/services/index.js
CHANGED
|
@@ -8,9 +8,13 @@ import { toRepoId } from "../utils/repo-id.js";
|
|
|
8
8
|
import { HimanError, errorCodes } from "../utils/errors.js";
|
|
9
9
|
import { getGlobalResourcePaths, getProjectResourcePaths, getSupportedAgentNames, normalizeAgents, } from "../utils/agent-configs.js";
|
|
10
10
|
import path from "node:path";
|
|
11
|
+
import { execFile } from "node:child_process";
|
|
11
12
|
import { promises as fs } from "node:fs";
|
|
13
|
+
import { promisify } from "node:util";
|
|
12
14
|
import { VersionResolver } from "../adapters/version/version-resolver.js";
|
|
13
15
|
import YAML from "yaml";
|
|
16
|
+
const execFileAsync = promisify(execFile);
|
|
17
|
+
const RESOURCE_TYPES = ["rule", "command", "skill"];
|
|
14
18
|
export class ServiceFactory {
|
|
15
19
|
stateStore = new StateStore();
|
|
16
20
|
projectConfigStore = new ProjectConfigStore();
|
|
@@ -43,9 +47,7 @@ export class ServiceFactory {
|
|
|
43
47
|
};
|
|
44
48
|
}
|
|
45
49
|
async addSource(name, type, repo) {
|
|
46
|
-
|
|
47
|
-
throw new HimanError(errorCodes.INVALID_INPUT, `Invalid source name: ${name}`);
|
|
48
|
-
}
|
|
50
|
+
this.validateSourceName(name);
|
|
49
51
|
await this.stateStore.ensureBaseDirs();
|
|
50
52
|
const sourceConfig = this.buildSourceConfig(type, repo);
|
|
51
53
|
const source = this.createSource(type);
|
|
@@ -105,6 +107,47 @@ export class ServiceFactory {
|
|
|
105
107
|
const source = await this.loadSourceFromConfig();
|
|
106
108
|
return source.initDocs(options);
|
|
107
109
|
}
|
|
110
|
+
async cloneSource(from, to, options = {}) {
|
|
111
|
+
await this.stateStore.ensureBaseDirs();
|
|
112
|
+
this.validateSourceTransferOptions(to, options);
|
|
113
|
+
const sourceEndpoint = await this.resolveGitSourceEndpoint(from);
|
|
114
|
+
const targetEndpoint = await this.resolveGitSourceEndpoint(to);
|
|
115
|
+
this.validateSourceTransferUseTarget(targetEndpoint, options);
|
|
116
|
+
this.ensureDifferentGitSources(sourceEndpoint, targetEndpoint);
|
|
117
|
+
const source = await this.loadGitSourceFromEndpoint(sourceEndpoint);
|
|
118
|
+
const result = await source.cloneTo(targetEndpoint.repo, {
|
|
119
|
+
branch: options.branch,
|
|
120
|
+
targetBranch: options.targetBranch,
|
|
121
|
+
dryRun: options.dryRun,
|
|
122
|
+
});
|
|
123
|
+
const configUpdates = await this.applySourceTransferConfigUpdates(targetEndpoint, options);
|
|
124
|
+
return {
|
|
125
|
+
source: sourceEndpoint,
|
|
126
|
+
target: targetEndpoint,
|
|
127
|
+
...result,
|
|
128
|
+
...configUpdates,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
async syncSource(from, to, options = {}) {
|
|
132
|
+
await this.stateStore.ensureBaseDirs();
|
|
133
|
+
this.validateSourceTransferOptions(to, options);
|
|
134
|
+
const sourceEndpoint = await this.resolveGitSourceEndpoint(from);
|
|
135
|
+
const targetEndpoint = await this.resolveGitSourceEndpoint(to);
|
|
136
|
+
this.validateSourceTransferUseTarget(targetEndpoint, options);
|
|
137
|
+
this.ensureDifferentGitSources(sourceEndpoint, targetEndpoint);
|
|
138
|
+
const source = await this.loadGitSourceFromEndpoint(sourceEndpoint);
|
|
139
|
+
const result = await source.syncLatestTo(targetEndpoint.repo, {
|
|
140
|
+
targetBranch: options.targetBranch,
|
|
141
|
+
dryRun: options.dryRun,
|
|
142
|
+
});
|
|
143
|
+
const configUpdates = await this.applySourceTransferConfigUpdates(targetEndpoint, options);
|
|
144
|
+
return {
|
|
145
|
+
source: sourceEndpoint,
|
|
146
|
+
target: targetEndpoint,
|
|
147
|
+
...result,
|
|
148
|
+
...configUpdates,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
108
151
|
async setAgents(agents, scope, projectDir) {
|
|
109
152
|
const normalized = normalizeAgents(agents);
|
|
110
153
|
if (scope === "project") {
|
|
@@ -137,6 +180,45 @@ export class ServiceFactory {
|
|
|
137
180
|
supported: getSupportedAgentNames(),
|
|
138
181
|
};
|
|
139
182
|
}
|
|
183
|
+
async doctor(projectDir) {
|
|
184
|
+
const checks = [];
|
|
185
|
+
checks.push(this.checkNodeVersion());
|
|
186
|
+
checks.push(await this.checkGit());
|
|
187
|
+
checks.push(await this.checkHomeState());
|
|
188
|
+
const config = await this.stateStore.loadConfig();
|
|
189
|
+
if (!config?.source) {
|
|
190
|
+
checks.push({
|
|
191
|
+
name: "source",
|
|
192
|
+
status: "error",
|
|
193
|
+
message: "No source configured. Run `himan init <git_repo>` first.",
|
|
194
|
+
details: { configPath: this.stateStore.getConfigPath() },
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
else {
|
|
198
|
+
const currentName = config.sources?.default ?? "default";
|
|
199
|
+
const currentSource = config.sources?.items[currentName] ?? config.source;
|
|
200
|
+
checks.push({
|
|
201
|
+
name: "source",
|
|
202
|
+
status: "ok",
|
|
203
|
+
message: `Using ${currentSource.type} source ${currentName}.`,
|
|
204
|
+
details: {
|
|
205
|
+
name: currentName,
|
|
206
|
+
type: currentSource.type,
|
|
207
|
+
repo: currentSource.repo,
|
|
208
|
+
repoId: currentSource.repoId,
|
|
209
|
+
},
|
|
210
|
+
});
|
|
211
|
+
checks.push(await this.checkSourceResources());
|
|
212
|
+
}
|
|
213
|
+
checks.push(await this.checkAgents(projectDir));
|
|
214
|
+
const lockCheck = await this.checkProjectLock(projectDir);
|
|
215
|
+
checks.push(lockCheck.check);
|
|
216
|
+
checks.push(await this.checkProjectTargets(projectDir, lockCheck.lock));
|
|
217
|
+
return {
|
|
218
|
+
ok: !checks.some((check) => check.status === "error"),
|
|
219
|
+
checks,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
140
222
|
async clearAgents(scope, projectDir) {
|
|
141
223
|
if (scope === "project") {
|
|
142
224
|
await this.projectConfigStore.clearAgents(projectDir);
|
|
@@ -149,6 +231,172 @@ export class ServiceFactory {
|
|
|
149
231
|
});
|
|
150
232
|
return { scope };
|
|
151
233
|
}
|
|
234
|
+
checkNodeVersion() {
|
|
235
|
+
const version = process.versions.node;
|
|
236
|
+
const major = Number(version.split(".")[0]);
|
|
237
|
+
if (major === 22) {
|
|
238
|
+
return {
|
|
239
|
+
name: "node",
|
|
240
|
+
status: "ok",
|
|
241
|
+
message: `Node.js ${version}.`,
|
|
242
|
+
};
|
|
243
|
+
}
|
|
244
|
+
return {
|
|
245
|
+
name: "node",
|
|
246
|
+
status: "error",
|
|
247
|
+
message: `Node.js ${version} is unsupported. Use Node.js 22.x.`,
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
async checkGit() {
|
|
251
|
+
try {
|
|
252
|
+
const result = await execFileAsync("git", ["--version"]);
|
|
253
|
+
return {
|
|
254
|
+
name: "git",
|
|
255
|
+
status: "ok",
|
|
256
|
+
message: result.stdout.trim() || "Git is available.",
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
catch (error) {
|
|
260
|
+
return this.errorCheck("git", "Git is not available on PATH.", error);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
async checkHomeState() {
|
|
264
|
+
const root = this.paths.getHimanRoot();
|
|
265
|
+
const reposDir = this.paths.getReposDir();
|
|
266
|
+
const storeDir = this.paths.getStoreDir();
|
|
267
|
+
const missing = [];
|
|
268
|
+
if (!(await this.exists(root)))
|
|
269
|
+
missing.push(root);
|
|
270
|
+
if (!(await this.exists(reposDir)))
|
|
271
|
+
missing.push(reposDir);
|
|
272
|
+
if (!(await this.exists(storeDir)))
|
|
273
|
+
missing.push(storeDir);
|
|
274
|
+
if (missing.length > 0) {
|
|
275
|
+
return {
|
|
276
|
+
name: "home",
|
|
277
|
+
status: "warn",
|
|
278
|
+
message: "Himan home directories are not fully initialized yet.",
|
|
279
|
+
details: { root, reposDir, storeDir, missing },
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
name: "home",
|
|
284
|
+
status: "ok",
|
|
285
|
+
message: `Himan home is initialized at ${root}.`,
|
|
286
|
+
details: { root, reposDir, storeDir },
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
async checkSourceResources() {
|
|
290
|
+
try {
|
|
291
|
+
const source = await this.loadSourceFromConfig();
|
|
292
|
+
const entries = await Promise.all(RESOURCE_TYPES.map(async (type) => [type, await source.list(type)]));
|
|
293
|
+
const counts = Object.fromEntries(entries.map(([type, resources]) => [type, resources.length]));
|
|
294
|
+
const total = RESOURCE_TYPES.reduce((sum, type) => sum + counts[type], 0);
|
|
295
|
+
return {
|
|
296
|
+
name: "resources",
|
|
297
|
+
status: "ok",
|
|
298
|
+
message: `Scanned ${total} resources from current source.`,
|
|
299
|
+
details: { counts },
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
return this.errorCheck("resources", "Cannot scan current source resources.", error);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
async checkAgents(projectDir) {
|
|
307
|
+
try {
|
|
308
|
+
const settings = await this.getAgentSettings(projectDir);
|
|
309
|
+
const scope = settings.project ? "project" : settings.global ? "global" : "default";
|
|
310
|
+
return {
|
|
311
|
+
name: "agents",
|
|
312
|
+
status: "ok",
|
|
313
|
+
message: `Effective agents: ${settings.effective.join(", ")} (${scope}).`,
|
|
314
|
+
details: settings,
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
catch (error) {
|
|
318
|
+
return this.errorCheck("agents", "Cannot resolve effective agents.", error);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
async checkProjectLock(projectDir) {
|
|
322
|
+
const lockPath = this.lockStore.getLockPath(projectDir);
|
|
323
|
+
const { lock, state } = await this.lockStore.loadWithState(projectDir);
|
|
324
|
+
if (state === "missing") {
|
|
325
|
+
return {
|
|
326
|
+
check: {
|
|
327
|
+
name: "lock",
|
|
328
|
+
status: "ok",
|
|
329
|
+
message: "No himan.lock found for this project yet.",
|
|
330
|
+
details: { lockPath },
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
}
|
|
334
|
+
if (state === "invalid" || !lock) {
|
|
335
|
+
return {
|
|
336
|
+
check: {
|
|
337
|
+
name: "lock",
|
|
338
|
+
status: "error",
|
|
339
|
+
message: `Lock file is invalid: ${lockPath}`,
|
|
340
|
+
details: { lockPath },
|
|
341
|
+
},
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
if (lock.resources.length === 0) {
|
|
345
|
+
return {
|
|
346
|
+
check: {
|
|
347
|
+
name: "lock",
|
|
348
|
+
status: "warn",
|
|
349
|
+
message: "himan.lock has no resources.",
|
|
350
|
+
details: { lockPath },
|
|
351
|
+
},
|
|
352
|
+
lock,
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
return {
|
|
356
|
+
check: {
|
|
357
|
+
name: "lock",
|
|
358
|
+
status: "ok",
|
|
359
|
+
message: `himan.lock tracks ${lock.resources.length} resources.`,
|
|
360
|
+
details: { lockPath, source: lock.source },
|
|
361
|
+
},
|
|
362
|
+
lock,
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
async checkProjectTargets(projectDir, lock) {
|
|
366
|
+
if (!lock || lock.resources.length === 0) {
|
|
367
|
+
return {
|
|
368
|
+
name: "targets",
|
|
369
|
+
status: "ok",
|
|
370
|
+
message: "No locked project targets to verify.",
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
const missing = [];
|
|
374
|
+
for (const resource of lock.resources) {
|
|
375
|
+
const agents = normalizeAgents(resource.agents);
|
|
376
|
+
const targets = getProjectResourcePaths(projectDir, resource.type, resource.name, agents);
|
|
377
|
+
for (const targetPath of targets) {
|
|
378
|
+
if (!(await this.exists(targetPath))) {
|
|
379
|
+
missing.push({
|
|
380
|
+
resource: `${resource.type}/${resource.name}@${resource.version}`,
|
|
381
|
+
path: targetPath,
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
if (missing.length > 0) {
|
|
387
|
+
return {
|
|
388
|
+
name: "targets",
|
|
389
|
+
status: "warn",
|
|
390
|
+
message: `Missing ${missing.length} installed targets. Run \`himan install\` to restore them.`,
|
|
391
|
+
details: { missing },
|
|
392
|
+
};
|
|
393
|
+
}
|
|
394
|
+
return {
|
|
395
|
+
name: "targets",
|
|
396
|
+
status: "ok",
|
|
397
|
+
message: "All locked project targets exist.",
|
|
398
|
+
};
|
|
399
|
+
}
|
|
152
400
|
async list(type, agents) {
|
|
153
401
|
const source = await this.loadSourceFromConfig();
|
|
154
402
|
const resources = await source.list(type);
|
|
@@ -192,22 +440,33 @@ export class ServiceFactory {
|
|
|
192
440
|
return this.installWithSource(source, undefined, type, name, version, projectDir, agents, mode, "global");
|
|
193
441
|
}
|
|
194
442
|
async dev(type, name, projectDir) {
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
443
|
+
const projectTarget = await this.tryResolveProjectResourceTarget(projectDir, type, name);
|
|
444
|
+
if (projectTarget) {
|
|
445
|
+
return {
|
|
446
|
+
type,
|
|
447
|
+
name,
|
|
448
|
+
devPath: projectTarget.resourcePath,
|
|
449
|
+
linkPath: projectTarget.linkPaths[0],
|
|
450
|
+
mode: projectTarget.mode,
|
|
451
|
+
sourceScope: "project",
|
|
452
|
+
};
|
|
201
453
|
}
|
|
202
|
-
|
|
203
|
-
|
|
454
|
+
const globalTarget = await this.tryResolveGlobalResourceTarget(projectDir, type, name);
|
|
455
|
+
if (!globalTarget) {
|
|
456
|
+
throw new HimanError(errorCodes.INSTALL_NOT_FOUND, `Installed resource link not found for ${type}/${name}. Run install first.`);
|
|
457
|
+
}
|
|
458
|
+
const projectLinkPaths = getProjectResourcePaths(projectDir, type, name, globalTarget.agents);
|
|
459
|
+
for (const [index, linkPath] of projectLinkPaths.entries()) {
|
|
460
|
+
const sourcePath = globalTarget.linkPaths[index] ?? globalTarget.resourcePath;
|
|
461
|
+
await this.materializeResource(sourcePath, linkPath, "copy");
|
|
204
462
|
}
|
|
205
463
|
return {
|
|
206
464
|
type,
|
|
207
465
|
name,
|
|
208
|
-
devPath,
|
|
209
|
-
linkPath:
|
|
210
|
-
mode:
|
|
466
|
+
devPath: projectLinkPaths[0],
|
|
467
|
+
linkPath: projectLinkPaths[0],
|
|
468
|
+
mode: "copy",
|
|
469
|
+
sourceScope: "global",
|
|
211
470
|
};
|
|
212
471
|
}
|
|
213
472
|
async uninstall(type, name, projectDir) {
|
|
@@ -221,58 +480,142 @@ export class ServiceFactory {
|
|
|
221
480
|
await this.lockStore.removeResource(projectDir, { type, name });
|
|
222
481
|
return { type, name, linkPath: installInfo.linkPaths[0] };
|
|
223
482
|
}
|
|
224
|
-
async publish(type, name, releaseType, projectDir) {
|
|
483
|
+
async publish(type, name, releaseType, projectDir, options = {}) {
|
|
484
|
+
const installScope = options.installScope ?? "project";
|
|
485
|
+
this.reportPublishProgress(options, "prepare", `Preparing ${type}/${name}.`);
|
|
225
486
|
const source = await this.loadSourceFromConfig();
|
|
226
487
|
const sourceDir = await this.resolvePublishSourceDir(type, name, projectDir);
|
|
227
488
|
const existingInstallInfo = await this.tryResolveInstalledResource(projectDir, type, name);
|
|
489
|
+
const existingGlobalInstallInfo = await this.tryResolveGlobalResourceTarget(projectDir, type, name);
|
|
228
490
|
const history = await source.history(type, name);
|
|
229
491
|
const latest = history[0]?.version ?? "0.0.0";
|
|
230
492
|
const nextVersion = this.versions.nextVersion(latest, releaseType);
|
|
493
|
+
this.reportPublishProgress(options, "resolve-version", `Resolved ${releaseType} version ${nextVersion}.`);
|
|
494
|
+
this.reportPublishProgress(options, "publish-source", `Publishing ${type}/${name}@${nextVersion} to the Git source.`);
|
|
231
495
|
const result = await source.publish(type, name, nextVersion, sourceDir, {
|
|
232
496
|
releaseType,
|
|
233
497
|
});
|
|
234
498
|
const storePath = this.getStorePath(type, name, nextVersion);
|
|
499
|
+
this.reportPublishProgress(options, "sync-store", `Syncing ${type}/${name}@${nextVersion} into the local store.`);
|
|
235
500
|
if (!(await this.exists(storePath))) {
|
|
236
501
|
await source.pull(type, name, nextVersion, storePath);
|
|
237
502
|
}
|
|
238
503
|
const locked = await this.getLockedResource(projectDir, type, name);
|
|
239
504
|
const resourceMeta = await this.readResourceMetaFromDir(storePath, type);
|
|
240
505
|
const configuredAgents = await this.getConfiguredAgents(projectDir);
|
|
241
|
-
const nextAgents = locked?.agents?.length
|
|
242
|
-
? normalizeAgents(locked.agents)
|
|
243
|
-
: existingInstallInfo?.agents.length
|
|
244
|
-
? normalizeAgents(existingInstallInfo.agents)
|
|
245
|
-
: configuredAgents ?? normalizeAgents(resourceMeta?.agents);
|
|
246
506
|
const installMode = "copy";
|
|
247
|
-
const
|
|
507
|
+
const nextAgents = installScope === "global"
|
|
508
|
+
? existingGlobalInstallInfo?.agents.length
|
|
509
|
+
? normalizeAgents(existingGlobalInstallInfo.agents)
|
|
510
|
+
: existingInstallInfo?.agents.length
|
|
511
|
+
? normalizeAgents(existingInstallInfo.agents)
|
|
512
|
+
: locked?.agents?.length
|
|
513
|
+
? normalizeAgents(locked.agents)
|
|
514
|
+
: configuredAgents ?? normalizeAgents(resourceMeta?.agents)
|
|
515
|
+
: locked?.agents?.length
|
|
516
|
+
? normalizeAgents(locked.agents)
|
|
517
|
+
: existingInstallInfo?.agents.length
|
|
518
|
+
? normalizeAgents(existingInstallInfo.agents)
|
|
519
|
+
: configuredAgents ?? normalizeAgents(resourceMeta?.agents);
|
|
520
|
+
const linkPaths = installScope === "global"
|
|
521
|
+
? getGlobalResourcePaths(this.paths.getHomeDir(), type, name, nextAgents)
|
|
522
|
+
: getProjectResourcePaths(projectDir, type, name, nextAgents);
|
|
523
|
+
this.reportPublishProgress(options, "install", installScope === "global"
|
|
524
|
+
? `Installing published version globally for ${nextAgents.join(", ")}.`
|
|
525
|
+
: `Installing published version into the current project for ${nextAgents.join(", ")}.`);
|
|
248
526
|
for (const linkPath of linkPaths) {
|
|
249
527
|
await this.materializeResource(storePath, linkPath, installMode);
|
|
250
528
|
}
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
529
|
+
if (installScope === "project") {
|
|
530
|
+
const sourceInfo = await this.getLockSourceInfo();
|
|
531
|
+
await this.lockStore.upsertResource(projectDir, sourceInfo, {
|
|
532
|
+
type,
|
|
533
|
+
name,
|
|
534
|
+
version: nextVersion,
|
|
535
|
+
agents: nextAgents,
|
|
536
|
+
mode: installMode,
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
this.reportPublishProgress(options, "cleanup", `Cleaning up legacy dev copy if present.`);
|
|
259
540
|
await fs.rm(this.getProjectDevPath(projectDir, type, name), {
|
|
260
541
|
recursive: true,
|
|
261
542
|
force: true,
|
|
262
543
|
});
|
|
263
|
-
|
|
544
|
+
this.reportPublishProgress(options, "done", `Published ${type}/${name}@${result.version}.`);
|
|
545
|
+
return {
|
|
546
|
+
type,
|
|
547
|
+
name,
|
|
548
|
+
version: result.version,
|
|
549
|
+
tag: result.tag,
|
|
550
|
+
installScope,
|
|
551
|
+
linkPath: linkPaths[0],
|
|
552
|
+
};
|
|
264
553
|
}
|
|
265
554
|
async create(type, name, options, projectDir) {
|
|
266
555
|
this.validateCreateInput(type, name, options);
|
|
556
|
+
await this.loadSourceFromConfig();
|
|
557
|
+
const agents = await this.resolveEffectiveAgents(projectDir, options.agents);
|
|
558
|
+
const resourcePaths = getProjectResourcePaths(projectDir, type, name, agents);
|
|
559
|
+
const entry = options.entry ?? this.getDefaultEntry(type);
|
|
560
|
+
const files = resourcePaths.flatMap((resourcePath) => [
|
|
561
|
+
path.join(resourcePath, "himan.yaml"),
|
|
562
|
+
path.join(resourcePath, entry),
|
|
563
|
+
]);
|
|
564
|
+
const existingPaths = [];
|
|
565
|
+
for (const resourcePath of resourcePaths) {
|
|
566
|
+
if (await this.exists(resourcePath))
|
|
567
|
+
existingPaths.push(resourcePath);
|
|
568
|
+
}
|
|
569
|
+
if (existingPaths.length > 0 && !options.force) {
|
|
570
|
+
throw new HimanError(errorCodes.RESOURCE_EXISTS, `Resource already exists: ${type}/${name}`, { paths: existingPaths });
|
|
571
|
+
}
|
|
572
|
+
if (!options.dryRun) {
|
|
573
|
+
for (const resourcePath of resourcePaths) {
|
|
574
|
+
await fs.rm(resourcePath, { recursive: true, force: true });
|
|
575
|
+
await fs.mkdir(resourcePath, { recursive: true });
|
|
576
|
+
await fs.writeFile(path.join(resourcePath, "himan.yaml"), YAML.stringify({
|
|
577
|
+
name,
|
|
578
|
+
type,
|
|
579
|
+
version: "0.1.0",
|
|
580
|
+
entry,
|
|
581
|
+
description: options.description ?? `${type} resource ${name}`,
|
|
582
|
+
agents,
|
|
583
|
+
}), "utf8");
|
|
584
|
+
await fs.writeFile(path.join(resourcePath, entry), this.getDefaultContent(type, name), "utf8");
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return {
|
|
588
|
+
type,
|
|
589
|
+
name,
|
|
590
|
+
resourceDir: resourcePaths[0],
|
|
591
|
+
files,
|
|
592
|
+
dryRun: Boolean(options.dryRun),
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
async rename(type, oldName, newName, projectDir, options = {}) {
|
|
596
|
+
this.validateRenameInput(type, oldName, newName);
|
|
267
597
|
const source = await this.loadSourceFromConfig();
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
598
|
+
const shouldMigrateProject = options.migrateProject !== false && !options.dryRun;
|
|
599
|
+
const locked = shouldMigrateProject
|
|
600
|
+
? await this.getLockedResource(projectDir, type, oldName)
|
|
601
|
+
: undefined;
|
|
602
|
+
const installInfo = shouldMigrateProject
|
|
603
|
+
? await this.tryResolveInstalledResource(projectDir, type, oldName)
|
|
604
|
+
: undefined;
|
|
605
|
+
const hasDevPath = shouldMigrateProject &&
|
|
606
|
+
(await this.exists(this.getProjectDevPath(projectDir, type, oldName)));
|
|
607
|
+
if (shouldMigrateProject && (locked || installInfo || hasDevPath)) {
|
|
608
|
+
await this.ensureRenamedProjectResourceAvailable(projectDir, type, oldName, newName, locked, installInfo);
|
|
609
|
+
}
|
|
610
|
+
const result = await source.rename(type, oldName, newName, {
|
|
274
611
|
dryRun: options.dryRun,
|
|
275
612
|
});
|
|
613
|
+
const projectMigrated = shouldMigrateProject &&
|
|
614
|
+
(await this.migrateRenamedProjectResource(source, type, oldName, newName, projectDir, result, locked, installInfo));
|
|
615
|
+
return {
|
|
616
|
+
...result,
|
|
617
|
+
projectMigrated,
|
|
618
|
+
};
|
|
276
619
|
}
|
|
277
620
|
async installFromLock(projectDir, agents, mode) {
|
|
278
621
|
const { lock, state } = await this.lockStore.loadWithState(projectDir);
|
|
@@ -331,6 +674,76 @@ export class ServiceFactory {
|
|
|
331
674
|
async loadSourceFromConfig() {
|
|
332
675
|
return (await this.loadSourceWithInfoFromConfig()).source;
|
|
333
676
|
}
|
|
677
|
+
async resolveGitSourceEndpoint(ref) {
|
|
678
|
+
const config = await this.stateStore.loadConfig();
|
|
679
|
+
const configured = config?.sources?.items[ref];
|
|
680
|
+
if (configured) {
|
|
681
|
+
if (configured.type !== "git" || !configured.repo) {
|
|
682
|
+
throw new HimanError(errorCodes.INVALID_INPUT, `Source is not a git source: ${ref}`);
|
|
683
|
+
}
|
|
684
|
+
return {
|
|
685
|
+
name: ref,
|
|
686
|
+
repo: configured.repo,
|
|
687
|
+
repoId: configured.repoId ?? toRepoId(configured.repo),
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
if (!ref.trim()) {
|
|
691
|
+
throw new HimanError(errorCodes.INVALID_INPUT, "Git repo is required.");
|
|
692
|
+
}
|
|
693
|
+
return {
|
|
694
|
+
repo: ref,
|
|
695
|
+
repoId: toRepoId(ref),
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
async loadGitSourceFromEndpoint(endpoint) {
|
|
699
|
+
const sourceConfig = this.buildSourceConfig("git", endpoint.repo, endpoint.repoId);
|
|
700
|
+
const source = new GitSourceAdapter();
|
|
701
|
+
await source.init(sourceConfig);
|
|
702
|
+
return source;
|
|
703
|
+
}
|
|
704
|
+
validateSourceTransferOptions(targetRef, options) {
|
|
705
|
+
if (options.addSource) {
|
|
706
|
+
this.validateSourceName(options.addSource);
|
|
707
|
+
}
|
|
708
|
+
if (!options.use || options.addSource) {
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(targetRef)) {
|
|
712
|
+
throw new HimanError(errorCodes.INVALID_INPUT, "`--use` requires the target to be a configured source name or `--add-source <name>`.");
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
validateSourceTransferUseTarget(target, options) {
|
|
716
|
+
if (options.use && !options.addSource && !target.name) {
|
|
717
|
+
throw new HimanError(errorCodes.INVALID_INPUT, "`--use` requires the target to be a configured source name or `--add-source <name>`.");
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
ensureDifferentGitSources(source, target) {
|
|
721
|
+
if (source.repo === target.repo) {
|
|
722
|
+
throw new HimanError(errorCodes.INVALID_INPUT, "Source and target repositories must be different.");
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
async applySourceTransferConfigUpdates(target, options) {
|
|
726
|
+
if (options.dryRun)
|
|
727
|
+
return {};
|
|
728
|
+
let addedSource;
|
|
729
|
+
if (options.addSource) {
|
|
730
|
+
await this.addSource(options.addSource, "git", target.repo);
|
|
731
|
+
addedSource = options.addSource;
|
|
732
|
+
}
|
|
733
|
+
const sourceToUse = options.use ? options.addSource ?? target.name : undefined;
|
|
734
|
+
if (sourceToUse) {
|
|
735
|
+
await this.useSource(sourceToUse);
|
|
736
|
+
}
|
|
737
|
+
return {
|
|
738
|
+
addedSource,
|
|
739
|
+
usedSource: sourceToUse,
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
validateSourceName(name) {
|
|
743
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
|
|
744
|
+
throw new HimanError(errorCodes.INVALID_INPUT, `Invalid source name: ${name}`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
334
747
|
async loadSourceWithInfoFromConfig() {
|
|
335
748
|
const { name, source: stateSource } = await this.getCurrentSourceState();
|
|
336
749
|
const sourceInfo = this.toLockSourceInfo(stateSource, name);
|
|
@@ -391,6 +804,75 @@ export class ServiceFactory {
|
|
|
391
804
|
return undefined;
|
|
392
805
|
return lock.resources.find((item) => item.type === type && item.name === name);
|
|
393
806
|
}
|
|
807
|
+
async ensureRenamedProjectResourceAvailable(projectDir, type, oldName, newName, locked, installInfo) {
|
|
808
|
+
const lockedNewName = await this.getLockedResource(projectDir, type, newName);
|
|
809
|
+
if (lockedNewName) {
|
|
810
|
+
throw new HimanError(errorCodes.RESOURCE_EXISTS, `Installed resource already exists: ${type}/${newName}`);
|
|
811
|
+
}
|
|
812
|
+
const newDevPath = this.getProjectDevPath(projectDir, type, newName);
|
|
813
|
+
if (await this.exists(newDevPath)) {
|
|
814
|
+
throw new HimanError(errorCodes.RESOURCE_EXISTS, `Development resource already exists: ${type}/${newName}`);
|
|
815
|
+
}
|
|
816
|
+
const agents = locked?.agents?.length
|
|
817
|
+
? normalizeAgents(locked.agents)
|
|
818
|
+
: installInfo?.agents;
|
|
819
|
+
if (!agents?.length)
|
|
820
|
+
return;
|
|
821
|
+
for (const linkPath of getProjectResourcePaths(projectDir, type, newName, agents)) {
|
|
822
|
+
if (await this.exists(linkPath)) {
|
|
823
|
+
throw new HimanError(errorCodes.RESOURCE_EXISTS, `Installed resource already exists: ${type}/${newName}`);
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
async migrateRenamedProjectResource(source, type, oldName, newName, projectDir, result, locked, installInfo) {
|
|
828
|
+
const oldDevPath = this.getProjectDevPath(projectDir, type, oldName);
|
|
829
|
+
const newDevPath = this.getProjectDevPath(projectDir, type, newName);
|
|
830
|
+
const hasDevPath = await this.exists(oldDevPath);
|
|
831
|
+
if (!locked && !installInfo && !hasDevPath) {
|
|
832
|
+
return false;
|
|
833
|
+
}
|
|
834
|
+
let sourcePath;
|
|
835
|
+
if (hasDevPath) {
|
|
836
|
+
await fs.mkdir(path.dirname(newDevPath), { recursive: true });
|
|
837
|
+
await fs.rename(oldDevPath, newDevPath);
|
|
838
|
+
await this.updateRenamedResourceMetadata(newDevPath, type, oldName, newName);
|
|
839
|
+
sourcePath = newDevPath;
|
|
840
|
+
}
|
|
841
|
+
else if (result.latestVersion) {
|
|
842
|
+
const storePath = this.getStorePath(type, newName, result.latestVersion);
|
|
843
|
+
if (!(await this.exists(storePath))) {
|
|
844
|
+
await source.pull(type, newName, result.latestVersion, storePath);
|
|
845
|
+
}
|
|
846
|
+
sourcePath = storePath;
|
|
847
|
+
}
|
|
848
|
+
else if (installInfo) {
|
|
849
|
+
sourcePath = installInfo.installedPath;
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
sourcePath = result.resourceDir;
|
|
853
|
+
}
|
|
854
|
+
const agents = locked?.agents?.length
|
|
855
|
+
? normalizeAgents(locked.agents)
|
|
856
|
+
: installInfo?.agents ?? normalizeAgents();
|
|
857
|
+
const mode = installInfo?.mode ?? this.resolveInstallMode(locked?.mode);
|
|
858
|
+
const oldLinkPaths = installInfo?.linkPaths ?? getProjectResourcePaths(projectDir, type, oldName, agents);
|
|
859
|
+
const newLinkPaths = getProjectResourcePaths(projectDir, type, newName, agents);
|
|
860
|
+
for (const linkPath of newLinkPaths) {
|
|
861
|
+
await this.materializeResource(sourcePath, linkPath, mode);
|
|
862
|
+
}
|
|
863
|
+
for (const linkPath of oldLinkPaths) {
|
|
864
|
+
await fs.rm(linkPath, { recursive: true, force: true });
|
|
865
|
+
}
|
|
866
|
+
if (locked) {
|
|
867
|
+
await this.lockStore.renameResource(projectDir, {
|
|
868
|
+
type,
|
|
869
|
+
oldName,
|
|
870
|
+
newName,
|
|
871
|
+
version: result.latestVersion ?? locked.version,
|
|
872
|
+
});
|
|
873
|
+
}
|
|
874
|
+
return true;
|
|
875
|
+
}
|
|
394
876
|
buildSourceConfig(type, repo, repoId) {
|
|
395
877
|
if (type === "registry") {
|
|
396
878
|
return { type };
|
|
@@ -433,6 +915,80 @@ export class ServiceFactory {
|
|
|
433
915
|
resolveInstallMode(mode) {
|
|
434
916
|
return mode === "link" ? "link" : "copy";
|
|
435
917
|
}
|
|
918
|
+
reportPublishProgress(options, stage, message) {
|
|
919
|
+
options.onProgress?.({ stage, message });
|
|
920
|
+
}
|
|
921
|
+
async tryResolveProjectResourceTarget(projectDir, type, name) {
|
|
922
|
+
const locked = await this.getLockedResource(projectDir, type, name);
|
|
923
|
+
const configuredAgents = await this.getConfiguredAgents(projectDir);
|
|
924
|
+
if (locked?.agents?.length || configuredAgents?.length) {
|
|
925
|
+
const agents = locked?.agents?.length
|
|
926
|
+
? normalizeAgents(locked.agents)
|
|
927
|
+
: (configuredAgents ?? normalizeAgents());
|
|
928
|
+
const linkPaths = getProjectResourcePaths(projectDir, type, name, agents);
|
|
929
|
+
const existingLinkPath = await this.findFirstExistingPath(linkPaths);
|
|
930
|
+
if (existingLinkPath) {
|
|
931
|
+
return {
|
|
932
|
+
resourcePath: existingLinkPath,
|
|
933
|
+
linkPaths,
|
|
934
|
+
agents,
|
|
935
|
+
mode: this.resolveInstallMode(locked?.mode ?? (await this.readPathMode(existingLinkPath))),
|
|
936
|
+
};
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
const existingCandidates = await this.findExistingAgentPaths(projectDir, type, name, "project");
|
|
940
|
+
if (existingCandidates.length === 0) {
|
|
941
|
+
return undefined;
|
|
942
|
+
}
|
|
943
|
+
const existingAgents = normalizeAgents(existingCandidates.map((candidate) => candidate.agent));
|
|
944
|
+
const linkPaths = getProjectResourcePaths(projectDir, type, name, existingAgents);
|
|
945
|
+
return {
|
|
946
|
+
resourcePath: existingCandidates[0].path,
|
|
947
|
+
linkPaths,
|
|
948
|
+
agents: existingAgents,
|
|
949
|
+
mode: await this.readPathMode(existingCandidates[0].path),
|
|
950
|
+
};
|
|
951
|
+
}
|
|
952
|
+
async tryResolveGlobalResourceTarget(projectDir, type, name) {
|
|
953
|
+
const existingCandidates = await this.findExistingAgentPaths(projectDir, type, name, "global");
|
|
954
|
+
if (existingCandidates.length === 0) {
|
|
955
|
+
return undefined;
|
|
956
|
+
}
|
|
957
|
+
const agents = normalizeAgents(existingCandidates.map((candidate) => candidate.agent));
|
|
958
|
+
const linkPaths = getGlobalResourcePaths(this.paths.getHomeDir(), type, name, agents);
|
|
959
|
+
return {
|
|
960
|
+
resourcePath: existingCandidates[0].path,
|
|
961
|
+
linkPaths,
|
|
962
|
+
agents,
|
|
963
|
+
mode: await this.readPathMode(existingCandidates[0].path),
|
|
964
|
+
};
|
|
965
|
+
}
|
|
966
|
+
async findExistingAgentPaths(projectDir, type, name, scope) {
|
|
967
|
+
const rootDir = scope === "global" ? this.paths.getHomeDir() : projectDir;
|
|
968
|
+
const candidates = getSupportedAgentNames().map((agent) => ({
|
|
969
|
+
agent,
|
|
970
|
+
path: scope === "global"
|
|
971
|
+
? getGlobalResourcePaths(rootDir, type, name, [agent])[0]
|
|
972
|
+
: getProjectResourcePaths(rootDir, type, name, [agent])[0],
|
|
973
|
+
}));
|
|
974
|
+
const existingCandidates = [];
|
|
975
|
+
for (const candidate of candidates) {
|
|
976
|
+
if (await this.exists(candidate.path))
|
|
977
|
+
existingCandidates.push(candidate);
|
|
978
|
+
}
|
|
979
|
+
return existingCandidates;
|
|
980
|
+
}
|
|
981
|
+
async findFirstExistingPath(paths) {
|
|
982
|
+
for (const targetPath of paths) {
|
|
983
|
+
if (await this.exists(targetPath))
|
|
984
|
+
return targetPath;
|
|
985
|
+
}
|
|
986
|
+
return undefined;
|
|
987
|
+
}
|
|
988
|
+
async readPathMode(targetPath) {
|
|
989
|
+
const stat = await fs.lstat(targetPath);
|
|
990
|
+
return stat.isSymbolicLink() ? "link" : "copy";
|
|
991
|
+
}
|
|
436
992
|
async resolveInstalledResource(projectDir, type, name) {
|
|
437
993
|
const locked = await this.getLockedResource(projectDir, type, name);
|
|
438
994
|
const configuredAgents = await this.getConfiguredAgents(projectDir);
|
|
@@ -546,6 +1102,48 @@ export class ServiceFactory {
|
|
|
546
1102
|
this.readStringArrayMetadata(metadata, "targets"),
|
|
547
1103
|
};
|
|
548
1104
|
}
|
|
1105
|
+
async updateRenamedResourceMetadata(resourceDir, type, oldName, newName) {
|
|
1106
|
+
const yamlPath = path.join(resourceDir, "himan.yaml");
|
|
1107
|
+
if (await this.exists(yamlPath)) {
|
|
1108
|
+
const raw = await fs.readFile(yamlPath, "utf8");
|
|
1109
|
+
const parsed = YAML.parse(raw);
|
|
1110
|
+
if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
|
|
1111
|
+
return;
|
|
1112
|
+
}
|
|
1113
|
+
await fs.writeFile(yamlPath, YAML.stringify({
|
|
1114
|
+
...parsed,
|
|
1115
|
+
name: newName,
|
|
1116
|
+
}), "utf8");
|
|
1117
|
+
return;
|
|
1118
|
+
}
|
|
1119
|
+
if (type !== "skill")
|
|
1120
|
+
return;
|
|
1121
|
+
const entryPath = path.join(resourceDir, this.getDefaultEntry(type));
|
|
1122
|
+
if (!(await this.exists(entryPath)))
|
|
1123
|
+
return;
|
|
1124
|
+
const raw = await fs.readFile(entryPath, "utf8");
|
|
1125
|
+
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(raw);
|
|
1126
|
+
if (!match)
|
|
1127
|
+
return;
|
|
1128
|
+
let parsed;
|
|
1129
|
+
try {
|
|
1130
|
+
parsed = YAML.parse(match[1]);
|
|
1131
|
+
}
|
|
1132
|
+
catch {
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
if (typeof parsed !== "object" ||
|
|
1136
|
+
parsed === null ||
|
|
1137
|
+
Array.isArray(parsed) ||
|
|
1138
|
+
parsed.name !== oldName) {
|
|
1139
|
+
return;
|
|
1140
|
+
}
|
|
1141
|
+
const frontMatter = YAML.stringify({
|
|
1142
|
+
...parsed,
|
|
1143
|
+
name: newName,
|
|
1144
|
+
}).trimEnd();
|
|
1145
|
+
await fs.writeFile(entryPath, `---\n${frontMatter}\n---\n${raw.slice(match[0].length)}`, "utf8");
|
|
1146
|
+
}
|
|
549
1147
|
async readFrontMatter(filePath) {
|
|
550
1148
|
const raw = await fs.readFile(filePath, "utf8");
|
|
551
1149
|
const match = /^---\r?\n([\s\S]*?)\r?\n---(?:\r?\n|$)/.exec(raw.trimStart());
|
|
@@ -571,6 +1169,13 @@ export class ServiceFactory {
|
|
|
571
1169
|
.filter(Boolean);
|
|
572
1170
|
return items.length > 0 ? items : undefined;
|
|
573
1171
|
}
|
|
1172
|
+
errorCheck(name, message, error) {
|
|
1173
|
+
return {
|
|
1174
|
+
name,
|
|
1175
|
+
status: "error",
|
|
1176
|
+
message: `${message} ${error instanceof Error ? error.message : String(error)}`,
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
574
1179
|
async exists(targetPath) {
|
|
575
1180
|
try {
|
|
576
1181
|
await fs.access(targetPath);
|
|
@@ -585,6 +1190,10 @@ export class ServiceFactory {
|
|
|
585
1190
|
if (await this.exists(devPath)) {
|
|
586
1191
|
return devPath;
|
|
587
1192
|
}
|
|
1193
|
+
const projectTarget = await this.tryResolveProjectResourceTarget(projectDir, type, name);
|
|
1194
|
+
if (projectTarget) {
|
|
1195
|
+
return projectTarget.resourcePath;
|
|
1196
|
+
}
|
|
588
1197
|
const repoResourceDir = await this.getRepoResourceDir(type, name);
|
|
589
1198
|
if (await this.exists(repoResourceDir)) {
|
|
590
1199
|
return repoResourceDir;
|
|
@@ -612,6 +1221,15 @@ export class ServiceFactory {
|
|
|
612
1221
|
getDefaultEntry(type) {
|
|
613
1222
|
return type === "skill" ? "SKILL.md" : "content.md";
|
|
614
1223
|
}
|
|
1224
|
+
getDefaultContent(type, name) {
|
|
1225
|
+
if (type === "rule") {
|
|
1226
|
+
return `# ${name}\n\nDescribe rule instructions here.\n`;
|
|
1227
|
+
}
|
|
1228
|
+
if (type === "command") {
|
|
1229
|
+
return `# ${name}\n\nDescribe command behavior here.\n`;
|
|
1230
|
+
}
|
|
1231
|
+
return `# ${name}\n\nDescribe skill workflow here.\n`;
|
|
1232
|
+
}
|
|
615
1233
|
validateCreateInput(type, name, options) {
|
|
616
1234
|
if (!["rule", "command", "skill"].includes(type)) {
|
|
617
1235
|
throw new HimanError(errorCodes.UNSUPPORTED_RESOURCE_TYPE, `Unsupported resource type for create: ${type}`);
|
|
@@ -623,4 +1241,15 @@ export class ServiceFactory {
|
|
|
623
1241
|
throw new HimanError(errorCodes.TEMPLATE_NOT_FOUND, `Template not found: ${options.template}`);
|
|
624
1242
|
}
|
|
625
1243
|
}
|
|
1244
|
+
validateRenameInput(type, oldName, newName) {
|
|
1245
|
+
if (!["rule", "command", "skill"].includes(type)) {
|
|
1246
|
+
throw new HimanError(errorCodes.UNSUPPORTED_RESOURCE_TYPE, `Unsupported resource type for rename: ${type}`);
|
|
1247
|
+
}
|
|
1248
|
+
if (oldName === newName) {
|
|
1249
|
+
throw new HimanError(errorCodes.INVALID_INPUT, "Old and new resource names must be different.");
|
|
1250
|
+
}
|
|
1251
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(newName)) {
|
|
1252
|
+
throw new HimanError(errorCodes.INVALID_RESOURCE_NAME, `Invalid resource name: ${newName}. Use kebab-case only.`);
|
|
1253
|
+
}
|
|
1254
|
+
}
|
|
626
1255
|
}
|