skillmds 1.0.9 → 1.0.11
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 +12 -0
- package/dist/cli.js +91 -29
- package/package.json +1 -1
- package/vendor/index.js +18 -8
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to `skillmds` are documented here. Versions follow [semver](https://semver.org).
|
|
4
4
|
|
|
5
|
+
## 1.0.11
|
|
6
|
+
|
|
7
|
+
- Security flags render with full readable labels, each with its own marker: `⚑ Executes Scripts ⚑ Calls Network ⚑ Reads Secrets`.
|
|
8
|
+
|
|
9
|
+
## 1.0.10
|
|
10
|
+
|
|
11
|
+
- 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).
|
|
12
|
+
- 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.
|
|
13
|
+
- New `--errors-only` flag to show only failing skills.
|
|
14
|
+
- Fixed emoji spacing/overlap; rule ids are grouped by severity per skill.
|
|
15
|
+
- The closer states how many skills failed instead of a vague "Problems found".
|
|
16
|
+
|
|
5
17
|
## 1.0.9
|
|
6
18
|
|
|
7
19
|
- 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,46 +380,80 @@ 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"
|
|
377
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_LABEL = {
|
|
401
|
+
network_calls: "Calls Network",
|
|
402
|
+
executes_scripts: "Executes Scripts",
|
|
403
|
+
reads_secrets: "Reads Secrets"
|
|
404
|
+
};
|
|
405
|
+
var titleCase = (flag) => flag.split("_").map((w) => w[0].toUpperCase() + w.slice(1)).join(" ");
|
|
378
406
|
var scoreColor = (s) => s >= 80 ? pc.green : s >= 50 ? pc.yellow : pc.red;
|
|
379
407
|
function label(file) {
|
|
380
408
|
const parts = file.split(/[\\/]/).filter(Boolean);
|
|
381
409
|
const i = parts.lastIndexOf("SKILL.md");
|
|
382
410
|
return i > 0 ? parts[i - 1] : parts[parts.length - 1] || file;
|
|
383
411
|
}
|
|
412
|
+
function securityFlags(result) {
|
|
413
|
+
return result.security.flags.filter((f) => f !== "docs_only");
|
|
414
|
+
}
|
|
415
|
+
function secLabels(result) {
|
|
416
|
+
const flags = securityFlags(result);
|
|
417
|
+
return flags.map((f) => pc.magenta(`${SEC_GLYPH} ${SEC_LABEL[f] ?? titleCase(f)}`)).join(" ");
|
|
418
|
+
}
|
|
384
419
|
function statusDot(result) {
|
|
385
420
|
if (!result.ok) return pc.red("\u25CF");
|
|
386
|
-
|
|
421
|
+
const attention = result.diagnostics.some((d) => d.severity === "warn") || securityFlags(result).length > 0;
|
|
422
|
+
return attention ? pc.yellow("\u25CF") : pc.green("\u25CF");
|
|
387
423
|
}
|
|
388
|
-
function
|
|
389
|
-
|
|
424
|
+
function failureBlock(file, result) {
|
|
425
|
+
const lines = [`${pc.red("\u2716")} ${pc.bold(label(file))}`];
|
|
426
|
+
for (const d of result.diagnostics.filter((x) => x.severity === "error")) {
|
|
427
|
+
const loc = d.line !== void 0 ? pc.dim(`:${d.line}`) : "";
|
|
428
|
+
lines.push(` ${SEV_COLOR.error(d.id)}${loc} ${d.message}`);
|
|
429
|
+
}
|
|
430
|
+
lines.push(` ${pc.dim(file)}`);
|
|
431
|
+
return lines.join("\n");
|
|
390
432
|
}
|
|
391
433
|
function renderOne(file, result) {
|
|
392
434
|
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`)}`];
|
|
435
|
+
const out = [`${status} ${pc.bold(label(file))} ${pc.dim("quality")} ${scoreColor(result.score)(`${result.score}/100`)}`];
|
|
394
436
|
for (const d of result.diagnostics) {
|
|
395
437
|
const loc = d.line !== void 0 ? pc.dim(`:${d.line}`) : "";
|
|
396
|
-
out.push(` ${GLYPH[d.severity]} ${
|
|
438
|
+
out.push(` ${GLYPH[d.severity]} ${SEV_COLOR[d.severity](d.id)}${loc} ${d.message}`);
|
|
397
439
|
}
|
|
398
|
-
const
|
|
399
|
-
if (
|
|
440
|
+
const sec = secLabels(result);
|
|
441
|
+
if (sec) out.push(` ${sec}`);
|
|
400
442
|
return out.join("\n");
|
|
401
443
|
}
|
|
402
444
|
function renderMany(results) {
|
|
403
445
|
const names = results.map((r) => label(r.file));
|
|
404
|
-
const w = Math.min(
|
|
446
|
+
const w = Math.min(32, Math.max(4, ...names.map((n) => n.length)));
|
|
405
447
|
const out = [];
|
|
448
|
+
let anySec = false;
|
|
406
449
|
results.forEach(({ result }, i) => {
|
|
407
450
|
const score = scoreColor(result.score)(String(result.score).padStart(3));
|
|
408
|
-
const
|
|
409
|
-
const
|
|
410
|
-
const
|
|
411
|
-
|
|
412
|
-
|
|
451
|
+
const nm = names[i];
|
|
452
|
+
const name = (nm.length > w ? nm.slice(0, w - 1) + "\u2026" : nm).padEnd(w);
|
|
453
|
+
const secStr = secLabels(result);
|
|
454
|
+
if (secStr) anySec = true;
|
|
455
|
+
const sec = secStr ? ` ${secStr}` : "";
|
|
456
|
+
out.push(`${statusDot(result)} ${score} ${name} ${chips(result)}${sec}`.trimEnd());
|
|
413
457
|
});
|
|
414
458
|
const failed = results.filter((r) => !r.result.ok).length;
|
|
415
459
|
const warned = results.filter((r) => r.result.ok && r.result.diagnostics.some((d) => d.severity === "warn")).length;
|
|
@@ -419,11 +463,28 @@ function renderMany(results) {
|
|
|
419
463
|
const parts = [pc.green(`${clean} clean`)];
|
|
420
464
|
if (warned) parts.push(pc.yellow(`${warned} warn`));
|
|
421
465
|
if (failed) parts.push(pc.red(`${failed} fail`));
|
|
422
|
-
out.push(`${parts.join(pc.dim(" \xB7 "))} ${pc.dim("avg")} ${scoreColor(avg)(`${avg}/100`)}`);
|
|
466
|
+
out.push(`${parts.join(pc.dim(" \xB7 "))} ${pc.dim("avg score")} ${scoreColor(avg)(`${avg}/100`)}`);
|
|
467
|
+
if (anySec) out.push(pc.dim(`${SEC_GLYPH} = risky behaviour, factored into the score alongside spec correctness and completeness`));
|
|
468
|
+
const fails = results.filter((r) => !r.result.ok);
|
|
469
|
+
if (fails.length) {
|
|
470
|
+
out.push("");
|
|
471
|
+
out.push(pc.red(pc.bold(`Failed (${fails.length}):`)));
|
|
472
|
+
for (const f of fails.slice(0, 25)) out.push(failureBlock(f.file, f.result));
|
|
473
|
+
if (fails.length > 25) out.push(pc.dim(` \u2026and ${fails.length - 25} more \u2014 run with --errors-only to see all`));
|
|
474
|
+
}
|
|
475
|
+
return out.join("\n");
|
|
476
|
+
}
|
|
477
|
+
function renderFailuresView(results) {
|
|
478
|
+
const fails = results.filter((r) => !r.result.ok);
|
|
479
|
+
if (!fails.length) return pc.green(`\u2713 No failures across ${results.length} skill${results.length === 1 ? "" : "s"}.`);
|
|
480
|
+
const out = fails.map((f) => failureBlock(f.file, f.result));
|
|
481
|
+
out.push("");
|
|
482
|
+
out.push(`${pc.red(`${fails.length} failed`)} ${pc.dim(`of ${results.length}`)}`);
|
|
423
483
|
return out.join("\n");
|
|
424
484
|
}
|
|
425
|
-
function renderReport(results) {
|
|
485
|
+
function renderReport(results, opts = {}) {
|
|
426
486
|
if (results.length === 0) return pc.dim("No skills found.");
|
|
487
|
+
if (opts.errorsOnly) return renderFailuresView(results);
|
|
427
488
|
if (results.length === 1) return renderOne(results[0].file, results[0].result);
|
|
428
489
|
return renderMany(results);
|
|
429
490
|
}
|
|
@@ -472,12 +533,12 @@ async function runLint(target, flags) {
|
|
|
472
533
|
output = toGithub(results);
|
|
473
534
|
break;
|
|
474
535
|
default:
|
|
475
|
-
output = renderReport(results);
|
|
536
|
+
output = renderReport(results, { errorsOnly: flags.errorsOnly });
|
|
476
537
|
}
|
|
477
538
|
return { results, output, exitCode };
|
|
478
539
|
}
|
|
479
540
|
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) => {
|
|
541
|
+
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
542
|
const ui = Boolean(process.stdout.isTTY) && opts.format === "text";
|
|
482
543
|
if (resolve(target) === homedir()) {
|
|
483
544
|
process.stderr.write(pc2.yellow(`Warning: scanning your entire home directory. Pass a narrower path, e.g. skillmd lint ./my-skill
|
|
@@ -491,7 +552,8 @@ function lintCommand() {
|
|
|
491
552
|
const run2 = await runLint(target, opts);
|
|
492
553
|
s.stop(`Scanned ${run2.results.length} skill${run2.results.length === 1 ? "" : "s"}`);
|
|
493
554
|
console.log(run2.output);
|
|
494
|
-
|
|
555
|
+
const failed = run2.results.filter((r) => !r.result.ok).length;
|
|
556
|
+
p.outro(failed === 0 ? pc2.green("All good") : pc2.red(`${failed} skill${failed === 1 ? "" : "s"} failed \u2014 details above`));
|
|
495
557
|
process.exitCode = run2.exitCode;
|
|
496
558
|
} catch (err) {
|
|
497
559
|
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
|