@baton-dx/cli 0.3.0 → 0.3.2

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.
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import { r as __toESM } from "./chunk-BbwQpWto.mjs";
3
3
  import { a as Ne, h as defineCommand, i as Le, l as We, p as Ct, t as findSourceRoot, u as Ze } from "./context-detection-DqOTnD6_.mjs";
4
- import { X as KEBAB_CASE_REGEX, h as require_lib } from "./src-BgiJfm14.mjs";
4
+ import { _ as require_lib, tt as KEBAB_CASE_REGEX } from "./src-BCGnnv5D.mjs";
5
5
  import "./agent-detection-DTiVeO5W.mjs";
6
6
  import "./esm-BagM-kVd.mjs";
7
7
  import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
@@ -79,4 +79,4 @@ async function copyProfileTemplate(sourceDir, targetDir, variables) {
79
79
 
80
80
  //#endregion
81
81
  export { createCommand };
82
- //# sourceMappingURL=create-BG_VVOTI.mjs.map
82
+ //# sourceMappingURL=create-BkpEXaht.mjs.map
@@ -1 +1 @@
1
- {"version":3,"file":"create-BG_VVOTI.mjs","names":["p.text","p.isCancel","Handlebars"],"sources":["../src/commands/profile/create.ts"],"sourcesContent":["import { mkdir, readFile, readdir, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { KEBAB_CASE_REGEX } from \"@baton-dx/core\";\nimport * as p from \"@clack/prompts\";\nimport { defineCommand } from \"citty\";\nimport Handlebars from \"handlebars\";\nimport { findSourceRoot } from \"../../utils/context-detection.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nexport const createCommand = defineCommand({\n meta: {\n name: \"create\",\n description: \"Create a new profile in your source repository\",\n },\n args: {\n name: {\n type: \"positional\",\n description: \"Profile name (kebab-case)\",\n required: false,\n },\n },\n async run({ args }) {\n p.intro(\"Create Profile\");\n\n // Check for baton.source.yaml in current or parent directories\n const sourceRoot = await findSourceRoot();\n if (!sourceRoot) {\n p.cancel(\"This command must be run inside a source directory (baton.source.yaml not found)\");\n process.exit(1);\n }\n\n // Get profile name — from argument or wizard prompt\n let name = args.name as string | undefined;\n\n if (!name) {\n const nameInput = await p.text({\n message: \"Profile name (kebab-case)\",\n placeholder: \"e.g., backend, frontend, my-profile\",\n validate(value) {\n if (!value || value.trim().length === 0) {\n return \"Profile name is required\";\n }\n if (!KEBAB_CASE_REGEX.test(value.trim())) {\n return \"Profile name must be in kebab-case (e.g., my-profile, backend, frontend)\";\n }\n },\n });\n\n if (p.isCancel(nameInput)) {\n p.cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n name = (nameInput as string).trim();\n }\n\n // Validate name format (kebab-case)\n if (!KEBAB_CASE_REGEX.test(name)) {\n p.cancel(\"Profile name must be in kebab-case (e.g., my-profile, backend, frontend)\");\n process.exit(1);\n }\n\n // Check if profile already exists in profiles/ directory\n const targetDir = join(sourceRoot, \"profiles\", name);\n try {\n await readdir(targetDir);\n p.cancel(`Profile '${name}' already exists in profiles/${name}/`);\n process.exit(1);\n } catch {\n // Directory doesn't exist - good to proceed\n }\n\n // Create profile directory\n await mkdir(targetDir, { recursive: true });\n\n // Copy minimal template files\n const templateDir = join(__dirname, \"templates\", \"profile\", \"minimal\");\n await copyProfileTemplate(templateDir, targetDir, { name });\n\n p.outro(`Profile '${name}' created in profiles/${name}/`);\n },\n});\n\n/**\n * Recursively copy profile template with variable substitution\n */\nasync function copyProfileTemplate(\n sourceDir: string,\n targetDir: string,\n variables: { name: string },\n): Promise<void> {\n const entries = await readdir(sourceDir, { withFileTypes: true });\n\n for (const entry of entries) {\n const sourcePath = join(sourceDir, entry.name);\n const targetPath = join(targetDir, entry.name);\n\n if (entry.isDirectory()) {\n await mkdir(targetPath, { recursive: true });\n await copyProfileTemplate(sourcePath, targetPath, variables);\n } else {\n // Read file content\n const content = await readFile(sourcePath, \"utf-8\");\n\n // Apply Handlebars substitution for text files\n const processed = Handlebars.compile(content, { noEscape: true })(variables);\n\n // Write processed content\n await writeFile(targetPath, processed, \"utf-8\");\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;AASA,MAAM,YAAY,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;AAEzD,MAAa,gBAAgB,cAAc;CACzC,MAAM;EACJ,MAAM;EACN,aAAa;EACd;CACD,MAAM,EACJ,MAAM;EACJ,MAAM;EACN,aAAa;EACb,UAAU;EACX,EACF;CACD,MAAM,IAAI,EAAE,QAAQ;AAClB,KAAQ,iBAAiB;EAGzB,MAAM,aAAa,MAAM,gBAAgB;AACzC,MAAI,CAAC,YAAY;AACf,MAAS,mFAAmF;AAC5F,WAAQ,KAAK,EAAE;;EAIjB,IAAI,OAAO,KAAK;AAEhB,MAAI,CAAC,MAAM;GACT,MAAM,YAAY,MAAMA,GAAO;IAC7B,SAAS;IACT,aAAa;IACb,SAAS,OAAO;AACd,SAAI,CAAC,SAAS,MAAM,MAAM,CAAC,WAAW,EACpC,QAAO;AAET,SAAI,CAAC,iBAAiB,KAAK,MAAM,MAAM,CAAC,CACtC,QAAO;;IAGZ,CAAC;AAEF,OAAIC,GAAW,UAAU,EAAE;AACzB,OAAS,aAAa;AACtB,YAAQ,KAAK,EAAE;;AAGjB,UAAQ,UAAqB,MAAM;;AAIrC,MAAI,CAAC,iBAAiB,KAAK,KAAK,EAAE;AAChC,MAAS,2EAA2E;AACpF,WAAQ,KAAK,EAAE;;EAIjB,MAAM,YAAY,KAAK,YAAY,YAAY,KAAK;AACpD,MAAI;AACF,SAAM,QAAQ,UAAU;AACxB,MAAS,YAAY,KAAK,+BAA+B,KAAK,GAAG;AACjE,WAAQ,KAAK,EAAE;UACT;AAKR,QAAM,MAAM,WAAW,EAAE,WAAW,MAAM,CAAC;AAI3C,QAAM,oBADc,KAAK,WAAW,aAAa,WAAW,UAAU,EAC/B,WAAW,EAAE,MAAM,CAAC;AAE3D,KAAQ,YAAY,KAAK,wBAAwB,KAAK,GAAG;;CAE5D,CAAC;;;;AAKF,eAAe,oBACb,WACA,WACA,WACe;CACf,MAAM,UAAU,MAAM,QAAQ,WAAW,EAAE,eAAe,MAAM,CAAC;AAEjE,MAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,aAAa,KAAK,WAAW,MAAM,KAAK;EAC9C,MAAM,aAAa,KAAK,WAAW,MAAM,KAAK;AAE9C,MAAI,MAAM,aAAa,EAAE;AACvB,SAAM,MAAM,YAAY,EAAE,WAAW,MAAM,CAAC;AAC5C,SAAM,oBAAoB,YAAY,YAAY,UAAU;SACvD;GAEL,MAAM,UAAU,MAAM,SAAS,YAAY,QAAQ;AAMnD,SAAM,UAAU,YAHEC,mBAAW,QAAQ,SAAS,EAAE,UAAU,MAAM,CAAC,CAAC,UAAU,EAGrC,QAAQ"}
1
+ {"version":3,"file":"create-BkpEXaht.mjs","names":["p.text","p.isCancel","Handlebars"],"sources":["../src/commands/profile/create.ts"],"sourcesContent":["import { mkdir, readFile, readdir, writeFile } from \"node:fs/promises\";\nimport { dirname, join } from \"node:path\";\nimport { fileURLToPath } from \"node:url\";\nimport { KEBAB_CASE_REGEX } from \"@baton-dx/core\";\nimport * as p from \"@clack/prompts\";\nimport { defineCommand } from \"citty\";\nimport Handlebars from \"handlebars\";\nimport { findSourceRoot } from \"../../utils/context-detection.js\";\n\nconst __dirname = dirname(fileURLToPath(import.meta.url));\n\nexport const createCommand = defineCommand({\n meta: {\n name: \"create\",\n description: \"Create a new profile in your source repository\",\n },\n args: {\n name: {\n type: \"positional\",\n description: \"Profile name (kebab-case)\",\n required: false,\n },\n },\n async run({ args }) {\n p.intro(\"Create Profile\");\n\n // Check for baton.source.yaml in current or parent directories\n const sourceRoot = await findSourceRoot();\n if (!sourceRoot) {\n p.cancel(\"This command must be run inside a source directory (baton.source.yaml not found)\");\n process.exit(1);\n }\n\n // Get profile name — from argument or wizard prompt\n let name = args.name as string | undefined;\n\n if (!name) {\n const nameInput = await p.text({\n message: \"Profile name (kebab-case)\",\n placeholder: \"e.g., backend, frontend, my-profile\",\n validate(value) {\n if (!value || value.trim().length === 0) {\n return \"Profile name is required\";\n }\n if (!KEBAB_CASE_REGEX.test(value.trim())) {\n return \"Profile name must be in kebab-case (e.g., my-profile, backend, frontend)\";\n }\n },\n });\n\n if (p.isCancel(nameInput)) {\n p.cancel(\"Cancelled.\");\n process.exit(0);\n }\n\n name = (nameInput as string).trim();\n }\n\n // Validate name format (kebab-case)\n if (!KEBAB_CASE_REGEX.test(name)) {\n p.cancel(\"Profile name must be in kebab-case (e.g., my-profile, backend, frontend)\");\n process.exit(1);\n }\n\n // Check if profile already exists in profiles/ directory\n const targetDir = join(sourceRoot, \"profiles\", name);\n try {\n await readdir(targetDir);\n p.cancel(`Profile '${name}' already exists in profiles/${name}/`);\n process.exit(1);\n } catch {\n // Directory doesn't exist - good to proceed\n }\n\n // Create profile directory\n await mkdir(targetDir, { recursive: true });\n\n // Copy minimal template files\n const templateDir = join(__dirname, \"templates\", \"profile\", \"minimal\");\n await copyProfileTemplate(templateDir, targetDir, { name });\n\n p.outro(`Profile '${name}' created in profiles/${name}/`);\n },\n});\n\n/**\n * Recursively copy profile template with variable substitution\n */\nasync function copyProfileTemplate(\n sourceDir: string,\n targetDir: string,\n variables: { name: string },\n): Promise<void> {\n const entries = await readdir(sourceDir, { withFileTypes: true });\n\n for (const entry of entries) {\n const sourcePath = join(sourceDir, entry.name);\n const targetPath = join(targetDir, entry.name);\n\n if (entry.isDirectory()) {\n await mkdir(targetPath, { recursive: true });\n await copyProfileTemplate(sourcePath, targetPath, variables);\n } else {\n // Read file content\n const content = await readFile(sourcePath, \"utf-8\");\n\n // Apply Handlebars substitution for text files\n const processed = Handlebars.compile(content, { noEscape: true })(variables);\n\n // Write processed content\n await writeFile(targetPath, processed, \"utf-8\");\n }\n }\n}\n"],"mappings":";;;;;;;;;;;;AASA,MAAM,YAAY,QAAQ,cAAc,OAAO,KAAK,IAAI,CAAC;AAEzD,MAAa,gBAAgB,cAAc;CACzC,MAAM;EACJ,MAAM;EACN,aAAa;EACd;CACD,MAAM,EACJ,MAAM;EACJ,MAAM;EACN,aAAa;EACb,UAAU;EACX,EACF;CACD,MAAM,IAAI,EAAE,QAAQ;AAClB,KAAQ,iBAAiB;EAGzB,MAAM,aAAa,MAAM,gBAAgB;AACzC,MAAI,CAAC,YAAY;AACf,MAAS,mFAAmF;AAC5F,WAAQ,KAAK,EAAE;;EAIjB,IAAI,OAAO,KAAK;AAEhB,MAAI,CAAC,MAAM;GACT,MAAM,YAAY,MAAMA,GAAO;IAC7B,SAAS;IACT,aAAa;IACb,SAAS,OAAO;AACd,SAAI,CAAC,SAAS,MAAM,MAAM,CAAC,WAAW,EACpC,QAAO;AAET,SAAI,CAAC,iBAAiB,KAAK,MAAM,MAAM,CAAC,CACtC,QAAO;;IAGZ,CAAC;AAEF,OAAIC,GAAW,UAAU,EAAE;AACzB,OAAS,aAAa;AACtB,YAAQ,KAAK,EAAE;;AAGjB,UAAQ,UAAqB,MAAM;;AAIrC,MAAI,CAAC,iBAAiB,KAAK,KAAK,EAAE;AAChC,MAAS,2EAA2E;AACpF,WAAQ,KAAK,EAAE;;EAIjB,MAAM,YAAY,KAAK,YAAY,YAAY,KAAK;AACpD,MAAI;AACF,SAAM,QAAQ,UAAU;AACxB,MAAS,YAAY,KAAK,+BAA+B,KAAK,GAAG;AACjE,WAAQ,KAAK,EAAE;UACT;AAKR,QAAM,MAAM,WAAW,EAAE,WAAW,MAAM,CAAC;AAI3C,QAAM,oBADc,KAAK,WAAW,aAAa,WAAW,UAAU,EAC/B,WAAW,EAAE,MAAM,CAAC;AAE3D,KAAQ,YAAY,KAAK,wBAAwB,KAAK,GAAG;;CAE5D,CAAC;;;;AAKF,eAAe,oBACb,WACA,WACA,WACe;CACf,MAAM,UAAU,MAAM,QAAQ,WAAW,EAAE,eAAe,MAAM,CAAC;AAEjE,MAAK,MAAM,SAAS,SAAS;EAC3B,MAAM,aAAa,KAAK,WAAW,MAAM,KAAK;EAC9C,MAAM,aAAa,KAAK,WAAW,MAAM,KAAK;AAE9C,MAAI,MAAM,aAAa,EAAE;AACvB,SAAM,MAAM,YAAY,EAAE,WAAW,MAAM,CAAC;AAC5C,SAAM,oBAAoB,YAAY,YAAY,UAAU;SACvD;GAEL,MAAM,UAAU,MAAM,SAAS,YAAY,QAAQ;AAMnD,SAAM,UAAU,YAHEC,mBAAW,QAAQ,SAAS,EAAE,UAAU,MAAM,CAAC,CAAC,UAAU,EAGrC,QAAQ"}
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 getAgentConfig, A as discoverProfilesInSourceRepo, B as getRegisteredIdePlatforms, C as isLockedProfile, D as resolveProfileChain, E as resolveProfileSupport, F as resolveVersion, G as parseFrontmatter, H as isKnownIdePlatform, I as cloneGitSource, J as loadProfileManifest, K as parseSource, L as collectProfileSupportPatterns, M as generateLock, N as readLock, O as detectLegacyPaths, P as writeLock, Q as SourceParseError, R as updateGitignore, S as getProfileWeight, T as mergeContentParts, U as getAdaptersForKeys, V as idePlatformRegistry, W as getAllAdapters, X as KEBAB_CASE_REGEX, Y as loadProjectManifest, Z as FileNotFoundError, _ as mergeMemoryWithWarnings, a as clearIdeCache, b as mergeSkills, c as getDefaultGlobalSource, d as getGlobalSources, et as getAgentPath, 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, o as detectInstalledIdes, p as setGlobalAiTools, q as loadLockfile, r as writeProjectPreferences, s as addGlobalSource, t as resolvePreferences, tt as getAllAgentKeys, u as getGlobalIdePlatforms, v as mergeRules, w as sortProfilesByWeight, x as mergeSkillsWithWarnings, y as mergeRulesWithWarnings, z as getIdePlatformTargetDir } from "./src-BgiJfm14.mjs";
4
+ import { $ as loadProfileManifest, A as detectLegacyPaths, B as collectComprehensivePatterns, C as mergeSkillsWithWarnings, D as mergeContentParts, E as sortProfilesByWeight, F as generateLock, G as getRegisteredIdePlatforms, H as removeGitignoreManagedSection, I as readLock, J as getAdaptersForKeys, K as idePlatformRegistry, L as writeLock, M as discoverProfilesInSourceRepo, N as findSourceManifest, O as resolveProfileSupport, P as removePlacedFiles, Q as loadLockfile, R as resolveVersion, S as mergeSkills, T as isLockedProfile, U as updateGitignore, V as ensureBatonDirGitignored, W as getIdePlatformTargetDir, X as parseFrontmatter, Y as getAllAdapters, Z as parseSource, _ as require_lib, a as clearIdeCache, at as getAgentPath, b as mergeRules, c as getBatonHome, d as getGlobalConfigPath, et as loadProjectManifest, f as getGlobalIdePlatforms, g as setGlobalIdePlatforms, h as setGlobalAiTools, i as computeIntersection, it as getAgentConfig, j as placeFile, k as resolveProfileChain, l as getDefaultGlobalSource, m as removeGlobalSource, n as readProjectPreferences, nt as FileNotFoundError, o as detectInstalledIdes, ot as getAllAgentKeys, p as getGlobalSources, q as isKnownIdePlatform, r as writeProjectPreferences, rt as SourceParseError, s as addGlobalSource, t as resolvePreferences, tt as KEBAB_CASE_REGEX, u as getGlobalAiTools, v as mergeMemory, w as getProfileWeight, x as mergeRulesWithWarnings, y as mergeMemoryWithWarnings, z as cloneGitSource } from "./src-BCGnnv5D.mjs";
5
5
  import { n as detectInstalledAgents, t as clearAgentCache } from "./agent-detection-DTiVeO5W.mjs";
6
6
  import { d as esm_default } from "./esm-BagM-kVd.mjs";
7
- import { access, mkdir, readFile, readdir, rm, rmdir, stat, unlink, writeFile } from "node:fs/promises";
8
- import { dirname, join, resolve } from "node:path";
7
+ import { access, mkdir, readFile, readdir, rm, stat, writeFile } from "node:fs/promises";
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({
@@ -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");
@@ -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 });
@@ -1965,13 +1981,33 @@ async function handleRemoveBaton(cwd) {
1965
1981
  R.warn("Cancelled.");
1966
1982
  return false;
1967
1983
  }
1984
+ const lockPath = join(cwd, "baton.lock");
1985
+ await cleanupPlacedFilesFromLock(lockPath, cwd);
1968
1986
  await rm(join(cwd, "baton.yaml"), { force: true });
1969
- await rm(join(cwd, "baton.lock"), { force: true });
1987
+ await rm(lockPath, { force: true });
1970
1988
  R.success("Baton has been removed from this project.");
1971
- R.info("Note: Synced files (rules, skills, memory) were not removed.");
1972
- R.info("Run 'baton sync' before removing to clean up, or delete them manually.");
1973
1989
  return true;
1974
1990
  }
