feat: enhance CLI validation for skill version and reputation threshold; update documentation

This commit is contained in:
David Abutbul
2026-02-16 21:20:49 +02:00
parent 45386225eb
commit 6d6bb6a6e2
5 changed files with 104 additions and 5 deletions
-1
View File
@@ -89,7 +89,6 @@ node scripts/check_clawhub_reputation.mjs some-skill 1.0.0 70
Environment variables:
- `CLAWHUB_REPUTATION_THRESHOLD` - Minimum score (0-100, default: 70)
- `CLAWHUB_ALLOW_SUSPICIOUS` - Skip reputation checks (not recommended)
## Integration Points
-2
View File
@@ -81,8 +81,6 @@ The enhanced flow:
Environment variables:
- `CLAWHUB_REPUTATION_THRESHOLD` - Minimum reputation score (0-100, default: 70)
- `CLAWHUB_ALLOW_SUSPICIOUS` - Allow installation of suspicious skills without confirmation (default: false)
- `CLAWHUB_VIRUSTOTAL_API_KEY` - Optional: Your own VirusTotal API key for deeper scans
## Integration with Existing Suite
@@ -1,6 +1,8 @@
#!/usr/bin/env node
import { spawnSync } from "node:child_process";
import path from "node:path";
import { pathToFileURL } from "node:url";
/**
* Check ClawHub reputation for a skill
@@ -182,7 +184,11 @@ export async function checkClawhubReputation(skillSlug, version, threshold = 70)
}
// CLI interface for direct usage
if (import.meta.url === `file://${process.argv[1]}`) {
const isCliEntrypoint =
process.argv[1] !== undefined &&
import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href;
if (isCliEntrypoint) {
async function main() {
const args = process.argv.slice(2);
if (args.length < 1) {
@@ -192,7 +198,17 @@ if (import.meta.url === `file://${process.argv[1]}`) {
const skillSlug = args[0];
const version = args[1] || "";
const threshold = args[2] ? parseInt(args[2], 10) : 70;
let threshold = 70;
if (args[2] !== undefined) {
const parsedThreshold = parseInt(args[2], 10);
if (!Number.isInteger(parsedThreshold) || parsedThreshold < 0 || parsedThreshold > 100) {
console.error(
`Invalid threshold: "${args[2]}". Threshold must be an integer between 0 and 100.`
);
process.exit(1);
}
threshold = parsedThreshold;
}
const result = await checkClawhubReputation(skillSlug, version, threshold);
@@ -99,6 +99,11 @@ function parseArgs(argv) {
if (!/^[a-z0-9][a-z0-9-]*$/.test(parsed.skill)) {
throw new Error("Invalid --skill value. Must start with a letter or digit, followed by lowercase letters, digits, and hyphens.");
}
if (parsed.version && !/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.-]+)?(?:\+[a-zA-Z0-9.-]+)?$/.test(parsed.version)) {
throw new Error(
"Invalid --version value. Must be semantic version format (e.g., 1.2.3, 1.2.3-beta.1, 1.2.3+build.45)."
);
}
if (parsed.reputationThreshold < 0 || parsed.reputationThreshold > 100 || Number.isNaN(parsed.reputationThreshold)) {
throw new Error("Invalid --reputation-threshold value. Must be between 0 and 100.");
}
@@ -208,6 +208,61 @@ async function testPreReleaseVersionAccepted() {
}
}
// -----------------------------------------------------------------------------
// Test: CLI entrypoint guard works when script path is relative
// -----------------------------------------------------------------------------
async function testRelativePathCliEntrypointWorks() {
const testName = "reputation_check: CLI entrypoint works with relative script path";
try {
const relativeCheckerScript = path.relative(process.cwd(), CHECKER_SCRIPT);
const result = await runScript(relativeCheckerScript, ['bad slug', '', '70']);
let parsed;
try {
parsed = JSON.parse(result.stdout);
} catch {
fail(testName, `Could not parse output with relative script path: ${result.stdout}`);
return;
}
if (
result.code === 43 &&
parsed.safe === false &&
parsed.warnings.some((w) => w.includes("Invalid skill slug"))
) {
pass(testName);
} else {
fail(
testName,
`Expected exit 43 with invalid slug warning via relative path, got code ${result.code}: ${JSON.stringify(parsed)}`
);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Invalid threshold format is rejected in CLI mode
// -----------------------------------------------------------------------------
async function testInvalidThresholdRejected() {
const testName = "reputation_check: invalid threshold is rejected";
try {
const result = await runScript(CHECKER_SCRIPT, ['test-skill', '1.0.0', 'abc']);
if (result.code === 1 && result.stderr.includes("Invalid threshold")) {
pass(testName);
} else {
fail(
testName,
`Expected exit 1 with invalid threshold message, got code ${result.code}: ${result.stderr}`
);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Test: Enhanced installer rejects invalid skill name
// -----------------------------------------------------------------------------
@@ -321,6 +376,29 @@ async function testFormatReputationWarningNull() {
}
}
// -----------------------------------------------------------------------------
// Test: Enhanced installer validates --version even with --confirm-reputation
// -----------------------------------------------------------------------------
async function testEnhancedInstallerRejectsInvalidVersion() {
const testName = "enhanced_install: rejects invalid version format even with --confirm-reputation";
try {
const result = await runScript(ENHANCED_INSTALL_SCRIPT, [
'--skill', 'test-skill', '--version', '1.0.0;rm -rf /', '--confirm-reputation'
]);
if (result.code === 1 && result.stderr.includes("Invalid --version value")) {
pass(testName);
} else {
fail(
testName,
`Expected exit 1 with invalid version message, got code ${result.code}: ${result.stderr}`
);
}
} catch (error) {
fail(testName, error);
}
}
// -----------------------------------------------------------------------------
// Main test runner
// -----------------------------------------------------------------------------
@@ -333,8 +411,11 @@ async function runTests() {
await testUppercaseSlugRejected();
await testEmptySlugShowsUsage();
await testPreReleaseVersionAccepted();
await testRelativePathCliEntrypointWorks();
await testInvalidThresholdRejected();
await testEnhancedInstallerRejectsInvalidSkill();
await testEnhancedInstallerRequiresSkill();
await testEnhancedInstallerRejectsInvalidVersion();
await testEnhancedInstallerRejectsInvalidThreshold();
await testFormatReputationWarning();
await testFormatReputationWarningNull();