moluoxixi-ai-rules 0.2.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.
@@ -4,7 +4,7 @@ import { fileURLToPath } from 'node:url';
4
4
  import { HOST_IDS, resolveHostId } from '../../constants/hosts.js';
5
5
  import { ensureGlobalSkillLink, ensureInstallRoot, getDefaultInstallPaths, projectHostById, runSkillSetupCommands, } from './install.js';
6
6
  import { resolveRoleManifestPath } from './roles.js';
7
- import { rebuildVendorAssets } from './vendor-staging.js';
7
+ import { cleanupEmptyVendorSkillDirectories, rebuildVendorAssets } from './vendor-staging.js';
8
8
  import { ensureVendorRepo, verifyVendorRepoRevision } from './vendor-sync.js';
9
9
  import { loadVendorManifest } from './vendors.js';
10
10
  import { verifyGlobalAgentSkills, verifyHost } from './verify.js';
@@ -105,6 +105,7 @@ export async function syncToHosts(options) {
105
105
  }
106
106
  }
107
107
  }
108
+ cleanupEmptyVendorSkillDirectories(paths.moluoHome);
108
109
  if (failedHosts.length > 0) {
109
110
  throw new Error(`Host verification failed: ${failedHosts.join(', ')}`);
110
111
  }
@@ -8,4 +8,5 @@ export interface RebuildVendorAssetsOptions {
8
8
  role: string;
9
9
  manifestPath: string;
10
10
  }
11
+ export declare function cleanupEmptyVendorSkillDirectories(homeDir: string): void;
11
12
  export declare function rebuildVendorAssets(options: RebuildVendorAssetsOptions): Promise<VendorAssetInventory>;
@@ -412,6 +412,71 @@ function rollbackManagedEntries(entries) {
412
412
  }
413
413
  return rollbackErrors;
414
414
  }
415
+ function managedChildNames(currentRoot, nextRoot, label) {
416
+ const names = new Map();
417
+ for (const root of [currentRoot, nextRoot]) {
418
+ if (!fs.existsSync(root))
419
+ continue;
420
+ requirePlainDirectoryOrMissing(root, label);
421
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
422
+ if (!entry.isDirectory() && !entry.isFile())
423
+ throw new Error(`${label} has unsupported filesystem type: ${path.join(root, entry.name)}`);
424
+ const key = entry.name.toLowerCase();
425
+ const previous = names.get(key);
426
+ if (previous !== undefined && previous !== entry.name)
427
+ throw new Error(`${label} names differ only by case: ${previous}, ${entry.name}`);
428
+ names.set(key, entry.name);
429
+ }
430
+ }
431
+ return [...names.values()].sort(compareStable);
432
+ }
433
+ function isPlainDirectory(target) {
434
+ const stats = fs.lstatSync(target, { throwIfNoEntry: false });
435
+ return stats !== undefined && stats.isDirectory() && !stats.isSymbolicLink();
436
+ }
437
+ function commitDirectoryContents(currentRoot, nextRoot, backupRoot, committedEntries, emptiedRoots) {
438
+ const childNames = managedChildNames(currentRoot, nextRoot, 'Managed vendor skill directory');
439
+ for (const name of childNames) {
440
+ const currentChild = path.join(currentRoot, name);
441
+ const nextChild = path.join(nextRoot, name);
442
+ const hasNextChild = fs.existsSync(nextChild);
443
+ if (isPlainDirectory(currentChild) && (!hasNextChild || isPlainDirectory(nextChild))) {
444
+ commitDirectoryContents(currentChild, nextChild, path.join(backupRoot, name), committedEntries, emptiedRoots);
445
+ if (!hasNextChild)
446
+ emptiedRoots.push(currentChild);
447
+ continue;
448
+ }
449
+ const entry = {
450
+ current: currentChild,
451
+ backup: path.join(backupRoot, name),
452
+ movedCurrent: false,
453
+ installedNext: false,
454
+ };
455
+ committedEntries.push(entry);
456
+ if (fs.existsSync(entry.current)) {
457
+ fs.mkdirSync(path.dirname(entry.backup), { recursive: true });
458
+ fs.renameSync(entry.current, entry.backup);
459
+ entry.movedCurrent = true;
460
+ }
461
+ if (fs.existsSync(nextChild)) {
462
+ fs.renameSync(nextChild, entry.current);
463
+ entry.installedNext = true;
464
+ }
465
+ }
466
+ }
467
+ function commitManagedChildren(spec, committedEntries, createdRoots, emptiedRoots) {
468
+ requirePlainDirectoryOrMissing(spec.current, 'Managed vendor skills root');
469
+ requirePlainDirectoryOrMissing(spec.next, 'Staged vendor skills root');
470
+ const childNames = managedChildNames(spec.current, spec.next, 'Managed vendor skill');
471
+ if (!fs.existsSync(spec.current)) {
472
+ if (!fs.existsSync(spec.next))
473
+ return;
474
+ fs.mkdirSync(spec.current, { recursive: true });
475
+ createdRoots.push(spec.current);
476
+ }
477
+ if (childNames.length > 0)
478
+ commitDirectoryContents(spec.current, spec.next, spec.backup, committedEntries, emptiedRoots);
479
+ }
415
480
  async function commitManagedEntries(stagingRoot, roleStagingRoot, homeDir, role) {
416
481
  const resolvedHome = path.resolve(homeDir);
417
482
  const vendorRoot = path.join(resolvedHome, 'vendor');
@@ -429,6 +494,8 @@ async function commitManagedEntries(stagingRoot, roleStagingRoot, homeDir, role)
429
494
  const nextRoot = path.join(nextWorkspace, 'vendor');
430
495
  const backupRoot = fs.mkdtempSync(path.join(resolvedHome, '.airules-vendor-backup-'));
431
496
  const committedEntries = [];
497
+ const createdRoots = [];
498
+ const emptiedRoots = [];
432
499
  try {
433
500
  fs.mkdirSync(nextRoot);
434
501
  fs.cpSync(stagingRoot, nextRoot, { recursive: true, dereference: true });
@@ -436,6 +503,7 @@ async function commitManagedEntries(stagingRoot, roleStagingRoot, homeDir, role)
436
503
  current: path.join(vendorRoot, name),
437
504
  next: path.join(nextRoot, name),
438
505
  backup: path.join(backupRoot, 'vendor', name),
506
+ preserveRoot: name === 'skills',
439
507
  }));
