agentradar-cli 0.2.5 → 0.2.6

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,4 +1,4 @@
1
- import { chmod, lstat, mkdir, readFile, readdir, realpath, rename, rm, unlink, writeFile } from "node:fs/promises";
1
+ import { chmod, lstat, mkdir, readFile, readdir, realpath, rename, rm, stat, unlink, writeFile } from "node:fs/promises";
2
2
  import { randomUUID } from "node:crypto";
3
3
  import { dirname, isAbsolute, join, normalize, sep } from "node:path";
4
4
  import { awpRequest, envValue } from "./tool-run.js";
@@ -50,6 +50,21 @@ const isManaged = async (dir) => (await lstat(dir).then((info) => info.isDirecto
50
50
  * untrusted input to this box, so names and paths are checked, each skill is staged then renamed into place,
51
51
  * a folder without the marker (the vendor's own) is never touched, and a managed skill no longer listed goes.
52
52
  */
53
+ const sameSkill = async (dir, files) => {
54
+ try {
55
+ const have = [];
56
+ for (const p of await readdir(dir, { recursive: true }))
57
+ if (p !== MANAGED_MARK && (await stat(join(dir, p))).isFile())
58
+ have.push(p);
59
+ for (const f of files)
60
+ if (await readFile(join(dir, f.path), "utf8") !== f.content)
61
+ return false;
62
+ return have.length === files.length;
63
+ }
64
+ catch {
65
+ return false;
66
+ }
67
+ };
53
68
  export async function writeManagedSkills(skillsDir, skills) {
54
69
  await mkdir(skillsDir, { recursive: true });
55
70
  const written = new Set();
@@ -64,7 +79,13 @@ export async function writeManagedSkills(skillsDir, skills) {
64
79
  if (files.some((f) => typeof f?.path !== "string" || typeof f?.content !== "string" || isAbsolute(f.path)
65
80
  || normalize(f.path).split(sep).includes("..") || [".", MANAGED_MARK].includes(normalize(f.path))))
66
81
  continue;
82
+ // An unchanged skill is not rewritten: the agent's CLI may be reading it.
83
+ if (await sameSkill(target, files)) {
84
+ written.add(name);
85
+ continue;
86
+ }
67
87
  const staging = join(skillsDir, `.${name}.sync-${process.pid}`);
88
+ const old = join(skillsDir, `.${name}.old-${process.pid}`);
68
89
  try {
69
90
  await rm(staging, { recursive: true, force: true });
70
91
  for (const f of files) {
@@ -72,12 +93,18 @@ export async function writeManagedSkills(skillsDir, skills) {
72
93
  await writeFile(join(staging, f.path), f.content);
73
94
  }
74
95
  await writeFile(join(staging, MANAGED_MARK), "Written by the AgentRadar runner; replaced on every sync.\n");
75
- await rm(target, { recursive: true, force: true });
96
+ // Rename aside, then into place: the skill is missing for two renames, not for a recursive delete.
97
+ await rm(old, { recursive: true, force: true });
98
+ await rename(target, old).catch((err) => { if (err?.code !== "ENOENT")
99
+ throw err; });
76
100
  await rename(staging, target);
101
+ await rm(old, { recursive: true, force: true });
77
102
  written.add(name);
78
103
  }
79
104
  catch {
80
105
  await rm(staging, { recursive: true, force: true });
106
+ if (!(await lstat(target).then(() => true, () => false)))
107
+ await rename(old, target).catch(() => { });
81
108
  if (await isManaged(target))
82
109
  written.add(name);
83
110
  }
@@ -663,6 +663,14 @@ const isManaged = (dir) => {
663
663
  const st = fs.lstatSync(dir, { throwIfNoEntry: false });
664
664
  return Boolean(st?.isDirectory() && fs.existsSync(join(dir, MANAGED_MARK)));
665
665
  };
666
+ const sameSkill = (dir, files) => {
667
+ try {
668
+ const have = fs.readdirSync(dir, { recursive: true }).filter((p) => p !== MANAGED_MARK && fs.statSync(join(dir, p)).isFile());
669
+ return have.length === files.length && files.every((f) => fs.readFileSync(join(dir, f.path), "utf8") === f.content);
670
+ } catch {
671
+ return false;
672
+ }
673
+ };
666
674
 
667
675
  export function writeManagedSkills(skillsDir, skills) {
668
676
  fs.mkdirSync(skillsDir, { recursive: true });
@@ -679,7 +687,11 @@ export function writeManagedSkills(skillsDir, skills) {
679
687
  const bad = files.find((f) => typeof f?.path !== "string" || typeof f?.content !== "string" || isAbsolute(f.path)
680
688
  || normalize(f.path).split(sep).includes("..") || [".", MANAGED_MARK].includes(normalize(f.path)));
681
689
  if (bad) { console.warn(`[feeder] skill sync: ${name} has an unsafe file path, skipped`); continue; }
690
+ // An unchanged skill is not rewritten: the agent's CLI may be reading it (Codex skipped a SKILL.md that a
691
+ // periodic resync had just removed).
692
+ if (sameSkill(target, files)) { written.add(name); continue; }
682
693
  const staging = join(skillsDir, `.${name}.sync-${process.pid}`);
694
+ const old = join(skillsDir, `.${name}.old-${process.pid}`);
683
695
  try {
684
696
  fs.rmSync(staging, { recursive: true, force: true });
685
697
  for (const f of files) {
@@ -687,12 +699,16 @@ export function writeManagedSkills(skillsDir, skills) {
687
699
  fs.writeFileSync(join(staging, f.path), f.content);
688
700
  }
689
701
  fs.writeFileSync(join(staging, MANAGED_MARK), "Written by the AgentRadar runner; replaced on every sync.\n");
690
- fs.rmSync(target, { recursive: true, force: true });
702
+ // Rename aside, then into place: the skill is missing for two renames, not for a recursive delete.
703
+ fs.rmSync(old, { recursive: true, force: true });
704
+ if (fs.existsSync(target)) fs.renameSync(target, old);
691
705
  fs.renameSync(staging, target);
706
+ fs.rmSync(old, { recursive: true, force: true });
692
707
  written.add(name);
693
708
  } catch (err) {
694
709
  // One skill that cannot be written keeps its previous copy and does not stop the rest of the sync.
695
710
  fs.rmSync(staging, { recursive: true, force: true });
711
+ if (!fs.existsSync(target) && fs.existsSync(old)) fs.renameSync(old, target);
696
712
  if (isManaged(target)) written.add(name);
697
713
  console.warn(`[feeder] skill sync: ${name} not written: ${err instanceof Error ? err.message : String(err)}`);
698
714
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentradar-cli",
3
- "version": "0.2.5",
3
+ "version": "0.2.6",
4
4
  "engines": {
5
5
  "node": ">=20"
6
6
  },