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 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
22 changes: 21 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`, and `update` work today. Registry, publish, and eval flows land in v0.1.
> **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.

## Install

Expand Down Expand Up @@ -106,6 +106,26 @@ skillforge update ./code-review/SKILL.md --new-version 1.0.0

Pass exactly one of `--bump <patch|minor|major>` or `--new-version <semver>`. Pre-release tags (`-beta`, `-rc.1`, …) are dropped on any bump, matching `npm version`. A missing `version:` field is treated as `0.0.1` (the schema default) so a `patch` on a freshly-scaffolded skill produces `0.0.2`. The proposed frontmatter is validated against the schema before anything hits disk, and the write is line-surgical — body bytes, field order, and other YAML formatting are preserved byte-for-byte. Pass `--dry-run` to print the would-be new version without writing.

### `skillforge format <path>`

Reformat a `SKILL.md` to canonical shape — frontmatter keys in the schema's declared order (`name`, `description`, `version`, `tags`, `author`, `homepage`, then any passthrough fields alphabetized at the end), trailing whitespace stripped, runs of blank lines collapsed to two, exactly one trailing newline. Think `prettier` for the SKILL.md envelope: `lint` flags style smells, `format` fixes them.

```bash
skillforge format ./code-review/SKILL.md
# ✓ formatted ./code-review/SKILL.md

skillforge format ./code-review/SKILL.md --check
# exits 1 if anything would change — useful in CI

skillforge format ./code-review/SKILL.md --dry-run
# dry-run ./code-review/SKILL.md (nothing written)

skillforge format ./code-review/SKILL.md --write=false
# prints the formatted result to stdout instead of writing
```

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.

## Schema

```yaml
Expand Down
52 changes: 52 additions & 0 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,11 @@
* pack <dir> bundle a skill directory into a .skill archive
* 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
*/
import { cac } from "cac";
import kleur from "kleur";
import { formatSkill } from "./format.js";
import { initSkill } from "./init.js";
import { installSkill } from "./install.js";
import { computeExitCode, lintSkill } from "./lint.js";
Expand Down Expand Up @@ -175,6 +177,56 @@ cli
}
});

cli
.command("format <path>", "Reformat a SKILL.md to canonical shape")
.option("--write", "Write the formatted result back (default: true; pass --write=false to skip)")
.option("--dry-run", "Compute the formatted result but write nothing")
.option("--check", "Exit 1 if the file would change; write nothing (CI mode)")
.action(async (path: string, opts) => {
try {
// `--check` is dry-run + a non-zero exit on changes. The CLI fans
// these flags out; the library API stays simple.
const isCheck = !!opts.check;
const dryRun = !!opts.dryRun || isCheck;
// cac parses `--write=false` to `false`; `--write` alone to `true`;
// absent to `undefined` (library default of true).
const writeOpt: boolean | undefined = opts.write;
const result = await formatSkill({
path,
write: writeOpt,
dryRun,
});
if (isCheck) {
if (result.changed) {
process.stdout.write(`${kleur.yellow("would-change")} ${result.path}\n`);
process.exit(1);
}
process.stdout.write(`${kleur.green("✓")} ${result.path} — already canonical\n`);
process.exit(0);
}
if (dryRun) {
const tag = result.changed ? kleur.yellow("dry-run") : kleur.green("✓");
const suffix = result.changed ? " (nothing written)" : " — already canonical";
process.stdout.write(`${tag} ${result.path}${suffix}\n`);
process.exit(0);
}
if (writeOpt === false) {
// Print the formatted output to stdout instead of writing.
process.stdout.write(result.after);
process.exit(0);
}
if (result.changed) {
process.stdout.write(`${kleur.green("✓")} formatted ${result.path}\n`);
} else {
process.stdout.write(`${kleur.green("✓")} ${result.path} — already canonical\n`);
}
process.exit(0);
} catch (err) {
process.stderr.write(`${kleur.red("error:")} ${(err as Error).message}\n`);
process.exit(1);
}
});

cli.help();
cli.version(VERSION);
cli.parse();
Loading
Loading