@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.
- package/README.md +15 -6
- package/dist/cli.js +176 -95
- 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
|
|
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
|
-
-
|
|
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
|
|
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
|
|
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
|
|
4824
|
-
const
|
|
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:
|
|
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(
|
|
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 (!
|
|
4889
|
-
return
|
|
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 &&
|
|
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) =>
|
|
4921
|
-
if (relinkable.has(
|
|
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
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
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
|
|
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 (!
|
|
4956
|
-
return
|
|
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 &&
|
|
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
|
|
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
|
|
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(
|
|
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 =
|
|
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
|
|
5101
|
+
import path9 from "path";
|
|
5048
5102
|
|
|
5049
5103
|
// src/core/backup.ts
|
|
5050
5104
|
import fs4 from "fs";
|
|
5051
|
-
import
|
|
5105
|
+
import path8 from "path";
|
|
5052
5106
|
var MANIFEST_NAME = "manifest.json";
|
|
5053
5107
|
function backupPathFor(target, backupRoot) {
|
|
5054
|
-
const root =
|
|
5055
|
-
const rel =
|
|
5056
|
-
return
|
|
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 =
|
|
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 +
|
|
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(
|
|
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(
|
|
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 =
|
|
5146
|
+
const dir = path8.join(opts.canonicalRoot, "backup", timestamp);
|
|
5093
5147
|
await ensureDir(dir);
|
|
5094
5148
|
return {
|
|
5095
5149
|
dir,
|
|
5096
|
-
manifestPath:
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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(
|
|
5221
|
+
await ensureDir(path9.dirname(target));
|
|
5167
5222
|
const type2 = kind === "dir" ? "junction" : "file";
|
|
5168
|
-
await fs5.promises.symlink(
|
|
5223
|
+
await fs5.promises.symlink(linkTarget.link, target, type2);
|
|
5169
5224
|
return { created: true, backedUp };
|
|
5170
5225
|
}
|
|
5171
|
-
await ensureDir(
|
|
5226
|
+
await ensureDir(path9.dirname(target));
|
|
5172
5227
|
const type = kind === "dir" ? "junction" : "file";
|
|
5173
|
-
await fs5.promises.symlink(
|
|
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) =>
|
|
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 =
|
|
5251
|
-
return rel ||
|
|
5305
|
+
const rel = path10.relative(canonicalRoot, targetPath);
|
|
5306
|
+
return rel || path10.basename(targetPath);
|
|
5252
5307
|
}
|
|
5253
|
-
return
|
|
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 =
|
|
5262
|
-
const canonicalHooks =
|
|
5263
|
-
const canonicalSkills =
|
|
5264
|
-
const canonicalAgents =
|
|
5265
|
-
const canonicalClaude =
|
|
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:
|
|
5269
|
-
clients.has("factory") ? { label: "Factory commands", dir:
|
|
5270
|
-
clients.has("codex") ? { label: "Codex prompts", dir:
|
|
5271
|
-
clients.has("cursor") ? { label: "Cursor commands", dir:
|
|
5272
|
-
clients.has("opencode") ? { label: "OpenCode commands", dir:
|
|
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:
|
|
5276
|
-
clients.has("factory") ? { label: "Factory hooks", dir:
|
|
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:
|
|
5280
|
-
clients.has("factory") ? { label: "Factory skills", dir:
|
|
5281
|
-
clients.has("codex") ? { label: "Codex skills", dir:
|
|
5282
|
-
clients.has("cursor") ? { label: "Cursor skills", dir:
|
|
5283
|
-
clients.has("opencode") ? { label: "OpenCode skills", dir:
|
|
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:
|
|
5287
|
-
clients.has("factory") ? { label: "Factory AGENTS.md", file:
|
|
5288
|
-
clients.has("codex") ? { label: "Codex AGENTS.md", file:
|
|
5289
|
-
clients.has("opencode") ? { label: "OpenCode AGENTS.md", file:
|
|
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:
|
|
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 =
|
|
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 =
|
|
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(
|
|
5325
|
-
const targetPath =
|
|
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
|
|
5496
|
+
import path11 from "path";
|
|
5428
5497
|
async function listBackupDirs(canonicalRoot) {
|
|
5429
|
-
const root =
|
|
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) =>
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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(
|
|
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(
|
|
5583
|
-
await fs8.promises.access(
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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" });
|