@baton-dx/cli 0.4.4 → 0.6.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,12 +1,11 @@
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-D9yccWot.mjs";
3
+ import { $ as isKnownIdePlatform, A as isLockedProfile, B as generateLock, C as mergeMemoryWithWarnings, D as mergeSkills, E as mergeRulesWithWarnings, F as detectLegacyPaths, G as esm_default, H as writeLock, I as placeFile, J as removeGitignoreManagedSection, K as collectComprehensivePatterns, L as discoverProfilesInSourceRepo, M as mergeContentParts, N as resolveProfileSupport, O as mergeSkillsWithWarnings, P as resolveProfileChain, Q as idePlatformRegistry, R as findSourceManifest, S as mergeMemory, T as mergeRules, U as resolveVersion, V as readLock, W as cloneGitSource, X as getIdePlatformTargetDir, Y as updateGitignore, Z as getRegisteredIdePlatforms, _ as removeGlobalSource, a as resolvePreferences, at as loadProjectManifest, b as setGlobalIdePlatforms, c as computeIntersection, ct as SourceParseError, d as addGlobalSource, dt as getAllAIToolKeys, et as getAIToolAdaptersForKeys, f as getDefaultGlobalSource, g as loadGlobalConfig, h as getGlobalSources, i as formatInstallCommand, it as loadProfileManifest, j as sortProfilesByWeight, k as getProfileWeight, l as clearIdeCache, lt as getAIToolConfig, m as getGlobalIdePlatforms, n as isUpdateAvailable, nt as parseFrontmatter, o as readProjectPreferences, ot as KEBAB_CASE_REGEX, p as getGlobalAiTools, q as ensureBatonDirGitignored, r as detectInstallMethod, rt as parseSource, s as writeProjectPreferences, st as FileNotFoundError, t as checkLatestVersion, tt as getAllAIToolAdapters, u as detectInstalledIdes, ut as getAIToolPath, v as saveGlobalConfig, w as mergeAgentsWithWarnings, x as require_lib, y as setGlobalAiTools, z as removePlacedFiles } from "./src-BVo3M5-4.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";
8
+ import { execFile } from "node:child_process";
10
9
 
11
10
  //#region src/commands/ai-tools/configure.ts
