addx-skills 1.0.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.
Files changed (3) hide show
  1. package/README.md +153 -0
  2. package/dist/cli.js +771 -0
  3. package/package.json +45 -0
package/README.md ADDED
@@ -0,0 +1,153 @@
1
+ # addx-skills
2
+
3
+ 从公司私有 GitLab 仓库 (`engineering/skills`) 搜索和安装 AI Agent Skills 的 CLI 工具。
4
+
5
+ ## 快速开始
6
+
7
+ ```bash
8
+ # 搜索所有可用 Skills
9
+ npx addx-skills find
10
+
11
+ # 按关键词搜索
12
+ npx addx-skills find "sql"
13
+ npx addx-skills find "安全检查"
14
+
15
+ # 安装 Skill(默认安装到当前项目 .agents/skills/)
16
+ npx addx-skills add security/sql-injection
17
+
18
+ # 安装到全局(所有项目可用)
19
+ npx addx-skills add dev-standards-review -g
20
+ ```
21
+
22
+ ## 前置条件
23
+
24
+ - **Node.js >= 18**
25
+ - **Git 凭证**:能通过 `git clone` 访问 `gitlab.addx.ai`(SSH key 或 HTTPS credential helper)
26
+
27
+ 无需额外配置 Token,复用系统已有的 git 凭证。
28
+
29
+ ## 命令
30
+
31
+ ### `find [query]` — 搜索 Skills
32
+
33
+ ```bash
34
+ npx addx-skills find # 列出全部
35
+ npx addx-skills find "sql" # 按关键词搜索
36
+ npx addx-skills find --no-cache # 强制刷新缓存后搜索
37
+ ```
38
+
39
+ 搜索按以下优先级匹配:Skill ID > name > description > 全文内容。
40
+
41
+ ### `add <skill-id>` — 安装 Skill
42
+
43
+ ```bash
44
+ npx addx-skills add security/sql-injection # 安装到当前项目(默认)
45
+ npx addx-skills add security/sql-injection -g # 安装到全局
46
+ npx addx-skills add security/sql-injection -a claude-code # 指定 Agent
47
+ npx addx-skills add security/sql-injection --force # 强制覆盖
48
+ ```
49
+
50
+ 支持的 Agent:`cursor`(默认)、`claude-code`、`codex`、`antigravity`
51
+
52
+ ### `list` / `ls` — 列出已安装
53
+
54
+ ```bash
55
+ npx addx-skills list
56
+ npx addx-skills ls --global # 仅全局
57
+ npx addx-skills ls --project # 仅当前项目
58
+ ```
59
+
60
+ ### `remove` / `rm` — 卸载 Skill
61
+
62
+ ```bash
63
+ npx addx-skills remove security/sql-injection
64
+ npx addx-skills rm dev-standards-review -g # 从全局卸载
65
+ ```
66
+
67
+ ### `check` — 检查更新
68
+
69
+ ```bash
70
+ npx addx-skills check # 检查已安装 Skills 是否有更新
71
+ ```
72
+
73
+ ### `update` — 更新
74
+
75
+ ```bash
76
+ npx addx-skills update # 更新缓存 + 已安装的 Skills
77
+ npx addx-skills update --cache # 仅更新缓存
78
+ ```
79
+
80
+ ### `config` — 配置管理
81
+
82
+ ```bash
83
+ npx addx-skills config # 查看当前配置
84
+ npx addx-skills config --repo <url> # 设置自定义仓库
85
+ npx addx-skills config --branch develop # 设置分支
86
+ npx addx-skills config --cache-ttl 7200 # 设置缓存有效期(秒)
87
+ ```
88
+
89
+ ## 配置
90
+
91
+ 默认零配置。可通过环境变量或配置文件自定义:
92
+
93
+ | 配置项 | 环境变量 | 默认值 |
94
+ |--------|----------|--------|
95
+ | Skills 仓库 URL | `ADDX_SKILLS_REPO` | `https://gitlab.addx.ai/engineering/skills.git` |
96
+ | 默认分支 | `ADDX_SKILLS_BRANCH` | `main` |
97
+ | 缓存过期时间 | `ADDX_SKILLS_CACHE_TTL` | `3600`(秒) |
98
+
99
+ 配置文件路径:`~/.config/addx-skills/config.json`
100
+
101
+ ## 与官方 CLI 的关系
102
+
103
+ ```
104
+ npx skills find "xxx" → 搜索公共 skills.sh 市场
105
+ npx skills add <url> → 从任意 Git 源安装
106
+
107
+ npx addx-skills find "xxx" → 搜索公司私有 GitLab 仓库(本工具)
108
+ npx addx-skills add <id> → 从私有仓库安装(本工具)
109
+ ```
110
+
111
+ 两个工具互补共存,不冲突。命令和选项风格与官方 CLI 保持一致。
112
+
113
+ ## 开发
114
+
115
+ ```bash
116
+ npm install
117
+ npm run build # 构建
118
+ npm run typecheck # 类型检查
119
+ npm run dev # 开发模式(watch)
120
+ npm test # 运行测试
121
+ ```
122
+
123
+ ## 发布
124
+
125
+ ```bash
126
+ # 1. 确保在 main 分支且代码最新
127
+ git checkout main && git pull
128
+
129
+ # 2. 升版本(自动修改 package.json + git commit + git tag)
130
+ npm version patch # bug 修复: 1.0.0 → 1.0.1
131
+ npm version minor # 新功能: 1.0.0 → 1.1.0
132
+ npm version major # 破坏性: 1.0.0 → 2.0.0
133
+
134
+ # 3. 推送,CI 自动 npm publish
135
+ git push origin main --tags
136
+ ```
137
+
138
+ 首次发布(已是 `1.0.0`)可直接打 tag:
139
+
140
+ ```bash
141
+ git tag v1.0.0 && git push origin v1.0.0
142
+ ```
143
+
144
+ ## 技术栈
145
+
146
+ | 组件 | 技术 |
147
+ |------|------|
148
+ | 语言 | TypeScript |
149
+ | 运行时 | Node.js >= 18 |
150
+ | 构建 | tsup |
151
+ | Git 操作 | simple-git |
152
+ | Frontmatter 解析 | gray-matter |
153
+ | 交互式 UI | @clack/prompts |
package/dist/cli.js ADDED
@@ -0,0 +1,771 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/cli.ts
4
+ import * as p8 from "@clack/prompts";
5
+
6
+ // src/commands/find.ts
7
+ import * as p from "@clack/prompts";
8
+
9
+ // src/lib/cache.ts
10
+ import fs2 from "fs";
11
+ import path3 from "path";
12
+ import simpleGit from "simple-git";
13
+
14
+ // src/lib/constants.ts
15
+ import path from "path";
16
+ import os from "os";
17
+ var DEFAULT_REPO = "https://gitlab.addx.ai/engineering/skills.git";
18
+ var DEFAULT_BRANCH = "main";
19
+ var DEFAULT_CACHE_TTL = 3600;
20
+ function getCacheDir() {
21
+ const cacheBase = process.env.XDG_CACHE_HOME || path.join(os.homedir(), ".cache");
22
+ return path.join(cacheBase, "addx-skills", "skills-repo");
23
+ }
24
+ function getConfigPath() {
25
+ const configBase = process.env.XDG_CONFIG_HOME || path.join(os.homedir(), ".config");
26
+ return path.join(configBase, "addx-skills", "config.json");
27
+ }
28
+ var EXCLUDED_DIRS = [
29
+ ".config",
30
+ ".cursor",
31
+ ".git",
32
+ ".gitlab",
33
+ ".specstory",
34
+ "docs",
35
+ "scripts",
36
+ "templates"
37
+ ];
38
+ var AGENT_PATHS = {
39
+ cursor: {
40
+ global: path.join(os.homedir(), ".cursor", "skills"),
41
+ project: path.join(".agents", "skills")
42
+ },
43
+ "claude-code": {
44
+ global: path.join(os.homedir(), ".claude", "skills"),
45
+ project: path.join(".claude", "skills")
46
+ },
47
+ codex: {
48
+ global: path.join(os.homedir(), ".codex", "skills"),
49
+ project: path.join(".agents", "skills")
50
+ },
51
+ antigravity: {
52
+ global: path.join(os.homedir(), ".gemini", "antigravity", "skills"),
53
+ project: path.join(".agent", "skills")
54
+ }
55
+ };
56
+ var DEFAULT_AGENT = "cursor";
57
+ var DEFAULT_CONFIG = {
58
+ repo: DEFAULT_REPO,
59
+ branch: DEFAULT_BRANCH,
60
+ cacheTTL: DEFAULT_CACHE_TTL
61
+ };
62
+
63
+ // src/lib/config.ts
64
+ import fs from "fs";
65
+ import path2 from "path";
66
+ function loadConfig() {
67
+ const fileConfig = readConfigFile();
68
+ return {
69
+ repo: process.env.ADDX_SKILLS_REPO || fileConfig.repo || DEFAULT_REPO,
70
+ branch: process.env.ADDX_SKILLS_BRANCH || fileConfig.branch || DEFAULT_BRANCH,
71
+ cacheTTL: process.env.ADDX_SKILLS_CACHE_TTL ? parseInt(process.env.ADDX_SKILLS_CACHE_TTL, 10) : fileConfig.cacheTTL ?? DEFAULT_CACHE_TTL
72
+ };
73
+ }
74
+ function readConfigFile() {
75
+ const configPath = getConfigPath();
76
+ try {
77
+ if (fs.existsSync(configPath)) {
78
+ const raw = fs.readFileSync(configPath, "utf-8");
79
+ return JSON.parse(raw);
80
+ }
81
+ } catch {
82
+ }
83
+ return {};
84
+ }
85
+ function saveConfig(updates) {
86
+ const configPath = getConfigPath();
87
+ const current = readConfigFile();
88
+ const merged = { ...DEFAULT_CONFIG, ...current, ...updates };
89
+ fs.mkdirSync(path2.dirname(configPath), { recursive: true });
90
+ fs.writeFileSync(configPath, JSON.stringify(merged, null, 2) + "\n", "utf-8");
91
+ }
92
+ function getEffectiveConfig() {
93
+ const fileConfig = readConfigFile();
94
+ const configPath = getConfigPath();
95
+ const source = {};
96
+ const result = loadConfig();
97
+ for (const key of ["repo", "branch", "cacheTTL"]) {
98
+ if (key === "cacheTTL" && process.env.ADDX_SKILLS_CACHE_TTL) {
99
+ source[key] = "env: ADDX_SKILLS_CACHE_TTL";
100
+ } else if (key === "repo" && process.env.ADDX_SKILLS_REPO) {
101
+ source[key] = "env: ADDX_SKILLS_REPO";
102
+ } else if (key === "branch" && process.env.ADDX_SKILLS_BRANCH) {
103
+ source[key] = "env: ADDX_SKILLS_BRANCH";
104
+ } else if (fileConfig[key] !== void 0) {
105
+ source[key] = `file: ${configPath}`;
106
+ } else {
107
+ source[key] = "default";
108
+ }
109
+ }
110
+ return { ...result, configPath, source };
111
+ }
112
+
113
+ // src/lib/cache.ts
114
+ function isCacheValid(cacheDir, ttlSeconds) {
115
+ const marker = path3.join(cacheDir, ".git", "HEAD");
116
+ try {
117
+ const stat = fs2.statSync(marker);
118
+ const ageMs = Date.now() - stat.mtimeMs;
119
+ return ageMs < ttlSeconds * 1e3;
120
+ } catch {
121
+ return false;
122
+ }
123
+ }
124
+ function touchCache(cacheDir) {
125
+ const marker = path3.join(cacheDir, ".git", "HEAD");
126
+ try {
127
+ const now = /* @__PURE__ */ new Date();
128
+ fs2.utimesSync(marker, now, now);
129
+ } catch {
130
+ }
131
+ }
132
+ function isGitRepo(dir) {
133
+ return fs2.existsSync(path3.join(dir, ".git", "HEAD"));
134
+ }
135
+ async function syncCache(options) {
136
+ const config = loadConfig();
137
+ const cacheDir = getCacheDir();
138
+ if (options?.noCache && isGitRepo(cacheDir)) {
139
+ return forceUpdate(cacheDir, config.branch);
140
+ }
141
+ if (!isGitRepo(cacheDir)) {
142
+ return cloneRepo(cacheDir, config.repo, config.branch);
143
+ }
144
+ if (!isCacheValid(cacheDir, config.cacheTTL)) {
145
+ return updateRepo(cacheDir, config.branch);
146
+ }
147
+ return { status: "cached", cacheDir };
148
+ }
149
+ async function cloneRepo(cacheDir, repo, branch) {
150
+ fs2.mkdirSync(path3.dirname(cacheDir), { recursive: true });
151
+ const git = simpleGit();
152
+ await git.clone(repo, cacheDir, ["--depth", "1", "--branch", branch, "--single-branch"]);
153
+ return { status: "cloned", cacheDir };
154
+ }
155
+ async function updateRepo(cacheDir, branch) {
156
+ const git = simpleGit(cacheDir);
157
+ await git.fetch(["--depth", "1", "origin", branch]);
158
+ await git.reset(["--hard", `origin/${branch}`]);
159
+ touchCache(cacheDir);
160
+ return { status: "updated", cacheDir };
161
+ }
162
+ async function forceUpdate(cacheDir, branch) {
163
+ return updateRepo(cacheDir, branch);
164
+ }
165
+
166
+ // src/lib/registry.ts
167
+ import fs3 from "fs";
168
+ import path4 from "path";
169
+ import matter from "gray-matter";
170
+ function scanSkills(repoDir) {
171
+ const skills = [];
172
+ walkDir(repoDir, repoDir, skills);
173
+ return skills.sort((a, b) => a.id.localeCompare(b.id));
174
+ }
175
+ function walkDir(baseDir, currentDir, results) {
176
+ let entries;
177
+ try {
178
+ entries = fs3.readdirSync(currentDir, { withFileTypes: true });
179
+ } catch {
180
+ return;
181
+ }
182
+ for (const entry of entries) {
183
+ if (entry.isDirectory()) {
184
+ const relative = path4.relative(baseDir, path4.join(currentDir, entry.name));
185
+ const topLevel = relative.split(path4.sep)[0];
186
+ if (EXCLUDED_DIRS.includes(topLevel)) continue;
187
+ walkDir(baseDir, path4.join(currentDir, entry.name), results);
188
+ } else if (entry.name === "SKILL.md") {
189
+ const skill = parseSkillFile(baseDir, path4.join(currentDir, entry.name));
190
+ if (skill) results.push(skill);
191
+ }
192
+ }
193
+ }
194
+ function parseSkillFile(baseDir, filePath) {
195
+ try {
196
+ const raw = fs3.readFileSync(filePath, "utf-8");
197
+ const { data, content } = matter(raw);
198
+ const relativePath = path4.relative(baseDir, filePath);
199
+ const id = deriveSkillId(relativePath);
200
+ return {
201
+ id,
202
+ name: data.name || id,
203
+ description: data.description || extractFirstLine(content),
204
+ relativePath,
205
+ content
206
+ };
207
+ } catch {
208
+ return null;
209
+ }
210
+ }
211
+ function deriveSkillId(relativePath) {
212
+ const normalized = relativePath.split(path4.sep).join("/");
213
+ return normalized.replace(/\/SKILL\.md$/, "");
214
+ }
215
+ function extractFirstLine(content) {
216
+ const lines = content.trim().split("\n");
217
+ for (const line of lines) {
218
+ const stripped = line.replace(/^#+\s*/, "").trim();
219
+ if (stripped) return stripped.slice(0, 100);
220
+ }
221
+ return "";
222
+ }
223
+ function searchSkills(skills, query) {
224
+ if (!query || query.trim() === "") {
225
+ return skills;
226
+ }
227
+ const q = query.toLowerCase();
228
+ const matches = [];
229
+ for (const skill of skills) {
230
+ const priority = getMatchPriority(skill, q);
231
+ if (priority > 0) {
232
+ matches.push({ skill, priority });
233
+ }
234
+ }
235
+ matches.sort((a, b) => {
236
+ if (a.priority !== b.priority) return a.priority - b.priority;
237
+ return a.skill.id.localeCompare(b.skill.id);
238
+ });
239
+ return matches.map((m) => m.skill);
240
+ }
241
+ function getMatchPriority(skill, query) {
242
+ if (skill.id.toLowerCase().includes(query)) return 1;
243
+ if (skill.name.toLowerCase().includes(query)) return 2;
244
+ if (skill.description.toLowerCase().includes(query)) return 3;
245
+ if (skill.content.toLowerCase().includes(query)) return 4;
246
+ return 0;
247
+ }
248
+ function findSkillById(skills, id) {
249
+ return skills.find((s) => s.id === id);
250
+ }
251
+
252
+ // src/lib/installer.ts
253
+ import fs4 from "fs";
254
+ import path5 from "path";
255
+ function installSkill(repoDir, skill, options = {}) {
256
+ const agent = options.agent || DEFAULT_AGENT;
257
+ const paths = AGENT_PATHS[agent];
258
+ const baseDir = options.global ? paths.global : paths.project;
259
+ const targetDir = path5.join(baseDir, skill.id);
260
+ const exists = fs4.existsSync(targetDir);
261
+ if (exists && !options.force) {
262
+ throw new Error(
263
+ `Skill "${skill.id}" already installed at ${targetDir}
264
+ Use --force to overwrite.`
265
+ );
266
+ }
267
+ const sourceDir = path5.join(repoDir, path5.dirname(skill.relativePath));
268
+ copyDirSync(sourceDir, targetDir);
269
+ return { skillId: skill.id, targetDir, overwritten: exists };
270
+ }
271
+ function removeSkill(skillId, options = {}) {
272
+ const agent = options.agent || DEFAULT_AGENT;
273
+ const paths = AGENT_PATHS[agent];
274
+ const baseDir = options.global ? paths.global : paths.project;
275
+ const targetDir = path5.join(baseDir, skillId);
276
+ if (!fs4.existsSync(targetDir)) {
277
+ throw new Error(`Skill "${skillId}" is not installed at ${targetDir}`);
278
+ }
279
+ fs4.rmSync(targetDir, { recursive: true, force: true });
280
+ return targetDir;
281
+ }
282
+ function listInstalledSkills(options) {
283
+ const agent = options.agent || DEFAULT_AGENT;
284
+ const paths = AGENT_PATHS[agent];
285
+ const results = [];
286
+ const scanGlobal = options.global || !options.global && !options.project;
287
+ const scanProject = options.project || !options.global && !options.project;
288
+ if (scanGlobal) {
289
+ scanInstalled(paths.global, "global", results);
290
+ }
291
+ if (scanProject) {
292
+ scanInstalled(paths.project, "project", results);
293
+ }
294
+ return results.sort((a, b) => a.id.localeCompare(b.id));
295
+ }
296
+ function isSkillInstalled(skillId, agent) {
297
+ const a = agent || DEFAULT_AGENT;
298
+ const paths = AGENT_PATHS[a];
299
+ const globalDir = path5.join(paths.global, skillId);
300
+ const projectDir = path5.join(paths.project, skillId);
301
+ return fs4.existsSync(path5.join(globalDir, "SKILL.md")) || fs4.existsSync(path5.join(projectDir, "SKILL.md"));
302
+ }
303
+ function scanInstalled(baseDir, location, results) {
304
+ if (!fs4.existsSync(baseDir)) return;
305
+ const entries = fs4.readdirSync(baseDir, { withFileTypes: true });
306
+ for (const entry of entries) {
307
+ if (!entry.isDirectory()) continue;
308
+ const entryPath = path5.join(baseDir, entry.name);
309
+ if (fs4.existsSync(path5.join(entryPath, "SKILL.md"))) {
310
+ results.push({ id: entry.name, location, dir: entryPath });
311
+ } else {
312
+ const subEntries = safeReaddir(entryPath);
313
+ for (const sub of subEntries) {
314
+ if (!sub.isDirectory()) continue;
315
+ const subPath = path5.join(entryPath, sub.name);
316
+ if (fs4.existsSync(path5.join(subPath, "SKILL.md"))) {
317
+ results.push({ id: `${entry.name}/${sub.name}`, location, dir: subPath });
318
+ }
319
+ }
320
+ }
321
+ }
322
+ }
323
+ function safeReaddir(dir) {
324
+ try {
325
+ return fs4.readdirSync(dir, { withFileTypes: true });
326
+ } catch {
327
+ return [];
328
+ }
329
+ }
330
+ function copyDirSync(src, dest) {
331
+ fs4.mkdirSync(dest, { recursive: true });
332
+ const entries = fs4.readdirSync(src, { withFileTypes: true });
333
+ for (const entry of entries) {
334
+ const srcPath = path5.join(src, entry.name);
335
+ const destPath = path5.join(dest, entry.name);
336
+ if (entry.isDirectory()) {
337
+ copyDirSync(srcPath, destPath);
338
+ } else {
339
+ fs4.copyFileSync(srcPath, destPath);
340
+ }
341
+ }
342
+ }
343
+
344
+ // src/commands/find.ts
345
+ async function findCommand(query, options = {}) {
346
+ const spinner5 = p.spinner();
347
+ spinner5.start("Syncing skills registry...");
348
+ try {
349
+ const result = await syncCache({ noCache: options.noCache });
350
+ const statusText = result.status === "cached" ? "cached" : result.status;
351
+ spinner5.stop(`Syncing skills registry... done (${statusText})`);
352
+ const allSkills = scanSkills(result.cacheDir);
353
+ const matched = searchSkills(allSkills, query);
354
+ for (const skill of matched) {
355
+ skill.installed = isSkillInstalled(skill.id);
356
+ }
357
+ if (matched.length === 0) {
358
+ p.log.warn(query ? `No skills found matching "${query}"` : "No skills found");
359
+ return;
360
+ }
361
+ p.log.info("Available Skills (engineering/skills):");
362
+ p.log.message("");
363
+ for (const skill of matched) {
364
+ const tag = skill.installed ? " [installed]" : "";
365
+ const desc = skill.description ? `
366
+ ${truncate(skill.description, 60)}` : "";
367
+ p.log.message(` ${skill.id}${tag}${desc}`);
368
+ }
369
+ p.log.message("");
370
+ p.log.info(`${matched.length} skill${matched.length > 1 ? "s" : ""} found`);
371
+ p.log.message(" Install: npx addx-skills add <skill-id>");
372
+ } catch (err) {
373
+ spinner5.stop("Syncing skills registry... failed");
374
+ p.log.error(err instanceof Error ? err.message : String(err));
375
+ process.exit(1);
376
+ }
377
+ }
378
+ function truncate(str, maxLen) {
379
+ if (str.length <= maxLen) return str;
380
+ return str.slice(0, maxLen - 3) + "...";
381
+ }
382
+
383
+ // src/commands/add.ts
384
+ import * as p2 from "@clack/prompts";
385
+ async function addCommand(skillId, options = {}) {
386
+ const spinner5 = p2.spinner();
387
+ spinner5.start("Syncing skills registry...");
388
+ try {
389
+ const result = await syncCache({ noCache: options.noCache });
390
+ spinner5.stop("Syncing skills registry... done");
391
+ const allSkills = scanSkills(result.cacheDir);
392
+ const skill = findSkillById(allSkills, skillId);
393
+ if (!skill) {
394
+ p2.log.error(`Skill "${skillId}" not found`);
395
+ p2.log.info("Use `npx addx-skills find` to see available skills");
396
+ process.exit(1);
397
+ return;
398
+ }
399
+ const installResult = installSkill(result.cacheDir, skill, {
400
+ agent: options.agent,
401
+ global: options.global,
402
+ force: options.force
403
+ });
404
+ if (installResult.overwritten) {
405
+ p2.log.success(`Skill "${skillId}" updated at ${installResult.targetDir}`);
406
+ } else {
407
+ p2.log.success(`Skill "${skillId}" installed to ${installResult.targetDir}`);
408
+ }
409
+ } catch (err) {
410
+ spinner5.stop("Failed");
411
+ p2.log.error(err instanceof Error ? err.message : String(err));
412
+ process.exit(1);
413
+ }
414
+ }
415
+
416
+ // src/commands/list.ts
417
+ import * as p3 from "@clack/prompts";
418
+ async function listCommand(options = {}) {
419
+ const installed = listInstalledSkills({
420
+ agent: options.agent,
421
+ global: options.global,
422
+ project: options.project
423
+ });
424
+ if (installed.length === 0) {
425
+ p3.log.info("No skills installed");
426
+ p3.log.message(" Install: npx addx-skills add <skill-id>");
427
+ return;
428
+ }
429
+ p3.log.info("Installed Skills:");
430
+ p3.log.message("");
431
+ for (const skill of installed) {
432
+ const locationTag = skill.location === "project" ? " [project]" : " [global]";
433
+ p3.log.message(` ${skill.id}${locationTag}`);
434
+ p3.log.message(` ${skill.dir}`);
435
+ }
436
+ p3.log.message("");
437
+ p3.log.info(`${installed.length} skill${installed.length > 1 ? "s" : ""} installed`);
438
+ }
439
+
440
+ // src/commands/remove.ts
441
+ import * as p4 from "@clack/prompts";
442
+ async function removeCommand(skillId, options = {}) {
443
+ try {
444
+ const removedDir = removeSkill(skillId, {
445
+ agent: options.agent,
446
+ global: options.global
447
+ });
448
+ p4.log.success(`Skill "${skillId}" removed from ${removedDir}`);
449
+ } catch (err) {
450
+ p4.log.error(err instanceof Error ? err.message : String(err));
451
+ process.exit(1);
452
+ }
453
+ }
454
+
455
+ // src/commands/update.ts
456
+ import * as p5 from "@clack/prompts";
457
+ async function updateCommand(options = {}) {
458
+ const spinner5 = p5.spinner();
459
+ spinner5.start("Updating skills registry cache...");
460
+ try {
461
+ const result = await syncCache({ noCache: true });
462
+ spinner5.stop(`Cache ${result.status === "cloned" ? "initialized" : "updated"} successfully`);
463
+ if (options.cache) {
464
+ return;
465
+ }
466
+ const installed = listInstalledSkills({});
467
+ if (installed.length === 0) {
468
+ p5.log.info("No installed skills to update");
469
+ return;
470
+ }
471
+ const allSkills = scanSkills(result.cacheDir);
472
+ let updatedCount = 0;
473
+ for (const entry of installed) {
474
+ const skill = findSkillById(allSkills, entry.id);
475
+ if (!skill) {
476
+ p5.log.warn(`Skill "${entry.id}" not found in registry, skipping`);
477
+ continue;
478
+ }
479
+ installSkill(result.cacheDir, skill, {
480
+ force: true,
481
+ global: entry.location === "global"
482
+ });
483
+ updatedCount++;
484
+ }
485
+ p5.log.success(`${updatedCount} skill${updatedCount > 1 ? "s" : ""} updated`);
486
+ } catch (err) {
487
+ spinner5.stop("Update failed");
488
+ p5.log.error(err instanceof Error ? err.message : String(err));
489
+ process.exit(1);
490
+ }
491
+ }
492
+
493
+ // src/commands/check.ts
494
+ import fs5 from "fs";
495
+ import path6 from "path";
496
+ import * as p6 from "@clack/prompts";
497
+ async function checkCommand() {
498
+ const spinner5 = p6.spinner();
499
+ spinner5.start("Checking for updates...");
500
+ try {
501
+ const result = await syncCache({ noCache: true });
502
+ spinner5.stop("Checking for updates... done");
503
+ const installed = listInstalledSkills({});
504
+ if (installed.length === 0) {
505
+ p6.log.info("No installed skills to check");
506
+ return;
507
+ }
508
+ const allSkills = scanSkills(result.cacheDir);
509
+ let updatesAvailable = 0;
510
+ for (const entry of installed) {
511
+ const remoteSkill = findSkillById(allSkills, entry.id);
512
+ if (!remoteSkill) {
513
+ p6.log.warn(` ${entry.id} \u2014 not found in registry`);
514
+ continue;
515
+ }
516
+ const localContent = readLocalSkillContent(entry.dir);
517
+ const remoteDir = path6.join(result.cacheDir, path6.dirname(remoteSkill.relativePath));
518
+ const remoteContent = readSkillContent(remoteDir);
519
+ if (localContent !== remoteContent) {
520
+ p6.log.warn(` ${entry.id} \u2014 update available`);
521
+ updatesAvailable++;
522
+ } else {
523
+ p6.log.success(` ${entry.id} \u2014 up to date`);
524
+ }
525
+ }
526
+ p6.log.message("");
527
+ if (updatesAvailable > 0) {
528
+ p6.log.info(`${updatesAvailable} update${updatesAvailable > 1 ? "s" : ""} available`);
529
+ p6.log.message(" Run `npx addx-skills update` to update all");
530
+ } else {
531
+ p6.log.info("All skills are up to date");
532
+ }
533
+ } catch (err) {
534
+ spinner5.stop("Check failed");
535
+ p6.log.error(err instanceof Error ? err.message : String(err));
536
+ process.exit(1);
537
+ }
538
+ }
539
+ function readLocalSkillContent(dir) {
540
+ return readSkillContent(dir);
541
+ }
542
+ function readSkillContent(dir) {
543
+ const skillPath = path6.join(dir, "SKILL.md");
544
+ try {
545
+ return fs5.readFileSync(skillPath, "utf-8");
546
+ } catch {
547
+ return "";
548
+ }
549
+ }
550
+
551
+ // src/commands/config.ts
552
+ import * as p7 from "@clack/prompts";
553
+ async function configCommand(options = {}) {
554
+ const hasUpdates = options.repo || options.branch || options.cacheTTL;
555
+ if (hasUpdates) {
556
+ const updates = {};
557
+ if (options.repo) updates.repo = options.repo;
558
+ if (options.branch) updates.branch = options.branch;
559
+ if (options.cacheTTL) updates.cacheTTL = parseInt(options.cacheTTL, 10);
560
+ saveConfig(updates);
561
+ p7.log.success("Configuration updated");
562
+ }
563
+ const config = getEffectiveConfig();
564
+ p7.log.info("Current configuration:");
565
+ p7.log.message("");
566
+ p7.log.message(` repo ${config.repo}`);
567
+ p7.log.message(` (${config.source.repo})`);
568
+ p7.log.message(` branch ${config.branch}`);
569
+ p7.log.message(` (${config.source.branch})`);
570
+ p7.log.message(` cacheTTL ${config.cacheTTL}s`);
571
+ p7.log.message(` (${config.source.cacheTTL})`);
572
+ p7.log.message("");
573
+ p7.log.message(` Config file: ${config.configPath}`);
574
+ }
575
+
576
+ // src/cli.ts
577
+ var VERSION = "1.0.0";
578
+ var HELP_TEXT = `
579
+ addx-skills - Search and install Skills from private GitLab registry
580
+
581
+ Usage:
582
+ addx-skills <command> [options]
583
+
584
+ Commands:
585
+ find [query] Search skills (list all if no query)
586
+ add <skill-id> Install a skill to current project
587
+ list, ls List installed skills
588
+ remove, rm <id> Remove an installed skill
589
+ check Check for skill updates
590
+ update Update cache and installed skills
591
+ config View/set configuration
592
+
593
+ Global Options:
594
+ --help, -h Show help
595
+ --version, -v Show version
596
+ --no-cache Skip cache, force refresh from remote
597
+
598
+ Find Options:
599
+ --no-cache Force refresh before search
600
+
601
+ Add Options:
602
+ -g, --global Install globally instead of project-level
603
+ -a, --agent <type> Target agent: cursor | claude-code | codex | antigravity (default: cursor)
604
+ --force Overwrite if already installed
605
+
606
+ List Options:
607
+ --global Show only global skills
608
+ --project Show only project skills
609
+
610
+ Remove Options:
611
+ -g, --global Remove from global instead of project-level
612
+ -a, --agent <type> Target agent
613
+
614
+ Update Options:
615
+ --cache Only update cache, skip installed skills
616
+
617
+ Config Options:
618
+ --repo <url> Set custom repository URL
619
+ --branch <name> Set branch name
620
+ --cache-ttl <sec> Set cache TTL in seconds
621
+
622
+ Examples:
623
+ npx addx-skills find
624
+ npx addx-skills find "sql"
625
+ npx addx-skills add security/sql-injection
626
+ npx addx-skills add dev-standards-review -g
627
+ npx addx-skills ls
628
+ npx addx-skills rm security/sql-injection
629
+ npx addx-skills check
630
+ npx addx-skills update
631
+ npx addx-skills config --repo https://gitlab.example.com/team/skills.git
632
+ `;
633
+ var SHORT_FLAGS = {
634
+ g: "global",
635
+ a: "agent",
636
+ h: "help",
637
+ v: "version"
638
+ };
639
+ function parseArgs(argv) {
640
+ const args = argv.slice(2);
641
+ let command = "";
642
+ const positional = [];
643
+ const flags = {};
644
+ let i = 0;
645
+ while (i < args.length) {
646
+ const arg = args[i];
647
+ if (arg.startsWith("--")) {
648
+ const key = arg.slice(2);
649
+ const next = args[i + 1];
650
+ if (next && !next.startsWith("-")) {
651
+ flags[key] = next;
652
+ i += 2;
653
+ } else {
654
+ flags[key] = true;
655
+ i += 1;
656
+ }
657
+ } else if (arg.startsWith("-")) {
658
+ const shortKey = arg.slice(1);
659
+ const longKey = SHORT_FLAGS[shortKey] || shortKey;
660
+ const next = args[i + 1];
661
+ if (next && !next.startsWith("-") && longKey === "agent") {
662
+ flags[longKey] = next;
663
+ i += 2;
664
+ } else {
665
+ flags[longKey] = true;
666
+ i += 1;
667
+ }
668
+ } else if (!command) {
669
+ command = arg;
670
+ i += 1;
671
+ } else {
672
+ positional.push(arg);
673
+ i += 1;
674
+ }
675
+ }
676
+ return { command, positional, flags };
677
+ }
678
+ function validateAgent(value) {
679
+ if (!value || typeof value === "boolean") return void 0;
680
+ const valid = ["cursor", "claude-code", "codex", "antigravity"];
681
+ if (!valid.includes(value)) {
682
+ p8.log.error(`Invalid agent "${value}". Must be one of: ${valid.join(", ")}`);
683
+ process.exit(1);
684
+ }
685
+ return value;
686
+ }
687
+ async function main() {
688
+ const { command, positional, flags } = parseArgs(process.argv);
689
+ if (flags.version) {
690
+ console.log(VERSION);
691
+ return;
692
+ }
693
+ if (flags.help || command === "help" || command === "") {
694
+ console.log(HELP_TEXT);
695
+ return;
696
+ }
697
+ p8.intro("addx-skills");
698
+ switch (command) {
699
+ case "find": {
700
+ const query = positional[0];
701
+ await findCommand(query, {
702
+ noCache: flags["no-cache"] === true
703
+ });
704
+ break;
705
+ }
706
+ case "add": {
707
+ const skillId = positional[0];
708
+ if (!skillId) {
709
+ p8.log.error("Missing skill ID. Usage: addx-skills add <skill-id>");
710
+ process.exit(1);
711
+ }
712
+ await addCommand(skillId, {
713
+ agent: validateAgent(flags.agent),
714
+ global: flags.global === true,
715
+ force: flags.force === true,
716
+ noCache: flags["no-cache"] === true
717
+ });
718
+ break;
719
+ }
720
+ case "list":
721
+ case "ls": {
722
+ await listCommand({
723
+ agent: validateAgent(flags.agent),
724
+ global: flags.global === true,
725
+ project: flags.project === true
726
+ });
727
+ break;
728
+ }
729
+ case "remove":
730
+ case "rm": {
731
+ const skillId = positional[0];
732
+ if (!skillId) {
733
+ p8.log.error("Missing skill ID. Usage: addx-skills remove <skill-id>");
734
+ process.exit(1);
735
+ }
736
+ await removeCommand(skillId, {
737
+ agent: validateAgent(flags.agent),
738
+ global: flags.global === true
739
+ });
740
+ break;
741
+ }
742
+ case "check": {
743
+ await checkCommand();
744
+ break;
745
+ }
746
+ case "update": {
747
+ await updateCommand({
748
+ cache: flags.cache === true
749
+ });
750
+ break;
751
+ }
752
+ case "config": {
753
+ await configCommand({
754
+ repo: typeof flags.repo === "string" ? flags.repo : void 0,
755
+ branch: typeof flags.branch === "string" ? flags.branch : void 0,
756
+ cacheTTL: typeof flags["cache-ttl"] === "string" ? flags["cache-ttl"] : void 0
757
+ });
758
+ break;
759
+ }
760
+ default: {
761
+ p8.log.error(`Unknown command: ${command}`);
762
+ console.log(HELP_TEXT);
763
+ process.exit(1);
764
+ }
765
+ }
766
+ p8.outro("Done");
767
+ }
768
+ main().catch((err) => {
769
+ p8.log.error(err instanceof Error ? err.message : String(err));
770
+ process.exit(1);
771
+ });
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "addx-skills",
3
+ "version": "1.0.0",
4
+ "description": "Search and install AI Agent Skills from private GitLab registry",
5
+ "type": "module",
6
+ "bin": {
7
+ "addx-skills": "./dist/cli.js"
8
+ },
9
+ "scripts": {
10
+ "build": "tsup",
11
+ "dev": "tsup --watch",
12
+ "start": "node dist/cli.js",
13
+ "typecheck": "tsc --noEmit",
14
+ "test": "vitest run",
15
+ "test:watch": "vitest",
16
+ "prepublishOnly": "npm run build"
17
+ },
18
+ "keywords": [
19
+ "skills",
20
+ "ai",
21
+ "agent",
22
+ "cursor",
23
+ "claude",
24
+ "codex",
25
+ "cli"
26
+ ],
27
+ "license": "MIT",
28
+ "engines": {
29
+ "node": ">=18"
30
+ },
31
+ "files": [
32
+ "dist"
33
+ ],
34
+ "dependencies": {
35
+ "@clack/prompts": "^0.10.0",
36
+ "gray-matter": "^4.0.3",
37
+ "simple-git": "^3.27.0"
38
+ },
39
+ "devDependencies": {
40
+ "@types/node": "^25.3.1",
41
+ "tsup": "^8.4.0",
42
+ "typescript": "^5.7.0",
43
+ "vitest": "^3.0.0"
44
+ }
45
+ }