@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.
@@ -1,6 +1,6 @@
1
1
  import { readConfig } from "../lib/config.js";
2
2
  import { readFileSync } from "node:fs";
3
- import { validateSkillForPublishing } from "@chvor/shared";
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
+ }
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Template types inlined from @chvor/shared for standalone npm distribution.
3
+ */
4
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chvor/cli",
3
- "version": "0.1.0",
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"