fix: Address high-severity bandit issues

This commit is contained in:
DSS
2025-12-11 07:13:06 -03:00
parent bcb4475744
commit 5b2a328dd1
167 changed files with 7051 additions and 7168 deletions

View File

@@ -55,7 +55,7 @@ function countProps(content) {
function countNestingDepth(content) {
let maxDepth = 0;
let currentDepth = 0;
for (const char of content) {
if (char === '{' || char === '(') {
currentDepth++;
@@ -64,7 +64,7 @@ function countNestingDepth(content) {
currentDepth = Math.max(0, currentDepth - 1);
}
}
return maxDepth;
}
@@ -74,7 +74,7 @@ function countFunctions(content) {
/const\s+\w+\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/g,
/const\s+\w+\s*=\s*(?:async\s*)?function/g
];
let count = 0;
for (const pattern of patterns) {
const matches = content.match(pattern);
@@ -87,17 +87,17 @@ function analyzeComplexity(content, filePath, config) {
const issues = [];
const monitorConfig = config.complexity_monitor || {};
const ext = path.extname(filePath).toLowerCase();
// Only analyze JS/TS files
if (!['.js', '.jsx', '.ts', '.tsx'].includes(ext)) {
return issues;
}
const lines = countLines(content);
const props = countProps(content);
const nesting = countNestingDepth(content);
const functions = countFunctions(content);
// Check component size (for tsx/jsx files)
if (['.tsx', '.jsx'].includes(ext)) {
if (lines > monitorConfig.max_component_lines) {
@@ -108,7 +108,7 @@ function analyzeComplexity(content, filePath, config) {
suggestion: 'Consider breaking into smaller components'
});
}
if (props > monitorConfig.max_props) {
issues.push({
type: 'prop_count',
@@ -118,7 +118,7 @@ function analyzeComplexity(content, filePath, config) {
});
}
}
// Check nesting depth
if (nesting > monitorConfig.max_nesting_depth) {
issues.push({
@@ -128,7 +128,7 @@ function analyzeComplexity(content, filePath, config) {
suggestion: 'Extract nested logic into separate functions'
});
}
// Check function count (indicator of file doing too much)
if (functions > 10) {
issues.push({
@@ -138,38 +138,38 @@ function analyzeComplexity(content, filePath, config) {
suggestion: 'Consider splitting into multiple modules'
});
}
return issues;
}
function formatOutput(issues, filePath) {
if (issues.length === 0) return '';
const severityIcons = {
high: '[HIGH]',
medium: '[MED]',
low: '[LOW]'
};
const lines = [`\n=== DSS Complexity Monitor: ${filePath} ===\n`];
for (const issue of issues) {
const icon = severityIcons[issue.severity] || '[?]';
lines.push(`${icon} ${issue.message}`);
lines.push(` Suggestion: ${issue.suggestion}\n`);
}
lines.push('='.repeat(50));
return lines.join('\n');
}
async function main() {
const config = loadConfig();
if (!config.complexity_monitor?.enabled) {
process.exit(0);
}
// Read input from stdin
let inputData;
try {
@@ -181,34 +181,34 @@ async function main() {
} catch (e) {
process.exit(0);
}
const toolName = inputData.tool_name || '';
const toolInput = inputData.tool_input || {};
if (!['Edit', 'Write'].includes(toolName)) {
process.exit(0);
}
const filePath = toolInput.file_path || '';
let content = '';
if (toolName === 'Write') {
content = toolInput.content || '';
} else if (toolName === 'Edit') {
content = toolInput.new_string || '';
}
if (!content || !filePath) {
process.exit(0);
}
const issues = analyzeComplexity(content, filePath, config);
if (issues.length > 0) {
const output = formatOutput(issues, filePath);
console.error(output);
}
process.exit(0);
}

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python3
"""
DSS Component Checker Hook
DSS Component Checker Hook.
Validates React components for best practices and accessibility.
Written from scratch for DSS.
"""
@@ -19,7 +20,7 @@ COMPONENT_PATTERNS = [
"category": "accessibility",
"severity": "high",
"message": "Missing alt attribute on <img>. Add alt text for accessibility.",
"file_types": [".jsx", ".tsx"]
"file_types": [".jsx", ".tsx"],
},
{
"id": "a11y-button-type",
@@ -27,7 +28,7 @@ COMPONENT_PATTERNS = [
"category": "accessibility",
"severity": "medium",
"message": "Button missing type attribute. Add type='button' or type='submit'.",
"file_types": [".jsx", ".tsx"]
"file_types": [".jsx", ".tsx"],
},
{
"id": "a11y-anchor-href",
@@ -35,7 +36,7 @@ COMPONENT_PATTERNS = [
"category": "accessibility",
"severity": "high",
"message": "Anchor tag missing href. Use button for actions without navigation.",
"file_types": [".jsx", ".tsx"]
"file_types": [".jsx", ".tsx"],
},
{
"id": "a11y-click-handler",
@@ -43,7 +44,7 @@ COMPONENT_PATTERNS = [
"category": "accessibility",
"severity": "medium",
"message": "Click handler on non-interactive element. Use <button> or add role/tabIndex.",
"file_types": [".jsx", ".tsx"]
"file_types": [".jsx", ".tsx"],
},
{
"id": "a11y-form-label",
@@ -51,7 +52,7 @@ COMPONENT_PATTERNS = [
"category": "accessibility",
"severity": "medium",
"message": "Input may be missing label association. Add id with <label> or aria-label.",
"file_types": [".jsx", ".tsx"]
"file_types": [".jsx", ".tsx"],
},
# React best practices
{
@@ -60,7 +61,7 @@ COMPONENT_PATTERNS = [
"category": "react",
"severity": "medium",
"message": "Using array index as key. Use unique, stable IDs when possible.",
"file_types": [".jsx", ".tsx"]
"file_types": [".jsx", ".tsx"],
},
{
"id": "react-bind-render",
@@ -68,7 +69,7 @@ COMPONENT_PATTERNS = [
"category": "react",
"severity": "low",
"message": "Binding in render creates new function each time. Use arrow function or bind in constructor.",
"file_types": [".jsx", ".tsx"]
"file_types": [".jsx", ".tsx"],
},
{
"id": "react-inline-style-object",
@@ -76,7 +77,7 @@ COMPONENT_PATTERNS = [
"category": "react",
"severity": "low",
"message": "Large inline style object. Consider extracting to a constant or CSS module.",
"file_types": [".jsx", ".tsx"]
"file_types": [".jsx", ".tsx"],
},
{
"id": "react-console-log",
@@ -84,7 +85,7 @@ COMPONENT_PATTERNS = [
"category": "react",
"severity": "low",
"message": "Console statement detected. Remove before production.",
"file_types": [".js", ".jsx", ".ts", ".tsx"]
"file_types": [".js", ".jsx", ".ts", ".tsx"],
},
# TypeScript checks
{
@@ -93,7 +94,7 @@ COMPONENT_PATTERNS = [
"category": "typescript",
"severity": "medium",
"message": "Using 'any' type loses type safety. Consider using a specific type or 'unknown'.",
"file_types": [".ts", ".tsx"]
"file_types": [".ts", ".tsx"],
},
{
"id": "ts-type-assertion",
@@ -101,7 +102,7 @@ COMPONENT_PATTERNS = [
"category": "typescript",
"severity": "medium",
"message": "Type assertion to 'any'. This bypasses type checking.",
"file_types": [".ts", ".tsx"]
"file_types": [".ts", ".tsx"],
},
# Component structure
{
@@ -110,7 +111,7 @@ COMPONENT_PATTERNS = [
"category": "structure",
"severity": "low",
"message": "Component may not be exported. Ensure it's exported if meant to be reused.",
"file_types": [".jsx", ".tsx"]
"file_types": [".jsx", ".tsx"],
},
{
"id": "component-missing-displayname",
@@ -118,10 +119,11 @@ COMPONENT_PATTERNS = [
"category": "structure",
"severity": "low",
"message": "HOC component may need displayName for debugging.",
"file_types": [".jsx", ".tsx"]
}
"file_types": [".jsx", ".tsx"],
},
]
def get_config():
"""Load hook configuration."""
config_path = Path.home() / ".dss" / "hooks-config.json"
@@ -129,10 +131,10 @@ def get_config():
"component_checker": {
"enabled": True,
"categories": ["accessibility", "react", "typescript"],
"min_severity": "low"
"min_severity": "low",
}
}
if config_path.exists():
try:
with open(config_path) as f:
@@ -142,64 +144,65 @@ def get_config():
pass
return default_config
def severity_level(severity: str) -> int:
"""Convert severity to numeric level."""
levels = {"low": 1, "medium": 2, "high": 3}
return levels.get(severity, 0)
def check_content(content: str, file_path: str, config: dict) -> list:
"""Check content for component issues."""
issues = []
file_ext = Path(file_path).suffix.lower()
checker_config = config.get("component_checker", {})
enabled_categories = checker_config.get("categories", [])
min_severity = checker_config.get("min_severity", "low")
min_level = severity_level(min_severity)
for pattern_def in COMPONENT_PATTERNS:
# Skip if file type doesn't match
if file_ext not in pattern_def.get("file_types", []):
continue
# Skip if category not enabled
if enabled_categories and pattern_def["category"] not in enabled_categories:
continue
# Skip if below minimum severity
if severity_level(pattern_def["severity"]) < min_level:
continue
if re.search(pattern_def["regex"], content, re.MULTILINE):
issues.append({
"id": pattern_def["id"],
"category": pattern_def["category"],
"severity": pattern_def["severity"],
"message": pattern_def["message"]
})
issues.append(
{
"id": pattern_def["id"],
"category": pattern_def["category"],
"severity": pattern_def["severity"],
"message": pattern_def["message"],
}
)
return issues
def format_output(issues: list, file_path: str) -> str:
"""Format issues for display."""
if not issues:
return ""
severity_icons = {
"high": "[HIGH]",
"medium": "[MED]",
"low": "[LOW]"
}
severity_icons = {"high": "[HIGH]", "medium": "[MED]", "low": "[LOW]"}
category_labels = {
"accessibility": "A11Y",
"react": "REACT",
"typescript": "TS",
"structure": "STRUCT"
"structure": "STRUCT",
}
lines = [f"\n=== DSS Component Checker: {file_path} ===\n"]
# Group by category
by_category = {}
for issue in issues:
@@ -207,7 +210,7 @@ def format_output(issues: list, file_path: str) -> str:
if cat not in by_category:
by_category[cat] = []
by_category[cat].append(issue)
for category, cat_issues in by_category.items():
label = category_labels.get(category, category.upper())
lines.append(f"[{label}]")
@@ -215,36 +218,37 @@ def format_output(issues: list, file_path: str) -> str:
sev = severity_icons.get(issue["severity"], "[?]")
lines.append(f" {sev} {issue['message']}")
lines.append("")
lines.append("=" * 50)
return "\n".join(lines)
def main():
"""Main hook entry point."""
config = get_config()
if not config.get("component_checker", {}).get("enabled", True):
sys.exit(0)
# Read hook input from stdin
try:
input_data = json.loads(sys.stdin.read())
except json.JSONDecodeError:
sys.exit(0)
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
if tool_name not in ["Edit", "Write"]:
sys.exit(0)
file_path = tool_input.get("file_path", "")
file_ext = Path(file_path).suffix.lower() if file_path else ""
# Only check React/TypeScript files
if file_ext not in [".jsx", ".tsx", ".js", ".ts"]:
sys.exit(0)
# Get content to check
if tool_name == "Write":
content = tool_input.get("content", "")
@@ -252,17 +256,18 @@ def main():
content = tool_input.get("new_string", "")
else:
content = ""
if not content:
sys.exit(0)
issues = check_content(content, file_path, config)
if issues:
output = format_output(issues, file_path)
print(output, file=sys.stderr)
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -44,14 +44,14 @@ function checkLock() {
if (!fs.existsSync(STATE_DIR)) {
fs.mkdirSync(STATE_DIR, { recursive: true });
}
if (fs.existsSync(LOCK_FILE)) {
const lastRun = parseInt(fs.readFileSync(LOCK_FILE, 'utf8'));
if (!isNaN(lastRun) && (Date.now() - lastRun < LOCK_TIMEOUT_MS)) {
return false; // Already ran recently
}
}
fs.writeFileSync(LOCK_FILE, Date.now().toString(), 'utf8');
return true;
} catch (e) {
@@ -81,16 +81,16 @@ function getChangeSummary() {
try {
const status = execSync('git status --short', { encoding: 'utf8' });
const lines = status.trim().split('\n').filter(Boolean);
let added = 0, modified = 0, deleted = 0;
for (const line of lines) {
const status = line.trim().charAt(0);
if (status === 'A' || status === '?') added++;
else if (status === 'M') modified++;
else if (status === 'D') deleted++;
}
return { added, modified, deleted, total: lines.length };
} catch (e) {
return { added: 0, modified: 0, deleted: 0, total: 0 };
@@ -99,30 +99,30 @@ function getChangeSummary() {
function createBackup(config) {
const backupConfig = config.git_backup || {};
try {
// Stage all changes
execSync('git add -A', { stdio: 'pipe' });
// Build commit message
const parts = [backupConfig.commit_prefix || 'auto-backup'];
if (backupConfig.include_timestamp) {
const timestamp = new Date().toISOString().replace('T', ' ').replace(/\..+/, '');
parts.push(timestamp);
}
const summary = getChangeSummary();
const summaryText = `(${summary.total} files: +${summary.added} ~${summary.modified} -${summary.deleted})`;
const commitMessage = `${parts.join(': ')} ${summaryText}\n\nGenerated by DSS Git Backup Hook`;
// Create commit
execSync(`git commit -m "${commitMessage}"`, { stdio: 'pipe' });
// Get commit hash
const commitHash = execSync('git rev-parse --short HEAD', { encoding: 'utf8' }).trim();
return { success: true, hash: commitHash, files: summary.total };
} catch (e) {
return { success: false, error: e.message };
@@ -143,39 +143,39 @@ function main() {
if (!checkLock()) {
process.exit(0);
}
// Prevent hook recursion
if (process.env.STOP_HOOK_ACTIVE === 'true') {
process.exit(0);
}
const config = loadConfig();
if (!config.git_backup?.enabled) {
process.exit(0);
}
// Check for git repo
if (config.git_backup.require_git_repo && !isGitRepo()) {
log(config, 'DSS Git Backup: Not a git repository, skipping');
process.exit(0);
}
// Check for changes
if (config.git_backup.commit_only_if_changes && !hasChanges()) {
log(config, 'DSS Git Backup: No changes to commit');
process.exit(0);
}
// Create backup
const result = createBackup(config);
if (result.success) {
log(config, `DSS Git Backup: Committed ${result.files} files (${result.hash})`);
} else {
log(config, `DSS Git Backup: Failed - ${result.error}`);
}
process.exit(0);
}

View File

@@ -1,14 +1,13 @@
#!/usr/bin/env python3
"""
DSS Security Check Hook
DSS Security Check Hook.
Validates file edits for common security vulnerabilities.
Written from scratch for DSS - no external dependencies.
"""
import json
import os
import sys
from datetime import datetime
from pathlib import Path
# Security patterns to detect
@@ -18,73 +17,74 @@ SECURITY_PATTERNS = [
"patterns": [".innerHTML =", ".innerHTML=", "innerHTML:"],
"severity": "high",
"message": "Potential XSS: innerHTML assignment detected. Use textContent for plain text or sanitize HTML with DOMPurify.",
"file_types": [".js", ".jsx", ".ts", ".tsx"]
"file_types": [".js", ".jsx", ".ts", ".tsx"],
},
{
"id": "xss-dangerously",
"patterns": ["dangerouslySetInnerHTML"],
"severity": "high",
"message": "Potential XSS: dangerouslySetInnerHTML detected. Ensure content is sanitized before rendering.",
"file_types": [".js", ".jsx", ".ts", ".tsx"]
"file_types": [".js", ".jsx", ".ts", ".tsx"],
},
{
"id": "eval-usage",
"patterns": ["eval(", "new Function("],
"severity": "critical",
"message": "Code injection risk: eval() or new Function() detected. These can execute arbitrary code.",
"file_types": [".js", ".jsx", ".ts", ".tsx"]
"file_types": [".js", ".jsx", ".ts", ".tsx"],
},
{
"id": "document-write",
"patterns": ["document.write("],
"severity": "medium",
"message": "Deprecated: document.write() detected. Use DOM manipulation methods instead.",
"file_types": [".js", ".jsx", ".ts", ".tsx", ".html"]
"file_types": [".js", ".jsx", ".ts", ".tsx", ".html"],
},
{
"id": "sql-injection",
"patterns": ["execute(f\"", "execute(f'", "cursor.execute(\"", ".query(`${"],
"patterns": ['execute(f"', "execute(f'", 'cursor.execute("', ".query(`${"],
"severity": "critical",
"message": "Potential SQL injection: String interpolation in SQL query. Use parameterized queries.",
"file_types": [".py", ".js", ".ts"]
"file_types": [".py", ".js", ".ts"],
},
{
"id": "hardcoded-secret",
"patterns": ["password=", "api_key=", "secret=", "token=", "apiKey:"],
"severity": "high",
"message": "Potential hardcoded secret detected. Use environment variables instead.",
"file_types": [".py", ".js", ".ts", ".jsx", ".tsx"]
"file_types": [".py", ".js", ".ts", ".jsx", ".tsx"],
},
{
"id": "python-pickle",
"patterns": ["pickle.load", "pickle.loads"],
"severity": "high",
"message": "Insecure deserialization: pickle can execute arbitrary code. Use JSON for untrusted data.",
"file_types": [".py"]
"file_types": [".py"],
},
{
"id": "python-shell",
"patterns": ["os.system(", "subprocess.call(shell=True", "subprocess.run(shell=True"],
"severity": "high",
"message": "Shell injection risk: Use subprocess with shell=False and pass args as list.",
"file_types": [".py"]
"file_types": [".py"],
},
{
"id": "react-ref-current",
"patterns": ["ref.current.innerHTML"],
"severity": "high",
"message": "XSS via React ref: Avoid setting innerHTML on refs. Use state/props instead.",
"file_types": [".jsx", ".tsx"]
"file_types": [".jsx", ".tsx"],
},
{
"id": "unsafe-regex",
"patterns": ["new RegExp(", "RegExp("],
"severity": "medium",
"message": "Potential ReDoS: Dynamic regex from user input can cause denial of service.",
"file_types": [".js", ".ts", ".jsx", ".tsx"]
}
"file_types": [".js", ".ts", ".jsx", ".tsx"],
},
]
def get_config():
"""Load hook configuration."""
config_path = Path.home() / ".dss" / "hooks-config.json"
@@ -93,10 +93,10 @@ def get_config():
"enabled": True,
"block_on_critical": False,
"warn_only": True,
"ignored_patterns": []
"ignored_patterns": [],
}
}
if config_path.exists():
try:
with open(config_path) as f:
@@ -106,72 +106,77 @@ def get_config():
pass
return default_config
def check_content(content: str, file_path: str) -> list:
"""Check content for security patterns."""
issues = []
file_ext = Path(file_path).suffix.lower()
for pattern_def in SECURITY_PATTERNS:
# Skip if file type doesn't match
if file_ext not in pattern_def.get("file_types", []):
continue
for pattern in pattern_def["patterns"]:
if pattern.lower() in content.lower():
issues.append({
"id": pattern_def["id"],
"severity": pattern_def["severity"],
"message": pattern_def["message"],
"pattern": pattern
})
issues.append(
{
"id": pattern_def["id"],
"severity": pattern_def["severity"],
"message": pattern_def["message"],
"pattern": pattern,
}
)
break # One match per pattern definition is enough
return issues
def format_output(issues: list, file_path: str) -> str:
"""Format issues for display."""
if not issues:
return ""
severity_icons = {
"critical": "[CRITICAL]",
"high": "[HIGH]",
"medium": "[MEDIUM]",
"low": "[LOW]"
"low": "[LOW]",
}
lines = [f"\n=== DSS Security Check: {file_path} ===\n"]
for issue in issues:
icon = severity_icons.get(issue["severity"], "[?]")
lines.append(f"{icon} {issue['message']}")
lines.append(f" Pattern: {issue['pattern']}\n")
lines.append("=" * 50)
return "\n".join(lines)
def main():
"""Main hook entry point."""
config = get_config()
if not config.get("security_check", {}).get("enabled", True):
sys.exit(0)
# Read hook input from stdin
try:
input_data = json.loads(sys.stdin.read())
except json.JSONDecodeError:
sys.exit(0) # Allow tool to proceed if we can't parse
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
# Only check Edit and Write tools
if tool_name not in ["Edit", "Write"]:
sys.exit(0)
file_path = tool_input.get("file_path", "")
# Get content to check
if tool_name == "Write":
content = tool_input.get("content", "")
@@ -179,23 +184,24 @@ def main():
content = tool_input.get("new_string", "")
else:
content = ""
if not content or not file_path:
sys.exit(0)
# Check for security issues
issues = check_content(content, file_path)
if issues:
output = format_output(issues, file_path)
print(output, file=sys.stderr)
# Check if we should block on critical issues
has_critical = any(i["severity"] == "critical" for i in issues)
if has_critical and config.get("security_check", {}).get("block_on_critical", False):
sys.exit(2) # Block the tool
sys.exit(0) # Allow tool to proceed
if __name__ == "__main__":
main()

View File

@@ -40,17 +40,17 @@ function getGitInfo() {
diff: '',
modifiedFiles: []
};
try {
// Check if in git repo
execSync('git rev-parse --is-inside-work-tree', { stdio: 'pipe' });
// Get branch
info.branch = execSync('git branch --show-current', { encoding: 'utf8' }).trim();
// Get status
info.status = execSync('git status --short', { encoding: 'utf8' }).trim();
// Get modified files
const statusLines = info.status.split('\n').filter(Boolean);
info.modifiedFiles = statusLines.map(line => {
@@ -60,7 +60,7 @@ function getGitInfo() {
file: parts.slice(1).join(' ')
};
});
// Get diff summary
try {
info.diff = execSync('git diff --stat', { encoding: 'utf8' }).trim();
@@ -70,7 +70,7 @@ function getGitInfo() {
} catch (e) {
// Not a git repo or git not available
}
return info;
}
@@ -82,12 +82,12 @@ function getSessionStats() {
linesAdded: 0,
linesRemoved: 0
};
try {
// Get diff stats from git
const diffStat = execSync('git diff --numstat', { encoding: 'utf8' });
const lines = diffStat.trim().split('\n').filter(Boolean);
for (const line of lines) {
const [added, removed] = line.split('\t');
stats.linesAdded += parseInt(added) || 0;
@@ -97,7 +97,7 @@ function getSessionStats() {
} catch (e) {
// Git not available
}
return stats;
}
@@ -105,29 +105,29 @@ function generateReport(config) {
const summaryConfig = config.session_summary || {};
const gitInfo = getGitInfo();
const stats = getSessionStats();
const timestamp = new Date().toLocaleString();
const lines = [];
lines.push('# DSS Session Summary');
lines.push(`\n**Generated:** ${timestamp}`);
if (gitInfo.branch) {
lines.push(`**Branch:** ${gitInfo.branch}`);
}
lines.push('\n## Changes Overview');
lines.push('');
lines.push(`- Files modified: ${stats.filesModified}`);
lines.push(`- Lines added: +${stats.linesAdded}`);
lines.push(`- Lines removed: -${stats.linesRemoved}`);
if (summaryConfig.include_file_list && gitInfo.modifiedFiles.length > 0) {
lines.push('\n## Modified Files');
lines.push('');
lines.push('| Status | File |');
lines.push('|--------|------|');
const statusLabels = {
'M': 'Modified',
'A': 'Added',
@@ -135,17 +135,17 @@ function generateReport(config) {
'R': 'Renamed',
'??': 'Untracked'
};
for (const file of gitInfo.modifiedFiles.slice(0, 20)) {
const label = statusLabels[file.status] || file.status;
lines.push(`| ${label} | ${file.file} |`);
}
if (gitInfo.modifiedFiles.length > 20) {
lines.push(`| ... | +${gitInfo.modifiedFiles.length - 20} more files |`);
}
}
if (summaryConfig.include_git_diff && gitInfo.diff) {
lines.push('\n## Diff Summary');
lines.push('');
@@ -158,27 +158,27 @@ function generateReport(config) {
}
lines.push('```');
}
lines.push('\n---');
lines.push('*Generated by DSS Session Summary Hook*');
return lines.join('\n');
}
function main() {
const config = loadConfig();
if (!config.session_summary?.enabled) {
process.exit(0);
}
try {
const report = generateReport(config);
const outputFile = config.session_summary.output_file || '.dss-session-summary.md';
const outputPath = path.join(process.cwd(), outputFile);
fs.writeFileSync(outputPath, report, 'utf8');
// Output confirmation
console.log(JSON.stringify({
systemMessage: `Session summary saved to ${outputFile}`,
@@ -187,7 +187,7 @@ function main() {
} catch (e) {
// Fail silently
}
process.exit(0);
}

View File

@@ -1,16 +1,17 @@
#!/usr/bin/env python3
"""
DSS Storybook Reminder Hook
DSS Storybook Reminder Hook.
Reminds developers to update Storybook stories when components change.
Written from scratch for DSS.
"""
import json
import os
import re
import sys
from pathlib import Path
def get_config():
"""Load hook configuration."""
config_path = Path.home() / ".dss" / "hooks-config.json"
@@ -20,10 +21,10 @@ def get_config():
"component_patterns": ["**/components/**/*.tsx", "**/ui/**/*.tsx"],
"story_extensions": [".stories.tsx", ".stories.jsx", ".stories.ts", ".stories.js"],
"remind_on_new": True,
"remind_on_props_change": True
"remind_on_props_change": True,
}
}
if config_path.exists():
try:
with open(config_path) as f:
@@ -33,38 +34,40 @@ def get_config():
pass
return default_config
def is_component_file(file_path: str) -> bool:
"""Check if file is a React component."""
path = Path(file_path)
# Must be a tsx/jsx file
if path.suffix.lower() not in [".tsx", ".jsx"]:
return False
# Skip story files, test files, index files
name = path.stem.lower()
if any(x in name for x in [".stories", ".story", ".test", ".spec", "index"]):
return False
# Check if in component-like directory
parts = str(path).lower()
component_dirs = ["components", "ui", "atoms", "molecules", "organisms", "templates"]
return any(d in parts for d in component_dirs)
def find_story_file(component_path: str) -> tuple:
"""Find corresponding story file for a component."""
path = Path(component_path)
base_name = path.stem
parent = path.parent
story_extensions = [".stories.tsx", ".stories.jsx", ".stories.ts", ".stories.js"]
# Check same directory
for ext in story_extensions:
story_path = parent / f"{base_name}{ext}"
if story_path.exists():
return (True, str(story_path))
# Check __stories__ subdirectory
stories_dir = parent / "__stories__"
if stories_dir.exists():
@@ -72,7 +75,7 @@ def find_story_file(component_path: str) -> tuple:
story_path = stories_dir / f"{base_name}{ext}"
if story_path.exists():
return (True, str(story_path))
# Check stories subdirectory
stories_dir = parent / "stories"
if stories_dir.exists():
@@ -80,9 +83,10 @@ def find_story_file(component_path: str) -> tuple:
story_path = stories_dir / f"{base_name}{ext}"
if story_path.exists():
return (True, str(story_path))
return (False, None)
def detect_props_change(content: str) -> bool:
"""Detect if content includes prop changes."""
prop_patterns = [
@@ -90,20 +94,21 @@ def detect_props_change(content: str) -> bool:
r"type\s+\w+Props\s*=",
r"Props\s*=\s*\{",
r"defaultProps\s*=",
r"propTypes\s*="
r"propTypes\s*=",
]
for pattern in prop_patterns:
if re.search(pattern, content):
return True
return False
def format_reminder(file_path: str, has_story: bool, story_path: str, props_changed: bool) -> str:
"""Format the reminder message."""
lines = [f"\n=== DSS Storybook Reminder ===\n"]
lines = ["\n=== DSS Storybook Reminder ===\n"]
component_name = Path(file_path).stem
if not has_story:
lines.append(f"[NEW] Component '{component_name}' has no Storybook story!")
lines.append(f" Consider creating: {component_name}.stories.tsx")
@@ -116,36 +121,37 @@ def format_reminder(file_path: str, has_story: bool, story_path: str, props_chan
lines.append(f"[UPDATE] Props changed in '{component_name}'")
lines.append(f" Story file: {story_path}")
lines.append(" Consider updating stories to reflect new props.")
lines.append("")
lines.append("=" * 40)
return "\n".join(lines)
def main():
"""Main hook entry point."""
config = get_config()
if not config.get("storybook_reminder", {}).get("enabled", True):
sys.exit(0)
# Read hook input from stdin
try:
input_data = json.loads(sys.stdin.read())
except json.JSONDecodeError:
sys.exit(0)
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
if tool_name not in ["Edit", "Write"]:
sys.exit(0)
file_path = tool_input.get("file_path", "")
# Only check component files
if not is_component_file(file_path):
sys.exit(0)
# Get content
if tool_name == "Write":
content = tool_input.get("content", "")
@@ -153,27 +159,28 @@ def main():
content = tool_input.get("new_string", "")
else:
content = ""
# Check for story file
has_story, story_path = find_story_file(file_path)
# Check for props changes
props_changed = detect_props_change(content) if content else False
reminder_config = config.get("storybook_reminder", {})
# Determine if we should show reminder
should_remind = False
if not has_story and reminder_config.get("remind_on_new", True):
should_remind = True
elif has_story and props_changed and reminder_config.get("remind_on_props_change", True):
should_remind = True
if should_remind:
output = format_reminder(file_path, has_story, story_path, props_changed)
print(output, file=sys.stderr)
sys.exit(0)
if __name__ == "__main__":
main()

View File

@@ -1,6 +1,7 @@
#!/usr/bin/env python3
"""
DSS Token Validator Hook
DSS Token Validator Hook.
Detects hardcoded values that should use design tokens.
Written from scratch for DSS.
"""
@@ -18,7 +19,7 @@ HARDCODED_PATTERNS = [
"category": "color",
"message": "Hardcoded hex color detected. Consider using a design token.",
"suggestion": "Use: var(--color-*) or theme.colors.*",
"file_types": [".css", ".scss", ".less", ".js", ".jsx", ".ts", ".tsx"]
"file_types": [".css", ".scss", ".less", ".js", ".jsx", ".ts", ".tsx"],
},
{
"id": "color-rgb",
@@ -26,7 +27,7 @@ HARDCODED_PATTERNS = [
"category": "color",
"message": "Hardcoded RGB color detected. Consider using a design token.",
"suggestion": "Use: var(--color-*) or theme.colors.*",
"file_types": [".css", ".scss", ".less", ".js", ".jsx", ".ts", ".tsx"]
"file_types": [".css", ".scss", ".less", ".js", ".jsx", ".ts", ".tsx"],
},
{
"id": "color-hsl",
@@ -34,7 +35,7 @@ HARDCODED_PATTERNS = [
"category": "color",
"message": "Hardcoded HSL color detected. Consider using a design token.",
"suggestion": "Use: var(--color-*) or theme.colors.*",
"file_types": [".css", ".scss", ".less", ".js", ".jsx", ".ts", ".tsx"]
"file_types": [".css", ".scss", ".less", ".js", ".jsx", ".ts", ".tsx"],
},
{
"id": "spacing-px",
@@ -42,7 +43,7 @@ HARDCODED_PATTERNS = [
"category": "spacing",
"message": "Hardcoded pixel spacing detected. Consider using a spacing token.",
"suggestion": "Use: var(--spacing-*) or theme.spacing.*",
"file_types": [".css", ".scss", ".less"]
"file_types": [".css", ".scss", ".less"],
},
{
"id": "font-size-px",
@@ -50,7 +51,7 @@ HARDCODED_PATTERNS = [
"category": "typography",
"message": "Hardcoded font-size detected. Consider using a typography token.",
"suggestion": "Use: var(--font-size-*) or theme.fontSize.*",
"file_types": [".css", ".scss", ".less"]
"file_types": [".css", ".scss", ".less"],
},
{
"id": "font-family-direct",
@@ -58,7 +59,7 @@ HARDCODED_PATTERNS = [
"category": "typography",
"message": "Hardcoded font-family detected. Consider using a typography token.",
"suggestion": "Use: var(--font-family-*) or theme.fontFamily.*",
"file_types": [".css", ".scss", ".less"]
"file_types": [".css", ".scss", ".less"],
},
{
"id": "border-radius-px",
@@ -66,7 +67,7 @@ HARDCODED_PATTERNS = [
"category": "border",
"message": "Hardcoded border-radius detected. Consider using a radius token.",
"suggestion": "Use: var(--radius-*) or theme.borderRadius.*",
"file_types": [".css", ".scss", ".less"]
"file_types": [".css", ".scss", ".less"],
},
{
"id": "box-shadow-direct",
@@ -74,7 +75,7 @@ HARDCODED_PATTERNS = [
"category": "effects",
"message": "Hardcoded box-shadow detected. Consider using a shadow token.",
"suggestion": "Use: var(--shadow-*) or theme.boxShadow.*",
"file_types": [".css", ".scss", ".less"]
"file_types": [".css", ".scss", ".less"],
},
{
"id": "z-index-magic",
@@ -82,7 +83,7 @@ HARDCODED_PATTERNS = [
"category": "layout",
"message": "Magic number z-index detected. Consider using a z-index token.",
"suggestion": "Use: var(--z-index-*) with semantic names (modal, dropdown, tooltip)",
"file_types": [".css", ".scss", ".less"]
"file_types": [".css", ".scss", ".less"],
},
{
"id": "inline-style-color",
@@ -90,7 +91,7 @@ HARDCODED_PATTERNS = [
"category": "color",
"message": "Hardcoded color in inline style. Consider using theme tokens.",
"suggestion": "Use: style={{ color: theme.colors.* }}",
"file_types": [".jsx", ".tsx"]
"file_types": [".jsx", ".tsx"],
},
{
"id": "tailwind-arbitrary",
@@ -98,8 +99,8 @@ HARDCODED_PATTERNS = [
"category": "color",
"message": "Arbitrary Tailwind color value. Consider using theme colors.",
"suggestion": "Use: bg-primary, text-secondary, etc.",
"file_types": [".jsx", ".tsx", ".html"]
}
"file_types": [".jsx", ".tsx", ".html"],
},
]
# Allowlist patterns (common exceptions)
@@ -114,6 +115,7 @@ ALLOWLIST = [
r"colors\.", # Already using colors object
]
def get_config():
"""Load hook configuration."""
config_path = Path.home() / ".dss" / "hooks-config.json"
@@ -122,10 +124,10 @@ def get_config():
"enabled": True,
"strict_mode": False,
"warn_only": True,
"categories": ["color", "spacing", "typography"]
"categories": ["color", "spacing", "typography"],
}
}
if config_path.exists():
try:
with open(config_path) as f:
@@ -135,6 +137,7 @@ def get_config():
pass
return default_config
def is_allowlisted(match: str) -> bool:
"""Check if match is in allowlist."""
for pattern in ALLOWLIST:
@@ -142,33 +145,36 @@ def is_allowlisted(match: str) -> bool:
return True
return False
def check_content(content: str, file_path: str, config: dict) -> list:
"""Check content for hardcoded values."""
issues = []
file_ext = Path(file_path).suffix.lower()
enabled_categories = config.get("token_validator", {}).get("categories", [])
for pattern_def in HARDCODED_PATTERNS:
# Skip if file type doesn't match
if file_ext not in pattern_def.get("file_types", []):
continue
# Skip if category not enabled (unless empty = all)
if enabled_categories and pattern_def["category"] not in enabled_categories:
continue
matches = re.findall(pattern_def["regex"], content, re.IGNORECASE)
for match in matches:
if not is_allowlisted(match):
issues.append({
"id": pattern_def["id"],
"category": pattern_def["category"],
"message": pattern_def["message"],
"suggestion": pattern_def["suggestion"],
"value": match[:50] # Truncate long matches
})
issues.append(
{
"id": pattern_def["id"],
"category": pattern_def["category"],
"message": pattern_def["message"],
"suggestion": pattern_def["suggestion"],
"value": match[:50], # Truncate long matches
}
)
# Deduplicate by id
seen = set()
unique_issues = []
@@ -176,55 +182,57 @@ def check_content(content: str, file_path: str, config: dict) -> list:
if issue["id"] not in seen:
seen.add(issue["id"])
unique_issues.append(issue)
return unique_issues
def format_output(issues: list, file_path: str) -> str:
"""Format issues for display."""
if not issues:
return ""
category_icons = {
"color": "[COLOR]",
"spacing": "[SPACE]",
"typography": "[FONT]",
"border": "[BORDER]",
"effects": "[EFFECT]",
"layout": "[LAYOUT]"
"layout": "[LAYOUT]",
}
lines = [f"\n=== DSS Token Validator: {file_path} ===\n"]
for issue in issues:
icon = category_icons.get(issue["category"], "[TOKEN]")
lines.append(f"{icon} {issue['message']}")
lines.append(f" Found: {issue['value']}")
lines.append(f" {issue['suggestion']}\n")
lines.append("=" * 50)
return "\n".join(lines)
def main():
"""Main hook entry point."""
config = get_config()
if not config.get("token_validator", {}).get("enabled", True):
sys.exit(0)
# Read hook input from stdin
try:
input_data = json.loads(sys.stdin.read())
except json.JSONDecodeError:
sys.exit(0)
tool_name = input_data.get("tool_name", "")
tool_input = input_data.get("tool_input", {})
if tool_name not in ["Edit", "Write"]:
sys.exit(0)
file_path = tool_input.get("file_path", "")
# Get content to check
if tool_name == "Write":
content = tool_input.get("content", "")
@@ -232,22 +240,23 @@ def main():
content = tool_input.get("new_string", "")
else:
content = ""
if not content or not file_path:
sys.exit(0)
# Check for token issues
issues = check_content(content, file_path, config)
if issues:
output = format_output(issues, file_path)
print(output, file=sys.stderr)
# In strict mode, block on issues
if config.get("token_validator", {}).get("strict_mode", False):
sys.exit(2)
sys.exit(0)
if __name__ == "__main__":
main()