@skillmarkdown/cli 0.1.3 → 0.1.5
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/README.md +4 -0
- package/dist/cli.js +12 -9
- package/dist/commands/init.js +5 -6
- package/dist/commands/validate.js +6 -9
- package/dist/lib/cli-text.js +6 -0
- package/dist/lib/command-output.js +16 -0
- package/dist/lib/normalize-name.js +4 -4
- package/dist/lib/scaffold.js +2 -2
- package/dist/lib/skill-spec.js +20 -0
- package/dist/lib/templates.js +3 -23
- package/dist/lib/validator.js +39 -78
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,6 +42,8 @@ skillmd init --no-validate
|
|
|
42
42
|
skillmd validate
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
+
Default `validate` is spec-only. It checks `SKILL.md` and frontmatter rules, and does not require scaffold directories/files.
|
|
46
|
+
|
|
45
47
|
You can also pass an explicit path:
|
|
46
48
|
|
|
47
49
|
```bash
|
|
@@ -54,6 +56,8 @@ Run additional scaffold/template checks:
|
|
|
54
56
|
skillmd validate --strict
|
|
55
57
|
```
|
|
56
58
|
|
|
59
|
+
`--strict` is intentionally stronger than base spec mode and enforces scaffold/template conventions (for example `.gitkeep` files and required section headings).
|
|
60
|
+
|
|
57
61
|
Compare local validation with `skills-ref` (when installed):
|
|
58
62
|
|
|
59
63
|
```bash
|
package/dist/cli.js
CHANGED
|
@@ -3,24 +3,27 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const init_1 = require("./commands/init");
|
|
5
5
|
const validate_1 = require("./commands/validate");
|
|
6
|
+
const cli_text_1 = require("./lib/cli-text");
|
|
7
|
+
const COMMAND_HANDLERS = {
|
|
8
|
+
init: init_1.runInitCommand,
|
|
9
|
+
validate: validate_1.runValidateCommand,
|
|
10
|
+
};
|
|
6
11
|
function main() {
|
|
7
12
|
const args = process.argv.slice(2);
|
|
13
|
+
const command = args[0];
|
|
8
14
|
if (args.length === 0) {
|
|
9
15
|
console.error("skillmd: no command provided");
|
|
10
|
-
console.error(
|
|
16
|
+
console.error(cli_text_1.ROOT_USAGE);
|
|
11
17
|
process.exitCode = 1;
|
|
12
18
|
return;
|
|
13
19
|
}
|
|
14
|
-
|
|
15
|
-
|
|
20
|
+
const handler = COMMAND_HANDLERS[command];
|
|
21
|
+
if (handler) {
|
|
22
|
+
process.exitCode = handler(args.slice(1));
|
|
16
23
|
return;
|
|
17
24
|
}
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
return;
|
|
21
|
-
}
|
|
22
|
-
console.error(`skillmd: unknown command '${args[0]}'`);
|
|
23
|
-
console.error("Usage: skillmd <init|validate>");
|
|
25
|
+
console.error(`skillmd: unknown command '${command}'`);
|
|
26
|
+
console.error(cli_text_1.ROOT_USAGE);
|
|
24
27
|
process.exitCode = 1;
|
|
25
28
|
}
|
|
26
29
|
main();
|
package/dist/commands/init.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.runInitCommand = runInitCommand;
|
|
4
|
+
const cli_text_1 = require("../lib/cli-text");
|
|
5
|
+
const command_output_1 = require("../lib/command-output");
|
|
4
6
|
const scaffold_1 = require("../lib/scaffold");
|
|
5
7
|
const validator_1 = require("../lib/validator");
|
|
6
8
|
function runInitCommand(args, options = {}) {
|
|
@@ -9,23 +11,20 @@ function runInitCommand(args, options = {}) {
|
|
|
9
11
|
const skipValidation = args.includes("--no-validate");
|
|
10
12
|
const hasUnsupportedArgs = args.length > 1 || (args.length === 1 && args[0] !== "--no-validate");
|
|
11
13
|
if (hasUnsupportedArgs) {
|
|
12
|
-
|
|
13
|
-
console.error("Usage: skillmd init [--no-validate]");
|
|
14
|
-
return 1;
|
|
14
|
+
return (0, command_output_1.failWithUsage)("skillmd init: unsupported argument(s)", cli_text_1.INIT_USAGE);
|
|
15
15
|
}
|
|
16
16
|
try {
|
|
17
17
|
const result = (0, scaffold_1.scaffoldSkillInDirectory)(cwd);
|
|
18
|
-
console.log(`Initialized skill '${result.skillName}'
|
|
18
|
+
console.log(`Initialized skill '${result.skillName}'.`);
|
|
19
19
|
if (skipValidation) {
|
|
20
20
|
console.log("Validation skipped (--no-validate).");
|
|
21
21
|
return 0;
|
|
22
22
|
}
|
|
23
23
|
const validation = validateSkillFn(cwd);
|
|
24
|
+
(0, command_output_1.printValidationResult)(validation);
|
|
24
25
|
if (validation.status === "passed") {
|
|
25
|
-
console.log(`Validation passed: ${validation.message}`);
|
|
26
26
|
return 0;
|
|
27
27
|
}
|
|
28
|
-
console.error(`Validation failed: ${validation.message}`);
|
|
29
28
|
console.error("Run 'skillmd validate' after fixing issues.");
|
|
30
29
|
return 1;
|
|
31
30
|
}
|
|
@@ -2,8 +2,10 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.runValidateCommand = runValidateCommand;
|
|
4
4
|
const node_path_1 = require("node:path");
|
|
5
|
-
const
|
|
5
|
+
const cli_text_1 = require("../lib/cli-text");
|
|
6
|
+
const command_output_1 = require("../lib/command-output");
|
|
6
7
|
const upstream_validator_1 = require("../lib/upstream-validator");
|
|
8
|
+
const validator_1 = require("../lib/validator");
|
|
7
9
|
function runValidateCommand(args, options = {}) {
|
|
8
10
|
const cwd = options.cwd ?? process.cwd();
|
|
9
11
|
const validateLocal = options.validateLocal ?? validator_1.validateSkill;
|
|
@@ -21,14 +23,10 @@ function runValidateCommand(args, options = {}) {
|
|
|
21
23
|
continue;
|
|
22
24
|
}
|
|
23
25
|
if (arg.startsWith("-")) {
|
|
24
|
-
|
|
25
|
-
console.error("Usage: skillmd validate [path] [--strict] [--parity]");
|
|
26
|
-
return 1;
|
|
26
|
+
return (0, command_output_1.failWithUsage)(`skillmd validate: unsupported flag '${arg}'`, cli_text_1.VALIDATE_USAGE);
|
|
27
27
|
}
|
|
28
28
|
if (pathArg) {
|
|
29
|
-
|
|
30
|
-
console.error("Usage: skillmd validate [path] [--strict] [--parity]");
|
|
31
|
-
return 1;
|
|
29
|
+
return (0, command_output_1.failWithUsage)("skillmd validate: accepts at most one path argument", cli_text_1.VALIDATE_USAGE);
|
|
32
30
|
}
|
|
33
31
|
pathArg = arg;
|
|
34
32
|
}
|
|
@@ -50,14 +48,13 @@ function runValidateCommand(args, options = {}) {
|
|
|
50
48
|
return 1;
|
|
51
49
|
}
|
|
52
50
|
}
|
|
51
|
+
(0, command_output_1.printValidationResult)(validation);
|
|
53
52
|
if (validation.status === "passed") {
|
|
54
|
-
console.log(`Validation passed: ${validation.message}`);
|
|
55
53
|
if (parity) {
|
|
56
54
|
console.log("Validation parity passed (skills-ref).");
|
|
57
55
|
}
|
|
58
56
|
return 0;
|
|
59
57
|
}
|
|
60
|
-
console.error(`Validation failed: ${validation.message}`);
|
|
61
58
|
if (parity) {
|
|
62
59
|
console.error("Validation parity matched (skills-ref also failed).");
|
|
63
60
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.VALIDATE_USAGE = exports.INIT_USAGE = exports.ROOT_USAGE = void 0;
|
|
4
|
+
exports.ROOT_USAGE = "Usage: skillmd <init|validate>";
|
|
5
|
+
exports.INIT_USAGE = "Usage: skillmd init [--no-validate]";
|
|
6
|
+
exports.VALIDATE_USAGE = "Usage: skillmd validate [path] [--strict] [--parity]";
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.failWithUsage = failWithUsage;
|
|
4
|
+
exports.printValidationResult = printValidationResult;
|
|
5
|
+
function failWithUsage(message, usage) {
|
|
6
|
+
console.error(message);
|
|
7
|
+
console.error(usage);
|
|
8
|
+
return 1;
|
|
9
|
+
}
|
|
10
|
+
function printValidationResult(validation) {
|
|
11
|
+
if (validation.status === "passed") {
|
|
12
|
+
console.log(`Validation passed: ${validation.message}`);
|
|
13
|
+
return;
|
|
14
|
+
}
|
|
15
|
+
console.error(`Validation failed: ${validation.message}`);
|
|
16
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.normalizeSkillName = normalizeSkillName;
|
|
4
4
|
exports.getMaxSkillNameLength = getMaxSkillNameLength;
|
|
5
|
-
const
|
|
5
|
+
const skill_spec_1 = require("./skill-spec");
|
|
6
6
|
function normalizeSkillName(input) {
|
|
7
7
|
const normalized = input
|
|
8
8
|
.trim()
|
|
@@ -13,8 +13,8 @@ function normalizeSkillName(input) {
|
|
|
13
13
|
if (normalized.length === 0) {
|
|
14
14
|
throw new Error("skill name is empty after normalization; use letters/numbers and optional hyphens");
|
|
15
15
|
}
|
|
16
|
-
if (normalized.length > MAX_SKILL_NAME_LENGTH) {
|
|
17
|
-
throw new Error(`skill name must be at most ${MAX_SKILL_NAME_LENGTH} characters`);
|
|
16
|
+
if (normalized.length > skill_spec_1.MAX_SKILL_NAME_LENGTH) {
|
|
17
|
+
throw new Error(`skill name must be at most ${skill_spec_1.MAX_SKILL_NAME_LENGTH} characters`);
|
|
18
18
|
}
|
|
19
19
|
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(normalized)) {
|
|
20
20
|
throw new Error("skill name must use lowercase letters, numbers, and single hyphens only");
|
|
@@ -22,5 +22,5 @@ function normalizeSkillName(input) {
|
|
|
22
22
|
return normalized;
|
|
23
23
|
}
|
|
24
24
|
function getMaxSkillNameLength() {
|
|
25
|
-
return MAX_SKILL_NAME_LENGTH;
|
|
25
|
+
return skill_spec_1.MAX_SKILL_NAME_LENGTH;
|
|
26
26
|
}
|
package/dist/lib/scaffold.js
CHANGED
|
@@ -4,6 +4,7 @@ exports.scaffoldSkillInDirectory = scaffoldSkillInDirectory;
|
|
|
4
4
|
const node_fs_1 = require("node:fs");
|
|
5
5
|
const node_path_1 = require("node:path");
|
|
6
6
|
const normalize_name_1 = require("./normalize-name");
|
|
7
|
+
const skill_spec_1 = require("./skill-spec");
|
|
7
8
|
const templates_1 = require("./templates");
|
|
8
9
|
function assertDirectoryEmpty(targetDir) {
|
|
9
10
|
const entries = (0, node_fs_1.readdirSync)(targetDir);
|
|
@@ -22,8 +23,7 @@ function assertDirectoryNameMatchesNormalized(targetDir) {
|
|
|
22
23
|
function scaffoldSkillInDirectory(targetDir) {
|
|
23
24
|
const skillName = assertDirectoryNameMatchesNormalized(targetDir);
|
|
24
25
|
assertDirectoryEmpty(targetDir);
|
|
25
|
-
const
|
|
26
|
-
for (const directory of directories) {
|
|
26
|
+
for (const directory of skill_spec_1.SCAFFOLD_DIRECTORIES) {
|
|
27
27
|
const fullPath = (0, node_path_1.join)(targetDir, directory);
|
|
28
28
|
(0, node_fs_1.mkdirSync)(fullPath, { recursive: true });
|
|
29
29
|
(0, node_fs_1.writeFileSync)((0, node_path_1.join)(fullPath, ".gitkeep"), "", "utf8");
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.STRICT_SECTION_HEADINGS = exports.STRICT_SECTION_TITLES = exports.STRICT_REQUIRED_FILES = exports.SCAFFOLD_DIRECTORIES = exports.MAX_SKILL_NAME_LENGTH = void 0;
|
|
4
|
+
exports.MAX_SKILL_NAME_LENGTH = 64;
|
|
5
|
+
exports.SCAFFOLD_DIRECTORIES = ["scripts", "references", "assets"];
|
|
6
|
+
exports.STRICT_REQUIRED_FILES = [
|
|
7
|
+
".gitignore",
|
|
8
|
+
...exports.SCAFFOLD_DIRECTORIES.map((directory) => `${directory}/.gitkeep`),
|
|
9
|
+
];
|
|
10
|
+
exports.STRICT_SECTION_TITLES = [
|
|
11
|
+
"Scope",
|
|
12
|
+
"When to use",
|
|
13
|
+
"Inputs",
|
|
14
|
+
"Outputs",
|
|
15
|
+
"Steps / Procedure",
|
|
16
|
+
"Examples",
|
|
17
|
+
"Limitations / Failure modes",
|
|
18
|
+
"Security / Tool access",
|
|
19
|
+
];
|
|
20
|
+
exports.STRICT_SECTION_HEADINGS = exports.STRICT_SECTION_TITLES.map((title) => `## ${title}`);
|
package/dist/lib/templates.js
CHANGED
|
@@ -2,36 +2,16 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.buildSkillMarkdown = buildSkillMarkdown;
|
|
4
4
|
exports.buildGitignore = buildGitignore;
|
|
5
|
+
const skill_spec_1 = require("./skill-spec");
|
|
5
6
|
function buildSkillMarkdown(name) {
|
|
7
|
+
const sections = skill_spec_1.STRICT_SECTION_TITLES.map((title) => `## ${title}\nTODO`).join("\n\n");
|
|
6
8
|
return `---
|
|
7
9
|
name: ${name}
|
|
8
10
|
description: "TODO: Describe what this skill does and when to use it."
|
|
9
11
|
license: TODO
|
|
10
12
|
---
|
|
11
13
|
|
|
12
|
-
|
|
13
|
-
TODO
|
|
14
|
-
|
|
15
|
-
## When to use
|
|
16
|
-
TODO
|
|
17
|
-
|
|
18
|
-
## Inputs
|
|
19
|
-
TODO
|
|
20
|
-
|
|
21
|
-
## Outputs
|
|
22
|
-
TODO
|
|
23
|
-
|
|
24
|
-
## Steps / Procedure
|
|
25
|
-
TODO
|
|
26
|
-
|
|
27
|
-
## Examples
|
|
28
|
-
TODO
|
|
29
|
-
|
|
30
|
-
## Limitations / Failure modes
|
|
31
|
-
TODO
|
|
32
|
-
|
|
33
|
-
## Security / Tool access
|
|
34
|
-
TODO
|
|
14
|
+
${sections}
|
|
35
15
|
`;
|
|
36
16
|
}
|
|
37
17
|
function buildGitignore() {
|
package/dist/lib/validator.js
CHANGED
|
@@ -4,98 +4,78 @@ exports.validateSkill = validateSkill;
|
|
|
4
4
|
const node_fs_1 = require("node:fs");
|
|
5
5
|
const node_path_1 = require("node:path");
|
|
6
6
|
const yaml_1 = require("yaml");
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
"scripts/.gitkeep",
|
|
10
|
-
"references/.gitkeep",
|
|
11
|
-
"assets/.gitkeep",
|
|
12
|
-
];
|
|
13
|
-
const REQUIRED_SECTIONS = [
|
|
14
|
-
"## Scope",
|
|
15
|
-
"## When to use",
|
|
16
|
-
"## Inputs",
|
|
17
|
-
"## Outputs",
|
|
18
|
-
"## Steps / Procedure",
|
|
19
|
-
"## Examples",
|
|
20
|
-
"## Limitations / Failure modes",
|
|
21
|
-
"## Security / Tool access",
|
|
22
|
-
];
|
|
7
|
+
const skill_spec_1 = require("./skill-spec");
|
|
8
|
+
const SKILL_FILE = "SKILL.md";
|
|
23
9
|
function stripUtf8Bom(content) {
|
|
24
10
|
return content.replace(/^\uFEFF/, "");
|
|
25
11
|
}
|
|
12
|
+
function readSkillContentIfExists(targetDir) {
|
|
13
|
+
const skillPath = (0, node_path_1.join)(targetDir, SKILL_FILE);
|
|
14
|
+
return (0, node_fs_1.existsSync)(skillPath) ? (0, node_fs_1.readFileSync)(skillPath, "utf8") : null;
|
|
15
|
+
}
|
|
26
16
|
function extractFrontmatter(content) {
|
|
27
17
|
const normalizedContent = stripUtf8Bom(content);
|
|
28
18
|
const match = normalizedContent.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/);
|
|
29
19
|
if (!match || typeof match[1] !== "string") {
|
|
30
20
|
return null;
|
|
31
21
|
}
|
|
32
|
-
let parsed;
|
|
33
22
|
try {
|
|
34
|
-
parsed = (0, yaml_1.parse)(match[1]);
|
|
23
|
+
const parsed = (0, yaml_1.parse)(match[1]);
|
|
24
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
return { frontmatter: parsed };
|
|
35
28
|
}
|
|
36
29
|
catch {
|
|
37
30
|
return null;
|
|
38
31
|
}
|
|
39
|
-
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
return {
|
|
43
|
-
frontmatter: parsed,
|
|
44
|
-
};
|
|
45
32
|
}
|
|
46
33
|
function isValidSkillName(name) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return /^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name);
|
|
34
|
+
return (name.length > 0 &&
|
|
35
|
+
name.length <= skill_spec_1.MAX_SKILL_NAME_LENGTH &&
|
|
36
|
+
/^[a-z0-9]+(?:-[a-z0-9]+)*$/.test(name));
|
|
51
37
|
}
|
|
52
38
|
function isStringMap(value) {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
39
|
+
return (!!value &&
|
|
40
|
+
typeof value === "object" &&
|
|
41
|
+
!Array.isArray(value) &&
|
|
42
|
+
Object.values(value).every((entry) => typeof entry === "string"));
|
|
57
43
|
}
|
|
58
44
|
function collectSpecErrors(targetDir) {
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
errors.push("missing required file: SKILL.md");
|
|
63
|
-
return errors;
|
|
45
|
+
const skillContent = readSkillContentIfExists(targetDir);
|
|
46
|
+
if (!skillContent) {
|
|
47
|
+
return [`missing required file: ${SKILL_FILE}`];
|
|
64
48
|
}
|
|
65
|
-
const skillContent = (0, node_fs_1.readFileSync)(skillPath, "utf8");
|
|
66
49
|
const parsedSkill = extractFrontmatter(skillContent);
|
|
67
50
|
if (!parsedSkill) {
|
|
68
|
-
|
|
69
|
-
return errors;
|
|
51
|
+
return ["SKILL.md must start with valid YAML frontmatter"];
|
|
70
52
|
}
|
|
53
|
+
const errors = [];
|
|
71
54
|
const { frontmatter } = parsedSkill;
|
|
72
|
-
const name = frontmatter.name;
|
|
73
|
-
const description = frontmatter.description;
|
|
74
|
-
const license = frontmatter.license;
|
|
75
|
-
const compatibility = frontmatter.compatibility;
|
|
76
|
-
const metadata = frontmatter.metadata;
|
|
77
|
-
const allowedTools = frontmatter["allowed-tools"];
|
|
78
55
|
const directoryName = (0, node_path_1.basename)(targetDir);
|
|
56
|
+
const name = frontmatter.name;
|
|
79
57
|
if (typeof name !== "string" || name.length === 0) {
|
|
80
58
|
errors.push("frontmatter is missing required string field: name");
|
|
81
59
|
}
|
|
82
60
|
else {
|
|
83
61
|
if (!isValidSkillName(name)) {
|
|
84
|
-
errors.push(
|
|
62
|
+
errors.push(`frontmatter 'name' must be 1-${skill_spec_1.MAX_SKILL_NAME_LENGTH} chars using lowercase letters, numbers, and single hyphens`);
|
|
85
63
|
}
|
|
86
64
|
if (name !== directoryName) {
|
|
87
65
|
errors.push(`frontmatter 'name' (${name}) must match directory name (${directoryName})`);
|
|
88
66
|
}
|
|
89
67
|
}
|
|
68
|
+
const description = frontmatter.description;
|
|
90
69
|
if (typeof description !== "string" || description.trim().length === 0) {
|
|
91
70
|
errors.push("frontmatter is missing required non-empty string field: description");
|
|
92
71
|
}
|
|
93
72
|
else if (description.length > 1024) {
|
|
94
73
|
errors.push("frontmatter 'description' must be at most 1024 characters");
|
|
95
74
|
}
|
|
96
|
-
if (license !== undefined && typeof license !== "string") {
|
|
75
|
+
if (frontmatter.license !== undefined && typeof frontmatter.license !== "string") {
|
|
97
76
|
errors.push("frontmatter 'license' must be a string when provided");
|
|
98
77
|
}
|
|
78
|
+
const compatibility = frontmatter.compatibility;
|
|
99
79
|
if (compatibility !== undefined) {
|
|
100
80
|
if (typeof compatibility !== "string") {
|
|
101
81
|
errors.push("frontmatter 'compatibility' must be a string when provided");
|
|
@@ -104,54 +84,35 @@ function collectSpecErrors(targetDir) {
|
|
|
104
84
|
errors.push("frontmatter 'compatibility' must be 1-500 characters");
|
|
105
85
|
}
|
|
106
86
|
}
|
|
107
|
-
if (metadata !== undefined && !isStringMap(metadata)) {
|
|
87
|
+
if (frontmatter.metadata !== undefined && !isStringMap(frontmatter.metadata)) {
|
|
108
88
|
errors.push("frontmatter 'metadata' must be a mapping of string keys to string values");
|
|
109
89
|
}
|
|
90
|
+
const allowedTools = frontmatter["allowed-tools"];
|
|
110
91
|
if (allowedTools !== undefined && typeof allowedTools !== "string") {
|
|
111
92
|
errors.push("frontmatter 'allowed-tools' must be a string when provided");
|
|
112
93
|
}
|
|
113
94
|
return errors;
|
|
114
95
|
}
|
|
115
96
|
function collectStrictScaffoldErrors(targetDir) {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
const skillPath = (0, node_path_1.join)(targetDir, "SKILL.md");
|
|
123
|
-
if (!(0, node_fs_1.existsSync)(skillPath)) {
|
|
124
|
-
return errors;
|
|
125
|
-
}
|
|
126
|
-
const skillContent = (0, node_fs_1.readFileSync)(skillPath, "utf8");
|
|
127
|
-
for (const section of REQUIRED_SECTIONS) {
|
|
128
|
-
if (!hasHeadingOutsideFencedCode(skillContent, section)) {
|
|
129
|
-
errors.push(`SKILL.md is missing strict section: ${section}`);
|
|
130
|
-
}
|
|
97
|
+
const missingStrictFiles = skill_spec_1.STRICT_REQUIRED_FILES.filter((requiredFile) => !(0, node_fs_1.existsSync)((0, node_path_1.join)(targetDir, requiredFile))).map((requiredFile) => `missing strict scaffold file: ${requiredFile}`);
|
|
98
|
+
const skillContent = readSkillContentIfExists(targetDir);
|
|
99
|
+
if (!skillContent) {
|
|
100
|
+
return missingStrictFiles;
|
|
131
101
|
}
|
|
132
|
-
|
|
102
|
+
const missingStrictSections = skill_spec_1.STRICT_SECTION_HEADINGS.filter((section) => !hasHeadingOutsideFencedCode(skillContent, section)).map((section) => `SKILL.md is missing strict section: ${section}`);
|
|
103
|
+
return [...missingStrictFiles, ...missingStrictSections];
|
|
133
104
|
}
|
|
134
105
|
function hasHeadingOutsideFencedCode(content, heading) {
|
|
135
|
-
const lines = content.split(/\r?\n/);
|
|
136
106
|
const headingPattern = new RegExp(`^\\s{0,3}${escapeRegExp(heading)}\\s*$`);
|
|
137
107
|
let activeFence = null;
|
|
138
|
-
for (const line of
|
|
108
|
+
for (const line of content.split(/\r?\n/)) {
|
|
139
109
|
const trimmed = line.trim();
|
|
140
110
|
if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
|
|
141
111
|
const fence = trimmed.startsWith("```") ? "```" : "~~~";
|
|
142
|
-
|
|
143
|
-
activeFence = fence;
|
|
144
|
-
continue;
|
|
145
|
-
}
|
|
146
|
-
if (activeFence === fence) {
|
|
147
|
-
activeFence = null;
|
|
148
|
-
continue;
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
if (activeFence) {
|
|
112
|
+
activeFence = activeFence === fence ? null : (activeFence ?? fence);
|
|
152
113
|
continue;
|
|
153
114
|
}
|
|
154
|
-
if (headingPattern.test(line.replace(/\r$/, ""))) {
|
|
115
|
+
if (!activeFence && headingPattern.test(line.replace(/\r$/, ""))) {
|
|
155
116
|
return true;
|
|
156
117
|
}
|
|
157
118
|
}
|