Skip to content

Commit 8a75e62

Browse files
committed
Add configurable lint ignores
1 parent 0e31726 commit 8a75e62

16 files changed

Lines changed: 174 additions & 18 deletions

File tree

README.md

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,8 @@ Commands that write files are explicit:
5050
For cautious use, pin the npm version, review the source, and start with read-only commands:
5151

5252
```bash
53-
npx agent-skillforge@0.3.0 lint .
54-
npx agent-skillforge@0.3.0 compat . --target portable
53+
npx agent-skillforge@0.3.1 lint .
54+
npx agent-skillforge@0.3.1 compat . --target portable
5555
```
5656

5757
## Install
@@ -95,7 +95,7 @@ jobs:
9595
runs-on: ubuntu-latest
9696
steps:
9797
- uses: actions/checkout@v6
98-
- uses: f0d010c/skillforge@v0.3.0
98+
- uses: f0d010c/skillforge@v0.3.1
9999
with:
100100
path: .
101101
profile: source
@@ -104,7 +104,7 @@ jobs:
104104
For marketplace-ready checks, use:
105105
106106
```yaml
107-
- uses: f0d010c/skillforge@v0.3.0
107+
- uses: f0d010c/skillforge@v0.3.1
108108
with:
109109
path: .
110110
profile: marketplace
@@ -186,6 +186,30 @@ skillforge pack ./my-plugin
186186

187187
Default lint mode focuses on deterministic publish-readiness problems. Use `--strict` to include advisory checks such as trigger-description quality, large skill bodies, unreferenced scripts, and plugin name/folder mismatch.
188188

189+
## Configuration
190+
191+
Add `skillforge.json` at the repo or package root to tune checks:
192+
193+
```json
194+
{
195+
"name": "my-agent-skill-pack",
196+
"lint": {
197+
"ignore": [
198+
"templates/**",
199+
"tests/fixtures/**",
200+
"examples/broken-on-purpose/**"
201+
]
202+
},
203+
"checks": {
204+
"maxSkillMdLines": 500,
205+
"requireOpenAiYaml": false,
206+
"allowScripts": true
207+
}
208+
}
209+
```
210+
211+
Use `lint.ignore` for intentional fixtures, vendored examples, generated output, or template folders that should not be treated as publishable skills/plugins.
212+
189213
## Compatibility
190214

191215
Use `compat` to check whether a skill/package is likely to work in a specific agent ecosystem.

package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "agent-skillforge",
3-
"version": "0.3.0",
3+
"version": "0.3.1",
44
"description": "Creator tooling for agent skills and plugins.",
55
"type": "module",
66
"bin": {

skillforge.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"name": "agent-skillforge",
3+
"lint": {
4+
"allowEmptyCollection": true,
5+
"ignore": [
6+
"examples/real-world-cases/**",
7+
"templates/**",
8+
"tests/fixtures/**"
9+
]
10+
}
11+
}

src/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ const program = new Command();
1414
program
1515
.name("agent-skillforge")
1616
.description("Creator tooling for agent skills and plugins.")
17-
.version("0.3.0")
17+
.version("0.3.1")
1818
.exitOverride();
1919

2020
program

src/commands/compat.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import path from "node:path";
22
import fs from "fs-extra";
33
import { lintPath } from "../lib/lint.js";
4+
import { loadConfig } from "../lib/config.js";
45
import { parseMarkdownFrontmatter } from "../lib/frontmatter.js";
5-
import type { Issue, IssueImpact, IssueLevel } from "../types.js";
6+
import { defaultIgnoredDirs, isIgnoredPath } from "../lib/ignore.js";
7+
import type { Issue, IssueImpact, IssueLevel, SkillForgeConfig } from "../types.js";
68

79
export type CompatTarget = "codex" | "claude" | "portable";
810
export type CompatFormat = "text" | "json";
@@ -113,6 +115,8 @@ async function scriptPortabilityIssues(root: string): Promise<Issue[]> {
113115

114116
async function findSkillFiles(root: string): Promise<string[]> {
115117
const found: string[] = [];
118+
const config = await loadConfig(root).catch((): SkillForgeConfig => ({}));
119+
const ignorePatterns = config.lint?.ignore ?? [];
116120
async function walk(dir: string, depth: number): Promise<void> {
117121
if (depth > 5) return;
118122
const skillPath = path.join(dir, "SKILL.md");
@@ -121,10 +125,10 @@ async function findSkillFiles(root: string): Promise<string[]> {
121125
return;
122126
}
123127
for (const entry of await fs.readdir(dir)) {
124-
if (["node_modules", "dist", ".git", ".next", "coverage"].includes(entry)) continue;
128+
if (defaultIgnoredDirs.has(entry)) continue;
125129
const full = path.join(dir, entry);
126130
const stat = await fs.stat(full);
127-
if (stat.isDirectory()) await walk(full, depth + 1);
131+
if (stat.isDirectory() && !isIgnoredPath(root, full, ignorePatterns)) await walk(full, depth + 1);
128132
}
129133
}
130134
await walk(root, 0);

src/commands/smoke.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import path from "node:path";
22
import fs from "fs-extra";
33
import { loadConfig } from "../lib/config.js";
44
import { parseMarkdownFrontmatter } from "../lib/frontmatter.js";
5+
import { defaultIgnoredDirs, isIgnoredPath } from "../lib/ignore.js";
56
import { lintPath } from "../lib/lint.js";
67
import { countErrors } from "../lib/reporters.js";
8+
import type { SkillForgeConfig } from "../types.js";
79

810
export async function smokeCommand(targetPath: string): Promise<{ output: string; exitCode: number }> {
911
const absolute = path.resolve(targetPath);
@@ -58,13 +60,16 @@ async function promptMatchScore(root: string, prompt: string): Promise<number> {
5860
async function findSkillFiles(root: string): Promise<string[]> {
5961
const direct = path.join(root, "SKILL.md");
6062
if (await fs.pathExists(direct)) return [direct];
63+
const config = await loadConfig(root).catch((): SkillForgeConfig => ({}));
64+
const ignorePatterns = config.lint?.ignore ?? [];
6165
const files: string[] = [];
6266
async function walk(dir: string, depth: number): Promise<void> {
6367
if (depth > 5) return;
6468
for (const entry of await fs.readdir(dir)) {
65-
if (["node_modules", "dist", ".git"].includes(entry)) continue;
69+
if (defaultIgnoredDirs.has(entry)) continue;
6670
const full = path.join(dir, entry);
6771
if (!(await fs.stat(full)).isDirectory()) continue;
72+
if (isIgnoredPath(root, full, ignorePatterns)) continue;
6873
const skill = path.join(full, "SKILL.md");
6974
if (await fs.pathExists(skill)) files.push(skill);
7075
else await walk(full, depth + 1);

src/lib/config.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ const configSchema = z.object({
2020
requireOpenAiYaml: z.boolean().optional(),
2121
allowScripts: z.boolean().optional()
2222
})
23+
.optional(),
24+
lint: z
25+
.object({
26+
ignore: z.array(z.string().min(1)).optional(),
27+
allowEmptyCollection: z.boolean().optional()
28+
})
2329
.optional()
2430
});
2531

src/lib/ignore.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
import path from "node:path";
2+
3+
export const defaultIgnoredDirs = new Set(["node_modules", "dist", ".git", ".next", "coverage", "tmp-e2e"]);
4+
5+
export function isIgnoredPath(root: string, target: string, patterns: string[] = []): boolean {
6+
const relative = toPosix(path.relative(root, target));
7+
if (!relative || relative.startsWith("..")) return false;
8+
return patterns.some((pattern) => matchesPattern(relative, pattern));
9+
}
10+
11+
function matchesPattern(relative: string, rawPattern: string): boolean {
12+
const pattern = normalizePattern(rawPattern);
13+
if (!pattern) return false;
14+
15+
if (pattern.endsWith("/**")) {
16+
const base = pattern.slice(0, -3);
17+
return relative === base || relative.startsWith(`${base}/`);
18+
}
19+
20+
if (!pattern.includes("*")) {
21+
return relative === pattern || relative.startsWith(`${pattern}/`);
22+
}
23+
24+
return globToRegExp(pattern).test(relative);
25+
}
26+
27+
function normalizePattern(pattern: string): string {
28+
return toPosix(pattern).replace(/^\.\/+/, "").replace(/\/+$/, "");
29+
}
30+
31+
function toPosix(value: string): string {
32+
return value.replaceAll(path.sep, "/").replaceAll("\\", "/");
33+
}
34+
35+
function globToRegExp(pattern: string): RegExp {
36+
let source = "^";
37+
for (let index = 0; index < pattern.length; index += 1) {
38+
const char = pattern[index];
39+
const next = pattern[index + 1];
40+
if (char === "*" && next === "*") {
41+
source += ".*";
42+
index += 1;
43+
} else if (char === "*") {
44+
source += "[^/]*";
45+
} else {
46+
source += escapeRegExp(char);
47+
}
48+
}
49+
source += "$";
50+
return new RegExp(source);
51+
}
52+
53+
function escapeRegExp(value: string): string {
54+
return value.replace(/[\\^$+?.()|[\]{}]/g, "\\$&");
55+
}

src/lib/lint.ts

Lines changed: 22 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { z } from "zod";
55
import type { Issue, LintOptions, LintResult, SkillForgeConfig } from "../types.js";
66
import { loadConfig } from "./config.js";
77
import { parseMarkdownFrontmatter } from "./frontmatter.js";
8+
import { defaultIgnoredDirs, isIgnoredPath } from "./ignore.js";
89
import { hyphenName } from "./paths.js";
910

1011
const skillFrontmatterSchema = z.object({
@@ -71,7 +72,6 @@ const pluginManifestSchema = z
7172
})
7273
.passthrough();
7374

74-
const ignoredDirs = new Set(["node_modules", "dist", ".git", ".next", "coverage", "tmp-e2e"]);
7575
const advisoryCodes = new Set([
7676
"plugin.name.mismatch",
7777
"skill.description.vague",
@@ -99,18 +99,33 @@ export async function lintPath(targetPath: string, options: LintOptions = {}): P
9999
return applyLintOptions(direct, lintOptions);
100100
}
101101

102-
const children = await discoverLintTargets(absolute);
102+
const collectionIssues: Issue[] = [];
103+
const collectionConfig = await loadConfig(absolute).catch((error): SkillForgeConfig => {
104+
collectionIssues.push({ level: "error", impact: "blocking", code: "config.invalid", message: String(error), file: path.join(absolute, "skillforge.json") });
105+
return {};
106+
});
107+
const collectionOptions = { ...lintOptions, ignore: [...(lintOptions.ignore ?? []), ...(collectionConfig.lint?.ignore ?? [])] };
108+
const children = await discoverLintTargets(absolute, collectionOptions.ignore);
103109
if (children.length === 0) {
110+
if (collectionConfig.lint?.allowEmptyCollection) {
111+
return {
112+
targetPath: absolute,
113+
kind: "collection",
114+
profile: lintOptions.profile,
115+
checkedPaths: [],
116+
issues: filterIssues(collectionIssues, collectionOptions)
117+
};
118+
}
104119
return direct;
105120
}
106121

107-
const results = await Promise.all(children.map((child) => lintSingle(child, lintOptions)));
122+
const results = await Promise.all(children.map((child) => lintSingle(child, collectionOptions)));
108123
return {
109124
targetPath: absolute,
110125
kind: "collection",
111126
profile: lintOptions.profile,
112127
checkedPaths: results.map((result) => result.targetPath),
113-
issues: filterIssues(results.flatMap((result) => result.issues), lintOptions)
128+
issues: filterIssues([...collectionIssues, ...results.flatMap((result) => result.issues)], collectionOptions)
114129
};
115130
}
116131

@@ -177,15 +192,16 @@ async function lintSingle(root: string, options: LintOptions): Promise<LintResul
177192
return { targetPath: root, kind: isPlugin ? "plugin" : isSkill ? "skill" : "unknown", profile: options.profile, issues };
178193
}
179194

180-
async function discoverLintTargets(root: string): Promise<string[]> {
195+
async function discoverLintTargets(root: string, ignorePatterns: string[] = []): Promise<string[]> {
181196
const found = new Set<string>();
182197
async function walk(dir: string, depth: number): Promise<void> {
183198
if (depth > 5) return;
184199
for (const entry of await fs.readdir(dir)) {
185-
if (ignoredDirs.has(entry)) continue;
200+
if (defaultIgnoredDirs.has(entry)) continue;
186201
const full = path.join(dir, entry);
187202
const stat = await fs.stat(full);
188203
if (!stat.isDirectory()) continue;
204+
if (isIgnoredPath(root, full, ignorePatterns)) continue;
189205
if ((await fs.pathExists(path.join(full, "SKILL.md"))) || (await fs.pathExists(path.join(full, ".codex-plugin", "plugin.json")))) {
190206
found.add(full);
191207
continue;

0 commit comments

Comments
 (0)