@minhpnq1807/contextos 0.5.53 → 0.6.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.
- package/.codex/skills/contextos-community/SKILL.md +15 -0
- package/.codex/skills/contextos-community/skill.yaml +20 -0
- package/.codex/skills/contextos-release/SKILL.md +15 -0
- package/.codex/skills/contextos-release/skill.yaml +20 -0
- package/.codex/skills/contextos-routing/SKILL.md +15 -0
- package/.codex/skills/contextos-routing/skill.yaml +20 -0
- package/.codex/workflows/primary.md +13 -0
- package/.codex/workflows/release.md +12 -0
- package/CHANGELOG.md +13 -0
- package/README.md +100 -2
- package/bin/ctx.js +12 -0
- package/community-skills/README.md +42 -0
- package/community-skills/_template/SKILL.md +15 -0
- package/community-skills/_template/skill.yaml +20 -0
- package/community-skills/eas/SKILL.md +15 -0
- package/community-skills/eas/skill.yaml +23 -0
- package/community-skills/jwt-auth/SKILL.md +15 -0
- package/community-skills/jwt-auth/skill.yaml +22 -0
- package/community-skills/oauth-google/SKILL.md +15 -0
- package/community-skills/oauth-google/skill.yaml +22 -0
- package/community-skills/prisma/SKILL.md +15 -0
- package/community-skills/prisma/skill.yaml +22 -0
- package/community-skills/redis/SKILL.md +15 -0
- package/community-skills/redis/skill.yaml +22 -0
- package/community-skills/vercel/SKILL.md +15 -0
- package/community-skills/vercel/skill.yaml +22 -0
- package/docs/demo/agents-lost-middle.gif +0 -0
- package/docs/demo/agents-lost-middle.txt +28 -0
- package/docs/demo/contextos-ready.gif +0 -0
- package/docs/demo/contextos-ready.txt +20 -0
- package/docs/demo/same-prompt-different-context.gif +0 -0
- package/docs/demo/same-prompt-different-context.txt +26 -0
- package/docs/launch-demos.md +127 -0
- package/docs/roadmap.md +285 -0
- package/eval/hallucination/run-leaderboard.js +183 -0
- package/package.json +5 -1
- package/plugins/ctx/.codex-plugin/plugin.json +1 -1
- package/plugins/ctx/lib/certification.js +223 -0
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { fileURLToPath } from "node:url";
|
|
5
|
+
|
|
6
|
+
import { parseEvalYaml, runSkillRoutingEval } from "../skill-routing/run-eval.js";
|
|
7
|
+
|
|
8
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
9
|
+
const evalRoot = path.resolve(__dirname, "..", "skill-routing");
|
|
10
|
+
const DEFAULT_CASE_LIMIT = 20;
|
|
11
|
+
|
|
12
|
+
export async function runHallucinationLeaderboard({
|
|
13
|
+
rootDir = path.resolve(__dirname, "..", ".."),
|
|
14
|
+
casesPath = path.join(evalRoot, "cases.yaml"),
|
|
15
|
+
caseLimit = DEFAULT_CASE_LIMIT
|
|
16
|
+
} = {}) {
|
|
17
|
+
const config = parseEvalYaml(fs.readFileSync(casesPath, "utf8"));
|
|
18
|
+
const selectedCases = selectLeaderboardCases(config.cases, caseLimit);
|
|
19
|
+
const wanted = new Set(selectedCases.map(caseId));
|
|
20
|
+
const contextos = await runSkillRoutingEval({ rootDir, casesPath, topK: 3, threshold: 0.5 });
|
|
21
|
+
const contextRows = contextos.rows
|
|
22
|
+
.filter((row) => wanted.has(caseId(row)))
|
|
23
|
+
.map((row) => evaluateRow({
|
|
24
|
+
prompt: row.prompt,
|
|
25
|
+
fixture: row.fixture,
|
|
26
|
+
expected: row.expected,
|
|
27
|
+
allowed: row.allowed,
|
|
28
|
+
forbidden: row.forbidden,
|
|
29
|
+
selectedIds: row.selectedIds
|
|
30
|
+
}));
|
|
31
|
+
const rawRows = selectedCases.map((testCase) => evaluateRow({
|
|
32
|
+
prompt: testCase.prompt,
|
|
33
|
+
fixture: testCase.fixture,
|
|
34
|
+
expected: testCase.expected || [],
|
|
35
|
+
allowed: testCase.allowed || [],
|
|
36
|
+
forbidden: testCase.forbidden || [],
|
|
37
|
+
selectedIds: rawAgentSkills({ prompt: testCase.prompt, skills: config.skills, topK: 3 })
|
|
38
|
+
}));
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
caseCount: selectedCases.length,
|
|
42
|
+
repoCount: new Set(selectedCases.map((row) => row.fixture)).size,
|
|
43
|
+
systems: [
|
|
44
|
+
summarizeSystem("Raw Agent", rawRows),
|
|
45
|
+
summarizeSystem("ContextOS + Codex", contextRows)
|
|
46
|
+
],
|
|
47
|
+
rows: selectedCases.map((testCase) => ({
|
|
48
|
+
prompt: testCase.prompt,
|
|
49
|
+
fixture: testCase.fixture,
|
|
50
|
+
expected: testCase.expected || [],
|
|
51
|
+
raw: rawRows.find((row) => row.prompt === testCase.prompt && row.fixture === testCase.fixture),
|
|
52
|
+
contextos: contextRows.find((row) => row.prompt === testCase.prompt && row.fixture === testCase.fixture)
|
|
53
|
+
}))
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function formatHallucinationLeaderboard(result) {
|
|
58
|
+
const lines = [
|
|
59
|
+
"Hallucination Leaderboard",
|
|
60
|
+
`Repos: ${result.repoCount}`,
|
|
61
|
+
`Tasks: ${result.caseCount}`,
|
|
62
|
+
"",
|
|
63
|
+
"System Correct Skill",
|
|
64
|
+
"------------------ -------------"
|
|
65
|
+
];
|
|
66
|
+
for (const system of result.systems) {
|
|
67
|
+
lines.push(`${system.name.padEnd(18)} ${percent(system.correctRate)}`);
|
|
68
|
+
}
|
|
69
|
+
lines.push("", "Sample failures:");
|
|
70
|
+
const failures = result.rows
|
|
71
|
+
.filter((row) => !row.raw.correct || !row.contextos.correct)
|
|
72
|
+
.slice(0, 6);
|
|
73
|
+
if (!failures.length) {
|
|
74
|
+
lines.push("- none");
|
|
75
|
+
} else {
|
|
76
|
+
for (const row of failures) {
|
|
77
|
+
lines.push(`- ${row.fixture}: "${row.prompt}"`);
|
|
78
|
+
lines.push(` expected: ${row.expected.join(", ") || "(none)"}`);
|
|
79
|
+
lines.push(` raw: ${row.raw.selectedIds.join(", ") || "(none)"} ${row.raw.correct ? "✓" : "✗"}`);
|
|
80
|
+
lines.push(` contextos: ${row.contextos.selectedIds.join(", ") || "(none)"} ${row.contextos.correct ? "✓" : "✗"}`);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return lines.join("\n");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function selectLeaderboardCases(cases, limit) {
|
|
87
|
+
const wantedFixtures = [
|
|
88
|
+
"expo-eas",
|
|
89
|
+
"next-vercel",
|
|
90
|
+
"docker-node",
|
|
91
|
+
"railway-render",
|
|
92
|
+
"firebase-hosting",
|
|
93
|
+
"nest-prisma",
|
|
94
|
+
"express-mongo-jwt",
|
|
95
|
+
"oauth-google",
|
|
96
|
+
"redis-cache",
|
|
97
|
+
"contextos",
|
|
98
|
+
"frontend-only-next",
|
|
99
|
+
"static-docs"
|
|
100
|
+
];
|
|
101
|
+
const selected = [];
|
|
102
|
+
for (const fixture of wantedFixtures) {
|
|
103
|
+
const match = cases.find((row) => row.fixture === fixture && !selected.some((item) => caseId(item) === caseId(row)));
|
|
104
|
+
if (match) selected.push(match);
|
|
105
|
+
if (selected.length >= limit) return selected;
|
|
106
|
+
}
|
|
107
|
+
for (const row of cases) {
|
|
108
|
+
if (!selected.some((item) => caseId(item) === caseId(row))) selected.push(row);
|
|
109
|
+
if (selected.length >= limit) break;
|
|
110
|
+
}
|
|
111
|
+
return selected;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function rawAgentSkills({ prompt, skills, topK }) {
|
|
115
|
+
return skills
|
|
116
|
+
.map((skill) => ({ id: skill.id, score: rawPromptScore(prompt, skill) }))
|
|
117
|
+
.sort((a, b) => b.score - a.score || a.id.localeCompare(b.id))
|
|
118
|
+
.slice(0, topK)
|
|
119
|
+
.map((skill) => skill.id);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function rawPromptScore(prompt, skill) {
|
|
123
|
+
const promptTokens = new Set(tokenize(prompt));
|
|
124
|
+
const triggerTokens = tokenize([
|
|
125
|
+
skill.id,
|
|
126
|
+
skill.description,
|
|
127
|
+
...(skill.positive_triggers?.prompts || [])
|
|
128
|
+
].join(" "));
|
|
129
|
+
let score = 0;
|
|
130
|
+
for (const token of triggerTokens) {
|
|
131
|
+
if (promptTokens.has(token)) score += token.length > 5 ? 2 : 1;
|
|
132
|
+
}
|
|
133
|
+
return score;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function evaluateRow({ prompt, fixture, expected, allowed, forbidden, selectedIds }) {
|
|
137
|
+
const selected = new Set(selectedIds);
|
|
138
|
+
const accepted = new Set([...expected, ...allowed]);
|
|
139
|
+
const hasExpected = expected.length
|
|
140
|
+
? expected.every((skill) => selected.has(skill))
|
|
141
|
+
: selectedIds.length === 0;
|
|
142
|
+
const hasForbidden = forbidden.some((skill) => selected.has(skill));
|
|
143
|
+
const hasUnexpected = selectedIds.some((skill) => !accepted.has(skill));
|
|
144
|
+
return {
|
|
145
|
+
prompt,
|
|
146
|
+
fixture,
|
|
147
|
+
expected,
|
|
148
|
+
allowed,
|
|
149
|
+
forbidden,
|
|
150
|
+
selectedIds,
|
|
151
|
+
correct: hasExpected && !hasForbidden && !hasUnexpected
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function summarizeSystem(name, rows) {
|
|
156
|
+
const correct = rows.filter((row) => row.correct).length;
|
|
157
|
+
return {
|
|
158
|
+
name,
|
|
159
|
+
correct,
|
|
160
|
+
total: rows.length,
|
|
161
|
+
correctRate: rows.length ? correct / rows.length : 0
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function tokenize(value) {
|
|
166
|
+
return String(value || "")
|
|
167
|
+
.toLowerCase()
|
|
168
|
+
.split(/[^a-z0-9@.-]+/)
|
|
169
|
+
.filter((token) => token.length > 2);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function caseId(row) {
|
|
173
|
+
return `${row.fixture}\0${row.prompt}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
function percent(value) {
|
|
177
|
+
return `${(value * 100).toFixed(1)}%`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
181
|
+
const result = await runHallucinationLeaderboard();
|
|
182
|
+
console.log(formatHallucinationLeaderboard(result));
|
|
183
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@minhpnq1807/contextos",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.1",
|
|
4
4
|
"description": "Task-aware AGENTS.md context injection and compliance reporting for Codex, Claude Code, and Antigravity.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -10,10 +10,13 @@
|
|
|
10
10
|
"files": [
|
|
11
11
|
"bin/",
|
|
12
12
|
"plugins/",
|
|
13
|
+
".codex/skills/",
|
|
14
|
+
".codex/workflows/",
|
|
13
15
|
".agents/",
|
|
14
16
|
"README.md",
|
|
15
17
|
"DEMO.md",
|
|
16
18
|
"LAUNCH.md",
|
|
19
|
+
"community-skills/",
|
|
17
20
|
"eval/",
|
|
18
21
|
"docs/",
|
|
19
22
|
"LICENSE",
|
|
@@ -24,6 +27,7 @@
|
|
|
24
27
|
"build": "node bin/ctx.js --version",
|
|
25
28
|
"validate:plugin": "node test/validate-plugin.js",
|
|
26
29
|
"benchmark:skills": "node bin/ctx.js benchmark --skills",
|
|
30
|
+
"leaderboard:hallucination": "node eval/hallucination/run-leaderboard.js",
|
|
27
31
|
"test:mcp": "node test/mcp-protocol-smoke.js"
|
|
28
32
|
},
|
|
29
33
|
"engines": {
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
import { filterActionableRules, parseRules } from "./analyzer.js";
|
|
6
|
+
import { readAgentsChain } from "./reader.js";
|
|
7
|
+
import { scanSkills } from "./skill-discoverer.js";
|
|
8
|
+
import { scanWorkflows } from "./workflow-discoverer.js";
|
|
9
|
+
|
|
10
|
+
const PROJECT_SKILL_ROOTS = [
|
|
11
|
+
[".codex", "skills"],
|
|
12
|
+
[".agents", "skills"],
|
|
13
|
+
[".claude", "skills"],
|
|
14
|
+
[".gemini", "skills"],
|
|
15
|
+
[".gemini", "antigravity", "skills"],
|
|
16
|
+
[".gemini", "antigravity-cli", "skills"]
|
|
17
|
+
];
|
|
18
|
+
|
|
19
|
+
const PROJECT_WORKFLOW_ROOTS = [
|
|
20
|
+
[".claude", "workflows"],
|
|
21
|
+
[".codex", "workflows"],
|
|
22
|
+
[".gemini", "workflows"],
|
|
23
|
+
[".gemini", "antigravity", "workflows"],
|
|
24
|
+
[".gemini", "antigravity-cli", "workflows"]
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
export function inspectContextOSReady({ cwd = process.cwd(), home = os.homedir() } = {}) {
|
|
28
|
+
const root = findProjectRoot(cwd);
|
|
29
|
+
const rules = inspectRules({ cwd, root, home });
|
|
30
|
+
const skills = inspectSkills({ root });
|
|
31
|
+
const workflows = inspectWorkflows({ root });
|
|
32
|
+
const overall = Math.round((rules.score + skills.score + workflows.score) / 3);
|
|
33
|
+
const tier = readinessTier(overall, { rules, skills, workflows });
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
root,
|
|
37
|
+
rules,
|
|
38
|
+
skills,
|
|
39
|
+
workflows,
|
|
40
|
+
overall,
|
|
41
|
+
tier,
|
|
42
|
+
badge: tier === "Not Ready" ? "ContextOS Ready: Not Ready" : `ContextOS Ready ${tier}`
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function formatContextOSReady(result) {
|
|
47
|
+
const lines = [
|
|
48
|
+
"Repository Score",
|
|
49
|
+
"",
|
|
50
|
+
`Rules: ${result.rules.score}`,
|
|
51
|
+
`Skills: ${result.skills.score}`,
|
|
52
|
+
`Workflows: ${result.workflows.score}`,
|
|
53
|
+
"",
|
|
54
|
+
"Overall:",
|
|
55
|
+
result.badge,
|
|
56
|
+
"",
|
|
57
|
+
"Evidence:",
|
|
58
|
+
`- Rules: ${result.rules.summary}`,
|
|
59
|
+
`- Skills: ${result.skills.summary}`,
|
|
60
|
+
`- Workflows: ${result.workflows.summary}`
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const next = [
|
|
64
|
+
...result.rules.recommendations,
|
|
65
|
+
...result.skills.recommendations,
|
|
66
|
+
...result.workflows.recommendations
|
|
67
|
+
];
|
|
68
|
+
if (next.length) {
|
|
69
|
+
lines.push("", "Next:");
|
|
70
|
+
for (const item of [...new Set(next)].slice(0, 5)) lines.push(`- ${item}`);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return lines.join("\n");
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function inspectRules({ cwd, root, home }) {
|
|
77
|
+
const chain = readAgentsChain({ cwd, home });
|
|
78
|
+
const projectSources = chain.sources.filter((source) => isInsidePath(source, root));
|
|
79
|
+
const rules = parseRules(chain.content || "");
|
|
80
|
+
const actionable = filterActionableRules(rules);
|
|
81
|
+
const imperative = actionable.filter((rule) => /\b(always|never|must|required|use|prefer|avoid|do not|don't)\b/i.test(rule.content));
|
|
82
|
+
let score = 0;
|
|
83
|
+
const recommendations = [];
|
|
84
|
+
|
|
85
|
+
if (projectSources.length) score += 55;
|
|
86
|
+
else recommendations.push("Add a project AGENTS.md with repository-specific operating rules.");
|
|
87
|
+
|
|
88
|
+
if (actionable.length >= 3) score += 20;
|
|
89
|
+
else recommendations.push("Add at least three actionable AGENTS.md rules.");
|
|
90
|
+
|
|
91
|
+
if (imperative.length) score += 15;
|
|
92
|
+
else recommendations.push("Use explicit rule language such as always, never, must, use, prefer, or avoid.");
|
|
93
|
+
|
|
94
|
+
if (projectSources.length > 1 || fs.existsSync(path.join(root, ".ruler"))) score += 10;
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
score: Math.min(100, score),
|
|
98
|
+
sources: projectSources,
|
|
99
|
+
ruleCount: rules.length,
|
|
100
|
+
actionableCount: actionable.length,
|
|
101
|
+
summary: projectSources.length
|
|
102
|
+
? `${projectSources.length} AGENTS.md source(s), ${actionable.length} actionable rule(s)`
|
|
103
|
+
: "missing project AGENTS.md",
|
|
104
|
+
recommendations
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function inspectSkills({ root }) {
|
|
109
|
+
const roots = PROJECT_SKILL_ROOTS.map((parts) => path.join(root, ...parts));
|
|
110
|
+
const skills = scanSkills({ cwd: root, roots, maxSkills: 500 });
|
|
111
|
+
const metadataFiles = findFiles(roots, (filePath) => /skill\.ya?ml$/i.test(path.basename(filePath)));
|
|
112
|
+
const richMetadata = metadataFiles.filter((filePath) => {
|
|
113
|
+
const content = safeRead(filePath);
|
|
114
|
+
return /^positive_triggers:/m.test(content)
|
|
115
|
+
&& /^evidence:/m.test(content)
|
|
116
|
+
&& /^negative_triggers:/m.test(content)
|
|
117
|
+
&& /^workflow:/m.test(content);
|
|
118
|
+
});
|
|
119
|
+
let score = 0;
|
|
120
|
+
const recommendations = [];
|
|
121
|
+
|
|
122
|
+
if (skills.length) score += 50;
|
|
123
|
+
else recommendations.push("Add project skills under .codex/skills/ or .agents/skills/.");
|
|
124
|
+
|
|
125
|
+
if (metadataFiles.length) score += 20;
|
|
126
|
+
else recommendations.push("Add skill.yaml metadata beside important SKILL.md files.");
|
|
127
|
+
|
|
128
|
+
if (richMetadata.length) score += 20;
|
|
129
|
+
else recommendations.push("Include positive_triggers, negative_triggers, evidence, and workflow in skill.yaml.");
|
|
130
|
+
|
|
131
|
+
if (skills.length >= 3) score += 10;
|
|
132
|
+
else recommendations.push("Provide at least three project-relevant skills for common tasks.");
|
|
133
|
+
|
|
134
|
+
return {
|
|
135
|
+
score: Math.min(100, score),
|
|
136
|
+
count: skills.length,
|
|
137
|
+
metadataCount: metadataFiles.length,
|
|
138
|
+
richMetadataCount: richMetadata.length,
|
|
139
|
+
summary: skills.length
|
|
140
|
+
? `${skills.length} skill(s), ${metadataFiles.length} metadata file(s)`
|
|
141
|
+
: "missing project skill packs",
|
|
142
|
+
recommendations
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function inspectWorkflows({ root }) {
|
|
147
|
+
const roots = PROJECT_WORKFLOW_ROOTS.map((parts) => path.join(root, ...parts));
|
|
148
|
+
const workflows = scanWorkflows({ cwd: root, roots });
|
|
149
|
+
const withChain = workflows.filter((workflow) => workflow.chain?.length);
|
|
150
|
+
let score = 0;
|
|
151
|
+
const recommendations = [];
|
|
152
|
+
|
|
153
|
+
if (workflows.length) score += 60;
|
|
154
|
+
else recommendations.push("Add project workflows under .codex/workflows/ or .claude/workflows/.");
|
|
155
|
+
|
|
156
|
+
if (withChain.length) score += 25;
|
|
157
|
+
else recommendations.push("Include agent handoff names such as planner, tester, code-reviewer, or docs-manager in workflow files.");
|
|
158
|
+
|
|
159
|
+
if (workflows.length >= 2) score += 15;
|
|
160
|
+
else recommendations.push("Provide more than one workflow when the repo has distinct delivery paths.");
|
|
161
|
+
|
|
162
|
+
return {
|
|
163
|
+
score: Math.min(100, score),
|
|
164
|
+
count: workflows.length,
|
|
165
|
+
chainCount: withChain.length,
|
|
166
|
+
summary: workflows.length
|
|
167
|
+
? `${workflows.length} workflow(s), ${withChain.length} with agent chain(s)`
|
|
168
|
+
: "missing project workflows",
|
|
169
|
+
recommendations
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function readinessTier(overall, { rules, skills, workflows }) {
|
|
174
|
+
if (rules.score < 50 || skills.score < 50 || workflows.score < 50) return "Not Ready";
|
|
175
|
+
if (overall >= 85) return "Gold";
|
|
176
|
+
if (overall >= 70) return "Silver";
|
|
177
|
+
if (overall >= 50) return "Bronze";
|
|
178
|
+
return "Not Ready";
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function findProjectRoot(cwd) {
|
|
182
|
+
let current = path.resolve(cwd);
|
|
183
|
+
while (true) {
|
|
184
|
+
if (fs.existsSync(path.join(current, ".git"))) return current;
|
|
185
|
+
const parent = path.dirname(current);
|
|
186
|
+
if (parent === current) return path.resolve(cwd);
|
|
187
|
+
current = parent;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function findFiles(roots, predicate) {
|
|
192
|
+
const files = [];
|
|
193
|
+
for (const root of roots) walk(root, files, predicate, 0);
|
|
194
|
+
return files;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function walk(directory, files, predicate, depth) {
|
|
198
|
+
if (depth > 4) return;
|
|
199
|
+
let entries = [];
|
|
200
|
+
try {
|
|
201
|
+
entries = fs.readdirSync(directory, { withFileTypes: true });
|
|
202
|
+
} catch {
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
for (const entry of entries) {
|
|
206
|
+
const filePath = path.join(directory, entry.name);
|
|
207
|
+
if (entry.isDirectory()) walk(filePath, files, predicate, depth + 1);
|
|
208
|
+
else if (entry.isFile() && predicate(filePath)) files.push(filePath);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function isInsidePath(filePath, root) {
|
|
213
|
+
const relative = path.relative(path.resolve(root), path.resolve(filePath));
|
|
214
|
+
return relative && !relative.startsWith("..") && !path.isAbsolute(relative);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function safeRead(filePath) {
|
|
218
|
+
try {
|
|
219
|
+
return fs.readFileSync(filePath, "utf8");
|
|
220
|
+
} catch {
|
|
221
|
+
return "";
|
|
222
|
+
}
|
|
223
|
+
}
|