fix(codex): keep commented TOML fields, follow symlinked config surfaces
readTomlMeta was dropping any field with a trailing `# comment` (common in real agents/*.toml). readSkills/readAgents/readRules used bare isDirectory()/isFile(), which miss symlinked entries per cc-discovery.js's own documented Dirent quirk — reused its isDirLike/isFileLike instead (exporting isDirLike from cc-discovery.js, which wasn't previously public). Also swapped the "never walks outside" test for one that actually exercises a symlink escape, and dropped the unnecessary isUnder re-export.
This commit is contained in:
@@ -85,6 +85,14 @@ describe("codex-discovery", () => {
|
|||||||
assert.equal(meta.model, "gpt-5.3-codex-spark");
|
assert.equal(meta.model, "gpt-5.3-codex-spark");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("keeps a field with a trailing inline comment, and keeps a '#' that is inside the string", () => {
|
||||||
|
const meta = cx.readTomlMeta(
|
||||||
|
'model = "gpt-5.4" # default\n' + 'description = "uses # for headings"\n'
|
||||||
|
);
|
||||||
|
assert.equal(meta.model, "gpt-5.4");
|
||||||
|
assert.equal(meta.description, "uses # for headings");
|
||||||
|
});
|
||||||
|
|
||||||
it("reads a rule file's preview", () => {
|
it("reads a rule file's preview", () => {
|
||||||
write(
|
write(
|
||||||
path.join(TMP, "rules", "no-force-push.rules"),
|
path.join(TMP, "rules", "no-force-push.rules"),
|
||||||
@@ -119,11 +127,20 @@ describe("codex-discovery", () => {
|
|||||||
assert.equal(project.exists, false);
|
assert.equal(project.exists, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("never walks outside the codex roots", () => {
|
it("follows a symlinked skill dir (a supported install pattern) without ever reporting a path outside the codex roots", () => {
|
||||||
write(path.join(TMP, "skills", "esc", "SKILL.md"), "x");
|
const outside = fs.mkdtempSync(path.join(os.tmpdir(), "codex-disc-outside-"));
|
||||||
for (const row of cx.readSkills({ cwd: REPO })) {
|
write(path.join(outside, "SKILL.md"), "---\nname: linked\n---\nbody");
|
||||||
|
fs.symlinkSync(outside, path.join(TMP, "skills", "linked"), "dir");
|
||||||
|
|
||||||
|
const rows = cx.readSkills({ cwd: REPO });
|
||||||
|
const linked = rows.find((r) => r.name === "linked");
|
||||||
|
assert.ok(linked, "a symlinked skill directory must still be discovered");
|
||||||
|
assert.equal(linked.frontmatter.name, "linked");
|
||||||
|
for (const row of rows) {
|
||||||
assert.ok(row.file.startsWith(TMP) || row.file.startsWith(REPO));
|
assert.ok(row.file.startsWith(TMP) || row.file.startsWith(REPO));
|
||||||
|
assert.ok(!row.file.startsWith(outside));
|
||||||
}
|
}
|
||||||
|
fs.rmSync(outside, { recursive: true, force: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
it("summarizes counts and roots in the overview", () => {
|
it("summarizes counts and roots in the overview", () => {
|
||||||
|
|||||||
@@ -925,6 +925,7 @@ module.exports = {
|
|||||||
parseFrontmatter,
|
parseFrontmatter,
|
||||||
redactSettings,
|
redactSettings,
|
||||||
isUnder,
|
isUnder,
|
||||||
|
isDirLike,
|
||||||
isFileLike,
|
isFileLike,
|
||||||
MAX_FILE_BYTES,
|
MAX_FILE_BYTES,
|
||||||
HOOK_EVENT_TYPES,
|
HOOK_EVENT_TYPES,
|
||||||
|
|||||||
@@ -15,7 +15,13 @@
|
|||||||
const fs = require("node:fs");
|
const fs = require("node:fs");
|
||||||
const path = require("node:path");
|
const path = require("node:path");
|
||||||
const { getCodexHome, getProjectCodexDir, getProjectRoot } = require("./codex-home");
|
const { getCodexHome, getProjectCodexDir, getProjectRoot } = require("./codex-home");
|
||||||
const { parseFrontmatter, isUnder, MAX_FILE_BYTES } = require("./cc-discovery");
|
const {
|
||||||
|
parseFrontmatter,
|
||||||
|
isUnder,
|
||||||
|
isDirLike,
|
||||||
|
isFileLike,
|
||||||
|
MAX_FILE_BYTES,
|
||||||
|
} = require("./cc-discovery");
|
||||||
|
|
||||||
function safeReadText(absPath) {
|
function safeReadText(absPath) {
|
||||||
try {
|
try {
|
||||||
@@ -55,7 +61,7 @@ function readSkillsAt(scope, codexDir) {
|
|||||||
const skills = [];
|
const skills = [];
|
||||||
for (const ent of listDir(dir)) {
|
for (const ent of listDir(dir)) {
|
||||||
const skillDir = path.join(dir, ent.name);
|
const skillDir = path.join(dir, ent.name);
|
||||||
if (!ent.isDirectory() || !isUnder(codexDir, skillDir)) continue;
|
if (!isDirLike(ent, skillDir) || !isUnder(codexDir, skillDir)) continue;
|
||||||
const skillFile = path.join(skillDir, "SKILL.md");
|
const skillFile = path.join(skillDir, "SKILL.md");
|
||||||
const read = safeReadText(skillFile);
|
const read = safeReadText(skillFile);
|
||||||
if (!read) continue;
|
if (!read) continue;
|
||||||
@@ -116,10 +122,16 @@ function readTomlMeta(text) {
|
|||||||
(value.startsWith("'") && value.endsWith("'"))
|
(value.startsWith("'") && value.endsWith("'"))
|
||||||
) {
|
) {
|
||||||
value = value.slice(1, -1);
|
value = value.slice(1, -1);
|
||||||
} else if (value === "true" || value === "false") {
|
} else if (value.startsWith('"') || value.startsWith("'")) {
|
||||||
value = value === "true";
|
// A quoted value with a trailing `# comment` outside the string —
|
||||||
} else if (/^-?\d+(\.\d+)?$/.test(value)) {
|
// e.g. model = "gpt-5.4" # default. Strip from the closing quote on.
|
||||||
value = Number(value);
|
const quote = value[0];
|
||||||
|
const closeIdx = value.indexOf(quote, 1);
|
||||||
|
if (closeIdx > 0) value = value.slice(1, closeIdx);
|
||||||
|
else continue;
|
||||||
|
} else if (/^(true|false|-?\d+(\.\d+)?)\s*(#.*)?$/.test(value)) {
|
||||||
|
value = value.replace(/\s*#.*$/, "");
|
||||||
|
value = value === "true" ? true : value === "false" ? false : Number(value);
|
||||||
} else {
|
} else {
|
||||||
continue; // arrays, inline tables, etc. — not one of the shown fields
|
continue; // arrays, inline tables, etc. — not one of the shown fields
|
||||||
}
|
}
|
||||||
@@ -134,7 +146,7 @@ function readAgentsAt(scope, codexDir) {
|
|||||||
for (const ent of listDir(dir)) {
|
for (const ent of listDir(dir)) {
|
||||||
if (!ent.name.endsWith(".toml")) continue;
|
if (!ent.name.endsWith(".toml")) continue;
|
||||||
const file = path.join(dir, ent.name);
|
const file = path.join(dir, ent.name);
|
||||||
if (!ent.isFile() || !isUnder(codexDir, file)) continue;
|
if (!isFileLike(ent, file) || !isUnder(codexDir, file)) continue;
|
||||||
const read = safeReadText(file);
|
const read = safeReadText(file);
|
||||||
if (!read) continue;
|
if (!read) continue;
|
||||||
agents.push({
|
agents.push({
|
||||||
@@ -165,7 +177,7 @@ function readRulesAt(scope, codexDir) {
|
|||||||
for (const ent of listDir(dir)) {
|
for (const ent of listDir(dir)) {
|
||||||
if (!ent.name.endsWith(".rules")) continue;
|
if (!ent.name.endsWith(".rules")) continue;
|
||||||
const file = path.join(dir, ent.name);
|
const file = path.join(dir, ent.name);
|
||||||
if (!ent.isFile() || !isUnder(codexDir, file)) continue;
|
if (!isFileLike(ent, file) || !isUnder(codexDir, file)) continue;
|
||||||
const read = safeReadText(file);
|
const read = safeReadText(file);
|
||||||
if (!read) continue;
|
if (!read) continue;
|
||||||
rules.push({
|
rules.push({
|
||||||
@@ -268,5 +280,4 @@ module.exports = {
|
|||||||
readConfig,
|
readConfig,
|
||||||
readOverview,
|
readOverview,
|
||||||
readTomlMeta,
|
readTomlMeta,
|
||||||
isUnder,
|
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user