@iannuttall/dotagents 0.1.2 → 0.1.3

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.
Files changed (3) hide show
  1. package/README.md +15 -6
  2. package/dist/cli.js +176 -95
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -30,7 +30,7 @@ Global home affects all projects. Project folder only affects the current direct
30
30
  ## What it does
31
31
 
32
32
  - Keeps `.agents` as the source of truth.
33
- - Creates symlinks for Claude, Codex, Factory, Cursor, and OpenCode (based on your selection).
33
+ - Creates symlinks for Claude, Codex, Factory, Cursor, OpenCode, and Gemini (based on your selection).
34
34
  - Always creates a backup before any overwrite so changes are reversible.
35
35
 
36
36
  ## Where it links (global scope)
@@ -39,6 +39,10 @@ Global home affects all projects. Project folder only affects the current direct
39
39
 
40
40
  `.agents/AGENTS.md` → `~/.claude/CLAUDE.md` (fallback when no CLAUDE.md)
41
41
 
42
+ `.agents/GEMINI.md` → `~/.gemini/GEMINI.md` (if present)
43
+
44
+ `.agents/AGENTS.md` → `~/.gemini/GEMINI.md` (fallback when no GEMINI.md)
45
+
42
46
  `.agents/commands` → `~/.claude/commands`
43
47
 
44
48
  `.agents/commands` → `~/.factory/commands`
@@ -47,6 +51,10 @@ Global home affects all projects. Project folder only affects the current direct
47
51
 
48
52
  `.agents/commands` → `~/.cursor/commands`
49
53
 
54
+ `.agents/commands` → `~/.config/opencode/commands`
55
+
56
+ `.agents/commands` → `~/.gemini/commands`
57
+
50
58
  `.agents/hooks` → `~/.claude/hooks`
51
59
 
52
60
  `.agents/hooks` → `~/.factory/hooks`
@@ -57,8 +65,6 @@ Global home affects all projects. Project folder only affects the current direct
57
65
 
58
66
  `.agents/AGENTS.md` → `~/.config/opencode/AGENTS.md`
59
67
 
60
- `.agents/commands` → `~/.opencode/commands`
61
-
62
68
  `.agents/skills` → `~/.claude/skills`
63
69
 
64
70
  `.agents/skills` → `~/.factory/skills`
@@ -67,9 +73,11 @@ Global home affects all projects. Project folder only affects the current direct
67
73
 
68
74
  `.agents/skills` → `~/.cursor/skills`
69
75
 
70
- `.agents/skills` → `~/.opencode/skills`
76
+ `.agents/skills` → `~/.config/opencode/skills`
77
+
78
+ `.agents/skills` → `~/.gemini/skills`
71
79
 
72
- Project scope links only commands/hooks/skills into the project’s client folders (no AGENTS/CLAUDE rules).
80
+ Project scope links only commands/hooks/skills into the project’s client folders (no AGENTS/CLAUDE/GEMINI rules).
73
81
 
74
82
  ## Development
75
83
 
@@ -100,7 +108,8 @@ bun run build
100
108
  - Codex prompts always symlink to `.agents/commands` (canonical source).
101
109
  - Skills require a valid `SKILL.md` with `name` + `description` frontmatter.
102
110
  - Claude prompt precedence: if `.agents/CLAUDE.md` exists, it links to `.claude/CLAUDE.md`. Otherwise `.agents/AGENTS.md` is used. After adding or removing `.agents/CLAUDE.md`, re-run dotagents and apply/repair links to update the symlink. Factory/Codex always link to `.agents/AGENTS.md`.
103
- - Project scope creates `.agents` plus client folders for commands/hooks/skills only. Rule files (`AGENTS.md`/`CLAUDE.md`) are left to the repo root so you can manage them explicitly.
111
+ - Gemini context file precedence: if `.agents/GEMINI.md` exists, it links to `.gemini/GEMINI.md`. Otherwise `.agents/AGENTS.md` is used. After adding or removing `.agents/GEMINI.md`, re-run dotagents and apply/repair links to update the symlink.
112
+ - Project scope creates `.agents` plus client folders for commands/hooks/skills only. Rule files (`AGENTS.md`/`CLAUDE.md`/`GEMINI.md`) are left to the repo root so you can manage them explicitly.
104
113
  - Backups are stored under `.agents/backup/<timestamp>` and can be restored via “Undo last change.”
105
114
 
106
115
  ## License
package/dist/cli.js CHANGED
@@ -3621,7 +3621,7 @@ var require_gray_matter = __commonJS((exports, module) => {
3621
3621
  });
3622
3622
 
3623
3623
  // src/cli.tsx
3624
- import path12 from "path";
3624
+ import path13 from "path";
3625
3625
 
3626
3626
  // node_modules/chalk/source/vendor/ansi-styles/index.js
3627
3627
  var ANSI_BACKGROUND_OFFSET = 10;
