@baton-dx/cli 0.4.4 → 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/dist/index.mjs CHANGED
@@ -1,9 +1,7 @@
1
1
  #!/usr/bin/env node
2
- import { r as __toESM } from "./chunk-BbwQpWto.mjs";
3
- import { a as Ne, c as Ve, d as bt, f as je, g as runMain, h as defineCommand, i as Le, l as We, m as require_dist, o as R, p as Ct, r as Je, s as Re, t as findSourceRoot, u as Ze } from "./context-detection-Ddu0mj_K.mjs";
4
- import { $ as loadLockfile, A as resolveProfileChain, B as cloneGitSource, C as mergeSkills, D as sortProfilesByWeight, E as isLockedProfile, F as removePlacedFiles, G as getIdePlatformTargetDir, H as ensureBatonDirGitignored, I as generateLock, J as isKnownIdePlatform, K as getRegisteredIdePlatforms, L as readLock, M as placeFile, N as discoverProfilesInSourceRepo, O as mergeContentParts, P as findSourceManifest, Q as parseSource, R as writeLock, S as mergeRulesWithWarnings, T as getProfileWeight, U as removeGitignoreManagedSection, V as collectComprehensivePatterns, W as updateGitignore, X as getAllAIToolAdapters, Y as getAIToolAdaptersForKeys, Z as parseFrontmatter, _ as require_lib, a as clearIdeCache, at as getAIToolConfig, b as mergeAgentsWithWarnings, c as getDefaultGlobalSource, d as getGlobalSources, et as loadProfileManifest, f as loadGlobalConfig, g as setGlobalIdePlatforms, h as setGlobalAiTools, i as computeIntersection, it as SourceParseError, j as detectLegacyPaths, k as resolveProfileSupport, l as getGlobalAiTools, m as saveGlobalConfig, n as readProjectPreferences, nt as KEBAB_CASE_REGEX, o as detectInstalledIdes, ot as getAIToolPath, p as removeGlobalSource, q as idePlatformRegistry, r as writeProjectPreferences, rt as FileNotFoundError, s as addGlobalSource, st as getAllAIToolKeys, t as resolvePreferences, tt as loadProjectManifest, u as getGlobalIdePlatforms, v as mergeMemory, w as mergeSkillsWithWarnings, x as mergeRules, y as mergeMemoryWithWarnings, z as resolveVersion } from "./src-dY02psbw.mjs";
5
- import { n as detectInstalledAITools, t as clearAIToolCache } from "./ai-tool-detection-CMsBNa9e.mjs";
6
- import { d as esm_default } from "./esm-BagM-kVd.mjs";
2
+ import { a as Ne, c as Ve, d as bt, f as je, g as runMain, h as defineCommand, i as Le, l as We, m as require_dist, o as R, p as Ct, r as Je, s as Re, t as findSourceRoot, u as Ze, y as __toESM } from "./context-detection-C7T1evnW.mjs";
3
+ import { $ as parseSource, A as resolveProfileChain, B as cloneGitSource, C as mergeSkills, D as sortProfilesByWeight, E as isLockedProfile, F as removePlacedFiles, G as updateGitignore, H as collectComprehensivePatterns, I as generateLock, J as idePlatformRegistry, K as getIdePlatformTargetDir, L as readLock, M as placeFile, N as discoverProfilesInSourceRepo, O as mergeContentParts, P as findSourceManifest, Q as parseFrontmatter, R as writeLock, S as mergeRulesWithWarnings, T as getProfileWeight, U as ensureBatonDirGitignored, V as esm_default, W as removeGitignoreManagedSection, X as getAIToolAdaptersForKeys, Y as isKnownIdePlatform, Z as getAllAIToolAdapters, _ as require_lib, a as clearIdeCache, at as getAIToolConfig, b as mergeAgentsWithWarnings, c as getDefaultGlobalSource, d as getGlobalSources, et as loadProfileManifest, f as loadGlobalConfig, g as setGlobalIdePlatforms, h as setGlobalAiTools, i as computeIntersection, it as SourceParseError, j as detectLegacyPaths, k as resolveProfileSupport, l as getGlobalAiTools, m as saveGlobalConfig, n as readProjectPreferences, nt as KEBAB_CASE_REGEX, o as detectInstalledIdes, ot as getAIToolPath, p as removeGlobalSource, q as getRegisteredIdePlatforms, r as writeProjectPreferences, rt as FileNotFoundError, s as addGlobalSource, st as getAllAIToolKeys, t as resolvePreferences, tt as loadProjectManifest, u as getGlobalIdePlatforms, v as mergeMemory, w as mergeSkillsWithWarnings, x as mergeRules, y as mergeMemoryWithWarnings, z as resolveVersion } from "./src-n95I0s2u.mjs";
4
+ import { n as detectInstalledAITools, t as clearAIToolCache } from "./ai-tool-detection-DMnwwNBI.mjs";
7
5
  import { access, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
8
6
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
9
7
  import { fileURLToPath } from "node:url";
@@ -377,6 +375,120 @@ async function buildIntersection(sourceString, developerTools, cwd) {
377
375
  return computeIntersection(developerTools, resolveProfileSupport(profileManifest, sourceManifest));
378
376
  }
379
377
 
378
+ //#endregion
379
+ //#region src/utils/first-run-preferences.ts
380
+ /**
381
+ * Format an IDE platform key into a display name.
382
+ * Duplicated here to avoid circular dependency with ides/utils.
383
+ */
384
+ function formatIdeName$2(ideKey) {
385
+ return {
386
+ vscode: "VS Code",
387
+ jetbrains: "JetBrains",
388
+ cursor: "Cursor",
389
+ windsurf: "Windsurf",
390
+ antigravity: "Antigravity",
391
+ zed: "Zed"
392
+ }[ideKey] ?? ideKey;
393
+ }
394
+ /**
395
+ * Shows the first-run preferences prompt if .baton/preferences.yaml doesn't exist.
396
+ *
397
+ * Asks the user whether to use global config or customize AI tools and IDEs
398
+ * for this project, then writes the preferences file.
399
+ *
400
+ * @param projectRoot - Absolute path to the project root
401
+ * @param nonInteractive - If true, writes useGlobal: true silently
402
+ * @returns true if preferences were written, false if already existed
403
+ */
404
+ async function promptFirstRunPreferences(projectRoot, nonInteractive) {
405
+ if (await readProjectPreferences(projectRoot)) return false;
406
+ if (nonInteractive) {
407
+ await writeProjectPreferences(projectRoot, {
408
+ version: "1.0",
409
+ ai: {
410
+ useGlobal: true,
411
+ tools: []
412
+ },
413
+ ide: {
414
+ useGlobal: true,
415
+ platforms: []
416
+ }
417
+ });
418
+ return true;
419
+ }
420
+ const aiMode = await Je({
421
+ message: "How do you want to configure AI tools for this project?",
422
+ options: [{
423
+ value: "global",
424
+ label: "Use global config",
425
+ hint: "recommended"
426
+ }, {
427
+ value: "customize",
428
+ label: "Customize for this project"
429
+ }]
430
+ });
431
+ if (Ct(aiMode)) return false;
432
+ let aiUseGlobal = true;
433
+ let aiTools = [];
434
+ if (aiMode === "customize") {
435
+ const globalTools = await getGlobalAiTools();
436
+ const allAdapters = getAllAIToolAdapters();
437
+ const selected = await je({
438
+ message: "Select AI tools for this project:",
439
+ options: allAdapters.map((adapter) => ({
440
+ value: adapter.key,
441
+ label: globalTools.includes(adapter.key) ? `${adapter.name} (in global config)` : adapter.name
442
+ })),
443
+ initialValues: globalTools
444
+ });
445
+ if (Ct(selected)) return false;
446
+ aiUseGlobal = false;
447
+ aiTools = selected;
448
+ }
449
+ const ideMode = await Je({
450
+ message: "How do you want to configure IDE platforms for this project?",
451
+ options: [{
452
+ value: "global",
453
+ label: "Use global config",
454
+ hint: "recommended"
455
+ }, {
456
+ value: "customize",
457
+ label: "Customize for this project"
458
+ }]
459
+ });
460
+ if (Ct(ideMode)) return false;
461
+ let ideUseGlobal = true;
462
+ let idePlatforms = [];
463
+ if (ideMode === "customize") {
464
+ const globalPlatforms = await getGlobalIdePlatforms();
465
+ const allIdeKeys = getRegisteredIdePlatforms();
466
+ const selected = await je({
467
+ message: "Select IDE platforms for this project:",
468
+ options: allIdeKeys.map((ideKey) => ({
469
+ value: ideKey,
470
+ label: globalPlatforms.includes(ideKey) ? `${formatIdeName$2(ideKey)} (in global config)` : formatIdeName$2(ideKey)
471
+ })),
472
+ initialValues: globalPlatforms
473
+ });
474
+ if (Ct(selected)) return false;
475
+ ideUseGlobal = false;
476
+ idePlatforms = selected;
477
+ }
478
+ await writeProjectPreferences(projectRoot, {
479
+ version: "1.0",
480
+ ai: {
481
+ useGlobal: aiUseGlobal,
482
+ tools: aiTools
483
+ },
484
+ ide: {
485
+ useGlobal: ideUseGlobal,
486
+ platforms: idePlatforms
487
+ }
488
+ });
489
+ return true;
490
+ }
491
+
380
492
  //#endregion
381
493
  //#region src/utils/intersection-display.ts
382
494
  /**
@@ -392,30 +504,842 @@ function displayIntersection(intersection) {
392
504
  R.info("No tool or IDE intersection data available.");
393
505
  return;
394
506
  }
395
- if (hasAiData) displayDimension("AI Tools", intersection.aiTools);
396
- if (hasIdeData) displayDimension("IDE Platforms", intersection.idePlatforms);
397
- }
398
- /**
399
- * Display a single dimension (AI tools or IDE platforms) of the intersection.
400
- */
401
- function displayDimension(label, dimension) {
402
- const lines = [];
403
- if (dimension.synced.length > 0) for (const item of dimension.synced) lines.push(` \u2713 ${item}`);
404
- if (dimension.unavailable.length > 0) for (const item of dimension.unavailable) lines.push(` - ${item} (not installed)`);
405
- if (dimension.unsupported.length > 0) for (const item of dimension.unsupported) lines.push(` ~ ${item} (not supported by profile)`);
406
- if (lines.length > 0) Ve(lines.join("\n"), label);
407
- }
408
- /**
409
- * Format a compact intersection summary for inline display.
410
- * Example: "claude-code, cursor (AI) + vscode (IDE)"
411
- */
412
- function formatIntersectionSummary(intersection) {
413
- const parts = [];
414
- if (intersection.aiTools.synced.length > 0) parts.push(`${intersection.aiTools.synced.join(", ")} (AI)`);
415
- if (intersection.idePlatforms.synced.length > 0) parts.push(`${intersection.idePlatforms.synced.join(", ")} (IDE)`);
416
- if (parts.length === 0) return "No matching tools";
417
- return parts.join(" + ");
418
- }
507
+ if (hasAiData) displayDimension("AI Tools", intersection.aiTools);
508
+ if (hasIdeData) displayDimension("IDE Platforms", intersection.idePlatforms);
509
+ }
510
+ /**
511
+ * Display a single dimension (AI tools or IDE platforms) of the intersection.
512
+ */
513
+ function displayDimension(label, dimension) {
514
+ const lines = [];
515
+ if (dimension.synced.length > 0) for (const item of dimension.synced) lines.push(` \u2713 ${item}`);
516
+ if (dimension.unavailable.length > 0) for (const item of dimension.unavailable) lines.push(` - ${item} (not installed)`);
517
+ if (dimension.unsupported.length > 0) for (const item of dimension.unsupported) lines.push(` ~ ${item} (not supported by profile)`);
518
+ if (lines.length > 0) Ve(lines.join("\n"), label);
519
+ }
520
+ /**
521
+ * Format a compact intersection summary for inline display.
522
+ * Example: "claude-code, cursor (AI) + vscode (IDE)"
523
+ */
524
+ function formatIntersectionSummary(intersection) {
525
+ const parts = [];
526
+ if (intersection.aiTools.synced.length > 0) parts.push(`${intersection.aiTools.synced.join(", ")} (AI)`);
527
+ if (intersection.idePlatforms.synced.length > 0) parts.push(`${intersection.idePlatforms.synced.join(", ")} (IDE)`);
528
+ if (parts.length === 0) return "No matching tools";
529
+ return parts.join(" + ");
530
+ }
531
+
532
+ //#endregion
533
+ //#region src/commands/sync-pipeline.ts
534
+ const validCategories = [
535
+ "ai",
536
+ "files",
537
+ "ide"
538
+ ];
539
+ /** Get or initialize placed files for a profile, avoiding unsafe `as` casts on Map.get(). */
540
+ function getOrCreatePlacedFiles(map, profileName) {
541
+ let files = map.get(profileName);
542
+ if (!files) {
543
+ files = {};
544
+ map.set(profileName, files);
545
+ }
546
+ return files;
547
+ }
548
+ /**
549
+ * Recursively copy all files from sourceDir to targetDir.
550
+ * Returns the number of files written (skips identical content).
551
+ */
552
+ async function copyDirectoryRecursive(sourceDir, targetDir) {
553
+ await mkdir(targetDir, { recursive: true });
554
+ const entries = await readdir(sourceDir, { withFileTypes: true });
555
+ let placed = 0;
556
+ for (const entry of entries) {
557
+ const sourcePath = resolve(sourceDir, entry.name);
558
+ const targetPath = resolve(targetDir, entry.name);
559
+ if (entry.isDirectory()) placed += await copyDirectoryRecursive(sourcePath, targetPath);
560
+ else {
561
+ const content = await readFile(sourcePath, "utf-8");
562
+ if (await readFile(targetPath, "utf-8").catch(() => void 0) !== content) {
563
+ await writeFile(targetPath, content, "utf-8");
564
+ placed++;
565
+ }
566
+ }
567
+ }
568
+ return placed;
569
+ }
570
+ /**
571
+ * Handle .gitignore update based on the project manifest's gitignore setting.
572
+ *
573
+ * When gitignore is enabled (default): writes comprehensive patterns for ALL
574
+ * known AI tools and IDE platforms to ensure stable, dev-independent content.
575
+ * When disabled: removes any existing managed section.
576
+ * Always ensures .baton/ is gitignored regardless of setting.
577
+ */
578
+ async function handleGitignoreUpdate(params) {
579
+ const { projectManifest, fileMap, projectRoot, spinner } = params;
580
+ const gitignoreEnabled = projectManifest.gitignore !== false;
581
+ await ensureBatonDirGitignored(projectRoot);
582
+ if (gitignoreEnabled) {
583
+ spinner.start("Updating .gitignore...");
584
+ const updated = await updateGitignore(projectRoot, collectComprehensivePatterns({ fileTargets: [...fileMap.values()].map((f) => f.target) }));
585
+ spinner.stop(updated ? "Updated .gitignore with managed patterns" : ".gitignore already up to date");
586
+ } else {
587
+ spinner.start("Checking .gitignore...");
588
+ const removed = await removeGitignoreManagedSection(projectRoot);
589
+ spinner.stop(removed ? "Removed managed section from .gitignore" : ".gitignore unchanged");
590
+ }
591
+ }
592
+ /**
593
+ * Generate and write the baton.lock lockfile from placed files and profile metadata.
594
+ */
595
+ async function writeLockData(params) {
596
+ const { allProfiles, sourceShas, placedFiles, projectRoot, spinner } = params;
597
+ spinner.start("Updating lockfile...");
598
+ const lockPackages = {};
599
+ for (const profile of allProfiles) lockPackages[profile.name] = {
600
+ source: profile.source,
601
+ resolved: profile.source,
602
+ version: profile.manifest.version,
603
+ sha: sourceShas.get(profile.source) || "unknown",
604
+ files: placedFiles.get(profile.name) || {}
605
+ };
606
+ await writeLock(generateLock(lockPackages), resolve(projectRoot, "baton.lock"));
607
+ spinner.stop("Lockfile updated");
608
+ }
609
+ /**
610
+ * Detect and remove files that were in the previous lockfile but are no longer
611
+ * part of the current sync. Cleans up empty parent directories.
612
+ */
613
+ async function cleanupOrphanedFiles(params) {
614
+ const { previousPaths, placedFiles, projectRoot, dryRun, autoYes, spinner } = params;
615
+ if (previousPaths.size === 0) return;
616
+ const currentPaths = /* @__PURE__ */ new Set();
617
+ for (const files of placedFiles.values()) for (const filePath of Object.keys(files)) currentPaths.add(filePath);
618
+ const orphanedPaths = [...previousPaths].filter((prev) => !currentPaths.has(prev));
619
+ if (orphanedPaths.length === 0) return;
620
+ if (dryRun) {
621
+ R.warn(`Would remove ${orphanedPaths.length} orphaned file(s):`);
622
+ for (const orphanedPath of orphanedPaths) R.info(` Removed: ${orphanedPath}`);
623
+ return;
624
+ }
625
+ R.warn(`Found ${orphanedPaths.length} orphaned file(s) to remove:`);
626
+ for (const orphanedPath of orphanedPaths) R.info(` Removed: ${orphanedPath}`);
627
+ let shouldRemove = autoYes;
628
+ if (!autoYes) {
629
+ const confirmed = await Re({
630
+ message: `Remove ${orphanedPaths.length} orphaned file(s)?`,
631
+ initialValue: true
632
+ });
633
+ if (Ct(confirmed)) {
634
+ R.info("Skipped orphan removal.");
635
+ shouldRemove = false;
636
+ } else shouldRemove = confirmed;
637
+ }
638
+ if (!shouldRemove) {
639
+ R.info("Orphan removal skipped.");
640
+ return;
641
+ }
642
+ spinner.start("Removing orphaned files...");
643
+ const removedCount = await removePlacedFiles(orphanedPaths, projectRoot);
644
+ spinner.stop(`Removed ${removedCount} orphaned file(s)`);
645
+ }
646
+
647
+ //#endregion
648
+ //#region src/commands/apply.ts
649
+ /** Extract the package name from a source string for lockfile lookup. */
650
+ function getPackageNameFromSource(source, parsed) {
651
+ if (parsed.provider === "github" || parsed.provider === "gitlab") return `${parsed.org}/${parsed.repo}`;
652
+ if (parsed.provider === "npm") return parsed.scope ? `${parsed.scope}/${parsed.package}` : parsed.package;
653
+ if (parsed.provider === "git") return parsed.url;
654
+ return source;
655
+ }
656
+ const applyCommand = defineCommand({
657
+ meta: {
658
+ name: "apply",
659
+ description: "Apply locked configurations to the project (deterministic, reproducible)"
660
+ },
661
+ args: {
662
+ "dry-run": {
663
+ type: "boolean",
664
+ description: "Show what would be done without writing files",
665
+ default: false
666
+ },
667
+ category: {
668
+ type: "string",
669
+ description: "Apply only a specific category: ai, files, or ide",
670
+ required: false
671
+ },
672
+ yes: {
673
+ type: "boolean",
674
+ description: "Run non-interactively (no prompts)",
675
+ default: false
676
+ },
677
+ verbose: {
678
+ type: "boolean",
679
+ alias: "v",
680
+ description: "Show detailed output for each placed file",
681
+ default: false
682
+ },
683
+ fresh: {
684
+ type: "boolean",
685
+ description: "Force cache bypass (re-clone even if cached)",
686
+ default: false
687
+ }
688
+ },
689
+ async run({ args }) {
690
+ const dryRun = args["dry-run"];
691
+ const categoryArg = args.category;
692
+ const autoYes = args.yes;
693
+ const verbose = args.verbose;
694
+ const fresh = args.fresh;
695
+ let category;
696
+ if (categoryArg) {
697
+ if (!validCategories.includes(categoryArg)) {
698
+ Ne(`Invalid category "${categoryArg}". Valid categories: ${validCategories.join(", ")}`);
699
+ process.exit(1);
700
+ }
701
+ category = categoryArg;
702
+ }
703
+ const syncAi = !category || category === "ai";
704
+ const syncFiles = !category || category === "files";
705
+ const syncIde = !category || category === "ide";
706
+ We(category ? `📦 Baton Apply (category: ${category})` : "📦 Baton Apply");
707
+ const stats = {
708
+ created: 0,
709
+ errors: 0
710
+ };
711
+ try {
712
+ const projectRoot = process.cwd();
713
+ const manifestPath = resolve(projectRoot, "baton.yaml");
714
+ let projectManifest;
715
+ try {
716
+ projectManifest = await loadProjectManifest(manifestPath);
717
+ } catch (error) {
718
+ if (error instanceof FileNotFoundError) Ne("baton.yaml not found. Run `baton init` first.");
719
+ else Ne(`Failed to load baton.yaml: ${error instanceof Error ? error.message : String(error)}`);
720
+ process.exit(1);
721
+ }
722
+ await promptFirstRunPreferences(projectRoot, !!args.yes);
723
+ const lockfilePath = resolve(projectRoot, "baton.lock");
724
+ let lockfile = null;
725
+ try {
726
+ lockfile = await readLock(lockfilePath);
727
+ } catch {
728
+ if (verbose) R.warn("No lockfile found. Falling back to manifest versions.");
729
+ }
730
+ const maxCacheAgeMs = fresh ? 0 : void 0;
731
+ const previousPaths = /* @__PURE__ */ new Set();
732
+ if (lockfile) for (const pkg of Object.values(lockfile.packages)) for (const filePath of Object.keys(pkg.integrity)) previousPaths.add(filePath);
733
+ const spinner = bt();
734
+ spinner.start("Resolving profile chain...");
735
+ const allProfiles = [];
736
+ const sourceShas = /* @__PURE__ */ new Map();
737
+ for (const profileSource of projectManifest.profiles || []) try {
738
+ if (verbose) R.info(`Resolving source: ${profileSource.source}`);
739
+ const parsed = parseSource(profileSource.source);
740
+ let manifestPath;
741
+ let cloneContext;
742
+ if (parsed.provider === "local" || parsed.provider === "file") {
743
+ const absolutePath = parsed.path.startsWith("/") ? parsed.path : resolve(projectRoot, parsed.path);
744
+ manifestPath = resolve(absolutePath, "baton.profile.yaml");
745
+ try {
746
+ const git = esm_default(absolutePath);
747
+ await git.checkIsRepo();
748
+ const sha = await git.revparse(["HEAD"]);
749
+ sourceShas.set(profileSource.source, sha.trim());
750
+ } catch {
751
+ sourceShas.set(profileSource.source, "local");
752
+ }
753
+ } else {
754
+ const url = parsed.provider === "github" || parsed.provider === "gitlab" ? parsed.url : parsed.provider === "git" ? parsed.url : "";
755
+ if (!url) throw new Error(`Invalid source: ${profileSource.source}`);
756
+ let ref = profileSource.version;
757
+ if (lockfile) {
758
+ const packageName = getPackageNameFromSource(profileSource.source, parsed);
759
+ const lockedPkg = lockfile.packages[packageName];
760
+ if (lockedPkg?.sha && lockedPkg.sha !== "unknown") {
761
+ ref = lockedPkg.sha;
762
+ if (verbose) R.info(`Using locked SHA for ${profileSource.source}: ${ref.slice(0, 12)}`);
763
+ }
764
+ }
765
+ const cloned = await cloneGitSource({
766
+ url,
767
+ ref,
768
+ subpath: "subpath" in parsed ? parsed.subpath : void 0,
769
+ useCache: true,
770
+ maxCacheAgeMs
771
+ });
772
+ manifestPath = resolve(cloned.localPath, "baton.profile.yaml");
773
+ sourceShas.set(profileSource.source, cloned.sha);
774
+ cloneContext = {
775
+ cachePath: cloned.cachePath,
776
+ sparseCheckout: cloned.sparseCheckout
777
+ };
778
+ }
779
+ const manifest = await loadProfileManifest(manifestPath);
780
+ const profileDir = dirname(manifestPath);
781
+ const chain = await resolveProfileChain(manifest, profileSource.source, profileDir, cloneContext);
782
+ allProfiles.push(...chain);
783
+ } catch (error) {
784
+ spinner.stop(`Failed to resolve profile ${profileSource.source}: ${error}`);
785
+ stats.errors++;
786
+ }
787
+ if (allProfiles.length === 0) {
788
+ spinner.stop("No profiles configured");
789
+ Le("Nothing to apply. Run `baton manage` to add a profile.");
790
+ process.exit(2);
791
+ }
792
+ spinner.stop(`Resolved ${allProfiles.length} profile(s)`);
793
+ const weightSortedProfiles = sortProfilesByWeight(allProfiles);
794
+ spinner.start("Merging configurations...");
795
+ const allWeightWarnings = [];
796
+ const skillsResult = mergeSkillsWithWarnings(weightSortedProfiles);
797
+ const mergedSkills = skillsResult.skills;
798
+ allWeightWarnings.push(...skillsResult.warnings);
799
+ const rulesResult = mergeRulesWithWarnings(weightSortedProfiles);
800
+ const mergedRules = rulesResult.rules;
801
+ allWeightWarnings.push(...rulesResult.warnings);
802
+ const agentsResult = mergeAgentsWithWarnings(weightSortedProfiles);
803
+ const mergedAgents = agentsResult.agents;
804
+ allWeightWarnings.push(...agentsResult.warnings);
805
+ const memoryResult = mergeMemoryWithWarnings(weightSortedProfiles);
806
+ const mergedMemory = memoryResult.entries;
807
+ allWeightWarnings.push(...memoryResult.warnings);
808
+ const commandMap = /* @__PURE__ */ new Map();
809
+ const lockedCommands = /* @__PURE__ */ new Set();
810
+ const commandOwner = /* @__PURE__ */ new Map();
811
+ for (const profile of weightSortedProfiles) {
812
+ const weight = getProfileWeight(profile);
813
+ const locked = isLockedProfile(profile);
814
+ for (const cmd of profile.manifest.ai?.commands || []) {
815
+ if (lockedCommands.has(cmd)) continue;
816
+ const existing = commandOwner.get(cmd);
817
+ if (existing && existing.weight === weight && existing.profileName !== profile.name) allWeightWarnings.push({
818
+ key: cmd,
819
+ category: "command",
820
+ profileA: existing.profileName,
821
+ profileB: profile.name,
822
+ weight
823
+ });
824
+ commandMap.set(cmd, profile.name);
825
+ commandOwner.set(cmd, {
826
+ profileName: profile.name,
827
+ weight
828
+ });
829
+ if (locked) lockedCommands.add(cmd);
830
+ }
831
+ }
832
+ const mergedCommandCount = commandMap.size;
833
+ const fileMap = /* @__PURE__ */ new Map();
834
+ const lockedFiles = /* @__PURE__ */ new Set();
835
+ const fileOwner = /* @__PURE__ */ new Map();
836
+ for (const profile of weightSortedProfiles) {
837
+ const weight = getProfileWeight(profile);
838
+ const locked = isLockedProfile(profile);
839
+ for (const fileConfig of profile.manifest.files || []) {
840
+ const target = fileConfig.target || fileConfig.source;
841
+ if (lockedFiles.has(target)) continue;
842
+ const existing = fileOwner.get(target);
843
+ if (existing && existing.weight === weight && existing.profileName !== profile.name) allWeightWarnings.push({
844
+ key: target,
845
+ category: "file",
846
+ profileA: existing.profileName,
847
+ profileB: profile.name,
848
+ weight
849
+ });
850
+ fileMap.set(target, {
851
+ source: fileConfig.source,
852
+ target,
853
+ profileName: profile.name
854
+ });
855
+ fileOwner.set(target, {
856
+ profileName: profile.name,
857
+ weight
858
+ });
859
+ if (locked) lockedFiles.add(target);
860
+ }
861
+ }
862
+ const mergedFileCount = fileMap.size;
863
+ const ideMap = /* @__PURE__ */ new Map();
864
+ const lockedIdeConfigs = /* @__PURE__ */ new Set();
865
+ const ideOwner = /* @__PURE__ */ new Map();
866
+ for (const profile of weightSortedProfiles) {
867
+ if (!profile.manifest.ide) continue;
868
+ const weight = getProfileWeight(profile);
869
+ const locked = isLockedProfile(profile);
870
+ for (const [ideKey, files] of Object.entries(profile.manifest.ide)) {
871
+ if (!files) continue;
872
+ const targetDir = getIdePlatformTargetDir(ideKey);
873
+ if (!targetDir) {
874
+ if (!isKnownIdePlatform(ideKey)) R.warn(`Unknown IDE platform "${ideKey}" in profile "${profile.name}" — skipping. Register it in the IDE platform registry.`);
875
+ continue;
876
+ }
877
+ for (const fileName of files) {
878
+ const targetPath = `${targetDir}/${fileName}`;
879
+ if (lockedIdeConfigs.has(targetPath)) continue;
880
+ const existing = ideOwner.get(targetPath);
881
+ if (existing && existing.weight === weight && existing.profileName !== profile.name) allWeightWarnings.push({
882
+ key: targetPath,
883
+ category: "ide",
884
+ profileA: existing.profileName,
885
+ profileB: profile.name,
886
+ weight
887
+ });
888
+ ideMap.set(targetPath, {
889
+ ideKey,
890
+ fileName,
891
+ targetDir,
892
+ profileName: profile.name
893
+ });
894
+ ideOwner.set(targetPath, {
895
+ profileName: profile.name,
896
+ weight
897
+ });
898
+ if (locked) lockedIdeConfigs.add(targetPath);
899
+ }
900
+ }
901
+ }
902
+ const mergedIdeCount = ideMap.size;
903
+ spinner.stop(`Merged: ${mergedSkills.length} skills, ${mergedRules.length} rules, ${mergedAgents.length} agents, ${mergedMemory.length} memory files, ${mergedCommandCount} commands, ${mergedFileCount} files, ${mergedIdeCount} IDE configs`);
904
+ if (allWeightWarnings.length > 0) for (const w of allWeightWarnings) R.warn(`Weight conflict: "${w.profileA}" and "${w.profileB}" both define ${w.category} "${w.key}" with weight ${w.weight}. Last declared wins.`);
905
+ spinner.start("Computing tool intersection...");
906
+ const prefs = await resolvePreferences(projectRoot);
907
+ const detectedAITools = await detectInstalledAITools();
908
+ if (verbose) {
909
+ R.info(`AI tools: ${prefs.ai.tools.join(", ") || "(none)"} (from ${prefs.ai.source} preferences)`);
910
+ R.info(`IDE platforms: ${prefs.ide.platforms.join(", ") || "(none)"} (from ${prefs.ide.source} preferences)`);
911
+ }
912
+ let syncedAiTools;
913
+ let syncedIdePlatforms = null;
914
+ let allIntersections = null;
915
+ if (prefs.ai.tools.length > 0) {
916
+ const developerTools = {
917
+ aiTools: prefs.ai.tools,
918
+ idePlatforms: prefs.ide.platforms
919
+ };
920
+ const aggregatedSyncedAi = /* @__PURE__ */ new Set();
921
+ const aggregatedSyncedIde = /* @__PURE__ */ new Set();
922
+ allIntersections = /* @__PURE__ */ new Map();
923
+ for (const profileSource of projectManifest.profiles || []) try {
924
+ const intersection = await buildIntersection(profileSource.source, developerTools, projectRoot);
925
+ if (intersection) {
926
+ allIntersections.set(profileSource.source, intersection);
927
+ for (const tool of intersection.aiTools.synced) aggregatedSyncedAi.add(tool);
928
+ for (const platform of intersection.idePlatforms.synced) aggregatedSyncedIde.add(platform);
929
+ }
930
+ } catch {}
931
+ syncedAiTools = aggregatedSyncedAi.size > 0 ? [...aggregatedSyncedAi] : [];
932
+ syncedIdePlatforms = [...aggregatedSyncedIde];
933
+ } else {
934
+ syncedAiTools = detectedAITools;
935
+ syncedIdePlatforms = null;
936
+ if (detectedAITools.length > 0) {
937
+ R.warn("No AI tools configured. Run `baton ai-tools scan` to configure your tools.");
938
+ R.info(`Falling back to detected tools: ${detectedAITools.join(", ")}`);
939
+ }
940
+ }
941
+ if (syncedAiTools.length === 0 && detectedAITools.length === 0) {
942
+ spinner.stop("No AI tools available");
943
+ Ne("No AI tools found. Install an AI coding tool first.");
944
+ process.exit(1);
945
+ }
946
+ if (syncedAiTools.length === 0) {
947
+ spinner.stop("No AI tools in intersection");
948
+ Ne("No AI tools match between your configuration and profile support. Run `baton ai-tools scan` or check your profile's supported tools.");
949
+ process.exit(1);
950
+ }
951
+ if (allIntersections) for (const [source, intersection] of allIntersections) if (verbose) {
952
+ R.step(`Intersection for ${source}`);
953
+ displayIntersection(intersection);
954
+ } else {
955
+ const summary = formatIntersectionSummary(intersection);
956
+ R.info(`Applying for: ${summary}`);
957
+ }
958
+ const ideSummary = syncedIdePlatforms && syncedIdePlatforms.length > 0 ? ` | IDE platforms: ${syncedIdePlatforms.join(", ")}` : "";
959
+ spinner.stop(`Applying to AI tools: ${syncedAiTools.join(", ")}${ideSummary}`);
960
+ spinner.start("Checking for legacy paths...");
961
+ const legacyFiles = await detectLegacyPaths(projectRoot);
962
+ if (legacyFiles.length > 0 && !dryRun) {
963
+ spinner.stop(`Found ${legacyFiles.length} legacy file(s)`);
964
+ if (!autoYes) {
965
+ Ve(`Found legacy configuration files:\n${legacyFiles.map((f) => ` - ${f.legacyPath}`).join("\n")}`, "Legacy Files");
966
+ R.warn("Run migration manually with appropriate action (migrate/copy/skip)");
967
+ }
968
+ } else spinner.stop("No legacy files found");
969
+ spinner.start("Processing configurations...");
970
+ const adapters = getAIToolAdaptersForKeys(syncedAiTools);
971
+ const placementConfig = {
972
+ mode: "copy",
973
+ projectRoot
974
+ };
975
+ const placedFiles = /* @__PURE__ */ new Map();
976
+ const profileLocalPaths = /* @__PURE__ */ new Map();
977
+ for (const profileSource of projectManifest.profiles || []) {
978
+ const parsed = parseSource(profileSource.source);
979
+ if (parsed.provider === "local" || parsed.provider === "file") {
980
+ const localPath = parsed.path.startsWith("/") ? parsed.path : resolve(projectRoot, parsed.path);
981
+ for (const prof of allProfiles) if (prof.source === profileSource.source) profileLocalPaths.set(prof.name, localPath);
982
+ } else if (parsed.provider === "github" || parsed.provider === "gitlab" || parsed.provider === "git") {
983
+ const url = parsed.provider === "git" ? parsed.url : parsed.url;
984
+ let ref = profileSource.version;
985
+ if (lockfile) {
986
+ const packageName = getPackageNameFromSource(profileSource.source, parsed);
987
+ const lockedPkg = lockfile.packages[packageName];
988
+ if (lockedPkg?.sha && lockedPkg.sha !== "unknown") ref = lockedPkg.sha;
989
+ }
990
+ const cloned = await cloneGitSource({
991
+ url,
992
+ ref,
993
+ subpath: "subpath" in parsed ? parsed.subpath : void 0,
994
+ useCache: true,
995
+ maxCacheAgeMs
996
+ });
997
+ for (const prof of allProfiles) if (prof.source === profileSource.source) profileLocalPaths.set(prof.name, cloned.localPath);
998
+ }
999
+ }
1000
+ for (const prof of allProfiles) if (!profileLocalPaths.has(prof.name) && prof.localPath) profileLocalPaths.set(prof.name, prof.localPath);
1001
+ const contentAccumulator = /* @__PURE__ */ new Map();
1002
+ if (!dryRun && syncAi) for (const adapter of adapters) {
1003
+ if (verbose) R.step(`[${adapter.key}] Placing memory files...`);
1004
+ for (const memoryEntry of mergedMemory) try {
1005
+ const contentParts = [];
1006
+ for (const contribution of memoryEntry.contributions) {
1007
+ const profileDir = profileLocalPaths.get(contribution.profileName);
1008
+ if (!profileDir) {
1009
+ spinner.message(`Warning: Could not resolve local path for profile ${contribution.profileName}`);
1010
+ continue;
1011
+ }
1012
+ const memoryFilePath = resolve(profileDir, "ai", "memory", memoryEntry.filename);
1013
+ try {
1014
+ const content = await readFile(memoryFilePath, "utf-8");
1015
+ contentParts.push(content);
1016
+ } catch {
1017
+ spinner.message(`Warning: Could not read ${memoryFilePath}`);
1018
+ }
1019
+ }
1020
+ if (contentParts.length === 0) continue;
1021
+ const mergedContent = mergeContentParts(contentParts, memoryEntry.mergeStrategy);
1022
+ const transformed = adapter.transformMemory({
1023
+ filename: memoryEntry.filename,
1024
+ content: mergedContent
1025
+ });
1026
+ const targetPath = adapter.getPath("memory", "project", transformed.filename);
1027
+ const absolutePath = targetPath.startsWith("/") ? targetPath : resolve(projectRoot, targetPath);
1028
+ const existing = contentAccumulator.get(absolutePath);
1029
+ if (existing) {
1030
+ existing.parts.push(transformed.content);
1031
+ for (const c of memoryEntry.contributions) existing.profiles.add(c.profileName);
1032
+ } else {
1033
+ const profiles = /* @__PURE__ */ new Set();
1034
+ for (const c of memoryEntry.contributions) profiles.add(c.profileName);
1035
+ contentAccumulator.set(absolutePath, {
1036
+ parts: [transformed.content],
1037
+ adapter,
1038
+ type: "memory",
1039
+ name: transformed.filename,
1040
+ profiles
1041
+ });
1042
+ }
1043
+ } catch (error) {
1044
+ spinner.message(`Error placing ${memoryEntry.filename} for ${adapter.name}: ${error}`);
1045
+ stats.errors++;
1046
+ }
1047
+ }
1048
+ if (!dryRun && syncAi) for (const adapter of adapters) {
1049
+ if (verbose) R.step(`[${adapter.key}] Placing skills...`);
1050
+ for (const skillItem of mergedSkills) try {
1051
+ const profileDir = profileLocalPaths.get(skillItem.profileName);
1052
+ if (!profileDir) {
1053
+ spinner.message(`Warning: Could not resolve local path for profile ${skillItem.profileName}`);
1054
+ continue;
1055
+ }
1056
+ const skillSourceDir = resolve(profileDir, "ai", "skills", skillItem.name);
1057
+ try {
1058
+ await stat(skillSourceDir);
1059
+ } catch {
1060
+ spinner.message(`Warning: Skill directory not found: ${skillSourceDir}`);
1061
+ continue;
1062
+ }
1063
+ const targetSkillPath = adapter.getPath("skills", skillItem.scope, skillItem.name);
1064
+ const absoluteTargetDir = targetSkillPath.startsWith("/") ? targetSkillPath : resolve(projectRoot, targetSkillPath);
1065
+ const placed = await copyDirectoryRecursive(skillSourceDir, absoluteTargetDir);
1066
+ stats.created += placed;
1067
+ const profileFiles = getOrCreatePlacedFiles(placedFiles, skillItem.profileName);
1068
+ try {
1069
+ profileFiles[targetSkillPath] = {
1070
+ content: await readFile(resolve(skillSourceDir, "index.md"), "utf-8"),
1071
+ tool: adapter.key,
1072
+ category: "ai"
1073
+ };
1074
+ } catch {
1075
+ profileFiles[targetSkillPath] = {
1076
+ content: skillItem.name,
1077
+ tool: adapter.key,
1078
+ category: "ai"
1079
+ };
1080
+ }
1081
+ if (verbose) {
1082
+ const label = placed > 0 ? `${placed} file(s) created` : "unchanged, skipped";
1083
+ R.info(` -> ${absoluteTargetDir}/ (${label})`);
1084
+ }
1085
+ } catch (error) {
1086
+ spinner.message(`Error placing skill ${skillItem.name} for ${adapter.name}: ${error}`);
1087
+ stats.errors++;
1088
+ }
1089
+ }
1090
+ if (!dryRun && syncAi) for (const adapter of adapters) {
1091
+ if (verbose) R.step(`[${adapter.key}] Placing rules...`);
1092
+ for (const ruleEntry of mergedRules) try {
1093
+ const ruleName = ruleEntry.name.replace(/\.md$/, "");
1094
+ const isUniversal = ruleEntry.agents.length === 0;
1095
+ const isForThisAdapter = ruleEntry.agents.includes(adapter.key);
1096
+ if (!isUniversal && !isForThisAdapter) continue;
1097
+ const profileDir = profileLocalPaths.get(ruleEntry.profileName);
1098
+ if (!profileDir) {
1099
+ spinner.message(`Warning: Could not resolve local path for profile ${ruleEntry.profileName}`);
1100
+ continue;
1101
+ }
1102
+ const ruleSourcePath = resolve(profileDir, "ai", "rules", isUniversal ? "universal" : ruleEntry.agents[0], `${ruleName}.md`);
1103
+ let rawContent;
1104
+ try {
1105
+ rawContent = await readFile(ruleSourcePath, "utf-8");
1106
+ } catch {
1107
+ spinner.message(`Warning: Could not read rule file: ${ruleSourcePath}`);
1108
+ continue;
1109
+ }
1110
+ const parsed = parseFrontmatter(rawContent);
1111
+ const ruleFile = {
1112
+ name: ruleName,
1113
+ content: rawContent,
1114
+ frontmatter: Object.keys(parsed.data).length > 0 ? parsed.data : void 0
1115
+ };
1116
+ const transformed = adapter.transformRule(ruleFile);
1117
+ const targetPath = adapter.getPath("rules", "project", ruleName);
1118
+ const absolutePath = targetPath.startsWith("/") ? targetPath : resolve(projectRoot, targetPath);
1119
+ const existing = contentAccumulator.get(absolutePath);
1120
+ if (existing) {
1121
+ existing.parts.push(transformed.content);
1122
+ existing.profiles.add(ruleEntry.profileName);
1123
+ } else contentAccumulator.set(absolutePath, {
1124
+ parts: [transformed.content],
1125
+ adapter,
1126
+ type: "rules",
1127
+ name: ruleName,
1128
+ profiles: new Set([ruleEntry.profileName])
1129
+ });
1130
+ } catch (error) {
1131
+ spinner.message(`Error placing rule ${ruleEntry.name} for ${adapter.name}: ${error}`);
1132
+ stats.errors++;
1133
+ }
1134
+ }
1135
+ if (!dryRun && syncAi) for (const adapter of adapters) {
1136
+ if (verbose) R.step(`[${adapter.key}] Placing agents...`);
1137
+ for (const agentEntry of mergedAgents) try {
1138
+ const agentName = agentEntry.name.replace(/\.md$/, "");
1139
+ const isUniversal = agentEntry.agents.length === 0;
1140
+ const isForThisAdapter = agentEntry.agents.includes(adapter.key);
1141
+ if (!isUniversal && !isForThisAdapter) continue;
1142
+ const profileDir = profileLocalPaths.get(agentEntry.profileName);
1143
+ if (!profileDir) {
1144
+ spinner.message(`Warning: Could not resolve local path for profile ${agentEntry.profileName}`);
1145
+ continue;
1146
+ }
1147
+ const agentSourcePath = resolve(profileDir, "ai", "agents", isUniversal ? "universal" : agentEntry.agents[0], `${agentName}.md`);
1148
+ let rawContent;
1149
+ try {
1150
+ rawContent = await readFile(agentSourcePath, "utf-8");
1151
+ } catch {
1152
+ spinner.message(`Warning: Could not read agent file: ${agentSourcePath}`);
1153
+ continue;
1154
+ }
1155
+ const parsed = parseFrontmatter(rawContent);
1156
+ const frontmatter = Object.keys(parsed.data).length > 0 ? parsed.data : { name: agentName };
1157
+ const agentFile = {
1158
+ name: agentName,
1159
+ content: rawContent,
1160
+ description: frontmatter.description,
1161
+ frontmatter
1162
+ };
1163
+ const transformed = adapter.transformAgent(agentFile);
1164
+ const targetPath = adapter.getPath("agents", "project", agentName);
1165
+ const absolutePath = targetPath.startsWith("/") ? targetPath : resolve(projectRoot, targetPath);
1166
+ const existing = contentAccumulator.get(absolutePath);
1167
+ if (existing) {
1168
+ existing.parts.push(transformed.content);
1169
+ existing.profiles.add(agentEntry.profileName);
1170
+ } else contentAccumulator.set(absolutePath, {
1171
+ parts: [transformed.content],
1172
+ adapter,
1173
+ type: "agents",
1174
+ name: agentName,
1175
+ profiles: new Set([agentEntry.profileName])
1176
+ });
1177
+ } catch (error) {
1178
+ spinner.message(`Error placing agent ${agentEntry.name} for ${adapter.name}: ${error}`);
1179
+ stats.errors++;
1180
+ }
1181
+ }
1182
+ if (!dryRun && syncAi) for (const [absolutePath, entry] of contentAccumulator) try {
1183
+ const combinedContent = entry.parts.join("\n\n");
1184
+ const result = await placeFile(combinedContent, entry.adapter, entry.type, "project", entry.name, placementConfig);
1185
+ if (result.action !== "skipped") stats.created++;
1186
+ const relPath = isAbsolute(result.path) ? relative(projectRoot, result.path) : result.path;
1187
+ for (const profileName of entry.profiles) {
1188
+ const pf = getOrCreatePlacedFiles(placedFiles, profileName);
1189
+ pf[relPath] = {
1190
+ content: combinedContent,
1191
+ tool: entry.adapter.key,
1192
+ category: "ai"
1193
+ };
1194
+ }
1195
+ if (verbose) {
1196
+ const label = result.action === "skipped" ? "unchanged, skipped" : result.action;
1197
+ R.info(` -> ${result.path} (${label})`);
1198
+ }
1199
+ } catch (error) {
1200
+ spinner.message(`Error placing accumulated content to ${absolutePath}: ${error}`);
1201
+ stats.errors++;
1202
+ }
1203
+ if (!dryRun && syncAi) for (const adapter of adapters) {
1204
+ if (verbose) R.step(`[${adapter.key}] Placing commands...`);
1205
+ for (const profile of allProfiles) {
1206
+ const profileDir = profileLocalPaths.get(profile.name);
1207
+ if (!profileDir) continue;
1208
+ const commandNames = profile.manifest.ai?.commands || [];
1209
+ for (const commandName of commandNames) try {
1210
+ const commandSourcePath = resolve(profileDir, "ai", "commands", `${commandName}.md`);
1211
+ let content;
1212
+ try {
1213
+ content = await readFile(commandSourcePath, "utf-8");
1214
+ } catch {
1215
+ continue;
1216
+ }
1217
+ const result = await placeFile(content, adapter, "commands", "project", commandName, placementConfig);
1218
+ if (result.action !== "skipped") stats.created++;
1219
+ const cmdRelPath = isAbsolute(result.path) ? relative(projectRoot, result.path) : result.path;
1220
+ const pf = getOrCreatePlacedFiles(placedFiles, profile.name);
1221
+ pf[cmdRelPath] = {
1222
+ content,
1223
+ tool: adapter.key,
1224
+ category: "ai"
1225
+ };
1226
+ if (verbose) {
1227
+ const label = result.action === "skipped" ? "unchanged, skipped" : result.action;
1228
+ R.info(` -> ${result.path} (${label})`);
1229
+ }
1230
+ } catch (error) {
1231
+ spinner.message(`Error placing command ${commandName} for ${adapter.name}: ${error}`);
1232
+ stats.errors++;
1233
+ }
1234
+ }
1235
+ }
1236
+ if (!dryRun && syncFiles) for (const fileEntry of fileMap.values()) try {
1237
+ const profileDir = profileLocalPaths.get(fileEntry.profileName);
1238
+ if (!profileDir) continue;
1239
+ const fileSourcePath = resolve(profileDir, "files", fileEntry.source);
1240
+ let content;
1241
+ try {
1242
+ content = await readFile(fileSourcePath, "utf-8");
1243
+ } catch {
1244
+ continue;
1245
+ }
1246
+ const targetPath = resolve(projectRoot, fileEntry.target);
1247
+ await mkdir(dirname(targetPath), { recursive: true });
1248
+ if (await readFile(targetPath, "utf-8").catch(() => void 0) !== content) {
1249
+ await writeFile(targetPath, content, "utf-8");
1250
+ stats.created++;
1251
+ if (verbose) R.info(` -> ${fileEntry.target} (created)`);
1252
+ } else if (verbose) R.info(` -> ${fileEntry.target} (unchanged, skipped)`);
1253
+ const fpf = getOrCreatePlacedFiles(placedFiles, fileEntry.profileName);
1254
+ fpf[fileEntry.target] = {
1255
+ content,
1256
+ category: "files"
1257
+ };
1258
+ } catch (error) {
1259
+ spinner.message(`Error placing file ${fileEntry.source}: ${error}`);
1260
+ stats.errors++;
1261
+ }
1262
+ if (!dryRun && syncIde) for (const ideEntry of ideMap.values()) try {
1263
+ if (syncedIdePlatforms !== null && !syncedIdePlatforms.includes(ideEntry.ideKey)) {
1264
+ if (verbose) R.info(` -> ${ideEntry.targetDir}/${ideEntry.fileName} (skipped — IDE platform "${ideEntry.ideKey}" not in intersection)`);
1265
+ continue;
1266
+ }
1267
+ const profileDir = profileLocalPaths.get(ideEntry.profileName);
1268
+ if (!profileDir) continue;
1269
+ const ideSourcePath = resolve(profileDir, "ide", ideEntry.ideKey, ideEntry.fileName);
1270
+ let content;
1271
+ try {
1272
+ content = await readFile(ideSourcePath, "utf-8");
1273
+ } catch {
1274
+ continue;
1275
+ }
1276
+ const targetPath = resolve(projectRoot, ideEntry.targetDir, ideEntry.fileName);
1277
+ await mkdir(dirname(targetPath), { recursive: true });
1278
+ if (await readFile(targetPath, "utf-8").catch(() => void 0) !== content) {
1279
+ await writeFile(targetPath, content, "utf-8");
1280
+ stats.created++;
1281
+ if (verbose) R.info(` -> ${ideEntry.targetDir}/${ideEntry.fileName} (created)`);
1282
+ } else if (verbose) R.info(` -> ${ideEntry.targetDir}/${ideEntry.fileName} (unchanged, skipped)`);
1283
+ const ideRelPath = `${ideEntry.targetDir}/${ideEntry.fileName}`;
1284
+ const ipf = getOrCreatePlacedFiles(placedFiles, ideEntry.profileName);
1285
+ ipf[ideRelPath] = {
1286
+ content,
1287
+ tool: ideEntry.ideKey,
1288
+ category: "ide"
1289
+ };
1290
+ } catch (error) {
1291
+ spinner.message(`Error placing IDE config ${ideEntry.fileName}: ${error}`);
1292
+ stats.errors++;
1293
+ }
1294
+ spinner.stop(dryRun ? `Would place files for ${adapters.length} agent(s)` : `Placed ${stats.created} file(s) for ${adapters.length} agent(s)`);
1295
+ if (!dryRun) await handleGitignoreUpdate({
1296
+ projectManifest,
1297
+ fileMap,
1298
+ projectRoot,
1299
+ spinner
1300
+ });
1301
+ if (!dryRun) await writeLockData({
1302
+ allProfiles,
1303
+ sourceShas,
1304
+ placedFiles,
1305
+ projectRoot,
1306
+ spinner
1307
+ });
1308
+ await cleanupOrphanedFiles({
1309
+ previousPaths,
1310
+ placedFiles,
1311
+ projectRoot,
1312
+ dryRun,
1313
+ autoYes,
1314
+ spinner
1315
+ });
1316
+ if (dryRun) {
1317
+ const parts = [];
1318
+ if (syncAi) {
1319
+ parts.push(` • ${mergedSkills.length} skills`);
1320
+ parts.push(` • ${mergedRules.length} rules`);
1321
+ parts.push(` • ${mergedAgents.length} agents`);
1322
+ parts.push(` • ${mergedMemory.length} memory files`);
1323
+ parts.push(` • ${mergedCommandCount} commands`);
1324
+ }
1325
+ if (syncFiles) parts.push(` • ${mergedFileCount} files`);
1326
+ if (syncIde) {
1327
+ const filteredIdeCount = syncedIdePlatforms !== null ? [...ideMap.values()].filter((e) => syncedIdePlatforms.includes(e.ideKey)).length : mergedIdeCount;
1328
+ parts.push(` • ${filteredIdeCount} IDE configs`);
1329
+ }
1330
+ const categoryLabel = category ? ` (category: ${category})` : "";
1331
+ Le(`[Dry Run${categoryLabel}] Would apply:\n${parts.join("\n")}\n\nFor ${adapters.length} agent(s): ${syncedAiTools.join(", ")}`);
1332
+ } else {
1333
+ const categoryLabel = category ? ` (category: ${category})` : "";
1334
+ Le(`✅ Apply complete${categoryLabel}! Locked configurations applied.`);
1335
+ }
1336
+ process.exit(stats.errors > 0 ? 1 : 0);
1337
+ } catch (error) {
1338
+ Ne(`Apply failed: ${error}`);
1339
+ process.exit(1);
1340
+ }
1341
+ }
1342
+ });
419
1343
 
