@chvor/cli 0.1.0 → 0.1.1
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/commands/skill.js +1 -1
- package/dist/lib/validate-skill.js +119 -0
- package/dist/types/template.js +4 -0
- package/package.json +1 -2
package/dist/commands/skill.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readConfig } from "../lib/config.js";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import { validateSkillForPublishing } from "
|
|
3
|
+
import { validateSkillForPublishing } from "../lib/validate-skill.js";
|
|
4
4
|
function getBaseUrl() {
|
|
5
5
|
const config = readConfig();
|
|
6
6
|
return `http://localhost:${config.port}`;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Validates a skill markdown file for publishing to the registry.
|
|
3
|
+
* Inlined from @chvor/shared for standalone npm distribution.
|
|
4
|
+
*/
|
|
5
|
+
const SEMVER_RE = /^\d+\.\d+\.\d+$/;
|
|
6
|
+
const REQUIRED_FIELDS = ["name", "description", "version", "author"];
|
|
7
|
+
const MAX_SIZE_BYTES = 50_000;
|
|
8
|
+
// Patterns that suggest secrets or API keys
|
|
9
|
+
const SECRET_PATTERNS = [
|
|
10
|
+
/sk-[a-zA-Z0-9]{20,}/,
|
|
11
|
+
/AKIA[0-9A-Z]{16}/,
|
|
12
|
+
/ghp_[a-zA-Z0-9]{36}/,
|
|
13
|
+
/-----BEGIN (RSA |EC )?PRIVATE KEY-----/,
|
|
14
|
+
];
|
|
15
|
+
/**
|
|
16
|
+
* Parses YAML frontmatter from markdown content.
|
|
17
|
+
* Lightweight parser — does not depend on gray-matter so it can run in CLI/browser.
|
|
18
|
+
* Handles: scalar values, inline arrays [a, b], and multi-line list items (- item).
|
|
19
|
+
*/
|
|
20
|
+
function parseFrontmatter(content) {
|
|
21
|
+
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
|
|
22
|
+
if (!match)
|
|
23
|
+
return null;
|
|
24
|
+
const fm = {};
|
|
25
|
+
const lines = match[1].split("\n");
|
|
26
|
+
let currentKey = null;
|
|
27
|
+
let currentList = null;
|
|
28
|
+
for (const line of lines) {
|
|
29
|
+
// Indented list item (e.g., " - value")
|
|
30
|
+
const listItemMatch = line.match(/^\s+-\s+(.+)/);
|
|
31
|
+
if (listItemMatch && currentKey) {
|
|
32
|
+
if (!currentList)
|
|
33
|
+
currentList = [];
|
|
34
|
+
currentList.push(listItemMatch[1].trim().replace(/^["']|["']$/g, ""));
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
// Flush any pending list to the current key
|
|
38
|
+
if (currentKey && currentList) {
|
|
39
|
+
fm[currentKey] = currentList;
|
|
40
|
+
currentList = null;
|
|
41
|
+
currentKey = null;
|
|
42
|
+
}
|
|
43
|
+
const colonIdx = line.indexOf(":");
|
|
44
|
+
if (colonIdx === -1)
|
|
45
|
+
continue;
|
|
46
|
+
// Skip indented keys (nested objects like requires.credentials)
|
|
47
|
+
if (line.match(/^\s+\S/))
|
|
48
|
+
continue;
|
|
49
|
+
const key = line.slice(0, colonIdx).trim();
|
|
50
|
+
const rawValue = line.slice(colonIdx + 1).trim();
|
|
51
|
+
if (!key)
|
|
52
|
+
continue;
|
|
53
|
+
// Inline array: [a, b, c]
|
|
54
|
+
const inlineArrayMatch = rawValue.match(/^\[(.+)\]$/);
|
|
55
|
+
if (inlineArrayMatch) {
|
|
56
|
+
fm[key] = inlineArrayMatch[1].split(",").map((v) => v.trim().replace(/^["']|["']$/g, ""));
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (rawValue) {
|
|
60
|
+
fm[key] = rawValue.replace(/^["']|["']$/g, "");
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
// Empty value — might be followed by list items
|
|
64
|
+
currentKey = key;
|
|
65
|
+
currentList = null;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
// Flush final pending list
|
|
69
|
+
if (currentKey && currentList) {
|
|
70
|
+
fm[currentKey] = currentList;
|
|
71
|
+
}
|
|
72
|
+
return fm;
|
|
73
|
+
}
|
|
74
|
+
export function validateSkillForPublishing(content) {
|
|
75
|
+
const errors = [];
|
|
76
|
+
const warnings = [];
|
|
77
|
+
// Size check
|
|
78
|
+
const sizeBytes = new TextEncoder().encode(content).length;
|
|
79
|
+
if (sizeBytes > MAX_SIZE_BYTES) {
|
|
80
|
+
errors.push(`File too large: ${sizeBytes} bytes (max ${MAX_SIZE_BYTES})`);
|
|
81
|
+
}
|
|
82
|
+
// Frontmatter
|
|
83
|
+
const fm = parseFrontmatter(content);
|
|
84
|
+
if (!fm) {
|
|
85
|
+
errors.push("Missing YAML frontmatter (--- block)");
|
|
86
|
+
return { valid: false, errors, warnings };
|
|
87
|
+
}
|
|
88
|
+
// Required fields
|
|
89
|
+
for (const field of REQUIRED_FIELDS) {
|
|
90
|
+
if (!fm[field]) {
|
|
91
|
+
errors.push(`Missing required field: ${field}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
// Version must be valid semver
|
|
95
|
+
if (fm.version && !SEMVER_RE.test(String(fm.version))) {
|
|
96
|
+
errors.push(`Invalid version "${fm.version}" — must be semver (e.g. 1.0.0)`);
|
|
97
|
+
}
|
|
98
|
+
// Check for secrets in content
|
|
99
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
100
|
+
if (pattern.test(content)) {
|
|
101
|
+
errors.push("Content appears to contain a secret or API key");
|
|
102
|
+
break;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// Body must exist
|
|
106
|
+
const bodyMatch = content.match(/^---[\s\S]*?---\r?\n([\s\S]*)$/);
|
|
107
|
+
const body = bodyMatch?.[1]?.trim();
|
|
108
|
+
if (!body) {
|
|
109
|
+
warnings.push("Skill has no instructions body after frontmatter");
|
|
110
|
+
}
|
|
111
|
+
// Optional warnings
|
|
112
|
+
if (!fm.category) {
|
|
113
|
+
warnings.push("No category specified — skill may be harder to discover");
|
|
114
|
+
}
|
|
115
|
+
if (!fm.tags) {
|
|
116
|
+
warnings.push("No tags specified — consider adding tags for discoverability");
|
|
117
|
+
}
|
|
118
|
+
return { valid: errors.length === 0, errors, warnings };
|
|
119
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chvor/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"description": "Your own AI — install and run chvor.",
|
|
5
5
|
"license": "SEE LICENSE IN LICENSE.md",
|
|
6
6
|
"type": "module",
|
|
@@ -20,7 +20,6 @@
|
|
|
20
20
|
"typecheck": "tsc --noEmit"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@chvor/shared": "workspace:*",
|
|
24
23
|
"commander": "^13",
|
|
25
24
|
"@inquirer/prompts": "^7",
|
|
26
25
|
"yaml": "^2"
|