初始化提示流程
This commit is contained in:
38
.cursor/hooks/README.md
Normal file
38
.cursor/hooks/README.md
Normal file
@@ -0,0 +1,38 @@
|
||||
# Cursor Hooks 说明
|
||||
|
||||
## 结构
|
||||
|
||||
```
|
||||
hooks/
|
||||
├── run-hook.ps1 # Windows dispatcher(读取 local-env.json 后分发)
|
||||
├── session-init.ps1 # sessionStart hook(PowerShell 版)
|
||||
├── session-init.sh # sessionStart hook(bash 版)
|
||||
├── check-changelog.ps1 # stop hook(PowerShell 版)
|
||||
├── check-changelog.sh # stop hook(bash 版)
|
||||
└── README.md # 本文件
|
||||
```
|
||||
|
||||
## 跨平台适配
|
||||
|
||||
`hooks.json` 中的 `command` 字段是**平台绑定的**——Windows 用 `powershell`,macOS/Linux 用 `bash`。
|
||||
|
||||
### Windows(当前)
|
||||
|
||||
hooks.json 使用 `powershell ... run-hook.ps1` 作为入口,dispatcher 根据
|
||||
`.cursor/local-env.json` 的 `shell` 字段决定执行 `.ps1` 还是 `.sh` 脚本。
|
||||
|
||||
### 迁移到 macOS / Linux
|
||||
|
||||
将 hooks.json 的 command 改为直接调用 `.sh` 脚本:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": 1,
|
||||
"hooks": {
|
||||
"sessionStart": [{ "command": "bash .cursor/hooks/session-init.sh", "timeout": 5 }],
|
||||
"stop": [{ "command": "bash .cursor/hooks/check-changelog.sh", "timeout": 10, "loop_limit": 1 }]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
同时更新 `.cursor/local-env.json` 的 `shell` 字段为 `bash` 或 `zsh`。
|
||||
105
.cursor/hooks/check-changelog.ps1
Normal file
105
.cursor/hooks/check-changelog.ps1
Normal file
@@ -0,0 +1,105 @@
|
||||
# Changelog sync guard — 统一 stop hook
|
||||
#
|
||||
# 用确定性逻辑检查:
|
||||
# 1. 源文件是否比 changelog 更新(mtime 比较)
|
||||
# 2. stdin 中的 agent 上下文是否显示有源文件编辑操作
|
||||
#
|
||||
# 静默条件(不触发提醒):
|
||||
# 1. 环境变量 CURSOR_SKIP_CHANGELOG 被设置(sessionStart 在非 agent 模式设置)
|
||||
# 2. stdin JSON 中 composer_mode 不是 "agent"(如 debug/ask/edit 模式)
|
||||
# 3. changelog 文件不存在
|
||||
# 4. .changelog-ack 标记文件存在且足够新(本会话已确认过 changelog 状态)
|
||||
# 5. 没有源文件比 changelog 更新
|
||||
# 6. stdin 上下文中没有文件编辑操作的证据(防止跨会话残留 mtime 误触发)
|
||||
|
||||
$input = [Console]::In.ReadToEnd()
|
||||
|
||||
# === 豁免检查 1:环境变量跳过标志 ===
|
||||
if ($env:CURSOR_SKIP_CHANGELOG) {
|
||||
Write-Output '{}'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# === 豁免检查 2:从 stdin 解析 composer_mode ===
|
||||
try {
|
||||
$data = $input | ConvertFrom-Json
|
||||
$mode = $data.composer_mode
|
||||
if ($mode -and $mode -ne "agent") {
|
||||
Write-Output '{}'
|
||||
exit 0
|
||||
}
|
||||
} catch {
|
||||
# JSON 解析失败,继续后续检查
|
||||
}
|
||||
|
||||
# === 豁免检查 3:从 stdin 文本匹配 debug 上下文关键词 ===
|
||||
# stop hook 的 $ARGUMENTS 可能包含对话/工具上下文,检测 debug 相关信号
|
||||
if ($input -match '"mode"\s*:\s*"debug"' -or
|
||||
$input -match 'debug[\s_-]?mode' -or
|
||||
$input -match 'Debug Mode') {
|
||||
Write-Output '{}'
|
||||
exit 0
|
||||
}
|
||||
|
||||
$changelog = ".cursor\changelog\changelog-headlines.md"
|
||||
$srcDir = "art-agent"
|
||||
|
||||
# === 豁免检查 4:changelog 文件不存在 ===
|
||||
if (-not (Test-Path $changelog)) {
|
||||
Write-Output '{}'
|
||||
exit 0
|
||||
}
|
||||
|
||||
$clMtime = (Get-Item $changelog).LastWriteTime
|
||||
|
||||
# === 豁免检查 5:ack 标记文件足够新 ===
|
||||
$ackFile = ".cursor\changelog\.changelog-ack"
|
||||
if ((Test-Path $ackFile) -and (Get-Item $ackFile).LastWriteTime -ge $clMtime) {
|
||||
Write-Output '{}'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# === 豁免检查 6:stdin 中无文件编辑证据 ===
|
||||
# 防止跨会话残留 mtime 差异导致误触发:如果 ack 文件不存在(或过旧),
|
||||
# 但 stdin 上下文中也没有任何文件写入/编辑操作的痕迹,说明本次会话
|
||||
# 没有进行代码改动,不应触发提醒。
|
||||
$hasEditEvidence = (
|
||||
$input -match 'StrReplace|Write\s*tool|edit_file|file_write|write_to_file' -or
|
||||
$input -match 'Created file|Modified file|Wrote contents' -or
|
||||
$input -match '"tool"\s*:\s*"(str_replace|write|edit)"'
|
||||
)
|
||||
if (-not $hasEditEvidence) {
|
||||
Write-Output '{}'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# === 核心检查:是否有源文件比 changelog 更新 ===
|
||||
$extensions = @("*.py", "*.tsx", "*.ts", "*.css")
|
||||
$excludeDirs = @("node_modules", ".next", "__pycache__", "venv")
|
||||
|
||||
$newerFile = $null
|
||||
foreach ($ext in $extensions) {
|
||||
$files = Get-ChildItem -Path $srcDir -Filter $ext -Recurse -ErrorAction SilentlyContinue |
|
||||
Where-Object {
|
||||
$skip = $false
|
||||
foreach ($ex in $excludeDirs) {
|
||||
if ($_.FullName -like "*\$ex\*") { $skip = $true; break }
|
||||
}
|
||||
-not $skip -and $_.LastWriteTime -gt $clMtime
|
||||
} |
|
||||
Select-Object -First 1
|
||||
|
||||
if ($files) {
|
||||
$newerFile = $files.Name
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if ($newerFile) {
|
||||
$msg = "[Hook] Source file updated (e.g. $newerFile) but changelog not synced. Run dev-changelog Skill operation A NOW to write all three changelog layers."
|
||||
$json = '{"followup_message":"' + $msg.Replace('"','\"') + '"}'
|
||||
Write-Output $json
|
||||
} else {
|
||||
Write-Output '{}'
|
||||
}
|
||||
exit 0
|
||||
58
.cursor/hooks/check-changelog.sh
Normal file
58
.cursor/hooks/check-changelog.sh
Normal file
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env bash
|
||||
# Changelog sync guard — stop hook (bash 版)
|
||||
|
||||
input=$(cat)
|
||||
|
||||
# === 豁免检查 1:环境变量跳过标志 ===
|
||||
if [ -n "$CURSOR_SKIP_CHANGELOG" ]; then
|
||||
echo '{}'; exit 0
|
||||
fi
|
||||
|
||||
# === 豁免检查 2:从 stdin 解析 composer_mode ===
|
||||
mode=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('composer_mode',''))" 2>/dev/null || echo "")
|
||||
if [ -n "$mode" ] && [ "$mode" != "agent" ]; then
|
||||
echo '{}'; exit 0
|
||||
fi
|
||||
|
||||
# === 豁免检查 3:从 stdin 文本匹配 debug 上下文关键词 ===
|
||||
if echo "$input" | grep -qiE '"mode"\s*:\s*"debug"|debug[\s_-]?mode|Debug Mode'; then
|
||||
echo '{}'; exit 0
|
||||
fi
|
||||
|
||||
changelog=".cursor/changelog/changelog-headlines.md"
|
||||
srcDir="art-agent"
|
||||
|
||||
# === 豁免检查 4:changelog 文件不存在 ===
|
||||
if [ ! -f "$changelog" ]; then
|
||||
echo '{}'; exit 0
|
||||
fi
|
||||
|
||||
clMtime=$(stat -c %Y "$changelog" 2>/dev/null || stat -f %m "$changelog" 2>/dev/null)
|
||||
|
||||
# === 豁免检查 5:ack 标记文件足够新 ===
|
||||
ackFile=".cursor/changelog/.changelog-ack"
|
||||
if [ -f "$ackFile" ]; then
|
||||
ackMtime=$(stat -c %Y "$ackFile" 2>/dev/null || stat -f %m "$ackFile" 2>/dev/null)
|
||||
if [ "$ackMtime" -ge "$clMtime" ] 2>/dev/null; then
|
||||
echo '{}'; exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# === 豁免检查 6:stdin 中无文件编辑证据 ===
|
||||
if ! echo "$input" | grep -qE 'StrReplace|Write\s*tool|edit_file|file_write|write_to_file|Created file|Modified file|Wrote contents|"tool"\s*:\s*"(str_replace|write|edit)"'; then
|
||||
echo '{}'; exit 0
|
||||
fi
|
||||
|
||||
# === 核心检查:是否有源文件比 changelog 更新 ===
|
||||
newerFile=$(find "$srcDir" -type f \( -name "*.py" -o -name "*.tsx" -o -name "*.ts" -o -name "*.css" \) \
|
||||
! -path "*/node_modules/*" ! -path "*/.next/*" ! -path "*/__pycache__/*" ! -path "*/venv/*" \
|
||||
-newer "$changelog" -print -quit 2>/dev/null)
|
||||
|
||||
if [ -n "$newerFile" ]; then
|
||||
fname=$(basename "$newerFile")
|
||||
msg="[Hook] Source file updated (e.g. $fname) but changelog not synced. Run dev-changelog Skill operation A NOW to write all three changelog layers."
|
||||
echo "{\"followup_message\":\"$msg\"}"
|
||||
else
|
||||
echo '{}'
|
||||
fi
|
||||
exit 0
|
||||
35
.cursor/hooks/run-hook.ps1
Normal file
35
.cursor/hooks/run-hook.ps1
Normal file
@@ -0,0 +1,35 @@
|
||||
# run-hook.ps1 — 通用 hook dispatcher
|
||||
# 从 .cursor/local-env.json 读取 shell 类型,决定执行 .ps1 还是 .sh 脚本
|
||||
# 用法:powershell -ExecutionPolicy Bypass -File .cursor/hooks/run-hook.ps1 <hook-name>
|
||||
# 示例:run-hook.ps1 session-init → 执行 session-init.ps1 或 session-init.sh
|
||||
|
||||
param([string]$HookName)
|
||||
|
||||
$localEnvPath = ".cursor\local-env.json"
|
||||
$shell = "powershell"
|
||||
|
||||
if (Test-Path $localEnvPath) {
|
||||
try {
|
||||
$config = Get-Content $localEnvPath -Raw | ConvertFrom-Json
|
||||
if ($config.shell) { $shell = $config.shell }
|
||||
} catch {}
|
||||
}
|
||||
|
||||
$hookDir = ".cursor\hooks"
|
||||
$input = [Console]::In.ReadToEnd()
|
||||
|
||||
if ($shell -eq "powershell") {
|
||||
$scriptPath = Join-Path $hookDir "$HookName.ps1"
|
||||
if (Test-Path $scriptPath) {
|
||||
$input | powershell -ExecutionPolicy Bypass -File $scriptPath
|
||||
} else {
|
||||
Write-Output '{}'
|
||||
}
|
||||
} else {
|
||||
$scriptPath = Join-Path $hookDir "$HookName.sh"
|
||||
if (Test-Path $scriptPath) {
|
||||
$input | bash $scriptPath
|
||||
} else {
|
||||
Write-Output '{}'
|
||||
}
|
||||
}
|
||||
23
.cursor/hooks/session-init.ps1
Normal file
23
.cursor/hooks/session-init.ps1
Normal file
@@ -0,0 +1,23 @@
|
||||
# session-init.ps1 — 会话启动时检测 composer_mode,非 agent 模式设置跳过标志
|
||||
#
|
||||
# sessionStart input 包含 composer_mode 字段("agent" / "ask" / "edit" / "debug" 等)
|
||||
# 通过 env 输出的环境变量会传递给同会话内所有后续 hook
|
||||
|
||||
$input = [Console]::In.ReadToEnd()
|
||||
|
||||
try {
|
||||
$data = $input | ConvertFrom-Json
|
||||
$mode = $data.composer_mode
|
||||
} catch {
|
||||
$mode = $null
|
||||
}
|
||||
|
||||
Remove-Item ".cursor\changelog\.changelog-ack" -ErrorAction SilentlyContinue
|
||||
|
||||
if ($mode -and $mode -ne "agent") {
|
||||
$json = '{"env":{"CURSOR_SKIP_CHANGELOG":"1","CURSOR_COMPOSER_MODE":"' + $mode + '"}}'
|
||||
Write-Output $json
|
||||
} else {
|
||||
Write-Output '{}'
|
||||
}
|
||||
exit 0
|
||||
15
.cursor/hooks/session-init.sh
Normal file
15
.cursor/hooks/session-init.sh
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# session-init.sh — 会话启动时检测 composer_mode,非 agent 模式设置跳过标志
|
||||
|
||||
input=$(cat)
|
||||
|
||||
mode=$(echo "$input" | python3 -c "import sys,json; print(json.load(sys.stdin).get('composer_mode',''))" 2>/dev/null || echo "")
|
||||
|
||||
rm -f ".cursor/changelog/.changelog-ack"
|
||||
|
||||
if [ -n "$mode" ] && [ "$mode" != "agent" ]; then
|
||||
echo "{\"env\":{\"CURSOR_SKIP_CHANGELOG\":\"1\",\"CURSOR_COMPOSER_MODE\":\"$mode\"}}"
|
||||
else
|
||||
echo "{}"
|
||||
fi
|
||||
exit 0
|
||||
Reference in New Issue
Block a user