@baton-dx/cli 0.4.3 → 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/{ai-tool-detection-BFep6YS9.mjs → ai-tool-detection-Bw4qveB8.mjs} +1 -1
- package/dist/{ai-tool-detection-CMsBNa9e.mjs → ai-tool-detection-DMnwwNBI.mjs} +1 -1
- package/dist/{ai-tool-detection-CMsBNa9e.mjs.map → ai-tool-detection-DMnwwNBI.mjs.map} +1 -1
- package/dist/{context-detection-DdbrKid3.mjs → context-detection-C7T1evnW.mjs} +41 -12
- package/dist/context-detection-C7T1evnW.mjs.map +1 -0
- package/dist/{create-C1zm03eE.mjs → create-Cx1nCS3X.mjs} +4 -6
- package/dist/{create-C1zm03eE.mjs.map → create-Cx1nCS3X.mjs.map} +1 -1
- package/dist/index.mjs +1051 -486
- package/dist/index.mjs.map +1 -1
- package/dist/{list-BHFeSiTO.mjs → list-BsAASsXi.mjs} +4 -5
- package/dist/{list-BHFeSiTO.mjs.map → list-BsAASsXi.mjs.map} +1 -1
- package/dist/{prompt-B0zuVo8N.mjs → prompt-C7kAOs-S.mjs} +4 -4
- package/dist/prompt-C7kAOs-S.mjs.map +1 -0
- package/dist/{remove-iaf_gkie.mjs → remove-36qv7yQ3.mjs} +2 -2
- package/dist/{remove-iaf_gkie.mjs.map → remove-36qv7yQ3.mjs.map} +1 -1
- package/dist/{src-YrWWPWNR.mjs → src-n95I0s2u.mjs} +4674 -154
- package/dist/src-n95I0s2u.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-BbwQpWto.mjs +0 -33
- package/dist/context-detection-DdbrKid3.mjs.map +0 -1
- package/dist/esm-BagM-kVd.mjs +0 -4526
- package/dist/esm-BagM-kVd.mjs.map +0 -1
- package/dist/esm-CuRZ1S4C.mjs +0 -4
- package/dist/prompt-B0zuVo8N.mjs.map +0 -1
- package/dist/src-YrWWPWNR.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { r as __toESM } from "./
|
|
3
|
-
import { a as
|
|
4
|
-
import {
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
|
1326
|
-
|
|
1327
|
-
|
|
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:
|
|
1330
|
-
}
|
|
1331
|
-
initialValues: globalPlatforms
|
|
2125
|
+
label: detectedIdes.includes(ideKey) ? `${formatIdeName$1(ideKey)} (detected)` : formatIdeName$1(ideKey)
|
|
2126
|
+
};
|
|
1332
2127
|
});
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
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
|
-
|
|
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: "
|
|
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-
|
|
2236
|
-
list: () => import("./list-
|
|
2237
|
-
remove: () => import("./remove-
|
|
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: "
|
|
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:
|
|
3566
|
+
ref: resolvedRef,
|
|
2872
3567
|
subpath: "subpath" in parsed ? parsed.subpath : void 0,
|
|
2873
|
-
useCache:
|
|
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: "
|
|
4139
|
+
description: "(deprecated) Use 'baton sync' instead"
|
|
3447
4140
|
},
|
|
3448
4141
|
args: {
|
|
3449
4142
|
"dry-run": {
|
|
3450
4143
|
type: "boolean",
|
|
3451
|
-
description: "Show
|
|
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: "
|
|
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(
|
|
3461
|
-
|
|
3462
|
-
|
|
3463
|
-
|
|
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("
|
|
3665
|
-
console.log("
|
|
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");
|