Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/)
## [Unreleased]

### Added
- `skillforge inspect <path>`: one-shot diagnostic report that rolls validation, linting, frontmatter parsing, body stats (lines / words / characters / `## headings` extracted in document order, code-fence aware), and (for directory inputs) the attached-file inventory into one structured `InspectResult`. Reuses `pack`'s exclusion rules so the file list matches exactly what `pack` would bundle — no duplicated walker. Human-readable mode prints a tidy multi-section report with a frontmatter table, right-aligned body-stat numerals, and grouped validation / lint / attached-file lists; `--json` emits the full result for CI. Exits 0 when validation passes and there are no lint errors, 1 otherwise. Composed entirely on top of the existing `validateSkill` / `lintSkill` APIs.
- `skillforge format <path>`: reformat a SKILL.md to canonical shape — frontmatter keys reordered into the schema's declared order (`name`, `description`, `version`, `tags`, `author`, `homepage`) with passthrough fields alphabetized at the end, stringy primitives like `tags: "[]"` coerced back to YAML, trailing whitespace stripped per line, runs of 3+ blank lines collapsed to 2, exactly one trailing newline. The body envelope is normalized but prose is not reflowed and fenced code blocks (` ``` `) are preserved verbatim. The formatted output is validated against the schema before writing; an already-canonical file is a no-op. `--dry-run` returns the diff without writing; `--write=false` prints to stdout; `--check` exits 1 if anything would change (CI mode). Idempotent — running twice produces byte-identical output the second time. Style fixes that pair with `lint`'s style checks.
- `skillforge update <path>`: bump the `version:` field of a SKILL.md in one shot — `--bump <patch|minor|major>` or `--new-version <semver>` (mutually exclusive, exactly one required). Pre-release tags (`-beta`, `-rc.1`, …) are dropped on any bump, matching `npm version`. A missing `version:` field is treated as the schema default of `0.0.1`. Validates the proposed frontmatter against the schema before touching disk and uses a line-surgical write so the body bytes and other YAML formatting are preserved byte-for-byte. `--dry-run` reports the new version without writing.
- `skillforge lint <path>`: warnings-first style/quality linter for `SKILL.md` files. A stricter peer of `validate` that surfaces nine smells `validate` deliberately ignores — short or noun-phrase `description`, descriptions missing trigger language, empty `tags`, stale `version: 0.0.1` files (older than 7 days), missing `## When to use` / `## Examples` headings, `TODO` markers (error), `you should` / `always` second-person phrasing, and trailing whitespace. Exit 0 if only warnings, 1 on errors, 2 with `--strict`. `--json` emits machine-readable issues. Each rule is a tiny pure function so adding rules is a one-liner.
Expand Down
42 changes: 41 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Scaffold, validate, and lint `SKILL.md` files for the agent ecosystem.

---

> **Status — v0.0.2, early days.** `init`, `validate`, `lint`, `pack`, `install`, `update`, and `format` work today. Registry, publish, and eval flows land in v0.1.
> **Status — v0.0.2, early days.** `init`, `validate`, `lint`, `pack`, `install`, `update`, `format`, and `inspect` work today. Registry, publish, and eval flows land in v0.1.

## Install

Expand Down Expand Up @@ -126,6 +126,46 @@ skillforge format ./code-review/SKILL.md --write=false

Fenced code blocks (` ``` `) are preserved verbatim — `format` is gentle on the body, never reflowing prose. The output is always validated against the schema before writing; an already-canonical file is a no-op (`exit 0`, no write). Format is idempotent: running twice produces byte-identical output the second time.

### `skillforge inspect <path>`

One-shot diagnostic report — rolls `validate` + `lint` + a frontmatter summary + body stats + (for directory inputs) the attached-file inventory into one structured view. The "give me everything you know about this skill" command:

```bash
skillforge inspect ./code-review
# code-review /…/SKILL.md ISSUES
#
# FRONTMATTER
# name code-review
# description Use this when the user asks for a code review on a diff…
# version 0.1.0
# tags code, review
#
# BODY
# 42 lines
# 210 words
# 1832 characters
# 4 sections
# headings:
# · What this skill does
# · When to use
# · Examples
#
# VALIDATION
# ✓ no issues
#
# LINT
# · warning …/SKILL.md: tags-empty: tags array is empty…
#
# ATTACHED FILES
# · SKILL.md
# · templates/letter.md
#
# SUMMARY
# ✗ validation: 0 lint: 1
```

Pass `--json` for machine-readable output (CI-friendly — every field is JSON-stable). Exit `0` if validation passes and there are no lint errors, `1` otherwise. Reuses `pack`'s exclusion rules so the attached-file list matches exactly what `pack` would bundle.

## Schema

```yaml
Expand Down
119 changes: 119 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
* install <url> download a remote .skill into ~/.claude/skills/
* update <path> bump the version field of a SKILL.md
* format <path> reformat a SKILL.md to canonical shape
* inspect <path> one-shot report: validation + lint + frontmatter + body
*/
import { cac } from "cac";
import kleur from "kleur";
import { formatSkill } from "./format.js";
import { initSkill } from "./init.js";
import { type InspectResult, inspectSkill } from "./inspect.js";
import { installSkill } from "./install.js";
import { computeExitCode, lintSkill } from "./lint.js";
import { packSkill } from "./pack.js";
Expand Down Expand Up @@ -227,6 +229,123 @@ cli
}
});

