claude-flow-novice 2.14.6 → 2.14.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/cfn/run-tests.md +119 -0
- package/.claude/hooks/cfn-post-edit.config.json +11 -4
- package/.claude/skills/cfn-agent-selector/SKILL.md +3 -2
- package/.claude/skills/cfn-loop-orchestration/orchestrate.sh +1 -1
- package/.claude/skills/cfn-product-owner-decision/execute-decision.sh +141 -114
- package/.claude/skills/cfn-redis-coordination/report-completion.sh +86 -0
- package/.claude/skills/cfn-redis-coordination/store-context.sh +34 -0
- package/.claude/skills/pre-edit-backup/backup.sh +130 -0
- package/.claude/skills/pre-edit-backup/cleanup.sh +155 -0
- package/.claude/skills/pre-edit-backup/restore.sh +128 -0
- package/.claude/skills/pre-edit-backup/revert-file.sh +168 -0
- package/claude-assets/agents/README-AGENT_LIFECYCLE.md +522 -0
- package/claude-assets/agents/cfn-dev-team/coordinators/cfn-v3-coordinator.md +6 -3
- package/claude-assets/agents/cfn-dev-team/product-owners/product-owner.md +1 -1
- package/claude-assets/agents/cfn-dev-team/test-agent.md +141 -0
- package/claude-assets/agents/cfn-dev-team/utility/agent-builder.md +35 -0
- package/claude-assets/commands/cfn/run-tests.md +119 -0
- package/claude-assets/hooks/cfn-post-edit.config.json +11 -4
- package/claude-assets/skills/agent-name-validation/README.md +28 -0
- package/claude-assets/skills/agent-name-validation/SKILL.md +168 -0
- package/claude-assets/skills/agent-name-validation/validate-agent-names.sh +47 -0
- package/claude-assets/skills/cfn-agent-selector/SKILL.md +3 -2
- package/claude-assets/skills/cfn-loop-orchestration/orchestrate.sh +1 -1
- package/claude-assets/skills/cfn-product-owner-decision/execute-decision.sh +141 -114
- package/claude-assets/skills/cfn-redis-coordination/report-completion.sh +86 -0
- package/claude-assets/skills/cfn-redis-coordination/store-context.sh +34 -0
- package/claude-assets/skills/cfn-task-classifier/SKILL.md +1 -1
- package/claude-assets/skills/cfn-test-runner/SKILL.md +288 -0
- package/claude-assets/skills/cfn-test-runner/detect-regressions.sh +55 -0
- package/claude-assets/skills/cfn-test-runner/init-benchmark-db.sh +48 -0
- package/claude-assets/skills/cfn-test-runner/run-all-tests.sh +222 -0
- package/claude-assets/skills/cfn-test-runner/store-benchmarks.sh +55 -0
- package/claude-assets/skills/cfn-test-runner/validate-redis-keys.sh +143 -0
- package/claude-assets/skills/hook-pipeline/bash-dependency-checker.sh +89 -0
- package/claude-assets/skills/hook-pipeline/bash-pipe-safety.sh +69 -0
- package/claude-assets/skills/hook-pipeline/enforce-lf.sh +36 -0
- package/claude-assets/skills/hook-pipeline/js-promise-safety.sh +110 -0
- package/claude-assets/skills/hook-pipeline/python-async-safety.py +124 -0
- package/claude-assets/skills/hook-pipeline/python-import-checker.py +114 -0
- package/claude-assets/skills/hook-pipeline/python-subprocess-safety.py +77 -0
- package/claude-assets/skills/hook-pipeline/rust-command-safety.sh +38 -0
- package/claude-assets/skills/hook-pipeline/rust-dependency-checker.sh +50 -0
- package/claude-assets/skills/hook-pipeline/rust-future-safety.sh +50 -0
- package/dist/cli/agent-executor.js +1 -1
- package/dist/cli/agent-executor.js.map +1 -1
- package/dist/cli/agent-prompt-builder.js +40 -30
- package/dist/cli/agent-prompt-builder.js.map +1 -1
- package/dist/cli/config-manager.js +109 -91
- package/dist/cli/config-manager.js.map +1 -1
- package/package.json +2 -1
- package/scripts/init-project.js +4 -1
- package/claude-assets/agents/cfn-dev-team/developers/dev-backend-api.md +0 -147
- package/claude-assets/agents/cfn-dev-team/developers/frontend/spec-mobile-react-native.md +0 -199
- package/claude-assets/agents/cfn-dev-team/documentation/docs-api-openapi.md +0 -98
- package/claude-assets/agents/cfn-dev-team/product-owners/product-owner-agent.md +0 -155
- package/claude-assets/agents/cfn-dev-team/reviewers/quality/analyze-code-quality.md +0 -141
- /package/claude-assets/agents/cfn-dev-team/developers/{backend-dev.md → backend-developer.md} +0 -0
- /package/claude-assets/agents/cfn-dev-team/documentation/{api-docs.md → api-documentation.md} +0 -0
- /package/claude-assets/agents/cfn-dev-team/documentation/{specification.md → specification-agent.md} +0 -0
- /package/claude-assets/agents/cfn-dev-team/reviewers/quality/{code-analyzer.md → code-quality-validator.md} +0 -0
- /package/claude-assets/agents/cfn-dev-team/testers/e2e/{playwright-agent.md → playwright-tester.md} +0 -0
- /package/claude-assets/agents/cfn-dev-team/testers/unit/{tdd-london-swarm.md → tdd-london-unit-swarm.md} +0 -0
- /package/claude-assets/agents/cfn-dev-team/testers/validation/{production-validator.md → validation-production-validator.md} +0 -0
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Redis Key Validator
|
|
3
|
+
# Version: 1.0.0
|
|
4
|
+
# Purpose: Validate Redis key consistency across codebase
|
|
5
|
+
|
|
6
|
+
set -euo pipefail
|
|
7
|
+
|
|
8
|
+
# Colors
|
|
9
|
+
RED='\033[0;31m'
|
|
10
|
+
GREEN='\033[0;32m'
|
|
11
|
+
YELLOW='\033[1;33m'
|
|
12
|
+
BLUE='\033[0;34m'
|
|
13
|
+
NC='\033[0m'
|
|
14
|
+
|
|
15
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
16
|
+
PROJECT_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
|
|
17
|
+
|
|
18
|
+
VIOLATIONS=0
|
|
19
|
+
WARNINGS=0
|
|
20
|
+
|
|
21
|
+
echo -e "${GREEN}=========================================="
|
|
22
|
+
echo "Redis Key Consistency Validator"
|
|
23
|
+
echo -e "==========================================${NC}"
|
|
24
|
+
echo ""
|
|
25
|
+
|
|
26
|
+
# Standard patterns (from audit)
|
|
27
|
+
VALID_PATTERNS=(
|
|
28
|
+
"swarm:\${TASK_ID}:\${AGENT_ID}:done"
|
|
29
|
+
"swarm:\${TASK_ID}:\${AGENT_ID}:confidence"
|
|
30
|
+
"swarm:\${TASK_ID}:\${AGENT_ID}:result"
|
|
31
|
+
"swarm:\${TASK_ID}:decision"
|
|
32
|
+
"swarm:\${TASK_ID}:gate-passed"
|
|
33
|
+
"swarm:\${TASK_ID}:gate-failed"
|
|
34
|
+
"swarm:\${TASK_ID}:loop2:consensus"
|
|
35
|
+
"swarm:metrics:decisions:*"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
# Anti-patterns (non-standard)
|
|
39
|
+
ANTI_PATTERNS=(
|
|
40
|
+
"swarm:\${TASK_ID}:\${AGENT_ID}:decision" # Should be task-level
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
echo -e "${BLUE}[1/4] Checking for standard pattern usage...${NC}"
|
|
44
|
+
|
|
45
|
+
# Find all redis-cli commands
|
|
46
|
+
REDIS_FILES=$(grep -rl "redis-cli" "$PROJECT_ROOT/.claude" "$PROJECT_ROOT/tests" 2>/dev/null || true)
|
|
47
|
+
|
|
48
|
+
for file in $REDIS_FILES; do
|
|
49
|
+
# Skip backup files
|
|
50
|
+
if [[ "$file" =~ \.backup ]]; then
|
|
51
|
+
continue
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# Check for anti-patterns
|
|
55
|
+
for anti_pattern in "${ANTI_PATTERNS[@]}"; do
|
|
56
|
+
# Convert pattern to grep regex
|
|
57
|
+
grep_pattern=$(echo "$anti_pattern" | sed 's/\$/\\$/g' | sed 's/{[^}]*}/.*/g')
|
|
58
|
+
|
|
59
|
+
if grep -qE "$grep_pattern" "$file" 2>/dev/null; then
|
|
60
|
+
echo -e "${YELLOW}⚠️ WARNING: Non-standard pattern in $file${NC}"
|
|
61
|
+
echo " Found: $anti_pattern"
|
|
62
|
+
((WARNINGS++))
|
|
63
|
+
fi
|
|
64
|
+
done
|
|
65
|
+
done
|
|
66
|
+
|
|
67
|
+
echo ""
|
|
68
|
+
echo -e "${BLUE}[2/4] Validating Product Owner decision keys...${NC}"
|
|
69
|
+
|
|
70
|
+
# Check execute-decision.sh uses correct pattern
|
|
71
|
+
PO_SCRIPT="$PROJECT_ROOT/.claude/skills/cfn-product-owner-decision/execute-decision.sh"
|
|
72
|
+
if [ -f "$PO_SCRIPT" ]; then
|
|
73
|
+
# Should have: SET "swarm:${TASK_ID}:decision"
|
|
74
|
+
if grep -q 'SET "swarm:${TASK_ID}:decision"' "$PO_SCRIPT"; then
|
|
75
|
+
echo -e "${GREEN}✅ Product Owner uses standard decision key${NC}"
|
|
76
|
+
else
|
|
77
|
+
echo -e "${RED}❌ VIOLATION: Product Owner decision key non-standard${NC}"
|
|
78
|
+
((VIOLATIONS++))
|
|
79
|
+
fi
|
|
80
|
+
|
|
81
|
+
# Should have TTL (EX 3600)
|
|
82
|
+
if grep -q 'EX 3600' "$PO_SCRIPT"; then
|
|
83
|
+
echo -e "${GREEN}✅ Product Owner decision has TTL${NC}"
|
|
84
|
+
else
|
|
85
|
+
echo -e "${YELLOW}⚠️ WARNING: Product Owner decision missing TTL${NC}"
|
|
86
|
+
((WARNINGS++))
|
|
87
|
+
fi
|
|
88
|
+
else
|
|
89
|
+
echo -e "${RED}❌ VIOLATION: Product Owner script not found${NC}"
|
|
90
|
+
((VIOLATIONS++))
|
|
91
|
+
fi
|
|
92
|
+
|
|
93
|
+
echo ""
|
|
94
|
+
echo -e "${BLUE}[3/4] Checking key namespace consistency...${NC}"
|
|
95
|
+
|
|
96
|
+
# Find keys NOT starting with "swarm:"
|
|
97
|
+
NON_SWARM_KEYS=$(grep -rh "redis-cli.*[\"']" "$PROJECT_ROOT/.claude" "$PROJECT_ROOT/tests" 2>/dev/null | \
|
|
98
|
+
grep -oE '"[^"]*"' | \
|
|
99
|
+
grep -v "swarm:" | \
|
|
100
|
+
grep -v "complete" | \
|
|
101
|
+
grep -v "^\"$" | \
|
|
102
|
+
sort -u || true)
|
|
103
|
+
|
|
104
|
+
if [ -n "$NON_SWARM_KEYS" ]; then
|
|
105
|
+
echo -e "${YELLOW}⚠️ WARNING: Found keys outside 'swarm:' namespace:${NC}"
|
|
106
|
+
echo "$NON_SWARM_KEYS" | head -5
|
|
107
|
+
((WARNINGS++))
|
|
108
|
+
else
|
|
109
|
+
echo -e "${GREEN}✅ All keys use 'swarm:' namespace${NC}"
|
|
110
|
+
fi
|
|
111
|
+
|
|
112
|
+
echo ""
|
|
113
|
+
echo -e "${BLUE}[4/4] Checking for missing TTLs...${NC}"
|
|
114
|
+
|
|
115
|
+
# Find SET commands without EX/EXPIRE
|
|
116
|
+
NO_TTL_COUNT=$(grep -rh "redis-cli.*SET" "$PROJECT_ROOT/.claude" 2>/dev/null | \
|
|
117
|
+
grep -v "EX\|EXPIRE" | \
|
|
118
|
+
wc -l || echo 0)
|
|
119
|
+
|
|
120
|
+
if [ "$NO_TTL_COUNT" -gt 0 ]; then
|
|
121
|
+
echo -e "${YELLOW}⚠️ WARNING: $NO_TTL_COUNT SET commands without TTL${NC}"
|
|
122
|
+
((WARNINGS++))
|
|
123
|
+
else
|
|
124
|
+
echo -e "${GREEN}✅ All SET commands have TTL${NC}"
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
echo ""
|
|
128
|
+
echo -e "${GREEN}=========================================="
|
|
129
|
+
echo "Validation Summary"
|
|
130
|
+
echo -e "==========================================${NC}"
|
|
131
|
+
echo "Violations: $VIOLATIONS"
|
|
132
|
+
echo "Warnings: $WARNINGS"
|
|
133
|
+
|
|
134
|
+
if [ $VIOLATIONS -eq 0 ] && [ $WARNINGS -eq 0 ]; then
|
|
135
|
+
echo -e "${GREEN}✅ PASS: All Redis keys follow standards${NC}"
|
|
136
|
+
exit 0
|
|
137
|
+
elif [ $VIOLATIONS -eq 0 ]; then
|
|
138
|
+
echo -e "${YELLOW}⚠️ PASS WITH WARNINGS: $WARNINGS minor issues${NC}"
|
|
139
|
+
exit 0
|
|
140
|
+
else
|
|
141
|
+
echo -e "${RED}❌ FAIL: $VIOLATIONS critical violations${NC}"
|
|
142
|
+
exit 1
|
|
143
|
+
fi
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Bash Dependency Checker
|
|
5
|
+
# Validates script dependencies and checks for their existence
|
|
6
|
+
|
|
7
|
+
# Check if a file path is provided
|
|
8
|
+
if [[ $# -ne 1 ]]; then
|
|
9
|
+
echo "Usage: $0 <file_path>" >&2
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
FILE_PATH="$1"
|
|
14
|
+
|
|
15
|
+
# Skip non-bash files
|
|
16
|
+
if [[ ! "$FILE_PATH" =~ \.(sh|bash)$ ]]; then
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Skip empty files
|
|
21
|
+
if [[ ! -s "$FILE_PATH" ]]; then
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Function to resolve relative paths
|
|
26
|
+
resolve_path() {
|
|
27
|
+
local base_dir script_path="$1"
|
|
28
|
+
|
|
29
|
+
# If path is absolute, return as-is
|
|
30
|
+
if [[ "$script_path" =~ ^/ ]]; then
|
|
31
|
+
echo "$script_path"
|
|
32
|
+
return 0
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
# Get base directory of the current script
|
|
36
|
+
base_dir="$(dirname "$(readlink -f "$FILE_PATH")")"
|
|
37
|
+
|
|
38
|
+
# Resolve relative paths
|
|
39
|
+
readlink -f "$base_dir/$script_path"
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
# Function to extract script dependencies
|
|
43
|
+
extract_dependencies() {
|
|
44
|
+
local missing_deps=0
|
|
45
|
+
|
|
46
|
+
# Extract sourced scripts using source, ., or direct paths
|
|
47
|
+
while IFS= read -r line; do
|
|
48
|
+
local script_path=""
|
|
49
|
+
|
|
50
|
+
# Skip comments
|
|
51
|
+
[[ "$line" =~ ^[[:space:]]*# ]] && continue
|
|
52
|
+
|
|
53
|
+
# Match source, ., or sourced script patterns
|
|
54
|
+
if [[ "$line" =~ ^[[:space:]]*(source|\.)[[:space:]]+([^\;]+) ]]; then
|
|
55
|
+
script_path="${BASH_REMATCH[2]}"
|
|
56
|
+
elif [[ "$line" =~ ^[[:space:]]*bash[[:space:]]+([^\;]+) ]]; then
|
|
57
|
+
script_path="${BASH_REMATCH[1]}"
|
|
58
|
+
else
|
|
59
|
+
continue
|
|
60
|
+
fi
|
|
61
|
+
|
|
62
|
+
# Remove surrounding quotes and whitespace
|
|
63
|
+
script_path=$(echo "$script_path" | xargs)
|
|
64
|
+
|
|
65
|
+
# Skip variables and arithmetic expansions (after quote removal)
|
|
66
|
+
if [[ "$script_path" =~ ^\$ ]]; then
|
|
67
|
+
continue
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
# Resolve path
|
|
71
|
+
local resolved_path
|
|
72
|
+
resolved_path=$(resolve_path "$script_path")
|
|
73
|
+
|
|
74
|
+
# Check if resolved script exists
|
|
75
|
+
if [[ ! -f "$resolved_path" ]]; then
|
|
76
|
+
echo "Missing dependency: $resolved_path" >&2
|
|
77
|
+
missing_deps=$((missing_deps + 1))
|
|
78
|
+
fi
|
|
79
|
+
done < "$FILE_PATH"
|
|
80
|
+
|
|
81
|
+
return $missing_deps
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Run dependency checks
|
|
85
|
+
if ! extract_dependencies; then
|
|
86
|
+
exit 1
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
exit 0
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Bash Pipe Safety Validator
|
|
5
|
+
# Checks for potential pipe safety issues in bash scripts
|
|
6
|
+
|
|
7
|
+
# Risky commands list
|
|
8
|
+
RISKY_COMMANDS=(
|
|
9
|
+
"redis-cli"
|
|
10
|
+
"curl"
|
|
11
|
+
"wget"
|
|
12
|
+
"npm"
|
|
13
|
+
"docker"
|
|
14
|
+
"git"
|
|
15
|
+
"mysql"
|
|
16
|
+
"psql"
|
|
17
|
+
"python"
|
|
18
|
+
"node"
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
# Check if a file path is provided
|
|
22
|
+
if [[ $# -ne 1 ]]; then
|
|
23
|
+
echo "Usage: $0 <file_path>" >&2
|
|
24
|
+
exit 1
|
|
25
|
+
fi
|
|
26
|
+
|
|
27
|
+
FILE_PATH="$1"
|
|
28
|
+
|
|
29
|
+
# Skip non-bash files
|
|
30
|
+
if [[ ! "$FILE_PATH" =~ \.(sh|bash)$ ]]; then
|
|
31
|
+
exit 0
|
|
32
|
+
fi
|
|
33
|
+
|
|
34
|
+
# Skip empty files
|
|
35
|
+
if [[ ! -s "$FILE_PATH" ]]; then
|
|
36
|
+
exit 0
|
|
37
|
+
fi
|
|
38
|
+
|
|
39
|
+
# Check for pipefail (either 'set -o pipefail' or 'set -euo pipefail' or similar)
|
|
40
|
+
if ! grep -qE "set -(o pipefail|[a-z]*o[a-z]*)" "$FILE_PATH" || ! grep -q "pipefail" "$FILE_PATH"; then
|
|
41
|
+
echo "Warning: Missing 'set -o pipefail' in script" >&2
|
|
42
|
+
exit 2
|
|
43
|
+
fi
|
|
44
|
+
|
|
45
|
+
# Function to check for risky pipe usage
|
|
46
|
+
check_pipe_safety() {
|
|
47
|
+
local line issues=0
|
|
48
|
+
|
|
49
|
+
while IFS= read -r line; do
|
|
50
|
+
# Check for pipe usage without stderr redirection
|
|
51
|
+
if [[ "$line" =~ \| ]]; then
|
|
52
|
+
for cmd in "${RISKY_COMMANDS[@]}"; do
|
|
53
|
+
if [[ "$line" =~ $cmd ]] && [[ ! "$line" =~ (2>/dev/null|2>&1) ]]; then
|
|
54
|
+
echo "Potential pipe safety issue in line: $line" >&2
|
|
55
|
+
((issues++))
|
|
56
|
+
fi
|
|
57
|
+
done
|
|
58
|
+
fi
|
|
59
|
+
done < "$FILE_PATH"
|
|
60
|
+
|
|
61
|
+
return $issues
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
# Run safety checks
|
|
65
|
+
if ! check_pipe_safety; then
|
|
66
|
+
exit 2
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
exit 0
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
# Enforce Line Endings Validator
|
|
5
|
+
# Converts CRLF to LF for text files
|
|
6
|
+
|
|
7
|
+
# Check if a file path is provided
|
|
8
|
+
if [[ $# -ne 1 ]]; then
|
|
9
|
+
echo "Usage: $0 <file_path>" >&2
|
|
10
|
+
exit 1
|
|
11
|
+
fi
|
|
12
|
+
|
|
13
|
+
FILE_PATH="$1"
|
|
14
|
+
|
|
15
|
+
# Skip empty files
|
|
16
|
+
if [[ ! -s "$FILE_PATH" ]]; then
|
|
17
|
+
exit 0
|
|
18
|
+
fi
|
|
19
|
+
|
|
20
|
+
# Check if file is binary
|
|
21
|
+
if file --mime-type "$FILE_PATH" | grep -qE '(binary|application/)'; then
|
|
22
|
+
exit 0
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
# Skip files that are already LF (check for carriage return)
|
|
26
|
+
if ! grep -q $'\r' "$FILE_PATH"; then
|
|
27
|
+
exit 0
|
|
28
|
+
fi
|
|
29
|
+
|
|
30
|
+
# Convert CRLF to LF
|
|
31
|
+
sed -i 's/\r$//' "$FILE_PATH"
|
|
32
|
+
|
|
33
|
+
# Optional: Log the conversion
|
|
34
|
+
echo "Converted $FILE_PATH to LF line endings" >&2
|
|
35
|
+
|
|
36
|
+
exit 0
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Manual check for unhandled promises (fallback if ESLint not available)
|
|
3
|
+
|
|
4
|
+
set -euo pipefail
|
|
5
|
+
|
|
6
|
+
FILE="${1:-}"
|
|
7
|
+
if [[ ! "$FILE" =~ \.(js|ts|jsx|tsx|mjs|cjs)$ ]]; then
|
|
8
|
+
exit 0
|
|
9
|
+
fi
|
|
10
|
+
|
|
11
|
+
# Read file into array for context-aware checking
|
|
12
|
+
mapfile -t FILE_LINES < "$FILE"
|
|
13
|
+
|
|
14
|
+
# Check for async function calls without await/catch/then/return
|
|
15
|
+
UNHANDLED=""
|
|
16
|
+
|
|
17
|
+
for ((i=0; i<${#FILE_LINES[@]}; i++)); do
|
|
18
|
+
line_num=$((i + 1))
|
|
19
|
+
content="${FILE_LINES[$i]}"
|
|
20
|
+
|
|
21
|
+
# Skip empty lines and comments
|
|
22
|
+
if [[ -z "$content" ]] || [[ "$content" =~ ^[[:space:]]*// ]] || [[ "$content" =~ ^[[:space:]]*\* ]]; then
|
|
23
|
+
continue
|
|
24
|
+
fi
|
|
25
|
+
|
|
26
|
+
# Skip if line doesn't contain function calls
|
|
27
|
+
if ! echo "$content" | grep -qE '\w+\(\)'; then
|
|
28
|
+
continue
|
|
29
|
+
fi
|
|
30
|
+
|
|
31
|
+
# Skip function definitions (async function foo(), function foo(), const foo = async)
|
|
32
|
+
if echo "$content" | grep -qE '(async[[:space:]]+function|function[[:space:]]+|const[[:space:]]+\w+[[:space:]]*=[[:space:]]*async|let[[:space:]]+\w+[[:space:]]*=[[:space:]]*async|var[[:space:]]+\w+[[:space:]]*=[[:space:]]*async)'; then
|
|
33
|
+
continue
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
# Skip if line contains await, catch, then, or return
|
|
37
|
+
if echo "$content" | grep -qE '(await|\.catch|\.then|return)'; then
|
|
38
|
+
continue
|
|
39
|
+
fi
|
|
40
|
+
|
|
41
|
+
# Check next line for .catch() or .then() chaining (within 2 lines)
|
|
42
|
+
has_chaining=false
|
|
43
|
+
for ((j=1; j<=2 && (i+j)<${#FILE_LINES[@]}; j++)); do
|
|
44
|
+
next_line="${FILE_LINES[$((i+j))]}"
|
|
45
|
+
if echo "$next_line" | grep -qE '^[[:space:]]*\.(catch|then)'; then
|
|
46
|
+
has_chaining=true
|
|
47
|
+
break
|
|
48
|
+
fi
|
|
49
|
+
done
|
|
50
|
+
|
|
51
|
+
if [[ "$has_chaining" == "true" ]]; then
|
|
52
|
+
continue
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
# Check if inside try-catch block (look back up to 10 lines)
|
|
56
|
+
in_try_catch=false
|
|
57
|
+
try_depth=0
|
|
58
|
+
for ((j=1; j<=10 && (i-j)>=0; j++)); do
|
|
59
|
+
prev_line="${FILE_LINES[$((i-j))]}"
|
|
60
|
+
|
|
61
|
+
# Count braces to track depth
|
|
62
|
+
if echo "$prev_line" | grep -qE '\{'; then
|
|
63
|
+
((try_depth++)) || true
|
|
64
|
+
fi
|
|
65
|
+
if echo "$prev_line" | grep -qE '\}'; then
|
|
66
|
+
((try_depth--)) || true
|
|
67
|
+
fi
|
|
68
|
+
|
|
69
|
+
# Found a try block at same depth
|
|
70
|
+
if [[ $try_depth -gt 0 ]] && echo "$prev_line" | grep -qE '^[[:space:]]*try[[:space:]]*\{'; then
|
|
71
|
+
in_try_catch=true
|
|
72
|
+
break
|
|
73
|
+
fi
|
|
74
|
+
done
|
|
75
|
+
|
|
76
|
+
if [[ "$in_try_catch" == "true" ]]; then
|
|
77
|
+
continue
|
|
78
|
+
fi
|
|
79
|
+
|
|
80
|
+
# Extract function name and check if it's async
|
|
81
|
+
FUNC_NAME=$(echo "$content" | grep -oE '\w+\(\)' | head -1 | sed 's/()//')
|
|
82
|
+
|
|
83
|
+
if [[ -z "$FUNC_NAME" ]]; then
|
|
84
|
+
continue
|
|
85
|
+
fi
|
|
86
|
+
|
|
87
|
+
# Check if function is async (look for async function definition)
|
|
88
|
+
if grep -qE "async[[:space:]]+(function[[:space:]]+${FUNC_NAME}|const[[:space:]]+${FUNC_NAME}|let[[:space:]]+${FUNC_NAME}|var[[:space:]]+${FUNC_NAME})" "$FILE"; then
|
|
89
|
+
if [[ -n "$UNHANDLED" ]]; then
|
|
90
|
+
UNHANDLED="${UNHANDLED}"$'\n'"$line_num:$content"
|
|
91
|
+
else
|
|
92
|
+
UNHANDLED="$line_num:$content"
|
|
93
|
+
fi
|
|
94
|
+
fi
|
|
95
|
+
done
|
|
96
|
+
|
|
97
|
+
if [ -n "$UNHANDLED" ]; then
|
|
98
|
+
echo "⚠️ Promise Safety Warning: Potential unhandled async calls" >&2
|
|
99
|
+
echo "" >&2
|
|
100
|
+
echo "$UNHANDLED" | while IFS=: read -r line_num content; do
|
|
101
|
+
echo " Line $line_num: ${content}" >&2
|
|
102
|
+
done
|
|
103
|
+
echo "" >&2
|
|
104
|
+
echo " Recommendation: Add 'await' or '.catch()' to handle promise" >&2
|
|
105
|
+
echo " Better: Run ESLint with @typescript-eslint/no-floating-promises" >&2
|
|
106
|
+
echo "" >&2
|
|
107
|
+
exit 2
|
|
108
|
+
fi
|
|
109
|
+
|
|
110
|
+
exit 0
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import ast
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
class AsyncSafetyVisitor(ast.NodeVisitor):
|
|
6
|
+
def __init__(self, debug=False):
|
|
7
|
+
self.async_functions = {}
|
|
8
|
+
self.unsafe_calls = []
|
|
9
|
+
self.node_parents = {}
|
|
10
|
+
self.debug = debug
|
|
11
|
+
|
|
12
|
+
def visit(self, node):
|
|
13
|
+
# Recursive parent tracking
|
|
14
|
+
for child in ast.iter_child_nodes(node):
|
|
15
|
+
self.node_parents[child] = node
|
|
16
|
+
super().visit(node)
|
|
17
|
+
|
|
18
|
+
def get_parent(self, node, node_type=None):
|
|
19
|
+
"""Recursively find parent of specified type"""
|
|
20
|
+
current = self.node_parents.get(node)
|
|
21
|
+
while current:
|
|
22
|
+
if node_type is None or isinstance(current, node_type):
|
|
23
|
+
return current
|
|
24
|
+
current = self.node_parents.get(current)
|
|
25
|
+
return None
|
|
26
|
+
|
|
27
|
+
def is_awaited(self, node):
|
|
28
|
+
"""Check if node is directly inside an Await context"""
|
|
29
|
+
# Parent chain traversal to find await
|
|
30
|
+
parent = self.get_parent(node)
|
|
31
|
+
while parent:
|
|
32
|
+
if isinstance(parent, ast.Await):
|
|
33
|
+
return True
|
|
34
|
+
parent = self.get_parent(parent)
|
|
35
|
+
return False
|
|
36
|
+
|
|
37
|
+
def is_safe_call(self, node):
|
|
38
|
+
"""Detect safe async calls"""
|
|
39
|
+
safe_async_funcs = {'create_task', 'gather'}
|
|
40
|
+
safe_modules = {'asyncio'}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
isinstance(node.func, ast.Attribute) and
|
|
44
|
+
node.func.attr in safe_async_funcs and
|
|
45
|
+
(node.func.value.id if isinstance(node.func.value, ast.Name) else None) in safe_modules
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
def visit_AsyncFunctionDef(self, node):
|
|
49
|
+
"""Track async function contexts"""
|
|
50
|
+
self.async_functions[node] = {
|
|
51
|
+
'name': node.name,
|
|
52
|
+
'context': node
|
|
53
|
+
}
|
|
54
|
+
self.generic_visit(node)
|
|
55
|
+
|
|
56
|
+
def visit_Call(self, node):
|
|
57
|
+
"""Detect unsafe async calls"""
|
|
58
|
+
# Skip if not in async function
|
|
59
|
+
async_context = next(
|
|
60
|
+
(func for func, details in self.async_functions.items()
|
|
61
|
+
if node in ast.walk(details['context'])),
|
|
62
|
+
None
|
|
63
|
+
)
|
|
64
|
+
if not async_context:
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
# Skip if it's a known safe call
|
|
68
|
+
if self.is_safe_call(node):
|
|
69
|
+
return
|
|
70
|
+
|
|
71
|
+
# Detect function name
|
|
72
|
+
func_name = (
|
|
73
|
+
node.func.attr if isinstance(node.func, ast.Attribute)
|
|
74
|
+
else node.func.id if isinstance(node.func, ast.Name)
|
|
75
|
+
else 'Unknown'
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Detect if call is awaited
|
|
79
|
+
if not self.is_awaited(node):
|
|
80
|
+
if self.debug:
|
|
81
|
+
print(f"Debugging - Unsafe Call: {ast.dump(node)}")
|
|
82
|
+
print(f"Function: {func_name}, Awaited: False")
|
|
83
|
+
|
|
84
|
+
self.unsafe_calls.append(
|
|
85
|
+
f"Line {node.lineno}: Async function '{async_context.name}' calls '{func_name}' without await"
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
self.generic_visit(node)
|
|
89
|
+
|
|
90
|
+
def validate_async_safety(file_path, debug=False):
|
|
91
|
+
try:
|
|
92
|
+
with open(file_path, 'r') as f:
|
|
93
|
+
content = f.read()
|
|
94
|
+
tree = ast.parse(content, filename=file_path)
|
|
95
|
+
except SyntaxError as e:
|
|
96
|
+
print(f"Syntax error in {file_path}: {e}")
|
|
97
|
+
return 1
|
|
98
|
+
except FileNotFoundError:
|
|
99
|
+
print(f"File not found: {file_path}")
|
|
100
|
+
return 1
|
|
101
|
+
|
|
102
|
+
visitor = AsyncSafetyVisitor(debug=debug)
|
|
103
|
+
visitor.visit(tree)
|
|
104
|
+
|
|
105
|
+
if visitor.unsafe_calls:
|
|
106
|
+
print("Async safety warnings:")
|
|
107
|
+
for call in visitor.unsafe_calls:
|
|
108
|
+
print(call)
|
|
109
|
+
return 2
|
|
110
|
+
|
|
111
|
+
return 0
|
|
112
|
+
|
|
113
|
+
def main():
|
|
114
|
+
if len(sys.argv) < 2:
|
|
115
|
+
print("Usage: python3 python-async-safety.py <python_file>")
|
|
116
|
+
return 1
|
|
117
|
+
|
|
118
|
+
file_path = sys.argv[1]
|
|
119
|
+
debug = "--debug" in sys.argv
|
|
120
|
+
result = validate_async_safety(file_path, debug=debug)
|
|
121
|
+
sys.exit(result)
|
|
122
|
+
|
|
123
|
+
if __name__ == '__main__':
|
|
124
|
+
main()
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import ast
|
|
3
|
+
import sys
|
|
4
|
+
import importlib.util
|
|
5
|
+
import importlib.machinery
|
|
6
|
+
|
|
7
|
+
class ImportChecker(ast.NodeVisitor):
|
|
8
|
+
def __init__(self, debug=False):
|
|
9
|
+
self.imported_modules = set()
|
|
10
|
+
self.used_modules = {}
|
|
11
|
+
self.debug = debug
|
|
12
|
+
|
|
13
|
+
def visit_Name(self, node):
|
|
14
|
+
if isinstance(node.ctx, ast.Load):
|
|
15
|
+
# Specific modules to check
|
|
16
|
+
specific_modules = {'json', 'requests', 'numpy', 'np'}
|
|
17
|
+
|
|
18
|
+
if node.id in specific_modules and node.id not in self.imported_modules:
|
|
19
|
+
self.used_modules[node.id] = node.lineno
|
|
20
|
+
self.generic_visit(node)
|
|
21
|
+
|
|
22
|
+
def visit_Import(self, node):
|
|
23
|
+
for alias in node.names:
|
|
24
|
+
module_name = alias.name.split('.')[0]
|
|
25
|
+
self.imported_modules.add(module_name)
|
|
26
|
+
if alias.asname:
|
|
27
|
+
self.imported_modules.add(alias.asname)
|
|
28
|
+
if self.debug:
|
|
29
|
+
print(f"Imported module: {module_name}")
|
|
30
|
+
self.generic_visit(node)
|
|
31
|
+
|
|
32
|
+
def visit_ImportFrom(self, node):
|
|
33
|
+
if node.module:
|
|
34
|
+
base_module = node.module.split('.')[0]
|
|
35
|
+
self.imported_modules.add(base_module)
|
|
36
|
+
|
|
37
|
+
# Capture aliases for modules like 'numpy as np'
|
|
38
|
+
for alias in node.names:
|
|
39
|
+
if alias.asname:
|
|
40
|
+
self.imported_modules.add(alias.asname)
|
|
41
|
+
|
|
42
|
+
if self.debug:
|
|
43
|
+
print(f"Imported from module: {base_module}")
|
|
44
|
+
self.generic_visit(node)
|
|
45
|
+
|
|
46
|
+
def is_module_resolvable(module_name, debug=False):
|
|
47
|
+
"""Specific import detection for known modules"""
|
|
48
|
+
|
|
49
|
+
# Always check specific known modules
|
|
50
|
+
specific_modules = {
|
|
51
|
+
'json', 'requests', 'numpy', 'np',
|
|
52
|
+
'pandas', 'scipy', 'sklearn', 'matplotlib'
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if module_name in specific_modules:
|
|
56
|
+
return False
|
|
57
|
+
|
|
58
|
+
# Standard library
|
|
59
|
+
standard_modules = {
|
|
60
|
+
'sys', 'os', 're', 'typing', 'collections', 'dataclasses',
|
|
61
|
+
'datetime', 'math', 'asyncio', 'argparse', 'random',
|
|
62
|
+
'functools', 'itertools', 'enum', 'pathlib', 'subprocess'
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if module_name in standard_modules:
|
|
66
|
+
return True
|
|
67
|
+
|
|
68
|
+
# Fallback resolution strategies
|
|
69
|
+
try:
|
|
70
|
+
return importlib.util.find_spec(module_name) is not None
|
|
71
|
+
except (ImportError, AttributeError):
|
|
72
|
+
return False
|
|
73
|
+
|
|
74
|
+
def validate_imports(file_path, debug=False):
|
|
75
|
+
try:
|
|
76
|
+
with open(file_path, 'r') as f:
|
|
77
|
+
tree = ast.parse(f.read(), filename=file_path)
|
|
78
|
+
except SyntaxError as e:
|
|
79
|
+
print(f"Syntax error in {file_path}: {e}")
|
|
80
|
+
return 1
|
|
81
|
+
except FileNotFoundError:
|
|
82
|
+
print(f"File not found: {file_path}")
|
|
83
|
+
return 1
|
|
84
|
+
|
|
85
|
+
visitor = ImportChecker(debug=debug)
|
|
86
|
+
visitor.visit(tree)
|
|
87
|
+
|
|
88
|
+
# Check for unresolved modules
|
|
89
|
+
unresolved_modules = [
|
|
90
|
+
(module, lineno) for module, lineno in visitor.used_modules.items()
|
|
91
|
+
if not is_module_resolvable(module, debug=debug)
|
|
92
|
+
]
|
|
93
|
+
|
|
94
|
+
# Validation
|
|
95
|
+
if unresolved_modules:
|
|
96
|
+
print("Import resolution warnings:")
|
|
97
|
+
for module, lineno in unresolved_modules:
|
|
98
|
+
print(f"Line {lineno}: Unable to resolve import: {module}")
|
|
99
|
+
return 2
|
|
100
|
+
|
|
101
|
+
return 0
|
|
102
|
+
|
|
103
|
+
def main():
|
|
104
|
+
if len(sys.argv) < 2:
|
|
105
|
+
print("Usage: python3 python-import-checker.py <python_file>")
|
|
106
|
+
return 1
|
|
107
|
+
|
|
108
|
+
file_path = sys.argv[1]
|
|
109
|
+
debug = "--debug" in sys.argv
|
|
110
|
+
result = validate_imports(file_path, debug=debug)
|
|
111
|
+
sys.exit(result)
|
|
112
|
+
|
|
113
|
+
if __name__ == '__main__':
|
|
114
|
+
main()
|