aiblueprint-cli 1.4.73 → 1.4.74

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.
Files changed (2) hide show
  1. package/dist/cli.js +415 -25
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -33628,6 +33628,8 @@ async function mergeCodexConfigFile(sourceConfigPath, codexDir) {
33628
33628
  // src/lib/configs-store.ts
33629
33629
  var import_fs_extra7 = __toESM(require_lib4(), 1);
33630
33630
  import path11 from "path";
33631
+ var RETENTION_MANAGED_BACKUP_TRIGGERS = new Set(["setup", "sync", "load", "backup-load"]);
33632
+ var DEFAULT_BACKUP_RETENTION_DAYS = 30;
33631
33633
  function getConfigStorePaths(rootDir) {
33632
33634
  const baseDir = path11.join(rootDir, ".aiblueprint");
33633
33635
  return {
@@ -33866,6 +33868,117 @@ async function listConfigBackups(options = {}) {
33866
33868
  const paths = getConfigStorePaths(folders.rootDir);
33867
33869
  return listSnapshots(paths.backupsDir, "backup");
33868
33870
  }
33871
+ function normalizeBackupRetentionDays(value = DEFAULT_BACKUP_RETENTION_DAYS) {
33872
+ if (!Number.isInteger(value) || value <= 0) {
33873
+ throw new Error("Backup retention days must be a positive integer.");
33874
+ }
33875
+ return value;
33876
+ }
33877
+ function isRetentionManagedBackup(metadata, includeManual = false) {
33878
+ return RETENTION_MANAGED_BACKUP_TRIGGERS.has(metadata.trigger) || includeManual && metadata.trigger === "manual-backup";
33879
+ }
33880
+ function isPathInside(childPath, parentPath) {
33881
+ const relativePath = path11.relative(parentPath, childPath);
33882
+ return relativePath !== "" && !relativePath.startsWith("..") && !path11.isAbsolute(relativePath);
33883
+ }
33884
+ async function realBackupStorePath(backupsDir) {
33885
+ if (!await import_fs_extra7.default.pathExists(backupsDir))
33886
+ return null;
33887
+ const stats = await import_fs_extra7.default.lstat(backupsDir);
33888
+ if (stats.isSymbolicLink()) {
33889
+ throw new Error("Backup directory cannot be a symlink.");
33890
+ }
33891
+ if (!stats.isDirectory()) {
33892
+ throw new Error("Backup path is not a directory.");
33893
+ }
33894
+ return import_fs_extra7.default.realpath(backupsDir);
33895
+ }
33896
+ function cleanupDateForSnapshot(snapshot) {
33897
+ if (snapshot.metadata.type !== "backup")
33898
+ return null;
33899
+ const createdAt = new Date(snapshot.metadata.createdAt);
33900
+ if (Number.isNaN(createdAt.getTime()))
33901
+ return null;
33902
+ return createdAt;
33903
+ }
33904
+ async function removeBackupSnapshot(snapshot, backupsRealPath) {
33905
+ const snapshotRealPath = await import_fs_extra7.default.realpath(snapshot.path);
33906
+ if (!isPathInside(snapshotRealPath, backupsRealPath)) {
33907
+ throw new Error(`Refusing to delete backup outside store: ${snapshot.name}`);
33908
+ }
33909
+ await import_fs_extra7.default.remove(snapshot.path);
33910
+ }
33911
+ async function cleanConfigBackups(options = {}) {
33912
+ const days = normalizeBackupRetentionDays(options.days);
33913
+ const folders = resolveConfigStoreFolders(options);
33914
+ const paths = getConfigStorePaths(folders.rootDir);
33915
+ const backupsRealPath = await realBackupStorePath(paths.backupsDir);
33916
+ const now = options.now ?? new Date;
33917
+ const cutoffDate = new Date(now.getTime() - days * 24 * 60 * 60 * 1000);
33918
+ if (!backupsRealPath) {
33919
+ return {
33920
+ cutoff: cutoffDate.toISOString(),
33921
+ deleted: [],
33922
+ failed: [],
33923
+ kept: [],
33924
+ skipped: []
33925
+ };
33926
+ }
33927
+ const backups = await listSnapshots(paths.backupsDir, "backup");
33928
+ const deleted = [];
33929
+ const failed = [];
33930
+ const kept = [];
33931
+ const skipped = [];
33932
+ const deletionCandidates = [];
33933
+ for (const backup of backups) {
33934
+ if (!isRetentionManagedBackup(backup.metadata, options.includeManual)) {
33935
+ skipped.push({ snapshot: backup, reason: "not retention-managed" });
33936
+ continue;
33937
+ }
33938
+ const cleanupDate = cleanupDateForSnapshot(backup);
33939
+ if (!cleanupDate) {
33940
+ skipped.push({ snapshot: backup, reason: "missing valid backup metadata date" });
33941
+ continue;
33942
+ }
33943
+ if (cleanupDate < cutoffDate) {
33944
+ deletionCandidates.push(backup);
33945
+ } else {
33946
+ kept.push(backup);
33947
+ }
33948
+ }
33949
+ if (options.dryRun) {
33950
+ deleted.push(...deletionCandidates);
33951
+ } else {
33952
+ for (const backup of deletionCandidates) {
33953
+ try {
33954
+ await removeBackupSnapshot(backup, backupsRealPath);
33955
+ deleted.push(backup);
33956
+ } catch (error) {
33957
+ failed.push({
33958
+ snapshot: backup,
33959
+ error: error instanceof Error ? error.message : String(error)
33960
+ });
33961
+ }
33962
+ }
33963
+ if (deleted.length > 0 || failed.length > 0) {
33964
+ await appendHistory(paths, {
33965
+ action: "backup-clean",
33966
+ days,
33967
+ cutoff: cutoffDate.toISOString(),
33968
+ deleted: deleted.map((backup) => backup.name),
33969
+ failed: failed.map((entry) => ({ name: entry.snapshot.name, error: entry.error })),
33970
+ skipped: skipped.map((entry) => ({ name: entry.snapshot.name, reason: entry.reason }))
33971
+ });
33972
+ }
33973
+ }
33974
+ return {
33975
+ cutoff: cutoffDate.toISOString(),
33976
+ deleted,
33977
+ failed,
33978
+ kept,
33979
+ skipped
33980
+ };
33981
+ }
33869
33982
 
33870
33983
  // src/lib/codex-agents-renderer.ts
33871
33984
  var import_fs_extra8 = __toESM(require_lib4(), 1);
@@ -34083,6 +34196,17 @@ async function resolveClaudeAssetPath(sourceDir, name) {
34083
34196
  }
34084
34197
  return null;
34085
34198
  }
34199
+ async function resolveDirectoryCopyDestination(destination) {
34200
+ const stat = await import_fs_extra9.default.lstat(destination).catch(() => null);
34201
+ if (!stat?.isSymbolicLink())
34202
+ return destination;
34203
+ const targetStat = await import_fs_extra9.default.stat(destination).catch(() => null);
34204
+ if (targetStat?.isDirectory()) {
34205
+ return import_fs_extra9.default.realpath(destination);
34206
+ }
34207
+ await import_fs_extra9.default.remove(destination);
34208
+ return destination;
34209
+ }
34086
34210
  async function setupCommand(params = {}) {
34087
34211
  const {
34088
34212
  folder,
@@ -34195,11 +34319,10 @@ async function setupCommand(params = {}) {
34195
34319
  s.start("Setting up scripts");
34196
34320
  const scriptsSource = await resolveClaudeAssetPath(sourceDir, "scripts");
34197
34321
  if (scriptsSource) {
34198
- await import_fs_extra9.default.copy(scriptsSource, path13.join(claudeDir, "scripts"), {
34199
- overwrite: true
34200
- });
34201
- await replacePathPlaceholdersInDir(path13.join(claudeDir, "scripts"), claudeDir);
34202
- await import_fs_extra9.default.ensureDir(path13.join(claudeDir, "scripts/statusline/data"));
34322
+ const scriptsDestination = path13.join(claudeDir, "scripts");
34323
+ await import_fs_extra9.default.copy(scriptsSource, await resolveDirectoryCopyDestination(scriptsDestination), { overwrite: true });
34324
+ await replacePathPlaceholdersInDir(scriptsDestination, claudeDir);
34325
+ await import_fs_extra9.default.ensureDir(path13.join(scriptsDestination, "statusline/data"));
34203
34326
  s.stop("Scripts installed");
34204
34327
  } else {
34205
34328
  s.stop("Scripts not available in repository");
@@ -35738,6 +35861,7 @@ var IGNORED_ENTRY_NAMES2 = new Set([
35738
35861
  ".git",
35739
35862
  "node_modules"
35740
35863
  ]);
35864
+ var RULE_EXTENSION_RENAME_REASON = "Rule file extension normalized from .mdc to .md";
35741
35865
  function uniqueByPath(candidates) {
35742
35866
  const seen = new Set;
35743
35867
  const unique = [];
@@ -35971,6 +36095,60 @@ async function hashPath(targetPath) {
35971
36095
  }
35972
36096
  return hashString(`other:${stat.mode}:${stat.size}`);
35973
36097
  }
36098
+ async function hashRulePath(targetPath) {
36099
+ const stat = await import_fs_extra13.default.lstat(targetPath);
36100
+ if (stat.isSymbolicLink()) {
36101
+ const linkTarget = await import_fs_extra13.default.readlink(targetPath);
36102
+ return hashString(`symlink:${linkTarget}`);
36103
+ }
36104
+ if (stat.isFile()) {
36105
+ const fileHash = crypto.createHash("sha256");
36106
+ fileHash.update("file:");
36107
+ fileHash.update(await import_fs_extra13.default.readFile(targetPath));
36108
+ return fileHash.digest("hex");
36109
+ }
36110
+ if (stat.isDirectory()) {
36111
+ const entries = (await import_fs_extra13.default.readdir(targetPath, { withFileTypes: true })).filter((entry) => !IGNORED_ENTRY_NAMES2.has(entry.name)).sort(compareRuleEntryNames);
36112
+ const canonicalEntries = [];
36113
+ const usedNames = new Set;
36114
+ const hashesByName = new Map;
36115
+ const dirHash = crypto.createHash("sha256");
36116
+ dirHash.update("dir:");
36117
+ for (const entry of entries) {
36118
+ const entryHash = await hashRulePath(path17.join(targetPath, entry.name));
36119
+ let entryName = entry.isDirectory() ? entry.name : normalizeRuleFileName(entry.name);
36120
+ const existingHash = hashesByName.get(entryName);
36121
+ if (existingHash) {
36122
+ if (existingHash === entryHash)
36123
+ continue;
36124
+ entryName = findAvailableRuleName(entryName, usedNames);
36125
+ }
36126
+ usedNames.add(entryName);
36127
+ hashesByName.set(entryName, entryHash);
36128
+ canonicalEntries.push({ name: entryName, hash: entryHash });
36129
+ }
36130
+ for (const entry of canonicalEntries.sort((a, b3) => a.name.localeCompare(b3.name))) {
36131
+ dirHash.update(entry.name);
36132
+ dirHash.update("\x00");
36133
+ dirHash.update(entry.hash);
36134
+ dirHash.update("\x00");
36135
+ }
36136
+ return dirHash.digest("hex");
36137
+ }
36138
+ return hashString(`other:${stat.mode}:${stat.size}`);
36139
+ }
36140
+ async function hashPathForCategory(category, targetPath) {
36141
+ return category === "rules" ? hashRulePath(targetPath) : hashPath(targetPath);
36142
+ }
36143
+ function findAvailableRuleName(originalName, usedNames) {
36144
+ let index = 1;
36145
+ while (true) {
36146
+ const candidate = nameWithSuffix(originalName, "agents-rules", index);
36147
+ if (!usedNames.has(candidate))
36148
+ return candidate;
36149
+ index++;
36150
+ }
36151
+ }
35974
36152
  var TEXT_EXTENSIONS = new Set([
35975
36153
  ".cjs",
35976
36154
  ".js",
@@ -36049,15 +36227,40 @@ function nameWithSuffix(name, suffix, index) {
36049
36227
  }
36050
36228
  return `${name}--${numberedSuffix}`;
36051
36229
  }
36052
- async function addExistingDestinationHashes(destinationDir, knownHashes) {
36230
+ function normalizeRuleFileName(name) {
36231
+ return path17.extname(name).toLowerCase() === ".mdc" ? `${path17.basename(name, path17.extname(name))}.md` : name;
36232
+ }
36233
+ function ruleExtensionPriority(name) {
36234
+ const ext = path17.extname(name).toLowerCase();
36235
+ if (ext === ".md")
36236
+ return 0;
36237
+ if (ext === ".mdc")
36238
+ return 1;
36239
+ return 2;
36240
+ }
36241
+ function compareRuleEntryNames(a, b3) {
36242
+ const normalizedCompare = normalizeRuleFileName(a.name).localeCompare(normalizeRuleFileName(b3.name));
36243
+ if (normalizedCompare !== 0)
36244
+ return normalizedCompare;
36245
+ const priorityCompare = ruleExtensionPriority(a.name) - ruleExtensionPriority(b3.name);
36246
+ if (priorityCompare !== 0)
36247
+ return priorityCompare;
36248
+ return a.name.localeCompare(b3.name);
36249
+ }
36250
+ async function addExistingDestinationHashes(category, destinationDir, knownHashes, result) {
36053
36251
  if (!await import_fs_extra13.default.pathExists(destinationDir))
36054
36252
  return;
36253
+ const plannedRuleNames = new Map(result.renamed.filter((entry) => entry.category === "rules").map((entry) => [path17.resolve(entry.from), path17.basename(entry.to)]));
36254
+ const removedRulePaths = new Set(result.duplicates.filter((entry) => entry.category === "rules").map((entry) => path17.resolve(entry.from)));
36055
36255
  const entries = await import_fs_extra13.default.readdir(destinationDir, { withFileTypes: true });
36056
36256
  for (const entry of entries) {
36057
36257
  if (IGNORED_ENTRY_NAMES2.has(entry.name))
36058
36258
  continue;
36059
36259
  const entryPath = path17.join(destinationDir, entry.name);
36060
- knownHashes.set(await hashPath(entryPath), entry.name);
36260
+ if (category === "rules" && removedRulePaths.has(path17.resolve(entryPath)))
36261
+ continue;
36262
+ const entryName = category === "rules" ? plannedRuleNames.get(path17.resolve(entryPath)) ?? normalizeRuleFileName(entry.name) : entry.name;
36263
+ knownHashes.set(await hashPathForCategory(category, entryPath), entryName);
36061
36264
  }
36062
36265
  }
36063
36266
  async function importCategoryEntries(category, candidates, destinationDir, result, dryRun = false) {
@@ -36074,7 +36277,7 @@ async function importCategoryEntries(category, candidates, destinationDir, resul
36074
36277
  if (destinationRealPath && candidateRealPath && samePath(destinationRealPath, candidateRealPath)) {
36075
36278
  continue;
36076
36279
  }
36077
- const entries = await collectCandidateEntries(candidate).catch(() => null);
36280
+ const entries = await collectCandidateEntries(candidate, category).catch(() => null);
36078
36281
  if (!entries) {
36079
36282
  result.skipped.push({
36080
36283
  category,
@@ -36100,12 +36303,12 @@ async function importCategoryEntries(category, candidates, destinationDir, resul
36100
36303
  await import_fs_extra13.default.ensureDir(destinationDir);
36101
36304
  }
36102
36305
  const knownHashes = new Map;
36103
- await addExistingDestinationHashes(destinationDir, knownHashes);
36306
+ await addExistingDestinationHashes(category, destinationDir, knownHashes, result);
36104
36307
  const knownNames = new Set(knownHashes.values());
36105
36308
  for (const { candidate, entries } of sourceEntries) {
36106
36309
  for (const entry of entries) {
36107
36310
  const sourcePath = entry.path;
36108
- const sourceHash = await hashPath(sourcePath);
36311
+ const sourceHash = await hashPathForCategory(category, sourcePath);
36109
36312
  const existingName = knownHashes.get(sourceHash);
36110
36313
  if (existingName) {
36111
36314
  result.duplicates.push({
@@ -36116,10 +36319,10 @@ async function importCategoryEntries(category, candidates, destinationDir, resul
36116
36319
  });
36117
36320
  continue;
36118
36321
  }
36119
- let targetName = entry.name;
36322
+ let targetName = category === "rules" ? normalizeRuleFileName(entry.name) : entry.name;
36120
36323
  let targetPath = path17.join(destinationDir, targetName);
36121
36324
  if (await pathExistsOrSymlink(targetPath) || knownNames.has(targetName)) {
36122
- targetName = await findAvailableTargetName(destinationDir, entry.name, candidate.label, knownNames);
36325
+ targetName = await findAvailableTargetName(destinationDir, targetName, candidate.label, knownNames);
36123
36326
  targetPath = path17.join(destinationDir, targetName);
36124
36327
  result.renamed.push({
36125
36328
  category,
@@ -36137,7 +36340,7 @@ async function importCategoryEntries(category, candidates, destinationDir, resul
36137
36340
  await normalizePortableContent(targetPath);
36138
36341
  }
36139
36342
  knownNames.add(targetName);
36140
- knownHashes.set(dryRun ? sourceHash : await hashPath(targetPath), targetName);
36343
+ knownHashes.set(dryRun ? sourceHash : await hashPathForCategory(category, targetPath), targetName);
36141
36344
  result.imported.push({
36142
36345
  category,
36143
36346
  name: entry.name,
@@ -36148,6 +36351,84 @@ async function importCategoryEntries(category, candidates, destinationDir, resul
36148
36351
  }
36149
36352
  return true;
36150
36353
  }
36354
+ function recordRuleExtensionRename(result, from3, to) {
36355
+ result.renamed.push({
36356
+ category: "rules",
36357
+ name: path17.basename(from3),
36358
+ from: from3,
36359
+ to,
36360
+ reason: RULE_EXTENSION_RENAME_REASON
36361
+ });
36362
+ }
36363
+ async function migrateExistingRuleExtensions(rulesDir, result, dryRun = false) {
36364
+ const usedNamesByDir = new Map;
36365
+ async function usedNamesForDir(currentDir) {
36366
+ const existing = usedNamesByDir.get(currentDir);
36367
+ if (existing)
36368
+ return existing;
36369
+ const names = new Set(await import_fs_extra13.default.readdir(currentDir).catch(() => []));
36370
+ usedNamesByDir.set(currentDir, names);
36371
+ return names;
36372
+ }
36373
+ async function walk(currentDir) {
36374
+ const entries = sortSourceEntries(await import_fs_extra13.default.readdir(currentDir, { withFileTypes: true }).catch(() => []), "rules");
36375
+ for (const entry of entries) {
36376
+ if (IGNORED_ENTRY_NAMES2.has(entry.name))
36377
+ continue;
36378
+ const sourcePath = path17.join(currentDir, entry.name);
36379
+ if (entry.isDirectory()) {
36380
+ await walk(sourcePath);
36381
+ continue;
36382
+ }
36383
+ if (!entry.isFile() && !entry.isSymbolicLink())
36384
+ continue;
36385
+ if (path17.extname(entry.name).toLowerCase() !== ".mdc")
36386
+ continue;
36387
+ const targetPath = path17.join(currentDir, normalizeRuleFileName(entry.name));
36388
+ if (samePath(sourcePath, targetPath))
36389
+ continue;
36390
+ if (await pathExistsOrSymlink(targetPath)) {
36391
+ const sourceHash = await hashPath(sourcePath);
36392
+ const targetHash = await hashPath(targetPath);
36393
+ if (sourceHash === targetHash) {
36394
+ const backupRoot = await ensureBackupPath(result, dryRun);
36395
+ const backupTarget = path17.join(backupRoot, safeRelativePath(result.rootDir, sourcePath));
36396
+ if (!dryRun) {
36397
+ await import_fs_extra13.default.ensureDir(path17.dirname(backupTarget));
36398
+ await import_fs_extra13.default.move(sourcePath, backupTarget, { overwrite: false });
36399
+ }
36400
+ result.duplicates.push({
36401
+ category: "rules",
36402
+ name: entry.name,
36403
+ from: sourcePath,
36404
+ keptAs: targetPath
36405
+ });
36406
+ const usedNames3 = await usedNamesForDir(currentDir);
36407
+ usedNames3.delete(entry.name);
36408
+ continue;
36409
+ }
36410
+ const usedNames2 = await usedNamesForDir(currentDir);
36411
+ const targetName = await findAvailableTargetName(currentDir, path17.basename(targetPath), "agents-rules", usedNames2);
36412
+ const renamedTargetPath = path17.join(currentDir, targetName);
36413
+ if (!dryRun) {
36414
+ await import_fs_extra13.default.move(sourcePath, renamedTargetPath, { overwrite: false });
36415
+ }
36416
+ usedNames2.delete(entry.name);
36417
+ usedNames2.add(targetName);
36418
+ recordRuleExtensionRename(result, sourcePath, renamedTargetPath);
36419
+ continue;
36420
+ }
36421
+ const usedNames = await usedNamesForDir(currentDir);
36422
+ if (!dryRun) {
36423
+ await import_fs_extra13.default.move(sourcePath, targetPath, { overwrite: false });
36424
+ }
36425
+ usedNames.delete(entry.name);
36426
+ usedNames.add(path17.basename(targetPath));
36427
+ recordRuleExtensionRename(result, sourcePath, targetPath);
36428
+ }
36429
+ }
36430
+ await walk(rulesDir);
36431
+ }
36151
36432
  async function findAvailableTargetName(destinationDir, originalName, label, knownNames) {
36152
36433
  const suffix = suffixFromLabel(label);
36153
36434
  let index = 1;
@@ -36159,11 +36440,18 @@ async function findAvailableTargetName(destinationDir, originalName, label, know
36159
36440
  index++;
36160
36441
  }
36161
36442
  }
36162
- async function collectCandidateEntries(candidate) {
36443
+ function sortSourceEntries(entries, category) {
36444
+ return [...entries].sort((a, b3) => {
36445
+ if (category === "rules")
36446
+ return compareRuleEntryNames(a, b3);
36447
+ return a.name.localeCompare(b3.name);
36448
+ });
36449
+ }
36450
+ async function collectCandidateEntries(candidate, category) {
36163
36451
  const stat = await import_fs_extra13.default.lstat(candidate.path);
36164
36452
  if (stat.isDirectory()) {
36165
36453
  const entries = await import_fs_extra13.default.readdir(candidate.path, { withFileTypes: true });
36166
- return entries.map((entry) => ({
36454
+ return sortSourceEntries(entries, category).map((entry) => ({
36167
36455
  name: entry.name,
36168
36456
  path: path17.join(candidate.path, entry.name)
36169
36457
  }));
@@ -36172,7 +36460,7 @@ async function collectCandidateEntries(candidate) {
36172
36460
  const targetStat = await import_fs_extra13.default.stat(candidate.path).catch(() => null);
36173
36461
  if (targetStat?.isDirectory()) {
36174
36462
  const entries = await import_fs_extra13.default.readdir(candidate.path, { withFileTypes: true });
36175
- return entries.map((entry) => ({
36463
+ return sortSourceEntries(entries, category).map((entry) => ({
36176
36464
  name: entry.name,
36177
36465
  path: path17.join(candidate.path, entry.name)
36178
36466
  }));
@@ -36450,7 +36738,7 @@ async function collectRuleIndexEntries(rulesDir, rootDir, currentDir = rulesDir)
36450
36738
  }
36451
36739
  if (!entry.isFile() && !entry.isSymbolicLink())
36452
36740
  continue;
36453
- if (![".md", ".mdc"].includes(path17.extname(entry.name).toLowerCase()))
36741
+ if (path17.extname(entry.name).toLowerCase() !== ".md")
36454
36742
  continue;
36455
36743
  rules.push({
36456
36744
  title: await readRuleTitle(entryPath),
@@ -36601,21 +36889,20 @@ ${renderRulesIndexBlock(rules)}
36601
36889
  }
36602
36890
  async function collectPlannedRuleIndexEntries(result, rootDir) {
36603
36891
  const rules = [];
36892
+ const removedRulePaths = new Set([
36893
+ ...result.renamed.filter((entry) => entry.category === "rules").map((entry) => path17.relative(rootDir, entry.from)),
36894
+ ...result.duplicates.filter((entry) => entry.category === "rules").map((entry) => path17.relative(rootDir, entry.from))
36895
+ ]);
36604
36896
  const plannedRuleEntries = [
36605
36897
  ...result.imported.filter((entry) => entry.category === "rules"),
36606
36898
  ...result.renamed.filter((entry) => entry.category === "rules")
36607
36899
  ];
36608
36900
  for (const entry of plannedRuleEntries) {
36609
- const ext = path17.extname(entry.to).toLowerCase();
36610
- if (![".md", ".mdc"].includes(ext))
36611
- continue;
36612
- rules.push({
36613
- title: await readRuleTitle(entry.from),
36614
- relativePath: path17.relative(rootDir, entry.to)
36615
- });
36901
+ rules.push(...await collectPlannedRuleEntryFiles(entry, rootDir));
36616
36902
  }
36617
36903
  if (await pathExistsOrSymlink(path17.join(rootDir, ".agents", "rules"))) {
36618
- rules.push(...await collectRuleIndexEntries(path17.join(rootDir, ".agents", "rules"), rootDir));
36904
+ const existingRules = await collectRuleIndexEntries(path17.join(rootDir, ".agents", "rules"), rootDir);
36905
+ rules.push(...existingRules.filter((rule) => !removedRulePaths.has(rule.relativePath)));
36619
36906
  }
36620
36907
  const byPath = new Map;
36621
36908
  for (const rule of rules) {
@@ -36623,6 +36910,58 @@ async function collectPlannedRuleIndexEntries(result, rootDir) {
36623
36910
  }
36624
36911
  return [...byPath.values()].sort((a, b3) => a.relativePath.localeCompare(b3.relativePath));
36625
36912
  }
36913
+ async function collectPlannedRuleEntryFiles(entry, rootDir) {
36914
+ const sourceStat = await import_fs_extra13.default.lstat(entry.from).catch(() => null);
36915
+ if (!sourceStat?.isDirectory()) {
36916
+ const ext = path17.extname(entry.to).toLowerCase();
36917
+ if (ext !== ".md")
36918
+ return [];
36919
+ return [{
36920
+ title: await readRuleTitle(entry.from),
36921
+ relativePath: path17.relative(rootDir, entry.to)
36922
+ }];
36923
+ }
36924
+ return collectPlannedRuleDirectoryFiles(entry.from, entry.to, rootDir);
36925
+ }
36926
+ async function collectPlannedRuleDirectoryFiles(sourceDir, targetDir, rootDir) {
36927
+ const entries = sortSourceEntries(await import_fs_extra13.default.readdir(sourceDir, { withFileTypes: true }).catch(() => []), "rules");
36928
+ const rules = [];
36929
+ const usedNames = new Set;
36930
+ const plannedSourcesByName = new Map;
36931
+ for (const entry of entries) {
36932
+ if (IGNORED_ENTRY_NAMES2.has(entry.name))
36933
+ continue;
36934
+ const sourcePath = path17.join(sourceDir, entry.name);
36935
+ if (entry.isDirectory()) {
36936
+ const targetPath2 = path17.join(targetDir, entry.name);
36937
+ usedNames.add(entry.name);
36938
+ rules.push(...await collectPlannedRuleDirectoryFiles(sourcePath, targetPath2, rootDir));
36939
+ continue;
36940
+ }
36941
+ if (!entry.isFile() && !entry.isSymbolicLink())
36942
+ continue;
36943
+ const ext = path17.extname(entry.name).toLowerCase();
36944
+ if (![".md", ".mdc"].includes(ext))
36945
+ continue;
36946
+ let targetName = normalizeRuleFileName(entry.name);
36947
+ const existingSourcePath = plannedSourcesByName.get(targetName);
36948
+ if (existingSourcePath) {
36949
+ const sourceHash = await hashRulePath(sourcePath);
36950
+ const existingHash = await hashRulePath(existingSourcePath);
36951
+ if (sourceHash === existingHash)
36952
+ continue;
36953
+ targetName = await findAvailableTargetName(targetDir, targetName, "agents-rules", usedNames);
36954
+ }
36955
+ usedNames.add(targetName);
36956
+ plannedSourcesByName.set(targetName, sourcePath);
36957
+ const targetPath = path17.join(targetDir, targetName);
36958
+ rules.push({
36959
+ title: await readRuleTitle(sourcePath),
36960
+ relativePath: path17.relative(rootDir, targetPath)
36961
+ });
36962
+ }
36963
+ return rules;
36964
+ }
36626
36965
  async function unifyAgentsConfiguration(options = {}) {
36627
36966
  const scope = options.scope ?? "global";
36628
36967
  const dryRun = Boolean(options.dryRun);
@@ -36651,6 +36990,9 @@ async function unifyAgentsConfiguration(options = {}) {
36651
36990
  };
36652
36991
  await importInstructionFiles(instructionCandidates, destinationByCategory.instructions, result, dryRun);
36653
36992
  const categories = scope === "repository" ? ["skills", "agents", "rules"] : ["skills", "agents"];
36993
+ if (scope === "repository" && await pathExistsOrSymlink(destinationByCategory.rules)) {
36994
+ await migrateExistingRuleExtensions(destinationByCategory.rules, result, dryRun);
36995
+ }
36654
36996
  const activeCategories = new Set;
36655
36997
  for (const category of categories) {
36656
36998
  const isActive = await importCategoryEntries(category, candidates, destinationByCategory[category], result, dryRun);
@@ -36667,6 +37009,9 @@ async function unifyAgentsConfiguration(options = {}) {
36667
37009
  await linkInstructionFile(candidate, destinationByCategory.instructions, result, dryRun);
36668
37010
  }
36669
37011
  if (scope === "repository") {
37012
+ if (!dryRun && await pathExistsOrSymlink(destinationByCategory.rules)) {
37013
+ await migrateExistingRuleExtensions(destinationByCategory.rules, result, dryRun);
37014
+ }
36670
37015
  await writeRepositoryInstructionIndex(result, folders, dryRun);
36671
37016
  }
36672
37017
  return result;
@@ -38454,6 +38799,35 @@ async function configsBackupsCreateCommand(reason, options = {}) {
38454
38799
  process.exit(1);
38455
38800
  }
38456
38801
  }
38802
+ async function configsBackupsCleanCommand(options = {}) {
38803
+ try {
38804
+ const days = options.days ?? DEFAULT_BACKUP_RETENTION_DAYS;
38805
+ console.log(source_default.gray(`Cleaning config backups older than ${days} day${days !== 1 ? "s" : ""}...`));
38806
+ const result = await cleanConfigBackups(options);
38807
+ if (options.dryRun) {
38808
+ console.log(source_default.yellow(`Dry run: ${result.deleted.length} backup${result.deleted.length !== 1 ? "s" : ""} would be deleted.`));
38809
+ } else {
38810
+ console.log(source_default.green(`Deleted ${result.deleted.length} old backup${result.deleted.length !== 1 ? "s" : ""}.`));
38811
+ }
38812
+ console.log(source_default.gray(`Cutoff: ${formatDate2(result.cutoff)}`));
38813
+ for (const backup of result.deleted) {
38814
+ console.log(source_default.gray(` ${backup.name}`));
38815
+ }
38816
+ if (result.skipped.length > 0) {
38817
+ console.log(source_default.gray(`Skipped ${result.skipped.length} backup${result.skipped.length !== 1 ? "s" : ""}.`));
38818
+ }
38819
+ if (result.failed.length > 0) {
38820
+ console.error(source_default.red(`Failed to delete ${result.failed.length} backup${result.failed.length !== 1 ? "s" : ""}.`));
38821
+ for (const failure of result.failed) {
38822
+ console.error(source_default.red(` ${failure.snapshot.name}: ${failure.error}`));
38823
+ }
38824
+ process.exit(1);
38825
+ }
38826
+ } catch (error) {
38827
+ console.error(source_default.red(error instanceof Error ? error.message : String(error)));
38828
+ process.exit(1);
38829
+ }
38830
+ }
38457
38831
 
38458
38832
  // src/commands/openclaw-pro.ts
38459
38833
  import os20 from "os";
@@ -39068,6 +39442,13 @@ function readConfigOptions(command, options = {}) {
39068
39442
  agentsFolder: findOption("agentsFolder")
39069
39443
  };
39070
39444
  }
39445
+ function parseRetentionDays(value) {
39446
+ try {
39447
+ return normalizeBackupRetentionDays(Number(value));
39448
+ } catch (error) {
39449
+ throw new InvalidArgumentError(error instanceof Error ? error.message : String(error));
39450
+ }
39451
+ }
39071
39452
  var agentsCmd = program2.command("agents").description("AI coding configuration commands");
39072
39453
  registerAgentsCommands(agentsCmd);
39073
39454
  var aiCodingCmd = program2.command("ai-coding").description("Legacy alias for agents configuration commands");
@@ -39119,6 +39500,15 @@ addConfigFolderOptions(configsBackupsCmd.command("create [reason]").description(
39119
39500
  ...folderOptions
39120
39501
  });
39121
39502
  });
39503
+ addConfigFolderOptions(configsBackupsCmd.command("clean").description("Delete old config backups managed by retention").option("-d, --days <days>", "Delete backups older than this many days", parseRetentionDays, DEFAULT_BACKUP_RETENTION_DAYS).option("--dry-run", "Show backups that would be deleted without removing them").option("--include-manual", "Also delete manual backups created with configs backups create")).action((options, command) => {
39504
+ const folderOptions = readConfigOptions(command, options);
39505
+ configsBackupsCleanCommand({
39506
+ ...folderOptions,
39507
+ days: options.days,
39508
+ dryRun: options.dryRun,
39509
+ includeManual: options.includeManual
39510
+ });
39511
+ });
39122
39512
  var openclawCmd = program2.command("openclaw").description("OpenClaw configuration commands").option("-f, --folder <path>", "Specify custom OpenClaw folder path (default: ~/.openclaw)");
39123
39513
  var openclawProCmd = openclawCmd.command("pro").description("Manage OpenClaw Pro features");
39124
39514
  openclawProCmd.command("activate [token]").description("Activate OpenClaw Pro with your access token").action((token) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "aiblueprint-cli",
3
- "version": "1.4.73",
3
+ "version": "1.4.74",
4
4
  "description": "AIBlueprint CLI for setting up AI coding configurations",
5
5
  "author": "AIBlueprint",
6
6
  "license": "MIT",