1991
+ async function cleanupPlacedFilesFromLock(lockPath, projectRoot) {
1992
+ let placedPaths;
1993
+ try {
1994
+ const lockfile = await readLock(lockPath);
1995
+ placedPaths = Object.values(lockfile.packages).flatMap((pkg) => Object.keys(pkg.integrity));
1996
+ } catch (error) {
1997
+ if (error instanceof FileNotFoundError) return;
1998
+ return;
1999
+ }
2000
+ if (placedPaths.length === 0) return;
2001
+ R.info(`Found ${placedPaths.length} placed file(s):`);
2002
+ for (const filePath of placedPaths) R.info(` ${filePath}`);
2003
+ const shouldClean = await Re({
2004
+ message: `Also remove ${placedPaths.length} placed file(s)?`,
2005
+ initialValue: false
2006
+ });
2007
+ if (Ct(shouldClean) || !shouldClean) return;
2008
+ const removedCount = await removePlacedFiles(placedPaths, projectRoot);
2009
+ R.success(`Removed ${removedCount} placed file(s).`);
2010
+ }
1975
2011
  function formatIdeName(ideKey) {
1976
2012
  return {
1977
2013
  vscode: "VS Code",
@@ -2112,6 +2148,31 @@ async function handleConfigureIdes(cwd) {
2112
2148
  });
2113
2149
  R.success(`Project configured with ${selectedKeys.length} IDE platform(s).`);
2114
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
+ }
2115
2176
  const manageCommand = defineCommand({
2116
2177
  meta: {
2117
2178
  name: "manage",
@@ -2154,6 +2215,11 @@ const manageCommand = defineCommand({
2154
2215
  label: "Configure IDEs for this project",
2155
2216
  hint: "Choose which IDEs to sync"
2156
2217
  },
2218
+ {
2219
+ value: "configure-gitignore",
2220
+ label: "Configure .gitignore",
2221
+ hint: "Choose whether synced files are gitignored"
2222
+ },
2157
2223
  {
2158
2224
  value: "remove-baton",
2159
2225
  label: "Remove Baton",
@@ -2189,6 +2255,10 @@ const manageCommand = defineCommand({
2189
2255
  console.log("");
2190
2256
  await handleConfigureIdes(cwd);
2191
2257
  console.log("");
2258
+ } else if (action === "configure-gitignore") {
2259
+ console.log("");
2260
+ await handleConfigureGitignore(cwd);
2261
+ console.log("");
2192
2262
  } else if (action === "remove-baton") {
2193
2263
  console.log("");
2194
2264
  if (await handleRemoveBaton(cwd)) {
@@ -2209,8 +2279,8 @@ const profileCommand = defineCommand({
2209
2279
  description: "Manage profiles (create, list, remove)"
2210
2280
  },
2211
2281
  subCommands: {
2212
- create: () => import("./create-BG_VVOTI.mjs").then((m) => m.createCommand),
2213
- list: () => import("./list-CCzjta6J.mjs").then((m) => m.profileListCommand),
2282
+ create: () => import("./create-BkpEXaht.mjs").then((m) => m.createCommand),
2283
+ list: () => import("./list-DSLwzhBG.mjs").then((m) => m.profileListCommand),
2214
2284
  remove: () => import("./remove-BBs6Mv8t.mjs").then((m) => m.profileRemoveCommand)
2215
2285
  }
2216
2286
  });
@@ -2668,35 +2738,25 @@ async function copyDirectoryRecursive(sourceDir, targetDir) {
2668
2738
  return placed;
2669
2739
  }
2670
2740
  /**
2671
- * Collect profile-support-based .gitignore patterns and update .gitignore.
2672
- * Uses ALL tools the profile supports (not just the developer's intersection)
2673
- * 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.
2674
2747
  */
2675
- async function updateGitignorePatterns(params) {
2676
- const { allIntersections, adapters, ideMap, mergedSkills, mergedRules, mergedMemory, mergedCommandCount, fileMap, projectRoot, spinner } = params;
2677
- const profileSupportedAiTools = /* @__PURE__ */ new Set();
2678
- const profileSupportedIdePlatforms = /* @__PURE__ */ new Set();
2679
- if (allIntersections) for (const intersection of allIntersections.values()) {
2680
- for (const tool of intersection.aiTools.synced) profileSupportedAiTools.add(tool);
2681
- for (const tool of intersection.aiTools.unavailable) profileSupportedAiTools.add(tool);
2682
- for (const plat of intersection.idePlatforms.synced) profileSupportedIdePlatforms.add(plat);
2683
- for (const plat of intersection.idePlatforms.unavailable) profileSupportedIdePlatforms.add(plat);
2684
- }
2685
- else {
2686
- for (const adapter of adapters) profileSupportedAiTools.add(adapter.key);
2687
- for (const entry of ideMap.values()) profileSupportedIdePlatforms.add(entry.ideKey);
2688
- }
2689
- const hasContent = mergedSkills.length > 0 || mergedRules.length > 0 || mergedMemory.length > 0 || mergedCommandCount > 0;
2690
- const gitignorePatterns = collectProfileSupportPatterns({
2691
- profileAiTools: [...profileSupportedAiTools],
2692
- profileIdePlatforms: [...profileSupportedIdePlatforms],
2693
- fileTargets: [...fileMap.values()].map((f) => f.target),
2694
- hasContent
2695
- });
2696
- 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) {
2697
2753
  spinner.start("Updating .gitignore...");
2698
- const updated = await updateGitignore(projectRoot, gitignorePatterns);
2699
- 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");
2700
2760
  }
2701
2761
  }
2702
2762
  /**
@@ -2750,23 +2810,7 @@ async function cleanupOrphanedFiles(params) {
2750
2810
  return;
2751
2811
  }
2752
2812
  spinner.start("Removing orphaned files...");
2753
- let removedCount = 0;
2754
- for (const orphanedPath of orphanedPaths) {
2755
- const absolutePath = orphanedPath.startsWith("/") ? orphanedPath : resolve(projectRoot, orphanedPath);
2756
- try {
2757
- await unlink(absolutePath);
2758
- removedCount++;
2759
- let dir = dirname(absolutePath);
2760
- while (dir !== projectRoot && dir.startsWith(projectRoot)) try {
2761
- if ((await readdir(dir)).length === 0) {
2762
- await rmdir(dir);
2763
- dir = dirname(dir);
2764
- } else break;
2765
- } catch {
2766
- break;
2767
- }
2768
- } catch {}
2769
- }
2813
+ const removedCount = await removePlacedFiles(orphanedPaths, projectRoot);
2770
2814
  spinner.stop(`Removed ${removedCount} orphaned file(s)`);
2771
2815
  }
2772
2816
  const syncCommand = defineCommand({
@@ -3214,9 +3258,10 @@ const syncCommand = defineCommand({
3214
3258
  const combinedContent = entry.parts.join("\n\n");
3215
3259
  const result = await placeFile(combinedContent, entry.adapter, entry.type, "project", entry.name, placementConfig);
3216
3260
  if (result.action !== "skipped") stats.created++;
3261
+ const relPath = isAbsolute(result.path) ? relative(projectRoot, result.path) : result.path;
3217
3262
  for (const profileName of entry.profiles) {
3218
3263
  const pf = getOrCreatePlacedFiles(placedFiles, profileName);
3219
- pf[result.path] = {
3264
+ pf[relPath] = {
3220
3265
  content: combinedContent,
3221
3266
  tool: entry.adapter.key,
3222
3267
  category: "ai"
@@ -3246,8 +3291,9 @@ const syncCommand = defineCommand({
3246
3291
  }
3247
3292
  const result = await placeFile(content, adapter, "commands", "project", commandName, placementConfig);
3248
3293
  if (result.action !== "skipped") stats.created++;
3294
+ const cmdRelPath = isAbsolute(result.path) ? relative(projectRoot, result.path) : result.path;
3249
3295
  const pf = getOrCreatePlacedFiles(placedFiles, profile.name);
3250
- pf[result.path] = {
3296
+ pf[cmdRelPath] = {
3251
3297
  content,
3252
3298
  tool: adapter.key,
3253
3299
  category: "ai"
@@ -3321,14 +3367,8 @@ const syncCommand = defineCommand({
3321
3367
  stats.errors++;
3322
3368
  }
3323
3369
  spinner.stop(dryRun ? `Would place files for ${adapters.length} agent(s)` : `Placed ${stats.created} file(s) for ${adapters.length} agent(s)`);
3324
- if (!dryRun) await updateGitignorePatterns({
3325
- allIntersections,
3326
- adapters,
3327
- ideMap,
3328
- mergedSkills,
3329
- mergedRules,
3330
- mergedMemory,
3331
- mergedCommandCount,
3370
+ if (!dryRun) await handleGitignoreUpdate({
3371
+ projectManifest,
3332
3372
  fileMap,
3333
3373
  projectRoot,
3334
3374
  spinner