@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/{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-Ddu0mj_K.mjs → context-detection-D9yccWot.mjs} +41 -12
- package/dist/context-detection-D9yccWot.mjs.map +1 -0
- package/dist/{create-DEZA3dPb.mjs → create-Y2IeW_fK.mjs} +4 -6
- package/dist/{create-DEZA3dPb.mjs.map → create-Y2IeW_fK.mjs.map} +1 -1
- package/dist/index.mjs +1129 -447
- package/dist/index.mjs.map +1 -1
- package/dist/{list-BT2zFAVc.mjs → list-D3woEF2B.mjs} +4 -5
- package/dist/{list-BT2zFAVc.mjs.map → list-D3woEF2B.mjs.map} +1 -1
- package/dist/{prompt-DtgNNhRW.mjs → prompt-CLnET8eQ.mjs} +4 -4
- package/dist/prompt-CLnET8eQ.mjs.map +1 -0
- package/dist/{remove-BBFBYUPy.mjs → remove-CsxkkNiu.mjs} +2 -2
- package/dist/{remove-BBFBYUPy.mjs.map → remove-CsxkkNiu.mjs.map} +1 -1
- package/dist/{src-dY02psbw.mjs → src-BVo3M5-4.mjs} +4801 -193
- package/dist/src-BVo3M5-4.mjs.map +1 -0
- package/package.json +1 -1
- package/dist/chunk-BbwQpWto.mjs +0 -33
- package/dist/context-detection-Ddu0mj_K.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-DtgNNhRW.mjs.map +0 -1
- package/dist/src-dY02psbw.mjs.map +0 -1
package/dist/index.mjs
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
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-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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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: "
|
|
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-
|
|
2236
|
-
list: () => import("./list-
|
|
2237
|
-
remove: () => import("./remove-
|
|
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: "
|
|
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:
|
|
3679
|
+
ref: resolvedRef,
|
|
2872
3680
|
subpath: "subpath" in parsed ? parsed.subpath : void 0,
|
|
2873
|
-
useCache:
|
|
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: "
|
|
4252
|
+
description: "(deprecated) Use 'baton sync' instead"
|
|
3447
4253
|
},
|
|
3448
4254
|
args: {
|
|
3449
4255
|
"dry-run": {
|
|
3450
4256
|
type: "boolean",
|
|
3451
|
-
description: "Show
|
|
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: "
|
|
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(
|
|
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);
|
|
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("
|
|
3665
|
-
console.log("
|
|
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");
|