Revert "chore: Remove dss-claude-plugin directory"
Some checks failed
DSS Project Analysis / dss-context-update (push) Has been cancelled

This reverts commit 72cb7319f5.
This commit is contained in:
2025-12-10 15:54:39 -03:00
parent 72cb7319f5
commit 4de266de61
50 changed files with 10243 additions and 0 deletions

View File

@@ -0,0 +1,215 @@
#!/usr/bin/env node
/**
* DSS Complexity Monitor Hook
* Tracks code complexity metrics and warns on high-complexity code.
* Written from scratch for DSS.
*/
const fs = require('fs');
const path = require('path');
// Configuration
const DEFAULT_CONFIG = {
complexity_monitor: {
enabled: true,
max_function_lines: 50,
max_component_lines: 200,
max_props: 10,
max_nesting_depth: 4,
warn_only: true
}
};
function loadConfig() {
const configPath = path.join(process.env.HOME || '', '.dss', 'hooks-config.json');
try {
if (fs.existsSync(configPath)) {
const userConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
return { ...DEFAULT_CONFIG, ...userConfig };
}
} catch (e) {
// Use defaults
}
return DEFAULT_CONFIG;
}
function countLines(content) {
return content.split('\n').length;
}
function countProps(content) {
// Count props in interface/type definition
const propsMatch = content.match(/(?:interface|type)\s+\w*Props[^{]*\{([^}]+)\}/);
if (propsMatch) {
const propsContent = propsMatch[1];
// Count semicolons or newlines with property definitions
const props = propsContent.split(/[;\n]/).filter(line => {
const trimmed = line.trim();
return trimmed && !trimmed.startsWith('//') && trimmed.includes(':');
});
return props.length;
}
return 0;
}
function countNestingDepth(content) {
let maxDepth = 0;
let currentDepth = 0;
for (const char of content) {
if (char === '{' || char === '(') {
currentDepth++;
maxDepth = Math.max(maxDepth, currentDepth);
} else if (char === '}' || char === ')') {
currentDepth = Math.max(0, currentDepth - 1);
}
}
return maxDepth;
}
function countFunctions(content) {
const patterns = [
/function\s+\w+\s*\([^)]*\)\s*\{/g,
/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);
if (matches) count += matches.length;
}
return count;
}
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) {
issues.push({
type: 'component_size',
severity: 'medium',
message: `Component has ${lines} lines (max: ${monitorConfig.max_component_lines})`,
suggestion: 'Consider breaking into smaller components'
});
}
if (props > monitorConfig.max_props) {
issues.push({
type: 'prop_count',
severity: 'medium',
message: `Component has ${props} props (max: ${monitorConfig.max_props})`,
suggestion: 'Consider grouping related props or using composition'
});
}
}
// Check nesting depth
if (nesting > monitorConfig.max_nesting_depth) {
issues.push({
type: 'nesting_depth',
severity: 'high',
message: `Nesting depth of ${nesting} (max: ${monitorConfig.max_nesting_depth})`,
suggestion: 'Extract nested logic into separate functions'
});
}
// Check function count (indicator of file doing too much)
if (functions > 10) {
issues.push({
type: 'function_count',
severity: 'low',
message: `File contains ${functions} functions`,
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 {
const chunks = [];
for await (const chunk of process.stdin) {
chunks.push(chunk);
}
inputData = JSON.parse(Buffer.concat(chunks).toString());
} 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);
}
main().catch(() => process.exit(0));

View File

@@ -0,0 +1,268 @@
#!/usr/bin/env python3
"""
DSS Component Checker Hook
Validates React components for best practices and accessibility.
Written from scratch for DSS.
"""
import json
import re
import sys
from pathlib import Path
# React component patterns to check
COMPONENT_PATTERNS = [
# Accessibility checks
{
"id": "a11y-img-alt",
"regex": r"<img\s+(?![^>]*alt=)[^>]*>",
"category": "accessibility",
"severity": "high",
"message": "Missing alt attribute on <img>. Add alt text for accessibility.",
"file_types": [".jsx", ".tsx"]
},
{
"id": "a11y-button-type",
"regex": r"<button\s+(?![^>]*type=)[^>]*>",
"category": "accessibility",
"severity": "medium",
"message": "Button missing type attribute. Add type='button' or type='submit'.",
"file_types": [".jsx", ".tsx"]
},
{
"id": "a11y-anchor-href",
"regex": r"<a\s+(?![^>]*href=)[^>]*>",
"category": "accessibility",
"severity": "high",
"message": "Anchor tag missing href. Use button for actions without navigation.",
"file_types": [".jsx", ".tsx"]
},
{
"id": "a11y-click-handler",
"regex": r"<(?:div|span)\s+[^>]*onClick",
"category": "accessibility",
"severity": "medium",
"message": "Click handler on non-interactive element. Use <button> or add role/tabIndex.",
"file_types": [".jsx", ".tsx"]
},
{
"id": "a11y-form-label",
"regex": r"<input\s+(?![^>]*(?:aria-label|id))[^>]*>",
"category": "accessibility",
"severity": "medium",
"message": "Input may be missing label association. Add id with <label> or aria-label.",
"file_types": [".jsx", ".tsx"]
},
# React best practices
{
"id": "react-key-index",
"regex": r"\.map\([^)]*,\s*(?:index|i|idx)\s*\)[^{]*key=\{(?:index|i|idx)\}",
"category": "react",
"severity": "medium",
"message": "Using array index as key. Use unique, stable IDs when possible.",
"file_types": [".jsx", ".tsx"]
},
{
"id": "react-bind-render",
"regex": r"onClick=\{[^}]*\.bind\(this",
"category": "react",
"severity": "low",
"message": "Binding in render creates new function each time. Use arrow function or bind in constructor.",
"file_types": [".jsx", ".tsx"]
},
{
"id": "react-inline-style-object",
"regex": r"style=\{\{[^}]{100,}\}\}",
"category": "react",
"severity": "low",
"message": "Large inline style object. Consider extracting to a constant or CSS module.",
"file_types": [".jsx", ".tsx"]
},
{
"id": "react-console-log",
"regex": r"console\.(log|debug|info)\(",
"category": "react",
"severity": "low",
"message": "Console statement detected. Remove before production.",
"file_types": [".js", ".jsx", ".ts", ".tsx"]
},
# TypeScript checks
{
"id": "ts-any-type",
"regex": r":\s*any\b",
"category": "typescript",
"severity": "medium",
"message": "Using 'any' type loses type safety. Consider using a specific type or 'unknown'.",
"file_types": [".ts", ".tsx"]
},
{
"id": "ts-type-assertion",
"regex": r"as\s+any\b",
"category": "typescript",
"severity": "medium",
"message": "Type assertion to 'any'. This bypasses type checking.",
"file_types": [".ts", ".tsx"]
},
# Component structure
{
"id": "component-no-export",
"regex": r"^(?!.*export).*(?:function|const)\s+[A-Z][a-zA-Z]*\s*(?:=|:|\()",
"category": "structure",
"severity": "low",
"message": "Component may not be exported. Ensure it's exported if meant to be reused.",
"file_types": [".jsx", ".tsx"]
},
{
"id": "component-missing-displayname",
"regex": r"(?:forwardRef|memo)\s*\([^)]*\)",
"category": "structure",
"severity": "low",
"message": "HOC component may need displayName for debugging.",
"file_types": [".jsx", ".tsx"]
}
]
def get_config():
"""Load hook configuration."""
config_path = Path.home() / ".dss" / "hooks-config.json"
default_config = {
"component_checker": {
"enabled": True,
"categories": ["accessibility", "react", "typescript"],
"min_severity": "low"
}
}
if config_path.exists():
try:
with open(config_path) as f:
user_config = json.load(f)
return {**default_config, **user_config}
except:
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"]
})
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]"
}
category_labels = {
"accessibility": "A11Y",
"react": "REACT",
"typescript": "TS",
"structure": "STRUCT"
}
lines = [f"\n=== DSS Component Checker: {file_path} ===\n"]
# Group by category
by_category = {}
for issue in issues:
cat = issue["category"]
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}]")
for issue in cat_issues:
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", "")
elif tool_name == "Edit":
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