440
508
  if (roleStagingRoot !== undefined) {
441
509
  const nextRole = path.join(nextWorkspace, 'roles', role);
@@ -448,6 +516,10 @@ async function commitManagedEntries(stagingRoot, roleStagingRoot, homeDir, role)
448
516
  });
449
517
  }
450
518
  for (const spec of specs) {
519
+ if (spec.preserveRoot) {
520
+ commitManagedChildren(spec, committedEntries, createdRoots, emptiedRoots);
521
+ continue;
522
+ }
451
523
  const entry = {
452
524
  current: spec.current,
453
525
  backup: spec.backup,
@@ -469,6 +541,14 @@ async function commitManagedEntries(stagingRoot, roleStagingRoot, homeDir, role)
469
541
  }
470
542
  catch (error) {
471
543
  const rollbackErrors = rollbackManagedEntries(committedEntries);
544
+ for (const root of [...createdRoots].reverse()) {
545
+ try {
546
+ fs.rmdirSync(root);
547
+ }
548
+ catch (rollbackError) {
549
+ rollbackErrors.push(rollbackError instanceof Error ? rollbackError : new Error(String(rollbackError)));
550
+ }
551
+ }
472
552
  removeBestEffort(nextWorkspace);
473
553
  if (rollbackErrors.length > 0) {
474
554
  throw new AggregateError([error, ...rollbackErrors], `Role and vendor staging commit failed and rollback did not restore every managed asset: ${String(error)}`);
@@ -476,9 +556,36 @@ async function commitManagedEntries(stagingRoot, roleStagingRoot, homeDir, role)
476
556
  removeBestEffort(backupRoot);
477
557
  throw new Error(`Role and vendor staging commit failed; previous managed assets restored: ${String(error)}`, { cause: error });
478
558
  }
559
+ for (const root of emptiedRoots)
560
+ removeEmptyDirectoryBestEffort(root);
479
561
  removeBestEffort(nextWorkspace);
480
562
  removeBestEffort(backupRoot);
481
563
  }
564
+ function removeEmptyDirectoryBestEffort(target) {
565
+ try {
566
+ fs.rmdirSync(target);
567
+ }
568
+ catch {
569
+ // Host junctions can keep an empty skill directory locked until projection finishes.
570
+ }
571
+ }
572
+ function cleanupEmptyDirectoryTree(root) {
573
+ for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
574
+ if (entry.isDirectory())
575
+ cleanupEmptyDirectoryTree(path.join(root, entry.name));
576
+ }
577
+ removeEmptyDirectoryBestEffort(root);
578
+ }
579
+ export function cleanupEmptyVendorSkillDirectories(homeDir) {
580
+ const skillsRoot = path.resolve(homeDir, 'vendor', 'skills');
581
+ if (!fs.existsSync(skillsRoot))
582
+ return;
583
+ requirePlainDirectoryOrMissing(skillsRoot, 'Managed vendor skills root');
584
+ for (const entry of fs.readdirSync(skillsRoot, { withFileTypes: true })) {
585
+ if (entry.isDirectory())
586
+ cleanupEmptyDirectoryTree(path.join(skillsRoot, entry.name));
587
+ }
588
+ }
482
589
  export async function rebuildVendorAssets(options) {
483
590
  const role = requireRoleName(options.role);
484
591
  const manifest = await loadVendorManifest(options.manifestPath);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "moluoxixi-ai-rules",
3
3
  "type": "module",
4
- "version": "0.2.0",
4
+ "version": "0.2.2",
5
5
  "description": "Remote AI role asset distribution for supported hosts.",
6
6
  "license": "MIT",
7
7
  "repository": "https://github.com/moluoxixi/AIRules",