skillmds 1.0.9 → 1.0.10
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 +8 -0
- package/dist/cli.js +89 -29
- package/package.json +1 -1
- package/vendor/index.js +18 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `skillmds` are documented here. Versions follow [semver](https://semver.org).
|
|
4
4
|
|
|
5
|
+
## 1.0.10
|
|
6
|
+
|
|
7
|
+
- Quality score is now a composite of spec correctness, completeness, and **security risk** — skills that read secrets, execute scripts, or make network calls score lower (a risky/stub skill is no longer a green 100).
|
|
8
|
+
- Lint output ends with a **Failed** section listing each failure's rule, message, and full path, so problems don't scroll off in a large run.
|
|
9
|
+
- New `--errors-only` flag to show only failing skills.
|
|
10
|
+
- Fixed emoji spacing/overlap; rule ids are grouped by severity per skill.
|
|
11
|
+
- The closer states how many skills failed instead of a vague "Problems found".
|
|
12
|
+
|
|
5
13
|
## 1.0.9
|
|
6
14
|
|
|
7
15
|
- Compact lint output: one line per skill (status dot, score, name, rule-id chips) with a single summary line, instead of a tall multi-row block per skill. Linting a single skill still shows full diagnostic messages.
|
package/dist/cli.js
CHANGED
|
@@ -157,12 +157,21 @@ function runRules(skill, ctx = {}) {
|
|
|
157
157
|
|
|
158
158
|
// ../../packages/core/src/quality.ts
|
|
159
159
|
var WEIGHT = {
|
|
160
|
-
error:
|
|
161
|
-
warn:
|
|
162
|
-
info:
|
|
160
|
+
error: 30,
|
|
161
|
+
warn: 10,
|
|
162
|
+
info: 3
|
|
163
163
|
};
|
|
164
|
-
|
|
165
|
-
|
|
164
|
+
var RISK = {
|
|
165
|
+
docs_only: 0,
|
|
166
|
+
reads_secrets: 25,
|
|
167
|
+
executes_scripts: 15,
|
|
168
|
+
network_calls: 8
|
|
169
|
+
};
|
|
170
|
+
function qualityScore(diagnostics, security) {
|
|
171
|
+
let penalty = diagnostics.reduce((sum, d) => sum + WEIGHT[d.severity], 0);
|
|
172
|
+
if (security) {
|
|
173
|
+
for (const flag of security.flags) penalty += RISK[flag] ?? 0;
|
|
174
|
+
}
|
|
166
175
|
return Math.max(0, Math.min(100, 100 - penalty));
|
|
167
176
|
}
|
|
168
177
|
|
|
@@ -171,18 +180,19 @@ function lint(raw, opts = {}) {
|
|
|
171
180
|
const parsed = parseSkillMd(raw, opts.type ?? "single");
|
|
172
181
|
if ("error" in parsed) {
|
|
173
182
|
const diagnostics2 = [{ id: "SK001", severity: "error", message: parsed.error }];
|
|
183
|
+
const security2 = scanSecurity(raw);
|
|
174
184
|
return {
|
|
175
185
|
ok: false,
|
|
176
186
|
diagnostics: diagnostics2,
|
|
177
|
-
security:
|
|
178
|
-
score: qualityScore(diagnostics2)
|
|
187
|
+
security: security2,
|
|
188
|
+
score: qualityScore(diagnostics2, security2)
|
|
179
189
|
};
|
|
180
190
|
}
|
|
181
191
|
const ctx = opts.slug !== void 0 ? { slug: opts.slug } : {};
|
|
182
192
|
const diagnostics = runRules(parsed, ctx);
|
|
183
193
|
const security = scanSecurity(parsed.body);
|
|
184
194
|
const ok = !diagnostics.some((d) => d.severity === "error");
|
|
185
|
-
return { ok, skill: parsed, diagnostics, security, score: qualityScore(diagnostics) };
|
|
195
|
+
return { ok, skill: parsed, diagnostics, security, score: qualityScore(diagnostics, security) };
|
|
186
196
|
}
|
|
187
197
|
|
|
188
198
|
// ../../packages/core/src/format/text.ts
|
|
@@ -370,10 +380,27 @@ var decorate = (text3, kind) => {
|
|
|
370
380
|
return pc.dim(text3);
|
|
371
381
|
}
|
|
372
382
|
};
|
|
383
|
+
var SEV_COLOR = {
|
|
384
|
+
error: pc.red,
|
|
385
|
+
warn: pc.yellow,
|
|
386
|
+
info: pc.cyan
|
|
387
|
+
};
|
|
373
388
|
var GLYPH = {
|
|
374
|
-
error:
|
|
375
|
-
warn:
|
|
376
|
-
info:
|
|
389
|
+
error: "\u2716",
|
|
390
|
+
warn: "\u26A0",
|
|
391
|
+
info: "\u2139"
|
|
392
|
+
};
|
|
393
|
+
var SEC_GLYPH = "\u2691";
|
|
394
|
+
var SEV_ORDER = ["error", "warn", "info"];
|
|
395
|
+
function chips(result) {
|
|
396
|
+
const bySev = { error: [], warn: [], info: [] };
|
|
397
|
+
for (const d of result.diagnostics) bySev[d.severity].push(d.id);
|
|
398
|
+
return SEV_ORDER.filter((s) => bySev[s].length).map((s) => `${GLYPH[s]} ${SEV_COLOR[s](bySev[s].join(" "))}`).join(" ");
|
|
399
|
+
}
|
|
400
|
+
var SEC_SHORT = {
|
|
401
|
+
network_calls: "net",
|
|
402
|
+
executes_scripts: "exec",
|
|
403
|
+
reads_secrets: "secrets"
|
|
377
404
|
};
|
|
378
405
|
var scoreColor = (s) => s >= 80 ? pc.green : s >= 50 ? pc.yellow : pc.red;
|
|
379
406
|
function label(file) {
|
|
@@ -381,35 +408,50 @@ function label(file) {
|
|
|
381
408
|
const i = parts.lastIndexOf("SKILL.md");
|
|
382
409
|
return i > 0 ? parts[i - 1] : parts[parts.length - 1] || file;
|
|
383
410
|
}
|
|
411
|
+
function securityFlags(result) {
|
|
412
|
+
return result.security.flags.filter((f) => f !== "docs_only");
|
|
413
|
+
}
|
|
414
|
+
function secCodes(result) {
|
|
415
|
+
return securityFlags(result).map((f) => SEC_SHORT[f] ?? f);
|
|
416
|
+
}
|
|
384
417
|
function statusDot(result) {
|
|
385
418
|
if (!result.ok) return pc.red("\u25CF");
|
|
386
|
-
|
|
419
|
+
const attention = result.diagnostics.some((d) => d.severity === "warn") || securityFlags(result).length > 0;
|
|
420
|
+
return attention ? pc.yellow("\u25CF") : pc.green("\u25CF");
|
|
387
421
|
}
|
|
388
|
-
function
|
|
389
|
-
|
|
422
|
+
function failureBlock(file, result) {
|
|
423
|
+
const lines = [`${pc.red("\u2716")} ${pc.bold(label(file))}`];
|
|
424
|
+
for (const d of result.diagnostics.filter((x) => x.severity === "error")) {
|
|
425
|
+
const loc = d.line !== void 0 ? pc.dim(`:${d.line}`) : "";
|
|
426
|
+
lines.push(` ${SEV_COLOR.error(d.id)}${loc} ${d.message}`);
|
|
427
|
+
}
|
|
428
|
+
lines.push(` ${pc.dim(file)}`);
|
|
429
|
+
return lines.join("\n");
|
|
390
430
|
}
|
|
391
431
|
function renderOne(file, result) {
|
|
392
432
|
const status = result.ok ? pc.green("\u25CF PASS") : pc.red("\u25CF FAIL");
|
|
393
|
-
const out = [`${status} ${pc.bold(label(file))} ${scoreColor(result.score)(`${result.score}/100`)}`];
|
|
433
|
+
const out = [`${status} ${pc.bold(label(file))} ${pc.dim("quality")} ${scoreColor(result.score)(`${result.score}/100`)}`];
|
|
394
434
|
for (const d of result.diagnostics) {
|
|
395
435
|
const loc = d.line !== void 0 ? pc.dim(`:${d.line}`) : "";
|
|
396
|
-
out.push(` ${GLYPH[d.severity]} ${
|
|
436
|
+
out.push(` ${GLYPH[d.severity]} ${SEV_COLOR[d.severity](d.id)}${loc} ${d.message}`);
|
|
397
437
|
}
|
|
398
|
-
const
|
|
399
|
-
if (
|
|
438
|
+
const codes = secCodes(result);
|
|
439
|
+
if (codes.length) out.push(` ${pc.magenta(`${SEC_GLYPH} ${codes.join(", ")}`)}`);
|
|
400
440
|
return out.join("\n");
|
|
401
441
|
}
|
|
402
442
|
function renderMany(results) {
|
|
403
443
|
const names = results.map((r) => label(r.file));
|
|
404
|
-
const w = Math.min(
|
|
444
|
+
const w = Math.min(32, Math.max(4, ...names.map((n) => n.length)));
|
|
405
445
|
const out = [];
|
|
446
|
+
let anySec = false;
|
|
406
447
|
results.forEach(({ result }, i) => {
|
|
407
448
|
const score = scoreColor(result.score)(String(result.score).padStart(3));
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
449
|
+
const nm = names[i];
|
|
450
|
+
const name = (nm.length > w ? nm.slice(0, w - 1) + "\u2026" : nm).padEnd(w);
|
|
451
|
+
const codes = secCodes(result);
|
|
452
|
+
if (codes.length) anySec = true;
|
|
453
|
+
const sec = codes.length ? ` ${pc.magenta(`${SEC_GLYPH} ${codes.join(" ")}`)}` : "";
|
|
454
|
+
out.push(`${statusDot(result)} ${score} ${name} ${chips(result)}${sec}`.trimEnd());
|
|
413
455
|
});
|
|
414
456
|
const failed = results.filter((r) => !r.result.ok).length;
|
|
415
457
|
const warned = results.filter((r) => r.result.ok && r.result.diagnostics.some((d) => d.severity === "warn")).length;
|
|
@@ -419,11 +461,28 @@ function renderMany(results) {
|
|
|
419
461
|
const parts = [pc.green(`${clean} clean`)];
|
|
420
462
|
if (warned) parts.push(pc.yellow(`${warned} warn`));
|
|
421
463
|
if (failed) parts.push(pc.red(`${failed} fail`));
|
|
422
|
-
out.push(`${parts.join(pc.dim(" \xB7 "))} ${pc.dim("avg")} ${scoreColor(avg)(`${avg}/100`)}`);
|
|
464
|
+
out.push(`${parts.join(pc.dim(" \xB7 "))} ${pc.dim("avg score")} ${scoreColor(avg)(`${avg}/100`)}`);
|
|
465
|
+
if (anySec) out.push(pc.dim("score = spec + completeness + security risk; magenta \u2691 = risky behaviour (net, exec, secrets)"));
|
|
466
|
+
const fails = results.filter((r) => !r.result.ok);
|
|
467
|
+
if (fails.length) {
|
|
468
|
+
out.push("");
|
|
469
|
+
out.push(pc.red(pc.bold(`Failed (${fails.length}):`)));
|
|
470
|
+
for (const f of fails.slice(0, 25)) out.push(failureBlock(f.file, f.result));
|
|
471
|
+
if (fails.length > 25) out.push(pc.dim(` \u2026and ${fails.length - 25} more \u2014 run with --errors-only to see all`));
|
|
472
|
+
}
|
|
473
|
+
return out.join("\n");
|
|
474
|
+
}
|
|
475
|
+
function renderFailuresView(results) {
|
|
476
|
+
const fails = results.filter((r) => !r.result.ok);
|
|
477
|
+
if (!fails.length) return pc.green(`\u2713 No failures across ${results.length} skill${results.length === 1 ? "" : "s"}.`);
|
|
478
|
+
const out = fails.map((f) => failureBlock(f.file, f.result));
|
|
479
|
+
out.push("");
|
|
480
|
+
out.push(`${pc.red(`${fails.length} failed`)} ${pc.dim(`of ${results.length}`)}`);
|
|
423
481
|
return out.join("\n");
|
|
424
482
|
}
|
|
425
|
-
function renderReport(results) {
|
|
483
|
+
function renderReport(results, opts = {}) {
|
|
426
484
|
if (results.length === 0) return pc.dim("No skills found.");
|
|
485
|
+
if (opts.errorsOnly) return renderFailuresView(results);
|
|
427
486
|
if (results.length === 1) return renderOne(results[0].file, results[0].result);
|
|
428
487
|
return renderMany(results);
|
|
429
488
|
}
|
|
@@ -472,12 +531,12 @@ async function runLint(target, flags) {
|
|
|
472
531
|
output = toGithub(results);
|
|
473
532
|
break;
|
|
474
533
|
default:
|
|
475
|
-
output = renderReport(results);
|
|
534
|
+
output = renderReport(results, { errorsOnly: flags.errorsOnly });
|
|
476
535
|
}
|
|
477
536
|
return { results, output, exitCode };
|
|
478
537
|
}
|
|
479
538
|
function lintCommand() {
|
|
480
|
-
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) => {
|
|
539
|
+
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)").option("--errors-only", "show only skills that failed").action(async (target, opts) => {
|
|
481
540
|
const ui = Boolean(process.stdout.isTTY) && opts.format === "text";
|
|
482
541
|
if (resolve(target) === homedir()) {
|
|
483
542
|
process.stderr.write(pc2.yellow(`Warning: scanning your entire home directory. Pass a narrower path, e.g. skillmd lint ./my-skill
|
|
@@ -491,7 +550,8 @@ function lintCommand() {
|
|
|
491
550
|
const run2 = await runLint(target, opts);
|
|
492
551
|
s.stop(`Scanned ${run2.results.length} skill${run2.results.length === 1 ? "" : "s"}`);
|
|
493
552
|
console.log(run2.output);
|
|
494
|
-
|
|
553
|
+
const failed = run2.results.filter((r) => !r.result.ok).length;
|
|
554
|
+
p.outro(failed === 0 ? pc2.green("All good") : pc2.red(`${failed} skill${failed === 1 ? "" : "s"} failed \u2014 details above`));
|
|
495
555
|
process.exitCode = run2.exitCode;
|
|
496
556
|
} catch (err) {
|
|
497
557
|
s.stop(pc2.red("No skills found"));
|
package/package.json
CHANGED
package/vendor/index.js
CHANGED
|
@@ -163,12 +163,21 @@ function runRules(skill, ctx = {}) {
|
|
|
163
163
|
|
|
164
164
|
// ../../packages/core/src/quality.ts
|
|
165
165
|
var WEIGHT = {
|
|
166
|
-
error:
|
|
167
|
-
warn:
|
|
168
|
-
info:
|
|
166
|
+
error: 30,
|
|
167
|
+
warn: 10,
|
|
168
|
+
info: 3
|
|
169
169
|
};
|
|
170
|
-
|
|
171
|
-
|
|
170
|
+
var RISK = {
|
|
171
|
+
docs_only: 0,
|
|
172
|
+
reads_secrets: 25,
|
|
173
|
+
executes_scripts: 15,
|
|
174
|
+
network_calls: 8
|
|
175
|
+
};
|
|
176
|
+
function qualityScore(diagnostics, security) {
|
|
177
|
+
let penalty = diagnostics.reduce((sum, d) => sum + WEIGHT[d.severity], 0);
|
|
178
|
+
if (security) {
|
|
179
|
+
for (const flag of security.flags) penalty += RISK[flag] ?? 0;
|
|
180
|
+
}
|
|
172
181
|
return Math.max(0, Math.min(100, 100 - penalty));
|
|
173
182
|
}
|
|
174
183
|
|
|
@@ -177,18 +186,19 @@ function lint(raw, opts = {}) {
|
|
|
177
186
|
const parsed = parseSkillMd(raw, opts.type ?? "single");
|
|
178
187
|
if ("error" in parsed) {
|
|
179
188
|
const diagnostics2 = [{ id: "SK001", severity: "error", message: parsed.error }];
|
|
189
|
+
const security2 = scanSecurity(raw);
|
|
180
190
|
return {
|
|
181
191
|
ok: false,
|
|
182
192
|
diagnostics: diagnostics2,
|
|
183
|
-
security:
|
|
184
|
-
score: qualityScore(diagnostics2)
|
|
193
|
+
security: security2,
|
|
194
|
+
score: qualityScore(diagnostics2, security2)
|
|
185
195
|
};
|
|
186
196
|
}
|
|
187
197
|
const ctx = opts.slug !== void 0 ? { slug: opts.slug } : {};
|
|
188
198
|
const diagnostics = runRules(parsed, ctx);
|
|
189
199
|
const security = scanSecurity(parsed.body);
|
|
190
200
|
const ok = !diagnostics.some((d) => d.severity === "error");
|
|
191
|
-
return { ok, skill: parsed, diagnostics, security, score: qualityScore(diagnostics) };
|
|
201
|
+
return { ok, skill: parsed, diagnostics, security, score: qualityScore(diagnostics, security) };
|
|
192
202
|
}
|
|
193
203
|
|
|
194
204
|
// ../../packages/core/src/format/text.ts
|