420
1344
  //#endregion
421
1345
  //#region src/commands/config/set.ts
@@ -943,7 +1867,7 @@ async function loadFilesFromDirectory(dirPath) {
943
1867
  /**
944
1868
  * Format an IDE platform key into a display name.
945
1869
  */
946
- function formatIdeName$2(ideKey) {
1870
+ function formatIdeName$1(ideKey) {
947
1871
  return {
948
1872
  vscode: "VS Code",
949
1873
  jetbrains: "JetBrains",
@@ -989,7 +1913,7 @@ async function runGlobalMode(nonInteractive) {
989
1913
  const options = getRegisteredIdePlatforms().map((ideKey) => {
990
1914
  return {
991
1915
  value: ideKey,
992
- label: currentPlatforms.includes(ideKey) ? `${formatIdeName$2(ideKey)} (currently saved)` : formatIdeName$2(ideKey)
1916
+ label: currentPlatforms.includes(ideKey) ? `${formatIdeName$1(ideKey)} (currently saved)` : formatIdeName$1(ideKey)
993
1917
  };
994
1918
  });
995
1919
  const selected = await je({
@@ -1076,7 +2000,7 @@ async function runProjectMode(nonInteractive) {
1076
2000
  const options = allIdeKeys.map((ideKey) => {
1077
2001
  return {
1078
2002
  value: ideKey,
1079
- label: globalPlatforms.includes(ideKey) ? `${formatIdeName$2(ideKey)} (in global config)` : formatIdeName$2(ideKey)
2003
+ label: globalPlatforms.includes(ideKey) ? `${formatIdeName$1(ideKey)} (in global config)` : formatIdeName$1(ideKey)
1080
2004
  };
1081
2005
  });
1082
2006
  const selected = await je({
@@ -1132,7 +2056,7 @@ const idesListCommand = defineCommand({
1132
2056
  const entry = idePlatformRegistry[ideKey];
1133
2057
  return {
1134
2058
  key: ideKey,
1135
- name: formatIdeName$2(ideKey),
2059
+ name: formatIdeName$1(ideKey),
1136
2060
  saved: isSaved,
1137
2061
  targetDir: entry?.targetDir ?? "unknown"
1138
2062
  };
@@ -1148,7 +2072,7 @@ const idesListCommand = defineCommand({
1148
2072
  R.info(`All ${allIdeKeys.length} supported platforms:`);
1149
2073
  for (const key of allIdeKeys) {
1150
2074
  const entry = idePlatformRegistry[key];
1151
- console.log(` \x1b[90m- ${formatIdeName$2(key)} (${entry?.targetDir ?? key})\x1b[0m`);
2075
+ console.log(` \x1b[90m- ${formatIdeName$1(key)} (${entry?.targetDir ?? key})\x1b[0m`);
1152
2076
  }
1153
2077
  Le("Run 'baton ides scan' to get started.");
1154
2078
  return;
@@ -1176,180 +2100,66 @@ const idesScanCommand = defineCommand({
1176
2100
  alias: "y",
1177
2101
  description: "Automatically save detected platforms without confirmation"
1178
2102
  } },
1179
- async run({ args }) {
1180
- We("Baton - IDE Platform Scanner");
1181
- const spinner = bt();
1182
- spinner.start("Scanning for IDE platforms...");
1183
- clearIdeCache();
1184
- const detectedIdes = await detectInstalledIdes();
1185
- const allIdeKeys = getRegisteredIdePlatforms();
1186
- const currentPlatforms = await getGlobalIdePlatforms();
1187
- spinner.stop("Scan complete.");
1188
- if (detectedIdes.length > 0) R.success(`Found ${detectedIdes.length} IDE platform${detectedIdes.length !== 1 ? "s" : ""} on your system.`);
1189
- else R.warn("No IDE platforms detected on your system.");
1190
- if (args.yes) {
1191
- if (detectedIdes.length !== currentPlatforms.length || detectedIdes.some((key) => !currentPlatforms.includes(key))) {
1192
- await setGlobalIdePlatforms(detectedIdes);
1193
- R.success(`Saved ${detectedIdes.length} detected platform(s) to global config.`);
1194
- } else R.info("Global config is already up to date.");
1195
- Le("Scan finished.");
1196
- return;
1197
- }
1198
- const options = allIdeKeys.map((ideKey) => {
1199
- return {
1200
- value: ideKey,
1201
- label: detectedIdes.includes(ideKey) ? `${formatIdeName$2(ideKey)} (detected)` : formatIdeName$2(ideKey)
1202
- };
1203
- });
1204
- const selected = await je({
1205
- message: "Select which IDE platforms to save:",
1206
- options,
1207
- initialValues: detectedIdes
1208
- });
1209
- if (Ct(selected)) {
1210
- Le("Scan finished (not saved).");
1211
- return;
1212
- }
1213
- const selectedKeys = selected;
1214
- if (selectedKeys.length !== currentPlatforms.length || selectedKeys.some((key) => !currentPlatforms.includes(key))) {
1215
- await setGlobalIdePlatforms(selectedKeys);
1216
- R.success(`Saved ${selectedKeys.length} platform(s) to global config.`);
1217
- } else R.info("Global config is already up to date.");
1218
- Le("Scan finished.");
1219
- }
1220
- });
1221
-
1222
- //#endregion
1223
- //#region src/commands/ides/index.ts
1224
- const idesCommand = defineCommand({
1225
- meta: {
1226
- name: "ides",
1227
- description: "Manage IDE platform detection and configuration"
1228
- },
1229
- subCommands: {
1230
- configure: idesConfigureCommand,
1231
- list: idesListCommand,
1232
- scan: idesScanCommand
1233
- }
1234
- });
1235
-
1236
- //#endregion
1237
- //#region src/utils/first-run-preferences.ts
1238
- var import_dist = require_dist();
1239
- /**
1240
- * Format an IDE platform key into a display name.
1241
- * Duplicated here to avoid circular dependency with ides/utils.
1242
- */
1243
- function formatIdeName$1(ideKey) {
1244
- return {
1245
- vscode: "VS Code",
1246
- jetbrains: "JetBrains",
1247
- cursor: "Cursor",
1248
- windsurf: "Windsurf",
1249
- antigravity: "Antigravity",
1250
- zed: "Zed"
1251
- }[ideKey] ?? ideKey;
1252
- }
1253
- /**
1254
- * Shows the first-run preferences prompt if .baton/preferences.yaml doesn't exist.
1255
- *
1256
- * Asks the user whether to use global config or customize AI tools and IDEs
1257
- * for this project, then writes the preferences file.
1258
- *
1259
- * @param projectRoot - Absolute path to the project root
1260
- * @param nonInteractive - If true, writes useGlobal: true silently
1261
- * @returns true if preferences were written, false if already existed
1262
- */
1263
- async function promptFirstRunPreferences(projectRoot, nonInteractive) {
1264
- if (await readProjectPreferences(projectRoot)) return false;
1265
- if (nonInteractive) {
1266
- await writeProjectPreferences(projectRoot, {
1267
- version: "1.0",
1268
- ai: {
1269
- useGlobal: true,
1270
- tools: []
1271
- },
1272
- ide: {
1273
- useGlobal: true,
1274
- platforms: []
1275
- }
1276
- });
1277
- return true;
1278
- }
1279
- const aiMode = await Je({
1280
- message: "How do you want to configure AI tools for this project?",
1281
- options: [{
1282
- value: "global",
1283
- label: "Use global config",
1284
- hint: "recommended"
1285
- }, {
1286
- value: "customize",
1287
- label: "Customize for this project"
1288
- }]
1289
- });
1290
- if (Ct(aiMode)) return false;
1291
- let aiUseGlobal = true;
1292
- let aiTools = [];
1293
- if (aiMode === "customize") {
1294
- const globalTools = await getGlobalAiTools();
1295
- const allAdapters = getAllAIToolAdapters();
1296
- const selected = await je({
1297
- message: "Select AI tools for this project:",
1298
- options: allAdapters.map((adapter) => ({
1299
- value: adapter.key,
1300
- label: globalTools.includes(adapter.key) ? `${adapter.name} (in global config)` : adapter.name
1301
- })),
1302
- initialValues: globalTools
1303
- });
1304
- if (Ct(selected)) return false;
1305
- aiUseGlobal = false;
1306
- aiTools = selected;
1307
- }
1308
- const ideMode = await Je({
1309
- message: "How do you want to configure IDE platforms for this project?",
1310
- options: [{
1311
- value: "global",
1312
- label: "Use global config",
1313
- hint: "recommended"
1314
- }, {
1315
- value: "customize",
1316
- label: "Customize for this project"
1317
- }]
1318
- });
1319
- if (Ct(ideMode)) return false;
1320
- let ideUseGlobal = true;
1321
- let idePlatforms = [];
1322
- if (ideMode === "customize") {
1323
- const globalPlatforms = await getGlobalIdePlatforms();
2103
+ async run({ args }) {
2104
+ We("Baton - IDE Platform Scanner");
2105
+ const spinner = bt();
2106
+ spinner.start("Scanning for IDE platforms...");
2107
+ clearIdeCache();
2108
+ const detectedIdes = await detectInstalledIdes();
1324
2109
  const allIdeKeys = getRegisteredIdePlatforms();
1325
- const selected = await je({
1326
- message: "Select IDE platforms for this project:",
1327
- options: allIdeKeys.map((ideKey) => ({
2110
+ const currentPlatforms = await getGlobalIdePlatforms();
2111
+ spinner.stop("Scan complete.");
2112
+ if (detectedIdes.length > 0) R.success(`Found ${detectedIdes.length} IDE platform${detectedIdes.length !== 1 ? "s" : ""} on your system.`);
2113
+ else R.warn("No IDE platforms detected on your system.");
2114
+ if (args.yes) {
2115
+ if (detectedIdes.length !== currentPlatforms.length || detectedIdes.some((key) => !currentPlatforms.includes(key))) {
2116
+ await setGlobalIdePlatforms(detectedIdes);
2117
+ R.success(`Saved ${detectedIdes.length} detected platform(s) to global config.`);
2118
+ } else R.info("Global config is already up to date.");
2119
+ Le("Scan finished.");
2120
+ return;
2121
+ }
2122
+ const options = allIdeKeys.map((ideKey) => {
2123
+ return {
1328
2124
  value: ideKey,
1329
- label: globalPlatforms.includes(ideKey) ? `${formatIdeName$1(ideKey)} (in global config)` : formatIdeName$1(ideKey)
1330
- })),
1331
- initialValues: globalPlatforms
2125
+ label: detectedIdes.includes(ideKey) ? `${formatIdeName$1(ideKey)} (detected)` : formatIdeName$1(ideKey)
2126
+ };
1332
2127
  });
1333
- if (Ct(selected)) return false;
1334
- ideUseGlobal = false;
1335
- idePlatforms = selected;
1336
- }
1337
- await writeProjectPreferences(projectRoot, {
1338
- version: "1.0",
1339
- ai: {
1340
- useGlobal: aiUseGlobal,
1341
- tools: aiTools
1342
- },
1343
- ide: {
1344
- useGlobal: ideUseGlobal,
1345
- platforms: idePlatforms
2128
+ const selected = await je({
2129
+ message: "Select which IDE platforms to save:",
2130
+ options,
2131
+ initialValues: detectedIdes
2132
+ });
2133
+ if (Ct(selected)) {
2134
+ Le("Scan finished (not saved).");
2135
+ return;
1346
2136
  }
1347
- });
1348
- return true;
1349
- }
2137
+ const selectedKeys = selected;
2138
+ if (selectedKeys.length !== currentPlatforms.length || selectedKeys.some((key) => !currentPlatforms.includes(key))) {
2139
+ await setGlobalIdePlatforms(selectedKeys);
2140
+ R.success(`Saved ${selectedKeys.length} platform(s) to global config.`);
2141
+ } else R.info("Global config is already up to date.");
2142
+ Le("Scan finished.");
2143
+ }
2144
+ });
2145
+
2146
+ //#endregion
2147
+ //#region src/commands/ides/index.ts
2148
+ const idesCommand = defineCommand({
2149
+ meta: {
2150
+ name: "ides",
2151
+ description: "Manage IDE platform detection and configuration"
2152
+ },
2153
+ subCommands: {
2154
+ configure: idesConfigureCommand,
2155
+ list: idesListCommand,
2156
+ scan: idesScanCommand
2157
+ }
2158
+ });
1350
2159
 
1351
2160
  //#endregion
1352
2161
  //#region src/utils/profile-selection.ts
2162
+ var import_dist = require_dist();
1353
2163
  /**
1354
2164
  * Discovers and prompts user to select a profile from a source.
1355
2165
  * Used by `baton init --profile` and `baton manage` (add profile).
@@ -1643,11 +2453,11 @@ const initCommand = defineCommand({
1643
2453
  await promptFirstRunPreferences(cwd, !isInteractive);
1644
2454
  if (profileSources.length > 0) {
1645
2455
  const shouldSync = isInteractive ? await Re({
1646
- message: "Sync profiles now?",
2456
+ message: "Fetch profiles and sync now?",
1647
2457
  initialValue: true
1648
2458
  }) : true;
1649
2459
  if (!Ct(shouldSync) && shouldSync) await runBatonSync(cwd);
1650
- else R.info("Run 'baton sync' later to apply your profiles.");
2460
+ else R.info("Run 'baton sync' later to fetch and apply your profiles.");
1651
2461
  }
1652
2462
  Le("Baton initialized successfully!");
1653
2463
  }
@@ -2232,9 +3042,9 @@ const profileCommand = defineCommand({
2232
3042
  description: "Manage profiles (create, list, remove)"
2233
3043
  },
2234
3044
  subCommands: {
2235
- create: () => import("./create-DEZA3dPb.mjs").then((m) => m.createCommand),
2236
- list: () => import("./list-BT2zFAVc.mjs").then((m) => m.profileListCommand),
2237
- remove: () => import("./remove-BBFBYUPy.mjs").then((m) => m.profileRemoveCommand)
3045
+ create: () => import("./create-Cx1nCS3X.mjs").then((m) => m.createCommand),
3046
+ list: () => import("./list-BsAASsXi.mjs").then((m) => m.profileListCommand),
3047
+ remove: () => import("./remove-36qv7yQ3.mjs").then((m) => m.profileRemoveCommand)
2238
3048
  }
2239
3049
  });
2240
3050
 
@@ -2654,122 +3464,10 @@ const sourceCommand = defineCommand({
2654
3464
 
2655
3465
  //#endregion
2656
3466
  //#region src/commands/sync.ts
2657
- const validCategories = [
2658
- "ai",
2659
- "files",
2660
- "ide"
2661
- ];
2662
- /** Get or initialize placed files for a profile, avoiding unsafe `as` casts on Map.get(). */
2663
- function getOrCreatePlacedFiles(map, profileName) {
2664
- let files = map.get(profileName);
2665
- if (!files) {
2666
- files = {};
2667
- map.set(profileName, files);
2668
- }
2669
- return files;
2670
- }
2671
- /**
2672
- * Recursively copy all files from sourceDir to targetDir.
2673
- * Returns the number of files written (skips identical content).
2674
- */
2675
- async function copyDirectoryRecursive(sourceDir, targetDir) {
2676
- await mkdir(targetDir, { recursive: true });
2677
- const entries = await readdir(sourceDir, { withFileTypes: true });
2678
- let placed = 0;
2679
- for (const entry of entries) {
2680
- const sourcePath = resolve(sourceDir, entry.name);
2681
- const targetPath = resolve(targetDir, entry.name);
2682
- if (entry.isDirectory()) placed += await copyDirectoryRecursive(sourcePath, targetPath);
2683
- else {
2684
- const content = await readFile(sourcePath, "utf-8");
2685
- if (await readFile(targetPath, "utf-8").catch(() => void 0) !== content) {
2686
- await writeFile(targetPath, content, "utf-8");
2687
- placed++;
2688
- }
2689
- }
2690
- }
2691
- return placed;
2692
- }
2693
- /**
2694
- * Handle .gitignore update based on the project manifest's gitignore setting.
2695
- *
2696
- * When gitignore is enabled (default): writes comprehensive patterns for ALL
2697
- * known AI tools and IDE platforms to ensure stable, dev-independent content.
2698
- * When disabled: removes any existing managed section.
2699
- * Always ensures .baton/ is gitignored regardless of setting.
2700
- */
2701
- async function handleGitignoreUpdate(params) {
2702
- const { projectManifest, fileMap, projectRoot, spinner } = params;
2703
- const gitignoreEnabled = projectManifest.gitignore !== false;
2704
- await ensureBatonDirGitignored(projectRoot);
2705
- if (gitignoreEnabled) {
2706
- spinner.start("Updating .gitignore...");
2707
- const updated = await updateGitignore(projectRoot, collectComprehensivePatterns({ fileTargets: [...fileMap.values()].map((f) => f.target) }));
2708
- spinner.stop(updated ? "Updated .gitignore with managed patterns" : ".gitignore already up to date");
2709
- } else {
2710
- spinner.start("Checking .gitignore...");
2711
- const removed = await removeGitignoreManagedSection(projectRoot);
2712
- spinner.stop(removed ? "Removed managed section from .gitignore" : ".gitignore unchanged");
2713
- }
2714
- }
2715
- /**
2716
- * Generate and write the baton.lock lockfile from placed files and profile metadata.
2717
- */
2718
- async function writeLockData(params) {
2719
- const { allProfiles, sourceShas, placedFiles, projectRoot, spinner } = params;
2720
- spinner.start("Updating lockfile...");
2721
- const lockPackages = {};
2722
- for (const profile of allProfiles) lockPackages[profile.name] = {
2723
- source: profile.source,
2724
- resolved: profile.source,
2725
- version: profile.manifest.version,
2726
- sha: sourceShas.get(profile.source) || "unknown",
2727
- files: placedFiles.get(profile.name) || {}
2728
- };
2729
- await writeLock(generateLock(lockPackages), resolve(projectRoot, "baton.lock"));
2730
- spinner.stop("Lockfile updated");
2731
- }
2732
- /**
2733
- * Detect and remove files that were in the previous lockfile but are no longer
2734
- * part of the current sync. Cleans up empty parent directories.
2735
- */
2736
- async function cleanupOrphanedFiles(params) {
2737
- const { previousPaths, placedFiles, projectRoot, dryRun, autoYes, spinner } = params;
2738
- if (previousPaths.size === 0) return;
2739
- const currentPaths = /* @__PURE__ */ new Set();
2740
- for (const files of placedFiles.values()) for (const filePath of Object.keys(files)) currentPaths.add(filePath);
2741
- const orphanedPaths = [...previousPaths].filter((prev) => !currentPaths.has(prev));
2742
- if (orphanedPaths.length === 0) return;
2743
- if (dryRun) {
2744
- R.warn(`Would remove ${orphanedPaths.length} orphaned file(s):`);
2745
- for (const orphanedPath of orphanedPaths) R.info(` Removed: ${orphanedPath}`);
2746
- return;
2747
- }
2748
- R.warn(`Found ${orphanedPaths.length} orphaned file(s) to remove:`);
2749
- for (const orphanedPath of orphanedPaths) R.info(` Removed: ${orphanedPath}`);
2750
- let shouldRemove = autoYes;
2751
- if (!autoYes) {
2752
- const confirmed = await Re({
2753
- message: `Remove ${orphanedPaths.length} orphaned file(s)?`,
2754
- initialValue: true
2755
- });
2756
- if (Ct(confirmed)) {
2757
- R.info("Skipped orphan removal.");
2758
- shouldRemove = false;
2759
- } else shouldRemove = confirmed;
2760
- }
2761
- if (!shouldRemove) {
2762
- R.info("Orphan removal skipped.");
2763
- return;
2764
- }
2765
- spinner.start("Removing orphaned files...");
2766
- const removedCount = await removePlacedFiles(orphanedPaths, projectRoot);
2767
- spinner.stop(`Removed ${removedCount} orphaned file(s)`);
2768
- }
2769
3467
  const syncCommand = defineCommand({
2770
3468
  meta: {
2771
3469
  name: "sync",
2772
- description: "Sync all profiles, skills, agents, and rules to installed AI tools"
3470
+ description: "Fetch latest versions, sync all configurations, and update lockfile"
2773
3471
  },
2774
3472
  args: {
2775
3473
  "dry-run": {
@@ -2792,11 +3490,6 @@ const syncCommand = defineCommand({
2792
3490
  alias: "v",
2793
3491
  description: "Show detailed output for each placed file",
2794
3492
  default: false
2795
- },
2796
- fresh: {
2797
- type: "boolean",
2798
- description: "Force an immediate source refresh (ignore cache TTL)",
2799
- default: false
2800
3493
  }
2801
3494
  },
2802
3495
  async run({ args }) {
@@ -2804,7 +3497,6 @@ const syncCommand = defineCommand({
2804
3497
  const categoryArg = args.category;
2805
3498
  const autoYes = args.yes;
2806
3499
  const verbose = args.verbose;
2807
- const fresh = args.fresh;
2808
3500
  let category;
2809
3501
  if (categoryArg) {
2810
3502
  if (!validCategories.includes(categoryArg)) {
@@ -2833,11 +3525,6 @@ const syncCommand = defineCommand({
2833
3525
  process.exit(1);
2834
3526
  }
2835
3527
  await promptFirstRunPreferences(projectRoot, !!args.yes);
2836
- let cacheTtlHours = 24;
2837
- try {
2838
- cacheTtlHours = (await loadGlobalConfig()).sync?.cacheTtlHours ?? 24;
2839
- } catch {}
2840
- const maxCacheAgeMs = fresh ? 0 : cacheTtlHours * 60 * 60 * 1e3;
2841
3528
  const previousPaths = /* @__PURE__ */ new Set();
2842
3529
  try {
2843
3530
  const previousLock = await readLock(resolve(projectRoot, "baton.lock"));
@@ -2866,12 +3553,19 @@ const syncCommand = defineCommand({
2866
3553
  } else {
2867
3554
  const url = parsed.provider === "github" || parsed.provider === "gitlab" ? parsed.url : parsed.provider === "git" ? parsed.url : "";
2868
3555
  if (!url) throw new Error(`Invalid source: ${profileSource.source}`);
3556
+ let resolvedRef;
3557
+ try {
3558
+ resolvedRef = await resolveVersion(url, "latest");
3559
+ if (verbose) R.info(`Resolved latest: ${profileSource.source} → ${resolvedRef.slice(0, 12)}`);
3560
+ } catch {
3561
+ resolvedRef = profileSource.version || "HEAD";
3562
+ if (verbose) R.warn(`Could not resolve latest for ${url}, using ${resolvedRef}`);
3563
+ }
2869
3564
  const cloned = await cloneGitSource({
2870
3565
  url,
2871
- ref: profileSource.version,
3566
+ ref: resolvedRef,
2872
3567
  subpath: "subpath" in parsed ? parsed.subpath : void 0,
2873
- useCache: true,
2874
- maxCacheAgeMs
3568
+ useCache: false
2875
3569
  });
2876
3570
  manifestPath = resolve(cloned.localPath, "baton.profile.yaml");
2877
3571
  sourceShas.set(profileSource.source, cloned.sha);
@@ -3086,10 +3780,9 @@ const syncCommand = defineCommand({
3086
3780
  } else if (parsed.provider === "github" || parsed.provider === "gitlab" || parsed.provider === "git") {
3087
3781
  const cloned = await cloneGitSource({
3088
3782
  url: parsed.provider === "git" ? parsed.url : parsed.url,
3089
- ref: profileSource.version,
3783
+ ref: sourceShas.get(profileSource.source) || profileSource.version,
3090
3784
  subpath: "subpath" in parsed ? parsed.subpath : void 0,
3091
- useCache: true,
3092
- maxCacheAgeMs
3785
+ useCache: true
3093
3786
  });
3094
3787
  for (const prof of allProfiles) if (prof.source === profileSource.source) profileLocalPaths.set(prof.name, cloned.localPath);
3095
3788
  }
@@ -3443,167 +4136,37 @@ const syncCommand = defineCommand({
3443
4136
  const updateCommand = defineCommand({
3444
4137
  meta: {
3445
4138
  name: "update",
3446
- description: "Check for and apply updates to installed profiles and packages"
4139
+ description: "(deprecated) Use 'baton sync' instead"
3447
4140
  },
3448
4141
  args: {
3449
4142
  "dry-run": {
3450
4143
  type: "boolean",
3451
- description: "Show available updates without applying them",
4144
+ description: "Show what would be done without writing files",
3452
4145
  default: false
3453
4146
  },
4147
+ category: {
4148
+ type: "string",
4149
+ description: "Sync only a specific category: ai, files, or ide",
4150
+ required: false
4151
+ },
3454
4152
  yes: {
3455
4153
  type: "boolean",
3456
- description: "Apply all updates without confirmation prompts",
4154
+ description: "Run non-interactively (no prompts)",
4155
+ default: false
4156
+ },
4157
+ verbose: {
4158
+ type: "boolean",
4159
+ alias: "v",
4160
+ description: "Show detailed output for each placed file",
3457
4161
  default: false
3458
4162
  }
3459
4163
  },
3460
- async run({ args }) {
3461
- const dryRun = args["dry-run"];
3462
- const autoConfirm = args.yes;
3463
- We("Baton Update");
3464
- const cwd = process.cwd();
3465
- const manifestPath = resolve(cwd, "baton.yaml");
3466
- const lockfilePath = resolve(cwd, "baton.lock");
3467
- const spinner = bt();
3468
- spinner.start("Loading project configuration");
3469
- let manifest;
3470
- try {
3471
- manifest = await loadProjectManifest(manifestPath);
3472
- } catch (error) {
3473
- spinner.stop("Failed to load baton.yaml");
3474
- Ne(error instanceof Error ? error.message : "Could not load project manifest");
3475
- process.exit(1);
3476
- }
3477
- let lockfile = null;
3478
- try {
3479
- lockfile = await loadLockfile(lockfilePath);
3480
- spinner.stop("Configuration loaded");
3481
- } catch {
3482
- spinner.stop("Configuration loaded (no lockfile found)");
3483
- Ve("No lockfile found. Run 'baton sync' first to create one.");
3484
- }
3485
- spinner.start("Checking for updates");
3486
- const updateCandidates = [];
3487
- for (const profile of manifest.profiles || []) try {
3488
- const parsed = parseSource(profile.source);
3489
- if (parsed.provider === "local" || parsed.provider === "file") continue;
3490
- const packageName = getPackageName(parsed);
3491
- const currentVersion = lockfile?.packages[packageName]?.version || profile.version || "HEAD";
3492
- const latestVersion = await getLatestVersion(parsed);
3493
- if (currentVersion !== latestVersion) {
3494
- const changes = await getChangeSummary(parsed, currentVersion, latestVersion);
3495
- updateCandidates.push({
3496
- name: packageName,
3497
- source: profile.source,
3498
- currentVersion,
3499
- latestVersion,
3500
- changes
3501
- });
3502
- }
3503
- } catch (error) {
3504
- if (error instanceof SourceParseError) R.warn(`Skipping invalid source: ${profile.source}`);
3505
- }
3506
- spinner.stop("Update check complete");
3507
- if (updateCandidates.length === 0) {
3508
- Le("All packages are up to date!");
3509
- process.exit(0);
3510
- }
3511
- Ve(`Found ${updateCandidates.length} update${updateCandidates.length > 1 ? "s" : ""}`);
3512
- for (const candidate of updateCandidates) {
3513
- console.log(`\n📦 ${candidate.name}: ${candidate.currentVersion} → ${candidate.latestVersion}`);
3514
- if (candidate.changes.length > 0) {
3515
- console.log(" Changes:");
3516
- for (const change of candidate.changes) console.log(` - ${change}`);
3517
- }
3518
- }
3519
- if (dryRun) {
3520
- Le("Dry-run mode enabled. No changes were made.\nRun 'baton update' without --dry-run to apply updates.");
3521
- process.exit(0);
3522
- }
3523
- if (!autoConfirm) {
3524
- const confirmed = await Re({
3525
- message: `Apply ${updateCandidates.length} update${updateCandidates.length > 1 ? "s" : ""}?`,
3526
- initialValue: true
3527
- });
3528
- if (Ct(confirmed) || !confirmed) {
3529
- Ne("Update cancelled");
3530
- process.exit(0);
3531
- }
3532
- }
3533
- spinner.start("Applying updates");
3534
- const updatedPackages = {};
3535
- for (const candidate of updateCandidates) try {
3536
- const parsed = parseSource(candidate.source);
3537
- const url = parsed.provider === "github" || parsed.provider === "gitlab" ? parsed.url : parsed.provider === "git" ? parsed.url : "";
3538
- if (!url) {
3539
- spinner.stop("Update failed");
3540
- Ne(`Cannot update local source: ${candidate.name}`);
3541
- process.exit(1);
3542
- }
3543
- const clonedSource = await cloneGitSource({
3544
- url,
3545
- ref: candidate.latestVersion,
3546
- subpath: parsed.provider !== "local" && "subpath" in parsed ? parsed.subpath : void 0
3547
- });
3548
- const files = {};
3549
- updatedPackages[candidate.name] = {
3550
- source: candidate.source,
3551
- resolved: url,
3552
- version: candidate.latestVersion,
3553
- sha: clonedSource.sha,
3554
- files
3555
- };
3556
- } catch (error) {
3557
- spinner.stop("Update failed");
3558
- Ne(`Failed to update ${candidate.name}: ${error instanceof Error ? error.message : "Unknown error"}`);
3559
- process.exit(1);
3560
- }
3561
- const newLock = generateLock(updatedPackages);
3562
- if (lockfile) {
3563
- lockfile.packages = {
3564
- ...lockfile.packages,
3565
- ...newLock.packages
3566
- };
3567
- lockfile.locked_at = (/* @__PURE__ */ new Date()).toISOString();
3568
- await writeLock(lockfile, lockfilePath);
3569
- } else await writeLock(newLock, lockfilePath);
3570
- spinner.stop("Updates applied successfully");
3571
- Le(`✅ Updated ${updateCandidates.length} package${updateCandidates.length > 1 ? "s" : ""}!\n\nRun 'baton sync' to apply the updated configurations.`);
3572
- process.exit(0);
4164
+ async run(context) {
4165
+ R.warn("`baton update` is deprecated. Use `baton sync` instead.");
4166
+ R.info("");
4167
+ if (syncCommand.run) await syncCommand.run(context);
3573
4168
  }
3574
4169
  });
3575
- function getPackageName(parsed) {
3576
- if (parsed.provider === "local" || parsed.provider === "file") return parsed.path;
3577
- if (parsed.provider === "github" || parsed.provider === "gitlab") return `${parsed.org}/${parsed.repo}`;
3578
- if (parsed.provider === "npm") return parsed.scope ? `${parsed.scope}/${parsed.package}` : parsed.package;
3579
- if (parsed.provider === "git") return parsed.url;
3580
- return "unknown";
3581
- }
3582
- async function getLatestVersion(parsed) {
3583
- if (parsed.provider === "local") return "local";
3584
- const url = parsed.provider === "github" || parsed.provider === "gitlab" ? parsed.url : parsed.provider === "git" ? parsed.url : "";
3585
- if (!url) return "HEAD";
3586
- return await resolveVersion(url, "latest");
3587
- }
3588
- async function getChangeSummary(parsed, fromVersion, toVersion) {
3589
- try {
3590
- const url = parsed.provider === "github" || parsed.provider === "gitlab" ? parsed.url : parsed.provider === "git" ? parsed.url : "";
3591
- if (!url) return [`Updated from ${fromVersion} to ${toVersion}`];
3592
- const clonedSource = await cloneGitSource({
3593
- url,
3594
- ref: toVersion,
3595
- subpath: parsed.provider !== "local" && "subpath" in parsed ? parsed.subpath : void 0
3596
- });
3597
- const simpleGit = (await import("./esm-CuRZ1S4C.mjs")).default;
3598
- return (await simpleGit(clonedSource.localPath).log({
3599
- from: fromVersion,
3600
- to: toVersion,
3601
- maxCount: 5
3602
- })).all.map((commit) => commit.message.split("\n")[0]);
3603
- } catch {
3604
- return [`Updated from ${fromVersion} to ${toVersion}`];
3605
- }
3606
- }
3607
4170
 
3608
4171
  //#endregion
3609
4172
  //#region src/index.ts
@@ -3640,6 +4203,7 @@ runMain(defineCommand({
3640
4203
  },
3641
4204
  subCommands: {
3642
4205
  init: initCommand,
4206
+ apply: applyCommand,
3643
4207
  sync: syncCommand,
3644
4208
  update: updateCommand,
3645
4209
  diff: diffCommand,
@@ -3661,8 +4225,9 @@ runMain(defineCommand({
3661
4225
  console.log("");
3662
4226
  console.log("Available commands:");
3663
4227
  console.log(" init Initialize Baton in your project");
3664
- console.log(" sync Sync all configurations to installed AI tools");
3665
- console.log(" update Check for and apply updates to installed packages");
4228
+ console.log(" apply Apply locked configurations (deterministic, reproducible)");
4229
+ console.log(" sync Fetch latest versions, sync, and update lockfile");
4230
+ console.log(" update (deprecated) Use 'baton sync' instead");
3666
4231
  console.log(" diff Compare local files with remote source versions");
3667
4232
  console.log(" manage Interactive project management wizard");
3668
4233
  console.log(" config Show dashboard overview or configure settings");