@ericrisco/rsc 2.0.7 → 2.0.8

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/manifest.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.0.7",
2
+ "version": "2.0.8",
3
3
  "counts": {
4
4
  "skills": 273,
5
5
  "agents": 33,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ericrisco/rsc",
3
- "version": "2.0.7",
3
+ "version": "2.0.8",
4
4
  "description": "A guided harness builder that turns your goal into a minimal, verified AI-agent setup using strict best practices.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -44,6 +44,12 @@ function agentSpecFor(target, root) {
44
44
  return { dir: dirname(p), ext };
45
45
  }
46
46
 
47
+ // A real file, or a link that leads to one. `statSync` follows; a dangling link throws and is
48
+ // therefore not a file, which is the answer we want.
49
+ function isFile(path) {
50
+ try { return statSync(path).isFile(); } catch { return false; }
51
+ }
52
+
47
53
  // Real files only. Directories (a dir named `foo.md` is not an agent), dangling
48
54
  // symlinks and dotfiles are not capabilities.
49
55
  function entriesWithExt(dir, ext) {
@@ -96,8 +102,19 @@ export function listSkills({ target, home, cwd = process.cwd() } = {}) {
96
102
  try {
97
103
  for (const e of readdirSync(paths.dir, { withFileTypes: true })) {
98
104
  if (e.name.startsWith('.')) continue;
99
- if (ext) { if (e.isFile() && e.name.endsWith(ext)) out.push({ id: e.name.slice(0, -ext.length), scope }); }
100
- else if (e.isDirectory() && existsSync(join(paths.dir, e.name, 'SKILL.md'))) out.push({ id: e.name, scope });
105
+ // Ask what a skill IS, never how it was put on disk. A `Dirent` from `withFileTypes`
106
+ // describes the LINK and not its target, so `isDirectory()`/`isFile()` are both false for
107
+ // every skill on macOS and Linux — where a symlink per assistant, pointing at one shared
108
+ // copy, is the layout the installer deliberately writes. Windows gets real files (relative
109
+ // dir symlinks need Developer Mode there), which is why this read as correct for so long
110
+ // and why CI never caught it.
111
+ //
112
+ // `existsSync` and `statSync` follow links on their own, so one question answers all three
113
+ // platforms with no branch: is there a SKILL.md behind this name? Following a link is not
114
+ // the same as believing anything — a dangling link, a directory with no SKILL.md and a
115
+ // plain file all still fail it.
116
+ if (ext) { if (e.name.endsWith(ext) && isFile(join(paths.dir, e.name))) out.push({ id: e.name.slice(0, -ext.length), scope }); }
117
+ else if (existsSync(join(paths.dir, e.name, 'SKILL.md'))) out.push({ id: e.name, scope });
101
118
  }
102
119
  } catch { /* unreadable scope → contributes nothing */ }
103
120
  }