Repository navigation
Expand file tree
/
Copy pathlib.ts
More file actions
138 lines (121 loc) · 6.14 KB
/
Copy pathlib.ts
File metadata and controls
138 lines (121 loc) · 6.14 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { join } from "node:path";
export const ROOT = join(import.meta.dir, "..");
export const PLUGIN_DIR = join(ROOT, "plugins/pstack");
export const UPSTREAM_DIR = join(ROOT, "upstream");
export const T3_DIR = join(ROOT, "t3");
export type Pins = {
upstream: {
repo: string;
commit: string;
version: string;
/** Upstream path (file or directory) to its path inside the plugin. */
map: Record<string, string>;
/** Upstream paths left out of the plugin, matched as path prefixes. */
exclude: string[];
/** Upstream files kept in the snapshot for reference but not shipped. */
meta: string[];
};
t3: {
repo: string;
commit: string;
/** Files that make up the T3 contract, snapshotted under t3/. */
paths: string[];
};
};
/** Plugin path to the reason it differs from upstream or exists only here. */
export type Drift = Record<string, string>;
/** Relative POSIX path to file bytes. */
export type Tree = Map<string, Buffer>;
export const loadPins = (): Pins => JSON.parse(readFileSync(join(ROOT, "pins.json"), "utf8"));
export const savePins = (pins: Pins) => writeFileSync(join(ROOT, "pins.json"), `${JSON.stringify(pins, null, 2)}\n`);
export const loadDrift = (): Drift => JSON.parse(readFileSync(join(ROOT, "drift.json"), "utf8"));
export function readTree(dir: string, prefix = ""): Tree {
const tree: Tree = new Map();
if (!existsSync(dir)) return tree;
for (const entry of readdirSync(dir, { withFileTypes: true })) {
if (entry.name === ".DS_Store") continue;
const rel = prefix ? `${prefix}/${entry.name}` : entry.name;
const abs = join(dir, entry.name);
if (entry.isDirectory()) for (const [p, c] of readTree(abs, rel)) tree.set(p, c);
else tree.set(rel, readFileSync(abs));
}
return tree;
}
const SKILL_FILE = /^skills\/([^/]+)\/SKILL\.md$/;
const FRONTMATTER = /^---\n([\s\S]*?)\n---\n/;
/** The mechanical rewrite applied to every upstream file before drift. */
export function transform(path: string, content: Buffer): Buffer {
const skill = SKILL_FILE.exec(path)?.[1];
if (!skill) return content;
const text = content.toString("utf8");
const fm = FRONTMATTER.exec(text);
if (!fm) return content;
// Codex names a skill by this field, Claude by its directory. Upstream poteto-mode says "Poteto Mode".
const lines = fm[1]!.split("\n").map((line) => (line.startsWith("name:") ? `name: ${skill}` : line));
return Buffer.from(`---\n${lines.join("\n")}\n---\n${text.slice(fm[0].length)}`);
}
export function frontmatterProblems(path: string, content: Buffer): string[] {
const skill = SKILL_FILE.exec(path)?.[1];
if (!skill) return [];
const fm = FRONTMATTER.exec(content.toString("utf8"))?.[1];
if (fm === undefined) return [`${path}: no frontmatter.`];
const problems: string[] = [];
const name = /^name:\s*(.+)$/m.exec(fm)?.[1]?.trim();
if (name !== skill) problems.push(`${path}: name is "${name}", expected "${skill}" (Codex names skills by this field).`);
if (!/^description:/m.test(fm)) problems.push(`${path}: no description.`);
return problems;
}
const isExcluded = (pins: Pins, src: string) =>
pins.upstream.exclude.some((e) => src === e || src.startsWith(`${e}/`));
/** Every upstream file the plugin ships, with its plugin path. */
export function mappedFiles(pins: Pins, upstreamRoot: string): Array<{ dst: string; abs: string }> {
const files: Array<{ dst: string; abs: string }> = [];
for (const [from, to] of Object.entries(pins.upstream.map)) {
const abs = join(upstreamRoot, from);
if (!existsSync(abs)) continue;
const rels = statSync(abs).isDirectory() ? [...readTree(abs).keys()].map((r) => `/${r}`) : [""];
for (const rel of rels) if (!isExcluded(pins, from + rel)) files.push({ dst: to + rel, abs: abs + rel });
}
return files;
}
// Codex ignores disable-model-invocation and keeps a skill out of implicit use only through this file.
const CODEX_TYPED_ONLY = Buffer.from("policy:\n allow_implicit_invocation: false\n");
/** What the plugin tree would be with zero drift, built from an upstream snapshot. */
export function expectedTree(pins: Pins, upstreamRoot: string): Tree {
const tree: Tree = new Map(mappedFiles(pins, upstreamRoot).map(({ dst, abs }) => [dst, transform(dst, readFileSync(abs))]));
for (const [path, content] of [...tree]) {
const skill = SKILL_FILE.exec(path)?.[1];
if (!skill) continue;
const fm = FRONTMATTER.exec(content.toString("utf8"))?.[1];
const sidecar = `skills/${skill}/agents/openai.yaml`;
if (fm && /^disable-model-invocation: true$/m.test(fm) && !tree.has(sidecar)) tree.set(sidecar, CODEX_TYPED_ONLY);
}
return tree;
}
export const isExecutable = (abs: string) => (statSync(abs).mode & 0o111) !== 0;
/** Every upstream path the snapshot holds. */
export const snapshotSources = (pins: Pins) => [...Object.keys(pins.upstream.map), ...pins.upstream.meta];
export function run(cmd: string[], opts: { cwd?: string; input?: Buffer; allowFail?: boolean } = {}) {
const proc = Bun.spawnSync(cmd, { cwd: opts.cwd, stdin: opts.input ?? "ignore", stdout: "pipe", stderr: "pipe" });
if (proc.exitCode !== 0 && !opts.allowFail)
throw new Error(`${cmd.join(" ")} failed (${proc.exitCode}): ${proc.stderr.toString().trim()}`);
return { code: proc.exitCode, stdout: proc.stdout, stderr: proc.stderr.toString() };
}
/** Tool names defined in the snapshotted T3 toolkits, constants resolved across the snapshot. */
export function t3ToolNames(contract: Tree): Set<string> {
const constants = new Map<string, string>();
for (const content of contract.values())
for (const m of content.toString("utf8").matchAll(/export const ([A-Z0-9_]+) = "([a-z0-9_]+)"/g))
constants.set(m[1]!, m[2]!);
const names = new Set<string>();
for (const [path, content] of contract) {
if (!/\/toolkits\/[^/]+\/tools\.ts$/.test(path)) continue;
for (const m of content.toString("utf8").matchAll(/Tool\.make\(\s*(?:"([a-z0-9_]+)"|([A-Z0-9_]+))/g)) {
const name = m[1] ?? constants.get(m[2]!);
if (!name) throw new Error(`${path}: cannot resolve tool name constant ${m[2]}`);
names.add(name);
}
}
return names;
}