@morodomi/ait3
Version:
AIT³ Development Platform - AI + Ticket + Test + Tool driven development methodology
366 lines (340 loc) • 13 kB
JavaScript
import { Command } from 'commander';
import { join } from 'path';
import { mkdir, writeFile, access, chmod } from 'fs/promises';
import { STYLES } from '../../common/styles.js';
export async function installHooks(args, _services) {
const { targetDir = '.', force = false } = args;
const hooksDir = join(targetDir, '.claude', 'hooks');
const messages = [];
let installedCount = 0;
let skippedCount = 0;
let hasExistingFile = false;
try {
// Create hooks directory
try {
await access(hooksDir);
}
catch {
await mkdir(hooksDir, { recursive: true });
messages.push(`${STYLES.success('SUCCESS:')} Created directory: ${STYLES.info(hooksDir)}`);
}
// Settings file goes in .claude/, hook scripts go in .claude/hooks/
const settingsFile = { name: 'settings.json', content: getSettingsTemplate(), dir: join(targetDir, '.claude') };
const hookScripts = [
{ name: 'check-bash-commands.sh', content: getBashCommandsTemplate(), dir: hooksDir },
{ name: 'check-ambiguous-language.sh', content: getAmbiguousLanguageTemplate(), dir: hooksDir },
{ name: 'check-web-tools.sh', content: getWebToolsTemplate(), dir: hooksDir }
];
const filesToCreate = [settingsFile, ...hookScripts];
// Process each file
for (const file of filesToCreate) {
const filePath = join(file.dir, file.name);
// Create directory if needed
try {
await access(file.dir);
}
catch {
await mkdir(file.dir, { recursive: true });
messages.push(`${STYLES.success('SUCCESS:')} Created directory: ${STYLES.info(file.dir)}`);
}
// Check if file exists
let shouldWrite = true;
try {
await access(filePath);
hasExistingFile = true;
if (!force) {
shouldWrite = false;
messages.push(`${STYLES.warning('⚠')} Skipped: ${STYLES.info(filePath)} already exists (use --force to overwrite)`);
skippedCount++;
}
else {
messages.push(`${STYLES.warning('⚠')} Overwriting existing file: ${STYLES.info(filePath)}`);
}
}
catch {
// File doesn't exist, good to proceed
}
if (shouldWrite) {
await writeFile(filePath, file.content, 'utf-8');
// Make shell scripts executable
if (file.name.endsWith('.sh')) {
await chmod(filePath, 0o755);
}
messages.push(`${STYLES.success('SUCCESS:')} Installed: ${STYLES.info(filePath)}`);
installedCount++;
}
}
// Check if we have existing files and didn't install anything
if (hasExistingFile && !force && installedCount === 0) {
return {
success: false,
message: `${STYLES.danger('ERROR:')} Hook system already exists. Use --force to overwrite.\n\n${messages.join('\n')}`
};
}
// Summary
messages.push('');
messages.push(`${STYLES.success('SUCCESS: Hook system installed')}: ${installedCount} file(s) ${force && hasExistingFile ? 'overwritten' : 'created'}${skippedCount > 0 ? `, ${skippedCount} skipped` : ''}`);
messages.push('');
messages.push(`${STYLES.info('INFO:')} Hook system ready for AI safety and quality control`);
messages.push(`${STYLES.info('LOCATION:')} ${hooksDir}`);
return {
success: true,
message: messages.join('\n')
};
}
catch (error) {
return {
success: false,
message: `${STYLES.danger('ERROR: Failed to create')} hook system in ${hooksDir}\n${error instanceof Error ? error.message : 'Unknown error'}`
};
}
}
function getSettingsTemplate() {
return JSON.stringify({
hooks: {
PreToolUse: [
{
matcher: 'Bash',
hooks: [
{
type: 'command',
command: '.claude/hooks/check-bash-commands.sh'
}
]
},
{
matcher: 'Edit|Write|MultiEdit',
hooks: [
{
type: 'command',
command: '.claude/hooks/check-ambiguous-language.sh'
}
]
},
{
matcher: 'WebFetch|WebSearch',
hooks: [
{
type: 'command',
command: '.claude/hooks/check-web-tools.sh'
}
]
}
]
}
}, null, 2);
}
function getBashCommandsTemplate() {
return `#!/bin/bash
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
# 入力JSON読み込み
input_json=$(cat)
# コマンド抽出
command=$(echo "$input_json" | jq -r '.tool_input.command // ""')
# プロジェクトルートディレクトリを取得(gitリポジトリのルート)
PROJECT_ROOT=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
# sudo チェック(最優先でブロック)
if [[ "$command" =~ ^sudo[[:space:]] ]]; then
cat <<EOF >&2
ERROR: 'sudo' コマンドはブロックされました。
理由: AIに権限昇格を許可することはできません。
代替案: 必要なコマンドを提示しますので、手動で実行してください。
EOF
exit 1
fi
# rm コマンドチェック
if [[ "$command" =~ ^rm[[:space:]] ]]; then
cat <<EOF >&2
ERROR: 'rm' コマンドはブロックされました。
代替案: Claude Codeのコンソールで '! rm' を使って手動実行してください。
例: ! rm -rf /path/to/directory
EOF
exit 1
fi
# chmod チェック
if [[ "$command" =~ ^chmod[[:space:]] ]]; then
# 特に危険なパターン
if [[ "$command" =~ chmod[[:space:]]+777 ]] || [[ "$command" =~ chmod[[:space:]]+\\+s ]]; then
cat <<EOF >&2
ERROR: 危険な 'chmod' コマンドがブロックされました。
検出: $command
理由: セキュリティリスクが高い権限設定です。
代替案: 必要な権限のみを設定してください。
- 実行可能: chmod +x または chmod 755
- 読み取り専用: chmod 644
EOF
else
cat <<EOF >&2
ERROR: 'chmod' コマンドはブロックされました。
代替案: 権限変更が必要な場合は、以下を手動で実行してください。
- スクリプトを実行可能に: chmod +x filename
- 通常のファイル権限: chmod 644 filename
EOF
fi
exit 1
fi
# chown チェック
if [[ "$command" =~ ^chown[[:space:]] ]]; then
cat <<EOF >&2
ERROR: 'chown' コマンドはブロックされました。
理由: 所有者変更は管理者権限が必要な操作です。
代替案: 必要に応じて手動で実行してください。
EOF
exit 1
fi
# dd チェック
if [[ "$command" =~ ^dd[[:space:]] ]]; then
cat <<EOF >&2
ERROR: 'dd' コマンドはブロックされました。
理由: ディスクレベルの操作は破壊的な可能性があります。
代替案: 通常のファイルコピーには 'cp' を使用してください。
EOF
exit 1
fi
# ln チェック(シンボリックリンク)
if [[ "$command" =~ ^ln[[:space:]] ]]; then
cat <<EOF >&2
ERROR: 'ln' コマンドはブロックされました。
理由: シンボリックリンクはセキュリティリスクになる可能性があります。
代替案: 必要に応じて手動で作成してください。
EOF
exit 1
fi
# mv/cp のプロジェクト外チェック
if [[ "$command" =~ ^(mv|cp)[[:space:]] ]]; then
# コマンドから対象パスを抽出(簡易的)
target_path=$(echo "$command" | awk '{print $NF}')
# 絶対パスまたは親ディレクトリ参照を含む場合
if [[ "$target_path" =~ ^/ ]] || [[ "$target_path" =~ \\.\\. ]]; then
# プロジェクトディレクトリ外への操作をチェック
if [[ ! "$target_path" =~ ^$PROJECT_ROOT ]]; then
cat <<EOF >&2
ERROR: プロジェクトディレクトリ外への操作がブロックされました。
コマンド: $command
理由: プロジェクト外へのファイル操作は許可されていません。
代替案: プロジェクト内でのみファイル操作を行ってください。
EOF
exit 1
fi
fi
fi
# /dev/ への書き込みチェック
if [[ "$command" =~ \\>[[:space:]]*/dev/ ]] || [[ "$command" =~ \\>\\>[[:space:]]*/dev/ ]]; then
# /dev/null は許可
if [[ ! "$command" =~ /dev/null ]]; then
cat <<EOF >&2
ERROR: デバイスファイルへの書き込みがブロックされました。
検出: $command
理由: デバイスファイルへの直接書き込みは危険です。
許可: /dev/null への出力のみ許可されています。
EOF
exit 1
fi
fi
# curl/wget チェック
if [[ "$command" =~ ^(curl|wget)[[:space:]] ]]; then
cat <<EOF >&2
ERROR: 'curl'/'wget' コマンドはブロックされました。
代替案1: gemini -p "@file.txt URLの内容を確認して: <URL>"
代替案2: WebFetch ツールを使用してください
EOF
exit 1
fi
# git commit チェック
if [[ "$command" =~ ^git[[:space:]]commit ]]; then
if ! [[ "$command" =~ -m[[:space:]]*\\"(feat|fix|docs|style|refactor|test|chore)(\\(#[0-9]+\\))?:[[:space:]] ]]; then
cat <<EOF >&2
ERROR: コミットメッセージが不適切です。
形式: type(#ticket): description
例: feat(#93): implement hook system
EOF
exit 1
fi
fi
exit 0`;
}
function getAmbiguousLanguageTemplate() {
return `#!/bin/bash
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
input_json=$(cat)
content=$(echo "$input_json" | jq -r '.tool_input.new_string // .tool_input.content // ""')
file_path=$(echo "$input_json" | jq -r '.tool_input.file_path // ""')
# コードファイルのみチェック(README等は除外)
if [[ ! "$file_path" =~ \\.(ts|js|py|java|go)$ ]]; then
exit 0
fi
ambiguous_patterns="probably|might|maybe|perhaps|たぶん|かもしれない|おそらく|思われる"
if echo "$content" | grep -qiE "$ambiguous_patterns"; then
detected=$(echo "$content" | grep -oiE "$ambiguous_patterns" | head -1)
cat <<EOF >&2
WARNING: 曖昧な表現が検出されました: '$detected'
ファイル: $file_path
推奨: 明確で断定的な表現を使用してください。
EOF
# 警告のみ(exit 0でブロックしない)
fi
# "something went wrong" チェック
if echo "$content" | grep -qi "something went wrong"; then
cat <<EOF >&2
ERROR: 曖昧なエラーメッセージです。
検出: "something went wrong"
代替案: 具体的なエラー内容を記載してください
例: "Failed to connect to database (timeout after 30s)"
EOF
exit 1
fi
exit 0`;
}
function getWebToolsTemplate() {
return `#!/bin/bash
export LANG=en_US.UTF-8
export LC_ALL=en_US.UTF-8
input_json=$(cat)
tool_name=$(echo "$input_json" | jq -r '.tool_name // ""')
url=$(echo "$input_json" | jq -r '.tool_input.url // ""')
query=$(echo "$input_json" | jq -r '.tool_input.query // ""')
# WebFetchの場合
if [[ "$tool_name" == "WebFetch" ]]; then
cat <<EOF >&2
INFO: WebFetchを使用しています。
代替案: gemini -p "@file.txt このURLの内容を確認して: $url"
※ Geminiの大規模コンテキストウィンドウを活用できます
EOF
fi
# WebSearchの場合
if [[ "$tool_name" == "WebSearch" ]]; then
cat <<EOF >&2
INFO: WebSearchを使用しています。
代替案: gemini -p "次の検索について調べて: $query"
※ Geminiの最新情報や幅広い知識を活用できます
EOF
fi
# 警告のみなので、常に正常終了
exit 0`;
}
export const hooksCommand = new Command('hooks')
.description('Install AI safety and quality control hook system')
.option('-f, --force', 'Overwrite existing hook files', false)
.action(async (options) => {
const args = { force: options.force };
try {
// Create minimal services object for this command
// Note: This command doesn't actually use services, but maintains interface consistency
const services = {
ticketService: {},
gitService: {},
projectAnalyzer: {}
};
const result = await installHooks(args, services);
console.log(result.message);
if (!result.success) {
process.exit(1);
}
}
catch (error) {
console.error(`Error: ${error instanceof Error ? error.message : 'Unknown error'}`);
process.exit(1);
}
});