Initial commit

This commit is contained in:
Zhongwei Li
2025-11-29 17:59:27 +08:00
commit 43b47e6fad
14 changed files with 2472 additions and 0 deletions

13
hooks/hooks.json Normal file
View File

@@ -0,0 +1,13 @@
{
"PreToolUse": [
{
"matcher": "*",
"hooks": [
{
"type": "command",
"command": "node ${CLAUDE_PLUGIN_ROOT}/hooks/pre-deployment-check.js"
}
]
}
]
}

221
hooks/pre-deployment-check.js Executable file
View File

@@ -0,0 +1,221 @@
#!/usr/bin/env node
/**
* Pre-Deployment Safety Check Hook
*
* Runs before tool execution to validate deployment readiness.
* Prevents deployments with missing credentials, uncommitted changes, etc.
*/
const fs = require('fs');
const path = require('path');
// Read event data from stdin
let inputData = '';
try {
inputData = fs.readFileSync(0, 'utf8').trim();
} catch (error) {
// Silent exit if no input
process.exit(0);
}
// Parse event data
let eventData;
try {
eventData = JSON.parse(inputData);
} catch (error) {
// Invalid JSON, silent exit
process.exit(0);
}
/**
* Check if DevOps plugin is initialized
*/
function isDevOpsInitialized() {
const configPath = path.join(process.cwd(), '.devops', 'config.json');
return fs.existsSync(configPath);
}
/**
* Check if deployment is in progress
*/
function isDeploymentCommand(eventData) {
const toolName = eventData.toolName || '';
const args = eventData.args || {};
// Check if SlashCommand tool is being used with deployment commands
if (toolName === 'SlashCommand') {
const command = args.command || '';
return command.startsWith('/devops:deploy') ||
command.startsWith('/devops:infra') ||
command.startsWith('/devops:rollback');
}
return false;
}
/**
* Validate git repository status
*/
function checkGitStatus() {
try {
const { execSync } = require('child_process');
// Check if git repo exists
try {
execSync('git rev-parse --git-dir', { stdio: 'ignore' });
} catch {
return { valid: true, warning: 'Not a git repository' };
}
// Check for uncommitted changes
const status = execSync('git status --porcelain', { encoding: 'utf8' });
if (status.trim()) {
return {
valid: false,
error: 'Uncommitted changes detected',
message: 'Please commit or stash changes before deploying'
};
}
return { valid: true };
} catch (error) {
return { valid: true, warning: 'Could not check git status' };
}
}
/**
* Check if credentials are configured
*/
function checkCredentials() {
if (!isDevOpsInitialized()) {
return { valid: true }; // Skip if not initialized
}
const configPath = path.join(process.cwd(), '.devops', 'config.json');
try {
const config = JSON.parse(fs.readFileSync(configPath, 'utf8'));
const secretsMode = config.secrets?.mode || 'manual';
if (secretsMode === 'local') {
const credentialsPath = path.join(process.cwd(), '.devops', 'credentials.enc');
if (!fs.existsSync(credentialsPath)) {
return {
valid: false,
error: 'No credentials configured',
message: 'Run /devops:secrets set to configure credentials'
};
}
}
return { valid: true };
} catch (error) {
return { valid: true, warning: 'Could not validate credentials' };
}
}
/**
* Main hook execution
*/
function main() {
// Only run checks for deployment commands
if (!isDeploymentCommand(eventData)) {
// Not a deployment command, allow execution
const output = {
hookSpecificOutput: {}
};
console.log(JSON.stringify(output));
process.exit(0);
return;
}
// Check if DevOps is initialized
if (!isDevOpsInitialized()) {
// Not initialized, allow execution (init command will handle it)
const output = {
hookSpecificOutput: {}
};
console.log(JSON.stringify(output));
process.exit(0);
return;
}
// Run safety checks
const checks = {
git: checkGitStatus(),
credentials: checkCredentials()
};
// Collect errors and warnings
const errors = [];
const warnings = [];
for (const [checkName, result] of Object.entries(checks)) {
if (!result.valid && result.error) {
errors.push({
check: checkName,
error: result.error,
message: result.message
});
}
if (result.warning) {
warnings.push({
check: checkName,
warning: result.warning
});
}
}
// If there are errors, block execution
if (errors.length > 0) {
const errorMessages = errors.map(e =>
`${e.error}: ${e.message}`
).join('\n');
const output = {
hookSpecificOutput: {
additionalContext: `⚠️ Pre-Deployment Check Failed\n\n${errorMessages}\n\n💡 Fix these issues before deploying`
},
blocked: true,
blockMessage: 'Deployment blocked by safety checks'
};
console.log(JSON.stringify(output));
process.exit(1);
return;
}
// If there are warnings, show them but allow execution
if (warnings.length > 0) {
const warningMessages = warnings.map(w =>
`⚠️ ${w.warning}`
).join('\n');
const output = {
hookSpecificOutput: {
additionalContext: `⚠️ Pre-Deployment Warnings\n\n${warningMessages}\n\n✓ Proceeding with deployment`
}
};
console.log(JSON.stringify(output));
process.exit(0);
return;
}
// All checks passed
const output = {
hookSpecificOutput: {
additionalContext: '✓ Pre-deployment checks passed'
}
};
console.log(JSON.stringify(output));
process.exit(0);
}
// Run main function
try {
main();
} catch (error) {
// Silent exit on any error to avoid blocking Claude Code
process.exit(0);
}