@@ -4719,7 +4719,7 @@ function me() {
4719
4719
 
4720
4720
  // src/core/plan.ts
4721
4721
  import fs2 from "fs";
4722
- import path4 from "path";
4722
+ import path5 from "path";
4723
4723
 
4724
4724
  // src/core/mappings.ts
4725
4725
  import path3 from "path";
@@ -4737,8 +4737,12 @@ function resolveRoots(opts) {
4737
4737
  factoryRoot: path.join(homeDir, ".factory"),
4738
4738
  codexRoot: path.join(homeDir, ".codex"),
4739
4739
  cursorRoot: path.join(homeDir, ".cursor"),
4740
- opencodeRoot: path.join(homeDir, ".opencode"),
4740
+ opencodeRoot: path.join(homeDir, ".config", "opencode"),
4741
4741
  opencodeConfigRoot: path.join(homeDir, ".config", "opencode"),
4742
+ geminiRoot: path.join(homeDir, ".gemini"),
4743
+ githubRoot: path.join(projectRoot, ".github"),
4744
+ copilotRoot: path.join(homeDir, ".copilot"),
4745
+ ampcodeConfigRoot: path.join(homeDir, ".config", "amp"),
4742
4746
  projectRoot,
4743
4747
  homeDir
4744
4748
  };
@@ -4751,6 +4755,10 @@ function resolveRoots(opts) {
4751
4755
  cursorRoot: path.join(projectRoot, ".cursor"),
4752
4756
  opencodeRoot: path.join(projectRoot, ".opencode"),
4753
4757
  opencodeConfigRoot: path.join(homeDir, ".config", "opencode"),
4758
+ geminiRoot: path.join(projectRoot, ".gemini"),
4759
+ githubRoot: path.join(projectRoot, ".github"),
4760
+ copilotRoot: path.join(homeDir, ".copilot"),
4761
+ ampcodeConfigRoot: path.join(homeDir, ".config", "amp"),
4754
4762
  projectRoot,
4755
4763
  homeDir
4756
4764
  };
@@ -4819,24 +4827,36 @@ async function getMappings(opts) {
4819
4827
  const roots = resolveRoots(opts);
4820
4828
  const canonical = roots.canonicalRoot;
4821
4829
  const claudeOverride = path3.join(canonical, "CLAUDE.md");
4830
+ const geminiOverride = path3.join(canonical, "GEMINI.md");
4822
4831
  const agentsFallback = path3.join(canonical, "AGENTS.md");
4823
- const agentsSource = await pathExists(claudeOverride) ? claudeOverride : agentsFallback;
4824
- const clients = new Set(opts.clients ?? ["claude", "factory", "codex", "cursor", "opencode"]);
4832
+ const claudeSource = await pathExists(claudeOverride) ? claudeOverride : agentsFallback;
4833
+ const geminiSource = await pathExists(geminiOverride) ? geminiOverride : agentsFallback;
4834
+ const clients = new Set(opts.clients ?? ["claude", "factory", "codex", "cursor", "opencode", "gemini", "github", "ampcode"]);
4835
+ const opencodeSkillsRoot = opts.scope === "global" ? roots.opencodeConfigRoot : roots.opencodeRoot;
4825
4836
  const mappings = [];
4826
4837
  const includeAgentFiles = opts.scope === "global";
4827
4838
  if (includeAgentFiles && clients.has("claude")) {
4828
4839
  mappings.push({
4829
4840
  name: "claude-md",
4830
- source: agentsSource,
4841
+ source: claudeSource,
4831
4842
  targets: [path3.join(roots.claudeRoot, "CLAUDE.md")],
4832
4843
  kind: "file"
4833
4844
  });
4834
4845
  }
4846
+ if (includeAgentFiles && clients.has("gemini")) {
4847
+ mappings.push({
4848
+ name: "gemini-md",
4849
+ source: geminiSource,
4850
+ targets: [path3.join(roots.geminiRoot, "GEMINI.md")],
4851
+ kind: "file"
4852
+ });
4853
+ }
4835
4854
  if (includeAgentFiles) {
4836
4855
  const agentTargets = [
4837
4856
  clients.has("factory") ? path3.join(roots.factoryRoot, "AGENTS.md") : null,
4838
4857
  clients.has("codex") ? path3.join(roots.codexRoot, "AGENTS.md") : null,
4839
- clients.has("opencode") ? path3.join(roots.opencodeConfigRoot, "AGENTS.md") : null
4858
+ clients.has("opencode") ? path3.join(roots.opencodeConfigRoot, "AGENTS.md") : null,
4859
+ clients.has("ampcode") ? path3.join(roots.ampcodeConfigRoot, "AGENTS.md") : null
4840
4860
  ].filter(Boolean);
4841
4861
  if (agentTargets.length > 0) {
4842
4862
  mappings.push({
@@ -4855,7 +4875,8 @@ async function getMappings(opts) {
4855
4875
  clients.has("factory") ? path3.join(roots.factoryRoot, "commands") : null,
4856
4876
  clients.has("codex") ? path3.join(roots.codexRoot, "prompts") : null,
4857
4877
  clients.has("opencode") ? path3.join(roots.opencodeRoot, "commands") : null,
4858
- clients.has("cursor") ? path3.join(roots.cursorRoot, "commands") : null
4878
+ clients.has("cursor") ? path3.join(roots.cursorRoot, "commands") : null,
4879
+ clients.has("gemini") ? path3.join(roots.geminiRoot, "commands") : null
4859
4880
  ].filter(Boolean),
4860
4881
  kind: "dir"
4861
4882
  }, {
@@ -4873,20 +4894,40 @@ async function getMappings(opts) {
4873
4894
  clients.has("claude") ? path3.join(roots.claudeRoot, "skills") : null,
4874
4895
  clients.has("factory") ? path3.join(roots.factoryRoot, "skills") : null,
4875
4896
  clients.has("codex") ? path3.join(roots.codexRoot, "skills") : null,
4876
- clients.has("opencode") ? path3.join(roots.opencodeRoot, "skills") : null,
4877
- clients.has("cursor") ? path3.join(roots.cursorRoot, "skills") : null
4897
+ clients.has("opencode") ? path3.join(opencodeSkillsRoot, "skills") : null,
4898
+ clients.has("cursor") ? path3.join(roots.cursorRoot, "skills") : null,
4899
+ clients.has("gemini") ? path3.join(roots.geminiRoot, "skills") : null,
4900
+ clients.has("github") ? opts.scope === "global" ? path3.join(roots.copilotRoot, "skills") : path3.join(roots.githubRoot, "skills") : null
4878
4901
  ].filter(Boolean),
4879
4902
  kind: "dir"
4880
4903
  });
4881
4904
  return mappings;
4882
4905
  }
4883
4906
 
4907
+ // src/core/link-target.ts
4908
+ import path4 from "path";
4909
+ function getLinkTarget(source, target, kind) {
4910
+ const resolvedSource = path4.resolve(source);
4911
+ if (process.platform === "win32" && kind === "dir") {
4912
+ return { link: resolvedSource, isRelative: false };
4913
+ }
4914
+ const relative = path4.relative(path4.dirname(target), resolvedSource);
4915
+ if (!relative || path4.isAbsolute(relative)) {
4916
+ return { link: resolvedSource, isRelative: false };
4917
+ }
4918
+ const resolvedRelative = path4.resolve(path4.dirname(target), relative);
4919
+ if (resolvedRelative !== resolvedSource) {
4920
+ return { link: resolvedSource, isRelative: false };
4921
+ }
4922
+ return { link: relative, isRelative: true };
4923
+ }
4924
+
4884
4925
  // src/core/plan.ts
4885
4926
  async function getLinkTargetAbsolute(targetPath) {
4886
4927
  try {
4887
4928
  const link = await fs2.promises.readlink(targetPath);
4888
- if (!path4.isAbsolute(link)) {
4889
- return path4.resolve(path4.dirname(targetPath), link);
4929
+ if (!path5.isAbsolute(link)) {
4930
+ return path5.resolve(path5.dirname(targetPath), link);
4890
4931
  }
4891
4932
  return link;
4892
4933
  } catch {
@@ -4912,13 +4953,18 @@ async function analyzeTarget(source, target, kind, opts) {
4912
4953
  return { type: "link", source, target, kind };
4913
4954
  const stat = await fs2.promises.lstat(target);
4914
4955
  if (stat.isSymbolicLink()) {
4956
+ const rawLink = await fs2.promises.readlink(target);
4915
4957
  const resolved = await getLinkTargetAbsolute(target);
4916
- if (resolved && path4.resolve(resolved) === path4.resolve(source)) {
4958
+ if (resolved && path5.resolve(resolved) === path5.resolve(source)) {
4959
+ const desired = getLinkTarget(source, target, kind);
4960
+ if (desired.isRelative && path5.isAbsolute(rawLink)) {
4961
+ return { type: "link", source, target, kind, replaceSymlink: true };
4962
+ }
4917
4963
  return { type: "noop", source, target };
4918
4964
  }
4919
4965
  if (resolved && opts?.relinkableSources) {
4920
- const relinkable = new Set(opts.relinkableSources.map((p) => path4.resolve(p)));
4921
- if (relinkable.has(path4.resolve(resolved))) {
4966
+ const relinkable = new Set(opts.relinkableSources.map((p) => path5.resolve(p)));
4967
+ if (relinkable.has(path5.resolve(resolved))) {
4922
4968
  return { type: "link", source, target, kind, replaceSymlink: true };
4923
4969
  }
4924
4970
  }
@@ -4933,10 +4979,18 @@ async function buildLinkPlan(opts) {
4933
4979
  const tasks = [];
4934
4980
  for (const mapping of mappings) {
4935
4981
  tasks.push(...await ensureSourceTask(mapping.source, mapping.kind));
4936
- const relinkableSources = mapping.name === "claude-md" ? [
4937
- path4.join(path4.dirname(mapping.source), "AGENTS.md"),
4938
- path4.join(path4.dirname(mapping.source), "CLAUDE.md")
4939
- ] : undefined;
4982
+ let relinkableSources;
4983
+ if (mapping.name === "claude-md") {
4984
+ relinkableSources = [
4985
+ path5.join(path5.dirname(mapping.source), "AGENTS.md"),
4986
+ path5.join(path5.dirname(mapping.source), "CLAUDE.md")
4987
+ ];
4988
+ } else if (mapping.name === "gemini-md") {
4989
+ relinkableSources = [
4990
+ path5.join(path5.dirname(mapping.source), "AGENTS.md"),
4991
+ path5.join(path5.dirname(mapping.source), "GEMINI.md")
4992
+ ];
4993
+ }
4940
4994
  for (const target of mapping.targets) {
4941
4995
  tasks.push(await analyzeTarget(mapping.source, target, mapping.kind, { relinkableSources }));
4942
4996
  }
@@ -4948,12 +5002,12 @@ async function buildLinkPlan(opts) {
4948
5002
 
4949
5003
  // src/core/status.ts
4950
5004
  import fs3 from "fs";
4951
- import path5 from "path";
5005
+ import path6 from "path";
4952
5006
  async function resolveLinkTarget(targetPath) {
4953
5007
  try {
4954
5008
  const link = await fs3.promises.readlink(targetPath);
4955
- if (!path5.isAbsolute(link))
4956
- return path5.resolve(path5.dirname(targetPath), link);
5009
+ if (!path6.isAbsolute(link))
5010
+ return path6.resolve(path6.dirname(targetPath), link);
4957
5011
  return link;
4958
5012
  } catch {
4959
5013
  return null;
@@ -4973,7 +5027,7 @@ async function getLinkStatus(opts) {
4973
5027
  const stat = await fs3.promises.lstat(target);
4974
5028
  if (stat.isSymbolicLink()) {
4975
5029
  const resolved = await resolveLinkTarget(target);
4976
- if (resolved && path5.resolve(resolved) === path5.resolve(mapping.source)) {
5030
+ if (resolved && path6.resolve(resolved) === path6.resolve(mapping.source)) {
4977
5031
  targets.push({ path: target, status: "linked" });
4978
5032
  } else {
4979
5033
  targets.push({ path: target, status: "conflict" });
@@ -4989,11 +5043,11 @@ async function getLinkStatus(opts) {
4989
5043
 
4990
5044
  // src/core/migrate.ts
4991
5045
  import fs6 from "fs";
4992
- import path9 from "path";
5046
+ import path10 from "path";
4993
5047
 
4994
5048
  // src/core/skills.ts
4995
5049
  var import_gray_matter = __toESM(require_gray_matter(), 1);
4996
- import path6 from "path";
5050
+ import path7 from "path";
4997
5051
  var NAME_RE = /^[a-z0-9-]{1,64}$/;
4998
5052
  async function parseSkillFile(skillFile) {
4999
5053
  const raw = await readText(skillFile);
@@ -5015,13 +5069,13 @@ async function parseSkillFile(skillFile) {
5015
5069
  };
5016
5070
  }
5017
5071
  async function isSkillDir(dir) {
5018
- return await pathExists(path6.join(dir, "SKILL.md"));
5072
+ return await pathExists(path7.join(dir, "SKILL.md"));
5019
5073
  }
5020
5074
  async function findSkillDirs(root) {
5021
5075
  const direct = await isSkillDir(root);
5022
5076
  if (direct)
5023
5077
  return [root];
5024
- const skillsDir = path6.join(root, "skills");
5078
+ const skillsDir = path7.join(root, "skills");
5025
5079
  const skillsDirExists = await pathExists(skillsDir);
5026
5080
  if (skillsDirExists) {
5027
5081
  const children2 = await listDirs(skillsDir);
@@ -5044,35 +5098,35 @@ async function findSkillDirs(root) {
5044
5098
 
5045
5099
  // src/core/apply.ts
5046
5100
  import fs5 from "fs";
5047
- import path8 from "path";
5101
+ import path9 from "path";
5048
5102
 
5049
5103
  // src/core/backup.ts
5050
5104
  import fs4 from "fs";
5051
- import path7 from "path";
5105
+ import path8 from "path";
5052
5106
  var MANIFEST_NAME = "manifest.json";
5053
5107
  function backupPathFor(target, backupRoot) {
5054
- const root = path7.parse(target).root || path7.sep;
5055
- const rel = path7.relative(root, target);
5056
- return path7.join(backupRoot, rel);
5108
+ const root = path8.parse(target).root || path8.sep;
5109
+ const rel = path8.relative(root, target);
5110
+ return path8.join(backupRoot, rel);
5057
5111
  }
5058
5112
  function hasParentPath(target, seen) {
5059
- const resolved = path7.resolve(target);
5113
+ const resolved = path8.resolve(target);
5060
5114
  for (const parent of seen) {
5061
5115
  if (resolved === parent)
5062
5116
  return true;
5063
- if (resolved.startsWith(parent + path7.sep))
5117
+ if (resolved.startsWith(parent + path8.sep))
5064
5118
  return true;
5065
5119
  }
5066
5120
  return false;
5067
5121
  }
5068
5122
  async function backupSymlink(target, dest) {
5069
5123
  const link = await fs4.promises.readlink(target);
5070
- await ensureDir(path7.dirname(dest));
5124
+ await ensureDir(path8.dirname(dest));
5071
5125
  await fs4.promises.symlink(link, dest);
5072
5126
  await fs4.promises.unlink(target);
5073
5127
  }
5074
5128
  async function backupPathImpl(target, dest, kind) {
5075
- await ensureDir(path7.dirname(dest));
5129
+ await ensureDir(path8.dirname(dest));
5076
5130
  try {
5077
5131
  await fs4.promises.rename(target, dest);
5078
5132
  return;
@@ -5089,11 +5143,11 @@ async function backupPathImpl(target, dest, kind) {
5089
5143
  }
5090
5144
  async function createBackupSession(opts) {
5091
5145
  const timestamp = opts.timestamp || new Date().toISOString().replace(/[:.]/g, "-");
5092
- const dir = path7.join(opts.canonicalRoot, "backup", timestamp);
5146
+ const dir = path8.join(opts.canonicalRoot, "backup", timestamp);
5093
5147
  await ensureDir(dir);
5094
5148
  return {
5095
5149
  dir,
5096
- manifestPath: path7.join(dir, MANIFEST_NAME),
5150
+ manifestPath: path8.join(dir, MANIFEST_NAME),
5097
5151
  createdAt: new Date().toISOString(),
5098
5152
  scope: opts.scope,
5099
5153
  operation: opts.operation,
@@ -5104,7 +5158,7 @@ async function createBackupSession(opts) {
5104
5158
  async function backupPath(target, session) {
5105
5159
  if (!await pathExists(target))
5106
5160
  return false;
5107
- const resolved = path7.resolve(target);
5161
+ const resolved = path8.resolve(target);
5108
5162
  if (hasParentPath(resolved, session._seen))
5109
5163
  return false;
5110
5164
  const stat = await fs4.promises.lstat(target);
@@ -5120,7 +5174,7 @@ async function backupPath(target, session) {
5120
5174
  return true;
5121
5175
  }
5122
5176
  function recordCreatedPath(target, kind, session) {
5123
- const resolved = path7.resolve(target);
5177
+ const resolved = path8.resolve(target);
5124
5178
  if (hasParentPath(resolved, session._seen))
5125
5179
  return;
5126
5180
  session.entries.push({ originalPath: resolved, kind, action: "create" });
@@ -5137,7 +5191,7 @@ async function finalizeBackup(session) {
5137
5191
  await fs4.promises.writeFile(session.manifestPath, JSON.stringify(manifest, null, 2), "utf8");
5138
5192
  }
5139
5193
  async function loadBackupManifest(dir) {
5140
- const manifestPath = path7.join(dir, MANIFEST_NAME);
5194
+ const manifestPath = path8.join(dir, MANIFEST_NAME);
5141
5195
  if (!await pathExists(manifestPath))
5142
5196
  return null;
5143
5197
  const raw = await fs4.promises.readFile(manifestPath, "utf8");
@@ -5157,20 +5211,21 @@ async function createSource(task) {
5157
5211
  await ensureFile(task.path, DEFAULT_AGENTS);
5158
5212
  }
5159
5213
  async function createLink(source, target, kind, overwrite, backup) {
5214
+ const linkTarget = getLinkTarget(source, target, kind);
5160
5215
  if (await pathExists(target)) {
5161
5216
  if (!overwrite)
5162
5217
  return { created: false, backedUp: false };
5163
5218
  const backedUp = backup ? await backupPath(target, backup) : false;
5164
5219
  if (await pathExists(target))
5165
5220
  await removePath(target);
5166
- await ensureDir(path8.dirname(target));
5221
+ await ensureDir(path9.dirname(target));
5167
5222
  const type2 = kind === "dir" ? "junction" : "file";
5168
- await fs5.promises.symlink(source, target, type2);
5223
+ await fs5.promises.symlink(linkTarget.link, target, type2);
5169
5224
  return { created: true, backedUp };
5170
5225
  }
5171
- await ensureDir(path8.dirname(target));
5226
+ await ensureDir(path9.dirname(target));
5172
5227
  const type = kind === "dir" ? "junction" : "file";
5173
- await fs5.promises.symlink(source, target, type);
5228
+ await fs5.promises.symlink(linkTarget.link, target, type);
5174
5229
  return { created: true, backedUp: false };
5175
5230
  }
5176
5231
  async function applyLinkPlan(plan, opts) {
@@ -5240,56 +5295,64 @@ async function isSymlink(p) {
5240
5295
  async function listFiles(dir) {
5241
5296
  try {
5242
5297
  const entries = await fs6.promises.readdir(dir, { withFileTypes: true });
5243
- return entries.filter((e2) => e2.isFile()).map((e2) => path9.join(dir, e2.name));
5298
+ return entries.filter((e2) => e2.isFile()).map((e2) => path10.join(dir, e2.name));
5244
5299
  } catch {
5245
5300
  return [];
5246
5301
  }
5247
5302
  }
5248
5303
  function conflictLabel(targetPath, canonicalRoot) {
5249
5304
  if (targetPath.startsWith(canonicalRoot)) {
5250
- const rel = path9.relative(canonicalRoot, targetPath);
5251
- return rel || path9.basename(targetPath);
5305
+ const rel = path10.relative(canonicalRoot, targetPath);
5306
+ return rel || path10.basename(targetPath);
5252
5307
  }
5253
- return path9.basename(targetPath);
5308
+ return path10.basename(targetPath);
5254
5309
  }
5255
5310
  async function scanMigration(opts) {
5256
5311
  const roots = resolveRoots(opts);
5257
5312
  const canonicalRoot = roots.canonicalRoot;
5258
5313
  const candidatesByTarget = new Map;
5259
- const clients = new Set(opts.clients ?? ["claude", "factory", "codex", "cursor", "opencode"]);
5314
+ const clients = new Set(opts.clients ?? ["claude", "factory", "codex", "cursor", "opencode", "gemini", "github", "ampcode"]);
5260
5315
  const includeAgentFiles = opts.scope === "global";
5261
- const canonicalCommands = path9.join(canonicalRoot, "commands");
5262
- const canonicalHooks = path9.join(canonicalRoot, "hooks");
5263
- const canonicalSkills = path9.join(canonicalRoot, "skills");
5264
- const canonicalAgents = path9.join(canonicalRoot, "AGENTS.md");
5265
- const canonicalClaude = path9.join(canonicalRoot, "CLAUDE.md");
5316
+ const canonicalCommands = path10.join(canonicalRoot, "commands");
5317
+ const canonicalHooks = path10.join(canonicalRoot, "hooks");
5318
+ const canonicalSkills = path10.join(canonicalRoot, "skills");
5319
+ const canonicalAgents = path10.join(canonicalRoot, "AGENTS.md");
5320
+ const canonicalClaude = path10.join(canonicalRoot, "CLAUDE.md");
5321
+ const opencodeSkillsRoot = opts.scope === "global" ? roots.opencodeConfigRoot : roots.opencodeRoot;
5266
5322
  const sources = {
5267
5323
  commands: [
5268
- clients.has("claude") ? { label: "Claude commands", dir: path9.join(roots.claudeRoot, "commands") } : null,
5269
- clients.has("factory") ? { label: "Factory commands", dir: path9.join(roots.factoryRoot, "commands") } : null,
5270
- clients.has("codex") ? { label: "Codex prompts", dir: path9.join(roots.codexRoot, "prompts") } : null,
5271
- clients.has("cursor") ? { label: "Cursor commands", dir: path9.join(roots.cursorRoot, "commands") } : null,
5272
- clients.has("opencode") ? { label: "OpenCode commands", dir: path9.join(roots.opencodeRoot, "commands") } : null
5324
+ clients.has("claude") ? { label: "Claude commands", dir: path10.join(roots.claudeRoot, "commands") } : null,
5325
+ clients.has("factory") ? { label: "Factory commands", dir: path10.join(roots.factoryRoot, "commands") } : null,
5326
+ clients.has("codex") ? { label: "Codex prompts", dir: path10.join(roots.codexRoot, "prompts") } : null,
5327
+ clients.has("cursor") ? { label: "Cursor commands", dir: path10.join(roots.cursorRoot, "commands") } : null,
5328
+ clients.has("opencode") ? { label: "OpenCode commands", dir: path10.join(roots.opencodeRoot, "commands") } : null,
5329
+ clients.has("gemini") ? { label: "Gemini commands", dir: path10.join(roots.geminiRoot, "commands") } : null
5273
5330
  ].filter(Boolean),
5274
5331
  hooks: [
5275
- clients.has("claude") ? { label: "Claude hooks", dir: path9.join(roots.claudeRoot, "hooks") } : null,
5276
- clients.has("factory") ? { label: "Factory hooks", dir: path9.join(roots.factoryRoot, "hooks") } : null
5332
+ clients.has("claude") ? { label: "Claude hooks", dir: path10.join(roots.claudeRoot, "hooks") } : null,
5333
+ clients.has("factory") ? { label: "Factory hooks", dir: path10.join(roots.factoryRoot, "hooks") } : null
5277
5334
  ].filter(Boolean),
5278
5335
  skills: [
5279
- clients.has("claude") ? { label: "Claude skills", dir: path9.join(roots.claudeRoot, "skills") } : null,
5280
- clients.has("factory") ? { label: "Factory skills", dir: path9.join(roots.factoryRoot, "skills") } : null,
5281
- clients.has("codex") ? { label: "Codex skills", dir: path9.join(roots.codexRoot, "skills") } : null,
5282
- clients.has("cursor") ? { label: "Cursor skills", dir: path9.join(roots.cursorRoot, "skills") } : null,
5283
- clients.has("opencode") ? { label: "OpenCode skills", dir: path9.join(roots.opencodeRoot, "skills") } : null
5336
+ clients.has("claude") ? { label: "Claude skills", dir: path10.join(roots.claudeRoot, "skills") } : null,
5337
+ clients.has("factory") ? { label: "Factory skills", dir: path10.join(roots.factoryRoot, "skills") } : null,
5338
+ clients.has("codex") ? { label: "Codex skills", dir: path10.join(roots.codexRoot, "skills") } : null,
5339
+ clients.has("cursor") ? { label: "Cursor skills", dir: path10.join(roots.cursorRoot, "skills") } : null,
5340
+ clients.has("opencode") ? { label: "OpenCode skills", dir: path10.join(opencodeSkillsRoot, "skills") } : null,
5341
+ clients.has("gemini") ? { label: "Gemini skills", dir: path10.join(roots.geminiRoot, "skills") } : null,
5342
+ clients.has("github") ? opts.scope === "global" ? { label: "GitHub Copilot skills", dir: path10.join(roots.copilotRoot, "skills") } : { label: "GitHub skills", dir: path10.join(roots.githubRoot, "skills") } : null
5284
5343
  ].filter(Boolean),
5285
5344
  agents: includeAgentFiles ? [
5286
- clients.has("claude") ? { label: "Claude AGENTS.md", file: path9.join(roots.claudeRoot, "AGENTS.md") } : null,
5287
- clients.has("factory") ? { label: "Factory AGENTS.md", file: path9.join(roots.factoryRoot, "AGENTS.md") } : null,
5288
- clients.has("codex") ? { label: "Codex AGENTS.md", file: path9.join(roots.codexRoot, "AGENTS.md") } : null,
5289
- clients.has("opencode") ? { label: "OpenCode AGENTS.md", file: path9.join(roots.opencodeConfigRoot, "AGENTS.md") } : null
5345
+ clients.has("claude") ? { label: "Claude AGENTS.md", file: path10.join(roots.claudeRoot, "AGENTS.md") } : null,
5346
+ clients.has("factory") ? { label: "Factory AGENTS.md", file: path10.join(roots.factoryRoot, "AGENTS.md") } : null,
5347
+ clients.has("codex") ? { label: "Codex AGENTS.md", file: path10.join(roots.codexRoot, "AGENTS.md") } : null,
5348
+ clients.has("opencode") ? { label: "OpenCode AGENTS.md", file: path10.join(roots.opencodeConfigRoot, "AGENTS.md") } : null,
5349
+ clients.has("ampcode") ? { label: "Ampcode AGENTS.md", file: path10.join(roots.ampcodeConfigRoot, "AGENTS.md") } : null
5290
5350
  ].filter(Boolean) : [],
5291
5351
  claude: includeAgentFiles ? [
5292
- clients.has("claude") ? { label: "Claude CLAUDE.md", file: path9.join(roots.claudeRoot, "CLAUDE.md") } : null
5352
+ clients.has("claude") ? { label: "Claude CLAUDE.md", file: path10.join(roots.claudeRoot, "CLAUDE.md") } : null
5353
+ ].filter(Boolean) : [],
5354
+ gemini: includeAgentFiles ? [
5355
+ clients.has("gemini") ? { label: "Gemini GEMINI.md", file: path10.join(roots.geminiRoot, "GEMINI.md") } : null
5293
5356
  ].filter(Boolean) : []
5294
5357
  };
5295
5358
  const addCandidate = (candidate) => {
@@ -5302,7 +5365,7 @@ async function scanMigration(opts) {
5302
5365
  continue;
5303
5366
  const files = await listFiles(src.dir);
5304
5367
  for (const file of files) {
5305
- const targetPath = path9.join(canonicalCommands, path9.basename(file));
5368
+ const targetPath = path10.join(canonicalCommands, path10.basename(file));
5306
5369
  addCandidate({ label: src.label, targetPath, kind: "file", action: "copy", sourcePath: file });
5307
5370
  }
5308
5371
  }
@@ -5311,7 +5374,7 @@ async function scanMigration(opts) {
5311
5374
  continue;
5312
5375
  const files = await listFiles(src.dir);
5313
5376
  for (const file of files) {
5314
- const targetPath = path9.join(canonicalHooks, path9.basename(file));
5377
+ const targetPath = path10.join(canonicalHooks, path10.basename(file));
5315
5378
  addCandidate({ label: src.label, targetPath, kind: "file", action: "copy", sourcePath: file });
5316
5379
  }
5317
5380
  }
@@ -5321,8 +5384,8 @@ async function scanMigration(opts) {
5321
5384
  const skillDirs = await findSkillDirs(src.dir);
5322
5385
  for (const dir of skillDirs) {
5323
5386
  try {
5324
- const meta = await parseSkillFile(path9.join(dir, "SKILL.md"));
5325
- const targetPath = path9.join(canonicalSkills, meta.name);
5387
+ const meta = await parseSkillFile(path10.join(dir, "SKILL.md"));
5388
+ const targetPath = path10.join(canonicalSkills, meta.name);
5326
5389
  addCandidate({ label: src.label, targetPath, kind: "dir", action: "copy", sourcePath: dir });
5327
5390
  } catch {}
5328
5391
  }
@@ -5337,6 +5400,12 @@ async function scanMigration(opts) {
5337
5400
  continue;
5338
5401
  addCandidate({ label: src.label, targetPath: canonicalClaude, kind: "file", action: "copy", sourcePath: src.file });
5339
5402
  }
5403
+ const canonicalGemini = path10.join(canonicalRoot, "GEMINI.md");
5404
+ for (const src of sources.gemini) {
5405
+ if (!await pathExists(src.file) || await isSymlink(src.file))
5406
+ continue;
5407
+ addCandidate({ label: src.label, targetPath: canonicalGemini, kind: "file", action: "copy", sourcePath: src.file });
5408
+ }
5340
5409
  const auto = [];
5341
5410
  const conflicts = [];
5342
5411
  for (const [targetPath, list] of candidatesByTarget.entries()) {
@@ -5424,13 +5493,13 @@ async function applyMigration(plan, selections, opts) {
5424
5493
 
5425
5494
  // src/core/undo.ts
5426
5495
  import fs7 from "fs";
5427
- import path10 from "path";
5496
+ import path11 from "path";
5428
5497
  async function listBackupDirs(canonicalRoot) {
5429
- const root = path10.join(canonicalRoot, "backup");
5498
+ const root = path11.join(canonicalRoot, "backup");
5430
5499
  if (!await pathExists(root))
5431
5500
  return [];
5432
5501
  const entries = await fs7.promises.readdir(root, { withFileTypes: true });
5433
- return entries.filter((e2) => e2.isDirectory()).map((e2) => path10.join(root, e2.name));
5502
+ return entries.filter((e2) => e2.isDirectory()).map((e2) => path11.join(root, e2.name));
5434
5503
  }
5435
5504
  async function findLatestBackup(canonicalRoot) {
5436
5505
  const dirs = await listBackupDirs(canonicalRoot);
@@ -5449,7 +5518,7 @@ async function restoreSymlink(entry) {
5449
5518
  if (!entry.backupPath)
5450
5519
  return;
5451
5520
  const link = await fs7.promises.readlink(entry.backupPath);
5452
- await ensureDir(path10.dirname(entry.originalPath));
5521
+ await ensureDir(path11.dirname(entry.originalPath));
5453
5522
  await fs7.promises.symlink(link, entry.originalPath);
5454
5523
  await fs7.promises.unlink(entry.backupPath);
5455
5524
  }
@@ -5464,7 +5533,7 @@ async function restorePath(entry) {
5464
5533
  await restoreSymlink(entry);
5465
5534
  return;
5466
5535
  }
5467
- await ensureDir(path10.dirname(entry.originalPath));
5536
+ await ensureDir(path11.dirname(entry.originalPath));
5468
5537
  try {
5469
5538
  await fs7.promises.rename(entry.backupPath, entry.originalPath);
5470
5539
  return;
@@ -5527,7 +5596,7 @@ async function undoLastChange(opts) {
5527
5596
 
5528
5597
  // src/core/preflight.ts
5529
5598
  import fs8 from "fs";
5530
- import path11 from "path";
5599
+ import path12 from "path";
5531
5600
  async function needsLinkBackup(task, forceLinks) {
5532
5601
  if (task.type === "conflict")
5533
5602
  return forceLinks && task.target !== task.source;
@@ -5559,7 +5628,7 @@ async function preflightBackup(opts) {
5559
5628
  const targets = new Set;
5560
5629
  for (const task of opts.linkPlan.tasks) {
5561
5630
  if (await needsLinkBackup(task, opts.forceLinks)) {
5562
- targets.add(path11.resolve(task.target));
5631
+ targets.add(path12.resolve(task.target));
5563
5632
  }
5564
5633
  }
5565
5634
  const migrationCandidates = collectMigrationCandidates(opts.migratePlan, opts.selections);
@@ -5567,7 +5636,7 @@ async function preflightBackup(opts) {
5567
5636
  if (!candidate.sourcePath)
5568
5637
  continue;
5569
5638
  if (await pathExists(candidate.targetPath)) {
5570
- targets.add(path11.resolve(candidate.targetPath));
5639
+ targets.add(path12.resolve(candidate.targetPath));
5571
5640
  }
5572
5641
  }
5573
5642
  for (const target of targets) {
@@ -5579,8 +5648,8 @@ async function preflightBackup(opts) {
5579
5648
  await fs8.promises.readlink(target);
5580
5649
  }
5581
5650
  const dest = backupPathFor(target, opts.backup.dir);
5582
- await ensureDir(path11.dirname(dest));
5583
- await fs8.promises.access(path11.dirname(dest), fs8.constants.W_OK);
5651
+ await ensureDir(path12.dirname(dest));
5652
+ await fs8.promises.access(path12.dirname(dest), fs8.constants.W_OK);
5584
5653
  }
5585
5654
  return { targets: targets.size };
5586
5655
  }
@@ -5593,25 +5662,29 @@ function exitCancelled() {
5593
5662
  }
5594
5663
  function mergeAgentStatus(items) {
5595
5664
  const claudeEntry = items.find((s) => s.name === "claude-md") || null;
5665
+ const geminiEntry = items.find((s) => s.name === "gemini-md") || null;
5596
5666
  const agentsEntry = items.find((s) => s.name === "agents-md") || null;
5597
- if (!claudeEntry && !agentsEntry)
5667
+ if (!claudeEntry && !agentsEntry && !geminiEntry)
5598
5668
  return items;
5599
5669
  const merged = {
5600
5670
  name: "agents-md",
5601
- source: claudeEntry?.source || agentsEntry?.source || "",
5671
+ source: claudeEntry?.source || geminiEntry?.source || agentsEntry?.source || "",
5602
5672
  targets: [
5603
5673
  ...claudeEntry?.targets || [],
5674
+ ...geminiEntry?.targets || [],
5604
5675
  ...agentsEntry?.targets || []
5605
5676
  ]
5606
5677
  };
5607
- const withoutAgents = items.filter((s) => s.name !== "claude-md" && s.name !== "agents-md");
5678
+ const withoutAgents = items.filter((s) => s.name !== "claude-md" && s.name !== "gemini-md" && s.name !== "agents-md");
5608
5679
  return [merged, ...withoutAgents];
5609
5680
  }
5610
5681
  function displayName(entry) {
5611
5682
  if (entry.name === "agents-md") {
5612
- const sourceFile = path12.basename(entry.source);
5683
+ const sourceFile = path13.basename(entry.source);
5613
5684
  if (sourceFile === "CLAUDE.md")
5614
5685
  return "AGENTS.md (Claude override)";
5686
+ if (sourceFile === "GEMINI.md")
5687
+ return "AGENTS.md (Gemini override)";
5615
5688
  return "AGENTS.md";
5616
5689
  }
5617
5690
  return entry.name;
@@ -5683,7 +5756,10 @@ async function selectClients() {
5683
5756
  { label: "Factory", value: "factory" },
5684
5757
  { label: "Codex", value: "codex" },
5685
5758
  { label: "Cursor", value: "cursor" },
5686
- { label: "OpenCode", value: "opencode" }
5759
+ { label: "OpenCode", value: "opencode" },
5760
+ { label: "Gemini", value: "gemini" },
5761
+ { label: "GitHub", value: "github" },
5762
+ { label: "Ampcode", value: "ampcode" }
5687
5763
  ];
5688
5764
  const selected = await ae({
5689
5765
  message: "Select clients to manage",
@@ -5701,7 +5777,10 @@ function formatClients(clients) {
5701
5777
  factory: "Factory",
5702
5778
  codex: "Codex",
5703
5779
  cursor: "Cursor",
5704
- opencode: "OpenCode"
5780
+ opencode: "OpenCode",
5781
+ gemini: "Gemini",
5782
+ github: "GitHub",
5783
+ ampcode: "Ampcode"
5705
5784
  };
5706
5785
  return clients.map((c) => names[c]).join(", ");
5707
5786
  }
@@ -5732,7 +5811,7 @@ async function runChange(scope, clients) {
5732
5811
  const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
5733
5812
  const migrate = await scanMigration({ scope, clients });
5734
5813
  const link = await buildLinkPlan({ scope, clients });
5735
- const backupDir = path12.join(roots.canonicalRoot, "backup", timestamp);
5814
+ const backupDir = path13.join(roots.canonicalRoot, "backup", timestamp);
5736
5815
  spin.stop("Scan complete");
5737
5816
  const planSummary = [
5738
5817
  `Migration: ${migrate.auto.length} auto · ${migrate.conflicts.length} conflicts (choose sources)`,
@@ -5818,8 +5897,10 @@ async function run() {
5818
5897
  ].join(`
5819
5898
  `), "Overview");
5820
5899
  const options2 = [];
5821
- if (changes > 0)
5822
- options2.push({ label: `Apply ${changes} changes to .agents`, value: "change" });
5900
+ if (changes > 0 || conflicts > 0) {
5901
+ const label = conflicts > 0 && changes === 0 ? `Resolve ${conflicts} ${pluralize(conflicts, "conflict")}` : `Apply ${changes} ${pluralize(changes, "change")}`;
5902
+ options2.push({ label, value: "change" });
5903
+ }
5823
5904
  options2.push({ label: "View status", value: "status" });
5824
5905
  options2.push({ label: "Change clients", value: "clients" });
5825
5906
  options2.push({ label: "Undo last change", value: "undo" });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iannuttall/dotagents",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
4
4
  "description": "Canonical .agents manager with symlinks for popular AI tools",
5
5
  "type": "module",
6
6
  "license": "MIT",