@baton-dx/cli 0.3.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -1,13 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import { r as __toESM } from "./chunk-BbwQpWto.mjs";
3
3
  import { a as Ne, c as Ve, d as bt, f as je, g as runMain, h as defineCommand, i as Le, l as We, m as require_dist, o as R, p as Ct, r as Je, s as Re, t as findSourceRoot, u as Ze } from "./context-detection-DqOTnD6_.mjs";
4
- import { $ as SourceParseError, A as discoverProfilesInSourceRepo, B as getIdePlatformTargetDir, C as isLockedProfile, D as resolveProfileChain, E as resolveProfileSupport, F as writeLock, G as getAllAdapters, H as idePlatformRegistry, I as resolveVersion, J as loadLockfile, K as parseFrontmatter, L as cloneGitSource, M as removePlacedFiles, N as generateLock, O as detectLegacyPaths, P as readLock, Q as FileNotFoundError, R as collectProfileSupportPatterns, S as getProfileWeight, T as mergeContentParts, U as isKnownIdePlatform, V as getRegisteredIdePlatforms, W as getAdaptersForKeys, X as loadProjectManifest, Y as loadProfileManifest, Z as KEBAB_CASE_REGEX, _ as mergeMemoryWithWarnings, a as clearIdeCache, b as mergeSkills, c as getDefaultGlobalSource, d as getGlobalSources, et as getAgentConfig, f as removeGlobalSource, g as mergeMemory, h as require_lib, i as computeIntersection, j as findSourceManifest, k as placeFile, l as getGlobalAiTools, m as setGlobalIdePlatforms, n as readProjectPreferences, nt as getAllAgentKeys, o as detectInstalledIdes, p as setGlobalAiTools, q as parseSource, r as writeProjectPreferences, s as addGlobalSource, t as resolvePreferences, tt as getAgentPath, u as getGlobalIdePlatforms, v as mergeRules, w as sortProfilesByWeight, x as mergeSkillsWithWarnings, y as mergeRulesWithWarnings, z as updateGitignore } from "./src-C3-Vz-R7.mjs";
5
- import { n as detectInstalledAgents, t as clearAgentCache } from "./agent-detection-DTiVeO5W.mjs";
4
+ import { $ as loadLockfile, A as resolveProfileChain, B as cloneGitSource, C as mergeSkills, D as sortProfilesByWeight, E as isLockedProfile, F as removePlacedFiles, G as getIdePlatformTargetDir, H as ensureBatonDirGitignored, I as generateLock, J as isKnownIdePlatform, K as getRegisteredIdePlatforms, L as readLock, M as placeFile, N as discoverProfilesInSourceRepo, O as mergeContentParts, P as findSourceManifest, Q as parseSource, R as writeLock, S as mergeRulesWithWarnings, T as getProfileWeight, U as removeGitignoreManagedSection, V as collectComprehensivePatterns, W as updateGitignore, X as getAllAIToolAdapters, Y as getAIToolAdaptersForKeys, Z as parseFrontmatter, _ as require_lib, a as clearIdeCache, at as getAIToolConfig, b as mergeAgentsWithWarnings, c as getBatonHome, d as getGlobalConfigPath, et as loadProfileManifest, f as getGlobalIdePlatforms, g as setGlobalIdePlatforms, h as setGlobalAiTools, i as computeIntersection, it as SourceParseError, j as detectLegacyPaths, k as resolveProfileSupport, l as getDefaultGlobalSource, m as removeGlobalSource, n as readProjectPreferences, nt as KEBAB_CASE_REGEX, o as detectInstalledIdes, ot as getAIToolPath, p as getGlobalSources, q as idePlatformRegistry, r as writeProjectPreferences, rt as FileNotFoundError, s as addGlobalSource, st as getAllAIToolKeys, t as resolvePreferences, tt as loadProjectManifest, u as getGlobalAiTools, v as mergeMemory, w as mergeSkillsWithWarnings, x as mergeRules, y as mergeMemoryWithWarnings, z as resolveVersion } from "./src-D41VR6ro.mjs";
5
+ import { n as detectInstalledAITools, t as clearAIToolCache } from "./ai-tool-detection-CMsBNa9e.mjs";
6
6
  import { d as esm_default } from "./esm-BagM-kVd.mjs";
