aiblueprint-cli 1.4.73 → 1.4.75

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/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 = [];
@@ -35847,8 +35971,10 @@ function getContainerCandidates(options, includeCodex = true) {
35847
35971
  }
35848
35972
  ]);
35849
35973
  }
35850
- function getInstructionFileCandidates(options, includeCodex = true) {
35974
+ function getInstructionFileCandidates(options, scope) {
35851
35975
  const folders = resolveFolders(options);
35976
+ const cursorDir = path17.join(folders.rootDir, ".cursor");
35977
+ const linkToolInstructionsWhenMissing = scope === "global";
35852
35978
  return uniqueByPath([
35853
35979
  {
35854
35980
  label: "agents-instructions",
@@ -35860,11 +35986,17 @@ function getInstructionFileCandidates(options, includeCodex = true) {
35860
35986
  path: path17.join(folders.claudeDir, "CLAUDE.md"),
35861
35987
  linkWhenMissing: true
35862
35988
  },
35863
- ...includeCodex ? [{
35989
+ {
35864
35990
  label: "codex-instructions",
35865
35991
  path: path17.join(folders.codexDir, "AGENTS.md"),
35866
- linkWhenMissing: true
35867
- }] : []
35992
+ linkWhenMissing: linkToolInstructionsWhenMissing,
35993
+ linkWhenParentExists: true
35994
+ },
35995
+ {
35996
+ label: "cursor-instructions",
35997
+ path: path17.join(cursorDir, "AGENTS.md"),
35998
+ linkWhenParentExists: true
35999
+ }
35868
36000
  ]);
35869
36001
  }
35870
36002
  function getRepositoryContainerCandidates(options) {
@@ -35971,6 +36103,60 @@ async function hashPath(targetPath) {
35971
36103
  }
35972
36104
  return hashString(`other:${stat.mode}:${stat.size}`);
35973
36105
  }
36106
+ async function hashRulePath(targetPath) {
36107
+ const stat = await import_fs_extra13.default.lstat(targetPath);
36108
+ if (stat.isSymbolicLink()) {
36109
+ const linkTarget = await import_fs_extra13.default.readlink(targetPath);
36110
+ return hashString(`symlink:${linkTarget}`);
36111
+ }
36112
+ if (stat.isFile()) {
36113
+ const fileHash = crypto.createHash("sha256");
36114
+ fileHash.update("file:");
36115
+ fileHash.update(await import_fs_extra13.default.readFile(targetPath));
36116
+ return fileHash.digest("hex");
36117
+ }
36118
+ if (stat.isDirectory()) {
36119
+ const entries = (await import_fs_extra13.default.readdir(targetPath, { withFileTypes: true })).filter((entry) => !IGNORED_ENTRY_NAMES2.has(entry.name)).sort(compareRuleEntryNames);
36120
+ const canonicalEntries = [];
36121
+ const usedNames = new Set;
36122
+ const hashesByName = new Map;
36123
+ const dirHash = crypto.createHash("sha256");
36124
+ dirHash.update("dir:");
36125
+ for (const entry of entries) {
36126
+ const entryHash = await hashRulePath(path17.join(targetPath, entry.name));
36127
+ let entryName = entry.isDirectory() ? entry.name : normalizeRuleFileName(entry.name);
36128
+ const existingHash = hashesByName.get(entryName);
36129
+ if (existingHash) {
36130
+ if (existingHash === entryHash)
36131
+ continue;
36132
+ entryName = findAvailableRuleName(entryName, usedNames);
36133
+ }
36134
+ usedNames.add(entryName);
36135
+ hashesByName.set(entryName, entryHash);
36136
+ canonicalEntries.push({ name: entryName, hash: entryHash });
36137
+ }
36138
+ for (const entry of canonicalEntries.sort((a, b3) => a.name.localeCompare(b3.name))) {
36139
+ dirHash.update(entry.name);
36140
+ dirHash.update("\x00");
36141
+ dirHash.update(entry.hash);
36142
+ dirHash.update("\x00");
36143
+ }
36144
+ return dirHash.digest("hex");
36145
+ }
36146
+ return hashString(`other:${stat.mode}:${stat.size}`);
36147
+ }
36148
+ async function hashPathForCategory(category, targetPath) {
36149
+ return category === "rules" ? hashRulePath(targetPath) : hashPath(targetPath);
36150
+ }
36151
+ function findAvailableRuleName(originalName, usedNames) {
36152
+ let index = 1;
36153
+ while (true) {
36154
+ const candidate = nameWithSuffix(originalName, "agents-rules", index);
36155
+ if (!usedNames.has(candidate))
36156
+ return candidate;
36157
+ index++;
36158
+ }
36159
+ }
35974
36160
  var TEXT_EXTENSIONS = new Set([
35975
36161
  ".cjs",
35976
36162
  ".js",
@@ -36049,15 +36235,40 @@ function nameWithSuffix(name, suffix, index) {
36049
36235
  }
36050
36236
  return `${name}--${numberedSuffix}`;
36051
36237
  }
36052
- async function addExistingDestinationHashes(destinationDir, knownHashes) {
36238
+ function normalizeRuleFileName(name) {
36239
+ return path17.extname(name).toLowerCase() === ".mdc" ? `${path17.basename(name, path17.extname(name))}.md` : name;
36240
+ }
36241
+ function ruleExtensionPriority(name) {
36242
+ const ext = path17.extname(name).toLowerCase();
36243
+ if (ext === ".md")
36244
+ return 0;
36245
+ if (ext === ".mdc")
36246
+ return 1;
36247
+ return 2;
36248
+ }
36249
+ function compareRuleEntryNames(a, b3) {
36250
+ const normalizedCompare = normalizeRuleFileName(a.name).localeCompare(normalizeRuleFileName(b3.name));
36251
+ if (normalizedCompare !== 0)
36252
+ return normalizedCompare;
36253
+ const priorityCompare = ruleExtensionPriority(a.name) - ruleExtensionPriority(b3.name);
36254
+ if (priorityCompare !== 0)
36255
+ return priorityCompare;
36256
+ return a.name.localeCompare(b3.name);
36257
+ }
36258
+ async function addExistingDestinationHashes(category, destinationDir, knownHashes, result) {
36053
36259
  if (!await import_fs_extra13.default.pathExists(destinationDir))
36054
36260
  return;
36261
+ const plannedRuleNames = new Map(result.renamed.filter((entry) => entry.category === "rules").map((entry) => [path17.resolve(entry.from), path17.basename(entry.to)]));
36262
+ const removedRulePaths = new Set(result.duplicates.filter((entry) => entry.category === "rules").map((entry) => path17.resolve(entry.from)));
36055
36263
  const entries = await import_fs_extra13.default.readdir(destinationDir, { withFileTypes: true });
36056
36264
  for (const entry of entries) {
36057
36265
  if (IGNORED_ENTRY_NAMES2.has(entry.name))
36058
36266
  continue;
36059
36267
  const entryPath = path17.join(destinationDir, entry.name);
36060
- knownHashes.set(await hashPath(entryPath), entry.name);
36268
+ if (category === "rules" && removedRulePaths.has(path17.resolve(entryPath)))
36269
+ continue;
36270
+ const entryName = category === "rules" ? plannedRuleNames.get(path17.resolve(entryPath)) ?? normalizeRuleFileName(entry.name) : entry.name;
36271
+ knownHashes.set(await hashPathForCategory(category, entryPath), entryName);
36061
36272
  }
36062
36273
  }
36063
36274
  async function importCategoryEntries(category, candidates, destinationDir, result, dryRun = false) {
@@ -36074,7 +36285,7 @@ async function importCategoryEntries(category, candidates, destinationDir, resul
36074
36285
  if (destinationRealPath && candidateRealPath && samePath(destinationRealPath, candidateRealPath)) {
36075
36286
  continue;
36076
36287
  }
36077
- const entries = await collectCandidateEntries(candidate).catch(() => null);
36288
+ const entries = await collectCandidateEntries(candidate, category).catch(() => null);
36078
36289
  if (!entries) {
36079
36290
  result.skipped.push({
36080
36291
  category,
@@ -36100,12 +36311,12 @@ async function importCategoryEntries(category, candidates, destinationDir, resul
36100
36311
  await import_fs_extra13.default.ensureDir(destinationDir);
36101
36312
  }
36102
36313
  const knownHashes = new Map;
36103
- await addExistingDestinationHashes(destinationDir, knownHashes);
36314
+ await addExistingDestinationHashes(category, destinationDir, knownHashes, result);
36104
36315
  const knownNames = new Set(knownHashes.values());
36105
36316
  for (const { candidate, entries } of sourceEntries) {
36106
36317
  for (const entry of entries) {
36107
36318
  const sourcePath = entry.path;
36108
- const sourceHash = await hashPath(sourcePath);
36319
+ const sourceHash = await hashPathForCategory(category, sourcePath);
36109
36320
  const existingName = knownHashes.get(sourceHash);
36110
36321
  if (existingName) {
36111
36322
  result.duplicates.push({
@@ -36116,10 +36327,10 @@ async function importCategoryEntries(category, candidates, destinationDir, resul
36116
36327
  });
36117
36328
  continue;
36118
36329
  }
36119
- let targetName = entry.name;
36330
+ let targetName = category === "rules" ? normalizeRuleFileName(entry.name) : entry.name;
36120
36331
  let targetPath = path17.join(destinationDir, targetName);
36121
36332
  if (await pathExistsOrSymlink(targetPath) || knownNames.has(targetName)) {
36122
- targetName = await findAvailableTargetName(destinationDir, entry.name, candidate.label, knownNames);
36333
+ targetName = await findAvailableTargetName(destinationDir, targetName, candidate.label, knownNames);
36123
36334
  targetPath = path17.join(destinationDir, targetName);
36124
36335
  result.renamed.push({
36125
36336
  category,
@@ -36137,7 +36348,7 @@ async function importCategoryEntries(category, candidates, destinationDir, resul
36137
36348
  await normalizePortableContent(targetPath);
36138
36349
  }
36139
36350
  knownNames.add(targetName);
36140
- knownHashes.set(dryRun ? sourceHash : await hashPath(targetPath), targetName);
36351
+ knownHashes.set(dryRun ? sourceHash : await hashPathForCategory(category, targetPath), targetName);
36141
36352
  result.imported.push({
36142
36353
  category,
36143
36354
  name: entry.name,
@@ -36148,6 +36359,84 @@ async function importCategoryEntries(category, candidates, destinationDir, resul
36148
36359
  }
36149
36360
  return true;
36150
36361
  }
36362
+ function recordRuleExtensionRename(result, from3, to) {
36363
+ result.renamed.push({
36364
+ category: "rules",
36365
+ name: path17.basename(from3),
36366
+ from: from3,
36367
+ to,
36368
+ reason: RULE_EXTENSION_RENAME_REASON
36369
+ });
36370
+ }
36371
+ async function migrateExistingRuleExtensions(rulesDir, result, dryRun = false) {
36372
+ const usedNamesByDir = new Map;
36373
+ async function usedNamesForDir(currentDir) {
36374
+ const existing = usedNamesByDir.get(currentDir);
36375
+ if (existing)
36376
+ return existing;
36377
+ const names = new Set(await import_fs_extra13.default.readdir(currentDir).catch(() => []));
36378
+ usedNamesByDir.set(currentDir, names);
36379
+ return names;
36380
+ }
36381
+ async function walk(currentDir) {
36382
+ const entries = sortSourceEntries(await import_fs_extra13.default.readdir(currentDir, { withFileTypes: true }).catch(() => []), "rules");
36383
+ for (const entry of entries) {
36384
+ if (IGNORED_ENTRY_NAMES2.has(entry.name))
36385
+ continue;
36386
+ const sourcePath = path17.join(currentDir, entry.name);
36387
+ if (entry.isDirectory()) {
36388
+ await walk(sourcePath);
36389
+ continue;
36390
+ }
36391
+ if (!entry.isFile() && !entry.isSymbolicLink())
36392
+ continue;
36393
+ if (path17.extname(entry.name).toLowerCase() !== ".mdc")
36394
+ continue;
36395
+ const targetPath = path17.join(currentDir, normalizeRuleFileName(entry.name));
36396
+ if (samePath(sourcePath, targetPath))
36397
+ continue;
36398
+ if (await pathExistsOrSymlink(targetPath)) {
36399
+ const sourceHash = await hashPath(sourcePath);
36400
+ const targetHash = await hashPath(targetPath);
36401
+ if (sourceHash === targetHash) {
36402
+ const backupRoot = await ensureBackupPath(result, dryRun);
36403
+ const backupTarget = path17.join(backupRoot, safeRelativePath(result.rootDir, sourcePath));
36404
+ if (!dryRun) {
36405
+ await import_fs_extra13.default.ensureDir(path17.dirname(backupTarget));
36406
+ await import_fs_extra13.default.move(sourcePath, backupTarget, { overwrite: false });
36407
+ }
36408
+ result.duplicates.push({
36409
+ category: "rules",
36410
+ name: entry.name,
36411
+ from: sourcePath,
36412
+ keptAs: targetPath
36413
+ });
36414
+ const usedNames3 = await usedNamesForDir(currentDir);
36415
+ usedNames3.delete(entry.name);
36416
+ continue;
36417
+ }
36418
+ const usedNames2 = await usedNamesForDir(currentDir);
36419
+ const targetName = await findAvailableTargetName(currentDir, path17.basename(targetPath), "agents-rules", usedNames2);
36420
+ const renamedTargetPath = path17.join(currentDir, targetName);
36421
+ if (!dryRun) {
36422
+ await import_fs_extra13.default.move(sourcePath, renamedTargetPath, { overwrite: false });
36423
+ }
36424
+ usedNames2.delete(entry.name);
36425
+ usedNames2.add(targetName);
36426
+ recordRuleExtensionRename(result, sourcePath, renamedTargetPath);
36427
+ continue;
36428
+ }
36429
+ const usedNames = await usedNamesForDir(currentDir);
36430
+ if (!dryRun) {
36431
+ await import_fs_extra13.default.move(sourcePath, targetPath, { overwrite: false });
36432
+ }
36433
+ usedNames.delete(entry.name);
36434
+ usedNames.add(path17.basename(targetPath));
36435
+ recordRuleExtensionRename(result, sourcePath, targetPath);
36436
+ }
36437
+ }
36438
+ await walk(rulesDir);
36439
+ }
36151
36440
  async function findAvailableTargetName(destinationDir, originalName, label, knownNames) {
36152
36441
  const suffix = suffixFromLabel(label);
36153
36442
  let index = 1;
@@ -36159,11 +36448,18 @@ async function findAvailableTargetName(destinationDir, originalName, label, know
36159
36448
  index++;
36160
36449
  }
36161
36450
  }
36162
- async function collectCandidateEntries(candidate) {
36451
+ function sortSourceEntries(entries, category) {
36452
+ return [...entries].sort((a, b3) => {
36453
+ if (category === "rules")
36454
+ return compareRuleEntryNames(a, b3);
36455
+ return a.name.localeCompare(b3.name);
36456
+ });
36457
+ }
36458
+ async function collectCandidateEntries(candidate, category) {
36163
36459
  const stat = await import_fs_extra13.default.lstat(candidate.path);
36164
36460
  if (stat.isDirectory()) {
36165
36461
  const entries = await import_fs_extra13.default.readdir(candidate.path, { withFileTypes: true });
36166
- return entries.map((entry) => ({
36462
+ return sortSourceEntries(entries, category).map((entry) => ({
36167
36463
  name: entry.name,
36168
36464
  path: path17.join(candidate.path, entry.name)
36169
36465
  }));
@@ -36172,7 +36468,7 @@ async function collectCandidateEntries(candidate) {
36172
36468
  const targetStat = await import_fs_extra13.default.stat(candidate.path).catch(() => null);
36173
36469
  if (targetStat?.isDirectory()) {
36174
36470
  const entries = await import_fs_extra13.default.readdir(candidate.path, { withFileTypes: true });
36175
- return entries.map((entry) => ({
36471
+ return sortSourceEntries(entries, category).map((entry) => ({
36176
36472
  name: entry.name,
36177
36473
  path: path17.join(candidate.path, entry.name)
36178
36474
  }));
@@ -36368,6 +36664,8 @@ async function importInstructionFiles(candidates, destinationPath, result, dryRu
36368
36664
  async function shouldLinkMissingInstruction(candidate) {
36369
36665
  if (candidate.linkWhenMissing)
36370
36666
  return true;
36667
+ if (candidate.linkWhenParentExists)
36668
+ return import_fs_extra13.default.pathExists(path17.dirname(candidate.path));
36371
36669
  return false;
36372
36670
  }
36373
36671
  async function linkInstructionFile(candidate, destinationPath, result, dryRun = false) {
@@ -36450,7 +36748,7 @@ async function collectRuleIndexEntries(rulesDir, rootDir, currentDir = rulesDir)
36450
36748
  }
36451
36749
  if (!entry.isFile() && !entry.isSymbolicLink())
36452
36750
  continue;
36453
- if (![".md", ".mdc"].includes(path17.extname(entry.name).toLowerCase()))
36751
+ if (path17.extname(entry.name).toLowerCase() !== ".md")
36454
36752
  continue;
36455
36753
  rules.push({
36456
36754
  title: await readRuleTitle(entryPath),
@@ -36561,12 +36859,12 @@ async function ensureClaudeInstructionSymlink(result, agentsPath, claudePath, dr
36561
36859
  to: agentsPath
36562
36860
  });
36563
36861
  }
36564
- async function writeRepositoryInstructionIndex(result, folders, dryRun = false) {
36862
+ async function writeRepositoryInstructionIndex(result, folders, dryRun = false, includeRules = true) {
36565
36863
  const rulesDir = path17.join(folders.agentsDir, "rules");
36566
36864
  const agentsPath = path17.join(folders.rootDir, "AGENTS.md");
36567
36865
  const claudePath = path17.join(folders.rootDir, "CLAUDE.md");
36568
36866
  const rulesDirExists = await pathExistsOrSymlink(rulesDir);
36569
- const rules = dryRun || rulesDirExists ? dryRun ? await collectPlannedRuleIndexEntries(result, folders.rootDir) : await collectRuleIndexEntries(rulesDir, folders.rootDir) : [];
36867
+ const rules = includeRules && (dryRun || rulesDirExists) ? dryRun ? await collectPlannedRuleIndexEntries(result, folders.rootDir) : await collectRuleIndexEntries(rulesDir, folders.rootDir) : [];
36570
36868
  const agentsExists = await pathExistsOrSymlink(agentsPath);
36571
36869
  const claudeExists = await pathExistsOrSymlink(claudePath);
36572
36870
  if (rules.length === 0 && !agentsExists && !claudeExists) {
@@ -36601,21 +36899,20 @@ ${renderRulesIndexBlock(rules)}
36601
36899
  }
36602
36900
  async function collectPlannedRuleIndexEntries(result, rootDir) {
36603
36901
  const rules = [];
36902
+ const removedRulePaths = new Set([
36903
+ ...result.renamed.filter((entry) => entry.category === "rules").map((entry) => path17.relative(rootDir, entry.from)),
36904
+ ...result.duplicates.filter((entry) => entry.category === "rules").map((entry) => path17.relative(rootDir, entry.from))
36905
+ ]);
36604
36906
  const plannedRuleEntries = [
36605
36907
  ...result.imported.filter((entry) => entry.category === "rules"),
36606
36908
  ...result.renamed.filter((entry) => entry.category === "rules")
36607
36909
  ];
36608
36910
  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
- });
36911
+ rules.push(...await collectPlannedRuleEntryFiles(entry, rootDir));
36616
36912
  }
36617
36913
  if (await pathExistsOrSymlink(path17.join(rootDir, ".agents", "rules"))) {
36618
- rules.push(...await collectRuleIndexEntries(path17.join(rootDir, ".agents", "rules"), rootDir));
36914
+ const existingRules = await collectRuleIndexEntries(path17.join(rootDir, ".agents", "rules"), rootDir);
36915
+ rules.push(...existingRules.filter((rule) => !removedRulePaths.has(rule.relativePath)));
36619
36916
  }
36620
36917
  const byPath = new Map;
36621
36918
  for (const rule of rules) {
@@ -36623,12 +36920,66 @@ async function collectPlannedRuleIndexEntries(result, rootDir) {
36623
36920
  }
36624
36921
  return [...byPath.values()].sort((a, b3) => a.relativePath.localeCompare(b3.relativePath));
36625
36922
  }
36923
+ async function collectPlannedRuleEntryFiles(entry, rootDir) {
36924
+ const sourceStat = await import_fs_extra13.default.lstat(entry.from).catch(() => null);
36925
+ if (!sourceStat?.isDirectory()) {
36926
+ const ext = path17.extname(entry.to).toLowerCase();
36927
+ if (ext !== ".md")
36928
+ return [];
36929
+ return [{
36930
+ title: await readRuleTitle(entry.from),
36931
+ relativePath: path17.relative(rootDir, entry.to)
36932
+ }];
36933
+ }
36934
+ return collectPlannedRuleDirectoryFiles(entry.from, entry.to, rootDir);
36935
+ }
36936
+ async function collectPlannedRuleDirectoryFiles(sourceDir, targetDir, rootDir) {
36937
+ const entries = sortSourceEntries(await import_fs_extra13.default.readdir(sourceDir, { withFileTypes: true }).catch(() => []), "rules");
36938
+ const rules = [];
36939
+ const usedNames = new Set;
36940
+ const plannedSourcesByName = new Map;
36941
+ for (const entry of entries) {
36942
+ if (IGNORED_ENTRY_NAMES2.has(entry.name))
36943
+ continue;
36944
+ const sourcePath = path17.join(sourceDir, entry.name);
36945
+ if (entry.isDirectory()) {
36946
+ const targetPath2 = path17.join(targetDir, entry.name);
36947
+ usedNames.add(entry.name);
36948
+ rules.push(...await collectPlannedRuleDirectoryFiles(sourcePath, targetPath2, rootDir));
36949
+ continue;
36950
+ }
36951
+ if (!entry.isFile() && !entry.isSymbolicLink())
36952
+ continue;
36953
+ const ext = path17.extname(entry.name).toLowerCase();
36954
+ if (![".md", ".mdc"].includes(ext))
36955
+ continue;
36956
+ let targetName = normalizeRuleFileName(entry.name);
36957
+ const existingSourcePath = plannedSourcesByName.get(targetName);
36958
+ if (existingSourcePath) {
36959
+ const sourceHash = await hashRulePath(sourcePath);
36960
+ const existingHash = await hashRulePath(existingSourcePath);
36961
+ if (sourceHash === existingHash)
36962
+ continue;
36963
+ targetName = await findAvailableTargetName(targetDir, targetName, "agents-rules", usedNames);
36964
+ }
36965
+ usedNames.add(targetName);
36966
+ plannedSourcesByName.set(targetName, sourcePath);
36967
+ const targetPath = path17.join(targetDir, targetName);
36968
+ rules.push({
36969
+ title: await readRuleTitle(sourcePath),
36970
+ relativePath: path17.relative(rootDir, targetPath)
36971
+ });
36972
+ }
36973
+ return rules;
36974
+ }
36626
36975
  async function unifyAgentsConfiguration(options = {}) {
36627
36976
  const scope = options.scope ?? "global";
36628
36977
  const dryRun = Boolean(options.dryRun);
36629
36978
  const folders = resolveFolders(options);
36630
36979
  const includeCodex = scope !== "repository";
36631
- const instructionCandidates = getInstructionFileCandidates(options, includeCodex);
36980
+ const defaultCategories = scope === "repository" ? ["instructions", "skills", "agents", "rules"] : ["instructions", "skills", "agents"];
36981
+ const selectedCategories = new Set((options.categories ?? defaultCategories).filter((category) => defaultCategories.includes(category)));
36982
+ const instructionCandidates = getInstructionFileCandidates(options, scope);
36632
36983
  const candidates = scope === "repository" ? getRepositoryContainerCandidates(options) : getContainerCandidates(options);
36633
36984
  const result = {
36634
36985
  rootDir: folders.rootDir,
@@ -36649,10 +37000,17 @@ async function unifyAgentsConfiguration(options = {}) {
36649
37000
  instructions: path17.join(folders.agentsDir, "AGENTS.md"),
36650
37001
  rules: path17.join(folders.agentsDir, "rules")
36651
37002
  };
36652
- await importInstructionFiles(instructionCandidates, destinationByCategory.instructions, result, dryRun);
37003
+ if (selectedCategories.has("instructions")) {
37004
+ await importInstructionFiles(instructionCandidates, destinationByCategory.instructions, result, dryRun);
37005
+ }
36653
37006
  const categories = scope === "repository" ? ["skills", "agents", "rules"] : ["skills", "agents"];
37007
+ if (scope === "repository" && selectedCategories.has("rules") && await pathExistsOrSymlink(destinationByCategory.rules)) {
37008
+ await migrateExistingRuleExtensions(destinationByCategory.rules, result, dryRun);
37009
+ }
36654
37010
  const activeCategories = new Set;
36655
37011
  for (const category of categories) {
37012
+ if (!selectedCategories.has(category))
37013
+ continue;
36656
37014
  const isActive = await importCategoryEntries(category, candidates, destinationByCategory[category], result, dryRun);
36657
37015
  if (isActive) {
36658
37016
  activeCategories.add(category);
@@ -36663,11 +37021,16 @@ async function unifyAgentsConfiguration(options = {}) {
36663
37021
  continue;
36664
37022
  await linkContainer(candidate, destinationByCategory[candidate.category], result, dryRun);
36665
37023
  }
36666
- for (const candidate of instructionCandidates) {
36667
- await linkInstructionFile(candidate, destinationByCategory.instructions, result, dryRun);
37024
+ if (selectedCategories.has("instructions")) {
37025
+ for (const candidate of instructionCandidates) {
37026
+ await linkInstructionFile(candidate, destinationByCategory.instructions, result, dryRun);
37027
+ }
36668
37028
  }
36669
- if (scope === "repository") {
36670
- await writeRepositoryInstructionIndex(result, folders, dryRun);
37029
+ if (scope === "repository" && (selectedCategories.has("instructions") || selectedCategories.has("rules"))) {
37030
+ if (!dryRun && selectedCategories.has("rules") && await pathExistsOrSymlink(destinationByCategory.rules)) {
37031
+ await migrateExistingRuleExtensions(destinationByCategory.rules, result, dryRun);
37032
+ }
37033
+ await writeRepositoryInstructionIndex(result, folders, dryRun, selectedCategories.has("rules"));
36671
37034
  }
36672
37035
  return result;
36673
37036
  }
@@ -36676,6 +37039,46 @@ async function previewAgentsConfiguration(options = {}) {
36676
37039
  }
36677
37040
 
36678
37041
  // src/commands/agents-unify.ts
37042
+ var CATEGORY_LABELS = {
37043
+ instructions: "AGENTS.md",
37044
+ skills: "Skills",
37045
+ agents: "Agents",
37046
+ rules: "Rules and memories"
37047
+ };
37048
+ function defaultCategoriesForScope(scope) {
37049
+ return scope === "repository" ? ["instructions", "skills", "agents", "rules"] : ["instructions", "skills", "agents"];
37050
+ }
37051
+ function selectableCategoriesForScope(scope) {
37052
+ return defaultCategoriesForScope(scope);
37053
+ }
37054
+ async function resolveSelectedCategories(params) {
37055
+ const scope = params.scope ?? "global";
37056
+ const selectableCategories = selectableCategoriesForScope(scope);
37057
+ const requestedCategories = params.categories?.filter((category) => selectableCategories.includes(category));
37058
+ const initialValues = requestedCategories ?? selectableCategories;
37059
+ if (!params.interactive) {
37060
+ if (params.categories && initialValues.length === 0) {
37061
+ throw new Error(`Selected categories do not apply to ${scope} scope`);
37062
+ }
37063
+ return requestedCategories;
37064
+ }
37065
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
37066
+ throw new Error("Interactive category selection requires a TTY");
37067
+ }
37068
+ const selected = await fe({
37069
+ message: "What do you want to unify?",
37070
+ options: selectableCategories.map((category) => ({
37071
+ value: category,
37072
+ label: CATEGORY_LABELS[category]
37073
+ })),
37074
+ initialValues,
37075
+ required: true
37076
+ });
37077
+ if (pD(selected)) {
37078
+ return null;
37079
+ }
37080
+ return selected;
37081
+ }
36679
37082
  function countByCategory(result, key, category) {
36680
37083
  return result[key].filter((entry) => entry.category === category).length;
36681
37084
  }
@@ -36741,15 +37144,24 @@ AIBlueprint agents unify ${source_default.gray(`v${getVersion()}`)}
36741
37144
  `));
36742
37145
  console.log(source_default.gray(`Scope: ${params.scope ?? "global"}`));
36743
37146
  console.log(source_default.gray(params.scope === "repository" ? "Centralizing project agent configuration into .agents" : "Centralizing reusable agent configuration into .agents, then rendering Codex agents"));
36744
- const preview = await previewAgentsConfiguration(params);
37147
+ const selectedCategories = await resolveSelectedCategories(params);
37148
+ if (selectedCategories === null) {
37149
+ console.log(source_default.gray(`
37150
+ Unify cancelled`));
37151
+ return;
37152
+ }
37153
+ const commandParams = { ...params, categories: selectedCategories };
37154
+ const effectiveCategories = selectedCategories ?? defaultCategoriesForScope(params.scope ?? "global");
37155
+ console.log(source_default.gray(`Categories: ${effectiveCategories.map((category) => CATEGORY_LABELS[category]).join(", ")}`));
37156
+ const preview = await previewAgentsConfiguration(commandParams);
36745
37157
  printAgentsUnifyPreview(preview);
36746
37158
  if (!await confirmUnify()) {
36747
37159
  console.log(source_default.gray(`
36748
37160
  Unify cancelled`));
36749
37161
  return;
36750
37162
  }
36751
- const result = await unifyAgentsConfiguration(params);
36752
- const codexResult = params.scope === "repository" ? null : await renderCodexAgentsFromMarkdown(params);
37163
+ const result = await unifyAgentsConfiguration(commandParams);
37164
+ const codexResult = params.scope === "repository" || !effectiveCategories.includes("agents") ? null : await renderCodexAgentsFromMarkdown(commandParams);
36753
37165
  console.log(source_default.green(`
36754
37166
  Unify complete`));
36755
37167
  console.log(source_default.gray(` Shared folder: ${result.agentsDir}`));
@@ -38454,6 +38866,35 @@ async function configsBackupsCreateCommand(reason, options = {}) {
38454
38866
  process.exit(1);
38455
38867
  }
38456
38868
  }
38869
+ async function configsBackupsCleanCommand(options = {}) {
38870
+ try {
38871
+ const days = options.days ?? DEFAULT_BACKUP_RETENTION_DAYS;
38872
+ console.log(source_default.gray(`Cleaning config backups older than ${days} day${days !== 1 ? "s" : ""}...`));
38873
+ const result = await cleanConfigBackups(options);
38874
+ if (options.dryRun) {
38875
+ console.log(source_default.yellow(`Dry run: ${result.deleted.length} backup${result.deleted.length !== 1 ? "s" : ""} would be deleted.`));
38876
+ } else {
38877
+ console.log(source_default.green(`Deleted ${result.deleted.length} old backup${result.deleted.length !== 1 ? "s" : ""}.`));
38878
+ }
38879
+ console.log(source_default.gray(`Cutoff: ${formatDate2(result.cutoff)}`));
38880
+ for (const backup of result.deleted) {
38881
+ console.log(source_default.gray(` ${backup.name}`));
38882
+ }
38883
+ if (result.skipped.length > 0) {
38884
+ console.log(source_default.gray(`Skipped ${result.skipped.length} backup${result.skipped.length !== 1 ? "s" : ""}.`));
38885
+ }
38886
+ if (result.failed.length > 0) {
38887
+ console.error(source_default.red(`Failed to delete ${result.failed.length} backup${result.failed.length !== 1 ? "s" : ""}.`));
38888
+ for (const failure of result.failed) {
38889
+ console.error(source_default.red(` ${failure.snapshot.name}: ${failure.error}`));
38890
+ }
38891
+ process.exit(1);
38892
+ }
38893
+ } catch (error) {
38894
+ console.error(source_default.red(error instanceof Error ? error.message : String(error)));
38895
+ process.exit(1);
38896
+ }
38897
+ }
38457
38898
 
38458
38899
  // src/commands/openclaw-pro.ts
38459
38900
  import os20 from "os";
@@ -38941,6 +39382,18 @@ var __dirname3 = dirname3(fileURLToPath3(import.meta.url));
38941
39382
  var packageJson = JSON.parse(readFileSync3(join2(__dirname3, "../package.json"), "utf8"));
38942
39383
  var program2 = new Command;
38943
39384
  program2.name("aiblueprint").description("AIBlueprint CLI for setting up AI coding configurations").version(packageJson.version);
39385
+ function readUnifyCategories(options) {
39386
+ const categories = [];
39387
+ if (options.agentsMd)
39388
+ categories.push("instructions");
39389
+ if (options.skills)
39390
+ categories.push("skills");
39391
+ if (options.agents)
39392
+ categories.push("agents");
39393
+ if (options.rules)
39394
+ categories.push("rules");
39395
+ return categories.length > 0 ? categories : undefined;
39396
+ }
38944
39397
  function registerAgentsCommands(cmd) {
38945
39398
  cmd.option("-f, --folder <path>", "Root folder that contains .claude/, .codex/, .agents/ (default: $HOME)").option("--claudeCodeFolder <path>", "Override Claude Code folder (default: {folder}/.claude)").option("--codexFolder <path>", "Override Codex folder (default: {folder}/.codex)").option("--agentsFolder <path>", "Override shared agents folder (default: {folder}/.agents)").option("-s, --skip", "Skip interactive prompts and install all features");
38946
39399
  cmd.command("setup").description("Setup AI coding configuration with AIBlueprint defaults").action((options, command) => {
@@ -38968,7 +39421,7 @@ function registerAgentsCommands(cmd) {
38968
39421
  codexFolder: parentOptions.codexFolder
38969
39422
  });
38970
39423
  });
38971
- cmd.command("unify [scope]").description("Unify agent configuration into .agents (scope: global or projects/repository; default: global)").action((scope, options, command) => {
39424
+ cmd.command("unify [scope]").description("Unify agent configuration into .agents (scope: global or projects/repository; default: global)").option("-i, --interactive", "Choose which categories to unify").option("--agents-md", "Unify instruction files into .agents/AGENTS.md").option("--skills", "Unify skills").option("--agents", "Unify Markdown agents").option("--rules", "Unify repository rules and memories").action((scope, options, command) => {
38972
39425
  const parentOptions = command.parent.opts();
38973
39426
  const requestedScope = scope ?? "global";
38974
39427
  const selectedScope = requestedScope === "projects" || requestedScope === "project" ? "repository" : requestedScope;
@@ -38983,7 +39436,9 @@ function registerAgentsCommands(cmd) {
38983
39436
  claudeCodeFolder: parentOptions.claudeCodeFolder,
38984
39437
  codexFolder: parentOptions.codexFolder,
38985
39438
  agentsFolder: parentOptions.agentsFolder,
38986
- scope: selectedScope
39439
+ scope: selectedScope,
39440
+ categories: readUnifyCategories(options),
39441
+ interactive: options.interactive
38987
39442
  });
38988
39443
  });
38989
39444
  cmd.command("codex-agents").description("Render shared Markdown agents from .agents/agents into Codex TOML custom agents").option("--overwrite", "Overwrite existing non-generated Codex agent files").action((options, command) => {
@@ -39068,6 +39523,13 @@ function readConfigOptions(command, options = {}) {
39068
39523
  agentsFolder: findOption("agentsFolder")
39069
39524
  };
39070
39525
  }
39526
+ function parseRetentionDays(value) {
39527
+ try {
39528
+ return normalizeBackupRetentionDays(Number(value));
39529
+ } catch (error) {
39530
+ throw new InvalidArgumentError(error instanceof Error ? error.message : String(error));
39531
+ }
39532
+ }
39071
39533
  var agentsCmd = program2.command("agents").description("AI coding configuration commands");
39072
39534
  registerAgentsCommands(agentsCmd);
39073
39535
  var aiCodingCmd = program2.command("ai-coding").description("Legacy alias for agents configuration commands");
@@ -39119,6 +39581,15 @@ addConfigFolderOptions(configsBackupsCmd.command("create [reason]").description(
39119
39581
  ...folderOptions
39120
39582
  });
39121
39583
  });
39584
+ 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) => {
39585
+ const folderOptions = readConfigOptions(command, options);
39586
+ configsBackupsCleanCommand({
39587
+ ...folderOptions,
39588
+ days: options.days,
39589
+ dryRun: options.dryRun,
39590
+ includeManual: options.includeManual
39591
+ });
39592
+ });
39122
39593
  var openclawCmd = program2.command("openclaw").description("OpenClaw configuration commands").option("-f, --folder <path>", "Specify custom OpenClaw folder path (default: ~/.openclaw)");
39123
39594
  var openclawProCmd = openclawCmd.command("pro").description("Manage OpenClaw Pro features");
39124
39595
  openclawProCmd.command("activate [token]").description("Activate OpenClaw Pro with your access token").action((token) => {