@iannuttall/dotagents 0.1.0 → 0.1.1

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 +2 -0
  2. package/dist/cli.js +168 -10
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -52,6 +52,8 @@ Global home affects all projects. Project folder only affects the current direct
52
52
 
53
53
  `.agents/skills` → `~/.factory/skills`
54
54
 
55
+ `.agents/skills` → `~/.codex/skills`
56
+
55
57
  ## Development
56
58
 
57
59
  Run the TUI in dev mode:
package/dist/cli.js CHANGED
@@ -3498,6 +3498,7 @@ import { render } from "ink";
3498
3498
  // src/tui/App.tsx
3499
3499
  import { useEffect as useEffect2, useMemo, useState } from "react";
3500
3500
  import fs10 from "fs";
3501
+ import path15 from "path";
3501
3502
  import { Box as Box4, Text as Text2, useApp, useInput as useInput2, useStdout as useStdout3 } from "ink";
3502
3503
  import SelectInput from "ink-select-input";
3503
3504
  import TextInput from "ink-text-input";
@@ -3571,7 +3572,8 @@ function getMappings(opts) {
3571
3572
  source: path2.join(canonical, "skills"),
3572
3573
  targets: [
3573
3574
  path2.join(roots.claudeRoot, "skills"),
3574
- path2.join(roots.factoryRoot, "skills")
3575
+ path2.join(roots.factoryRoot, "skills"),
3576
+ path2.join(roots.codexRoot, "skills")
3575
3577
  ],
3576
3578
  kind: "dir"
3577
3579
  }
@@ -3719,26 +3721,91 @@ async function createSource(task) {
3719
3721
  }
3720
3722
  await ensureFile(task.path, DEFAULT_AGENTS);
3721
3723
  }
