codex-skills-registry 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/README.md +24 -0
- package/bin/cli.js +249 -0
- package/package.json +32 -0
package/README.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# codex-skills-registry
|
|
2
|
+
|
|
3
|
+
Install Codex skills from `vadimcomanescu/codex-skills`.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
Install one or more skills (recommended):
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npx codex-skills-registry@latest --skill=development/webapp-testing --yes
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### Options
|
|
14
|
+
|
|
15
|
+
- `--skill=<category>/<slug>`: Skill to install. Repeatable.
|
|
16
|
+
- `--yes`: Skip prompts.
|
|
17
|
+
- `--local`: Install into `<repo>/.codex/skills` instead of `$CODEX_HOME/skills`.
|
|
18
|
+
- `--dest=<path>`: Explicit install root (overrides `--local`).
|
|
19
|
+
- `--ref=<git-ref>`: Git ref (default: `main`).
|
|
20
|
+
- `--owner=<owner>` / `--repo=<repo>`: Override GitHub repo (default: `vadimcomanescu/codex-skills`).
|
|
21
|
+
|
|
22
|
+
## Install location
|
|
23
|
+
|
|
24
|
+
By default this installs into `$CODEX_HOME/skills`. If `CODEX_HOME` is not set, it uses `~/.codex/skills`.
|
package/bin/cli.js
ADDED
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import * as fs from "node:fs";
|
|
4
|
+
import * as os from "node:os";
|
|
5
|
+
import * as path from "node:path";
|
|
6
|
+
import * as readline from "node:readline";
|
|
7
|
+
import { fileURLToPath } from "node:url";
|
|
8
|
+
|
|
9
|
+
const DEFAULT_OWNER = "vadimcomanescu";
|
|
10
|
+
const DEFAULT_REPO = "codex-skills";
|
|
11
|
+
const DEFAULT_REF = "main";
|
|
12
|
+
|
|
13
|
+
function formatVersion() {
|
|
14
|
+
try {
|
|
15
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
16
|
+
const pkgPath = path.join(__dirname, "..", "package.json");
|
|
17
|
+
const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf8"));
|
|
18
|
+
return pkg.version ? `v${pkg.version}` : "v0.0.0";
|
|
19
|
+
} catch {
|
|
20
|
+
return "v0.0.0";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function printHeader() {
|
|
25
|
+
const version = formatVersion();
|
|
26
|
+
const title = "š Install Codex skills š";
|
|
27
|
+
const width = Math.max(title.length, version.length) + 8;
|
|
28
|
+
const pad = (text) => {
|
|
29
|
+
const space = Math.max(0, Math.floor((width - text.length) / 2));
|
|
30
|
+
return `${" ".repeat(space)}${text}`;
|
|
31
|
+
};
|
|
32
|
+
process.stdout.write(`${pad(title)}\n`);
|
|
33
|
+
process.stdout.write(`${pad(version)}\n`);
|
|
34
|
+
process.stdout.write(`š Documentation: https://skillregistry.dev\n\n`);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function parseArgs(argv) {
|
|
38
|
+
const out = {
|
|
39
|
+
skills: [],
|
|
40
|
+
yes: false,
|
|
41
|
+
local: false,
|
|
42
|
+
dest: null,
|
|
43
|
+
ref: DEFAULT_REF,
|
|
44
|
+
owner: DEFAULT_OWNER,
|
|
45
|
+
repo: DEFAULT_REPO,
|
|
46
|
+
help: false,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
for (const raw of argv) {
|
|
50
|
+
if (raw === "--help" || raw === "-h") out.help = true;
|
|
51
|
+
else if (raw === "--yes" || raw === "-y") out.yes = true;
|
|
52
|
+
else if (raw === "--local") out.local = true;
|
|
53
|
+
else if (raw.startsWith("--dest=")) out.dest = raw.slice("--dest=".length);
|
|
54
|
+
else if (raw.startsWith("--ref=")) out.ref = raw.slice("--ref=".length);
|
|
55
|
+
else if (raw.startsWith("--owner=")) out.owner = raw.slice("--owner=".length);
|
|
56
|
+
else if (raw.startsWith("--repo=")) out.repo = raw.slice("--repo=".length);
|
|
57
|
+
else if (raw.startsWith("--skill=")) out.skills.push(raw.slice("--skill=".length));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return out;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function printHelp() {
|
|
64
|
+
process.stdout.write(
|
|
65
|
+
[
|
|
66
|
+
"codex-skills-registry",
|
|
67
|
+
"",
|
|
68
|
+
"Usage:",
|
|
69
|
+
" npx codex-skills-registry@latest --skill=<category>/<slug> [--skill=...] --yes",
|
|
70
|
+
"",
|
|
71
|
+
"Options:",
|
|
72
|
+
" --skill=<category>/<slug> Skill to install (repeatable).",
|
|
73
|
+
" --yes, -y Skip prompts.",
|
|
74
|
+
" --local Install into <repo>/.codex/skills.",
|
|
75
|
+
" --dest=<path> Install root (overrides --local).",
|
|
76
|
+
" --ref=<git-ref> Git ref (default: main).",
|
|
77
|
+
" --owner=<owner> GitHub owner (default: vadimcomanescu).",
|
|
78
|
+
" --repo=<repo> GitHub repo (default: codex-skills).",
|
|
79
|
+
"",
|
|
80
|
+
"Env:",
|
|
81
|
+
" CODEX_HOME Overrides default install root (~/.codex).",
|
|
82
|
+
" GITHUB_TOKEN Optional GitHub token for higher rate limits.",
|
|
83
|
+
"",
|
|
84
|
+
].join("\n"),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function getInstallRoot({ dest, local }) {
|
|
89
|
+
if (dest) return path.resolve(dest);
|
|
90
|
+
if (local) return path.join(process.cwd(), ".codex", "skills");
|
|
91
|
+
const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
92
|
+
return path.join(codexHome, "skills");
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function withAuthHeaders(headers = {}) {
|
|
96
|
+
const token = process.env.GITHUB_TOKEN;
|
|
97
|
+
if (!token) return headers;
|
|
98
|
+
return { ...headers, Authorization: `Bearer ${token}` };
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function fetchJson(url) {
|
|
102
|
+
const res = await fetch(url, {
|
|
103
|
+
headers: withAuthHeaders({ Accept: "application/vnd.github+json" }),
|
|
104
|
+
});
|
|
105
|
+
if (!res.ok) {
|
|
106
|
+
const text = await res.text().catch(() => "");
|
|
107
|
+
throw new Error(`GitHub request failed (${res.status}): ${text}`);
|
|
108
|
+
}
|
|
109
|
+
return res.json();
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
async function fetchBuffer(url) {
|
|
113
|
+
const res = await fetch(url, { headers: withAuthHeaders() });
|
|
114
|
+
if (!res.ok) {
|
|
115
|
+
const text = await res.text().catch(() => "");
|
|
116
|
+
throw new Error(`Download failed (${res.status}): ${text}`);
|
|
117
|
+
}
|
|
118
|
+
const ab = await res.arrayBuffer();
|
|
119
|
+
return Buffer.from(ab);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function confirm(question) {
|
|
123
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
124
|
+
return new Promise((resolve) => {
|
|
125
|
+
rl.question(question, (answer) => {
|
|
126
|
+
rl.close();
|
|
127
|
+
resolve(answer.trim().toLowerCase() === "y" || answer.trim().toLowerCase() === "yes");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function listRepoTree(owner, repo, ref) {
|
|
133
|
+
const url = `https://api.github.com/repos/${owner}/${repo}/git/trees/${ref}?recursive=1`;
|
|
134
|
+
const data = await fetchJson(url);
|
|
135
|
+
return Array.isArray(data.tree) ? data.tree : [];
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function findSkillPrefix(tree, skillRef) {
|
|
139
|
+
const curatedPrefix = `skills/.curated/${skillRef}/`;
|
|
140
|
+
const experimentalPrefix = `skills/.experimental/${skillRef}/`;
|
|
141
|
+
|
|
142
|
+
const hasCurated = tree.some((item) => item.type === "blob" && item.path.startsWith(curatedPrefix));
|
|
143
|
+
if (hasCurated) return curatedPrefix;
|
|
144
|
+
|
|
145
|
+
const hasExperimental = tree.some((item) => item.type === "blob" && item.path.startsWith(experimentalPrefix));
|
|
146
|
+
if (hasExperimental) return experimentalPrefix;
|
|
147
|
+
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
async function installSkill({ owner, repo, ref, tree, skillRef, installRoot, yes }) {
|
|
152
|
+
process.stdout.write(`š” Installing skill: ${skillRef}\n`);
|
|
153
|
+
const prefix = findSkillPrefix(tree, skillRef);
|
|
154
|
+
if (!prefix) {
|
|
155
|
+
throw new Error(`Could not find skill '${skillRef}' in ${owner}/${repo}@${ref}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const destDir = path.join(installRoot, skillRef);
|
|
159
|
+
if (!yes && fs.existsSync(destDir)) {
|
|
160
|
+
const ok = await confirm(`ā ļø ${destDir} already exists. Overwrite? (y/N) `);
|
|
161
|
+
if (!ok) return { installed: false };
|
|
162
|
+
fs.rmSync(destDir, { recursive: true, force: true });
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
fs.mkdirSync(destDir, { recursive: true });
|
|
166
|
+
process.stdout.write(`š„ Downloading skill from GitHub (${ref} branch)...\n`);
|
|
167
|
+
|
|
168
|
+
const files = tree
|
|
169
|
+
.filter((item) => item.type === "blob" && item.path.startsWith(prefix))
|
|
170
|
+
.map((item) => item.path)
|
|
171
|
+
.sort((a, b) => a.localeCompare(b));
|
|
172
|
+
|
|
173
|
+
const seenDirs = new Set();
|
|
174
|
+
for (const filePath of files) {
|
|
175
|
+
const relative = filePath.slice(prefix.length);
|
|
176
|
+
if (!relative) continue;
|
|
177
|
+
|
|
178
|
+
const dir = path.dirname(relative);
|
|
179
|
+
if (dir !== "." && !seenDirs.has(dir)) {
|
|
180
|
+
process.stdout.write(`š Downloading directory: ${dir}/\n`);
|
|
181
|
+
seenDirs.add(dir);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const rawUrl = `https://raw.githubusercontent.com/${owner}/${repo}/${ref}/${filePath}`;
|
|
185
|
+
const destPath = path.join(destDir, relative);
|
|
186
|
+
fs.mkdirSync(path.dirname(destPath), { recursive: true });
|
|
187
|
+
const buf = await fetchBuffer(rawUrl);
|
|
188
|
+
fs.writeFileSync(destPath, buf);
|
|
189
|
+
process.stdout.write(`ā Downloaded: ${relative}\n`);
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
return { installed: true, files: files.length };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function main() {
|
|
196
|
+
const args = parseArgs(process.argv.slice(2));
|
|
197
|
+
if (args.help) {
|
|
198
|
+
printHelp();
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (args.skills.length === 0) {
|
|
203
|
+
printHelp();
|
|
204
|
+
process.exitCode = 1;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
printHeader();
|
|
209
|
+
|
|
210
|
+
const installRoot = getInstallRoot({ dest: args.dest, local: args.local });
|
|
211
|
+
if (!args.yes) {
|
|
212
|
+
process.stdout.write(`š Install root: ${installRoot}\n`);
|
|
213
|
+
process.stdout.write(` (Tip: use --local to install into <repo>/.codex/skills)\n\n`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const total = args.skills.length;
|
|
217
|
+
process.stdout.write(`š§ Installing multiple components...\n`);
|
|
218
|
+
process.stdout.write(`š¦ Installing ${total} components:\n`);
|
|
219
|
+
process.stdout.write(` Agents: 0\n`);
|
|
220
|
+
process.stdout.write(` Commands: 0\n`);
|
|
221
|
+
process.stdout.write(` MCPs: 0\n`);
|
|
222
|
+
process.stdout.write(` Settings: 0\n`);
|
|
223
|
+
process.stdout.write(` Hooks: 0\n`);
|
|
224
|
+
process.stdout.write(` Skills: ${total}\n`);
|
|
225
|
+
|
|
226
|
+
const tree = await listRepoTree(args.owner, args.repo, args.ref);
|
|
227
|
+
|
|
228
|
+
let installedCount = 0;
|
|
229
|
+
for (const skillRef of args.skills) {
|
|
230
|
+
process.stdout.write(` Installing skill: ${skillRef}\n`);
|
|
231
|
+
const result = await installSkill({
|
|
232
|
+
owner: args.owner,
|
|
233
|
+
repo: args.repo,
|
|
234
|
+
ref: args.ref,
|
|
235
|
+
tree,
|
|
236
|
+
skillRef,
|
|
237
|
+
installRoot,
|
|
238
|
+
yes: args.yes,
|
|
239
|
+
});
|
|
240
|
+
if (result.installed) installedCount += 1;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
process.stdout.write(`\nā
Successfully installed ${installedCount} components!\n`);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
main().catch((err) => {
|
|
247
|
+
process.stderr.write(`\nā ${err instanceof Error ? err.message : String(err)}\n`);
|
|
248
|
+
process.exitCode = 1;
|
|
249
|
+
});
|
package/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codex-skills-registry",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Install Codex skills from GitHub into your local Codex setup.",
|
|
5
|
+
"private": false,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"homepage": "https://skillregistry.dev",
|
|
9
|
+
"repository": {
|
|
10
|
+
"type": "git",
|
|
11
|
+
"url": "git+https://github.com/vadimcomanescu/codex-tmpl.git",
|
|
12
|
+
"directory": "packages/codex-skills-registry"
|
|
13
|
+
},
|
|
14
|
+
"bugs": {
|
|
15
|
+
"url": "https://github.com/vadimcomanescu/codex-tmpl/issues"
|
|
16
|
+
},
|
|
17
|
+
"keywords": ["codex", "openai", "cli", "skills", "installer"],
|
|
18
|
+
"funding": {
|
|
19
|
+
"type": "individual",
|
|
20
|
+
"url": "https://buymeacoffee.com/vadim984"
|
|
21
|
+
},
|
|
22
|
+
"files": ["bin", "README.md", "package.json"],
|
|
23
|
+
"publishConfig": {
|
|
24
|
+
"access": "public"
|
|
25
|
+
},
|
|
26
|
+
"bin": {
|
|
27
|
+
"codex-skills-registry": "bin/cli.js"
|
|
28
|
+
},
|
|
29
|
+
"engines": {
|
|
30
|
+
"node": ">=18"
|
|
31
|
+
}
|
|
32
|
+
}
|