moluoxixi-ai-rules 0.2.1 → 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,38 +412,43 @@ function rollbackManagedEntries(entries) {
412
412
  }
413
413
  return rollbackErrors;
414
414
  }
415
- function managedChildNames(currentRoot, nextRoot) {
415
+ function managedChildNames(currentRoot, nextRoot, label) {
416
416
  const names = new Map();
417
417
  for (const root of [currentRoot, nextRoot]) {
418
418
  if (!fs.existsSync(root))
419
419
  continue;
420
- requirePlainDirectoryOrMissing(root, 'Managed vendor skills root');
420
+ requirePlainDirectoryOrMissing(root, label);
421
421
  for (const entry of fs.readdirSync(root, { withFileTypes: true })) {
422
422
  if (!entry.isDirectory() && !entry.isFile())
423
- throw new Error(`Managed vendor skill has unsupported filesystem type: ${path.join(root, entry.name)}`);
423
+ throw new Error(`${label} has unsupported filesystem type: ${path.join(root, entry.name)}`);
424
424
  const key = entry.name.toLowerCase();
425
425
  const previous = names.get(key);
426
426
  if (previous !== undefined && previous !== entry.name)
427
- throw new Error(`Managed vendor skill names differ only by case: ${previous}, ${entry.name}`);
427
+ throw new Error(`${label} names differ only by case: ${previous}, ${entry.name}`);
428
428
  names.set(key, entry.name);
429
429
  }
430
430
  }
431
431
  return [...names.values()].sort(compareStable);
432
432
  }
433
- function commitManagedChildren(spec, committedEntries, createdRoots) {
434
- requirePlainDirectoryOrMissing(spec.current, 'Managed vendor skills root');
435
- requirePlainDirectoryOrMissing(spec.next, 'Staged vendor skills root');
436
- const childNames = managedChildNames(spec.current, spec.next);
437
- if (!fs.existsSync(spec.current)) {
438
- if (!fs.existsSync(spec.next))
439
- return;
440
- fs.mkdirSync(spec.current, { recursive: true });
441
- createdRoots.push(spec.current);
442
- }
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');
443
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
+ }
444
449
  const entry = {
445
- current: path.join(spec.current, name),
446
- backup: path.join(spec.backup, name),
450
+ current: currentChild,
451
+ backup: path.join(backupRoot, name),
447
452
  movedCurrent: false,
448
453
  installedNext: false,
449
454
  };
@@ -453,13 +458,25 @@ function commitManagedChildren(spec, committedEntries, createdRoots) {
453
458
  fs.renameSync(entry.current, entry.backup);
454
459
  entry.movedCurrent = true;
455
460
  }
456
- const nextChild = path.join(spec.next, name);
457
461
  if (fs.existsSync(nextChild)) {
458
462
  fs.renameSync(nextChild, entry.current);
459
463
  entry.installedNext = true;
460
464
  }
461
465
  }
462
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
+ }
463
480
  async function commitManagedEntries(stagingRoot, roleStagingRoot, homeDir, role) {
464
481
  const resolvedHome = path.resolve(homeDir);
465
482
  const vendorRoot = path.join(resolvedHome, 'vendor');
@@ -478,6 +495,7 @@ async function commitManagedEntries(stagingRoot, roleStagingRoot, homeDir, role)
478
495
  const backupRoot = fs.mkdtempSync(path.join(resolvedHome, '.airules-vendor-backup-'));
479
496
  const committedEntries = [];
480
497
  const createdRoots = [];
498
+ const emptiedRoots = [];
481
499
  try {
482
500
  fs.mkdirSync(nextRoot);
483
501
  fs.cpSync(stagingRoot, nextRoot, { recursive: true, dereference: true });
@@ -499,7 +517,7 @@ async function commitManagedEntries(stagingRoot, roleStagingRoot, homeDir, role)
499
517
  }
500
518
  for (const spec of specs) {
501
519
  if (spec.preserveRoot) {
502
- commitManagedChildren(spec, committedEntries, createdRoots);
520
+ commitManagedChildren(spec, committedEntries, createdRoots, emptiedRoots);
503
521
  continue;
504
522
  }
505
523
  const entry = {
@@ -538,9 +556,36 @@ async function commitManagedEntries(stagingRoot, roleStagingRoot, homeDir, role)
538
556
  removeBestEffort(backupRoot);
539
557
  throw new Error(`Role and vendor staging commit failed; previous managed assets restored: ${String(error)}`, { cause: error });
540
558
  }
559
+ for (const root of emptiedRoots)
560
+ removeEmptyDirectoryBestEffort(root);
541
561
  removeBestEffort(nextWorkspace);
542
562
  removeBestEffort(backupRoot);
543
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
+ }
544
589
  export async function rebuildVendorAssets(options) {
545
590
  const role = requireRoleName(options.role);
546
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.1",
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",