3722
- async function createLink(source, target, kind, force) {
3724
+ function commonPath(paths) {
3725
+ if (paths.length === 0)
3726
+ return null;
3727
+ const splitPaths = paths.map((p) => path5.resolve(p).split(path5.sep));
3728
+ const minLen = Math.min(...splitPaths.map((parts) => parts.length));
3729
+ const shared = [];
3730
+ for (let i = 0;i < minLen; i += 1) {
3731
+ const segment = splitPaths[0]?.[i];
3732
+ if (!segment)
3733
+ break;
3734
+ if (splitPaths.every((parts) => parts[i] === segment)) {
3735
+ shared.push(segment);
3736
+ } else {
3737
+ break;
3738
+ }
3739
+ }
3740
+ if (shared.length === 0)
3741
+ return null;
3742
+ if (shared[0] === "")
3743
+ return path5.sep + shared.slice(1).join(path5.sep);
3744
+ return shared.join(path5.sep);
3745
+ }
3746
+ function inferBackupDir(plan) {
3747
+ const sourceDirs = plan.tasks.map((task) => task.type === "ensure-source" ? task.path : task.source).filter(Boolean).map((p) => path5.dirname(p));
3748
+ const root = commonPath(sourceDirs);
3749
+ if (!root)
3750
+ return null;
3751
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
3752
+ return path5.join(root, "backup", timestamp);
3753
+ }
3754
+ async function backupTarget(target, backupDir) {
3755
+ if (!await pathExists(target))
3756
+ return false;
3757
+ const stat = await fs3.promises.lstat(target);
3758
+ if (stat.isSymbolicLink())
3759
+ return false;
3760
+ const root = path5.parse(target).root || path5.sep;
3761
+ const rel = path5.relative(root, target);
3762
+ const dest = path5.join(backupDir, rel);
3763
+ await ensureDir(path5.dirname(dest));
3764
+ try {
3765
+ await fs3.promises.rename(target, dest);
3766
+ return true;
3767
+ } catch (err) {
3768
+ if (err?.code !== "EXDEV")
3769
+ throw err;
3770
+ }
3771
+ if (stat.isDirectory()) {
3772
+ await copyDir(target, dest, true);
3773
+ } else {
3774
+ await copyFile(target, dest, true);
3775
+ }
3776
+ await removePath(target);
3777
+ return true;
3778
+ }
3779
+ async function createLink(source, target, kind, force, backupDir) {
3723
3780
  if (await pathExists(target)) {
3724
3781
  if (!force)
3725
- return;
3782
+ return { created: false, backedUp: false };
3783
+ const backedUp = backupDir ? await backupTarget(target, backupDir) : false;
3726
3784
  await removePath(target);
3785
+ await ensureDir(path5.dirname(target));
3786
+ const type2 = kind === "dir" ? "junction" : "file";
3787
+ await fs3.promises.symlink(source, target, type2);
3788
+ return { created: true, backedUp };
3727
3789
  }
3728
3790
  await ensureDir(path5.dirname(target));
3729
3791
  const type = kind === "dir" ? "junction" : "file";
3730
3792
  await fs3.promises.symlink(source, target, type);
3793
+ return { created: true, backedUp: false };
3731
3794
  }
3732
3795
  async function applyLinkPlan(plan, opts) {
3733
3796
  const force = !!opts?.force;
3797
+ const backupDir = force ? opts?.backupDir || inferBackupDir(plan) || undefined : undefined;
3734
3798
  let applied = 0;
3735
3799
  let skipped = 0;
3736
3800
  let conflicts = 0;
3801
+ let backedUp = 0;
3737
3802
  for (const task of plan.tasks) {
3738
3803
  if (task.type === "conflict") {
3739
3804
  conflicts += 1;
3740
3805
  if (force && task.target !== task.source && task.kind) {
3741
- await createLink(task.source, task.target, task.kind, true);
3806
+ const result = await createLink(task.source, task.target, task.kind, true, backupDir);
3807
+ if (result.backedUp)
3808
+ backedUp += 1;
3742
3809
  applied += 1;
3743
3810
  }
3744
3811
  continue;
@@ -3754,14 +3821,16 @@ async function applyLinkPlan(plan, opts) {
3754
3821
  }
3755
3822
  if (task.type === "link") {
3756
3823
  const before = await pathExists(task.target);
3757
- await createLink(task.source, task.target, task.kind, force);
3824
+ const result = await createLink(task.source, task.target, task.kind, force, backupDir);
3825
+ if (result.backedUp)
3826
+ backedUp += 1;
3758
3827
  if (before && !force)
3759
3828
  skipped += 1;
3760
3829
  else
3761
3830
  applied += 1;
3762
3831
  }
3763
3832
  }
3764
- return { applied, skipped, conflicts };
3833
+ return { applied, skipped, conflicts, backupDir, backedUp };
3765
3834
  }
3766
3835
 
3767
3836
  // src/core/status.ts
@@ -3923,7 +3992,8 @@ async function scanMigration(opts) {
3923
3992
  ],
3924
3993
  skills: [
3925
3994
  { label: "Claude skills", dir: path8.join(roots.claudeRoot, "skills") },
3926
- { label: "Factory skills", dir: path8.join(roots.factoryRoot, "skills") }
3995
+ { label: "Factory skills", dir: path8.join(roots.factoryRoot, "skills") },
3996
+ { label: "Codex skills", dir: path8.join(roots.codexRoot, "skills") }
3927
3997
  ],
3928
3998
  agents: [
3929
3999
  { label: "Claude CLAUDE.md", file: path8.join(roots.claudeRoot, "CLAUDE.md") }
@@ -4006,6 +4076,7 @@ async function scanMigration(opts) {
4006
4076
  { label: "factory/hooks", path: path8.join(roots.factoryRoot, "hooks"), kind: "dir" },
4007
4077
  { label: "claude/skills", path: path8.join(roots.claudeRoot, "skills"), kind: "dir" },
4008
4078
  { label: "factory/skills", path: path8.join(roots.factoryRoot, "skills"), kind: "dir" },
4079
+ { label: "codex/skills", path: path8.join(roots.codexRoot, "skills"), kind: "dir" },
4009
4080
  { label: "claude/CLAUDE.md", path: path8.join(roots.claudeRoot, "CLAUDE.md"), kind: "file" }
4010
4081
  ];
4011
4082
  return { auto, conflicts, backupPaths, canonicalRoot };
@@ -4605,6 +4676,7 @@ var App = () => {
4605
4676
  const [skillInput, setSkillInput] = useState("");
4606
4677
  const [marketplaceInput, setMarketplaceInput] = useState("");
4607
4678
  const [marketplacePlugins, setMarketplacePlugins] = useState([]);
4679
+ const [forceBackupDir, setForceBackupDir] = useState(null);
4608
4680
  const [showDetails, setShowDetails] = useState(false);
4609
4681
  const [conflictsOnly, setConflictsOnly] = useState(false);
4610
4682
  const { stdout } = useStdout3();
@@ -4622,6 +4694,8 @@ var App = () => {
4622
4694
  return setStep("scope");
4623
4695
  if (step === "status")
4624
4696
  return setStep("action");
4697
+ if (step === "force-confirm")
4698
+ return setStep("action");
4625
4699
  if (step === "migrate-choice") {
4626
4700
  setMigratePlan(null);
4627
4701
  setMigrateSelections(new Map);
@@ -4659,7 +4733,7 @@ var App = () => {
4659
4733
  { label: "Apply/repair links", value: "apply" }
4660
4734
  ];
4661
4735
  if (conflicts > 0)
4662
- items.push({ label: "Force apply (overwrite conflicts)", value: "force-apply" });
4736
+ items.push({ label: "Force apply (backup + overwrite conflicts)", value: "force-apply" });
4663
4737
  items.push({ label: "View status", value: "view-status" });
4664
4738
  items.push({ label: "Migrate existing content", value: "migrate" });
4665
4739
  items.push({ label: "Add skill", value: "add-skill" });
@@ -4892,13 +4966,33 @@ var App = () => {
4892
4966
  return;
4893
4967
  }
4894
4968
  if (item.value === "apply" || item.value === "force-apply") {
4895
- setBusy(item.value === "force-apply" ? "Applying (force)..." : "Applying...");
4969
+ if (item.value === "force-apply") {
4970
+ setBusy("Preparing force apply...");
4971
+ setStep("applying");
4972
+ (async () => {
4973
+ try {
4974
+ if (!scope)
4975
+ return;
4976
+ const roots = resolveRoots({ scope });
4977
+ const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
4978
+ setForceBackupDir(path15.join(roots.canonicalRoot, "backup", timestamp));
4979
+ setStep("force-confirm");
4980
+ } catch (err) {
4981
+ setMessage(err?.message || String(err));
4982
+ setStep("done");
4983
+ } finally {
4984
+ setBusy(null);
4985
+ }
4986
+ })();
4987
+ return;
4988
+ }
4989
+ setBusy("Applying...");
4896
4990
  setStep("applying");
4897
4991
  (async () => {
4898
4992
  try {
4899
4993
  if (!plan || !scope)
4900
4994
  return;
4901
- const result = await applyLinkPlan(plan, { force: item.value === "force-apply" });
4995
+ const result = await applyLinkPlan(plan);
4902
4996
  setMessage(`Applied: ${result.applied}, Skipped: ${result.skipped}, Conflicts: ${result.conflicts}`);
4903
4997
  await refreshStatus(scope);
4904
4998
  } catch (err) {
@@ -4981,6 +5075,70 @@ var App = () => {
4981
5075
  ]
4982
5076
  }, undefined, true, undefined, this);
4983
5077
  }
5078
+ if (step === "force-confirm") {
5079
+ return /* @__PURE__ */ jsxDEV4(Screen, {
5080
+ children: [
5081
+ /* @__PURE__ */ jsxDEV4(Text2, {
5082
+ color: "yellow",
5083
+ children: "Force apply will overwrite existing real files/directories."
5084
+ }, undefined, false, undefined, this),
5085
+ /* @__PURE__ */ jsxDEV4(Box4, {
5086
+ flexDirection: "column",
5087
+ marginTop: 1,
5088
+ children: [
5089
+ /* @__PURE__ */ jsxDEV4(Text2, {
5090
+ children: "Backup will be created at:"
5091
+ }, undefined, false, undefined, this),
5092
+ /* @__PURE__ */ jsxDEV4(Text2, {
5093
+ dimColor: true,
5094
+ children: forceBackupDir || "(pending)"
5095
+ }, undefined, false, undefined, this)
5096
+ ]
5097
+ }, undefined, true, undefined, this),
5098
+ /* @__PURE__ */ jsxDEV4(Box4, {
5099
+ marginTop: 1,
5100
+ flexDirection: "column",
5101
+ children: [
5102
+ /* @__PURE__ */ jsxDEV4(SelectInput, {
5103
+ items: [
5104
+ { label: "Proceed (create backup + overwrite)", value: "proceed" },
5105
+ { label: "Cancel", value: "cancel" }
5106
+ ],
5107
+ onSelect: (item) => {
5108
+ if (item.value === "cancel") {
5109
+ setForceBackupDir(null);
5110
+ setStep("action");
5111
+ return;
5112
+ }
5113
+ setBusy("Applying (force)...");
5114
+ setStep("applying");
5115
+ (async () => {
5116
+ try {
5117
+ if (!plan || !scope)
5118
+ return;
5119
+ const backupDir = forceBackupDir || undefined;
5120
+ const result = await applyLinkPlan(plan, { force: true, backupDir });
5121
+ const backupNote = result.backedUp > 0 && result.backupDir ? `, Backed up: ${result.backedUp} (${result.backupDir})` : "";
5122
+ setMessage(`Applied: ${result.applied}, Skipped: ${result.skipped}, Conflicts: ${result.conflicts}${backupNote}`);
5123
+ await refreshStatus(scope);
5124
+ } catch (err) {
5125
+ setMessage(err?.message || String(err));
5126
+ } finally {
5127
+ setForceBackupDir(null);
5128
+ setBusy(null);
5129
+ setStep("done");
5130
+ }
5131
+ })();
5132
+ }
5133
+ }, undefined, false, undefined, this),
5134
+ /* @__PURE__ */ jsxDEV4(HelpBar, {
5135
+ text: "Enter to confirm · Esc to cancel · q to quit"
5136
+ }, undefined, false, undefined, this)
5137
+ ]
5138
+ }, undefined, true, undefined, this)
5139
+ ]
5140
+ }, undefined, true, undefined, this);
5141
+ }
4984
5142
  if (step === "migrate-choice" && migratePlan) {
4985
5143
  const conflict = migratePlan.conflicts[migrateIndex];
4986
5144
  if (!conflict) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@iannuttall/dotagents",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Canonical .agents manager with symlinks for popular AI tools",
5
5
  "type": "module",
6
6
  "license": "MIT",