@cristiancorreau/forge 2.1.0
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/CHANGELOG.md +228 -0
- package/LICENSE +191 -0
- package/README.md +156 -0
- package/assets/adapters/claude-code/commands/deploy-check.md +12 -0
- package/assets/adapters/claude-code/commands/new-feature.md +11 -0
- package/assets/adapters/claude-code/commands/plan.md +116 -0
- package/assets/adapters/claude-code/commands/review.md +219 -0
- package/assets/adapters/claude-code/commands/session-close.md +109 -0
- package/assets/adapters/claude-code/commands/session-start.md +59 -0
- package/assets/adapters/claude-code/commands/ship.md +133 -0
- package/assets/adapters/claude-code/commands/wiki-ingest.md +7 -0
- package/assets/adapters/claude-code/commands/wiki-lint.md +5 -0
- package/assets/adapters/claude-code/commands/wiki-query.md +7 -0
- package/assets/adapters/claude-code/commands/work.md +101 -0
- package/assets/adapters/claude-code/generate-claude-md.py +304 -0
- package/assets/adapters/codex/commands/plan.md +63 -0
- package/assets/adapters/codex/commands/review.md +53 -0
- package/assets/adapters/codex/commands/session-close.md +53 -0
- package/assets/adapters/codex/commands/session-start.md +49 -0
- package/assets/adapters/codex/commands/ship.md +53 -0
- package/assets/adapters/codex/commands/work.md +53 -0
- package/assets/adapters/codex/generate-codex-config.py +269 -0
- package/assets/adapters/codex/hooks/codex.yaml.tpl +43 -0
- package/assets/adapters/codex/hooks/forge-codex-finish.sh +158 -0
- package/assets/adapters/codex/hooks/forge-codex-start.sh +186 -0
- package/assets/adapters/kiro/generate-steering.py +367 -0
- package/assets/adapters/opencode/HOOKS.md +123 -0
- package/assets/adapters/opencode/commands/plan.md +119 -0
- package/assets/adapters/opencode/commands/review.md +164 -0
- package/assets/adapters/opencode/commands/session-close.md +111 -0
- package/assets/adapters/opencode/commands/session-start.md +62 -0
- package/assets/adapters/opencode/commands/ship.md +135 -0
- package/assets/adapters/opencode/commands/work.md +82 -0
- package/assets/adapters/opencode/generate-agents-md.py +262 -0
- package/assets/core/agents/backend-engineer.md +61 -0
- package/assets/core/agents/compliance-reviewer.md +83 -0
- package/assets/core/agents/docs-writer.md +77 -0
- package/assets/core/agents/frontend-engineer.md +70 -0
- package/assets/core/agents/orchestrator.md +104 -0
- package/assets/core/agents/security-auditor.md +54 -0
- package/assets/core/agents/test-engineer.md +57 -0
- package/assets/core/hooks/hooks-registry.yaml +48 -0
- package/assets/core/hooks/post-turn-check.sh +139 -0
- package/assets/core/hooks/pre-bash-check.py +202 -0
- package/assets/core/hooks/pre-edit-check.py +317 -0
- package/assets/core/hooks/session-start.sh +184 -0
- package/assets/core/schemas/project.schema.json +503 -0
- package/assets/core/skills/README.md +88 -0
- package/assets/core/skills/aitmpl-search/SKILL.md +74 -0
- package/assets/core/skills/browser-test/SKILL.md +177 -0
- package/assets/core/skills/db-migrate/SKILL.md +163 -0
- package/assets/core/skills/local2prod/SKILL.md +147 -0
- package/assets/core/skills/new-feature/SKILL.md +155 -0
- package/assets/core/skills/obsidian-sync/SKILL.md +152 -0
- package/assets/core/skills/phase-kickoff/SKILL.md +69 -0
- package/assets/core/skills/security-audit/SKILL.md +125 -0
- package/assets/core/skills/spec/SKILL.md +72 -0
- package/assets/core/skills/wiki-ingest/SKILL.md +183 -0
- package/assets/core/skills/wiki-lint/SKILL.md +109 -0
- package/assets/core/skills/wiki-query/SKILL.md +100 -0
- package/assets/core/templates/claude-md/architecture.rules +20 -0
- package/assets/core/templates/claude-md/global.md +30 -0
- package/assets/core/templates/claude-md/project.md +36 -0
- package/assets/core/templates/daily-note.md +38 -0
- package/assets/core/templates/spec-template.md +43 -0
- package/assets/core/workflows/sdd.md +69 -0
- package/assets/core/workflows/sprint.md +59 -0
- package/assets/forge.py +1265 -0
- package/assets/hooks/pre-commit +43 -0
- package/assets/manifest.json +274 -0
- package/assets/profiles/astro/README.md +24 -0
- package/assets/profiles/astro/agents/frontend-engineer.md +74 -0
- package/assets/profiles/django/agents/api-engineer.md +83 -0
- package/assets/profiles/expo/README.md +24 -0
- package/assets/profiles/expo/agents/mobile-engineer.md +69 -0
- package/assets/profiles/express/agents/api-engineer.md +60 -0
- package/assets/profiles/fastapi/README.md +32 -0
- package/assets/profiles/fastapi/agents/api-engineer.md +87 -0
- package/assets/profiles/go-gin/agents/api-engineer.md +98 -0
- package/assets/profiles/hono-drizzle/README.md +31 -0
- package/assets/profiles/hono-drizzle/agents/api-engineer.md +82 -0
- package/assets/profiles/laravel/README.md +32 -0
- package/assets/profiles/laravel/agents/api-engineer.md +114 -0
- package/assets/profiles/laravel/agents/fullstack-engineer.md +67 -0
- package/assets/profiles/laravel/agents/migration-specialist.md +420 -0
- package/assets/profiles/nestjs/agents/api-engineer.md +79 -0
- package/assets/profiles/nextjs-admin/README.md +32 -0
- package/assets/profiles/nextjs-admin/agents/admin-engineer.md +78 -0
- package/assets/profiles/playwright-crawler/agents/scanner-engineer.md +51 -0
- package/assets/profiles/rails/agents/fullstack-engineer.md +61 -0
- package/assets/profiles/sveltekit/agents/frontend-engineer.md +96 -0
- package/assets/profiles/vuenuxt/agents/frontend-engineer.md +82 -0
- package/assets/profiles/wordpress/README.md +30 -0
- package/assets/profiles/wordpress/agents/divi-engineer.md +273 -0
- package/assets/profiles/wordpress/agents/elementor-engineer.md +310 -0
- package/assets/profiles/wordpress/agents/wp-engineer.md +216 -0
- package/assets/requirements.txt +2 -0
- package/assets/scripts/aitmpl-search.py +808 -0
- package/assets/scripts/forge-add-opportunities.py +92 -0
- package/assets/scripts/forge-audit.py +1061 -0
- package/assets/scripts/forge-generate-all.py +283 -0
- package/assets/scripts/forge-init.py +900 -0
- package/assets/scripts/forge-migrate-project-yaml.py +397 -0
- package/assets/scripts/forge-scaffold-profile.py +181 -0
- package/assets/scripts/forge-teardown.py +193 -0
- package/assets/scripts/forge-validate-project-yaml.py +457 -0
- package/assets/scripts/forge-wizard.py +1003 -0
- package/assets/scripts/setup-codex.sh +229 -0
- package/assets/scripts/team-install.sh +147 -0
- package/assets/scripts/token-stats.py +201 -0
- package/assets/templates/modes/enterprise.yaml.tpl +114 -0
- package/assets/templates/modes/multi-runtime.yaml.tpl +89 -0
- package/assets/templates/modes/new-stack.yaml.tpl +101 -0
- package/assets/templates/modes/startup.yaml.tpl +74 -0
- package/assets/templates/project.yaml.tpl +185 -0
- package/assets/templates/wiki/concepts/_template.md +22 -0
- package/assets/templates/wiki/entities/_template.md +19 -0
- package/assets/templates/wiki/index.md +32 -0
- package/assets/templates/wiki/log.md +6 -0
- package/assets/templates/wiki/sources/_template.md +25 -0
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +64 -0
- package/dist/cli.js.map +1 -0
- package/dist/commands/audit.d.ts +2 -0
- package/dist/commands/audit.d.ts.map +1 -0
- package/dist/commands/audit.js +21 -0
- package/dist/commands/audit.js.map +1 -0
- package/dist/commands/doctor.d.ts +2 -0
- package/dist/commands/doctor.d.ts.map +1 -0
- package/dist/commands/doctor.js +58 -0
- package/dist/commands/doctor.js.map +1 -0
- package/dist/commands/generate.d.ts +2 -0
- package/dist/commands/generate.d.ts.map +1 -0
- package/dist/commands/generate.js +27 -0
- package/dist/commands/generate.js.map +1 -0
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.d.ts.map +1 -0
- package/dist/commands/init.js +22 -0
- package/dist/commands/init.js.map +1 -0
- package/dist/commands/validate.d.ts +2 -0
- package/dist/commands/validate.d.ts.map +1 -0
- package/dist/commands/validate.js +20 -0
- package/dist/commands/validate.js.map +1 -0
- package/dist/lib/paths.d.ts +10 -0
- package/dist/lib/paths.d.ts.map +1 -0
- package/dist/lib/paths.js +49 -0
- package/dist/lib/paths.js.map +1 -0
- package/dist/lib/python.d.ts +4 -0
- package/dist/lib/python.d.ts.map +1 -0
- package/dist/lib/python.js +46 -0
- package/dist/lib/python.js.map +1 -0
- package/package.json +46 -0
|
@@ -0,0 +1,317 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Forge v2 — PreToolUse hook: pre-edit-check.py
|
|
4
|
+
Enforces branch guard, debug detection, and secret detection before file edits.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
import json
|
|
8
|
+
import os
|
|
9
|
+
import re
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
DEBUG = os.environ.get("DEBUG", "") not in ("", "0", "false", "False")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def dbg(msg):
|
|
18
|
+
if DEBUG:
|
|
19
|
+
print(f"[forge-hook-debug] {msg}", flush=True)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def load_project_yaml():
|
|
23
|
+
"""Walk up from cwd to find project.yaml. Returns dict or {}."""
|
|
24
|
+
try:
|
|
25
|
+
import yaml
|
|
26
|
+
path = os.getcwd()
|
|
27
|
+
for _ in range(6):
|
|
28
|
+
candidate = os.path.join(path, "project.yaml")
|
|
29
|
+
if os.path.isfile(candidate):
|
|
30
|
+
with open(candidate) as f:
|
|
31
|
+
data = yaml.safe_load(f)
|
|
32
|
+
return data if isinstance(data, dict) else {}
|
|
33
|
+
parent = os.path.dirname(path)
|
|
34
|
+
if parent == path:
|
|
35
|
+
break
|
|
36
|
+
path = parent
|
|
37
|
+
except Exception as e:
|
|
38
|
+
dbg(f"project.yaml load error: {e}")
|
|
39
|
+
return {}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# ---------------------------------------------------------------------------
|
|
43
|
+
# File classification helpers
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
CODE_EXTENSIONS = {
|
|
47
|
+
".py", ".ts", ".js", ".tsx", ".jsx",
|
|
48
|
+
".php", ".rb", ".go", ".rs", ".java",
|
|
49
|
+
".cs", ".cpp", ".c", ".sh",
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
NON_CODE_EXTENSIONS = {
|
|
53
|
+
".md", ".yaml", ".yml", ".json", ".toml", ".txt", ".lock",
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
ROOT_PROTECTED_NAMES = {"README.md", "CLAUDE.md", "CHANGELOG.md"}
|
|
57
|
+
|
|
58
|
+
PROTECTED_DIRS = ("docs/", ".claude/")
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def is_code_file(file_path):
|
|
62
|
+
"""Return True if the file path is considered a code file."""
|
|
63
|
+
_, ext = os.path.splitext(file_path)
|
|
64
|
+
if ext.lower() in CODE_EXTENSIONS:
|
|
65
|
+
return True
|
|
66
|
+
if ext.lower() in NON_CODE_EXTENSIONS:
|
|
67
|
+
return False
|
|
68
|
+
# Default: treat unknown extensions as non-code (safe)
|
|
69
|
+
return False
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def is_exempt_from_branch_guard(file_path):
|
|
73
|
+
"""Return True if the file should be exempt from branch-guard blocking."""
|
|
74
|
+
norm = file_path.replace("\\", "/")
|
|
75
|
+
# Exempt protected dirs
|
|
76
|
+
for d in PROTECTED_DIRS:
|
|
77
|
+
if norm.startswith(d) or f"/{d.rstrip('/')}" in norm:
|
|
78
|
+
return True
|
|
79
|
+
# Exempt root-level protected names
|
|
80
|
+
basename = os.path.basename(norm)
|
|
81
|
+
if basename in ROOT_PROTECTED_NAMES:
|
|
82
|
+
return True
|
|
83
|
+
# Exempt root-level *.md files
|
|
84
|
+
if basename.endswith(".md") and "/" not in norm.lstrip("./"):
|
|
85
|
+
return True
|
|
86
|
+
# Exempt root-level *.yaml / *.json config files
|
|
87
|
+
if "/" not in norm.lstrip("./"):
|
|
88
|
+
_, ext = os.path.splitext(basename)
|
|
89
|
+
if ext.lower() in (".yaml", ".yml", ".json"):
|
|
90
|
+
return True
|
|
91
|
+
return False
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
# ---------------------------------------------------------------------------
|
|
95
|
+
# Check 1 — Branch guard
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
|
|
98
|
+
PROTECTED_BRANCHES = {"main", "master", "develop"}
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def check_branch_guard(file_path):
|
|
102
|
+
"""Block code edits on protected branches."""
|
|
103
|
+
try:
|
|
104
|
+
result = subprocess.run(
|
|
105
|
+
["git", "branch", "--show-current"],
|
|
106
|
+
capture_output=True,
|
|
107
|
+
text=True,
|
|
108
|
+
timeout=5,
|
|
109
|
+
)
|
|
110
|
+
branch = result.stdout.strip()
|
|
111
|
+
dbg(f"current branch: {branch!r}")
|
|
112
|
+
except Exception as e:
|
|
113
|
+
dbg(f"git branch error: {e}")
|
|
114
|
+
return None
|
|
115
|
+
|
|
116
|
+
if branch not in PROTECTED_BRANCHES:
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
if not is_code_file(file_path):
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
if is_exempt_from_branch_guard(file_path):
|
|
123
|
+
return None
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
f"forge: edición bloqueada en {branch}. Crea una feature branch:\n"
|
|
127
|
+
f" git checkout -b feature/<tema>-$(date +%Y-%m-%d)"
|
|
128
|
+
)
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# Check 2 — Debug statements
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
|
|
135
|
+
def check_debug_statements(file_path, content):
|
|
136
|
+
"""Warn (not block) if debug statements are found in new content."""
|
|
137
|
+
_, ext = os.path.splitext(file_path)
|
|
138
|
+
ext = ext.lower()
|
|
139
|
+
|
|
140
|
+
if ext not in CODE_EXTENSIONS:
|
|
141
|
+
return None
|
|
142
|
+
|
|
143
|
+
found = False
|
|
144
|
+
basename = os.path.basename(file_path)
|
|
145
|
+
norm = file_path.replace("\\", "/")
|
|
146
|
+
|
|
147
|
+
if ext in (".ts", ".js", ".tsx", ".jsx"):
|
|
148
|
+
if "console.log(" in content or "debugger;" in content:
|
|
149
|
+
found = True
|
|
150
|
+
|
|
151
|
+
elif ext == ".php":
|
|
152
|
+
if "var_dump(" in content or "dd(" in content or "print_r(" in content:
|
|
153
|
+
found = True
|
|
154
|
+
|
|
155
|
+
elif ext == ".py":
|
|
156
|
+
# Skip forge scripts and .agentic/ files
|
|
157
|
+
is_forge_script = basename.startswith("forge") and basename.endswith(".py")
|
|
158
|
+
in_agentic = ".agentic/" in norm
|
|
159
|
+
if not is_forge_script and not in_agentic:
|
|
160
|
+
if "print(" in content:
|
|
161
|
+
found = True
|
|
162
|
+
|
|
163
|
+
elif ext == ".rb":
|
|
164
|
+
if re.search(r"^\s*(puts |pp |p )", content, re.MULTILINE):
|
|
165
|
+
found = True
|
|
166
|
+
|
|
167
|
+
if found:
|
|
168
|
+
return (
|
|
169
|
+
f"forge: debug statement detectado en {file_path}"
|
|
170
|
+
" — recuerda quitarlo antes del commit"
|
|
171
|
+
)
|
|
172
|
+
return None
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
# ---------------------------------------------------------------------------
|
|
176
|
+
# Check 3 — Secret detection
|
|
177
|
+
# ---------------------------------------------------------------------------
|
|
178
|
+
|
|
179
|
+
SECRET_PATTERN = re.compile(
|
|
180
|
+
r'(password|passwd|secret|api_key|apikey|token|private_key)\s*[=:]\s*["\'][^"\']{8,}["\']',
|
|
181
|
+
re.IGNORECASE,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
LONG_SECRET_PATTERN = re.compile(
|
|
185
|
+
r'\b(key|secret|token|password|auth|api_key|apikey)\b\s*[=:]\s*["\'][A-Za-z0-9+/=_\-]{20,}["\']',
|
|
186
|
+
re.IGNORECASE,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
EXEMPT_EXTENSIONS = {".md"}
|
|
190
|
+
EXEMPT_SUFFIXES = (".env.example", ".env.sample")
|
|
191
|
+
TEST_PATTERNS = re.compile(r'\.(test|spec)\.')
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def is_exempt_from_secret_check(file_path):
|
|
195
|
+
norm = file_path.replace("\\", "/")
|
|
196
|
+
basename = os.path.basename(norm)
|
|
197
|
+
_, ext = os.path.splitext(basename)
|
|
198
|
+
|
|
199
|
+
if ext.lower() in EXEMPT_EXTENSIONS:
|
|
200
|
+
return True
|
|
201
|
+
for suffix in EXEMPT_SUFFIXES:
|
|
202
|
+
if norm.endswith(suffix):
|
|
203
|
+
return True
|
|
204
|
+
if TEST_PATTERNS.search(basename):
|
|
205
|
+
return True
|
|
206
|
+
return False
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def check_secret_detection(file_path, content):
|
|
210
|
+
"""Block if hardcoded credentials are detected."""
|
|
211
|
+
if is_exempt_from_secret_check(file_path):
|
|
212
|
+
return None
|
|
213
|
+
|
|
214
|
+
if SECRET_PATTERN.search(content) or LONG_SECRET_PATTERN.search(content):
|
|
215
|
+
return (
|
|
216
|
+
f"forge: posible credencial hardcodeada detectada en {file_path}."
|
|
217
|
+
" Usa variables de entorno."
|
|
218
|
+
)
|
|
219
|
+
return None
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
# ---------------------------------------------------------------------------
|
|
223
|
+
# project.yaml — custom forbidden patterns + enterprise mode
|
|
224
|
+
# ---------------------------------------------------------------------------
|
|
225
|
+
|
|
226
|
+
def check_project_yaml_patterns(file_path, content, project):
|
|
227
|
+
"""Check project.yaml forbidden_patterns if present."""
|
|
228
|
+
try:
|
|
229
|
+
rules = project.get("rules", {})
|
|
230
|
+
forbidden = rules.get("forbidden_patterns", [])
|
|
231
|
+
if not isinstance(forbidden, list):
|
|
232
|
+
return None
|
|
233
|
+
for pattern in forbidden:
|
|
234
|
+
if re.search(pattern, content):
|
|
235
|
+
return (
|
|
236
|
+
f"forge: patrón prohibido detectado en {file_path} "
|
|
237
|
+
f"(regla: {pattern!r})"
|
|
238
|
+
)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
dbg(f"project.yaml patterns error: {e}")
|
|
241
|
+
return None
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
# ---------------------------------------------------------------------------
|
|
245
|
+
# Main
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
|
|
248
|
+
def main():
|
|
249
|
+
try:
|
|
250
|
+
raw = sys.stdin.read()
|
|
251
|
+
if not raw.strip():
|
|
252
|
+
dbg("empty stdin, allowing")
|
|
253
|
+
sys.exit(0)
|
|
254
|
+
|
|
255
|
+
data = json.loads(raw)
|
|
256
|
+
except Exception as e:
|
|
257
|
+
dbg(f"stdin parse error: {e}")
|
|
258
|
+
sys.exit(0)
|
|
259
|
+
|
|
260
|
+
try:
|
|
261
|
+
tool_name = data.get("tool_name", "")
|
|
262
|
+
tool_input = data.get("tool_input", {})
|
|
263
|
+
|
|
264
|
+
file_path = tool_input.get("file_path", "")
|
|
265
|
+
if not file_path:
|
|
266
|
+
sys.exit(0)
|
|
267
|
+
|
|
268
|
+
# Determine new content being written
|
|
269
|
+
if tool_name == "Write":
|
|
270
|
+
new_content = tool_input.get("content", "")
|
|
271
|
+
elif tool_name == "Edit":
|
|
272
|
+
new_content = tool_input.get("new_string", "")
|
|
273
|
+
else:
|
|
274
|
+
sys.exit(0)
|
|
275
|
+
|
|
276
|
+
dbg(f"tool={tool_name} file={file_path} content_len={len(new_content)}")
|
|
277
|
+
|
|
278
|
+
project = load_project_yaml()
|
|
279
|
+
enterprise_mode = project.get("mode", "") == "enterprise"
|
|
280
|
+
|
|
281
|
+
# Check 1 — Branch guard
|
|
282
|
+
block_msg = check_branch_guard(file_path)
|
|
283
|
+
if block_msg:
|
|
284
|
+
print(block_msg, flush=True)
|
|
285
|
+
sys.exit(2)
|
|
286
|
+
|
|
287
|
+
# Check 2 — Debug statements
|
|
288
|
+
warn_msg = check_debug_statements(file_path, new_content)
|
|
289
|
+
if warn_msg:
|
|
290
|
+
if enterprise_mode:
|
|
291
|
+
print(warn_msg, flush=True)
|
|
292
|
+
sys.exit(2)
|
|
293
|
+
else:
|
|
294
|
+
print(warn_msg, flush=True)
|
|
295
|
+
# fall through — warning only
|
|
296
|
+
|
|
297
|
+
# Check 3 — Secret detection
|
|
298
|
+
block_msg = check_secret_detection(file_path, new_content)
|
|
299
|
+
if block_msg:
|
|
300
|
+
print(block_msg, flush=True)
|
|
301
|
+
sys.exit(2)
|
|
302
|
+
|
|
303
|
+
# project.yaml forbidden patterns
|
|
304
|
+
block_msg = check_project_yaml_patterns(file_path, new_content, project)
|
|
305
|
+
if block_msg:
|
|
306
|
+
print(block_msg, flush=True)
|
|
307
|
+
sys.exit(2)
|
|
308
|
+
|
|
309
|
+
except Exception as e:
|
|
310
|
+
dbg(f"unexpected error: {e}")
|
|
311
|
+
sys.exit(0)
|
|
312
|
+
|
|
313
|
+
sys.exit(0)
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
if __name__ == "__main__":
|
|
317
|
+
main()
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Forge v2 — SessionStart hook: verifica ambiente antes de cada sesión.
|
|
3
|
+
#
|
|
4
|
+
# Evento recomendado: UserPromptSubmit (primer mensaje) — Claude Code aún no
|
|
5
|
+
# expone SessionStart como evento de hook; UserPromptSubmit es el equivalente
|
|
6
|
+
# más cercano para verificaciones al inicio de sesión.
|
|
7
|
+
#
|
|
8
|
+
# Diferente del slash command /session-start: este hook es automático y
|
|
9
|
+
# determinístico — no requiere intervención del usuario.
|
|
10
|
+
|
|
11
|
+
set -euo pipefail
|
|
12
|
+
|
|
13
|
+
CHECKS_PASSED=0
|
|
14
|
+
CHECKS_TOTAL=0
|
|
15
|
+
OUTPUT=""
|
|
16
|
+
HAS_ERROR=false
|
|
17
|
+
|
|
18
|
+
check() {
|
|
19
|
+
local name="$1"
|
|
20
|
+
local result="$2" # "ok" | "warn: <msg>" | "error: <msg>"
|
|
21
|
+
CHECKS_TOTAL=$((CHECKS_TOTAL + 1))
|
|
22
|
+
if [[ "$result" == "ok" ]]; then
|
|
23
|
+
CHECKS_PASSED=$((CHECKS_PASSED + 1))
|
|
24
|
+
elif [[ "$result" == error:* ]]; then
|
|
25
|
+
HAS_ERROR=true
|
|
26
|
+
local msg="${result#error: }"
|
|
27
|
+
OUTPUT+=" error: ${msg}\n"
|
|
28
|
+
else
|
|
29
|
+
local msg="${result#warn: }"
|
|
30
|
+
OUTPUT+=" warn: ${msg}\n"
|
|
31
|
+
fi
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
# ---------------------------------------------------------------------------
|
|
35
|
+
# Check 1 — Herramientas básicas disponibles
|
|
36
|
+
# ---------------------------------------------------------------------------
|
|
37
|
+
if ! command -v git &>/dev/null; then
|
|
38
|
+
check "git" "error: git no está instalado o no está en PATH"
|
|
39
|
+
else
|
|
40
|
+
check "git" "ok"
|
|
41
|
+
fi
|
|
42
|
+
|
|
43
|
+
if ! command -v python3 &>/dev/null; then
|
|
44
|
+
check "python3" "error: python3 no está instalado o no está en PATH"
|
|
45
|
+
else
|
|
46
|
+
check "python3" "ok"
|
|
47
|
+
fi
|
|
48
|
+
|
|
49
|
+
# Si faltan herramientas críticas, salir ya con error
|
|
50
|
+
if [[ "$HAS_ERROR" == "true" ]]; then
|
|
51
|
+
printf "forge session: ERROR — herramientas críticas faltantes:\n"
|
|
52
|
+
printf "%b" "$OUTPUT"
|
|
53
|
+
exit 2
|
|
54
|
+
fi
|
|
55
|
+
|
|
56
|
+
# ---------------------------------------------------------------------------
|
|
57
|
+
# Check 2 — Branch actual no es main/master
|
|
58
|
+
# ---------------------------------------------------------------------------
|
|
59
|
+
CURRENT_BRANCH="$(git branch --show-current 2>/dev/null || true)"
|
|
60
|
+
if [[ "$CURRENT_BRANCH" == "main" || "$CURRENT_BRANCH" == "master" ]]; then
|
|
61
|
+
check "branch" "warn: branch '${CURRENT_BRANCH}' — considera trabajar en una feature branch"
|
|
62
|
+
else
|
|
63
|
+
check "branch" "ok"
|
|
64
|
+
fi
|
|
65
|
+
|
|
66
|
+
# ---------------------------------------------------------------------------
|
|
67
|
+
# Check 3 — Cambios sin commitear
|
|
68
|
+
# ---------------------------------------------------------------------------
|
|
69
|
+
GIT_STATUS="$(git status --short 2>/dev/null || true)"
|
|
70
|
+
if [[ -n "$GIT_STATUS" ]]; then
|
|
71
|
+
check "uncommitted" "warn: cambios sin commitear en el worktree"
|
|
72
|
+
else
|
|
73
|
+
check "uncommitted" "ok"
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
# ---------------------------------------------------------------------------
|
|
77
|
+
# Check 4 — project.yaml existe
|
|
78
|
+
# ---------------------------------------------------------------------------
|
|
79
|
+
PROJECT_YAML=""
|
|
80
|
+
SEARCH_PATH="$(pwd)"
|
|
81
|
+
for _i in 1 2 3 4 5 6; do
|
|
82
|
+
if [[ -f "${SEARCH_PATH}/project.yaml" ]]; then
|
|
83
|
+
PROJECT_YAML="${SEARCH_PATH}/project.yaml"
|
|
84
|
+
break
|
|
85
|
+
fi
|
|
86
|
+
PARENT="$(dirname "$SEARCH_PATH")"
|
|
87
|
+
[[ "$PARENT" == "$SEARCH_PATH" ]] && break
|
|
88
|
+
SEARCH_PATH="$PARENT"
|
|
89
|
+
done
|
|
90
|
+
|
|
91
|
+
if [[ -z "$PROJECT_YAML" ]]; then
|
|
92
|
+
check "project.yaml" "warn: project.yaml no encontrado — ejecutar forge-wizard.py"
|
|
93
|
+
else
|
|
94
|
+
check "project.yaml" "ok"
|
|
95
|
+
|
|
96
|
+
# ---------------------------------------------------------------------------
|
|
97
|
+
# Check 5 — project.yaml tiene project.name y project.mode
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
YAML_VALID="$(python3 - "$PROJECT_YAML" <<'PYEOF'
|
|
100
|
+
import sys, yaml
|
|
101
|
+
path = sys.argv[1]
|
|
102
|
+
try:
|
|
103
|
+
with open(path) as f:
|
|
104
|
+
data = yaml.safe_load(f)
|
|
105
|
+
if not isinstance(data, dict):
|
|
106
|
+
print("invalid")
|
|
107
|
+
sys.exit(0)
|
|
108
|
+
missing = []
|
|
109
|
+
project = data.get("project", {})
|
|
110
|
+
if not project.get("name"):
|
|
111
|
+
missing.append("project.name")
|
|
112
|
+
if not project.get("mode"):
|
|
113
|
+
missing.append("project.mode")
|
|
114
|
+
if missing:
|
|
115
|
+
print("missing:" + ",".join(missing))
|
|
116
|
+
else:
|
|
117
|
+
print("ok")
|
|
118
|
+
except Exception as e:
|
|
119
|
+
print(f"error:{e}")
|
|
120
|
+
PYEOF
|
|
121
|
+
)"
|
|
122
|
+
if [[ "$YAML_VALID" == "ok" ]]; then
|
|
123
|
+
check "project.yaml.fields" "ok"
|
|
124
|
+
elif [[ "$YAML_VALID" == missing:* ]]; then
|
|
125
|
+
MISSING_FIELDS="${YAML_VALID#missing:}"
|
|
126
|
+
check "project.yaml.fields" "warn: project.yaml faltan campos: ${MISSING_FIELDS}"
|
|
127
|
+
else
|
|
128
|
+
check "project.yaml.fields" "warn: project.yaml no se pudo parsear — verificar sintaxis"
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
# ---------------------------------------------------------------------------
|
|
132
|
+
# Check 6 — Variables de entorno de producción activas en sesión local
|
|
133
|
+
# ---------------------------------------------------------------------------
|
|
134
|
+
HAS_DEPLOY="$(python3 - "$PROJECT_YAML" <<'PYEOF'
|
|
135
|
+
import sys, yaml
|
|
136
|
+
path = sys.argv[1]
|
|
137
|
+
try:
|
|
138
|
+
with open(path) as f:
|
|
139
|
+
data = yaml.safe_load(f)
|
|
140
|
+
if isinstance(data, dict) and data.get("deploy"):
|
|
141
|
+
print("yes")
|
|
142
|
+
else:
|
|
143
|
+
print("no")
|
|
144
|
+
except Exception:
|
|
145
|
+
print("no")
|
|
146
|
+
PYEOF
|
|
147
|
+
)"
|
|
148
|
+
if [[ "$HAS_DEPLOY" == "yes" ]]; then
|
|
149
|
+
PROD_VARS=""
|
|
150
|
+
while IFS='=' read -r key _value; do
|
|
151
|
+
if [[ "$key" =~ ^(PROD_|PRODUCTION_|PROD$|PRODUCTION$) ]]; then
|
|
152
|
+
PROD_VARS+="${key} "
|
|
153
|
+
fi
|
|
154
|
+
done < <(env)
|
|
155
|
+
if [[ -n "$PROD_VARS" ]]; then
|
|
156
|
+
check "prod-env" "warn: variables de producción activas en sesión: ${PROD_VARS// /, } — verificar que es intencional"
|
|
157
|
+
else
|
|
158
|
+
check "prod-env" "ok"
|
|
159
|
+
fi
|
|
160
|
+
else
|
|
161
|
+
check "prod-env" "ok"
|
|
162
|
+
fi
|
|
163
|
+
fi
|
|
164
|
+
|
|
165
|
+
# ---------------------------------------------------------------------------
|
|
166
|
+
# Salida
|
|
167
|
+
# ---------------------------------------------------------------------------
|
|
168
|
+
WARNINGS=$((CHECKS_TOTAL - CHECKS_PASSED))
|
|
169
|
+
|
|
170
|
+
if [[ $WARNINGS -eq 0 ]]; then
|
|
171
|
+
# Silencioso — todo OK
|
|
172
|
+
exit 0
|
|
173
|
+
fi
|
|
174
|
+
|
|
175
|
+
# Construir etiquetas para el resumen
|
|
176
|
+
LABELS=""
|
|
177
|
+
[[ "$CURRENT_BRANCH" == "main" || "$CURRENT_BRANCH" == "master" ]] && LABELS+="[branch ${CURRENT_BRANCH}] "
|
|
178
|
+
[[ -n "$GIT_STATUS" ]] && LABELS+="[cambios sin commitear] "
|
|
179
|
+
[[ -z "$PROJECT_YAML" ]] && LABELS+="[sin project.yaml] "
|
|
180
|
+
|
|
181
|
+
printf "forge session: %d advertencia(s) — %s\n" "$WARNINGS" "${LABELS%% }"
|
|
182
|
+
printf "%b" "$OUTPUT"
|
|
183
|
+
|
|
184
|
+
exit 0
|