cli
.command(
"inspect <path>",
"One-shot report: validation + lint + frontmatter summary + body stats",
)
.option("--json", "Emit the full result as JSON (CI-friendly)")
.action(async (path: string, opts) => {
try {
const result = await inspectSkill({ path, json: !!opts.json });
if (opts.json) {
process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
process.exit(result.summary.ok ? 0 : 1);
}
printInspectReport(result);
process.exit(result.summary.ok ? 0 : 1);
} catch (err) {
process.stderr.write(`${kleur.red("error:")} ${(err as Error).message}\n`);
process.exit(1);
}
});

/**
* Human-readable `inspect` report. Tidy multi-section layout: a header, an
* aligned frontmatter table, body stats with tabular numerals, then any
* validation issues, lint issues, and the attached-file inventory. Each
* heading is sentence-case in tracked uppercase — quiet structural signal,
* not editorial shouting.
*/
function printInspectReport(r: InspectResult): void {
const out = process.stdout;
const status = r.summary.ok ? kleur.green("OK") : kleur.red("ISSUES");
const heading = (label: string) => out.write(`\n${kleur.bold(kleur.dim(label.toUpperCase()))}\n`);

out.write(`${kleur.bold(r.name ?? "<unparsed>")} ${kleur.dim(r.path)} ${status}\n`);

heading("Frontmatter");
if (r.frontmatter) {
const rows: Array<[string, string]> = [
["name", r.frontmatter.name],
["description", truncate(r.frontmatter.description.replace(/\s+/g, " "), 64)],
["version", r.frontmatter.version],
["tags", r.frontmatter.tags.length ? r.frontmatter.tags.join(", ") : kleur.dim("∅")],
];
if (r.frontmatter.author) rows.push(["author", r.frontmatter.author]);
if (r.frontmatter.homepage) rows.push(["homepage", r.frontmatter.homepage]);
const keyWidth = Math.max(...rows.map((row) => row[0].length));
for (const [k, v] of rows) {
out.write(` ${kleur.dim(k.padEnd(keyWidth))} ${v}\n`);
}
} else {
out.write(` ${kleur.dim("(unparseable — see validation issues below)")}\n`);
}

heading("Body");
// tabular-nums analogue: right-align the numbers in a fixed column so the
// eye can compare them at a glance.
const stats: Array<[string, number]> = [
["lines", r.body.lines],
["words", r.body.words],
["characters", r.body.characters],
["sections", r.body.sections.length],
];
const valWidth = Math.max(...stats.map(([, n]) => String(n).length));
for (const [label, n] of stats) {
out.write(` ${String(n).padStart(valWidth)} ${kleur.dim(label)}\n`);
}
if (r.body.sections.length > 0) {
out.write(` ${kleur.dim("headings:")}\n`);
for (const section of r.body.sections) {
out.write(` ${kleur.dim("·")} ${section}\n`);
}
}

heading("Validation");
if (r.validation.ok) {
out.write(` ${kleur.green("✓")} no issues\n`);
} else {
for (const issue of r.validation.issues) {
out.write(` ${kleur.red("·")} ${issue}\n`);
}
}

heading("Lint");
if (r.lint.issues.length === 0) {
out.write(` ${kleur.green("✓")} no issues\n`);
} else {
for (const issue of r.lint.issues) {
const color = issue.severity === "error" ? kleur.red : kleur.yellow;
const loc = issue.line ? `${r.path}:${issue.line}` : r.path;
out.write(
` ${color("·")} ${color(issue.severity)} ${loc}: ${issue.rule}: ${issue.message}\n`,
);
}
}

if (r.attachedFiles) {
heading("Attached files");
if (r.attachedFiles.length === 0) {
out.write(` ${kleur.dim("(none)")}\n`);
} else {
for (const f of r.attachedFiles) {
out.write(` ${kleur.dim("·")} ${f}\n`);
}
}
}

heading("Summary");
out.write(
` ${r.summary.ok ? kleur.green("✓") : kleur.red("✗")} validation: ${r.summary.validationIssues} lint: ${r.summary.lintIssues}\n`,
);
}

function truncate(s: string, max: number): string {
if (s.length <= max) return s;
return `${s.slice(0, max - 1)}…`;
}

cli.help();
cli.version(VERSION);
cli.parse();
7 changes: 7 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
/** Public programmatic API. */
export { type FormatOptions, type FormatResult, formatSkill } from "./format.js";
export { initSkill, type InitOptions } from "./init.js";
export {
type InspectBodyStats,
type InspectOptions,
type InspectResult,
type InspectSummary,
inspectSkill,
} from "./inspect.js";
export {
computeExitCode,
type Issue,
Expand Down
Loading
Loading