codex-context-map 0.1.0

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/src/report.js ADDED
@@ -0,0 +1,25 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { validateSnapshot } from "./core.js";
3
+
4
+ export async function renderReport(snapshot, { demo = false } = {}) {
5
+ snapshot = validateSnapshot(snapshot);
6
+ const read = (relative) =>
7
+ readFile(new URL(relative, import.meta.url), "utf8");
8
+ const [template, css, core, demoSource, app] = await Promise.all([
9
+ read("../site/template.html"),
10
+ read("../site/style.css"),
11
+ read("./core.js"),
12
+ read("./demo.js"),
13
+ read("../site/app.js"),
14
+ ]);
15
+ const moduleBody = (text) =>
16
+ text.replace(/^import .*;\r?\n/gm, "").replace(/^export /gm, "");
17
+ const data = JSON.stringify({ snapshot, demo })
18
+ .replace(/</g, "\\u003c")
19
+ .replace(/\u2028/g, "\\u2028")
20
+ .replace(/\u2029/g, "\\u2029");
21
+ const script = `${moduleBody(core)}\n${moduleBody(demoSource)}\nconst initialData = ${data};\n${app}`;
22
+ return template
23
+ .replace("/* STYLE */", () => css)
24
+ .replace("/* SCRIPT */", () => script);
25
+ }
package/src/scan.js ADDED
@@ -0,0 +1,170 @@
1
+ import { readdir, lstat, readFile, realpath } from "node:fs/promises";
2
+ import path from "node:path";
3
+ import os from "node:os";
4
+ import {
5
+ candidateNames,
6
+ validateSnapshot,
7
+ MAX_BUDGET,
8
+ MAX_FILES,
9
+ MAX_DIRECTORIES,
10
+ MAX_SNAPSHOT_BYTES,
11
+ DEFAULT_BUDGET,
12
+ } from "./core.js";
13
+
14
+ export const SKIP_DIRECTORIES = new Set([
15
+ ".git",
16
+ ".hg",
17
+ ".svn",
18
+ "node_modules",
19
+ ".venv",
20
+ "venv",
21
+ "__pycache__",
22
+ ".next",
23
+ ".nuxt",
24
+ "coverage",
25
+ "dist",
26
+ "build",
27
+ "target",
28
+ "vendor",
29
+ ".codex-context-map",
30
+ ".artifacts",
31
+ ]);
32
+ const utf8 = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true });
33
+ const posix = (value) => value.split(path.sep).join("/") || ".";
34
+
35
+ async function exists(file) {
36
+ try {
37
+ await lstat(file);
38
+ return true;
39
+ } catch (error) {
40
+ if (error.code === "ENOENT") return false;
41
+ throw error;
42
+ }
43
+ }
44
+
45
+ export async function findProjectRoot(directory) {
46
+ let current = directory;
47
+ while (true) {
48
+ if (await exists(path.join(current, ".git"))) return current;
49
+ const parent = path.dirname(current);
50
+ if (parent === current) return directory;
51
+ current = parent;
52
+ }
53
+ }
54
+
55
+ export async function scanProject(target = ".", options = {}) {
56
+ let cwd = await realpath(path.resolve(target));
57
+ if (!(await lstat(cwd)).isDirectory()) cwd = path.dirname(cwd);
58
+ const root = options.root
59
+ ? await realpath(path.resolve(options.root))
60
+ : await findProjectRoot(cwd);
61
+ if (!(await lstat(root)).isDirectory())
62
+ throw new Error("Project root must be a directory.");
63
+ const relative = path.relative(root, cwd);
64
+ if (
65
+ relative === ".." ||
66
+ relative.startsWith(`..${path.sep}`) ||
67
+ path.isAbsolute(relative)
68
+ )
69
+ throw new Error("Target must be inside --root.");
70
+ const settings = {
71
+ maxBytes: options.maxBytes ?? DEFAULT_BUDGET,
72
+ fallbackNames: options.fallbackNames ?? [],
73
+ };
74
+ const names = candidateNames(settings.fallbackNames);
75
+ const files = [],
76
+ directories = [],
77
+ projectRoots = [],
78
+ warnings = [];
79
+ let totalBytes = 0;
80
+ const warn = (message) => {
81
+ if (warnings.length < 100) warnings.push(message);
82
+ };
83
+
84
+ async function readInstruction(absolute, relativePath, scope = "project") {
85
+ const info = await lstat(absolute);
86
+ if (info.isSymbolicLink()) {
87
+ warn(`Skipped symlink: ${relativePath}. Codex may follow it.`);
88
+ return;
89
+ }
90
+ if (!info.isFile()) return;
91
+ if (info.size > MAX_BUDGET)
92
+ throw new Error(`Instruction file exceeds 1 MiB: ${relativePath}`);
93
+ if (
94
+ totalBytes + info.size > MAX_SNAPSHOT_BYTES ||
95
+ files.length >= MAX_FILES
96
+ )
97
+ throw new Error("Scan exceeds the snapshot size or file limit.");
98
+ const raw = await readFile(absolute);
99
+ totalBytes += raw.length;
100
+ if (totalBytes > MAX_SNAPSHOT_BYTES || raw.length > MAX_BUDGET)
101
+ throw new Error("Scan exceeds the instruction size limit.");
102
+ let content;
103
+ try {
104
+ content = utf8.decode(raw);
105
+ } catch {
106
+ throw new Error(`Instruction file is not valid UTF-8: ${relativePath}`);
107
+ }
108
+ files.push({ path: relativePath, scope, content });
109
+ }
110
+
111
+ async function walk(absolute, dir = ".", depth = 0) {
112
+ if (depth > 64 || directories.length >= MAX_DIRECTORIES)
113
+ throw new Error(
114
+ "Scan exceeds the directory or depth limit. Choose a smaller --root.",
115
+ );
116
+ directories.push(dir);
117
+ const entries = await readdir(absolute, { withFileTypes: true });
118
+ entries.sort((a, b) => a.name.localeCompare(b.name, "en"));
119
+ if (entries.some((e) => e.name === ".git") || (dir === "." && options.root))
120
+ projectRoots.push(dir);
121
+ for (const name of names) {
122
+ if (entries.some((e) => e.name === name)) {
123
+ await readInstruction(
124
+ path.join(absolute, name),
125
+ dir === "." ? name : `${dir}/${name}`,
126
+ );
127
+ }
128
+ }
129
+ for (const entry of entries) {
130
+ const child = dir === "." ? entry.name : `${dir}/${entry.name}`;
131
+ if (entry.isSymbolicLink()) {
132
+ if (!names.includes(entry.name)) warn(`Skipped symlink: ${child}.`);
133
+ continue;
134
+ }
135
+ if (!entry.isDirectory() || entry.name === ".git") continue;
136
+ // Still traverse the requested directory and its ancestors if usually excluded.
137
+ const selected = posix(relative);
138
+ const onTarget = selected === child || selected.startsWith(`${child}/`);
139
+ if (
140
+ !options.includeIgnored &&
141
+ SKIP_DIRECTORIES.has(entry.name) &&
142
+ !onTarget
143
+ )
144
+ continue;
145
+ await walk(path.join(absolute, entry.name), child, depth + 1);
146
+ }
147
+ }
148
+ await walk(root);
149
+ if (options.global) {
150
+ const codexDir = path.resolve(
151
+ options.codexHome ??
152
+ process.env.CODEX_HOME ??
153
+ path.join(os.homedir(), ".codex"),
154
+ );
155
+ for (const name of ["AGENTS.override.md", "AGENTS.md"]) {
156
+ const file = path.join(codexDir, name);
157
+ if (await exists(file)) await readInstruction(file, name, "global");
158
+ }
159
+ }
160
+ return validateSnapshot({
161
+ schemaVersion: 1,
162
+ projectName: path.basename(root),
163
+ defaultTarget: posix(relative),
164
+ settings,
165
+ files,
166
+ directories,
167
+ projectRoots,
168
+ warnings,
169
+ });
170
+ }