@@ -0,0 +1,182 @@
#!/usr/bin/env node
/**
* DSS Git Backup Hook
* Automatically commits changes when Claude Code session ends.
* Written from scratch for DSS.
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Configuration
const DEFAULT_CONFIG = {
git_backup: {
enabled: true,
require_git_repo: true,
commit_only_if_changes: true,
include_timestamp: true,
commit_prefix: 'auto-backup',
show_logs: false
}
};
// Prevent duplicate execution (Claude Code bug workaround)
const STATE_DIR = path.join(__dirname, '..', '.state');
const LOCK_FILE = path.join(STATE_DIR, '.git-backup.lock');
const LOCK_TIMEOUT_MS = 3000;
function loadConfig() {
const configPath = path.join(process.env.HOME || '', '.dss', 'hooks-config.json');
try {
if (fs.existsSync(configPath)) {
const userConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
return { ...DEFAULT_CONFIG, ...userConfig };
}
} catch (e) {
// Use defaults
}
return DEFAULT_CONFIG;
}
function checkLock() {
try {
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) {
return true; // Proceed on error
}
}
function isGitRepo() {
try {
execSync('git rev-parse --is-inside-work-tree', { stdio: 'pipe' });
return true;
} catch (e) {
return false;
}
}
function hasChanges() {
try {
const status = execSync('git status --porcelain', { encoding: 'utf8' });
return status.trim().length > 0;
} catch (e) {
return false;
}
}
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 };
}
}
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 };
}
}
function log(config, message) {
if (config.git_backup?.show_logs) {
console.log(JSON.stringify({
systemMessage: message,
continue: true
}));
}
}
function main() {
// Prevent duplicate execution
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);
}
main();

View File

@@ -0,0 +1,201 @@
#!/usr/bin/env python3
"""
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
SECURITY_PATTERNS = [
{
"id": "xss-innerhtml",
"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"]
},
{
"id": "xss-dangerously",
"patterns": ["dangerouslySetInnerHTML"],
"severity": "high",
"message": "Potential XSS: dangerouslySetInnerHTML detected. Ensure content is sanitized before rendering.",
"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"]
},
{
"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"]
},
{
"id": "sql-injection",
"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"]
},
{
"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"]
},
{
"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"]
},
{
"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"]
},
{
"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"]
},
{
"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"]
}
]
def get_config():
"""Load hook configuration."""
config_path = Path.home() / ".dss" / "hooks-config.json"
default_config = {
"security_check": {
"enabled": True,
"block_on_critical": False,
"warn_only": True,
"ignored_patterns": []
}
}
if config_path.exists():
try:
with open(config_path) as f:
user_config = json.load(f)
return {**default_config, **user_config}
except:
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
})
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]"
}
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", "")
elif tool_name == "Edit":
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

@@ -0,0 +1,194 @@
#!/usr/bin/env node
/**
* DSS Session Summary Hook
* Generates a summary report at the end of each Claude Code session.
* Written from scratch for DSS.
*/
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
// Configuration
const DEFAULT_CONFIG = {
session_summary: {
enabled: true,
output_file: '.dss-session-summary.md',
include_git_diff: true,
include_file_list: true,
max_diff_lines: 100
}
};
function loadConfig() {
const configPath = path.join(process.env.HOME || '', '.dss', 'hooks-config.json');
try {
if (fs.existsSync(configPath)) {
const userConfig = JSON.parse(fs.readFileSync(configPath, 'utf8'));
return { ...DEFAULT_CONFIG, ...userConfig };
}
} catch (e) {
// Use defaults
}
return DEFAULT_CONFIG;
}
function getGitInfo() {
const info = {
branch: '',
status: '',
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 => {
const parts = line.trim().split(/\s+/);
return {
status: parts[0],
file: parts.slice(1).join(' ')
};
});
// Get diff summary
try {
info.diff = execSync('git diff --stat', { encoding: 'utf8' }).trim();
} catch (e) {
info.diff = '';
}
} catch (e) {
// Not a git repo or git not available
}
return info;
}
function getSessionStats() {
// Try to read from session state if available
const stats = {
startTime: new Date().toISOString(),
filesModified: 0,
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;
stats.linesRemoved += parseInt(removed) || 0;
stats.filesModified++;
}
} catch (e) {
// Git not available
}
return stats;
}
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',
'D': 'Deleted',
'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('');
lines.push('```');
const diffLines = gitInfo.diff.split('\n');
const maxLines = summaryConfig.max_diff_lines || 100;
lines.push(diffLines.slice(0, maxLines).join('\n'));
if (diffLines.length > maxLines) {
lines.push(`... (${diffLines.length - maxLines} more lines)`);
}
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}`,
continue: true
}));
} catch (e) {
// Fail silently
}
process.exit(0);
}
main();

View File

@@ -0,0 +1,179 @@
#!/usr/bin/env python3
"""
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"
default_config = {
"storybook_reminder": {
"enabled": True,
"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
}
}
if config_path.exists():
try:
with open(config_path) as f:
user_config = json.load(f)
return {**default_config, **user_config}
except:
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():
for ext in story_extensions:
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():
for ext in story_extensions:
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 = [
r"interface\s+\w+Props",
r"type\s+\w+Props\s*=",
r"Props\s*=\s*\{",
r"defaultProps\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"]
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")
lines.append("")
lines.append(" Quick template:")
lines.append(f" import {{ {component_name} }} from './{component_name}';")
lines.append(f" export default {{ title: 'Components/{component_name}' }};")
lines.append(f" export const Default = () => <{component_name} />;")
elif props_changed:
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", "")
elif tool_name == "Edit":
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

@@ -0,0 +1,253 @@
#!/usr/bin/env python3
"""
DSS Token Validator Hook
Detects hardcoded values that should use design tokens.
Written from scratch for DSS.
"""
import json
import re
import sys
from pathlib import Path
# Patterns for hardcoded values that should be tokens
HARDCODED_PATTERNS = [
{
"id": "color-hex",
"regex": r"(?<!var\()#[0-9a-fA-F]{3,8}\b",
"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"]
},
{
"id": "color-rgb",
"regex": r"rgba?\s*\(\s*\d+\s*,\s*\d+\s*,\s*\d+",
"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"]
},
{
"id": "color-hsl",
"regex": r"hsla?\s*\(\s*\d+\s*,\s*\d+%?\s*,\s*\d+%?",
"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"]
},
{
"id": "spacing-px",
"regex": r":\s*\d{2,}px",
"category": "spacing",
"message": "Hardcoded pixel spacing detected. Consider using a spacing token.",
"suggestion": "Use: var(--spacing-*) or theme.spacing.*",
"file_types": [".css", ".scss", ".less"]
},
{
"id": "font-size-px",
"regex": r"font-size:\s*\d+px",
"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"]
},
{
"id": "font-family-direct",
"regex": r"font-family:\s*['\"]?(?:Arial|Helvetica|Times|Verdana|Georgia)",
"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"]
},
{
"id": "border-radius-px",
"regex": r"border-radius:\s*\d+px",
"category": "border",
"message": "Hardcoded border-radius detected. Consider using a radius token.",
"suggestion": "Use: var(--radius-*) or theme.borderRadius.*",
"file_types": [".css", ".scss", ".less"]
},
{
"id": "box-shadow-direct",
"regex": r"box-shadow:\s*\d+px\s+\d+px",
"category": "effects",
"message": "Hardcoded box-shadow detected. Consider using a shadow token.",
"suggestion": "Use: var(--shadow-*) or theme.boxShadow.*",
"file_types": [".css", ".scss", ".less"]
},
{
"id": "z-index-magic",
"regex": r"z-index:\s*(?:999|9999|99999|\d{4,})",
"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"]
},
{
"id": "inline-style-color",
"regex": r"style=\{?\{[^}]*color:\s*['\"]#[0-9a-fA-F]+['\"]",
"category": "color",
"message": "Hardcoded color in inline style. Consider using theme tokens.",
"suggestion": "Use: style={{ color: theme.colors.* }}",
"file_types": [".jsx", ".tsx"]
},
{
"id": "tailwind-arbitrary",
"regex": r"(?:bg|text|border)-\[#[0-9a-fA-F]+\]",
"category": "color",
"message": "Arbitrary Tailwind color value. Consider using theme colors.",
"suggestion": "Use: bg-primary, text-secondary, etc.",
"file_types": [".jsx", ".tsx", ".html"]
}
]
# Allowlist patterns (common exceptions)
ALLOWLIST = [
r"#000000?", # Pure black
r"#fff(fff)?", # Pure white
r"transparent",
r"inherit",
r"currentColor",
r"var\(--", # Already using CSS variables
r"theme\.", # Already using theme
r"colors\.", # Already using colors object
]
def get_config():
"""Load hook configuration."""
config_path = Path.home() / ".dss" / "hooks-config.json"
default_config = {
"token_validator": {
"enabled": True,
"strict_mode": False,
"warn_only": True,
"categories": ["color", "spacing", "typography"]
}
}
if config_path.exists():
try:
with open(config_path) as f:
user_config = json.load(f)
return {**default_config, **user_config}
except:
pass
return default_config
def is_allowlisted(match: str) -> bool:
"""Check if match is in allowlist."""
for pattern in ALLOWLIST:
if re.search(pattern, match, re.IGNORECASE):
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
})
# Deduplicate by id
seen = set()
unique_issues = []
for issue in issues:
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]"
}
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", "")
elif tool_name == "Edit":
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()