@comet/cli 9.0.0-beta.0 → 9.0.0-beta.1

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/lib/comet.js CHANGED
@@ -4,10 +4,12 @@ var commander_1 = require("commander");
4
4
  var download_mitmproxy_1 = require("./commands/download-mitmproxy");
5
5
  var download_oauth2_proxy_1 = require("./commands/download-oauth2-proxy");
6
6
  var generate_block_types_1 = require("./commands/generate-block-types");
7
+ var install_agent_skills_1 = require("./commands/install-agent-skills");
7
8
  var site_configs_1 = require("./commands/site-configs");
8
9
  var program = new commander_1.Command();
9
10
  program.addCommand(generate_block_types_1.generateBlockTypes);
10
11
  program.addCommand(site_configs_1.injectSiteConfigsCommand);
11
12
  program.addCommand(download_oauth2_proxy_1.downloadOAuth2ProxyCommand);
12
13
  program.addCommand(download_mitmproxy_1.downloadMitmproxyCommand);
14
+ program.addCommand(install_agent_skills_1.installAgentSkillsCommand);
13
15
  program.parse();
@@ -0,0 +1,15 @@
1
+ import { Command } from "commander";
2
+ export interface SkillSource {
3
+ label: string;
4
+ directory: string;
5
+ /** If true, create symlinks; if false, copy files (used for tmp clone dirs) */
6
+ symlink: boolean;
7
+ /** If true, skills with metadata.internal: true in their SKILL.md are excluded */
8
+ filterInternal?: boolean;
9
+ }
10
+ export interface InstallOptions {
11
+ dryRun: boolean;
12
+ }
13
+ export declare function isInternalSkill(skillFolderPath: string): boolean;
14
+ export declare function installSkills(sources: SkillSource[], targetDirs: string[], { dryRun }: InstallOptions): void;
15
+ export declare const installAgentSkillsCommand: Command;
@@ -0,0 +1,244 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
36
+ function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
37
+ return new (P || (P = Promise))(function (resolve, reject) {
38
+ function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
39
+ function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
40
+ function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
41
+ step((generator = generator.apply(thisArg, _arguments || [])).next());
42
+ });
43
+ };
44
+ var __generator = (this && this.__generator) || function (thisArg, body) {
45
+ var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
46
+ return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
47
+ function verb(n) { return function (v) { return step([n, v]); }; }
48
+ function step(op) {
49
+ if (f) throw new TypeError("Generator is already executing.");
50
+ while (g && (g = 0, op[0] && (_ = 0)), _) try {
51
+ if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
52
+ if (y = 0, t) op = [op[0] & 2, t.value];
53
+ switch (op[0]) {
54
+ case 0: case 1: t = op; break;
55
+ case 4: _.label++; return { value: op[1], done: false };
56
+ case 5: _.label++; y = op[1]; op = [0]; continue;
57
+ case 7: op = _.ops.pop(); _.trys.pop(); continue;
58
+ default:
59
+ if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
60
+ if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
61
+ if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
62
+ if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
63
+ if (t[2]) _.ops.pop();
64
+ _.trys.pop(); continue;
65
+ }
66
+ op = body.call(thisArg, _);
67
+ } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
68
+ if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
69
+ }
70
+ };
71
+ Object.defineProperty(exports, "__esModule", { value: true });
72
+ exports.installAgentSkillsCommand = void 0;
73
+ exports.isInternalSkill = isInternalSkill;
74
+ exports.installSkills = installSkills;
75
+ /* eslint-disable no-console */
76
+ var child_process_1 = require("child_process");
77
+ var commander_1 = require("commander");
78
+ var fs = __importStar(require("fs"));
79
+ var yaml = __importStar(require("js-yaml"));
80
+ var os = __importStar(require("os"));
81
+ var path = __importStar(require("path"));
82
+ function parseRepoUrl(rawUrl) {
83
+ var hashIndex = rawUrl.lastIndexOf("#");
84
+ if (hashIndex === -1) {
85
+ return { repoUrl: rawUrl, ref: undefined };
86
+ }
87
+ return { repoUrl: rawUrl.slice(0, hashIndex), ref: rawUrl.slice(hashIndex + 1) || undefined };
88
+ }
89
+ function cloneRepo(rawUrl) {
90
+ var _a = parseRepoUrl(rawUrl), repoUrl = _a.repoUrl, ref = _a.ref;
91
+ var tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "comet-agent-skills-"));
92
+ // Use sparse checkout to fetch only the skills/ folder
93
+ (0, child_process_1.execFileSync)("git", ["init", tmpDir], { stdio: "pipe" });
94
+ (0, child_process_1.execFileSync)("git", ["-C", tmpDir, "remote", "add", "origin", "--", repoUrl], { stdio: "pipe" });
95
+ (0, child_process_1.execFileSync)("git", ["-C", tmpDir, "config", "core.sparseCheckout", "true"], { stdio: "pipe" });
96
+ fs.writeFileSync(path.join(tmpDir, ".git", "info", "sparse-checkout"), "skills/\n");
97
+ var fetchRef = ref !== null && ref !== void 0 ? ref : "HEAD";
98
+ try {
99
+ console.log("Fetching ".concat(repoUrl, " ref \"").concat(fetchRef, "\" (sparse, skills/ only)..."));
100
+ (0, child_process_1.execFileSync)("git", ["-C", tmpDir, "fetch", "--depth", "1", "origin", fetchRef], { stdio: "pipe" });
101
+ (0, child_process_1.execFileSync)("git", ["-C", tmpDir, "checkout", "FETCH_HEAD"], { stdio: "pipe" });
102
+ }
103
+ catch (_b) {
104
+ console.log("Shallow fetch failed for ref \"".concat(fetchRef, "\"; falling back to full fetch..."));
105
+ (0, child_process_1.execFileSync)("git", ["-C", tmpDir, "fetch", "origin", fetchRef], { stdio: "pipe" });
106
+ (0, child_process_1.execFileSync)("git", ["-C", tmpDir, "checkout", "FETCH_HEAD"], { stdio: "pipe" });
107
+ }
108
+ return tmpDir;
109
+ }
110
+ function pathExists(p) {
111
+ try {
112
+ fs.lstatSync(p);
113
+ return true;
114
+ }
115
+ catch (_a) {
116
+ return false;
117
+ }
118
+ }
119
+ function isInternalSkill(skillFolderPath) {
120
+ var _a;
121
+ var skillMdPath = path.join(skillFolderPath, "SKILL.md");
122
+ var skillMdContent;
123
+ try {
124
+ skillMdContent = fs.readFileSync(skillMdPath, "utf-8");
125
+ }
126
+ catch (_b) {
127
+ return false;
128
+ }
129
+ var frontmatterMatch = skillMdContent.match(/^---\n([\s\S]*?)\n---/);
130
+ if (!frontmatterMatch)
131
+ return false;
132
+ try {
133
+ var parsed = yaml.load(frontmatterMatch[1]);
134
+ return ((_a = parsed === null || parsed === void 0 ? void 0 : parsed.metadata) === null || _a === void 0 ? void 0 : _a.internal) === true;
135
+ }
136
+ catch (_c) {
137
+ return false;
138
+ }
139
+ }
140
+ function listSkillFolders(directory, filterInternal) {
141
+ if (filterInternal === void 0) { filterInternal = false; }
142
+ if (!fs.existsSync(directory))
143
+ return [];
144
+ return fs
145
+ .readdirSync(directory)
146
+ .filter(function (f) { return fs.statSync(path.join(directory, f)).isDirectory(); })
147
+ .filter(function (folder) { return !filterInternal || !isInternalSkill(path.join(directory, folder)); })
148
+ .sort();
149
+ }
150
+ function installSkills(sources, targetDirs, _a) {
151
+ var dryRun = _a.dryRun;
152
+ var installed = new Set();
153
+ for (var _i = 0, sources_1 = sources; _i < sources_1.length; _i++) {
154
+ var _b = sources_1[_i], label = _b.label, directory = _b.directory, symlink = _b.symlink, filterInternal = _b.filterInternal;
155
+ var folders = listSkillFolders(directory, filterInternal);
156
+ if (folders.length === 0) {
157
+ console.log("No skills found in ".concat(label));
158
+ continue;
159
+ }
160
+ console.log("Installing ".concat(folders.length, " skill(s) from ").concat(label, "..."));
161
+ for (var _c = 0, folders_1 = folders; _c < folders_1.length; _c++) {
162
+ var folder = folders_1[_c];
163
+ if (installed.has(folder)) {
164
+ console.warn(" CONFLICT: \"".concat(folder, "\" from ").concat(label, " skipped (already installed from a higher-priority source)"));
165
+ continue;
166
+ }
167
+ var srcPath = path.resolve(path.join(directory, folder));
168
+ for (var _d = 0, targetDirs_1 = targetDirs; _d < targetDirs_1.length; _d++) {
169
+ var targetDir = targetDirs_1[_d];
170
+ var destPath = path.join(targetDir, folder);
171
+ var exists = pathExists(destPath);
172
+ if (dryRun) {
173
+ console.log(" [dry-run] Would ".concat(symlink ? "symlink" : "copy", ": ").concat(srcPath, " -> ").concat(destPath));
174
+ }
175
+ else {
176
+ if (exists)
177
+ fs.rmSync(destPath, { recursive: true, force: true });
178
+ if (symlink) {
179
+ fs.symlinkSync(srcPath, destPath);
180
+ }
181
+ else {
182
+ fs.cpSync(srcPath, destPath, { recursive: true });
183
+ }
184
+ console.log(" ".concat(symlink ? "Symlinked" : "Copied", ": ").concat(folder));
185
+ }
186
+ }
187
+ installed.add(folder);
188
+ }
189
+ }
190
+ console.log("\nTotal skills installed: ".concat(installed.size));
191
+ }
192
+ function loadConfig(configPath) {
193
+ var resolved = path.resolve(configPath);
194
+ if (!fs.existsSync(resolved)) {
195
+ throw new Error("Config file not found: ".concat(resolved));
196
+ }
197
+ var raw = fs.readFileSync(resolved, "utf-8");
198
+ return JSON.parse(raw);
199
+ }
200
+ exports.installAgentSkillsCommand = new commander_1.Command("install-agent-skills")
201
+ .description("Install agent skills from local directories and optional external git repos")
202
+ .option("--config <path>", "Path to a JSON config file specifying repos to install skills from", "agent-skills.json")
203
+ .option("--dry-run", "Show which symlinks/copies would be created without making changes", false)
204
+ .action(function (options) { return __awaiter(void 0, void 0, void 0, function () {
205
+ var configPath, dryRun, resolvedConfig, repos, cwd, targetDirs, _i, targetDirs_2, dir, sources, tempDirs, _a, repos_1, rawUrl, cloneDir, _b, tempDirs_1, tmpDir;
206
+ var _c;
207
+ return __generator(this, function (_d) {
208
+ configPath = options.config, dryRun = options.dryRun;
209
+ resolvedConfig = path.resolve(configPath);
210
+ repos = fs.existsSync(resolvedConfig) ? ((_c = loadConfig(configPath).repos) !== null && _c !== void 0 ? _c : []) : [];
211
+ console.log("=== Installing agent skills".concat(dryRun ? " (dry run)" : "", " ==="));
212
+ cwd = process.cwd();
213
+ targetDirs = [path.join(cwd, ".agents", "skills"), path.join(cwd, ".claude", "skills")];
214
+ // Ensure target directories exist (without clearing existing contents)
215
+ for (_i = 0, targetDirs_2 = targetDirs; _i < targetDirs_2.length; _i++) {
216
+ dir = targetDirs_2[_i];
217
+ fs.mkdirSync(dir, { recursive: true });
218
+ }
219
+ sources = [{ label: "local skills/", directory: path.join(cwd, "skills"), symlink: true }];
220
+ tempDirs = [];
221
+ try {
222
+ for (_a = 0, repos_1 = repos; _a < repos_1.length; _a++) {
223
+ rawUrl = repos_1[_a];
224
+ cloneDir = cloneRepo(rawUrl);
225
+ tempDirs.push(cloneDir);
226
+ sources.push({
227
+ label: "external ".concat(rawUrl),
228
+ directory: path.join(cloneDir, "skills"),
229
+ symlink: false,
230
+ filterInternal: true,
231
+ });
232
+ }
233
+ installSkills(sources, targetDirs, { dryRun: dryRun });
234
+ }
235
+ finally {
236
+ for (_b = 0, tempDirs_1 = tempDirs; _b < tempDirs_1.length; _b++) {
237
+ tmpDir = tempDirs_1[_b];
238
+ fs.rmSync(tmpDir, { recursive: true, force: true });
239
+ }
240
+ }
241
+ console.log("=== Finished ===");
242
+ return [2 /*return*/];
243
+ });
244
+ }); });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,278 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ /* eslint-disable no-console */
37
+ var fs = __importStar(require("fs"));
38
+ var path = __importStar(require("path"));
39
+ var vitest_1 = require("vitest");
40
+ var install_agent_skills_1 = require("./install-agent-skills");
41
+ vitest_1.vi.mock("fs");
42
+ var defaultOptions = { dryRun: false };
43
+ var targetDirs = [".agents/skills", ".claude/skills"];
44
+ /**
45
+ * Sets up fs mocks so that each key in skillMap is a readable directory
46
+ * containing the listed skill folder names. Destination paths do not exist
47
+ * unless listed under a target dir key.
48
+ */
49
+ function mockFilesystem(skillMap, existingDests, skillMdContents) {
50
+ if (existingDests === void 0) { existingDests = []; }
51
+ if (skillMdContents === void 0) { skillMdContents = {}; }
52
+ vitest_1.vi.mocked(fs.existsSync).mockImplementation(function (p) { return String(p) in skillMap; });
53
+ vitest_1.vi.mocked(fs.readdirSync).mockImplementation(function (p) { var _a; return (_a = skillMap[String(p)]) !== null && _a !== void 0 ? _a : []; });
54
+ vitest_1.vi.mocked(fs.statSync).mockImplementation(function (p) {
55
+ var _a;
56
+ var parentDir = path.dirname(String(p));
57
+ var name = path.basename(String(p));
58
+ var isDir = ((_a = skillMap[parentDir]) !== null && _a !== void 0 ? _a : []).includes(name);
59
+ return { isDirectory: function () { return isDir; } };
60
+ });
61
+ vitest_1.vi.mocked(fs.lstatSync).mockImplementation(function (p) {
62
+ if (existingDests.includes(String(p)))
63
+ return {};
64
+ throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
65
+ });
66
+ vitest_1.vi.mocked(fs.readFileSync).mockImplementation(function (p) {
67
+ var content = skillMdContents[String(p)];
68
+ if (content !== undefined)
69
+ return content;
70
+ throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
71
+ });
72
+ }
73
+ (0, vitest_1.beforeEach)(function () {
74
+ vitest_1.vi.mocked(fs.symlinkSync).mockImplementation(function () { return undefined; });
75
+ vitest_1.vi.mocked(fs.cpSync).mockImplementation(function () { return undefined; });
76
+ vitest_1.vi.mocked(fs.rmSync).mockImplementation(function () { return undefined; });
77
+ vitest_1.vi.mocked(fs.readFileSync).mockImplementation(function () {
78
+ throw Object.assign(new Error("ENOENT"), { code: "ENOENT" });
79
+ });
80
+ vitest_1.vi.spyOn(console, "log").mockImplementation(function () { return undefined; });
81
+ vitest_1.vi.spyOn(console, "warn").mockImplementation(function () { return undefined; });
82
+ });
83
+ (0, vitest_1.afterEach)(function () {
84
+ vitest_1.vi.clearAllMocks();
85
+ vitest_1.vi.restoreAllMocks();
86
+ });
87
+ (0, vitest_1.describe)("installSkills – symlink vs copy", function () {
88
+ (0, vitest_1.it)("symlinks skill folders into target folder if symlink: true", function () {
89
+ mockFilesystem({ "/local/skills": ["my-skill"] });
90
+ var sources = [{ label: "local skills/", directory: "/local/skills", symlink: true }];
91
+ (0, install_agent_skills_1.installSkills)(sources, targetDirs, defaultOptions);
92
+ (0, vitest_1.expect)(fs.symlinkSync).toHaveBeenCalledTimes(2);
93
+ (0, vitest_1.expect)(fs.symlinkSync).toHaveBeenCalledWith(path.resolve("/local/skills/my-skill"), ".agents/skills/my-skill");
94
+ (0, vitest_1.expect)(fs.symlinkSync).toHaveBeenCalledWith(path.resolve("/local/skills/my-skill"), ".claude/skills/my-skill");
95
+ (0, vitest_1.expect)(fs.cpSync).not.toHaveBeenCalled();
96
+ });
97
+ (0, vitest_1.it)("copies skill folders into target folder if symlink: false", function () {
98
+ mockFilesystem({ "/remote/skills": ["remote-skill"] });
99
+ var sources = [{ label: "external repo", directory: "/remote/skills", symlink: false }];
100
+ (0, install_agent_skills_1.installSkills)(sources, targetDirs, defaultOptions);
101
+ (0, vitest_1.expect)(fs.cpSync).toHaveBeenCalledTimes(2);
102
+ (0, vitest_1.expect)(fs.cpSync).toHaveBeenCalledWith(path.resolve("/remote/skills/remote-skill"), ".agents/skills/remote-skill", {
103
+ recursive: true,
104
+ });
105
+ (0, vitest_1.expect)(fs.symlinkSync).not.toHaveBeenCalled();
106
+ });
107
+ (0, vitest_1.it)("ignores files in the source directory, only installs subdirectories", function () {
108
+ mockFilesystem({ "/local/skills": ["real-skill"] });
109
+ // statSync returns isDirectory: false for "not-a-skill.md" (not in the skillMap)
110
+ vitest_1.vi.mocked(fs.readdirSync).mockImplementation(function () { return ["real-skill", "not-a-skill.md"]; });
111
+ var sources = [{ label: "local skills/", directory: "/local/skills", symlink: true }];
112
+ (0, install_agent_skills_1.installSkills)(sources, targetDirs, defaultOptions);
113
+ (0, vitest_1.expect)(fs.symlinkSync).toHaveBeenCalledTimes(2);
114
+ (0, vitest_1.expect)(fs.symlinkSync).toHaveBeenCalledWith(path.resolve("/local/skills/real-skill"), vitest_1.expect.any(String));
115
+ (0, vitest_1.expect)(fs.symlinkSync).not.toHaveBeenCalledWith(path.resolve("/local/skills/not-a-skill.md"), vitest_1.expect.any(String));
116
+ });
117
+ });
118
+ (0, vitest_1.describe)("installSkills – missing source directory", function () {
119
+ (0, vitest_1.it)("logs 'No skills found' and does nothing when source directory does not exist", function () {
120
+ mockFilesystem({}); // existsSync returns false for all paths
121
+ var sources = [{ label: "local skills/", directory: "/local/skills", symlink: true }];
122
+ (0, install_agent_skills_1.installSkills)(sources, targetDirs, defaultOptions);
123
+ (0, vitest_1.expect)(fs.symlinkSync).not.toHaveBeenCalled();
124
+ (0, vitest_1.expect)(fs.cpSync).not.toHaveBeenCalled();
125
+ (0, vitest_1.expect)(console.log).toHaveBeenCalledWith(vitest_1.expect.stringContaining("No skills found"));
126
+ });
127
+ (0, vitest_1.it)("logs 'No skills found' and does nothing when source directory is empty", function () {
128
+ mockFilesystem({ "/local/skills": [] }); // dir exists but has no entries
129
+ var sources = [{ label: "local skills/", directory: "/local/skills", symlink: true }];
130
+ (0, install_agent_skills_1.installSkills)(sources, targetDirs, defaultOptions);
131
+ (0, vitest_1.expect)(fs.symlinkSync).not.toHaveBeenCalled();
132
+ (0, vitest_1.expect)(fs.cpSync).not.toHaveBeenCalled();
133
+ (0, vitest_1.expect)(console.log).toHaveBeenCalledWith(vitest_1.expect.stringContaining("No skills found"));
134
+ });
135
+ (0, vitest_1.it)("logs 'No skills found' and does nothing when cloned external repo has no skills/ folder", function () {
136
+ mockFilesystem({}); // cloned tmp dir exists but contains no skills/ subdirectory
137
+ var sources = [
138
+ { label: "external git@github.com:org/repo.git (skills/)", directory: "/tmp/cloned-repo/skills", symlink: false },
139
+ ];
140
+ (0, install_agent_skills_1.installSkills)(sources, targetDirs, defaultOptions);
141
+ (0, vitest_1.expect)(fs.cpSync).not.toHaveBeenCalled();
142
+ (0, vitest_1.expect)(console.log).toHaveBeenCalledWith(vitest_1.expect.stringContaining("No skills found"));
143
+ });
144
+ });
145
+ (0, vitest_1.describe)("installSkills – dry-run mode", function () {
146
+ (0, vitest_1.it)("does not write any files or symlinks", function () {
147
+ mockFilesystem({ "/local/skills": ["my-skill"] });
148
+ var sources = [{ label: "local skills/", directory: "/local/skills", symlink: true }];
149
+ (0, install_agent_skills_1.installSkills)(sources, targetDirs, { dryRun: true });
150
+ (0, vitest_1.expect)(fs.symlinkSync).not.toHaveBeenCalled();
151
+ (0, vitest_1.expect)(fs.cpSync).not.toHaveBeenCalled();
152
+ (0, vitest_1.expect)(fs.rmSync).not.toHaveBeenCalled();
153
+ });
154
+ (0, vitest_1.it)("logs what would be symlinked", function () {
155
+ mockFilesystem({ "/local/skills": ["my-skill"] });
156
+ var sources = [{ label: "local skills/", directory: "/local/skills", symlink: true }];
157
+ (0, install_agent_skills_1.installSkills)(sources, targetDirs, { dryRun: true });
158
+ (0, vitest_1.expect)(console.log).toHaveBeenCalledWith(vitest_1.expect.stringContaining("[dry-run] Would symlink: ".concat(path.resolve("/local/skills/my-skill"))));
159
+ });
160
+ (0, vitest_1.it)("logs what would be copied for external sources", function () {
161
+ mockFilesystem({ "/remote/skills": ["remote-skill"] });
162
+ var sources = [{ label: "external repo", directory: "/remote/skills", symlink: false }];
163
+ (0, install_agent_skills_1.installSkills)(sources, targetDirs, { dryRun: true });
164
+ (0, vitest_1.expect)(console.log).toHaveBeenCalledWith(vitest_1.expect.stringContaining("[dry-run] Would copy: ".concat(path.resolve("/remote/skills/remote-skill"))));
165
+ });
166
+ });
167
+ (0, vitest_1.describe)("installSkills – overwrites existing destinations", function () {
168
+ (0, vitest_1.it)("removes existing destination before writing", function () {
169
+ mockFilesystem({ "/local/skills": ["my-skill"] }, [".agents/skills/my-skill", ".claude/skills/my-skill"]);
170
+ var sources = [{ label: "local skills/", directory: "/local/skills", symlink: true }];
171
+ (0, install_agent_skills_1.installSkills)(sources, targetDirs, defaultOptions);
172
+ (0, vitest_1.expect)(fs.rmSync).toHaveBeenCalledWith(".agents/skills/my-skill", { recursive: true, force: true });
173
+ (0, vitest_1.expect)(fs.rmSync).toHaveBeenCalledWith(".claude/skills/my-skill", { recursive: true, force: true });
174
+ (0, vitest_1.expect)(fs.symlinkSync).toHaveBeenCalledTimes(2);
175
+ });
176
+ });
177
+ (0, vitest_1.describe)("installSkills – conflict: local vs remote", function () {
178
+ (0, vitest_1.it)("local skill wins: symlinks local, skips remote with a CONFLICT warning", function () {
179
+ mockFilesystem({
180
+ "/local/skills": ["shared-skill"],
181
+ "/remote/skills": ["shared-skill"],
182
+ });
183
+ var sources = [
184
+ { label: "local skills/", directory: "/local/skills", symlink: true },
185
+ { label: "external repo", directory: "/remote/skills", symlink: false },
186
+ ];
187
+ (0, install_agent_skills_1.installSkills)(sources, targetDirs, defaultOptions);
188
+ (0, vitest_1.expect)(fs.symlinkSync).toHaveBeenCalled();
189
+ (0, vitest_1.expect)(fs.cpSync).not.toHaveBeenCalled();
190
+ (0, vitest_1.expect)(console.warn).toHaveBeenCalledWith(vitest_1.expect.stringContaining('CONFLICT: "shared-skill"'));
191
+ (0, vitest_1.expect)(console.warn).toHaveBeenCalledWith(vitest_1.expect.stringContaining("external repo"));
192
+ });
193
+ });
194
+ (0, vitest_1.describe)("installSkills – conflict: remote vs remote", function () {
195
+ (0, vitest_1.it)("first remote wins: copies remote1 skill, skips remote2 with a CONFLICT warning", function () {
196
+ mockFilesystem({
197
+ "/remote1/skills": ["shared-skill"],
198
+ "/remote2/skills": ["shared-skill"],
199
+ });
200
+ var sources = [
201
+ { label: "external repo1", directory: "/remote1/skills", symlink: false },
202
+ { label: "external repo2", directory: "/remote2/skills", symlink: false },
203
+ ];
204
+ (0, install_agent_skills_1.installSkills)(sources, targetDirs, defaultOptions);
205
+ // Only 2 calls (one per target dir from remote1), not 4
206
+ (0, vitest_1.expect)(fs.cpSync).toHaveBeenCalledTimes(2);
207
+ (0, vitest_1.expect)(fs.cpSync).toHaveBeenCalledWith(path.resolve("/remote1/skills/shared-skill"), vitest_1.expect.any(String), { recursive: true });
208
+ (0, vitest_1.expect)(fs.cpSync).not.toHaveBeenCalledWith(path.resolve("/remote2/skills/shared-skill"), vitest_1.expect.any(String), vitest_1.expect.any(Object));
209
+ (0, vitest_1.expect)(console.warn).toHaveBeenCalledWith(vitest_1.expect.stringContaining('CONFLICT: "shared-skill"'));
210
+ (0, vitest_1.expect)(console.warn).toHaveBeenCalledWith(vitest_1.expect.stringContaining("external repo2"));
211
+ });
212
+ });
213
+ (0, vitest_1.describe)("isInternalSkill", function () {
214
+ (0, vitest_1.it)("returns true when metadata.internal is true", function () {
215
+ mockFilesystem({}, [], { "/skills/my-skill/SKILL.md": "---\nmetadata:\n internal: true\n---\n# My Skill" });
216
+ (0, vitest_1.expect)((0, install_agent_skills_1.isInternalSkill)("/skills/my-skill")).toBe(true);
217
+ });
218
+ (0, vitest_1.it)("returns false when metadata.internal is false", function () {
219
+ mockFilesystem({}, [], { "/skills/my-skill/SKILL.md": "---\nmetadata:\n internal: false\n---\n# My Skill" });
220
+ (0, vitest_1.expect)((0, install_agent_skills_1.isInternalSkill)("/skills/my-skill")).toBe(false);
221
+ });
222
+ (0, vitest_1.it)("returns false when frontmatter has no metadata field", function () {
223
+ mockFilesystem({}, [], { "/skills/my-skill/SKILL.md": "---\ntitle: My Skill\n---\n# My Skill" });
224
+ (0, vitest_1.expect)((0, install_agent_skills_1.isInternalSkill)("/skills/my-skill")).toBe(false);
225
+ });
226
+ (0, vitest_1.it)("returns false when SKILL.md has no frontmatter", function () {
227
+ mockFilesystem({}, [], { "/skills/my-skill/SKILL.md": "# My Skill\nJust plain markdown, no frontmatter." });
228
+ (0, vitest_1.expect)((0, install_agent_skills_1.isInternalSkill)("/skills/my-skill")).toBe(false);
229
+ });
230
+ (0, vitest_1.it)("returns false when SKILL.md does not exist", function () {
231
+ mockFilesystem({}, [], {});
232
+ (0, vitest_1.expect)((0, install_agent_skills_1.isInternalSkill)("/skills/my-skill")).toBe(false);
233
+ });
234
+ (0, vitest_1.it)("returns false when SKILL.md frontmatter contains malformed YAML", function () {
235
+ mockFilesystem({}, [], { "/skills/my-skill/SKILL.md": "---\n: invalid: yaml: [\n---\n# My Skill" });
236
+ (0, vitest_1.expect)((0, install_agent_skills_1.isInternalSkill)("/skills/my-skill")).toBe(false);
237
+ });
238
+ });
239
+ (0, vitest_1.describe)("installSkills – filterInternal", function () {
240
+ (0, vitest_1.it)("excludes internal skills when filterInternal: true, installs only non-internal skills", function () {
241
+ mockFilesystem({ "/remote/skills": ["public-skill", "internal-skill"] }, [], {
242
+ "/remote/skills/internal-skill/SKILL.md": "---\nmetadata:\n internal: true\n---\n# Internal Skill",
243
+ });
244
+ var sources = [{ label: "external repo (skills/)", directory: "/remote/skills", symlink: false, filterInternal: true }];
245
+ (0, install_agent_skills_1.installSkills)(sources, targetDirs, defaultOptions);
246
+ (0, vitest_1.expect)(fs.cpSync).toHaveBeenCalledTimes(2);
247
+ (0, vitest_1.expect)(fs.cpSync).toHaveBeenCalledWith(path.resolve("/remote/skills/public-skill"), vitest_1.expect.any(String), { recursive: true });
248
+ (0, vitest_1.expect)(fs.cpSync).not.toHaveBeenCalledWith(path.resolve("/remote/skills/internal-skill"), vitest_1.expect.any(String), vitest_1.expect.any(Object));
249
+ });
250
+ (0, vitest_1.it)("installs all skills including internal ones when filterInternal is omitted", function () {
251
+ mockFilesystem({ "/local/skills": ["public-skill", "internal-skill"] }, [], {
252
+ "/local/skills/internal-skill/SKILL.md": "---\nmetadata:\n internal: true\n---\n# Internal Skill",
253
+ });
254
+ var sources = [{ label: "local skills/", directory: "/local/skills", symlink: true }];
255
+ (0, install_agent_skills_1.installSkills)(sources, targetDirs, defaultOptions);
256
+ (0, vitest_1.expect)(fs.symlinkSync).toHaveBeenCalledTimes(4);
257
+ (0, vitest_1.expect)(fs.symlinkSync).toHaveBeenCalledWith(path.resolve("/local/skills/public-skill"), vitest_1.expect.any(String));
258
+ (0, vitest_1.expect)(fs.symlinkSync).toHaveBeenCalledWith(path.resolve("/local/skills/internal-skill"), vitest_1.expect.any(String));
259
+ });
260
+ (0, vitest_1.it)("logs 'No skills found' when filterInternal: true and all skills are internal", function () {
261
+ mockFilesystem({ "/remote/skills": ["internal-skill"] }, [], {
262
+ "/remote/skills/internal-skill/SKILL.md": "---\nmetadata:\n internal: true\n---\n# Internal Skill",
263
+ });
264
+ var sources = [{ label: "external repo (skills/)", directory: "/remote/skills", symlink: false, filterInternal: true }];
265
+ (0, install_agent_skills_1.installSkills)(sources, targetDirs, defaultOptions);
266
+ (0, vitest_1.expect)(fs.cpSync).not.toHaveBeenCalled();
267
+ (0, vitest_1.expect)(console.log).toHaveBeenCalledWith(vitest_1.expect.stringContaining("No skills found"));
268
+ });
269
+ (0, vitest_1.it)("excludes internal skills from dry-run output when filterInternal: true", function () {
270
+ mockFilesystem({ "/remote/skills": ["public-skill", "internal-skill"] }, [], {
271
+ "/remote/skills/internal-skill/SKILL.md": "---\nmetadata:\n internal: true\n---\n# Internal Skill",
272
+ });
273
+ var sources = [{ label: "external repo (skills/)", directory: "/remote/skills", symlink: false, filterInternal: true }];
274
+ (0, install_agent_skills_1.installSkills)(sources, targetDirs, { dryRun: true });
275
+ (0, vitest_1.expect)(console.log).toHaveBeenCalledWith(vitest_1.expect.stringContaining("public-skill"));
276
+ (0, vitest_1.expect)(console.log).not.toHaveBeenCalledWith(vitest_1.expect.stringContaining("internal-skill"));
277
+ });
278
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@comet/cli",
3
- "version": "9.0.0-beta.0",
3
+ "version": "9.0.0-beta.1",
4
4
  "description": "A collection of CLI tools for Comet projects",
5
5
  "repository": {
6
6
  "directory": "packages/cli",
@@ -20,17 +20,19 @@
20
20
  ],
21
21
  "dependencies": {
22
22
  "commander": "^9.5.0",
23
+ "js-yaml": "^4.1.0",
23
24
  "prettier": "^3.6.2",
24
25
  "ts-node": "^10.9.2"
25
26
  },
26
27
  "devDependencies": {
27
- "@types/node": "^24.10.9",
28
+ "@types/js-yaml": "^4.0.9",
29
+ "@types/node": "^24.12.0",
28
30
  "eslint": "^9.39.2",
29
31
  "npm-run-all2": "^8.0.4",
30
32
  "rimraf": "^6.1.2",
31
33
  "typescript": "^5.9.3",
32
34
  "vitest": "^4.0.16",
33
- "@comet/eslint-config": "9.0.0-beta.0"
35
+ "@comet/eslint-config": "9.0.0-beta.1"
34
36
  },
35
37
  "engines": {
36
38
  "node": ">=22.0.0"
@@ -45,7 +47,7 @@
45
47
  "dev": "tsc --watch",
46
48
  "lint": "run-p lint:prettier lint:eslint lint:tsc",
47
49
  "lint:ci": "pnpm run lint",
48
- "lint:eslint": "eslint --max-warnings 0 src/ **/*.json --no-warn-ignored",
50
+ "lint:eslint": "eslint --max-warnings 0 src/ '**/*.json' --no-warn-ignored",
49
51
  "lint:prettier": "pnpm exec prettier --check '*.{ts,js,json,md,yml,yaml}'",
50
52
  "lint:tsc": "tsc",
51
53
  "test": "vitest --run",