@channlize/skills 0.0.0-stage → 0.5.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/dist/cli.js ADDED
@@ -0,0 +1,841 @@
1
+ #!/usr/bin/env node
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/cli.ts
22
+ var cli_exports = {};
23
+ __export(cli_exports, {
24
+ main: () => main,
25
+ parseArgs: () => parseArgs
26
+ });
27
+ module.exports = __toCommonJS(cli_exports);
28
+ var import_node_fs4 = require("fs");
29
+ var import_promises = require("readline/promises");
30
+ var import_node_path4 = require("path");
31
+
32
+ // src/catalog.ts
33
+ var import_node_fs = require("fs");
34
+ var import_node_path = require("path");
35
+
36
+ // src/frontmatter.ts
37
+ function unquote(raw) {
38
+ const v = raw.trim();
39
+ if (v.length >= 2 && v.startsWith('"') && v.endsWith('"')) {
40
+ return v.slice(1, -1).replace(/\\"/g, '"').replace(/\\\\/g, "\\");
41
+ }
42
+ if (v.length >= 2 && v.startsWith("'") && v.endsWith("'")) {
43
+ return v.slice(1, -1).replace(/''/g, "'");
44
+ }
45
+ return v;
46
+ }
47
+ function parseSkillFile(text) {
48
+ const src = text.replace(/^\uFEFF/, "").replace(/\r\n/g, "\n");
49
+ if (!src.startsWith("---\n")) {
50
+ return { data: null, body: src, errors: [] };
51
+ }
52
+ const end = src.indexOf("\n---", 4);
53
+ if (end === -1) {
54
+ return {
55
+ data: null,
56
+ body: src,
57
+ errors: ["frontmatter is not closed with ---"]
58
+ };
59
+ }
60
+ const block = src.slice(4, end);
61
+ const afterFence = src.indexOf("\n", end + 1);
62
+ const body = afterFence === -1 ? "" : src.slice(afterFence + 1);
63
+ const data = {};
64
+ const errors = [];
65
+ let currentMap = null;
66
+ block.split("\n").forEach((line, i) => {
67
+ const lineNo = i + 2;
68
+ if (!line.trim() || line.trimStart().startsWith("#")) return;
69
+ const nested = /^\s+([A-Za-z0-9_.-]+):\s*(.*)$/.exec(line);
70
+ if (nested && currentMap) {
71
+ currentMap[nested[1]] = unquote(nested[2]);
72
+ return;
73
+ }
74
+ const top = /^([A-Za-z0-9_-]+):\s*(.*)$/.exec(line);
75
+ if (!top) {
76
+ errors.push(`line ${lineNo}: expected "key: value"`);
77
+ return;
78
+ }
79
+ const [, key, rawValue] = top;
80
+ if (key in data) errors.push(`line ${lineNo}: duplicate key "${key}"`);
81
+ if (rawValue === "") {
82
+ currentMap = {};
83
+ data[key] = currentMap;
84
+ return;
85
+ }
86
+ currentMap = null;
87
+ if (/^[>|][+-]?$/.test(rawValue.trim())) {
88
+ errors.push(
89
+ `line ${lineNo}: "${key}" uses a block scalar; write it on one line`
90
+ );
91
+ return;
92
+ }
93
+ if (rawValue.trim().startsWith("[") || rawValue.trim().startsWith("{")) {
94
+ errors.push(`line ${lineNo}: "${key}" must be a plain string`);
95
+ return;
96
+ }
97
+ data[key] = unquote(rawValue);
98
+ });
99
+ return { data, body, errors };
100
+ }
101
+
102
+ // src/catalog.ts
103
+ function packageRoot() {
104
+ return (0, import_node_path.resolve)(__dirname, "..");
105
+ }
106
+ function bundledSkillsDir() {
107
+ return (0, import_node_path.join)(packageRoot(), "skills");
108
+ }
109
+ function packageVersion() {
110
+ const pkg = JSON.parse(
111
+ (0, import_node_fs.readFileSync)((0, import_node_path.join)(packageRoot(), "package.json"), "utf8")
112
+ );
113
+ return pkg.version;
114
+ }
115
+ function listSkillFiles(dir) {
116
+ const out2 = [];
117
+ const walk = (d) => {
118
+ for (const entry of (0, import_node_fs.readdirSync)(d).sort()) {
119
+ if (entry.startsWith(".")) continue;
120
+ const full = (0, import_node_path.join)(d, entry);
121
+ if ((0, import_node_fs.statSync)(full).isDirectory()) walk(full);
122
+ else out2.push((0, import_node_path.relative)(dir, full).split(import_node_path.sep).join("/"));
123
+ }
124
+ };
125
+ walk(dir);
126
+ return out2.sort(
127
+ (a, b) => a === "SKILL.md" ? -1 : b === "SKILL.md" ? 1 : a.localeCompare(b)
128
+ );
129
+ }
130
+ function skillFolders(root) {
131
+ return (0, import_node_fs.readdirSync)(root).filter((e) => !e.startsWith(".") && (0, import_node_fs.statSync)((0, import_node_path.join)(root, e)).isDirectory()).sort();
132
+ }
133
+ function loadSkills(root = bundledSkillsDir()) {
134
+ return skillFolders(root).flatMap((folder) => {
135
+ const dir = (0, import_node_path.join)(root, folder);
136
+ let text;
137
+ try {
138
+ text = (0, import_node_fs.readFileSync)((0, import_node_path.join)(dir, "SKILL.md"), "utf8");
139
+ } catch {
140
+ return [];
141
+ }
142
+ const { data } = parseSkillFile(text);
143
+ const name = typeof data?.name === "string" ? data.name : folder;
144
+ const description = typeof data?.description === "string" ? data.description : "";
145
+ const metadata = data && typeof data.metadata === "object" ? data.metadata : {};
146
+ const files = listSkillFiles(dir).map((path) => ({
147
+ path,
148
+ content: (0, import_node_fs.readFileSync)((0, import_node_path.join)(dir, path), "utf8")
149
+ }));
150
+ return [{ name, description, dir, metadata, files }];
151
+ });
152
+ }
153
+ function loadPrompts(file = (0, import_node_path.join)(packageRoot(), "prompts.json")) {
154
+ return JSON.parse((0, import_node_fs.readFileSync)(file, "utf8"));
155
+ }
156
+ var PROMPT_PLACEHOLDERS = [
157
+ "projectId",
158
+ "widgetKey",
159
+ "react",
160
+ "core",
161
+ "mcp"
162
+ ];
163
+ function placeholdersOf(template) {
164
+ return [...template.matchAll(/\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g)].map(
165
+ (m) => m[1]
166
+ );
167
+ }
168
+ function fillPrompt(template, library, values = {}) {
169
+ const map = {
170
+ projectId: values.projectId || "<your-project-id>",
171
+ widgetKey: values.widgetKey || "<your-widget-key (wk_\u2026)>",
172
+ ...library.versions
173
+ };
174
+ return template.replace(
175
+ /\{\{\s*([A-Za-z0-9_]+)\s*\}\}/g,
176
+ (whole, key) => map[key] ?? whole
177
+ );
178
+ }
179
+ function buildCatalog() {
180
+ return {
181
+ package: "@channlize/skills",
182
+ version: packageVersion(),
183
+ skills: loadSkills().map(({ dir: _dir, ...rest }) => rest),
184
+ prompts: loadPrompts()
185
+ };
186
+ }
187
+
188
+ // src/install.ts
189
+ var import_node_crypto = require("crypto");
190
+ var import_node_fs2 = require("fs");
191
+ var import_node_os = require("os");
192
+ var import_node_path2 = require("path");
193
+ var TARGETS = {
194
+ claude: {
195
+ label: "Claude Code",
196
+ project: ".claude/skills",
197
+ user: ".claude/skills"
198
+ },
199
+ cursor: {
200
+ label: "Cursor",
201
+ project: ".cursor/skills",
202
+ user: ".cursor/skills"
203
+ },
204
+ codex: {
205
+ label: "Codex (and other .agents readers)",
206
+ project: ".agents/skills",
207
+ user: ".agents/skills"
208
+ },
209
+ copilot: {
210
+ label: "VS Code / GitHub Copilot",
211
+ project: ".github/skills",
212
+ user: ".copilot/skills"
213
+ }
214
+ };
215
+ var TARGET_IDS = Object.keys(TARGETS);
216
+ var ALL_TARGETS = ["claude", "codex", "copilot"];
217
+ var MANIFEST = ".channlize-skill.json";
218
+ function targetDir(target, scope, cwd = process.cwd(), home = (0, import_node_os.homedir)()) {
219
+ const t = TARGETS[target];
220
+ return scope === "user" ? (0, import_node_path2.join)(home, t.user) : (0, import_node_path2.resolve)(cwd, t.project);
221
+ }
222
+ var sha256 = (s) => (0, import_node_crypto.createHash)("sha256").update(s).digest("hex");
223
+ function readManifest(skillDir) {
224
+ try {
225
+ return JSON.parse(
226
+ (0, import_node_fs2.readFileSync)((0, import_node_path2.join)(skillDir, MANIFEST), "utf8")
227
+ );
228
+ } catch {
229
+ return null;
230
+ }
231
+ }
232
+ async function installSkills(opts) {
233
+ const results = [];
234
+ for (const skill of opts.skills) {
235
+ const skillDir = (0, import_node_path2.join)(opts.dir, skill.name);
236
+ const previous = readManifest(skillDir);
237
+ const manifest = {
238
+ package: "@channlize/skills",
239
+ version: opts.version,
240
+ files: {}
241
+ };
242
+ for (const file of skill.files) {
243
+ const dest = (0, import_node_path2.join)(skillDir, file.path);
244
+ const next = sha256(file.content);
245
+ const result = {
246
+ skill: skill.name,
247
+ path: file.path,
248
+ dest,
249
+ action: "created"
250
+ };
251
+ if ((0, import_node_fs2.existsSync)(dest)) {
252
+ const current = sha256((0, import_node_fs2.readFileSync)(dest, "utf8"));
253
+ const recorded = previous?.files[file.path];
254
+ if (current === next) {
255
+ result.action = "unchanged";
256
+ } else if (recorded && current === recorded) {
257
+ result.action = "updated";
258
+ } else if (opts.force || opts.confirm && await opts.confirm({ ...result, action: "overwritten" })) {
259
+ result.action = "overwritten";
260
+ } else {
261
+ result.action = "kept";
262
+ }
263
+ }
264
+ if (!opts.dryRun && (result.action === "created" || result.action === "updated" || result.action === "overwritten")) {
265
+ (0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(dest), { recursive: true });
266
+ (0, import_node_fs2.writeFileSync)(dest, file.content);
267
+ }
268
+ manifest.files[file.path] = result.action === "kept" ? previous?.files[file.path] ?? "" : next;
269
+ results.push(result);
270
+ }
271
+ if (!opts.dryRun) {
272
+ (0, import_node_fs2.mkdirSync)(skillDir, { recursive: true });
273
+ (0, import_node_fs2.writeFileSync)(
274
+ (0, import_node_path2.join)(skillDir, MANIFEST),
275
+ JSON.stringify(manifest, null, 2) + "\n"
276
+ );
277
+ }
278
+ }
279
+ return results;
280
+ }
281
+
282
+ // src/validate.ts
283
+ var import_node_fs3 = require("fs");
284
+ var import_node_path3 = require("path");
285
+ var NAME_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
286
+ var NAME_MAX = 64;
287
+ var DESCRIPTION_MAX = 1024;
288
+ var COMPATIBILITY_MAX = 500;
289
+ var BODY_MAX_LINES = 500;
290
+ var SPEC_KEYS = [
291
+ "name",
292
+ "description",
293
+ "license",
294
+ "compatibility",
295
+ "metadata",
296
+ "allowed-tools"
297
+ ];
298
+ function displayPath(p) {
299
+ const rel = (0, import_node_path3.relative)(process.cwd(), p);
300
+ return rel && !rel.startsWith("..") ? rel : p;
301
+ }
302
+ function extractLinks(markdown) {
303
+ const prose = markdown.replace(/^[ \t]*(```|~~~)[^\n]*\n[\s\S]*?^[ \t]*\1[^\n]*$/gm, "").replace(/`[^`\n]*`/g, "");
304
+ const links = /* @__PURE__ */ new Set();
305
+ for (const m of prose.matchAll(
306
+ /\[[^\]]*\]\(\s*<?([^)\s>]+)>?(?:\s+"[^"]*")?\s*\)/g
307
+ ))
308
+ links.add(m[1]);
309
+ for (const m of prose.matchAll(/https?:\/\/[^\s<>)\]"'`]+/g))
310
+ links.add(m[0].replace(/[.,;:!?]+$/, ""));
311
+ return [...links];
312
+ }
313
+ function validateSkill(dir) {
314
+ const folder = dir.split(import_node_path3.sep).pop() ?? dir;
315
+ const issues = [];
316
+ const urls = /* @__PURE__ */ new Map();
317
+ const skillFile = (0, import_node_path3.join)(dir, "SKILL.md");
318
+ const at = (file) => displayPath((0, import_node_path3.join)(dir, file));
319
+ if (!(0, import_node_fs3.existsSync)(skillFile)) {
320
+ issues.push({ file: at("SKILL.md"), message: "missing SKILL.md" });
321
+ return { issues, urls };
322
+ }
323
+ const { data, body, errors } = parseSkillFile(
324
+ (0, import_node_fs3.readFileSync)(skillFile, "utf8")
325
+ );
326
+ for (const e of errors)
327
+ issues.push({ file: at("SKILL.md"), message: `frontmatter ${e}` });
328
+ if (!data) {
329
+ issues.push({
330
+ file: at("SKILL.md"),
331
+ message: "no YAML frontmatter (the file must start with ---)"
332
+ });
333
+ } else {
334
+ const { name, description, compatibility, metadata } = data;
335
+ if (typeof name !== "string" || !name) {
336
+ issues.push({ file: at("SKILL.md"), message: '"name" is required' });
337
+ } else {
338
+ if (name.length > NAME_MAX)
339
+ issues.push({
340
+ file: at("SKILL.md"),
341
+ message: `"name" is over ${NAME_MAX} characters`
342
+ });
343
+ if (!NAME_RE.test(name)) {
344
+ issues.push({
345
+ file: at("SKILL.md"),
346
+ message: `"name" must be lowercase letters, digits and single hyphens: "${name}"`
347
+ });
348
+ }
349
+ if (name !== folder)
350
+ issues.push({
351
+ file: at("SKILL.md"),
352
+ message: `"name" (${name}) must match its folder (${folder})`
353
+ });
354
+ }
355
+ if (typeof description !== "string" || !description.trim()) {
356
+ issues.push({
357
+ file: at("SKILL.md"),
358
+ message: '"description" is required'
359
+ });
360
+ } else if (description.length > DESCRIPTION_MAX) {
361
+ issues.push({
362
+ file: at("SKILL.md"),
363
+ message: `"description" is ${description.length} characters; the limit is ${DESCRIPTION_MAX}`
364
+ });
365
+ }
366
+ if (compatibility !== void 0 && (typeof compatibility !== "string" || compatibility.length > COMPATIBILITY_MAX)) {
367
+ issues.push({
368
+ file: at("SKILL.md"),
369
+ message: `"compatibility" must be a string of at most ${COMPATIBILITY_MAX} characters`
370
+ });
371
+ }
372
+ if (metadata !== void 0 && typeof metadata !== "object") {
373
+ issues.push({
374
+ file: at("SKILL.md"),
375
+ message: '"metadata" must be a map of strings'
376
+ });
377
+ }
378
+ for (const key of Object.keys(data)) {
379
+ if (!SPEC_KEYS.includes(key)) {
380
+ issues.push({
381
+ file: at("SKILL.md"),
382
+ message: `unknown frontmatter key "${key}" (allowed: ${SPEC_KEYS.join(", ")})`
383
+ });
384
+ }
385
+ }
386
+ }
387
+ if (!body.trim())
388
+ issues.push({ file: at("SKILL.md"), message: "the body is empty" });
389
+ const lines = body.split("\n").length;
390
+ if (lines > BODY_MAX_LINES)
391
+ issues.push({
392
+ file: at("SKILL.md"),
393
+ message: `the body has ${lines} lines; keep it under ${BODY_MAX_LINES}`
394
+ });
395
+ for (const file of listSkillFiles(dir).filter((f) => f.endsWith(".md"))) {
396
+ const text = (0, import_node_fs3.readFileSync)((0, import_node_path3.join)(dir, file), "utf8");
397
+ for (const link of extractLinks(text)) {
398
+ if (/^https?:\/\//.test(link)) {
399
+ if (!urls.has(link)) urls.set(link, at(file));
400
+ continue;
401
+ }
402
+ if (/^(mailto:|#)/.test(link)) continue;
403
+ const target = (0, import_node_path3.normalize)(
404
+ (0, import_node_path3.join)((0, import_node_path3.dirname)((0, import_node_path3.join)(dir, file)), decodeURI(link.split("#")[0]))
405
+ );
406
+ if (!target.startsWith((0, import_node_path3.resolve)(dir) + import_node_path3.sep)) {
407
+ issues.push({
408
+ file: at(file),
409
+ message: `link "${link}" points outside the skill folder`
410
+ });
411
+ } else if (!(0, import_node_fs3.existsSync)(target)) {
412
+ issues.push({ file: at(file), message: `broken link "${link}"` });
413
+ }
414
+ }
415
+ }
416
+ return { issues, urls };
417
+ }
418
+ function validatePrompts(library, skillNames, file = "prompts.json") {
419
+ const issues = [];
420
+ const ids = /* @__PURE__ */ new Set();
421
+ for (const [i, p] of library.prompts.entries()) {
422
+ const where = `${file} prompts[${i}] (${p.id ?? "?"})`;
423
+ const add = (message) => issues.push({ file: where, message });
424
+ if (!p.id || !NAME_RE.test(p.id)) add('"id" must be kebab-case');
425
+ if (ids.has(p.id)) add("duplicate id");
426
+ ids.add(p.id);
427
+ if (!p.title?.trim()) add('"title" is required');
428
+ if (!library.categories[p.category])
429
+ add(`unknown category "${p.category}"`);
430
+ if (!Array.isArray(p.frameworks)) add('"frameworks" must be an array');
431
+ if (!Array.isArray(p.skills) || p.skills.length === 0)
432
+ add('"skills" must list at least one skill');
433
+ for (const s of p.skills ?? [])
434
+ if (!skillNames.includes(s)) add(`unknown skill "${s}"`);
435
+ for (const ph of placeholdersOf(p.prompt ?? "")) {
436
+ if (!PROMPT_PLACEHOLDERS.includes(ph))
437
+ add(`unknown placeholder {{${ph}}}`);
438
+ }
439
+ const text = p.prompt ?? "";
440
+ for (const s of p.skills ?? [])
441
+ if (!text.includes(s))
442
+ add(`the prompt should tell the assistant to read the ${s} skill`);
443
+ if (!/\bverify\b/i.test(text))
444
+ add('the prompt should end with a verification step ("Verify: \u2026")');
445
+ if (/ch_live_[A-Za-z0-9]{6,}/.test(text))
446
+ add("the prompt contains what looks like a real server key");
447
+ }
448
+ for (const key of PROMPT_PLACEHOLDERS.slice(2)) {
449
+ if (!library.versions?.[key])
450
+ issues.push({ file, message: `versions.${key} is missing` });
451
+ }
452
+ return issues;
453
+ }
454
+ function validateMarketplace(root) {
455
+ const file = (0, import_node_path3.join)(root, ".claude-plugin", "marketplace.json");
456
+ const issues = [];
457
+ const add = (message) => issues.push({ file: displayPath(file), message });
458
+ let m;
459
+ try {
460
+ m = JSON.parse((0, import_node_fs3.readFileSync)(file, "utf8"));
461
+ } catch (e) {
462
+ add(`cannot read: ${e.message}`);
463
+ return issues;
464
+ }
465
+ const idPart = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
466
+ if (typeof m.name !== "string" || !idPart.test(m.name) || m.name.includes(".."))
467
+ add('"name" is missing or invalid');
468
+ if (typeof m.owner?.name !== "string" || !m.owner.name)
469
+ add('"owner.name" is required');
470
+ if (!Array.isArray(m.plugins) || m.plugins.length === 0)
471
+ add('"plugins" must list at least one plugin');
472
+ for (const [i, p] of (m.plugins ?? []).entries()) {
473
+ if (typeof p.name !== "string" || !idPart.test(p.name))
474
+ add(`plugins[${i}].name is invalid`);
475
+ if (typeof p.source === "string") {
476
+ if (!p.source.startsWith("./") && p.source !== ".")
477
+ add(`plugins[${i}].source must start with "./"`);
478
+ if (p.source.split("/").includes(".."))
479
+ add(`plugins[${i}].source must not contain ".."`);
480
+ const dir = (0, import_node_path3.resolve)(root, p.source);
481
+ const manifest = (0, import_node_path3.join)(dir, ".claude-plugin", "plugin.json");
482
+ if (!(0, import_node_fs3.existsSync)(dir) || !(0, import_node_fs3.statSync)(dir).isDirectory()) {
483
+ add(`plugins[${i}].source ${p.source} does not exist`);
484
+ } else if ((0, import_node_fs3.existsSync)(manifest)) {
485
+ const pj = JSON.parse((0, import_node_fs3.readFileSync)(manifest, "utf8"));
486
+ if (pj.name !== p.name)
487
+ add(
488
+ `plugins[${i}].name (${String(p.name)}) differs from plugin.json name (${pj.name})`
489
+ );
490
+ }
491
+ } else if (!p.source || typeof p.source !== "object" || typeof p.source.source !== "string") {
492
+ add(`plugins[${i}].source must be a "./" path or a source object`);
493
+ }
494
+ }
495
+ return issues;
496
+ }
497
+ async function checkUrls(urls, { retries = 2, timeoutMs = 15e3 } = {}) {
498
+ const issues = [];
499
+ const probe = async (url) => {
500
+ const npm = /^https:\/\/www\.npmjs\.com\/package\/(.+)$/.exec(url);
501
+ if (npm) url = `https://registry.npmjs.org/${npm[1].replace("/", "%2F")}`;
502
+ for (const method of ["HEAD", "GET"]) {
503
+ const res = await fetch(url, {
504
+ method,
505
+ redirect: "follow",
506
+ signal: AbortSignal.timeout(timeoutMs)
507
+ });
508
+ if (res.status < 400) return res.status;
509
+ if (method === "GET") return res.status;
510
+ }
511
+ return 0;
512
+ };
513
+ await Promise.all(
514
+ [...urls].map(async ([url, file]) => {
515
+ let last = "";
516
+ for (let attempt = 0; attempt <= retries; attempt++) {
517
+ try {
518
+ const status = await probe(url);
519
+ if (status < 400) return;
520
+ last = `HTTP ${status}`;
521
+ if (status < 500 && status !== 429) break;
522
+ } catch (e) {
523
+ last = e.message;
524
+ }
525
+ await new Promise((r) => setTimeout(r, 1e3 * (attempt + 1)));
526
+ }
527
+ issues.push({ file, message: `broken URL ${url} (${last})` });
528
+ })
529
+ );
530
+ return issues;
531
+ }
532
+ function validateSkillsDir(root) {
533
+ const issues = [];
534
+ const urls = /* @__PURE__ */ new Map();
535
+ const folders = skillFolders(root);
536
+ const names = /* @__PURE__ */ new Set();
537
+ for (const folder of folders) {
538
+ const r = validateSkill((0, import_node_path3.join)(root, folder));
539
+ issues.push(...r.issues);
540
+ for (const [u, f] of r.urls) if (!urls.has(u)) urls.set(u, f);
541
+ if (names.has(folder))
542
+ issues.push({ file: folder, message: "duplicate skill name" });
543
+ names.add(folder);
544
+ }
545
+ if (folders.length === 0)
546
+ issues.push({ file: root, message: "no skill folders found" });
547
+ return { issues, urls, count: folders.length };
548
+ }
549
+
550
+ // src/cli.ts
551
+ var HELP = `Channlize Agent Skills: teach AI coding agents to integrate Channlize.
552
+
553
+ Usage:
554
+ npx @channlize/skills install [targets] [--project | --user] [options]
555
+ npx @channlize/skills list
556
+ npx @channlize/skills prompts [id] [--project-id <id>] [--widget-key <wk_\u2026>] [--category <c>] [--json]
557
+ npx @channlize/skills validate [skills-dir] [--marketplace <dir>] [--check-urls]
558
+
559
+ Install targets (default: --claude):
560
+ --claude .claude/skills (Claude Code)
561
+ --cursor .cursor/skills (Cursor)
562
+ --codex .agents/skills (Codex; Cursor and VS Code read it too)
563
+ --copilot .github/skills (VS Code / GitHub Copilot)
564
+ --all --claude, --codex and --copilot
565
+ --dir <path> any other skills folder
566
+
567
+ Install options:
568
+ --project into the current directory (default)
569
+ --user into your home directory (~/.claude/skills, ~/.agents/skills, ~/.copilot/skills\u2026)
570
+ --skill <name> only this skill (repeatable)
571
+ --force overwrite files you changed, without asking
572
+ --dry-run show what would change
573
+ -y, --yes same as --force
574
+
575
+ Files you edited after installing are never replaced silently: the CLI asks
576
+ in a terminal and keeps them otherwise (or use --force).`;
577
+ var VALUE_FLAGS = /* @__PURE__ */ new Set([
578
+ "dir",
579
+ "skill",
580
+ "project-id",
581
+ "widget-key",
582
+ "category",
583
+ "marketplace",
584
+ "out"
585
+ ]);
586
+ function parseArgs(argv) {
587
+ const args = { _: [], flags: /* @__PURE__ */ new Set(), values: {} };
588
+ for (let i = 0; i < argv.length; i++) {
589
+ const a = argv[i];
590
+ if (a === "-y") args.flags.add("yes");
591
+ else if (a === "-h") args.flags.add("help");
592
+ else if (a.startsWith("--")) {
593
+ const [key, inline] = a.slice(2).split(/=(.*)/s, 2);
594
+ if (VALUE_FLAGS.has(key)) {
595
+ const value = inline ?? argv[++i];
596
+ if (value === void 0) throw new UsageError(`--${key} needs a value`);
597
+ (args.values[key] ??= []).push(value);
598
+ } else args.flags.add(key);
599
+ } else args._.push(a);
600
+ }
601
+ return args;
602
+ }
603
+ var UsageError = class extends Error {
604
+ };
605
+ var out = (s = "") => process.stdout.write(s + "\n");
606
+ var err = (s) => process.stderr.write(s + "\n");
607
+ function printIssues(issues) {
608
+ for (const i of issues) err(` \u2717 ${i.file}: ${i.message}`);
609
+ }
610
+ async function install(args) {
611
+ const known = /* @__PURE__ */ new Set([
612
+ ...TARGET_IDS,
613
+ "all",
614
+ "project",
615
+ "user",
616
+ "force",
617
+ "yes",
618
+ "dry-run"
619
+ ]);
620
+ for (const f of args.flags)
621
+ if (!known.has(f)) throw new UsageError(`unknown option --${f}`);
622
+ if (args.flags.has("project") && args.flags.has("user"))
623
+ throw new UsageError("use --project or --user, not both");
624
+ const scope = args.flags.has("user") ? "user" : "project";
625
+ let targets = args.flags.has("all") ? [...ALL_TARGETS] : TARGET_IDS.filter((t) => args.flags.has(t));
626
+ const dirs = [];
627
+ for (const d of args.values.dir ?? [])
628
+ dirs.push({ label: "custom", dir: (0, import_node_path4.resolve)(d) });
629
+ if (targets.length === 0 && dirs.length === 0) targets = ["claude"];
630
+ for (const t of targets)
631
+ dirs.push({ label: TARGETS[t].label, dir: targetDir(t, scope) });
632
+ const all = loadSkills();
633
+ const wanted = args.values.skill ?? [];
634
+ for (const w of wanted) {
635
+ if (!all.some((s) => s.name === w))
636
+ throw new UsageError(
637
+ `unknown skill "${w}" (see: npx @channlize/skills list)`
638
+ );
639
+ }
640
+ const skills = wanted.length ? all.filter((s) => wanted.includes(s.name)) : all;
641
+ const force = args.flags.has("force") || args.flags.has("yes");
642
+ const dryRun = args.flags.has("dry-run");
643
+ const interactive = process.stdin.isTTY && process.stdout.isTTY && !force && !dryRun;
644
+ const rl = interactive ? (0, import_promises.createInterface)({ input: process.stdin, output: process.stdout }) : null;
645
+ let overwriteAll = false;
646
+ const confirm = rl ? async (f) => {
647
+ if (overwriteAll) return true;
648
+ const answer = (await rl.question(
649
+ ` ${shown(f.dest)} was changed since it was installed. Overwrite? [y/N/a(ll)] `
650
+ )).trim().toLowerCase();
651
+ if (answer === "a" || answer === "all") overwriteAll = true;
652
+ return answer === "y" || answer === "yes" || overwriteAll;
653
+ } : void 0;
654
+ let kept = 0;
655
+ try {
656
+ for (const { label, dir } of dirs) {
657
+ out(`${dryRun ? "[dry run] " : ""}${label}: ${shown(dir)}`);
658
+ const results = await installSkills({
659
+ skills,
660
+ dir,
661
+ version: packageVersion(),
662
+ force,
663
+ dryRun,
664
+ confirm
665
+ });
666
+ for (const r of results) {
667
+ if (r.action === "unchanged") continue;
668
+ out(
669
+ ` ${symbol[r.action]} ${r.skill}/${r.path}${r.action === "kept" ? " (kept your changes)" : ""}`
670
+ );
671
+ }
672
+ const n = (a) => results.filter((r) => r.action === a).length;
673
+ kept += n("kept");
674
+ out(
675
+ ` ${skills.length} skills: ${n("created")} created, ${n("updated") + n("overwritten")} updated, ${n("unchanged")} unchanged, ${n("kept")} kept`
676
+ );
677
+ }
678
+ } finally {
679
+ rl?.close();
680
+ }
681
+ if (kept > 0)
682
+ out(
683
+ `
684
+ ${kept} file(s) you edited were kept. Re-run with --force to replace them.`
685
+ );
686
+ if (!dryRun) {
687
+ out(
688
+ `
689
+ Done. Start a new agent session so it picks up the skills, then ask it, for example:`
690
+ );
691
+ out(` "Add the Channlize chat widget to this app"`);
692
+ out(`Prompts to copy: npx @channlize/skills prompts`);
693
+ }
694
+ return 0;
695
+ }
696
+ var symbol = {
697
+ created: "+",
698
+ updated: "~",
699
+ overwritten: "!",
700
+ unchanged: "=",
701
+ kept: "\xB7"
702
+ };
703
+ function shown(p) {
704
+ const rel = (0, import_node_path4.relative)(process.cwd(), p);
705
+ return rel && !rel.startsWith("..") ? rel : p;
706
+ }
707
+ function list() {
708
+ for (const s of loadSkills()) {
709
+ out(s.name);
710
+ out(` ${s.description}`);
711
+ }
712
+ return 0;
713
+ }
714
+ function prompts(args) {
715
+ const lib = loadPrompts();
716
+ const category = args.values.category?.[0];
717
+ if (category && !lib.categories[category]) {
718
+ throw new UsageError(
719
+ `unknown category "${category}" (${Object.keys(lib.categories).join(", ")})`
720
+ );
721
+ }
722
+ const id = args._[1];
723
+ let entries = lib.prompts.filter((p) => !category || p.category === category);
724
+ if (id) {
725
+ entries = entries.filter((p) => p.id === id);
726
+ if (entries.length === 0) throw new UsageError(`unknown prompt "${id}"`);
727
+ }
728
+ const values = {
729
+ projectId: args.values["project-id"]?.[0],
730
+ widgetKey: args.values["widget-key"]?.[0]
731
+ };
732
+ if (values.widgetKey && !values.widgetKey.startsWith("wk_")) {
733
+ throw new UsageError(
734
+ "--widget-key takes the public widget key (wk_\u2026). Server keys never go in a prompt."
735
+ );
736
+ }
737
+ const filled = entries.map((p) => ({
738
+ ...p,
739
+ prompt: fillPrompt(p.prompt, lib, values)
740
+ }));
741
+ if (args.flags.has("json")) {
742
+ out(JSON.stringify(filled, null, 2));
743
+ return 0;
744
+ }
745
+ if (!id) {
746
+ for (const p of filled)
747
+ out(`${p.id.padEnd(26)} ${lib.categories[p.category]} \xB7 ${p.title}`);
748
+ out(
749
+ `
750
+ Print one: npx @channlize/skills prompts <id> --project-id <id> --widget-key <wk_\u2026>`
751
+ );
752
+ return 0;
753
+ }
754
+ out(filled[0].prompt);
755
+ return 0;
756
+ }
757
+ async function validate(args) {
758
+ const dir = (0, import_node_path4.resolve)(args._[1] ?? bundledSkillsDir());
759
+ const { issues, urls, count } = validateSkillsDir(dir);
760
+ const isBundled = dir === (0, import_node_path4.resolve)(bundledSkillsDir());
761
+ if (isBundled) {
762
+ const names = loadSkills(dir).map((s) => s.name);
763
+ issues.push(...validatePrompts(loadPrompts(), names, "prompts.json"));
764
+ }
765
+ for (const m of args.values.marketplace ?? [])
766
+ issues.push(...validateMarketplace((0, import_node_path4.resolve)(m)));
767
+ if (args.flags.has("check-urls")) issues.push(...await checkUrls(urls));
768
+ if (issues.length) {
769
+ err(`\u2717 ${issues.length} problem(s) in ${shown(dir)}:`);
770
+ printIssues(issues);
771
+ return 1;
772
+ }
773
+ out(
774
+ `\u2713 ${count} skills valid in ${shown(dir)}${args.flags.has("check-urls") ? `, ${urls.size} URLs reachable` : ""}`
775
+ );
776
+ return 0;
777
+ }
778
+ function catalog(args) {
779
+ const json = JSON.stringify(buildCatalog(), null, 2) + "\n";
780
+ const file = args.values.out?.[0];
781
+ if (file) (0, import_node_fs4.writeFileSync)((0, import_node_path4.resolve)(file), json);
782
+ else process.stdout.write(json);
783
+ return 0;
784
+ }
785
+ async function main(argv) {
786
+ try {
787
+ const args = parseArgs(argv);
788
+ if (args.flags.has("version")) {
789
+ out(packageVersion());
790
+ return 0;
791
+ }
792
+ const command = args._[0];
793
+ if (!command || args.flags.has("help") || command === "help") {
794
+ out(HELP);
795
+ return command || args.flags.has("help") ? 0 : 1;
796
+ }
797
+ switch (command) {
798
+ case "install":
799
+ return await install(args);
800
+ case "list":
801
+ return list();
802
+ case "prompts":
803
+ return prompts(args);
804
+ case "validate":
805
+ return await validate(args);
806
+ case "catalog":
807
+ return catalog(args);
808
+ case "path":
809
+ out((0, import_node_path4.join)(packageRoot(), "skills"));
810
+ return 0;
811
+ default:
812
+ throw new UsageError(`unknown command "${command}"`);
813
+ }
814
+ } catch (e) {
815
+ if (e instanceof UsageError) {
816
+ err(`error: ${e.message}
817
+ Run npx @channlize/skills --help for usage.`);
818
+ return 2;
819
+ }
820
+ throw e;
821
+ }
822
+ }
823
+ if (require.main === module) {
824
+ process.stdout.on("error", (e) => {
825
+ if (e.code === "EPIPE") process.exit(0);
826
+ throw e;
827
+ });
828
+ main(process.argv.slice(2)).then(
829
+ (code) => process.exit(code),
830
+ (e) => {
831
+ err(String(e?.stack ?? e));
832
+ process.exit(1);
833
+ }
834
+ );
835
+ }
836
+ // Annotate the CommonJS export names for ESM import in node:
837
+ 0 && (module.exports = {
838
+ main,
839
+ parseArgs
840
+ });
841
+ //# sourceMappingURL=cli.js.map