skillmds 1.0.6 → 1.0.8
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/CHANGELOG.md +10 -0
- package/bin/skillmd-mcp.mjs +8 -3
- package/dist/cli.js +226 -53
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `skillmds` are documented here. Versions follow [semver](https://semver.org).
|
|
4
4
|
|
|
5
|
+
## 1.0.8
|
|
6
|
+
|
|
7
|
+
- `skillmds` with no command in a terminal now opens the interactive CLI menu; it only starts the MCP server when launched over piped stdio (how MCP clients run it). Previously a bare `npx skillmds` in a terminal appeared to hang on the silent MCP server.
|
|
8
|
+
|
|
9
|
+
## 1.0.7
|
|
10
|
+
|
|
11
|
+
- Nicer CLI output: colored PASS/FAIL, quality-score bars, `ERROR`/`WARN`/`INFO` badges, and a summary with average score.
|
|
12
|
+
- Running `skillmd` (or `skillmds`) with no command on a terminal now opens a guided interactive menu (lint, scan, search, init, list). Piped/CI still prints help.
|
|
13
|
+
- Progress spinner while scanning.
|
|
14
|
+
|
|
5
15
|
## 1.0.6
|
|
6
16
|
|
|
7
17
|
- Bundle the banner image in the package and serve it via jsDelivr so it renders on the npm page (the previous absolute URL was unreachable).
|
package/bin/skillmd-mcp.mjs
CHANGED
|
@@ -211,16 +211,21 @@ async function startMcpServer() {
|
|
|
211
211
|
console.error("skillmd MCP server running (stdio)");
|
|
212
212
|
}
|
|
213
213
|
|
|
214
|
-
// This binary is invoked
|
|
215
|
-
// - `skillmds` → start the MCP server (stdio; how MCP clients launch it)
|
|
214
|
+
// This binary is invoked three ways:
|
|
216
215
|
// - `skillmds lint .` etc. → run the CLI (so `npx skillmds <cmd>` works without -p)
|
|
216
|
+
// - `skillmds` in a terminal → run the CLI (opens the interactive menu)
|
|
217
|
+
// - `skillmds` over piped stdio → start the MCP server (how MCP clients launch it)
|
|
218
|
+
// MCP clients always connect stdin as a pipe (never a TTY), so an interactive
|
|
219
|
+
// stdin is a reliable signal that a human is present.
|
|
217
220
|
const CLI_COMMANDS = new Set([
|
|
218
221
|
"lint", "check", "scan", "rules", "init", "search", "add", "list", "ls",
|
|
219
222
|
"remove", "rm", "update", "publish", "submit", "login", "logout", "help",
|
|
220
223
|
"--help", "-h", "--version", "-V",
|
|
221
224
|
]);
|
|
222
225
|
const first = process.argv[2];
|
|
223
|
-
|
|
226
|
+
const isCliCommand = first && CLI_COMMANDS.has(first);
|
|
227
|
+
const humanInTerminal = !first && process.stdin.isTTY;
|
|
228
|
+
if (isCliCommand || humanInTerminal) {
|
|
224
229
|
await import("../dist/cli.js"); // the CLI parses process.argv itself
|
|
225
230
|
} else {
|
|
226
231
|
await startMcpServer();
|
package/dist/cli.js
CHANGED
|
@@ -7,6 +7,8 @@ import { Command } from "commander";
|
|
|
7
7
|
import { writeFileSync } from "fs";
|
|
8
8
|
import { homedir } from "os";
|
|
9
9
|
import { resolve } from "path";
|
|
10
|
+
import pc2 from "picocolors";
|
|
11
|
+
import * as p from "@clack/prompts";
|
|
10
12
|
|
|
11
13
|
// ../../packages/core/src/parse.ts
|
|
12
14
|
import { parse as parseYaml } from "yaml";
|
|
@@ -46,9 +48,9 @@ var RULES = [
|
|
|
46
48
|
];
|
|
47
49
|
function scanSecurity(body) {
|
|
48
50
|
const findings = [];
|
|
49
|
-
body.split(/\r?\n/).forEach((
|
|
51
|
+
body.split(/\r?\n/).forEach((text3, i) => {
|
|
50
52
|
for (const [flag, re] of RULES) {
|
|
51
|
-
if (re.test(
|
|
53
|
+
if (re.test(text3)) findings.push({ flag, line: i + 1, snippet: text3.trim().slice(0, 120) });
|
|
52
54
|
}
|
|
53
55
|
});
|
|
54
56
|
const flags = [...new Set(findings.map((f) => f.flag))];
|
|
@@ -354,20 +356,54 @@ function normalizeGiget(arg) {
|
|
|
354
356
|
|
|
355
357
|
// ../cli/src/ui.ts
|
|
356
358
|
import pc from "picocolors";
|
|
357
|
-
var decorate = (
|
|
359
|
+
var decorate = (text3, kind) => {
|
|
358
360
|
switch (kind) {
|
|
359
361
|
case "error":
|
|
360
|
-
return pc.red(
|
|
362
|
+
return pc.red(text3);
|
|
361
363
|
case "warn":
|
|
362
|
-
return pc.yellow(
|
|
364
|
+
return pc.yellow(text3);
|
|
363
365
|
case "info":
|
|
364
|
-
return pc.cyan(
|
|
366
|
+
return pc.cyan(text3);
|
|
365
367
|
case "ok":
|
|
366
|
-
return pc.green(
|
|
368
|
+
return pc.green(text3);
|
|
367
369
|
case "dim":
|
|
368
|
-
return pc.dim(
|
|
370
|
+
return pc.dim(text3);
|
|
369
371
|
}
|
|
370
372
|
};
|
|
373
|
+
var badge = {
|
|
374
|
+
error: pc.bgRed(pc.white(" ERROR ")),
|
|
375
|
+
warn: pc.bgYellow(pc.black(" WARN ")),
|
|
376
|
+
info: pc.bgCyan(pc.black(" INFO "))
|
|
377
|
+
};
|
|
378
|
+
function scoreBar(score) {
|
|
379
|
+
const filled = Math.max(0, Math.min(10, Math.round(score / 10)));
|
|
380
|
+
const bar = "\u2588".repeat(filled) + "\u2591".repeat(10 - filled);
|
|
381
|
+
const color = score >= 80 ? pc.green : score >= 50 ? pc.yellow : pc.red;
|
|
382
|
+
return `${color(bar)} ${pc.bold(String(score))}${pc.dim("/100")}`;
|
|
383
|
+
}
|
|
384
|
+
function renderReport(results) {
|
|
385
|
+
const out = [];
|
|
386
|
+
for (const { file, result } of results) {
|
|
387
|
+
const status = result.ok ? pc.green("\u25CF") + pc.bold(" PASS") : pc.red("\u25CF") + pc.bold(" FAIL");
|
|
388
|
+
out.push("");
|
|
389
|
+
out.push(`${status} ${pc.bold(file)}`);
|
|
390
|
+
out.push(` ${scoreBar(result.score)}`);
|
|
391
|
+
for (const d of result.diagnostics) {
|
|
392
|
+
const loc = d.line !== void 0 ? pc.dim(`:${d.line}`) : "";
|
|
393
|
+
out.push(` ${badge[d.severity]} ${pc.dim(d.id)}${loc} ${d.message}`);
|
|
394
|
+
}
|
|
395
|
+
const flags = result.security.flags.filter((f) => f !== "docs_only");
|
|
396
|
+
if (flags.length) out.push(` ${pc.magenta("\u2691 security")} ${pc.dim(flags.join(", "))}`);
|
|
397
|
+
}
|
|
398
|
+
const failed = results.filter((r) => !r.result.ok).length;
|
|
399
|
+
const passed = results.length - failed;
|
|
400
|
+
const avg = results.length ? Math.round(results.reduce((s, r) => s + r.result.score, 0) / results.length) : 0;
|
|
401
|
+
out.push("");
|
|
402
|
+
out.push(pc.dim("\u2500".repeat(44)));
|
|
403
|
+
const tally = failed === 0 ? pc.green(`\u2713 ${passed} passed`) : `${pc.green(`${passed} passed`)} ${pc.red(`\u2717 ${failed} failed`)}`;
|
|
404
|
+
out.push(`${tally} ${pc.dim("avg")} ${scoreBar(avg)}`);
|
|
405
|
+
return out.join("\n");
|
|
406
|
+
}
|
|
371
407
|
function exitCodeFor(results, policy = {}) {
|
|
372
408
|
for (const { result } of results) {
|
|
373
409
|
const hasError = result.diagnostics.some((d) => d.severity === "error");
|
|
@@ -413,14 +449,33 @@ async function runLint(target, flags) {
|
|
|
413
449
|
output = toGithub(results);
|
|
414
450
|
break;
|
|
415
451
|
default:
|
|
416
|
-
output =
|
|
452
|
+
output = renderReport(results);
|
|
417
453
|
}
|
|
418
454
|
return { results, output, exitCode };
|
|
419
455
|
}
|
|
420
456
|
function lintCommand() {
|
|
421
457
|
return new Command("lint").alias("check").description("Validate SKILL.md file(s) against the spec and report a quality score").argument("[target]", "local path or GitHub source", ".").option("--format <fmt>", "text | json | sarif | github", "text").option("--strict", "treat warnings as errors").option("--fail-on-warning", "exit non-zero when warnings exist").option("--fix", "apply safe automatic fixes (e.g. add missing license)").action(async (target, opts) => {
|
|
458
|
+
const ui = Boolean(process.stdout.isTTY) && opts.format === "text";
|
|
422
459
|
if (resolve(target) === homedir()) {
|
|
423
|
-
process.stderr.write(
|
|
460
|
+
process.stderr.write(pc2.yellow(`Warning: scanning your entire home directory. Pass a narrower path, e.g. skillmd lint ./my-skill
|
|
461
|
+
`));
|
|
462
|
+
}
|
|
463
|
+
if (ui) {
|
|
464
|
+
p.intro(pc2.bgCyan(pc2.black(" skillmd lint ")));
|
|
465
|
+
const s = p.spinner();
|
|
466
|
+
s.start(`Scanning ${target}`);
|
|
467
|
+
try {
|
|
468
|
+
const run2 = await runLint(target, opts);
|
|
469
|
+
s.stop(`Scanned ${run2.results.length} skill${run2.results.length === 1 ? "" : "s"}`);
|
|
470
|
+
console.log(run2.output);
|
|
471
|
+
p.outro(run2.exitCode === 0 ? pc2.green("All good") : pc2.red("Problems found"));
|
|
472
|
+
process.exitCode = run2.exitCode;
|
|
473
|
+
} catch (err) {
|
|
474
|
+
s.stop(pc2.red("No skills found"));
|
|
475
|
+
p.outro(err instanceof Error ? err.message : String(err));
|
|
476
|
+
process.exitCode = 1;
|
|
477
|
+
}
|
|
478
|
+
return;
|
|
424
479
|
}
|
|
425
480
|
process.stderr.write(`Looking for SKILL.md under ${target} \u2026
|
|
426
481
|
`);
|
|
@@ -432,7 +487,7 @@ function lintCommand() {
|
|
|
432
487
|
|
|
433
488
|
// ../cli/src/commands/scan.ts
|
|
434
489
|
import { Command as Command2 } from "commander";
|
|
435
|
-
import
|
|
490
|
+
import pc3 from "picocolors";
|
|
436
491
|
async function runScan(target, flags) {
|
|
437
492
|
const resolved = await resolveSource(target);
|
|
438
493
|
const deny = new Set(flags.deny ?? []);
|
|
@@ -451,11 +506,11 @@ async function runScan(target, flags) {
|
|
|
451
506
|
const lines = [];
|
|
452
507
|
for (const e of entries) {
|
|
453
508
|
const flagStr = e.security.flags.join(", ");
|
|
454
|
-
const head = e.denied.length ?
|
|
455
|
-
lines.push(`${head} ${
|
|
509
|
+
const head = e.denied.length ? pc3.red(`DENY ${e.file}`) : pc3.green(`OK ${e.file}`);
|
|
510
|
+
lines.push(`${head} ${pc3.dim(flagStr)}`);
|
|
456
511
|
for (const f of e.security.findings) {
|
|
457
|
-
const mark = deny.has(f.flag) ?
|
|
458
|
-
lines.push(` ${mark} :${f.line} ${
|
|
512
|
+
const mark = deny.has(f.flag) ? pc3.red(f.flag) : pc3.yellow(f.flag);
|
|
513
|
+
lines.push(` ${mark} :${f.line} ${pc3.dim(f.snippet)}`);
|
|
459
514
|
}
|
|
460
515
|
}
|
|
461
516
|
output = lines.join("\n");
|
|
@@ -472,7 +527,7 @@ function scanCommand() {
|
|
|
472
527
|
|
|
473
528
|
// ../cli/src/commands/rules.ts
|
|
474
529
|
import { Command as Command3 } from "commander";
|
|
475
|
-
import
|
|
530
|
+
import pc4 from "picocolors";
|
|
476
531
|
function rulesCommand() {
|
|
477
532
|
return new Command3("rules").description("List lint rules, or show detail for one rule id").argument("[id]", "a rule id, e.g. SK010").option("--json", "machine-readable output").action((id, opts) => {
|
|
478
533
|
const list = id ? RULES2.filter((r) => r.id.toLowerCase() === id.toLowerCase()) : RULES2;
|
|
@@ -485,8 +540,10 @@ function rulesCommand() {
|
|
|
485
540
|
console.log(JSON.stringify(list.map((r) => ({ id: r.id, severity: r.severity, description: r.description })), null, 2));
|
|
486
541
|
return;
|
|
487
542
|
}
|
|
488
|
-
const
|
|
489
|
-
|
|
543
|
+
const badge2 = (sev) => sev === "error" ? pc4.bgRed(pc4.white(" error ")) : sev === "warn" ? pc4.bgYellow(pc4.black(" warn ")) : pc4.bgCyan(pc4.black(" info "));
|
|
544
|
+
console.log(pc4.bold("\nLint rules\n"));
|
|
545
|
+
for (const r of list) console.log(` ${pc4.bold(r.id)} ${badge2(r.severity)} ${r.description}`);
|
|
546
|
+
console.log("");
|
|
490
547
|
});
|
|
491
548
|
}
|
|
492
549
|
|
|
@@ -494,7 +551,7 @@ function rulesCommand() {
|
|
|
494
551
|
import { Command as Command4 } from "commander";
|
|
495
552
|
import { mkdirSync, writeFileSync as writeFileSync2, existsSync as existsSync2 } from "fs";
|
|
496
553
|
import { join as join2 } from "path";
|
|
497
|
-
import * as
|
|
554
|
+
import * as p2 from "@clack/prompts";
|
|
498
555
|
function skillTemplate(name, description, license) {
|
|
499
556
|
const desc = description.trim() || `A skill that helps agents with ${name}.`;
|
|
500
557
|
return `---
|
|
@@ -534,9 +591,9 @@ function initCommand() {
|
|
|
534
591
|
return new Command4("init").description("Scaffold a new SKILL.md from a template").argument("[name]", "skill name").option("--name <name>", "skill name (non-interactive)").option("--description <text>", "skill description").option("--license <license>", "license identifier", "MIT").option("-y, --yes", "skip prompts").action(async (nameArg, opts) => {
|
|
535
592
|
let name = nameArg ?? opts.name;
|
|
536
593
|
if (!name && !opts.yes) {
|
|
537
|
-
const answer = await
|
|
538
|
-
if (
|
|
539
|
-
|
|
594
|
+
const answer = await p2.text({ message: "Skill name?", placeholder: "my-skill" });
|
|
595
|
+
if (p2.isCancel(answer)) {
|
|
596
|
+
p2.cancel("Cancelled.");
|
|
540
597
|
return;
|
|
541
598
|
}
|
|
542
599
|
name = answer;
|
|
@@ -553,7 +610,7 @@ function initCommand() {
|
|
|
553
610
|
|
|
554
611
|
// ../cli/src/commands/search.ts
|
|
555
612
|
import { Command as Command5 } from "commander";
|
|
556
|
-
import
|
|
613
|
+
import pc5 from "picocolors";
|
|
557
614
|
|
|
558
615
|
// ../cli/src/config.ts
|
|
559
616
|
import { readFileSync as readFileSync2, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2 } from "fs";
|
|
@@ -618,10 +675,10 @@ async function runSearch(query, flags) {
|
|
|
618
675
|
const items = r.items ?? [];
|
|
619
676
|
if (flags.json) return { items, output: JSON.stringify(items, null, 2) };
|
|
620
677
|
const lines = items.map((s) => {
|
|
621
|
-
const check = s.verified ?
|
|
622
|
-
const rating = s.avg_rating ?
|
|
623
|
-
return `${check} ${
|
|
624
|
-
${
|
|
678
|
+
const check = s.verified ? pc5.green("\u2713") : " ";
|
|
679
|
+
const rating = s.avg_rating ? pc5.dim(`\u2605${s.avg_rating.toFixed(1)} (${s.rating_count ?? 0})`) : "";
|
|
680
|
+
return `${check} ${pc5.bold(s.slug)} ${rating}
|
|
681
|
+
${pc5.dim(s.description)}`;
|
|
625
682
|
});
|
|
626
683
|
return { items, output: lines.join("\n") || "No skills found." };
|
|
627
684
|
}
|
|
@@ -636,7 +693,7 @@ function searchCommand() {
|
|
|
636
693
|
// ../cli/src/commands/add.ts
|
|
637
694
|
import { Command as Command6 } from "commander";
|
|
638
695
|
import { existsSync as existsSync4 } from "fs";
|
|
639
|
-
import
|
|
696
|
+
import pc6 from "picocolors";
|
|
640
697
|
|
|
641
698
|
// ../cli/src/agents.ts
|
|
642
699
|
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync4, rmSync, symlinkSync, existsSync as existsSync3, readdirSync as readdirSync2, statSync as statSync2, readFileSync as readFileSync3 } from "fs";
|
|
@@ -737,24 +794,24 @@ async function runAdd(arg, flags, deps = defaultDeps) {
|
|
|
737
794
|
const deniedFlags = result.security.flags.filter((f) => deny.has(f));
|
|
738
795
|
if (!flags.skipLint && !result.ok) {
|
|
739
796
|
blocked.push({ name: c.name, reason: `lint errors: ${result.diagnostics.filter((d) => d.severity === "error").map((d) => d.id).join(", ")}` });
|
|
740
|
-
lines.push(
|
|
797
|
+
lines.push(pc6.red(`\u2717 ${c.name} blocked \u2014 failed lint (use --skip-lint to override)`));
|
|
741
798
|
continue;
|
|
742
799
|
}
|
|
743
800
|
if (deniedFlags.length) {
|
|
744
801
|
blocked.push({ name: c.name, reason: `denied security flags: ${deniedFlags.join(", ")}` });
|
|
745
|
-
lines.push(
|
|
802
|
+
lines.push(pc6.red(`\u2717 ${c.name} blocked \u2014 ${deniedFlags.join(", ")}`));
|
|
746
803
|
continue;
|
|
747
804
|
}
|
|
748
805
|
if (c.registrySlug && c.verified === false && !flags.allowUnverified) {
|
|
749
806
|
blocked.push({ name: c.name, reason: "unverified registry skill (use --allow-unverified)" });
|
|
750
|
-
lines.push(
|
|
807
|
+
lines.push(pc6.yellow(`\u2717 ${c.name} blocked \u2014 unverified (use --allow-unverified)`));
|
|
751
808
|
continue;
|
|
752
809
|
}
|
|
753
810
|
for (const agentId of targets) {
|
|
754
811
|
const dir = agentDir(agentId, scope);
|
|
755
812
|
const out = writeSkill(c.name, [{ path: "SKILL.md", contents: c.raw }], dir, { mode: "copy" });
|
|
756
813
|
written.push({ name: c.name, dir: out });
|
|
757
|
-
lines.push(
|
|
814
|
+
lines.push(pc6.green(`\u2713 ${c.name} \u2192 ${out}`));
|
|
758
815
|
}
|
|
759
816
|
if (c.registrySlug) deps.fireInstall(c.registrySlug, flags);
|
|
760
817
|
}
|
|
@@ -772,7 +829,7 @@ function addCommand() {
|
|
|
772
829
|
|
|
773
830
|
// ../cli/src/commands/list.ts
|
|
774
831
|
import { Command as Command7 } from "commander";
|
|
775
|
-
import
|
|
832
|
+
import pc7 from "picocolors";
|
|
776
833
|
function listCommand() {
|
|
777
834
|
return new Command7("list").alias("ls").description("List installed skills across detected agents").option("-g, --global", "only global skills").option("--json", "machine-readable output").action((opts) => {
|
|
778
835
|
const scope = { global: opts.global };
|
|
@@ -787,8 +844,8 @@ function listCommand() {
|
|
|
787
844
|
}
|
|
788
845
|
for (const s of skills) {
|
|
789
846
|
const parsed = parseSkillMd(s.raw);
|
|
790
|
-
const desc = "error" in parsed ?
|
|
791
|
-
console.log(`${
|
|
847
|
+
const desc = "error" in parsed ? pc7.red("(invalid SKILL.md)") : pc7.dim(parsed.description.slice(0, 80));
|
|
848
|
+
console.log(`${pc7.bold(s.name)} ${pc7.dim(`[${s.agent}/${s.scope}]`)}
|
|
792
849
|
${desc}`);
|
|
793
850
|
}
|
|
794
851
|
});
|
|
@@ -797,7 +854,7 @@ function listCommand() {
|
|
|
797
854
|
// ../cli/src/commands/remove.ts
|
|
798
855
|
import { Command as Command8 } from "commander";
|
|
799
856
|
import { rmSync as rmSync2 } from "fs";
|
|
800
|
-
import
|
|
857
|
+
import pc8 from "picocolors";
|
|
801
858
|
function runRemove(names, scope = {}) {
|
|
802
859
|
const installed = installedSkills(scope);
|
|
803
860
|
const targets = installed.filter((s) => names.includes(s.name));
|
|
@@ -806,10 +863,10 @@ function runRemove(names, scope = {}) {
|
|
|
806
863
|
for (const t of targets) {
|
|
807
864
|
rmSync2(t.path, { recursive: true, force: true });
|
|
808
865
|
removed.push(t.path);
|
|
809
|
-
lines.push(
|
|
866
|
+
lines.push(pc8.green(`\u2713 removed ${t.name} (${t.agent})`));
|
|
810
867
|
}
|
|
811
868
|
for (const n of names) {
|
|
812
|
-
if (!targets.some((t) => t.name === n)) lines.push(
|
|
869
|
+
if (!targets.some((t) => t.name === n)) lines.push(pc8.yellow(`- ${n} not installed`));
|
|
813
870
|
}
|
|
814
871
|
return { removed, output: lines.join("\n") };
|
|
815
872
|
}
|
|
@@ -824,7 +881,7 @@ function removeCommand() {
|
|
|
824
881
|
import { Command as Command9 } from "commander";
|
|
825
882
|
import { writeFileSync as writeFileSync5 } from "fs";
|
|
826
883
|
import { join as join5 } from "path";
|
|
827
|
-
import
|
|
884
|
+
import pc9 from "picocolors";
|
|
828
885
|
async function runUpdate(names, flags, scope = {}) {
|
|
829
886
|
const { api } = createClient(flags);
|
|
830
887
|
const installed = installedSkills(scope).filter((s) => names.length === 0 || names.includes(s.name));
|
|
@@ -833,7 +890,7 @@ async function runUpdate(names, flags, scope = {}) {
|
|
|
833
890
|
for (const s of installed) {
|
|
834
891
|
const slug = await findRegistrySlug(api, s);
|
|
835
892
|
if (!slug) {
|
|
836
|
-
lines.push(
|
|
893
|
+
lines.push(pc9.dim(`- ${s.name} skipped (no registry match)`));
|
|
837
894
|
continue;
|
|
838
895
|
}
|
|
839
896
|
try {
|
|
@@ -841,19 +898,19 @@ async function runUpdate(names, flags, scope = {}) {
|
|
|
841
898
|
const skill = await api(`/api/skills/${owner}/${name}`);
|
|
842
899
|
const next = reconstructSkillMd(skill);
|
|
843
900
|
if (next.trim() === s.raw.trim()) {
|
|
844
|
-
lines.push(
|
|
901
|
+
lines.push(pc9.dim(`- ${s.name} already up to date`));
|
|
845
902
|
continue;
|
|
846
903
|
}
|
|
847
904
|
const result = lint(next, { slug: s.name });
|
|
848
905
|
if (!result.ok) {
|
|
849
|
-
lines.push(
|
|
906
|
+
lines.push(pc9.red(`\u2717 ${s.name} update failed lint \u2014 skipped`));
|
|
850
907
|
continue;
|
|
851
908
|
}
|
|
852
909
|
writeFileSync5(join5(s.path, "SKILL.md"), next, "utf8");
|
|
853
910
|
updated.push(s.name);
|
|
854
|
-
lines.push(
|
|
911
|
+
lines.push(pc9.green(`\u2713 updated ${s.name}`));
|
|
855
912
|
} catch {
|
|
856
|
-
lines.push(
|
|
913
|
+
lines.push(pc9.red(`\u2717 ${s.name} update error`));
|
|
857
914
|
}
|
|
858
915
|
}
|
|
859
916
|
return { updated, output: lines.join("\n") || "Nothing to update." };
|
|
@@ -873,7 +930,7 @@ function updateCommand() {
|
|
|
873
930
|
|
|
874
931
|
// ../cli/src/commands/publish.ts
|
|
875
932
|
import { Command as Command10 } from "commander";
|
|
876
|
-
import
|
|
933
|
+
import pc10 from "picocolors";
|
|
877
934
|
var defaultDeps2 = {
|
|
878
935
|
resolve: resolveSource,
|
|
879
936
|
async submit(payload, flags) {
|
|
@@ -896,20 +953,20 @@ async function runPublish(target, flags, deps = defaultDeps2) {
|
|
|
896
953
|
const report = toText([{ file: src.path, result }], { decorate });
|
|
897
954
|
if (!result.ok) {
|
|
898
955
|
return { result, published: false, exitCode: 1, output: `${report}
|
|
899
|
-
${
|
|
956
|
+
${pc10.red("\u2717 Refusing to publish: fix the errors above.")}` };
|
|
900
957
|
}
|
|
901
958
|
const hasWarn = result.diagnostics.some((d) => d.severity === "warn");
|
|
902
959
|
if (hasWarn && !flags.force) {
|
|
903
960
|
return { result, published: false, exitCode: 1, output: `${report}
|
|
904
|
-
${
|
|
961
|
+
${pc10.yellow("\u2717 Warnings present. Re-run with --force to publish anyway.")}` };
|
|
905
962
|
}
|
|
906
963
|
if (flags.dryRun) {
|
|
907
964
|
return { result, published: false, exitCode: 0, output: `${report}
|
|
908
|
-
${
|
|
965
|
+
${pc10.green("\u2713 Dry run: would publish.")}` };
|
|
909
966
|
}
|
|
910
967
|
const res = await deps.submit({ content: src.raw, type: flags.type ?? "single" }, flags);
|
|
911
968
|
return { result, published: true, slug: res.slug, exitCode: 0, output: `${report}
|
|
912
|
-
${
|
|
969
|
+
${pc10.green(`\u2713 Published ${res.slug ?? src.slug} (status: ${res.status ?? "pending"})`)}` };
|
|
913
970
|
}
|
|
914
971
|
function publishCommand() {
|
|
915
972
|
return new Command10("publish").alias("submit").description("Publish a skill to the registry (refuses to publish on lint errors)").argument("[target]", "local path to the skill", ".").option("--type <type>", "single | pack", "single").option("--force", "publish despite warnings (never bypasses errors)").option("--dry-run", "lint only; do not publish").action(async (target, opts, cmd) => {
|
|
@@ -922,14 +979,14 @@ function publishCommand() {
|
|
|
922
979
|
|
|
923
980
|
// ../cli/src/commands/login.ts
|
|
924
981
|
import { Command as Command11 } from "commander";
|
|
925
|
-
import * as
|
|
982
|
+
import * as p3 from "@clack/prompts";
|
|
926
983
|
function loginCommand() {
|
|
927
984
|
return new Command11("login").description("Store a SkillMD personal access token for publishing").option("--token <token>", "token to store (otherwise prompt)").option("--api <url>", "registry API base to store").action(async (opts) => {
|
|
928
985
|
let token = opts.token;
|
|
929
986
|
if (!token) {
|
|
930
|
-
const answer = await
|
|
931
|
-
if (
|
|
932
|
-
|
|
987
|
+
const answer = await p3.password({ message: "Paste your SkillMD token (from your account page)" });
|
|
988
|
+
if (p3.isCancel(answer)) {
|
|
989
|
+
p3.cancel("Cancelled.");
|
|
933
990
|
return;
|
|
934
991
|
}
|
|
935
992
|
token = answer;
|
|
@@ -955,6 +1012,118 @@ function logoutCommand() {
|
|
|
955
1012
|
});
|
|
956
1013
|
}
|
|
957
1014
|
|
|
1015
|
+
// ../cli/src/commands/interactive.ts
|
|
1016
|
+
import * as p4 from "@clack/prompts";
|
|
1017
|
+
import pc11 from "picocolors";
|
|
1018
|
+
function bail(v) {
|
|
1019
|
+
if (p4.isCancel(v)) {
|
|
1020
|
+
p4.cancel("Cancelled.");
|
|
1021
|
+
return true;
|
|
1022
|
+
}
|
|
1023
|
+
return false;
|
|
1024
|
+
}
|
|
1025
|
+
async function runInteractive() {
|
|
1026
|
+
p4.intro(pc11.bgCyan(pc11.black(" skillmd ")));
|
|
1027
|
+
const action = await p4.select({
|
|
1028
|
+
message: "What would you like to do?",
|
|
1029
|
+
options: [
|
|
1030
|
+
{ value: "lint", label: "Lint a skill", hint: "validate a SKILL.md" },
|
|
1031
|
+
{ value: "scan", label: "Security scan", hint: "scripts, network, secrets" },
|
|
1032
|
+
{ value: "search", label: "Search the registry" },
|
|
1033
|
+
{ value: "init", label: "Create a new skill" },
|
|
1034
|
+
{ value: "list", label: "List installed skills" },
|
|
1035
|
+
{ value: "help", label: "Show all commands" }
|
|
1036
|
+
]
|
|
1037
|
+
});
|
|
1038
|
+
if (bail(action)) return;
|
|
1039
|
+
switch (action) {
|
|
1040
|
+
case "lint":
|
|
1041
|
+
case "scan": {
|
|
1042
|
+
const path = await p4.text({ message: "Path to a skill or folder", placeholder: ".", defaultValue: "." });
|
|
1043
|
+
if (bail(path)) return;
|
|
1044
|
+
const target = String(path || ".");
|
|
1045
|
+
const s = p4.spinner();
|
|
1046
|
+
s.start(action === "lint" ? "Linting" : "Scanning");
|
|
1047
|
+
try {
|
|
1048
|
+
const run = action === "lint" ? await runLint(target, { format: "text" }) : await runScan(target, { format: "text" });
|
|
1049
|
+
s.stop("Done");
|
|
1050
|
+
console.log(run.output);
|
|
1051
|
+
process.exitCode = run.exitCode;
|
|
1052
|
+
} catch (err) {
|
|
1053
|
+
s.stop(pc11.red(err instanceof Error ? err.message : String(err)));
|
|
1054
|
+
process.exitCode = 1;
|
|
1055
|
+
}
|
|
1056
|
+
break;
|
|
1057
|
+
}
|
|
1058
|
+
case "search": {
|
|
1059
|
+
const query = await p4.text({ message: "Search the registry for" });
|
|
1060
|
+
if (bail(query)) return;
|
|
1061
|
+
const verified = await p4.confirm({ message: "Verified skills only?", initialValue: false });
|
|
1062
|
+
if (bail(verified)) return;
|
|
1063
|
+
const s = p4.spinner();
|
|
1064
|
+
s.start("Searching");
|
|
1065
|
+
try {
|
|
1066
|
+
const run = await runSearch(String(query), { verified: Boolean(verified), limit: "20" });
|
|
1067
|
+
s.stop(`${run.items.length} result${run.items.length === 1 ? "" : "s"}`);
|
|
1068
|
+
console.log(run.output);
|
|
1069
|
+
} catch (err) {
|
|
1070
|
+
s.stop(pc11.red(err instanceof Error ? err.message : String(err)));
|
|
1071
|
+
process.exitCode = 1;
|
|
1072
|
+
}
|
|
1073
|
+
break;
|
|
1074
|
+
}
|
|
1075
|
+
case "init": {
|
|
1076
|
+
const name = await p4.text({ message: "Skill name", placeholder: "my-skill" });
|
|
1077
|
+
if (bail(name)) return;
|
|
1078
|
+
const description = await p4.text({ message: "One-line description", placeholder: "What this skill helps an agent do" });
|
|
1079
|
+
if (bail(description)) return;
|
|
1080
|
+
try {
|
|
1081
|
+
const file = initSkill(String(name), { description: String(description || "") });
|
|
1082
|
+
p4.log.success(`Created ${pc11.bold(file)}`);
|
|
1083
|
+
} catch (err) {
|
|
1084
|
+
p4.log.error(err instanceof Error ? err.message : String(err));
|
|
1085
|
+
process.exitCode = 1;
|
|
1086
|
+
}
|
|
1087
|
+
break;
|
|
1088
|
+
}
|
|
1089
|
+
case "list": {
|
|
1090
|
+
const global = await p4.confirm({ message: "Include global skills?", initialValue: true });
|
|
1091
|
+
if (bail(global)) return;
|
|
1092
|
+
const skills = installedSkills({ global: Boolean(global) });
|
|
1093
|
+
if (skills.length === 0) {
|
|
1094
|
+
p4.log.info("No installed skills found.");
|
|
1095
|
+
} else {
|
|
1096
|
+
p4.log.message(
|
|
1097
|
+
skills.map((sk) => {
|
|
1098
|
+
const parsed = parseSkillMd(sk.raw);
|
|
1099
|
+
const desc = "error" in parsed ? pc11.red("(invalid)") : pc11.dim(parsed.description.slice(0, 70));
|
|
1100
|
+
return `${pc11.bold(sk.name)} ${pc11.dim(`[${sk.agent}]`)}
|
|
1101
|
+
${desc}`;
|
|
1102
|
+
}).join("\n")
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
break;
|
|
1106
|
+
}
|
|
1107
|
+
case "help": {
|
|
1108
|
+
p4.note(
|
|
1109
|
+
[
|
|
1110
|
+
`${pc11.bold("lint")} [path] validate SKILL.md + quality score`,
|
|
1111
|
+
`${pc11.bold("scan")} [path] security scan`,
|
|
1112
|
+
`${pc11.bold("search")} <query> search the registry`,
|
|
1113
|
+
`${pc11.bold("add")} <source> install a skill (lints first)`,
|
|
1114
|
+
`${pc11.bold("init")} [name] scaffold a new skill`,
|
|
1115
|
+
`${pc11.bold("list / remove / update")} manage installed skills`,
|
|
1116
|
+
`${pc11.bold("publish")} [path] publish (fails on lint errors)`,
|
|
1117
|
+
`${pc11.bold("rules / login")}`
|
|
1118
|
+
].join("\n"),
|
|
1119
|
+
"Commands (run: skillmd <command>)"
|
|
1120
|
+
);
|
|
1121
|
+
break;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1124
|
+
p4.outro(pc11.dim("skillmd.com"));
|
|
1125
|
+
}
|
|
1126
|
+
|
|
958
1127
|
// ../cli/src/index.ts
|
|
959
1128
|
var version = "0.0.0";
|
|
960
1129
|
try {
|
|
@@ -975,6 +1144,10 @@ program.addCommand(publishCommand());
|
|
|
975
1144
|
program.addCommand(loginCommand());
|
|
976
1145
|
program.addCommand(logoutCommand());
|
|
977
1146
|
if (process.argv.slice(2).length === 0) {
|
|
1147
|
+
if (process.stdout.isTTY) {
|
|
1148
|
+
await runInteractive();
|
|
1149
|
+
process.exit(process.exitCode ?? 0);
|
|
1150
|
+
}
|
|
978
1151
|
program.outputHelp();
|
|
979
1152
|
process.exit(0);
|
|
980
1153
|
}
|
package/package.json
CHANGED