@skillmarkdown/cli 0.1.3 → 0.1.4

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/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("Usage: skillmd <init|validate>");
16
+ console.error(cli_text_1.ROOT_USAGE);
11
17
  process.exitCode = 1;
12
18
  return;
13
19
  }
14
- if (args[0] === "init") {
15
- process.exitCode = (0, init_1.runInitCommand)(args.slice(1));
20
+ const handler = COMMAND_HANDLERS[command];
21
+ if (handler) {
22
+ process.exitCode = handler(args.slice(1));
16
23
  return;
17
24
  }
18
- if (args[0] === "validate") {
19
- process.exitCode = (0, validate_1.runValidateCommand)(args.slice(1));
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();
@@ -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,9 +11,7 @@ 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
- console.error("skillmd init: unsupported argument(s)");
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);
@@ -21,11 +21,10 @@ function runInitCommand(args, options = {}) {
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 validator_1 = require("../lib/validator");
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
- console.error(`skillmd validate: unsupported flag '${arg}'`);
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
- console.error("skillmd validate: accepts at most one path argument");
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 MAX_SKILL_NAME_LENGTH = 64;
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
  }
@@ -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 directories = ["scripts", "references", "assets"];
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}`);
@@ -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
- ## Scope
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() {
@@ -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 STRICT_REQUIRED_FILES = [
8
- ".gitignore",
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
- if (name.length === 0 || name.length > 64) {
48
- return false;
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
- if (!value || typeof value !== "object" || Array.isArray(value)) {
54
- return false;
55
- }
56
- return Object.values(value).every((entry) => typeof entry === "string");
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 errors = [];
60
- const skillPath = (0, node_path_1.join)(targetDir, "SKILL.md");
61
- if (!(0, node_fs_1.existsSync)(skillPath)) {
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
- errors.push("SKILL.md must start with valid YAML frontmatter");
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("frontmatter 'name' must be 1-64 chars using lowercase letters, numbers, and single hyphens");
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 errors = [];
117
- for (const requiredFile of STRICT_REQUIRED_FILES) {
118
- if (!(0, node_fs_1.existsSync)((0, node_path_1.join)(targetDir, requiredFile))) {
119
- errors.push(`missing strict scaffold file: ${requiredFile}`);
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
- return errors;
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 lines) {
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
- if (activeFence === null) {
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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@skillmarkdown/cli",
3
- "version": "0.1.3",
3
+ "version": "0.1.4",
4
4
  "description": "CLI for scaffolding SKILL.md-based AI skills",
5
5
  "license": "MIT",
6
6
  "type": "commonjs",