7
7
  import { access, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
8
8
  import { dirname, isAbsolute, join, relative, resolve } from "node:path";
9
9
  import { fileURLToPath } from "node:url";
10
- import { homedir } from "node:os";
11
10
 
12
11
  //#region src/commands/ai-tools/configure.ts
13
12
  const aiToolsConfigureCommand = defineCommand({
@@ -40,7 +39,7 @@ async function runGlobalMode$1(nonInteractive) {
40
39
  Le("No changes made.");
41
40
  return;
42
41
  }
43
- const options = getAllAdapters().map((adapter) => {
42
+ const options = getAllAIToolAdapters().map((adapter) => {
44
43
  const isSaved = currentTools.includes(adapter.key);
45
44
  return {
46
45
  value: adapter.key,
@@ -126,7 +125,7 @@ async function runProjectMode$1(nonInteractive) {
126
125
  Le("Configuration complete.");
127
126
  return;
128
127
  }
129
- const allAdapters = getAllAdapters();
128
+ const allAdapters = getAllAIToolAdapters();
130
129
  const currentProjectTools = existing?.ai.useGlobal === false ? existing.ai.tools : globalTools;
131
130
  const options = allAdapters.map((adapter) => {
132
131
  const isGlobal = globalTools.includes(adapter.key);
@@ -182,37 +181,37 @@ const aiToolsListCommand = defineCommand({
182
181
  async run({ args }) {
183
182
  if (!args.json) We("Baton - AI Tools");
184
183
  const savedTools = await getGlobalAiTools();
185
- const allAgentKeys = getAllAgentKeys();
186
- const keysToShow = args.all ? allAgentKeys : savedTools.length > 0 ? savedTools : allAgentKeys;
187
- const agentStatuses = await Promise.all(keysToShow.map(async (agentKey) => {
188
- const isSaved = savedTools.includes(agentKey);
184
+ const allAIToolKeys = getAllAIToolKeys();
185
+ const keysToShow = args.all ? allAIToolKeys : savedTools.length > 0 ? savedTools : allAIToolKeys;
186
+ const toolStatuses = await Promise.all(keysToShow.map(async (toolKey) => {
187
+ const isSaved = savedTools.includes(toolKey);
189
188
  let skillCount = 0;
190
189
  let ruleCount = 0;
191
- let agentCount = 0;
190
+ let aiToolConfigCount = 0;
192
191
  let memoryCount = 0;
193
192
  let commandCount = 0;
194
193
  if (isSaved) {
195
- skillCount = await countConfigs(agentKey, "skills", "project");
196
- ruleCount = await countConfigs(agentKey, "rules", "project");
197
- agentCount = await countConfigs(agentKey, "agents", "project");
198
- memoryCount = await countConfigs(agentKey, "memory", "project");
199
- commandCount = await countConfigs(agentKey, "commands", "project");
194
+ skillCount = await countConfigs(toolKey, "skills", "project");
195
+ ruleCount = await countConfigs(toolKey, "rules", "project");
196
+ aiToolConfigCount = await countConfigs(toolKey, "agents", "project");
197
+ memoryCount = await countConfigs(toolKey, "memory", "project");
198
+ commandCount = await countConfigs(toolKey, "commands", "project");
200
199
  }
201
200
  const paths = {
202
- skills: getAgentPath(agentKey, "skills", "project", ""),
203
- rules: getAgentPath(agentKey, "rules", "project", ""),
204
- agents: getAgentPath(agentKey, "agents", "project", ""),
205
- memory: getAgentPath(agentKey, "memory", "project", ""),
206
- commands: getAgentPath(agentKey, "commands", "project", "")
201
+ skills: getAIToolPath(toolKey, "skills", "project", ""),
202
+ rules: getAIToolPath(toolKey, "rules", "project", ""),
203
+ agents: getAIToolPath(toolKey, "agents", "project", ""),
204
+ memory: getAIToolPath(toolKey, "memory", "project", ""),
205
+ commands: getAIToolPath(toolKey, "commands", "project", "")
207
206
  };
208
207
  return {
209
- key: agentKey,
210
- name: getAgentConfig(agentKey).name,
208
+ key: toolKey,
209
+ name: getAIToolConfig(toolKey).name,
211
210
  saved: isSaved,
212
211
  counts: {
213
212
  skills: skillCount,
214
213
  rules: ruleCount,
215
- agents: agentCount,
214
+ agents: aiToolConfigCount,
216
215
  memory: memoryCount,
217
216
  commands: commandCount
218
217
  },
@@ -220,23 +219,23 @@ const aiToolsListCommand = defineCommand({
220
219
  };
221
220
  }));
222
221
  if (args.json) {
223
- console.log(JSON.stringify(agentStatuses, null, 2));
222
+ console.log(JSON.stringify(toolStatuses, null, 2));
224
223
  return;
225
224
  }
226
225
  if (savedTools.length === 0) {
227
226
  R.warn("No AI tools saved in global config.");
228
227
  R.info("Run 'baton ai-tools scan' to detect and save your AI tools.");
229
228
  console.log("");
230
- R.info(`All ${allAgentKeys.length} supported tools:`);
231
- for (const key of allAgentKeys) {
232
- const config = getAgentConfig(key);
229
+ R.info(`All ${allAIToolKeys.length} supported tools:`);
230
+ for (const key of allAIToolKeys) {
231
+ const config = getAIToolConfig(key);
233
232
  console.log(` \x1b[90m- ${config.name}\x1b[0m`);
234
233
  }
235
234
  Le("Run 'baton ai-tools scan' to get started.");
236
235
  return;
237
236
  }
238
237
  console.log(`\nSaved AI tools (${savedTools.length}):\n`);
239
- for (const agent of agentStatuses) {
238
+ for (const agent of toolStatuses) {
240
239
  const statusColor = agent.saved ? "\x1B[32m" : "\x1B[90m";
241
240
  const status = agent.saved ? "✓" : "✗";
242
241
  console.log(`${statusColor}${status} ${agent.name.padEnd(20)}`);
@@ -257,11 +256,11 @@ const aiToolsListCommand = defineCommand({
257
256
  }
258
257
  });
259
258
  /**
260
- * Count config files of a given type for an agent
259
+ * Count config files of a given type for a tool
261
260
  */
262
- async function countConfigs(agentKey, configType, scope) {
261
+ async function countConfigs(toolKey, configType, scope) {
263
262
  try {
264
- const dirPath = getAgentPath(agentKey, configType, scope, "").replace(/{name}.*$/, "").replace(/\/$/, "");
263
+ const dirPath = getAIToolPath(toolKey, configType, scope, "").replace(/{name}.*$/, "").replace(/\/$/, "");
265
264
  if (!(await stat(dirPath)).isDirectory()) return 0;
266
265
  return (await readdir(dirPath)).length;
267
266
  } catch (_error) {
@@ -285,15 +284,15 @@ const aiToolsScanCommand = defineCommand({
285
284
  We("Baton - AI Tool Scanner");
286
285
  const spinner = bt();
287
286
  spinner.start("Scanning for AI tools...");
288
- clearAgentCache();
289
- const detectedAgents = await detectInstalledAgents();
290
- const allAdapters = getAllAdapters();
287
+ clearAIToolCache();
288
+ const detectedAITools = await detectInstalledAITools();
289
+ const allAdapters = getAllAIToolAdapters();
291
290
  const currentTools = await getGlobalAiTools();
292
291
  spinner.stop("Scan complete.");
293
- if (detectedAgents.length > 0) R.success(`Found ${detectedAgents.length} AI tool${detectedAgents.length !== 1 ? "s" : ""} on your system.`);
292
+ if (detectedAITools.length > 0) R.success(`Found ${detectedAITools.length} AI tool${detectedAITools.length !== 1 ? "s" : ""} on your system.`);
294
293
  else R.warn("No AI tools detected on your system.");
295
294
  if (args.yes) {
296
- const detectedKeys = detectedAgents;
295
+ const detectedKeys = detectedAITools;
297
296
  if (detectedKeys.length !== currentTools.length || detectedKeys.some((key) => !currentTools.includes(key))) {
298
297
  await setGlobalAiTools(detectedKeys);
299
298
  R.success(`Saved ${detectedKeys.length} detected tool(s) to global config.`);
@@ -302,7 +301,7 @@ const aiToolsScanCommand = defineCommand({
302
301
  return;
303
302
  }
304
303
  const options = allAdapters.map((adapter) => {
305
- const isDetected = detectedAgents.includes(adapter.key);
304
+ const isDetected = detectedAITools.includes(adapter.key);
306
305
  return {
307
306
  value: adapter.key,
308
307
  label: isDetected ? `${adapter.name} (detected)` : adapter.name
@@ -311,7 +310,7 @@ const aiToolsScanCommand = defineCommand({
311
310
  const selected = await je({
312
311
  message: "Select which AI tools to save:",
313
312
  options,
314
- initialValues: detectedAgents
313
+ initialValues: detectedAITools
315
314
  });
316
315
  if (Ct(selected)) {
317
316
  Le("Scan finished (not saved).");
@@ -421,7 +420,6 @@ function formatIntersectionSummary(intersection) {
421
420
 
422
421
  //#endregion
423
422
  //#region src/commands/config.ts
424
- const CONFIG_FILE = join(homedir(), ".baton", "config.yaml");
425
423
  const VALID_KEYS = [
426
424
  "cache-dir",
427
425
  "default-scope",
@@ -429,16 +427,18 @@ const VALID_KEYS = [
429
427
  "default-tools"
430
428
  ];
431
429
  async function loadConfig() {
430
+ const configFile = getGlobalConfigPath();
432
431
  try {
433
- await access(CONFIG_FILE);
432
+ await access(configFile);
434
433
  } catch {
435
434
  return {};
436
435
  }
437
- return (0, import_dist.parse)(await readFile(CONFIG_FILE, "utf-8"));
436
+ return (0, import_dist.parse)(await readFile(configFile, "utf-8"));
438
437
  }
439
438
  async function saveConfig(config) {
440
- await mkdir(dirname(CONFIG_FILE), { recursive: true });
441
- await writeFile(CONFIG_FILE, (0, import_dist.stringify)(config), "utf-8");
439
+ const configFile = getGlobalConfigPath();
440
+ await mkdir(getBatonHome(), { recursive: true });
441
+ await writeFile(configFile, (0, import_dist.stringify)(config), "utf-8");
442
442
  }
443
443
  async function showDashboard() {
444
444
  We("Baton Dashboard");
@@ -467,7 +467,7 @@ async function showDashboard() {
467
467
  if (resolvedAiTools.length > 0) {
468
468
  const toolNames = resolvedAiTools.map((key) => {
469
469
  try {
470
- return getAgentConfig(key).name;
470
+ return getAIToolConfig(key).name;
471
471
  } catch {
472
472
  return key;
473
473
  }
@@ -485,7 +485,7 @@ async function showDashboard() {
485
485
  if (aiTools.length > 0) {
486
486
  const toolNames = aiTools.map((key) => {
487
487
  try {
488
- return getAgentConfig(key).name;
488
+ return getAIToolConfig(key).name;
489
489
  } catch {
490
490
  return key;
491
491
  }
@@ -727,7 +727,7 @@ const diffCommand = defineCommand({
727
727
  spinner.stop("Configurations merged");
728
728
  spinner.start("Computing tool intersection...");
729
729
  const globalAiTools = await getGlobalAiTools();
730
- const detectedAgents = await detectInstalledAgents();
730
+ const detectedAITools = await detectInstalledAITools();
731
731
  let syncedAiTools;
732
732
  if (globalAiTools.length > 0) {
733
733
  const developerTools = {
@@ -740,13 +740,13 @@ const diffCommand = defineCommand({
740
740
  if (intersection) for (const tool of intersection.aiTools.synced) aggregatedSyncedAi.add(tool);
741
741
  } catch {}
742
742
  syncedAiTools = aggregatedSyncedAi.size > 0 ? [...aggregatedSyncedAi] : [];
743
- } else syncedAiTools = detectedAgents;
743
+ } else syncedAiTools = detectedAITools;
744
744
  if (syncedAiTools.length === 0) {
745
745
  spinner.stop("No AI tools in intersection");
746
746
  Ne("No AI tools match. Run `baton ai-tools scan`.");
747
747
  process.exit(1);
748
748
  }
749
- const adapters = getAdaptersForKeys(syncedAiTools);
749
+ const adapters = getAIToolAdaptersForKeys(syncedAiTools);
750
750
  spinner.stop(`Comparing for: ${syncedAiTools.join(", ")}`);
751
751
  spinner.start("Comparing remote sources with placed files...");
752
752
  const diffs = [];
@@ -1339,7 +1339,7 @@ async function promptFirstRunPreferences(projectRoot, nonInteractive) {
1339
1339
  let aiTools = [];
1340
1340
  if (aiMode === "customize") {
1341
1341
  const globalTools = await getGlobalAiTools();
1342
- const allAdapters = getAllAdapters();
1342
+ const allAdapters = getAllAIToolAdapters();
1343
1343
  const selected = await je({
1344
1344
  message: "Select AI tools for this project:",
1345
1345
  options: allAdapters.map((adapter) => ({
@@ -1650,7 +1650,22 @@ const initCommand = defineCommand({
1650
1650
  }
1651
1651
  }
1652
1652
  await showProfileIntersections(profileSources);
1653
- const yamlContent = (0, import_dist.stringify)({ profiles: profileSources.map((source) => ({ source })) });
1653
+ let gitignoreSetting = true;
1654
+ if (isInteractive) {
1655
+ const shouldGitignore = await Re({
1656
+ message: "Add synced AI tool and IDE config files to .gitignore?",
1657
+ initialValue: true
1658
+ });
1659
+ if (Ct(shouldGitignore)) {
1660
+ Ne("Setup cancelled.");
1661
+ process.exit(0);
1662
+ }
1663
+ gitignoreSetting = shouldGitignore;
1664
+ }
1665
+ const yamlContent = (0, import_dist.stringify)({
1666
+ profiles: profileSources.map((source) => ({ source })),
1667
+ gitignore: gitignoreSetting
1668
+ });
1654
1669
  spinner.start("Creating baton.yaml...");
1655
1670
  await writeFile(join(cwd, "baton.yaml"), yamlContent, "utf-8");
1656
1671
  spinner.stop("✅ Created baton.yaml");
@@ -1660,10 +1675,11 @@ const initCommand = defineCommand({
1660
1675
  try {
1661
1676
  gitignoreContent = await readFile(gitignorePath, "utf-8");
1662
1677
  } catch (_error) {}
1663
- if (!gitignoreContent.includes(".baton/")) {
1664
- await writeFile(gitignorePath, gitignoreContent ? `${gitignoreContent}\n\n# Baton cache\n.baton/\n` : "# Baton cache\n.baton/\n", "utf-8");
1665
- spinner.stop("✅ Added .baton/ to .gitignore");
1666
- } else spinner.stop("✅ .gitignore already contains .baton/");
1678
+ if (!gitignoreContent.includes(".baton/")) await writeFile(gitignorePath, gitignoreContent ? `${gitignoreContent}\n\n# Baton cache\n.baton/\n` : "# Baton cache\n.baton/\n", "utf-8");
1679
+ if (gitignoreSetting) {
1680
+ await updateGitignore(cwd, collectComprehensivePatterns({ fileTargets: [] }));
1681
+ spinner.stop("✅ Updated .gitignore with managed file patterns");
1682
+ } else spinner.stop("✅ Added .baton/ to .gitignore");
1667
1683
  spinner.start("Creating .baton directory...");
1668
1684
  try {
1669
1685
  await mkdir(join(cwd, ".baton"), { recursive: true });
@@ -1695,8 +1711,8 @@ async function autoScanAiTools(spinner, isInteractive) {
1695
1711
  return;
1696
1712
  }
1697
1713
  spinner.start("Scanning for installed AI tools...");
1698
- clearAgentCache();
1699
- const detectedTools = await detectInstalledAgents();
1714
+ clearAIToolCache();
1715
+ const detectedTools = await detectInstalledAITools();
1700
1716
  spinner.stop(detectedTools.length > 0 ? `Found ${detectedTools.length} AI tool${detectedTools.length !== 1 ? "s" : ""}: ${detectedTools.join(", ")}` : "No AI tools detected.");
1701
1717
  if (detectedTools.length === 0) {
1702
1718
  R.warn("No AI tools detected. You can run 'baton ai-tools scan' later.");
@@ -2038,7 +2054,7 @@ async function handleConfigureAiTools(cwd) {
2038
2054
  R.success("Project configured to use global AI tools.");
2039
2055
  return;
2040
2056
  }
2041
- const allAdapters = getAllAdapters();
2057
+ const allAdapters = getAllAIToolAdapters();
2042
2058
  const currentProjectTools = existing?.ai.useGlobal === false ? existing.ai.tools : globalTools;
2043
2059
  const options = allAdapters.map((adapter) => ({
2044
2060
  value: adapter.key,
@@ -2132,6 +2148,31 @@ async function handleConfigureIdes(cwd) {
2132
2148
  });
2133
2149
  R.success(`Project configured with ${selectedKeys.length} IDE platform(s).`);
2134
2150
  }
2151
+ async function handleConfigureGitignore(cwd) {
2152
+ const manifestPath = join(cwd, "baton.yaml");
2153
+ const manifest = await loadProjectManifestSafe(cwd);
2154
+ if (!manifest) {
2155
+ R.error("Could not load baton.yaml");
2156
+ return;
2157
+ }
2158
+ const currentSetting = manifest.gitignore !== false;
2159
+ R.info(currentSetting ? "Currently: synced files ARE gitignored" : "Currently: synced files are NOT gitignored (committed to repo)");
2160
+ const newSetting = await Re({
2161
+ message: "Add synced AI tool and IDE config files to .gitignore?",
2162
+ initialValue: currentSetting
2163
+ });
2164
+ if (Ct(newSetting)) {
2165
+ R.warn("Cancelled.");
2166
+ return;
2167
+ }
2168
+ if (newSetting === currentSetting) {
2169
+ R.info("No change.");
2170
+ return;
2171
+ }
2172
+ manifest.gitignore = newSetting;
2173
+ await writeFile(manifestPath, (0, import_dist.stringify)(manifest), "utf-8");
2174
+ R.success(newSetting ? "Enabled .gitignore management. Run 'baton sync' to update." : "Disabled .gitignore management. Run 'baton sync' to clean up.");
2175
+ }
2135
2176
  const manageCommand = defineCommand({
2136
2177
  meta: {
2137
2178
  name: "manage",
@@ -2174,6 +2215,11 @@ const manageCommand = defineCommand({
2174
2215
  label: "Configure IDEs for this project",
2175
2216
  hint: "Choose which IDEs to sync"
2176
2217
  },
2218
+ {
2219
+ value: "configure-gitignore",
2220
+ label: "Configure .gitignore",
2221
+ hint: "Choose whether synced files are gitignored"
2222
+ },
2177
2223
  {
2178
2224
  value: "remove-baton",
2179
2225
  label: "Remove Baton",
@@ -2209,6 +2255,10 @@ const manageCommand = defineCommand({
2209
2255
  console.log("");
2210
2256
  await handleConfigureIdes(cwd);
2211
2257
  console.log("");
2258
+ } else if (action === "configure-gitignore") {
2259
+ console.log("");
2260
+ await handleConfigureGitignore(cwd);
2261
+ console.log("");
2212
2262
  } else if (action === "remove-baton") {
2213
2263
  console.log("");
2214
2264
  if (await handleRemoveBaton(cwd)) {
@@ -2229,8 +2279,8 @@ const profileCommand = defineCommand({
2229
2279
  description: "Manage profiles (create, list, remove)"
2230
2280
  },
2231
2281
  subCommands: {
2232
- create: () => import("./create-BOcW-DBk.mjs").then((m) => m.createCommand),
2233
- list: () => import("./list-DmzVXCNF.mjs").then((m) => m.profileListCommand),
2282
+ create: () => import("./create-SYKl8g0B.mjs").then((m) => m.createCommand),
2283
+ list: () => import("./list-UzuMEqbc.mjs").then((m) => m.profileListCommand),
2234
2284
  remove: () => import("./remove-BBs6Mv8t.mjs").then((m) => m.profileRemoveCommand)
2235
2285
  }
2236
2286
  });
@@ -2688,35 +2738,25 @@ async function copyDirectoryRecursive(sourceDir, targetDir) {
2688
2738
  return placed;
2689
2739
  }
2690
2740
  /**
2691
- * Collect profile-support-based .gitignore patterns and update .gitignore.
2692
- * Uses ALL tools the profile supports (not just the developer's intersection)
2693
- * to ensure consistent .gitignore across all team members.
2741
+ * Handle .gitignore update based on the project manifest's gitignore setting.
2742
+ *
2743
+ * When gitignore is enabled (default): writes comprehensive patterns for ALL
2744
+ * known AI tools and IDE platforms to ensure stable, dev-independent content.
2745
+ * When disabled: removes any existing managed section.
2746
+ * Always ensures .baton/ is gitignored regardless of setting.
2694
2747
  */
2695
- async function updateGitignorePatterns(params) {
2696
- const { allIntersections, adapters, ideMap, mergedSkills, mergedRules, mergedMemory, mergedCommandCount, fileMap, projectRoot, spinner } = params;
2697
- const profileSupportedAiTools = /* @__PURE__ */ new Set();
2698
- const profileSupportedIdePlatforms = /* @__PURE__ */ new Set();
2699
- if (allIntersections) for (const intersection of allIntersections.values()) {
2700
- for (const tool of intersection.aiTools.synced) profileSupportedAiTools.add(tool);
2701
- for (const tool of intersection.aiTools.unavailable) profileSupportedAiTools.add(tool);
2702
- for (const plat of intersection.idePlatforms.synced) profileSupportedIdePlatforms.add(plat);
2703
- for (const plat of intersection.idePlatforms.unavailable) profileSupportedIdePlatforms.add(plat);
2704
- }
2705
- else {
2706
- for (const adapter of adapters) profileSupportedAiTools.add(adapter.key);
2707
- for (const entry of ideMap.values()) profileSupportedIdePlatforms.add(entry.ideKey);
2708
- }
2709
- const hasContent = mergedSkills.length > 0 || mergedRules.length > 0 || mergedMemory.length > 0 || mergedCommandCount > 0;
2710
- const gitignorePatterns = collectProfileSupportPatterns({
2711
- profileAiTools: [...profileSupportedAiTools],
2712
- profileIdePlatforms: [...profileSupportedIdePlatforms],
2713
- fileTargets: [...fileMap.values()].map((f) => f.target),
2714
- hasContent
2715
- });
2716
- if (gitignorePatterns.length > 0) {
2748
+ async function handleGitignoreUpdate(params) {
2749
+ const { projectManifest, fileMap, projectRoot, spinner } = params;
2750
+ const gitignoreEnabled = projectManifest.gitignore !== false;
2751
+ await ensureBatonDirGitignored(projectRoot);
2752
+ if (gitignoreEnabled) {
2717
2753
  spinner.start("Updating .gitignore...");
2718
- const updated = await updateGitignore(projectRoot, gitignorePatterns);
2719
- spinner.stop(updated ? "Updated .gitignore with synced patterns" : ".gitignore already up to date");
2754
+ const updated = await updateGitignore(projectRoot, collectComprehensivePatterns({ fileTargets: [...fileMap.values()].map((f) => f.target) }));
2755
+ spinner.stop(updated ? "Updated .gitignore with managed patterns" : ".gitignore already up to date");
2756
+ } else {
2757
+ spinner.start("Checking .gitignore...");
2758
+ const removed = await removeGitignoreManagedSection(projectRoot);
2759
+ spinner.stop(removed ? "Removed managed section from .gitignore" : ".gitignore unchanged");
2720
2760
  }
2721
2761
  }
2722
2762
  /**
@@ -2893,6 +2933,9 @@ const syncCommand = defineCommand({
2893
2933
  const rulesResult = mergeRulesWithWarnings(weightSortedProfiles);
2894
2934
  const mergedRules = rulesResult.rules;
2895
2935
  allWeightWarnings.push(...rulesResult.warnings);
2936
+ const agentsResult = mergeAgentsWithWarnings(weightSortedProfiles);
2937
+ const mergedAgents = agentsResult.agents;
2938
+ allWeightWarnings.push(...agentsResult.warnings);
2896
2939
  const memoryResult = mergeMemoryWithWarnings(weightSortedProfiles);
2897
2940
  const mergedMemory = memoryResult.entries;
2898
2941
  allWeightWarnings.push(...memoryResult.warnings);
@@ -2991,11 +3034,11 @@ const syncCommand = defineCommand({
2991
3034
  }
2992
3035
  }
2993
3036
  const mergedIdeCount = ideMap.size;
2994
- spinner.stop(`Merged: ${mergedSkills.length} skills, ${mergedRules.length} rules, ${mergedMemory.length} memory files, ${mergedCommandCount} commands, ${mergedFileCount} files, ${mergedIdeCount} IDE configs`);
3037
+ spinner.stop(`Merged: ${mergedSkills.length} skills, ${mergedRules.length} rules, ${mergedAgents.length} agents, ${mergedMemory.length} memory files, ${mergedCommandCount} commands, ${mergedFileCount} files, ${mergedIdeCount} IDE configs`);
2995
3038
  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.`);
2996
3039
  spinner.start("Computing tool intersection...");
2997
3040
  const prefs = await resolvePreferences(projectRoot);
2998
- const detectedAgents = await detectInstalledAgents();
3041
+ const detectedAITools = await detectInstalledAITools();
2999
3042
  if (verbose) {
3000
3043
  R.info(`AI tools: ${prefs.ai.tools.join(", ") || "(none)"} (from ${prefs.ai.source} preferences)`);
3001
3044
  R.info(`IDE platforms: ${prefs.ide.platforms.join(", ") || "(none)"} (from ${prefs.ide.source} preferences)`);
@@ -3022,14 +3065,14 @@ const syncCommand = defineCommand({
3022
3065
  syncedAiTools = aggregatedSyncedAi.size > 0 ? [...aggregatedSyncedAi] : [];
3023
3066
  syncedIdePlatforms = [...aggregatedSyncedIde];
3024
3067
  } else {
3025
- syncedAiTools = detectedAgents;
3068
+ syncedAiTools = detectedAITools;
3026
3069
  syncedIdePlatforms = null;
3027
- if (detectedAgents.length > 0) {
3070
+ if (detectedAITools.length > 0) {
3028
3071
  R.warn("No AI tools configured. Run `baton ai-tools scan` to configure your tools.");
3029
- R.info(`Falling back to detected tools: ${detectedAgents.join(", ")}`);
3072
+ R.info(`Falling back to detected tools: ${detectedAITools.join(", ")}`);
3030
3073
  }
3031
3074
  }
3032
- if (syncedAiTools.length === 0 && detectedAgents.length === 0) {
3075
+ if (syncedAiTools.length === 0 && detectedAITools.length === 0) {
3033
3076
  spinner.stop("No AI tools available");
3034
3077
  Ne("No AI tools found. Install an AI coding tool first.");
3035
3078
  process.exit(1);
@@ -3058,7 +3101,7 @@ const syncCommand = defineCommand({
3058
3101
  }
3059
3102
  } else spinner.stop("No legacy files found");
3060
3103
  spinner.start("Processing configurations...");
3061
- const adapters = getAdaptersForKeys(syncedAiTools);
3104
+ const adapters = getAIToolAdaptersForKeys(syncedAiTools);
3062
3105
  const placementConfig = {
3063
3106
  mode: "copy",
3064
3107
  projectRoot
@@ -3173,6 +3216,7 @@ const syncCommand = defineCommand({
3173
3216
  if (!dryRun && syncAi) for (const adapter of adapters) {
3174
3217
  if (verbose) R.step(`[${adapter.key}] Placing rules...`);
3175
3218
  for (const ruleEntry of mergedRules) try {
3219
+ const ruleName = ruleEntry.name.replace(/\.md$/, "");
3176
3220
  const isUniversal = ruleEntry.agents.length === 0;
3177
3221
  const isForThisAdapter = ruleEntry.agents.includes(adapter.key);
3178
3222
  if (!isUniversal && !isForThisAdapter) continue;
@@ -3181,7 +3225,7 @@ const syncCommand = defineCommand({
3181
3225
  spinner.message(`Warning: Could not resolve local path for profile ${ruleEntry.profileName}`);
3182
3226
  continue;
3183
3227
  }
3184
- const ruleSourcePath = resolve(profileDir, "ai", "rules", isUniversal ? "universal" : ruleEntry.agents[0], `${ruleEntry.name}.md`);
3228
+ const ruleSourcePath = resolve(profileDir, "ai", "rules", isUniversal ? "universal" : ruleEntry.agents[0], `${ruleName}.md`);
3185
3229
  let rawContent;
3186
3230
  try {
3187
3231
  rawContent = await readFile(ruleSourcePath, "utf-8");
@@ -3191,12 +3235,12 @@ const syncCommand = defineCommand({
3191
3235
  }
3192
3236
  const parsed = parseFrontmatter(rawContent);
3193
3237
  const ruleFile = {
3194
- name: ruleEntry.name,
3238
+ name: ruleName,
3195
3239
  content: rawContent,
3196
3240
  frontmatter: Object.keys(parsed.data).length > 0 ? parsed.data : void 0
3197
3241
  };
3198
3242
  const transformed = adapter.transformRule(ruleFile);
3199
- const targetPath = adapter.getPath("rules", "project", ruleEntry.name);
3243
+ const targetPath = adapter.getPath("rules", "project", ruleName);
3200
3244
  const absolutePath = targetPath.startsWith("/") ? targetPath : resolve(projectRoot, targetPath);
3201
3245
  const existing = contentAccumulator.get(absolutePath);
3202
3246
  if (existing) {
@@ -3206,7 +3250,7 @@ const syncCommand = defineCommand({
3206
3250
  parts: [transformed.content],
3207
3251
  adapter,
3208
3252
  type: "rules",
3209
- name: ruleEntry.name,
3253
+ name: ruleName,
3210
3254
  profiles: new Set([ruleEntry.profileName])
3211
3255
  });
3212
3256
  } catch (error) {
@@ -3214,6 +3258,53 @@ const syncCommand = defineCommand({
3214
3258
  stats.errors++;
3215
3259
  }
3216
3260
  }
3261
+ if (!dryRun && syncAi) for (const adapter of adapters) {
3262
+ if (verbose) R.step(`[${adapter.key}] Placing agents...`);
3263
+ for (const agentEntry of mergedAgents) try {
3264
+ const agentName = agentEntry.name.replace(/\.md$/, "");
3265
+ const isUniversal = agentEntry.agents.length === 0;
3266
+ const isForThisAdapter = agentEntry.agents.includes(adapter.key);
3267
+ if (!isUniversal && !isForThisAdapter) continue;
3268
+ const profileDir = profileLocalPaths.get(agentEntry.profileName);
3269
+ if (!profileDir) {
3270
+ spinner.message(`Warning: Could not resolve local path for profile ${agentEntry.profileName}`);
3271
+ continue;
3272
+ }
3273
+ const agentSourcePath = resolve(profileDir, "ai", "agents", isUniversal ? "universal" : agentEntry.agents[0], `${agentName}.md`);
3274
+ let rawContent;
3275
+ try {
3276
+ rawContent = await readFile(agentSourcePath, "utf-8");
3277
+ } catch {
3278
+ spinner.message(`Warning: Could not read agent file: ${agentSourcePath}`);
3279
+ continue;
3280
+ }
3281
+ const parsed = parseFrontmatter(rawContent);
3282
+ const frontmatter = Object.keys(parsed.data).length > 0 ? parsed.data : { name: agentName };
3283
+ const agentFile = {
3284
+ name: agentName,
3285
+ content: rawContent,
3286
+ description: frontmatter.description,
3287
+ frontmatter
3288
+ };
3289
+ const transformed = adapter.transformAgent(agentFile);
3290
+ const targetPath = adapter.getPath("agents", "project", agentName);
3291
+ const absolutePath = targetPath.startsWith("/") ? targetPath : resolve(projectRoot, targetPath);
3292
+ const existing = contentAccumulator.get(absolutePath);
3293
+ if (existing) {
3294
+ existing.parts.push(transformed.content);
3295
+ existing.profiles.add(agentEntry.profileName);
3296
+ } else contentAccumulator.set(absolutePath, {
3297
+ parts: [transformed.content],
3298
+ adapter,
3299
+ type: "agents",
3300
+ name: agentName,
3301
+ profiles: new Set([agentEntry.profileName])
3302
+ });
3303
+ } catch (error) {
3304
+ spinner.message(`Error placing agent ${agentEntry.name} for ${adapter.name}: ${error}`);
3305
+ stats.errors++;
3306
+ }
3307
+ }
3217
3308
  if (!dryRun && syncAi) for (const [absolutePath, entry] of contentAccumulator) try {
3218
3309
  const combinedContent = entry.parts.join("\n\n");
3219
3310
  const result = await placeFile(combinedContent, entry.adapter, entry.type, "project", entry.name, placementConfig);
@@ -3327,14 +3418,8 @@ const syncCommand = defineCommand({
3327
3418
  stats.errors++;
3328
3419
  }
3329
3420
  spinner.stop(dryRun ? `Would place files for ${adapters.length} agent(s)` : `Placed ${stats.created} file(s) for ${adapters.length} agent(s)`);
3330
- if (!dryRun) await updateGitignorePatterns({
3331
- allIntersections,
3332
- adapters,
3333
- ideMap,
3334
- mergedSkills,
3335
- mergedRules,
3336
- mergedMemory,
3337
- mergedCommandCount,
3421
+ if (!dryRun) await handleGitignoreUpdate({
3422
+ projectManifest,
3338
3423
  fileMap,
3339
3424
  projectRoot,
3340
3425
  spinner
@@ -3359,6 +3444,7 @@ const syncCommand = defineCommand({
3359
3444
  if (syncAi) {
3360
3445
  parts.push(` • ${mergedSkills.length} skills`);
3361
3446
  parts.push(` • ${mergedRules.length} rules`);
3447
+ parts.push(` • ${mergedAgents.length} agents`);
3362
3448
  parts.push(` • ${mergedMemory.length} memory files`);
3363
3449
  parts.push(` • ${mergedCommandCount} commands`);
3364
3450
  }