-
Notifications
You must be signed in to change notification settings - Fork 4.8k
Expand file tree
/
Copy pathmaterialize-plugins.mjs
More file actions
208 lines (184 loc) · 7.28 KB
/
Copy pathmaterialize-plugins.mjs
File metadata and controls
208 lines (184 loc) · 7.28 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
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
#!/usr/bin/env node
import fs from "fs";
import path from "path";
import { fileURLToPath } from "url";
import { ROOT_FOLDER } from "./constants.mjs";
const PLUGINS_DIR = path.join(ROOT_FOLDER, "plugins");
const EXTENSIONS_DIR = path.join(ROOT_FOLDER, "extensions");
const COPILOT_NAMESPACE = "com.github.copilot";
const AWESOME_COPILOT_NAMESPACE = "com.github.awesome-copilot";
const COPILOT_CONTENT_DIR = COPILOT_NAMESPACE;
/**
* Recursively copy a directory.
*/
function copyDirRecursive(src, dest) {
fs.mkdirSync(dest, { recursive: true });
for (const entry of fs.readdirSync(src, { withFileTypes: true })) {
const srcPath = path.join(src, entry.name);
const destPath = path.join(dest, entry.name);
if (entry.isDirectory()) {
copyDirRecursive(srcPath, destPath);
} else {
fs.copyFileSync(srcPath, destPath);
}
}
}
/**
* Resolve a plugin-relative path to the repo-root source file.
*
* ./agents/foo.md → ROOT/agents/foo.agent.md
* ./skills/baz/ → ROOT/skills/baz/
*/
function resolveSource(relPath) {
const basename = path.basename(relPath, ".md");
if (relPath.startsWith("./agents/")) {
return path.join(ROOT_FOLDER, "agents", `${basename}.agent.md`);
}
if (relPath.startsWith("./skills/")) {
// Strip trailing slash and get the skill folder name
const skillName = relPath.replace(/^\.\/skills\//, "").replace(/\/$/, "");
return path.join(ROOT_FOLDER, "skills", skillName);
}
if (relPath.startsWith("./extensions/")) {
const extensionName = relPath.replace(/^\.\/extensions\//, "").replace(/\/$/, "");
return path.join(ROOT_FOLDER, "extensions", extensionName);
}
if (relPath.startsWith("./hooks/")) {
return path.join(ROOT_FOLDER, "hooks", relPath.replace(/^\.\/hooks\//, ""));
}
return null;
}
function readExtensionReferences(metadata, pluginName) {
const extensionData = metadata.extensions?.[AWESOME_COPILOT_NAMESPACE];
const directories = extensionData?.extensions ?? [];
if (!Array.isArray(directories) ||
directories.some((entry) => typeof entry !== "string" || !entry.startsWith("./extensions/"))) {
throw new Error(`extensions["${AWESOME_COPILOT_NAMESPACE}"].extensions must contain plugin-relative paths`);
}
const names = new Set(directories.map((entry) =>
entry.replace(/^\.\/extensions\//, "").replace(/\/$/, "")
));
if (fs.existsSync(path.join(EXTENSIONS_DIR, pluginName, "extension.mjs"))) {
names.add(pluginName);
}
return [...names].sort();
}
export function materializePlugins() {
console.log("Materializing plugin files...\n");
if (!fs.existsSync(PLUGINS_DIR)) {
console.error(`Error: Plugins directory not found at ${PLUGINS_DIR}`);
process.exit(1);
}
const pluginDirs = fs.readdirSync(PLUGINS_DIR, { withFileTypes: true })
.filter(entry => entry.isDirectory())
.map(entry => entry.name)
.sort();
let totalAgents = 0;
let totalSkills = 0;
let totalExtensions = 0;
let warnings = 0;
let errors = 0;
for (const dirName of pluginDirs) {
const pluginPath = path.join(PLUGINS_DIR, dirName);
const pluginJsonPath = path.join(pluginPath, "plugin.json");
if (!fs.existsSync(pluginJsonPath)) {
continue;
}
let metadata;
try {
metadata = JSON.parse(fs.readFileSync(pluginJsonPath, "utf8"));
} catch (err) {
console.error(`Error: Failed to parse ${pluginJsonPath}: ${err.message}`);
errors++;
continue;
}
const pluginName = metadata.name || dirName;
const composition = metadata.extensions?.[AWESOME_COPILOT_NAMESPACE] ?? {};
// Process repository composition fields.
for (const field of ["agents", "hooks", "skills"]) {
const entries = composition[field];
if (!Array.isArray(entries)) continue;
for (const relPath of entries) {
const src = resolveSource(relPath);
if (!src) {
console.warn(` ⚠ ${pluginName}: Unknown ${field} path format: ${relPath}`);
warnings++;
continue;
}
if (!fs.existsSync(src)) {
console.warn(` ⚠ ${pluginName}: ${field} source not found: ${src}`);
warnings++;
continue;
}
const relativeDestination = relPath.replace(/^\.\//, "").replace(/\/$/, "");
const dest = field === "skills"
? path.join(pluginPath, relativeDestination)
: path.join(pluginPath, COPILOT_CONTENT_DIR, relativeDestination);
fs.mkdirSync(path.dirname(dest), { recursive: true });
if (fs.statSync(src).isDirectory()) copyDirRecursive(src, dest);
else fs.copyFileSync(src, dest);
if (field === "agents") totalAgents++;
if (field === "skills") totalSkills++;
}
}
// Process reusable extensions declared in the repository namespace.
const extensionRefs = readExtensionReferences(metadata, pluginName);
for (const extensionName of extensionRefs) {
const relPath = `./extensions/${extensionName}`;
const src = resolveSource(relPath);
if (!src) {
console.warn(` ⚠ ${pluginName}: Unknown extension path format: ${relPath}`);
warnings++;
continue;
}
if (!fs.existsSync(src) || !fs.statSync(src).isDirectory()) {
console.warn(` ⚠ ${pluginName}: Extension source directory not found: ${src}`);
warnings++;
continue;
}
const dest = path.join(pluginPath, COPILOT_CONTENT_DIR, "extensions", extensionName);
copyDirRecursive(src, dest);
totalExtensions++;
}
// Emit a spec-compliant served manifest for the marketplace branch.
// Source manifests keep repository composition fields for build tooling.
// The served manifest retains only Agent Plugins v1.0.0 fields; standard
// skills are discovered from skills/, while Copilot-specific content is
// discovered from com.github.copilot/.
const SPEC_FIELDS = new Set(["$schema", "name", "version", "description", "author",
"homepage", "repository", "license", "keywords", "extensions"]);
const AGENT_PLUGINS_SCHEMA = "https://agent-plugins.org/schemas/1.0.0/plugin.schema.json";
const served = { "$schema": AGENT_PLUGINS_SCHEMA };
for (const [key, val] of Object.entries(metadata)) {
if (SPEC_FIELDS.has(key) && key !== "$schema") {
if (key === "extensions") {
const copilot = val?.[COPILOT_NAMESPACE];
if (copilot) {
served.extensions = { [COPILOT_NAMESPACE]: { ...copilot } };
}
} else {
served[key] = val;
}
}
}
fs.writeFileSync(pluginJsonPath, JSON.stringify(served, null, 2) + "\n", "utf8");
const counts = [];
if (composition.agents?.length) counts.push(`${composition.agents.length} agents`);
if (composition.skills?.length) counts.push(`${composition.skills.length} skills`);
if (extensionRefs.length) counts.push(`${extensionRefs.length} extensions`);
if (counts.length) {
console.log(`✓ ${pluginName}: ${counts.join(", ")}`);
}
}
console.log(`\nDone. Copied ${totalAgents} agents, ${totalSkills} skills, ${totalExtensions} extensions.`);
if (warnings > 0) {
console.log(`${warnings} warning(s).`);
}
if (errors > 0) {
console.error(`${errors} error(s).`);
process.exit(1);
}
}
if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
materializePlugins();
}