12
11
  const aiToolsConfigureCommand = defineCommand({
@@ -377,6 +376,120 @@ async function buildIntersection(sourceString, developerTools, cwd) {
377
376
  return computeIntersection(developerTools, resolveProfileSupport(profileManifest, sourceManifest));
378
377
  }
379
378
 
379
+ //#endregion
380
+ //#region src/utils/first-run-preferences.ts
381
+ /**
382
+ * Format an IDE platform key into a display name.
383
+ * Duplicated here to avoid circular dependency with ides/utils.
384
+ */
385
+ function formatIdeName$2(ideKey) {
386
+ return {
387
+ vscode: "VS Code",
388
+ jetbrains: "JetBrains",
389
+ cursor: "Cursor",
390
+ windsurf: "Windsurf",
391
+ antigravity: "Antigravity",
392
+ zed: "Zed"
393
+ }[ideKey] ?? ideKey;
394
+ }
395
+ /**
396
+ * Shows the first-run preferences prompt if .baton/preferences.yaml doesn't exist.
397
+ *
398
+ * Asks the user whether to use global config or customize AI tools and IDEs
399
+ * for this project, then writes the preferences file.
400
+ *
401
+ * @param projectRoot - Absolute path to the project root
402
+ * @param nonInteractive - If true, writes useGlobal: true silently
403
+ * @returns true if preferences were written, false if already existed
404
+ */
405
+ async function promptFirstRunPreferences(projectRoot, nonInteractive) {
406
+ if (await readProjectPreferences(projectRoot)) return false;
407
+ if (nonInteractive) {
408
+ await writeProjectPreferences(projectRoot, {
409
+ version: "1.0",
410
+ ai: {
411
+ useGlobal: true,
412
+ tools: []
413
+ },
414
+ ide: {
415
+ useGlobal: true,
416
+ platforms: []
417
+ }
418
+ });
419
+ return true;
420
+ }
421
+ const aiMode = await Je({
422
+ message: "How do you want to configure AI tools for this project?",
423
+ options: [{
424
+ value: "global",
425
+ label: "Use global config",
426
+ hint: "recommended"
427
+ }, {
428
+ value: "customize",
429
+ label: "Customize for this project"
430
+ }]
431
+ });
432
+ if (Ct(aiMode)) return false;
433
+ let aiUseGlobal = true;
434
+ let aiTools = [];
435
+ if (aiMode === "customize") {
436
+ const globalTools = await getGlobalAiTools();
437
+ const allAdapters = getAllAIToolAdapters();
438
+ const selected = await je({
439
+ message: "Select AI tools for this project:",
440
+ options: allAdapters.map((adapter) => ({
441
+ value: adapter.key,
442
+ label: globalTools.includes(adapter.key) ? `${adapter.name} (in global config)` : adapter.name
443
+ })),
444
+ initialValues: globalTools
445
+ });
446
+ if (Ct(selected)) return false;
447
+ aiUseGlobal = false;
448
+ aiTools = selected;
449
+ }
450
+ const ideMode = await Je({
451
+ message: "How do you want to configure IDE platforms for this project?",
452
+ options: [{
453
+ value: "global",
454
+ label: "Use global config",
455
+ hint: "recommended"
456
+ }, {
457
+ value: "customize",
458
+ label: "Customize for this project"
459
+ }]
460
+ });
461
+ if (Ct(ideMode)) return false;
462
+ let ideUseGlobal = true;
463
+ let idePlatforms = [];
464
+ if (ideMode === "customize") {
465
+ const globalPlatforms = await getGlobalIdePlatforms();
466
+ const allIdeKeys = getRegisteredIdePlatforms();
467
+ const selected = await je({
468
+ message: "Select IDE platforms for this project:",
469
+ options: allIdeKeys.map((ideKey) => ({
470
+ value: ideKey,
471
+ label: globalPlatforms.includes(ideKey) ? `${formatIdeName$2(ideKey)} (in global config)` : formatIdeName$2(ideKey)
472
+ })),
473
+ initialValues: globalPlatforms
474
+ });
475
+ if (Ct(selected)) return false;
476
+ ideUseGlobal = false;
477
+ idePlatforms = selected;
478
+ }
479
+ await writeProjectPreferences(projectRoot, {
480
+ version: "1.0",
481
+ ai: {
482
+ useGlobal: aiUseGlobal,
483
+ tools: aiTools
484
+ },
485
+ ide: {
486
+ useGlobal: ideUseGlobal,
487
+ platforms: idePlatforms
488
+ }
489
+ });
490
+ return true;
491
+ }
492
+
380
493
  //#endregion
381
494
  //#region src/utils/intersection-display.ts
382
495
  /**
@@ -392,30 +505,842 @@ function displayIntersection(intersection) {
392
505
  R.info("No tool or IDE intersection data available.");
393
506
  return;
394
507
  }
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
- }
508
+ if (hasAiData) displayDimension("AI Tools", intersection.aiTools);
509
+ if (hasIdeData) displayDimension("IDE Platforms", intersection.idePlatforms);
510
+ }
511
+ /**
512
+ * Display a single dimension (AI tools or IDE platforms) of the intersection.
513
+ */
514
+ function displayDimension(label, dimension) {
515
+ const lines = [];
516
+ if (dimension.synced.length > 0) for (const item of dimension.synced) lines.push(` \u2713 ${item}`);
517
+ if (dimension.unavailable.length > 0) for (const item of dimension.unavailable) lines.push(` - ${item} (not installed)`);
518
+ if (dimension.unsupported.length > 0) for (const item of dimension.unsupported) lines.push(` ~ ${item} (not supported by profile)`);
519
+ if (lines.length > 0) Ve(lines.join("\n"), label);
520
+ }
521
+ /**
522
+ * Format a compact intersection summary for inline display.
523
+ * Example: "claude-code, cursor (AI) + vscode (IDE)"
524
+ */
525
+ function formatIntersectionSummary(intersection) {
526
+ const parts = [];
527
+ if (intersection.aiTools.synced.length > 0) parts.push(`${intersection.aiTools.synced.join(", ")} (AI)`);
528
+ if (intersection.idePlatforms.synced.length > 0) parts.push(`${intersection.idePlatforms.synced.join(", ")} (IDE)`);
529
+ if (parts.length === 0) return "No matching tools";
530
+ return parts.join(" + ");
531
+ }
532
+
533
+ //#endregion
534
+ //#region src/commands/sync-pipeline.ts
535
+ const validCategories = [
536
+ "ai",
537
+ "files",
538
+ "ide"
539
+ ];
540
+ /** Get or initialize placed files for a profile, avoiding unsafe `as` casts on Map.get(). */
541
+ function getOrCreatePlacedFiles(map, profileName) {
542
+ let files = map.get(profileName);
543
+ if (!files) {
544
+ files = {};
545
+ map.set(profileName, files);
546
+ }
547
+ return files;
548
+ }
549
+ /**
550
+ * Recursively copy all files from sourceDir to targetDir.
551
+ * Returns the number of files written (skips identical content).
552
+ */
553
+ async function copyDirectoryRecursive(sourceDir, targetDir) {
554
+ await mkdir(targetDir, { recursive: true });
555
+ const entries = await readdir(sourceDir, { withFileTypes: true });
556
+ let placed = 0;
557
+ for (const entry of entries) {
558
+ const sourcePath = resolve(sourceDir, entry.name);
559
+ const targetPath = resolve(targetDir, entry.name);
560
+ if (entry.isDirectory()) placed += await copyDirectoryRecursive(sourcePath, targetPath);
561
+ else {
562
+ const content = await readFile(sourcePath, "utf-8");
563
+ if (await readFile(targetPath, "utf-8").catch(() => void 0) !== content) {
564
+ await writeFile(targetPath, content, "utf-8");
565
+ placed++;
566
+ }
567
+ }
568
+ }
569
+ return placed;
570
+ }
571
+ /**
572
+ * Handle .gitignore update based on the project manifest's gitignore setting.
573
+ *
574
+ * When gitignore is enabled (default): writes comprehensive patterns for ALL
575
+ * known AI tools and IDE platforms to ensure stable, dev-independent content.
576
+ * When disabled: removes any existing managed section.
577
+ * Always ensures .baton/ is gitignored regardless of setting.
578
+ */
579
+ async function handleGitignoreUpdate(params) {
580
+ const { projectManifest, fileMap, projectRoot, spinner } = params;
581
+ const gitignoreEnabled = projectManifest.gitignore !== false;
582
+ await ensureBatonDirGitignored(projectRoot);
583
+ if (gitignoreEnabled) {
584
+ spinner.start("Updating .gitignore...");
585
+ const updated = await updateGitignore(projectRoot, collectComprehensivePatterns({ fileTargets: [...fileMap.values()].map((f) => f.target) }));
586
+ spinner.stop(updated ? "Updated .gitignore with managed patterns" : ".gitignore already up to date");
587
+ } else {
588
+ spinner.start("Checking .gitignore...");
589
+ const removed = await removeGitignoreManagedSection(projectRoot);
590
+ spinner.stop(removed ? "Removed managed section from .gitignore" : ".gitignore unchanged");
591
+ }
592
+ }
593
+ /**
594
+ * Generate and write the baton.lock lockfile from placed files and profile metadata.
595
+ */
596
+ async function writeLockData(params) {
597
+ const { allProfiles, sourceShas, placedFiles, projectRoot, spinner } = params;
598
+ spinner.start("Updating lockfile...");
599
+ const lockPackages = {};
600
+ for (const profile of allProfiles) lockPackages[profile.name] = {
601
+ source: profile.source,
602
+ resolved: profile.source,
603
+ version: profile.manifest.version,
604
+ sha: sourceShas.get(profile.source) || "unknown",
605
+ files: placedFiles.get(profile.name) || {}
606
+ };
607
+ await writeLock(generateLock(lockPackages), resolve(projectRoot, "baton.lock"));
608
+ spinner.stop("Lockfile updated");
609
+ }
610
+ /**
611
+ * Detect and remove files that were in the previous lockfile but are no longer
612
+ * part of the current sync. Cleans up empty parent directories.
613
+ */
614
+ async function cleanupOrphanedFiles(params) {
615
+ const { previousPaths, placedFiles, projectRoot, dryRun, autoYes, spinner } = params;
616
+ if (previousPaths.size === 0) return;
617
+ const currentPaths = /* @__PURE__ */ new Set();
618
+ for (const files of placedFiles.values()) for (const filePath of Object.keys(files)) currentPaths.add(filePath);
619
+ const orphanedPaths = [...previousPaths].filter((prev) => !currentPaths.has(prev));
620
+ if (orphanedPaths.length === 0) return;
621
+ if (dryRun) {
622
+ R.warn(`Would remove ${orphanedPaths.length} orphaned file(s):`);
623
+ for (const orphanedPath of orphanedPaths) R.info(` Removed: ${orphanedPath}`);
624
+ return;
625
+ }
626
+ R.warn(`Found ${orphanedPaths.length} orphaned file(s) to remove:`);
627
+ for (const orphanedPath of orphanedPaths) R.info(` Removed: ${orphanedPath}`);
628
+ let shouldRemove = autoYes;
629
+ if (!autoYes) {
630
+ const confirmed = await Re({
631
+ message: `Remove ${orphanedPaths.length} orphaned file(s)?`,
632
+ initialValue: true
633
+ });
634
+ if (Ct(confirmed)) {
635
+ R.info("Skipped orphan removal.");
636
+ shouldRemove = false;
637
+ } else shouldRemove = confirmed;
638
+ }
639
+ if (!shouldRemove) {
640
+ R.info("Orphan removal skipped.");
641
+ return;
642
+ }
643
+ spinner.start("Removing orphaned files...");
644
+ const removedCount = await removePlacedFiles(orphanedPaths, projectRoot);
645
+ spinner.stop(`Removed ${removedCount} orphaned file(s)`);
646
+ }
647
+
648
+ //#endregion
649
+ //#region src/commands/apply.ts
650
+ /** Extract the package name from a source string for lockfile lookup. */
651
+ function getPackageNameFromSource(source, parsed) {
652
+ if (parsed.provider === "github" || parsed.provider === "gitlab") return `${parsed.org}/${parsed.repo}`;
653
+ if (parsed.provider === "npm") return parsed.scope ? `${parsed.scope}/${parsed.package}` : parsed.package;
654
+ if (parsed.provider === "git") return parsed.url;
655
+ return source;
656
+ }
657
+ const applyCommand = defineCommand({
658
+ meta: {
659
+ name: "apply",
660
+ description: "Apply locked configurations to the project (deterministic, reproducible)"
661
+ },
662
+ args: {
663
+ "dry-run": {
664
+ type: "boolean",
665
+ description: "Show what would be done without writing files",
666
+ default: false
667
+ },
668
+ category: {
669
+ type: "string",
670
+ description: "Apply only a specific category: ai, files, or ide",
671
+ required: false
672
+ },
673
+ yes: {
674
+ type: "boolean",
675
+ description: "Run non-interactively (no prompts)",
676
+ default: false
677
+ },
678
+ verbose: {
679
+ type: "boolean",
680
+ alias: "v",
681
+ description: "Show detailed output for each placed file",
682
+ default: false
683
+ },
684
+ fresh: {
685
+ type: "boolean",
686
+ description: "Force cache bypass (re-clone even if cached)",
687
+ default: false
688
+ }
689
+ },
690
+ async run({ args }) {
691
+ const dryRun = args["dry-run"];
692
+ const categoryArg = args.category;
693
+ const autoYes = args.yes;
694
+ const verbose = args.verbose;
695
+ const fresh = args.fresh;
696
+ let category;
697
+ if (categoryArg) {
698
+ if (!validCategories.includes(categoryArg)) {
699
+ Ne(`Invalid category "${categoryArg}". Valid categories: ${validCategories.join(", ")}`);
700
+ process.exit(1);
701
+ }
702
+ category = categoryArg;
703
+ }
704
+ const syncAi = !category || category === "ai";
705
+ const syncFiles = !category || category === "files";
706
+ const syncIde = !category || category === "ide";
707
+ We(category ? `📦 Baton Apply (category: ${category})` : "📦 Baton Apply");
708
+ const stats = {
709
+ created: 0,
710
+ errors: 0
711
+ };
712
+ try {
713
+ const projectRoot = process.cwd();
714
+ const manifestPath = resolve(projectRoot, "baton.yaml");
715
+ let projectManifest;
716
+ try {
717
+ projectManifest = await loadProjectManifest(manifestPath);
718
+ } catch (error) {
719
+ if (error instanceof FileNotFoundError) Ne("baton.yaml not found. Run `baton init` first.");
720
+ else Ne(`Failed to load baton.yaml: ${error instanceof Error ? error.message : String(error)}`);
721
+ process.exit(1);
722
+ }
723
+ await promptFirstRunPreferences(projectRoot, !!args.yes);
724
+ const lockfilePath = resolve(projectRoot, "baton.lock");
725
+ let lockfile = null;
726
+ try {
727
+ lockfile = await readLock(lockfilePath);
728
+ } catch {
729
+ if (verbose) R.warn("No lockfile found. Falling back to manifest versions.");
730
+ }
731
+ const maxCacheAgeMs = fresh ? 0 : void 0;
732
+ const previousPaths = /* @__PURE__ */ new Set();
733
+ if (lockfile) for (const pkg of Object.values(lockfile.packages)) for (const filePath of Object.keys(pkg.integrity)) previousPaths.add(filePath);
734
+ const spinner = bt();
735
+ spinner.start("Resolving profile chain...");
736
+ const allProfiles = [];
737
+ const sourceShas = /* @__PURE__ */ new Map();
738
+ for (const profileSource of projectManifest.profiles || []) try {
739
+ if (verbose) R.info(`Resolving source: ${profileSource.source}`);
740
+ const parsed = parseSource(profileSource.source);
741
+ let manifestPath;
742
+ let cloneContext;
743
+ if (parsed.provider === "local" || parsed.provider === "file") {
744
+ const absolutePath = parsed.path.startsWith("/") ? parsed.path : resolve(projectRoot, parsed.path);
745
+ manifestPath = resolve(absolutePath, "baton.profile.yaml");
746
+ try {
747
+ const git = esm_default(absolutePath);
748
+ await git.checkIsRepo();
749
+ const sha = await git.revparse(["HEAD"]);
750
+ sourceShas.set(profileSource.source, sha.trim());
751
+ } catch {
752
+ sourceShas.set(profileSource.source, "local");
753
+ }
754
+ } else {
755
+ const url = parsed.provider === "github" || parsed.provider === "gitlab" ? parsed.url : parsed.provider === "git" ? parsed.url : "";
756
+ if (!url) throw new Error(`Invalid source: ${profileSource.source}`);
757
+ let ref = profileSource.version;
758
+ if (lockfile) {
759
+ const packageName = getPackageNameFromSource(profileSource.source, parsed);
760
+ const lockedPkg = lockfile.packages[packageName];
761
+ if (lockedPkg?.sha && lockedPkg.sha !== "unknown") {
762
+ ref = lockedPkg.sha;
763
+ if (verbose) R.info(`Using locked SHA for ${profileSource.source}: ${ref.slice(0, 12)}`);
764
+ }
765
+ }
766
+ const cloned = await cloneGitSource({
767
+ url,
768
+ ref,
769
+ subpath: "subpath" in parsed ? parsed.subpath : void 0,
770
+ useCache: true,
771
+ maxCacheAgeMs
772
+ });
773
+ manifestPath = resolve(cloned.localPath, "baton.profile.yaml");
774
+ sourceShas.set(profileSource.source, cloned.sha);
775
+ cloneContext = {
776
+ cachePath: cloned.cachePath,
777
+ sparseCheckout: cloned.sparseCheckout
778
+ };
779
+ }
780
+ const manifest = await loadProfileManifest(manifestPath);
781
+ const profileDir = dirname(manifestPath);
782
+ const chain = await resolveProfileChain(manifest, profileSource.source, profileDir, cloneContext);
783
+ allProfiles.push(...chain);
784
+ } catch (error) {
785
+ spinner.stop(`Failed to resolve profile ${profileSource.source}: ${error}`);
786
+ stats.errors++;
787
+ }
788
+ if (allProfiles.length === 0) {
789
+ spinner.stop("No profiles configured");
790
+ Le("Nothing to apply. Run `baton manage` to add a profile.");
791
+ process.exit(2);
792
+ }
793
+ spinner.stop(`Resolved ${allProfiles.length} profile(s)`);
794
+ const weightSortedProfiles = sortProfilesByWeight(allProfiles);
795
+ spinner.start("Merging configurations...");
796
+ const allWeightWarnings = [];
797
+ const skillsResult = mergeSkillsWithWarnings(weightSortedProfiles);
798
+ const mergedSkills = skillsResult.skills;
799
+ allWeightWarnings.push(...skillsResult.warnings);
800
+ const rulesResult = mergeRulesWithWarnings(weightSortedProfiles);
801
+ const mergedRules = rulesResult.rules;
802
+ allWeightWarnings.push(...rulesResult.warnings);
803
+ const agentsResult = mergeAgentsWithWarnings(weightSortedProfiles);
804
+ const mergedAgents = agentsResult.agents;
805
+ allWeightWarnings.push(...agentsResult.warnings);
806
+ const memoryResult = mergeMemoryWithWarnings(weightSortedProfiles);
807
+ const mergedMemory = memoryResult.entries;
808
+ allWeightWarnings.push(...memoryResult.warnings);
809
+ const commandMap = /* @__PURE__ */ new Map();
810
+ const lockedCommands = /* @__PURE__ */ new Set();
811
+ const commandOwner = /* @__PURE__ */ new Map();
812
+ for (const profile of weightSortedProfiles) {
813
+ const weight = getProfileWeight(profile);
814
+ const locked = isLockedProfile(profile);
815
+ for (const cmd of profile.manifest.ai?.commands || []) {
816
+ if (lockedCommands.has(cmd)) continue;
817
+ const existing = commandOwner.get(cmd);
818
+ if (existing && existing.weight === weight && existing.profileName !== profile.name) allWeightWarnings.push({
819
+ key: cmd,
820
+ category: "command",
821
+ profileA: existing.profileName,
822
+ profileB: profile.name,
823
+ weight
824
+ });
825
+ commandMap.set(cmd, profile.name);
826
+ commandOwner.set(cmd, {
827
+ profileName: profile.name,
828
+ weight
829
+ });
830
+ if (locked) lockedCommands.add(cmd);
831
+ }
832
+ }
833
+ const mergedCommandCount = commandMap.size;
834
+ const fileMap = /* @__PURE__ */ new Map();
835
+ const lockedFiles = /* @__PURE__ */ new Set();
836
+ const fileOwner = /* @__PURE__ */ new Map();
837
+ for (const profile of weightSortedProfiles) {
838
+ const weight = getProfileWeight(profile);
839
+ const locked = isLockedProfile(profile);
840
+ for (const fileConfig of profile.manifest.files || []) {
841
+ const target = fileConfig.target || fileConfig.source;
842
+ if (lockedFiles.has(target)) continue;
843
+ const existing = fileOwner.get(target);
844
+ if (existing && existing.weight === weight && existing.profileName !== profile.name) allWeightWarnings.push({
845
+ key: target,
846
+ category: "file",
847
+ profileA: existing.profileName,
848
+ profileB: profile.name,
849
+ weight
850
+ });
851
+ fileMap.set(target, {
852
+ source: fileConfig.source,
853
+ target,
854
+ profileName: profile.name
855
+ });
856
+ fileOwner.set(target, {
857
+ profileName: profile.name,
858
+ weight
859
+ });
860
+ if (locked) lockedFiles.add(target);
861
+ }
862
+ }
863
+ const mergedFileCount = fileMap.size;
864
+ const ideMap = /* @__PURE__ */ new Map();
865
+ const lockedIdeConfigs = /* @__PURE__ */ new Set();
866
+ const ideOwner = /* @__PURE__ */ new Map();
867
+ for (const profile of weightSortedProfiles) {
868
+ if (!profile.manifest.ide) continue;
869
+ const weight = getProfileWeight(profile);
870
+ const locked = isLockedProfile(profile);
871
+ for (const [ideKey, files] of Object.entries(profile.manifest.ide)) {
872
+ if (!files) continue;
873
+ const targetDir = getIdePlatformTargetDir(ideKey);
874
+ if (!targetDir) {
875
+ if (!isKnownIdePlatform(ideKey)) R.warn(`Unknown IDE platform "${ideKey}" in profile "${profile.name}" — skipping. Register it in the IDE platform registry.`);
876
+ continue;
877
+ }
878
+ for (const fileName of files) {
879
+ const targetPath = `${targetDir}/${fileName}`;
880
+ if (lockedIdeConfigs.has(targetPath)) continue;
881
+ const existing = ideOwner.get(targetPath);
882
+ if (existing && existing.weight === weight && existing.profileName !== profile.name) allWeightWarnings.push({
883
+ key: targetPath,
884
+ category: "ide",
885
+ profileA: existing.profileName,
886
+ profileB: profile.name,
887
+ weight
888
+ });
889
+ ideMap.set(targetPath, {
890
+ ideKey,
891
+ fileName,
892
+ targetDir,
893
+ profileName: profile.name
894
+ });
895
+ ideOwner.set(targetPath, {
896
+ profileName: profile.name,
897
+ weight
898
+ });
899
+ if (locked) lockedIdeConfigs.add(targetPath);
900
+ }
901
+ }
902
+ }
903
+ const mergedIdeCount = ideMap.size;
904
+ spinner.stop(`Merged: ${mergedSkills.length} skills, ${mergedRules.length} rules, ${mergedAgents.length} agents, ${mergedMemory.length} memory files, ${mergedCommandCount} commands, ${mergedFileCount} files, ${mergedIdeCount} IDE configs`);
905
+ 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.`);
906
+ spinner.start("Computing tool intersection...");
907
+ const prefs = await resolvePreferences(projectRoot);
908
+ const detectedAITools = await detectInstalledAITools();
909
+ if (verbose) {
910
+ R.info(`AI tools: ${prefs.ai.tools.join(", ") || "(none)"} (from ${prefs.ai.source} preferences)`);
911
+ R.info(`IDE platforms: ${prefs.ide.platforms.join(", ") || "(none)"} (from ${prefs.ide.source} preferences)`);
912
+ }
913
+ let syncedAiTools;
914
+ let syncedIdePlatforms = null;
915
+ let allIntersections = null;
916
+ if (prefs.ai.tools.length > 0) {
917
+ const developerTools = {
918
+ aiTools: prefs.ai.tools,
919
+ idePlatforms: prefs.ide.platforms
920
+ };
921
+ const aggregatedSyncedAi = /* @__PURE__ */ new Set();
922
+ const aggregatedSyncedIde = /* @__PURE__ */ new Set();
923
+ allIntersections = /* @__PURE__ */ new Map();
924
+ for (const profileSource of projectManifest.profiles || []) try {
925
+ const intersection = await buildIntersection(profileSource.source, developerTools, projectRoot);
926
+ if (intersection) {
927
+ allIntersections.set(profileSource.source, intersection);
928
+ for (const tool of intersection.aiTools.synced) aggregatedSyncedAi.add(tool);
929
+ for (const platform of intersection.idePlatforms.synced) aggregatedSyncedIde.add(platform);
930
+ }
931
+ } catch {}
932
+ syncedAiTools = aggregatedSyncedAi.size > 0 ? [...aggregatedSyncedAi] : [];
933
+ syncedIdePlatforms = [...aggregatedSyncedIde];
934
+ } else {
935
+ syncedAiTools = detectedAITools;
936
+ syncedIdePlatforms = null;
937
+ if (detectedAITools.length > 0) {
938
+ R.warn("No AI tools configured. Run `baton ai-tools scan` to configure your tools.");
939
+ R.info(`Falling back to detected tools: ${detectedAITools.join(", ")}`);
940
+ }
941
+ }
942
+ if (syncedAiTools.length === 0 && detectedAITools.length === 0) {
943
+ spinner.stop("No AI tools available");
944
+ Ne("No AI tools found. Install an AI coding tool first.");
945
+ process.exit(1);
946
+ }
947
+ if (syncedAiTools.length === 0) {
948
+ spinner.stop("No AI tools in intersection");
949
+ Ne("No AI tools match between your configuration and profile support. Run `baton ai-tools scan` or check your profile's supported tools.");
950
+ process.exit(1);
951
+ }
952
+ if (allIntersections) for (const [source, intersection] of allIntersections) if (verbose) {
953
+ R.step(`Intersection for ${source}`);
954
+ displayIntersection(intersection);
955
+ } else {
956
+ const summary = formatIntersectionSummary(intersection);
957
+ R.info(`Applying for: ${summary}`);
958
+ }
959
+ const ideSummary = syncedIdePlatforms && syncedIdePlatforms.length > 0 ? ` | IDE platforms: ${syncedIdePlatforms.join(", ")}` : "";
960
+ spinner.stop(`Applying to AI tools: ${syncedAiTools.join(", ")}${ideSummary}`);
961
+ spinner.start("Checking for legacy paths...");
962
+ const legacyFiles = await detectLegacyPaths(projectRoot);
963
+ if (legacyFiles.length > 0 && !dryRun) {
964
+ spinner.stop(`Found ${legacyFiles.length} legacy file(s)`);
965
+ if (!autoYes) {
966
+ Ve(`Found legacy configuration files:\n${legacyFiles.map((f) => ` - ${f.legacyPath}`).join("\n")}`, "Legacy Files");
967
+ R.warn("Run migration manually with appropriate action (migrate/copy/skip)");
968
+ }
969
+ } else spinner.stop("No legacy files found");
970
+ spinner.start("Processing configurations...");
971
+ const adapters = getAIToolAdaptersForKeys(syncedAiTools);
972
+ const placementConfig = {
973
+ mode: "copy",
974
+ projectRoot
975
+ };
976
+ const placedFiles = /* @__PURE__ */ new Map();
977
+ const profileLocalPaths = /* @__PURE__ */ new Map();
978
+ for (const profileSource of projectManifest.profiles || []) {
979
+ const parsed = parseSource(profileSource.source);
980
+ if (parsed.provider === "local" || parsed.provider === "file") {
981
+ const localPath = parsed.path.startsWith("/") ? parsed.path : resolve(projectRoot, parsed.path);
982
+ for (const prof of allProfiles) if (prof.source === profileSource.source) profileLocalPaths.set(prof.name, localPath);
983
+ } else if (parsed.provider === "github" || parsed.provider === "gitlab" || parsed.provider === "git") {
984
+ const url = parsed.provider === "git" ? parsed.url : parsed.url;
985
+ let ref = profileSource.version;
986
+ if (lockfile) {
987
+ const packageName = getPackageNameFromSource(profileSource.source, parsed);
988
+ const lockedPkg = lockfile.packages[packageName];
989
+ if (lockedPkg?.sha && lockedPkg.sha !== "unknown") ref = lockedPkg.sha;
990
+ }
991
+ const cloned = await cloneGitSource({
992
+ url,
993
+ ref,
994
+ subpath: "subpath" in parsed ? parsed.subpath : void 0,
995
+ useCache: true,
996
+ maxCacheAgeMs
997
+ });
998
+ for (const prof of allProfiles) if (prof.source === profileSource.source) profileLocalPaths.set(prof.name, cloned.localPath);
999
+ }
1000
+ }
1001
+ for (const prof of allProfiles) if (!profileLocalPaths.has(prof.name) && prof.localPath) profileLocalPaths.set(prof.name, prof.localPath);
1002
+ const contentAccumulator = /* @__PURE__ */ new Map();
1003
+ if (!dryRun && syncAi) for (const adapter of adapters) {
1004
+ if (verbose) R.step(`[${adapter.key}] Placing memory files...`);
1005
+ for (const memoryEntry of mergedMemory) try {
1006
+ const contentParts = [];
1007
+ for (const contribution of memoryEntry.contributions) {
1008
+ const profileDir = profileLocalPaths.get(contribution.profileName);
1009
+ if (!profileDir) {
1010
+ spinner.message(`Warning: Could not resolve local path for profile ${contribution.profileName}`);
1011
+ continue;
1012
+ }
1013
+ const memoryFilePath = resolve(profileDir, "ai", "memory", memoryEntry.filename);
1014
+ try {
1015
+ const content = await readFile(memoryFilePath, "utf-8");
1016
+ contentParts.push(content);
1017
+ } catch {
1018
+ spinner.message(`Warning: Could not read ${memoryFilePath}`);
1019
+ }
1020
+ }
1021
+ if (contentParts.length === 0) continue;
1022
+ const mergedContent = mergeContentParts(contentParts, memoryEntry.mergeStrategy);
1023
+ const transformed = adapter.transformMemory({
1024
+ filename: memoryEntry.filename,
1025
+ content: mergedContent
1026
+ });
1027
+ const targetPath = adapter.getPath("memory", "project", transformed.filename);
1028
+ const absolutePath = targetPath.startsWith("/") ? targetPath : resolve(projectRoot, targetPath);
1029
+ const existing = contentAccumulator.get(absolutePath);
1030
+ if (existing) {
1031
+ existing.parts.push(transformed.content);
1032
+ for (const c of memoryEntry.contributions) existing.profiles.add(c.profileName);
1033
+ } else {
1034
+ const profiles = /* @__PURE__ */ new Set();
1035
+ for (const c of memoryEntry.contributions) profiles.add(c.profileName);
1036
+ contentAccumulator.set(absolutePath, {
1037
+ parts: [transformed.content],
1038
+ adapter,
1039
+ type: "memory",
1040
+ name: transformed.filename,
1041
+ profiles
1042
+ });
1043
+ }
1044
+ } catch (error) {
1045
+ spinner.message(`Error placing ${memoryEntry.filename} for ${adapter.name}: ${error}`);
1046
+ stats.errors++;
1047
+ }
1048
+ }
1049
+ if (!dryRun && syncAi) for (const adapter of adapters) {
1050
+ if (verbose) R.step(`[${adapter.key}] Placing skills...`);
1051
+ for (const skillItem of mergedSkills) try {
1052
+ const profileDir = profileLocalPaths.get(skillItem.profileName);
1053
+ if (!profileDir) {
1054
+ spinner.message(`Warning: Could not resolve local path for profile ${skillItem.profileName}`);
1055
+ continue;
1056
+ }
1057
+ const skillSourceDir = resolve(profileDir, "ai", "skills", skillItem.name);
1058
+ try {
1059
+ await stat(skillSourceDir);
1060
+ } catch {
1061
+ spinner.message(`Warning: Skill directory not found: ${skillSourceDir}`);
1062
+ continue;
1063
+ }
1064
+ const targetSkillPath = adapter.getPath("skills", skillItem.scope, skillItem.name);
1065
+ const absoluteTargetDir = targetSkillPath.startsWith("/") ? targetSkillPath : resolve(projectRoot, targetSkillPath);
1066
+ const placed = await copyDirectoryRecursive(skillSourceDir, absoluteTargetDir);
1067
+ stats.created += placed;
1068
+ const profileFiles = getOrCreatePlacedFiles(placedFiles, skillItem.profileName);
1069
+ try {
1070
+ profileFiles[targetSkillPath] = {
1071
+ content: await readFile(resolve(skillSourceDir, "index.md"), "utf-8"),
1072
+ tool: adapter.key,
1073
+ category: "ai"
1074
+ };
1075
+ } catch {
1076
+ profileFiles[targetSkillPath] = {
1077
+ content: skillItem.name,
1078
+ tool: adapter.key,
1079
+ category: "ai"
1080
+ };
1081
+ }
1082
+ if (verbose) {
1083
+ const label = placed > 0 ? `${placed} file(s) created` : "unchanged, skipped";
1084
+ R.info(` -> ${absoluteTargetDir}/ (${label})`);
1085
+ }
1086
+ } catch (error) {
1087
+ spinner.message(`Error placing skill ${skillItem.name} for ${adapter.name}: ${error}`);
1088
+ stats.errors++;
1089
+ }
1090
+ }
1091
+ if (!dryRun && syncAi) for (const adapter of adapters) {
1092
+ if (verbose) R.step(`[${adapter.key}] Placing rules...`);
1093
+ for (const ruleEntry of mergedRules) try {
1094
+ const ruleName = ruleEntry.name.replace(/\.md$/, "");
1095
+ const isUniversal = ruleEntry.agents.length === 0;
1096
+ const isForThisAdapter = ruleEntry.agents.includes(adapter.key);
1097
+ if (!isUniversal && !isForThisAdapter) continue;
1098
+ const profileDir = profileLocalPaths.get(ruleEntry.profileName);
1099
+ if (!profileDir) {
1100
+ spinner.message(`Warning: Could not resolve local path for profile ${ruleEntry.profileName}`);
1101
+ continue;
1102
+ }
1103
+ const ruleSourcePath = resolve(profileDir, "ai", "rules", isUniversal ? "universal" : ruleEntry.agents[0], `${ruleName}.md`);
1104
+ let rawContent;
1105
+ try {
1106
+ rawContent = await readFile(ruleSourcePath, "utf-8");
1107
+ } catch {
1108
+ spinner.message(`Warning: Could not read rule file: ${ruleSourcePath}`);
1109
+ continue;
1110
+ }
1111
+ const parsed = parseFrontmatter(rawContent);
1112
+ const ruleFile = {
1113
+ name: ruleName,
1114
+ content: rawContent,
1115
+ frontmatter: Object.keys(parsed.data).length > 0 ? parsed.data : void 0
1116
+ };
1117
+ const transformed = adapter.transformRule(ruleFile);
1118
+ const targetPath = adapter.getPath("rules", "project", ruleName);
1119
+ const absolutePath = targetPath.startsWith("/") ? targetPath : resolve(projectRoot, targetPath);
1120
+ const existing = contentAccumulator.get(absolutePath);
1121
+ if (existing) {
1122
+ existing.parts.push(transformed.content);
1123
+ existing.profiles.add(ruleEntry.profileName);
1124
+ } else contentAccumulator.set(absolutePath, {
1125
+ parts: [transformed.content],
1126
+ adapter,
1127
+ type: "rules",
1128
+ name: ruleName,
1129
+ profiles: new Set([ruleEntry.profileName])
1130
+ });
1131
+ } catch (error) {
1132
+ spinner.message(`Error placing rule ${ruleEntry.name} for ${adapter.name}: ${error}`);
1133
+ stats.errors++;
1134
+ }
1135
+ }
1136
+ if (!dryRun && syncAi) for (const adapter of adapters) {
1137
+ if (verbose) R.step(`[${adapter.key}] Placing agents...`);
1138
+ for (const agentEntry of mergedAgents) try {
1139
+ const agentName = agentEntry.name.replace(/\.md$/, "");
1140
+ const isUniversal = agentEntry.agents.length === 0;
1141
+ const isForThisAdapter = agentEntry.agents.includes(adapter.key);
1142
+ if (!isUniversal && !isForThisAdapter) continue;
1143
+ const profileDir = profileLocalPaths.get(agentEntry.profileName);
1144
+ if (!profileDir) {
1145
+ spinner.message(`Warning: Could not resolve local path for profile ${agentEntry.profileName}`);
1146
+ continue;
1147
+ }
1148
+ const agentSourcePath = resolve(profileDir, "ai", "agents", isUniversal ? "universal" : agentEntry.agents[0], `${agentName}.md`);
1149
+ let rawContent;
1150
+ try {
1151
+ rawContent = await readFile(agentSourcePath, "utf-8");
1152
+ } catch {
1153
+ spinner.message(`Warning: Could not read agent file: ${agentSourcePath}`);
1154
+ continue;
1155
+ }
1156
+ const parsed = parseFrontmatter(rawContent);
1157
+ const frontmatter = Object.keys(parsed.data).length > 0 ? parsed.data : { name: agentName };
1158
+ const agentFile = {
1159
+ name: agentName,
1160
+ content: rawContent,
1161
+ description: frontmatter.description,
1162
+ frontmatter
1163
+ };
1164
+ const transformed = adapter.transformAgent(agentFile);
1165
+ const targetPath = adapter.getPath("agents", "project", agentName);
1166
+ const absolutePath = targetPath.startsWith("/") ? targetPath : resolve(projectRoot, targetPath);
1167
+ const existing = contentAccumulator.get(absolutePath);
1168
+ if (existing) {
1169
+ existing.parts.push(transformed.content);
1170
+ existing.profiles.add(agentEntry.profileName);
1171
+ } else contentAccumulator.set(absolutePath, {
1172
+ parts: [transformed.content],
1173
+ adapter,
1174
+ type: "agents",
1175
+ name: agentName,
1176
+ profiles: new Set([agentEntry.profileName])
1177
+ });
1178
+ } catch (error) {
1179
+ spinner.message(`Error placing agent ${agentEntry.name} for ${adapter.name}: ${error}`);
1180
+ stats.errors++;
1181
+ }
1182
+ }
1183
+ if (!dryRun && syncAi) for (const [absolutePath, entry] of contentAccumulator) try {
1184
+ const combinedContent = entry.parts.join("\n\n");
1185
+ const result = await placeFile(combinedContent, entry.adapter, entry.type, "project", entry.name, placementConfig);
1186
+ if (result.action !== "skipped") stats.created++;
1187
+ const relPath = isAbsolute(result.path) ? relative(projectRoot, result.path) : result.path;
1188
+ for (const profileName of entry.profiles) {
1189
+ const pf = getOrCreatePlacedFiles(placedFiles, profileName);
1190
+ pf[relPath] = {
1191
+ content: combinedContent,
1192
+ tool: entry.adapter.key,
1193
+ category: "ai"
1194
+ };
1195
+ }
1196
+ if (verbose) {
1197
+ const label = result.action === "skipped" ? "unchanged, skipped" : result.action;
1198
+ R.info(` -> ${result.path} (${label})`);
1199
+ }
1200
+ } catch (error) {
1201
+ spinner.message(`Error placing accumulated content to ${absolutePath}: ${error}`);
1202
+ stats.errors++;
1203
+ }
1204
+ if (!dryRun && syncAi) for (const adapter of adapters) {
1205
+ if (verbose) R.step(`[${adapter.key}] Placing commands...`);
1206
+ for (const profile of allProfiles) {
1207
+ const profileDir = profileLocalPaths.get(profile.name);
1208
+ if (!profileDir) continue;
1209
+ const commandNames = profile.manifest.ai?.commands || [];
1210
+ for (const commandName of commandNames) try {
1211
+ const commandSourcePath = resolve(profileDir, "ai", "commands", `${commandName}.md`);
1212
+ let content;
1213
+ try {
1214
+ content = await readFile(commandSourcePath, "utf-8");
1215
+ } catch {
1216
+ continue;
1217
+ }
1218
+ const result = await placeFile(content, adapter, "commands", "project", commandName, placementConfig);
1219
+ if (result.action !== "skipped") stats.created++;
1220
+ const cmdRelPath = isAbsolute(result.path) ? relative(projectRoot, result.path) : result.path;
1221
+ const pf = getOrCreatePlacedFiles(placedFiles, profile.name);
1222
+ pf[cmdRelPath] = {
1223
+ content,
1224
+ tool: adapter.key,
1225
+ category: "ai"
1226
+ };
1227
+ if (verbose) {
1228
+ const label = result.action === "skipped" ? "unchanged, skipped" : result.action;
1229
+ R.info(` -> ${result.path} (${label})`);
1230
+ }
1231
+ } catch (error) {
1232
+ spinner.message(`Error placing command ${commandName} for ${adapter.name}: ${error}`);
1233
+ stats.errors++;
1234
+ }
1235
+ }
1236
+ }
1237
+ if (!dryRun && syncFiles) for (const fileEntry of fileMap.values()) try {
1238
+ const profileDir = profileLocalPaths.get(fileEntry.profileName);
1239
+ if (!profileDir) continue;
1240
+ const fileSourcePath = resolve(profileDir, "files", fileEntry.source);
1241
+ let content;
1242
+ try {
1243
+ content = await readFile(fileSourcePath, "utf-8");
1244
+ } catch {
1245
+ continue;
1246
+ }
1247
+ const targetPath = resolve(projectRoot, fileEntry.target);
1248
+ await mkdir(dirname(targetPath), { recursive: true });
1249
+ if (await readFile(targetPath, "utf-8").catch(() => void 0) !== content) {
1250
+ await writeFile(targetPath, content, "utf-8");
1251
+ stats.created++;
1252
+ if (verbose) R.info(` -> ${fileEntry.target} (created)`);
1253
+ } else if (verbose) R.info(` -> ${fileEntry.target} (unchanged, skipped)`);
1254
+ const fpf = getOrCreatePlacedFiles(placedFiles, fileEntry.profileName);
1255
+ fpf[fileEntry.target] = {
1256
+ content,
1257
+ category: "files"
1258
+ };
1259
+ } catch (error) {
1260
+ spinner.message(`Error placing file ${fileEntry.source}: ${error}`);
1261
+ stats.errors++;
1262
+ }
1263
+ if (!dryRun && syncIde) for (const ideEntry of ideMap.values()) try {
1264
+ if (syncedIdePlatforms !== null && !syncedIdePlatforms.includes(ideEntry.ideKey)) {
1265
+ if (verbose) R.info(` -> ${ideEntry.targetDir}/${ideEntry.fileName} (skipped — IDE platform "${ideEntry.ideKey}" not in intersection)`);
1266
+ continue;
1267
+ }
1268
+ const profileDir = profileLocalPaths.get(ideEntry.profileName);
1269
+ if (!profileDir) continue;
1270
+ const ideSourcePath = resolve(profileDir, "ide", ideEntry.ideKey, ideEntry.fileName);
1271
+ let content;
1272
+ try {
1273
+ content = await readFile(ideSourcePath, "utf-8");
1274
+ } catch {
1275
+ continue;
1276
+ }
1277
+ const targetPath = resolve(projectRoot, ideEntry.targetDir, ideEntry.fileName);
1278
+ await mkdir(dirname(targetPath), { recursive: true });
1279
+ if (await readFile(targetPath, "utf-8").catch(() => void 0) !== content) {
1280
+ await writeFile(targetPath, content, "utf-8");
1281
+ stats.created++;
1282
+ if (verbose) R.info(` -> ${ideEntry.targetDir}/${ideEntry.fileName} (created)`);
1283
+ } else if (verbose) R.info(` -> ${ideEntry.targetDir}/${ideEntry.fileName} (unchanged, skipped)`);
1284
+ const ideRelPath = `${ideEntry.targetDir}/${ideEntry.fileName}`;
1285
+ const ipf = getOrCreatePlacedFiles(placedFiles, ideEntry.profileName);
1286
+ ipf[ideRelPath] = {
1287
+ content,
1288
+ tool: ideEntry.ideKey,
1289
+ category: "ide"
1290
+ };
1291
+ } catch (error) {
1292
+ spinner.message(`Error placing IDE config ${ideEntry.fileName}: ${error}`);
1293
+ stats.errors++;
1294
+ }
1295
+ spinner.stop(dryRun ? `Would place files for ${adapters.length} agent(s)` : `Placed ${stats.created} file(s) for ${adapters.length} agent(s)`);
1296
+ if (!dryRun) await handleGitignoreUpdate({
1297
+ projectManifest,
1298
+ fileMap,
1299
+ projectRoot,
1300
+ spinner
1301
+ });
1302
+ if (!dryRun) await writeLockData({
1303
+ allProfiles,
1304
+ sourceShas,
1305
+ placedFiles,
1306
+ projectRoot,
1307
+ spinner
1308
+ });
1309
+ await cleanupOrphanedFiles({
1310
+ previousPaths,
1311
+ placedFiles,
1312
+ projectRoot,
1313
+ dryRun,
1314
+ autoYes,
1315
+ spinner
1316
+ });
1317
+ if (dryRun) {
1318
+ const parts = [];
1319
+ if (syncAi) {
1320
+ parts.push(` • ${mergedSkills.length} skills`);
1321
+ parts.push(` • ${mergedRules.length} rules`);
1322
+ parts.push(` • ${mergedAgents.length} agents`);
1323
+ parts.push(` • ${mergedMemory.length} memory files`);
1324
+ parts.push(` • ${mergedCommandCount} commands`);
1325
+ }
1326
+ if (syncFiles) parts.push(` • ${mergedFileCount} files`);
1327
+ if (syncIde) {
1328
+ const filteredIdeCount = syncedIdePlatforms !== null ? [...ideMap.values()].filter((e) => syncedIdePlatforms.includes(e.ideKey)).length : mergedIdeCount;
1329
+ parts.push(` • ${filteredIdeCount} IDE configs`);
1330
+ }
1331
+ const categoryLabel = category ? ` (category: ${category})` : "";
1332
+ Le(`[Dry Run${categoryLabel}] Would apply:\n${parts.join("\n")}\n\nFor ${adapters.length} agent(s): ${syncedAiTools.join(", ")}`);
1333
+ } else {
1334
+ const categoryLabel = category ? ` (category: ${category})` : "";
1335
+ Le(`✅ Apply complete${categoryLabel}! Locked configurations applied.`);
1336
+ }
1337
+ process.exit(stats.errors > 0 ? 1 : 0);
1338
+ } catch (error) {
1339
+ Ne(`Apply failed: ${error}`);
1340
+ process.exit(1);
1341
+ }
1342
+ }
1343
+ });
419
1344
 
420
1345
  //#endregion
421
1346
  //#region src/commands/config/set.ts
@@ -943,7 +1868,7 @@ async function loadFilesFromDirectory(dirPath) {
943
1868
  /**
944
1869
  * Format an IDE platform key into a display name.
945
1870
  */
946
- function formatIdeName$2(ideKey) {
1871
+ function formatIdeName$1(ideKey) {
947
1872
  return {
948
1873
  vscode: "VS Code",
949
1874
  jetbrains: "JetBrains",
@@ -989,7 +1914,7 @@ async function runGlobalMode(nonInteractive) {
989
1914
  const options = getRegisteredIdePlatforms().map((ideKey) => {
990
1915
  return {
991
1916
  value: ideKey,
992
- label: currentPlatforms.includes(ideKey) ? `${formatIdeName$2(ideKey)} (currently saved)` : formatIdeName$2(ideKey)
1917
+ label: currentPlatforms.includes(ideKey) ? `${formatIdeName$1(ideKey)} (currently saved)` : formatIdeName$1(ideKey)
993
1918
  };
994
1919
  });
995
1920
  const selected = await je({
@@ -1076,7 +2001,7 @@ async function runProjectMode(nonInteractive) {
1076
2001
  const options = allIdeKeys.map((ideKey) => {
1077
2002
  return {
1078
2003
  value: ideKey,
1079
- label: globalPlatforms.includes(ideKey) ? `${formatIdeName$2(ideKey)} (in global config)` : formatIdeName$2(ideKey)
2004
+ label: globalPlatforms.includes(ideKey) ? `${formatIdeName$1(ideKey)} (in global config)` : formatIdeName$1(ideKey)
1080
2005
  };
1081
2006
  });
1082
2007
  const selected = await je({
@@ -1132,7 +2057,7 @@ const idesListCommand = defineCommand({
1132
2057
  const entry = idePlatformRegistry[ideKey];
1133
2058
  return {
1134
2059
  key: ideKey,
1135
- name: formatIdeName$2(ideKey),
2060
+ name: formatIdeName$1(ideKey),
1136
2061
  saved: isSaved,
1137
2062
  targetDir: entry?.targetDir ?? "unknown"
1138
2063
  };
@@ -1148,7 +2073,7 @@ const idesListCommand = defineCommand({
1148
2073
  R.info(`All ${allIdeKeys.length} supported platforms:`);
1149
2074
  for (const key of allIdeKeys) {
1150
2075
  const entry = idePlatformRegistry[key];
1151
- console.log(` \x1b[90m- ${formatIdeName$2(key)} (${entry?.targetDir ?? key})\x1b[0m`);
2076
+ console.log(` \x1b[90m- ${formatIdeName$1(key)} (${entry?.targetDir ?? key})\x1b[0m`);
1152
2077
  }
1153
2078
  Le("Run 'baton ides scan' to get started.");
1154
2079
  return;
@@ -1198,7 +2123,7 @@ const idesScanCommand = defineCommand({
1198
2123
  const options = allIdeKeys.map((ideKey) => {
1199
2124
  return {
1200
2125
  value: ideKey,
1201
- label: detectedIdes.includes(ideKey) ? `${formatIdeName$2(ideKey)} (detected)` : formatIdeName$2(ideKey)
2126
+ label: detectedIdes.includes(ideKey) ? `${formatIdeName$1(ideKey)} (detected)` : formatIdeName$1(ideKey)
1202
2127
  };
1203
2128
  });
1204
2129
  const selected = await je({
@@ -1220,136 +2145,22 @@ const idesScanCommand = defineCommand({
1220
2145
  });
1221
2146
 
1222
2147
  //#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();
1324
- const allIdeKeys = getRegisteredIdePlatforms();
1325
- const selected = await je({
1326
- message: "Select IDE platforms for this project:",
1327
- options: allIdeKeys.map((ideKey) => ({
1328
- value: ideKey,
1329
- label: globalPlatforms.includes(ideKey) ? `${formatIdeName$1(ideKey)} (in global config)` : formatIdeName$1(ideKey)
1330
- })),
1331
- initialValues: globalPlatforms
1332
- });
1333
- if (Ct(selected)) return false;
1334
- ideUseGlobal = false;
1335
- idePlatforms = selected;
2148
+ //#region src/commands/ides/index.ts
2149
+ const idesCommand = defineCommand({
2150
+ meta: {
2151
+ name: "ides",
2152
+ description: "Manage IDE platform detection and configuration"
2153
+ },
2154
+ subCommands: {
2155
+ configure: idesConfigureCommand,
2156
+ list: idesListCommand,
2157
+ scan: idesScanCommand
1336
2158
  }
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
1346
- }
1347
- });
1348
- return true;
1349
- }
2159
+ });
1350
2160
 
1351
2161
  //#endregion
1352
2162
  //#region src/utils/profile-selection.ts
2163
+ var import_dist = require_dist();
1353
2164
  /**
1354
2165
  * Discovers and prompts user to select a profile from a source.
1355
2166
  * Used by `baton init --profile` and `baton manage` (add profile).
@@ -1643,11 +2454,11 @@ const initCommand = defineCommand({
1643
2454
  await promptFirstRunPreferences(cwd, !isInteractive);
1644
2455
  if (profileSources.length > 0) {
1645
2456
  const shouldSync = isInteractive ? await Re({
1646
- message: "Sync profiles now?",
2457
+ message: "Fetch profiles and sync now?",
1647
2458
  initialValue: true
1648
2459
  }) : true;
1649
2460
  if (!Ct(shouldSync) && shouldSync) await runBatonSync(cwd);
1650
- else R.info("Run 'baton sync' later to apply your profiles.");
2461
+ else R.info("Run 'baton sync' later to fetch and apply your profiles.");
1651
2462
  }
1652
2463
  Le("Baton initialized successfully!");
1653
2464
  }
@@ -2232,9 +3043,121 @@ const profileCommand = defineCommand({
2232
3043
  description: "Manage profiles (create, list, remove)"
2233
3044
  },
2234
3045
  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)
3046
+ create: () => import("./create-Y2IeW_fK.mjs").then((m) => m.createCommand),
3047
+ list: () => import("./list-D3woEF2B.mjs").then((m) => m.profileListCommand),
3048
+ remove: () => import("./remove-CsxkkNiu.mjs").then((m) => m.profileRemoveCommand)
3049
+ }
3050
+ });
3051
+
3052
+ //#endregion
3053
+ //#region src/commands/self-update.ts
3054
+ const __dirname$2 = dirname(fileURLToPath(import.meta.url));
3055
+ async function readCurrentVersion() {
3056
+ try {
3057
+ const pkg = JSON.parse(await readFile(join(__dirname$2, "../package.json"), "utf-8"));
3058
+ return typeof pkg.version === "string" ? pkg.version : "0.0.0";
3059
+ } catch {
3060
+ return "0.0.0";
3061
+ }
3062
+ }
3063
+ const selfUpdateCommand = defineCommand({
3064
+ meta: {
3065
+ name: "self-update",
3066
+ description: "Update Baton to the latest stable version"
3067
+ },
3068
+ args: {
3069
+ changelog: {
3070
+ type: "boolean",
3071
+ description: "Show release notes for the new version",
3072
+ default: false
3073
+ },
3074
+ "dry-run": {
3075
+ type: "boolean",
3076
+ description: "Check for updates without performing the update",
3077
+ default: false
3078
+ },
3079
+ yes: {
3080
+ type: "boolean",
3081
+ alias: "y",
3082
+ description: "Skip confirmation prompt",
3083
+ default: false
3084
+ }
3085
+ },
3086
+ async run({ args }) {
3087
+ We("baton self-update");
3088
+ const currentVersion = await readCurrentVersion();
3089
+ const s = bt();
3090
+ s.start("Checking for updates...");
3091
+ let latestVersion;
3092
+ try {
3093
+ latestVersion = (await checkLatestVersion()).version;
3094
+ } catch (error) {
3095
+ s.stop("Failed to check for updates");
3096
+ R.error(error instanceof Error ? error.message : "Unknown error occurred");
3097
+ Le("Update check failed.");
3098
+ process.exit(1);
3099
+ }
3100
+ s.stop("Version check complete");
3101
+ const { updateAvailable } = isUpdateAvailable(currentVersion, latestVersion);
3102
+ if (!updateAvailable) {
3103
+ R.success(`Already up to date (v${currentVersion}).`);
3104
+ Le("No update needed.");
3105
+ return;
3106
+ }
3107
+ const installMethod = await detectInstallMethod();
3108
+ const displayCommand = formatInstallCommand(installMethod);
3109
+ R.info([
3110
+ `Current version: v${currentVersion}`,
3111
+ `Latest version: v${latestVersion}`,
3112
+ installMethod.type !== "unknown" ? `Install method: ${installMethod.type}` : ""
3113
+ ].filter(Boolean).join("\n"));
3114
+ if (installMethod.type === "unknown") {
3115
+ R.warn("Could not detect installation method.");
3116
+ R.message([
3117
+ "Please update manually using one of:",
3118
+ " npm update -g @baton-dx/cli",
3119
+ " pnpm update -g @baton-dx/cli",
3120
+ " bun update -g @baton-dx/cli",
3121
+ " brew upgrade baton-dx"
3122
+ ].join("\n"));
3123
+ Le("Manual update required.");
3124
+ return;
3125
+ }
3126
+ if (args["dry-run"]) {
3127
+ R.info(`Would run: ${displayCommand}`);
3128
+ Le("Dry run complete.");
3129
+ return;
3130
+ }
3131
+ if (args.changelog) {
3132
+ const changelogUrl = `https://github.com/baton-dx/baton/releases/tag/v${latestVersion}`;
3133
+ R.info(`Release notes: ${changelogUrl}`);
3134
+ }
3135
+ if (!args.yes) {
3136
+ const confirmed = await Re({ message: `Update to v${latestVersion}?` });
3137
+ if (Ct(confirmed) || !confirmed) {
3138
+ Le("Update cancelled.");
3139
+ return;
3140
+ }
3141
+ }
3142
+ const updateSpinner = bt();
3143
+ updateSpinner.start(`Running: ${displayCommand}`);
3144
+ try {
3145
+ await new Promise((resolve, reject) => {
3146
+ execFile(installMethod.bin, installMethod.args, (error) => {
3147
+ if (error) reject(error);
3148
+ else resolve();
3149
+ });
3150
+ });
3151
+ updateSpinner.stop(`Successfully updated to v${latestVersion}`);
3152
+ Le("Update complete!");
3153
+ } catch (error) {
3154
+ updateSpinner.stop("Update failed");
3155
+ const message = error instanceof Error ? error.message : "Unknown error";
3156
+ R.error(`Failed to run: ${displayCommand}`);
3157
+ R.error(message);
3158
+ Le("Update failed. Please try updating manually.");
3159
+ process.exit(1);
3160
+ }
2238
3161
  }
2239
3162
  });
2240
3163
 
@@ -2654,122 +3577,10 @@ const sourceCommand = defineCommand({
2654
3577
 
2655
3578
  //#endregion
2656
3579
  //#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
3580
  const syncCommand = defineCommand({
2770
3581
  meta: {
2771
3582
  name: "sync",
2772
- description: "Sync all profiles, skills, agents, and rules to installed AI tools"
3583
+ description: "Fetch latest versions, sync all configurations, and update lockfile"
2773
3584
  },
2774
3585
  args: {
2775
3586
  "dry-run": {
@@ -2792,11 +3603,6 @@ const syncCommand = defineCommand({
2792
3603
  alias: "v",
2793
3604
  description: "Show detailed output for each placed file",
2794
3605
  default: false
2795
- },
2796
- fresh: {
2797
- type: "boolean",
2798
- description: "Force an immediate source refresh (ignore cache TTL)",
2799
- default: false
2800
3606
  }
2801
3607
  },
2802
3608
  async run({ args }) {
@@ -2804,7 +3610,6 @@ const syncCommand = defineCommand({
2804
3610
  const categoryArg = args.category;
2805
3611
  const autoYes = args.yes;
2806
3612
  const verbose = args.verbose;
2807
- const fresh = args.fresh;
2808
3613
  let category;
2809
3614
  if (categoryArg) {
2810
3615
  if (!validCategories.includes(categoryArg)) {
@@ -2833,11 +3638,6 @@ const syncCommand = defineCommand({
2833
3638
  process.exit(1);
2834
3639
  }
2835
3640
  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
3641
  const previousPaths = /* @__PURE__ */ new Set();
2842
3642
  try {
2843
3643
  const previousLock = await readLock(resolve(projectRoot, "baton.lock"));
@@ -2866,12 +3666,19 @@ const syncCommand = defineCommand({
2866
3666
  } else {
2867
3667
  const url = parsed.provider === "github" || parsed.provider === "gitlab" ? parsed.url : parsed.provider === "git" ? parsed.url : "";
2868
3668
  if (!url) throw new Error(`Invalid source: ${profileSource.source}`);
3669
+ let resolvedRef;
3670
+ try {
3671
+ resolvedRef = await resolveVersion(url, "latest");
3672
+ if (verbose) R.info(`Resolved latest: ${profileSource.source} → ${resolvedRef.slice(0, 12)}`);
3673
+ } catch {
3674
+ resolvedRef = profileSource.version || "HEAD";
3675
+ if (verbose) R.warn(`Could not resolve latest for ${url}, using ${resolvedRef}`);
3676
+ }
2869
3677
  const cloned = await cloneGitSource({
2870
3678
  url,
2871
- ref: profileSource.version,
3679
+ ref: resolvedRef,
2872
3680
  subpath: "subpath" in parsed ? parsed.subpath : void 0,
2873
- useCache: true,
2874
- maxCacheAgeMs
3681
+ useCache: false
2875
3682
  });
2876
3683
  manifestPath = resolve(cloned.localPath, "baton.profile.yaml");
2877
3684
  sourceShas.set(profileSource.source, cloned.sha);
@@ -3086,10 +3893,9 @@ const syncCommand = defineCommand({
3086
3893
  } else if (parsed.provider === "github" || parsed.provider === "gitlab" || parsed.provider === "git") {
3087
3894
  const cloned = await cloneGitSource({
3088
3895
  url: parsed.provider === "git" ? parsed.url : parsed.url,
3089
- ref: profileSource.version,
3896
+ ref: sourceShas.get(profileSource.source) || profileSource.version,
3090
3897
  subpath: "subpath" in parsed ? parsed.subpath : void 0,
3091
- useCache: true,
3092
- maxCacheAgeMs
3898
+ useCache: true
3093
3899
  });
3094
3900
  for (const prof of allProfiles) if (prof.source === profileSource.source) profileLocalPaths.set(prof.name, cloned.localPath);
3095
3901
  }
@@ -3443,167 +4249,37 @@ const syncCommand = defineCommand({
3443
4249
  const updateCommand = defineCommand({
3444
4250
  meta: {
3445
4251
  name: "update",
3446
- description: "Check for and apply updates to installed profiles and packages"
4252
+ description: "(deprecated) Use 'baton sync' instead"
3447
4253
  },
3448
4254
  args: {
3449
4255
  "dry-run": {
3450
4256
  type: "boolean",
3451
- description: "Show available updates without applying them",
4257
+ description: "Show what would be done without writing files",
3452
4258
  default: false
3453
4259
  },
4260
+ category: {
4261
+ type: "string",
4262
+ description: "Sync only a specific category: ai, files, or ide",
4263
+ required: false
4264
+ },
3454
4265
  yes: {
3455
4266
  type: "boolean",
3456
- description: "Apply all updates without confirmation prompts",
4267
+ description: "Run non-interactively (no prompts)",
4268
+ default: false
4269
+ },
4270
+ verbose: {
4271
+ type: "boolean",
4272
+ alias: "v",
4273
+ description: "Show detailed output for each placed file",
3457
4274
  default: false
3458
4275
  }
3459
4276
  },
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);
4277
+ async run(context) {
4278
+ R.warn("`baton update` is deprecated. Use `baton sync` instead.");
4279
+ R.info("");
4280
+ if (syncCommand.run) await syncCommand.run(context);
3573
4281
  }
3574
4282
  });
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
4283
 
3608
4284
  //#endregion
3609
4285
  //#region src/index.ts
@@ -3640,6 +4316,7 @@ runMain(defineCommand({
3640
4316
  },
3641
4317
  subCommands: {
3642
4318
  init: initCommand,
4319
+ apply: applyCommand,
3643
4320
  sync: syncCommand,
3644
4321
  update: updateCommand,
3645
4322
  diff: diffCommand,
@@ -3648,7 +4325,8 @@ runMain(defineCommand({
3648
4325
  source: sourceCommand,
3649
4326
  profile: profileCommand,
3650
4327
  "ai-tools": aiToolsCommand,
3651
- ides: idesCommand
4328
+ ides: idesCommand,
4329
+ "self-update": selfUpdateCommand
3652
4330
  },
3653
4331
  run({ args }) {
3654
4332
  if (Object.keys(args).length === 0) {
@@ -3661,8 +4339,9 @@ runMain(defineCommand({
3661
4339
  console.log("");
3662
4340
  console.log("Available commands:");
3663
4341
  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");
4342
+ console.log(" apply Apply locked configurations (deterministic, reproducible)");
4343
+ console.log(" sync Fetch latest versions, sync, and update lockfile");
4344
+ console.log(" update (deprecated) Use 'baton sync' instead");
3666
4345
  console.log(" diff Compare local files with remote source versions");
3667
4346
  console.log(" manage Interactive project management wizard");
3668
4347
  console.log(" config Show dashboard overview or configure settings");
@@ -3673,6 +4352,9 @@ runMain(defineCommand({
3673
4352
  console.log(" ai-tools Manage AI tool detection and configuration");
3674
4353
  console.log(" ides Manage IDE platform detection and configuration");
3675
4354
  console.log("");
4355
+ console.log("Maintenance:");
4356
+ console.log(" self-update Update Baton to the latest stable version");
4357
+ console.log("");
3676
4358
  console.log("Global Options:");
3677
4359
  console.log(" --help, -h Show this help message");
3678
4360
  console.log(" --version, -v Show version number");