@hi-man/himan 0.4.0 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +22 -0
- package/README.md +53 -21
- package/dist/adapters/resource/resource-scanner.js +14 -4
- package/dist/adapters/source/git-source-adapter.js +159 -6
- package/dist/adapters/source/registry-source-adapter.js +10 -1
- package/dist/cli/builders.js +9 -3
- package/dist/cli/doctor-command.js +30 -0
- package/dist/cli/project-commands.js +23 -5
- package/dist/cli/resource-commands.js +62 -7
- package/dist/cli/source-commands.js +96 -3
- package/dist/domain/doctor.js +1 -0
- package/dist/services/index.js +484 -44
- package/dist/utils/errors.js +1 -0
- package/docs/error-codes.md +6 -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();
|
|
@@ -176,6 +180,46 @@ export class ServiceFactory {
|
|
|
176
180
|
supported: getSupportedAgentNames(),
|
|
177
181
|
};
|
|
178
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.checkProjectArchiveStatus(lockCheck.lock));
|
|
217
|
+
checks.push(await this.checkProjectTargets(projectDir, lockCheck.lock));
|
|
218
|
+
return {
|
|
219
|
+
ok: !checks.some((check) => check.status === "error"),
|
|
220
|
+
checks,
|
|
221
|
+
};
|
|
222
|
+
}
|
|
179
223
|
async clearAgents(scope, projectDir) {
|
|
180
224
|
if (scope === "project") {
|
|
181
225
|
await this.projectConfigStore.clearAgents(projectDir);
|
|
@@ -188,14 +232,231 @@ export class ServiceFactory {
|
|
|
188
232
|
});
|
|
189
233
|
return { scope };
|
|
190
234
|
}
|
|
191
|
-
|
|
235
|
+
checkNodeVersion() {
|
|
236
|
+
const version = process.versions.node;
|
|
237
|
+
const major = Number(version.split(".")[0]);
|
|
238
|
+
if (major === 22) {
|
|
239
|
+
return {
|
|
240
|
+
name: "node",
|
|
241
|
+
status: "ok",
|
|
242
|
+
message: `Node.js ${version}.`,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
return {
|
|
246
|
+
name: "node",
|
|
247
|
+
status: "error",
|
|
248
|
+
message: `Node.js ${version} is unsupported. Use Node.js 22.x.`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
async checkGit() {
|
|
252
|
+
try {
|
|
253
|
+
const result = await execFileAsync("git", ["--version"]);
|
|
254
|
+
return {
|
|
255
|
+
name: "git",
|
|
256
|
+
status: "ok",
|
|
257
|
+
message: result.stdout.trim() || "Git is available.",
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
return this.errorCheck("git", "Git is not available on PATH.", error);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
async checkHomeState() {
|
|
265
|
+
const root = this.paths.getHimanRoot();
|
|
266
|
+
const reposDir = this.paths.getReposDir();
|
|
267
|
+
const storeDir = this.paths.getStoreDir();
|
|
268
|
+
const missing = [];
|
|
269
|
+
if (!(await this.exists(root)))
|
|
270
|
+
missing.push(root);
|
|
271
|
+
if (!(await this.exists(reposDir)))
|
|
272
|
+
missing.push(reposDir);
|
|
273
|
+
if (!(await this.exists(storeDir)))
|
|
274
|
+
missing.push(storeDir);
|
|
275
|
+
if (missing.length > 0) {
|
|
276
|
+
return {
|
|
277
|
+
name: "home",
|
|
278
|
+
status: "warn",
|
|
279
|
+
message: "Himan home directories are not fully initialized yet.",
|
|
280
|
+
details: { root, reposDir, storeDir, missing },
|
|
281
|
+
};
|
|
282
|
+
}
|
|
283
|
+
return {
|
|
284
|
+
name: "home",
|
|
285
|
+
status: "ok",
|
|
286
|
+
message: `Himan home is initialized at ${root}.`,
|
|
287
|
+
details: { root, reposDir, storeDir },
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
async checkSourceResources() {
|
|
291
|
+
try {
|
|
292
|
+
const source = await this.loadSourceFromConfig();
|
|
293
|
+
const entries = await Promise.all(RESOURCE_TYPES.map(async (type) => [type, await source.list(type)]));
|
|
294
|
+
const counts = Object.fromEntries(entries.map(([type, resources]) => [type, resources.length]));
|
|
295
|
+
const total = RESOURCE_TYPES.reduce((sum, type) => sum + counts[type], 0);
|
|
296
|
+
return {
|
|
297
|
+
name: "resources",
|
|
298
|
+
status: "ok",
|
|
299
|
+
message: `Scanned ${total} resources from current source.`,
|
|
300
|
+
details: { counts },
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
return this.errorCheck("resources", "Cannot scan current source resources.", error);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
async checkAgents(projectDir) {
|
|
308
|
+
try {
|
|
309
|
+
const settings = await this.getAgentSettings(projectDir);
|
|
310
|
+
const scope = settings.project ? "project" : settings.global ? "global" : "default";
|
|
311
|
+
return {
|
|
312
|
+
name: "agents",
|
|
313
|
+
status: "ok",
|
|
314
|
+
message: `Effective agents: ${settings.effective.join(", ")} (${scope}).`,
|
|
315
|
+
details: settings,
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
catch (error) {
|
|
319
|
+
return this.errorCheck("agents", "Cannot resolve effective agents.", error);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
async checkProjectLock(projectDir) {
|
|
323
|
+
const lockPath = this.lockStore.getLockPath(projectDir);
|
|
324
|
+
const { lock, state } = await this.lockStore.loadWithState(projectDir);
|
|
325
|
+
if (state === "missing") {
|
|
326
|
+
return {
|
|
327
|
+
check: {
|
|
328
|
+
name: "lock",
|
|
329
|
+
status: "ok",
|
|
330
|
+
message: "No himan.lock found for this project yet.",
|
|
331
|
+
details: { lockPath },
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
if (state === "invalid" || !lock) {
|
|
336
|
+
return {
|
|
337
|
+
check: {
|
|
338
|
+
name: "lock",
|
|
339
|
+
status: "error",
|
|
340
|
+
message: `Lock file is invalid: ${lockPath}`,
|
|
341
|
+
details: { lockPath },
|
|
342
|
+
},
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
if (lock.resources.length === 0) {
|
|
346
|
+
return {
|
|
347
|
+
check: {
|
|
348
|
+
name: "lock",
|
|
349
|
+
status: "warn",
|
|
350
|
+
message: "himan.lock has no resources.",
|
|
351
|
+
details: { lockPath },
|
|
352
|
+
},
|
|
353
|
+
lock,
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
return {
|
|
357
|
+
check: {
|
|
358
|
+
name: "lock",
|
|
359
|
+
status: "ok",
|
|
360
|
+
message: `himan.lock tracks ${lock.resources.length} resources.`,
|
|
361
|
+
details: { lockPath, source: lock.source },
|
|
362
|
+
},
|
|
363
|
+
lock,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
async checkProjectArchiveStatus(lock) {
|
|
367
|
+
if (!lock || lock.resources.length === 0) {
|
|
368
|
+
return {
|
|
369
|
+
name: "archive",
|
|
370
|
+
status: "ok",
|
|
371
|
+
message: "No locked resources to check for archive status.",
|
|
372
|
+
};
|
|
373
|
+
}
|
|
374
|
+
try {
|
|
375
|
+
const source = await this.loadSourceFromLock(this.normalizeLockSourceInfo(lock.source));
|
|
376
|
+
const archived = [];
|
|
377
|
+
for (const resource of lock.resources) {
|
|
378
|
+
if (await source.isArchived(resource.type, resource.name)) {
|
|
379
|
+
archived.push(`${resource.type}/${resource.name}@${resource.version}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
if (archived.length > 0) {
|
|
383
|
+
return {
|
|
384
|
+
name: "archive",
|
|
385
|
+
status: "warn",
|
|
386
|
+
message: `${archived.length} locked resources are archived in the current source.`,
|
|
387
|
+
details: { resources: archived },
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
return {
|
|
391
|
+
name: "archive",
|
|
392
|
+
status: "ok",
|
|
393
|
+
message: "No locked resources are archived in the current source.",
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
return {
|
|
398
|
+
name: "archive",
|
|
399
|
+
status: "warn",
|
|
400
|
+
message: `Cannot check archived resources. ${error instanceof Error ? error.message : String(error)}`,
|
|
401
|
+
};
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
async checkProjectTargets(projectDir, lock) {
|
|
405
|
+
if (!lock || lock.resources.length === 0) {
|
|
406
|
+
return {
|
|
407
|
+
name: "targets",
|
|
408
|
+
status: "ok",
|
|
409
|
+
message: "No locked project targets to verify.",
|
|
410
|
+
};
|
|
411
|
+
}
|
|
412
|
+
const missing = [];
|
|
413
|
+
for (const resource of lock.resources) {
|
|
414
|
+
const agents = normalizeAgents(resource.agents);
|
|
415
|
+
const targets = getProjectResourcePaths(projectDir, resource.type, resource.name, agents);
|
|
416
|
+
for (const targetPath of targets) {
|
|
417
|
+
if (!(await this.exists(targetPath))) {
|
|
418
|
+
missing.push({
|
|
419
|
+
resource: `${resource.type}/${resource.name}@${resource.version}`,
|
|
420
|
+
path: targetPath,
|
|
421
|
+
});
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (missing.length > 0) {
|
|
426
|
+
return {
|
|
427
|
+
name: "targets",
|
|
428
|
+
status: "warn",
|
|
429
|
+
message: `Missing ${missing.length} installed targets. Run \`himan install\` to restore them.`,
|
|
430
|
+
details: { missing },
|
|
431
|
+
};
|
|
432
|
+
}
|
|
433
|
+
return {
|
|
434
|
+
name: "targets",
|
|
435
|
+
status: "ok",
|
|
436
|
+
message: "All locked project targets exist.",
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
async list(type, agents, options = {}) {
|
|
192
440
|
const source = await this.loadSourceFromConfig();
|
|
193
|
-
const resources = await source.list(type);
|
|
441
|
+
const resources = await source.list(type, options);
|
|
194
442
|
if (!agents?.length)
|
|
195
443
|
return resources;
|
|
196
444
|
const selected = normalizeAgents(agents);
|
|
197
445
|
return resources.filter((resource) => normalizeAgents(resource.agents).some((agent) => selected.includes(agent)));
|
|
198
446
|
}
|
|
447
|
+
async archive(type, name, options = {}) {
|
|
448
|
+
this.validateResourceIdentity(type, name, "archive");
|
|
449
|
+
const source = await this.loadSourceFromConfig();
|
|
450
|
+
return source.archive(type, name, {
|
|
451
|
+
...options,
|
|
452
|
+
reason: options.reason?.trim(),
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
async restore(type, name, options = {}) {
|
|
456
|
+
this.validateResourceIdentity(type, name, "restore");
|
|
457
|
+
const source = await this.loadSourceFromConfig();
|
|
458
|
+
return source.restore(type, name, options);
|
|
459
|
+
}
|
|
199
460
|
async listInstalled(projectDir, type, agents) {
|
|
200
461
|
const { lock, state } = await this.lockStore.loadWithState(projectDir);
|
|
201
462
|
if (state === "invalid") {
|
|
@@ -222,31 +483,42 @@ export class ServiceFactory {
|
|
|
222
483
|
const source = await this.loadSourceFromConfig();
|
|
223
484
|
return source.history(type, name);
|
|
224
485
|
}
|
|
225
|
-
async install(type, name, version, projectDir, agents, mode = "copy") {
|
|
486
|
+
async install(type, name, version, projectDir, agents, mode = "copy", options = {}) {
|
|
226
487
|
const { source, sourceInfo } = await this.loadSourceWithInfoFromConfig();
|
|
227
|
-
return this.installWithSource(source, sourceInfo, type, name, version, projectDir, agents, mode);
|
|
488
|
+
return this.installWithSource(source, sourceInfo, type, name, version, projectDir, agents, mode, "project", options);
|
|
228
489
|
}
|
|
229
|
-
async installGlobal(type, name, version, projectDir, agents, mode = "copy") {
|
|
490
|
+
async installGlobal(type, name, version, projectDir, agents, mode = "copy", options = {}) {
|
|
230
491
|
const source = await this.loadSourceFromConfig();
|
|
231
|
-
return this.installWithSource(source, undefined, type, name, version, projectDir, agents, mode, "global");
|
|
492
|
+
return this.installWithSource(source, undefined, type, name, version, projectDir, agents, mode, "global", options);
|
|
232
493
|
}
|
|
233
494
|
async dev(type, name, projectDir) {
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
495
|
+
const projectTarget = await this.tryResolveProjectResourceTarget(projectDir, type, name);
|
|
496
|
+
if (projectTarget) {
|
|
497
|
+
return {
|
|
498
|
+
type,
|
|
499
|
+
name,
|
|
500
|
+
devPath: projectTarget.resourcePath,
|
|
501
|
+
linkPath: projectTarget.linkPaths[0],
|
|
502
|
+
mode: projectTarget.mode,
|
|
503
|
+
sourceScope: "project",
|
|
504
|
+
};
|
|
240
505
|
}
|
|
241
|
-
|
|
242
|
-
|
|
506
|
+
const globalTarget = await this.tryResolveGlobalResourceTarget(projectDir, type, name);
|
|
507
|
+
if (!globalTarget) {
|
|
508
|
+
throw new HimanError(errorCodes.INSTALL_NOT_FOUND, `Installed resource link not found for ${type}/${name}. Run install first.`);
|
|
509
|
+
}
|
|
510
|
+
const projectLinkPaths = getProjectResourcePaths(projectDir, type, name, globalTarget.agents);
|
|
511
|
+
for (const [index, linkPath] of projectLinkPaths.entries()) {
|
|
512
|
+
const sourcePath = globalTarget.linkPaths[index] ?? globalTarget.resourcePath;
|
|
513
|
+
await this.materializeResource(sourcePath, linkPath, "copy");
|
|
243
514
|
}
|
|
244
515
|
return {
|
|
245
516
|
type,
|
|
246
517
|
name,
|
|
247
|
-
devPath,
|
|
248
|
-
linkPath:
|
|
249
|
-
mode:
|
|
518
|
+
devPath: projectLinkPaths[0],
|
|
519
|
+
linkPath: projectLinkPaths[0],
|
|
520
|
+
mode: "copy",
|
|
521
|
+
sourceScope: "global",
|
|
250
522
|
};
|
|
251
523
|
}
|
|
252
524
|
async uninstall(type, name, projectDir) {
|
|
@@ -260,58 +532,120 @@ export class ServiceFactory {
|
|
|
260
532
|
await this.lockStore.removeResource(projectDir, { type, name });
|
|
261
533
|
return { type, name, linkPath: installInfo.linkPaths[0] };
|
|
262
534
|
}
|
|
263
|
-
async publish(type, name, releaseType, projectDir) {
|
|
535
|
+
async publish(type, name, releaseType, projectDir, options = {}) {
|
|
536
|
+
const installScope = options.installScope ?? "project";
|
|
537
|
+
this.reportPublishProgress(options, "prepare", `Preparing ${type}/${name}.`);
|
|
264
538
|
const source = await this.loadSourceFromConfig();
|
|
539
|
+
if (await source.isArchived(type, name)) {
|
|
540
|
+
throw new HimanError(errorCodes.RESOURCE_ARCHIVED, `Resource is archived: ${type}/${name}. Restore it before publishing a new version.`);
|
|
541
|
+
}
|
|
265
542
|
const sourceDir = await this.resolvePublishSourceDir(type, name, projectDir);
|
|
266
543
|
const existingInstallInfo = await this.tryResolveInstalledResource(projectDir, type, name);
|
|
544
|
+
const existingGlobalInstallInfo = await this.tryResolveGlobalResourceTarget(projectDir, type, name);
|
|
267
545
|
const history = await source.history(type, name);
|
|
268
546
|
const latest = history[0]?.version ?? "0.0.0";
|
|
269
547
|
const nextVersion = this.versions.nextVersion(latest, releaseType);
|
|
548
|
+
this.reportPublishProgress(options, "resolve-version", `Resolved ${releaseType} version ${nextVersion}.`);
|
|
549
|
+
this.reportPublishProgress(options, "publish-source", `Publishing ${type}/${name}@${nextVersion} to the Git source.`);
|
|
270
550
|
const result = await source.publish(type, name, nextVersion, sourceDir, {
|
|
271
551
|
releaseType,
|
|
272
552
|
});
|
|
273
553
|
const storePath = this.getStorePath(type, name, nextVersion);
|
|
554
|
+
this.reportPublishProgress(options, "sync-store", `Syncing ${type}/${name}@${nextVersion} into the local store.`);
|
|
274
555
|
if (!(await this.exists(storePath))) {
|
|
275
556
|
await source.pull(type, name, nextVersion, storePath);
|
|
276
557
|
}
|
|
277
558
|
const locked = await this.getLockedResource(projectDir, type, name);
|
|
278
559
|
const resourceMeta = await this.readResourceMetaFromDir(storePath, type);
|
|
279
560
|
const configuredAgents = await this.getConfiguredAgents(projectDir);
|
|
280
|
-
const nextAgents = locked?.agents?.length
|
|
281
|
-
? normalizeAgents(locked.agents)
|
|
282
|
-
: existingInstallInfo?.agents.length
|
|
283
|
-
? normalizeAgents(existingInstallInfo.agents)
|
|
284
|
-
: configuredAgents ?? normalizeAgents(resourceMeta?.agents);
|
|
285
561
|
const installMode = "copy";
|
|
286
|
-
const
|
|
562
|
+
const nextAgents = installScope === "global"
|
|
563
|
+
? existingGlobalInstallInfo?.agents.length
|
|
564
|
+
? normalizeAgents(existingGlobalInstallInfo.agents)
|
|
565
|
+
: existingInstallInfo?.agents.length
|
|
566
|
+
? normalizeAgents(existingInstallInfo.agents)
|
|
567
|
+
: locked?.agents?.length
|
|
568
|
+
? normalizeAgents(locked.agents)
|
|
569
|
+
: configuredAgents ?? normalizeAgents(resourceMeta?.agents)
|
|
570
|
+
: locked?.agents?.length
|
|
571
|
+
? normalizeAgents(locked.agents)
|
|
572
|
+
: existingInstallInfo?.agents.length
|
|
573
|
+
? normalizeAgents(existingInstallInfo.agents)
|
|
574
|
+
: configuredAgents ?? normalizeAgents(resourceMeta?.agents);
|
|
575
|
+
const linkPaths = installScope === "global"
|
|
576
|
+
? getGlobalResourcePaths(this.paths.getHomeDir(), type, name, nextAgents)
|
|
577
|
+
: getProjectResourcePaths(projectDir, type, name, nextAgents);
|
|
578
|
+
this.reportPublishProgress(options, "install", installScope === "global"
|
|
579
|
+
? `Installing published version globally for ${nextAgents.join(", ")}.`
|
|
580
|
+
: `Installing published version into the current project for ${nextAgents.join(", ")}.`);
|
|
287
581
|
for (const linkPath of linkPaths) {
|
|
288
582
|
await this.materializeResource(storePath, linkPath, installMode);
|
|
289
583
|
}
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
584
|
+
if (installScope === "project") {
|
|
585
|
+
const sourceInfo = await this.getLockSourceInfo();
|
|
586
|
+
await this.lockStore.upsertResource(projectDir, sourceInfo, {
|
|
587
|
+
type,
|
|
588
|
+
name,
|
|
589
|
+
version: nextVersion,
|
|
590
|
+
agents: nextAgents,
|
|
591
|
+
mode: installMode,
|
|
592
|
+
});
|
|
593
|
+
}
|
|
594
|
+
this.reportPublishProgress(options, "cleanup", `Cleaning up legacy dev copy if present.`);
|
|
298
595
|
await fs.rm(this.getProjectDevPath(projectDir, type, name), {
|
|
299
596
|
recursive: true,
|
|
300
597
|
force: true,
|
|
301
598
|
});
|
|
302
|
-
|
|
599
|
+
this.reportPublishProgress(options, "done", `Published ${type}/${name}@${result.version}.`);
|
|
600
|
+
return {
|
|
601
|
+
type,
|
|
602
|
+
name,
|
|
603
|
+
version: result.version,
|
|
604
|
+
tag: result.tag,
|
|
605
|
+
installScope,
|
|
606
|
+
linkPath: linkPaths[0],
|
|
607
|
+
};
|
|
303
608
|
}
|
|
304
609
|
async create(type, name, options, projectDir) {
|
|
305
610
|
this.validateCreateInput(type, name, options);
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
611
|
+
await this.loadSourceFromConfig();
|
|
612
|
+
const agents = await this.resolveEffectiveAgents(projectDir, options.agents);
|
|
613
|
+
const resourcePaths = getProjectResourcePaths(projectDir, type, name, agents);
|
|
614
|
+
const entry = options.entry ?? this.getDefaultEntry(type);
|
|
615
|
+
const files = resourcePaths.flatMap((resourcePath) => [
|
|
616
|
+
path.join(resourcePath, "himan.yaml"),
|
|
617
|
+
path.join(resourcePath, entry),
|
|
618
|
+
]);
|
|
619
|
+
const existingPaths = [];
|
|
620
|
+
for (const resourcePath of resourcePaths) {
|
|
621
|
+
if (await this.exists(resourcePath))
|
|
622
|
+
existingPaths.push(resourcePath);
|
|
623
|
+
}
|
|
624
|
+
if (existingPaths.length > 0 && !options.force) {
|
|
625
|
+
throw new HimanError(errorCodes.RESOURCE_EXISTS, `Resource already exists: ${type}/${name}`, { paths: existingPaths });
|
|
626
|
+
}
|
|
627
|
+
if (!options.dryRun) {
|
|
628
|
+
for (const resourcePath of resourcePaths) {
|
|
629
|
+
await fs.rm(resourcePath, { recursive: true, force: true });
|
|
630
|
+
await fs.mkdir(resourcePath, { recursive: true });
|
|
631
|
+
await fs.writeFile(path.join(resourcePath, "himan.yaml"), YAML.stringify({
|
|
632
|
+
name,
|
|
633
|
+
type,
|
|
634
|
+
version: "0.1.0",
|
|
635
|
+
entry,
|
|
636
|
+
description: options.description ?? `${type} resource ${name}`,
|
|
637
|
+
agents,
|
|
638
|
+
}), "utf8");
|
|
639
|
+
await fs.writeFile(path.join(resourcePath, entry), this.getDefaultContent(type, name), "utf8");
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
return {
|
|
643
|
+
type,
|
|
644
|
+
name,
|
|
645
|
+
resourceDir: resourcePaths[0],
|
|
646
|
+
files,
|
|
647
|
+
dryRun: Boolean(options.dryRun),
|
|
648
|
+
};
|
|
315
649
|
}
|
|
316
650
|
async rename(type, oldName, newName, projectDir, options = {}) {
|
|
317
651
|
this.validateRenameInput(type, oldName, newName);
|
|
@@ -353,12 +687,16 @@ export class ServiceFactory {
|
|
|
353
687
|
const lockSourceInfo = this.normalizeLockSourceInfo(lock.source);
|
|
354
688
|
const lockedSource = await this.loadSourceFromLock(lockSourceInfo);
|
|
355
689
|
for (const item of lock.resources) {
|
|
356
|
-
const result = await this.installWithSource(lockedSource, lockSourceInfo, item.type, item.name, item.version, projectDir, agents ?? item.agents, mode ?? this.resolveInstallMode(item.mode), "project");
|
|
690
|
+
const result = await this.installWithSource(lockedSource, lockSourceInfo, item.type, item.name, item.version, projectDir, agents ?? item.agents, mode ?? this.resolveInstallMode(item.mode), "project", { includeArchived: true });
|
|
357
691
|
results.push(result);
|
|
358
692
|
}
|
|
359
693
|
return results;
|
|
360
694
|
}
|
|
361
|
-
async installWithSource(source, sourceInfo, type, name, version, projectDir, agents, mode, scope = "project") {
|
|
695
|
+
async installWithSource(source, sourceInfo, type, name, version, projectDir, agents, mode, scope = "project", options = {}) {
|
|
696
|
+
const archived = await source.isArchived(type, name);
|
|
697
|
+
if (archived && !options.includeArchived) {
|
|
698
|
+
throw new HimanError(errorCodes.RESOURCE_ARCHIVED, `Resource is archived: ${type}/${name}. Use --include-archived to install an archived version explicitly.`);
|
|
699
|
+
}
|
|
362
700
|
const history = await source.history(type, name);
|
|
363
701
|
if (history.length === 0) {
|
|
364
702
|
throw new HimanError(errorCodes.RESOURCE_NOT_FOUND, `Resource not found: ${type}/${name}`);
|
|
@@ -636,6 +974,80 @@ export class ServiceFactory {
|
|
|
636
974
|
resolveInstallMode(mode) {
|
|
637
975
|
return mode === "link" ? "link" : "copy";
|
|
638
976
|
}
|
|
977
|
+
reportPublishProgress(options, stage, message) {
|
|
978
|
+
options.onProgress?.({ stage, message });
|
|
979
|
+
}
|
|
980
|
+
async tryResolveProjectResourceTarget(projectDir, type, name) {
|
|
981
|
+
const locked = await this.getLockedResource(projectDir, type, name);
|
|
982
|
+
const configuredAgents = await this.getConfiguredAgents(projectDir);
|
|
983
|
+
if (locked?.agents?.length || configuredAgents?.length) {
|
|
984
|
+
const agents = locked?.agents?.length
|
|
985
|
+
? normalizeAgents(locked.agents)
|
|
986
|
+
: (configuredAgents ?? normalizeAgents());
|
|
987
|
+
const linkPaths = getProjectResourcePaths(projectDir, type, name, agents);
|
|
988
|
+
const existingLinkPath = await this.findFirstExistingPath(linkPaths);
|
|
989
|
+
if (existingLinkPath) {
|
|
990
|
+
return {
|
|
991
|
+
resourcePath: existingLinkPath,
|
|
992
|
+
linkPaths,
|
|
993
|
+
agents,
|
|
994
|
+
mode: this.resolveInstallMode(locked?.mode ?? (await this.readPathMode(existingLinkPath))),
|
|
995
|
+
};
|
|
996
|
+
}
|
|
997
|
+
}
|
|
998
|
+
const existingCandidates = await this.findExistingAgentPaths(projectDir, type, name, "project");
|
|
999
|
+
if (existingCandidates.length === 0) {
|
|
1000
|
+
return undefined;
|
|
1001
|
+
}
|
|
1002
|
+
const existingAgents = normalizeAgents(existingCandidates.map((candidate) => candidate.agent));
|
|
1003
|
+
const linkPaths = getProjectResourcePaths(projectDir, type, name, existingAgents);
|
|
1004
|
+
return {
|
|
1005
|
+
resourcePath: existingCandidates[0].path,
|
|
1006
|
+
linkPaths,
|
|
1007
|
+
agents: existingAgents,
|
|
1008
|
+
mode: await this.readPathMode(existingCandidates[0].path),
|
|
1009
|
+
};
|
|
1010
|
+
}
|
|
1011
|
+
async tryResolveGlobalResourceTarget(projectDir, type, name) {
|
|
1012
|
+
const existingCandidates = await this.findExistingAgentPaths(projectDir, type, name, "global");
|
|
1013
|
+
if (existingCandidates.length === 0) {
|
|
1014
|
+
return undefined;
|
|
1015
|
+
}
|
|
1016
|
+
const agents = normalizeAgents(existingCandidates.map((candidate) => candidate.agent));
|
|
1017
|
+
const linkPaths = getGlobalResourcePaths(this.paths.getHomeDir(), type, name, agents);
|
|
1018
|
+
return {
|
|
1019
|
+
resourcePath: existingCandidates[0].path,
|
|
1020
|
+
linkPaths,
|
|
1021
|
+
agents,
|
|
1022
|
+
mode: await this.readPathMode(existingCandidates[0].path),
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
async findExistingAgentPaths(projectDir, type, name, scope) {
|
|
1026
|
+
const rootDir = scope === "global" ? this.paths.getHomeDir() : projectDir;
|
|
1027
|
+
const candidates = getSupportedAgentNames().map((agent) => ({
|
|
1028
|
+
agent,
|
|
1029
|
+
path: scope === "global"
|
|
1030
|
+
? getGlobalResourcePaths(rootDir, type, name, [agent])[0]
|
|
1031
|
+
: getProjectResourcePaths(rootDir, type, name, [agent])[0],
|
|
1032
|
+
}));
|
|
1033
|
+
const existingCandidates = [];
|
|
1034
|
+
for (const candidate of candidates) {
|
|
1035
|
+
if (await this.exists(candidate.path))
|
|
1036
|
+
existingCandidates.push(candidate);
|
|
1037
|
+
}
|
|
1038
|
+
return existingCandidates;
|
|
1039
|
+
}
|
|
1040
|
+
async findFirstExistingPath(paths) {
|
|
1041
|
+
for (const targetPath of paths) {
|
|
1042
|
+
if (await this.exists(targetPath))
|
|
1043
|
+
return targetPath;
|
|
1044
|
+
}
|
|
1045
|
+
return undefined;
|
|
1046
|
+
}
|
|
1047
|
+
async readPathMode(targetPath) {
|
|
1048
|
+
const stat = await fs.lstat(targetPath);
|
|
1049
|
+
return stat.isSymbolicLink() ? "link" : "copy";
|
|
1050
|
+
}
|
|
639
1051
|
async resolveInstalledResource(projectDir, type, name) {
|
|
640
1052
|
const locked = await this.getLockedResource(projectDir, type, name);
|
|
641
1053
|
const configuredAgents = await this.getConfiguredAgents(projectDir);
|
|
@@ -816,6 +1228,13 @@ export class ServiceFactory {
|
|
|
816
1228
|
.filter(Boolean);
|
|
817
1229
|
return items.length > 0 ? items : undefined;
|
|
818
1230
|
}
|
|
1231
|
+
errorCheck(name, message, error) {
|
|
1232
|
+
return {
|
|
1233
|
+
name,
|
|
1234
|
+
status: "error",
|
|
1235
|
+
message: `${message} ${error instanceof Error ? error.message : String(error)}`,
|
|
1236
|
+
};
|
|
1237
|
+
}
|
|
819
1238
|
async exists(targetPath) {
|
|
820
1239
|
try {
|
|
821
1240
|
await fs.access(targetPath);
|
|
@@ -830,6 +1249,10 @@ export class ServiceFactory {
|
|
|
830
1249
|
if (await this.exists(devPath)) {
|
|
831
1250
|
return devPath;
|
|
832
1251
|
}
|
|
1252
|
+
const projectTarget = await this.tryResolveProjectResourceTarget(projectDir, type, name);
|
|
1253
|
+
if (projectTarget) {
|
|
1254
|
+
return projectTarget.resourcePath;
|
|
1255
|
+
}
|
|
833
1256
|
const repoResourceDir = await this.getRepoResourceDir(type, name);
|
|
834
1257
|
if (await this.exists(repoResourceDir)) {
|
|
835
1258
|
return repoResourceDir;
|
|
@@ -857,6 +1280,15 @@ export class ServiceFactory {
|
|
|
857
1280
|
getDefaultEntry(type) {
|
|
858
1281
|
return type === "skill" ? "SKILL.md" : "content.md";
|
|
859
1282
|
}
|
|
1283
|
+
getDefaultContent(type, name) {
|
|
1284
|
+
if (type === "rule") {
|
|
1285
|
+
return `# ${name}\n\nDescribe rule instructions here.\n`;
|
|
1286
|
+
}
|
|
1287
|
+
if (type === "command") {
|
|
1288
|
+
return `# ${name}\n\nDescribe command behavior here.\n`;
|
|
1289
|
+
}
|
|
1290
|
+
return `# ${name}\n\nDescribe skill workflow here.\n`;
|
|
1291
|
+
}
|
|
860
1292
|
validateCreateInput(type, name, options) {
|
|
861
1293
|
if (!["rule", "command", "skill"].includes(type)) {
|
|
862
1294
|
throw new HimanError(errorCodes.UNSUPPORTED_RESOURCE_TYPE, `Unsupported resource type for create: ${type}`);
|
|
@@ -868,6 +1300,14 @@ export class ServiceFactory {
|
|
|
868
1300
|
throw new HimanError(errorCodes.TEMPLATE_NOT_FOUND, `Template not found: ${options.template}`);
|
|
869
1301
|
}
|
|
870
1302
|
}
|
|
1303
|
+
validateResourceIdentity(type, name, action) {
|
|
1304
|
+
if (!["rule", "command", "skill"].includes(type)) {
|
|
1305
|
+
throw new HimanError(errorCodes.UNSUPPORTED_RESOURCE_TYPE, `Unsupported resource type for ${action}: ${type}`);
|
|
1306
|
+
}
|
|
1307
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name)) {
|
|
1308
|
+
throw new HimanError(errorCodes.INVALID_RESOURCE_NAME, `Invalid resource name: ${name}. Use kebab-case only.`);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
871
1311
|
validateRenameInput(type, oldName, newName) {
|
|
872
1312
|
if (!["rule", "command", "skill"].includes(type)) {
|
|
873
1313
|
throw new HimanError(errorCodes.UNSUPPORTED_RESOURCE_TYPE, `Unsupported resource type for rename: ${type}`);
|
package/dist/utils/errors.js
CHANGED
|
@@ -13,6 +13,7 @@ export const errorCodes = {
|
|
|
13
13
|
NOT_IMPLEMENTED: "E_NOT_IMPLEMENTED",
|
|
14
14
|
INVALID_INPUT: "E_INVALID_INPUT",
|
|
15
15
|
RESOURCE_NOT_FOUND: "E_RESOURCE_NOT_FOUND",
|
|
16
|
+
RESOURCE_ARCHIVED: "E_RESOURCE_ARCHIVED",
|
|
16
17
|
VERSION_NOT_FOUND: "E_VERSION_NOT_FOUND",
|
|
17
18
|
INSTALL_NOT_FOUND: "E_INSTALL_NOT_FOUND",
|
|
18
19
|
LOCK_NOT_FOUND: "E_LOCK_NOT_FOUND",
|
package/docs/error-codes.md
CHANGED
|
@@ -58,6 +58,12 @@
|
|
|
58
58
|
- **常见触发**:安装不存在的资源、切换到不存在的 source 名称、发布目标资源不存在。
|
|
59
59
|
- **建议处理**:先执行 `himan list <type>` / `himan source list` 确认名称。
|
|
60
60
|
|
|
61
|
+
### `E_RESOURCE_ARCHIVED`
|
|
62
|
+
|
|
63
|
+
- **含义**:资源已归档,默认不允许作为 active 资源继续使用。
|
|
64
|
+
- **常见触发**:直接安装或发布已归档资源,或重复归档已经在 `archive/<plural>/<name>` 下的资源。
|
|
65
|
+
- **建议处理**:如需继续维护,先执行 `himan resource restore <type> <name>`;如只需安装历史版本,显式传 `--include-archived`。
|
|
66
|
+
|
|
61
67
|
### `E_VERSION_NOT_FOUND`
|
|
62
68
|
|
|
63
69
|
- **含义**:指定版本不存在。
|