@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.
Files changed (153) hide show
  1. package/CHANGELOG.md +228 -0
  2. package/LICENSE +191 -0
  3. package/README.md +156 -0
  4. package/assets/adapters/claude-code/commands/deploy-check.md +12 -0
  5. package/assets/adapters/claude-code/commands/new-feature.md +11 -0
  6. package/assets/adapters/claude-code/commands/plan.md +116 -0
  7. package/assets/adapters/claude-code/commands/review.md +219 -0
  8. package/assets/adapters/claude-code/commands/session-close.md +109 -0
  9. package/assets/adapters/claude-code/commands/session-start.md +59 -0
  10. package/assets/adapters/claude-code/commands/ship.md +133 -0
  11. package/assets/adapters/claude-code/commands/wiki-ingest.md +7 -0
  12. package/assets/adapters/claude-code/commands/wiki-lint.md +5 -0
  13. package/assets/adapters/claude-code/commands/wiki-query.md +7 -0
  14. package/assets/adapters/claude-code/commands/work.md +101 -0
  15. package/assets/adapters/claude-code/generate-claude-md.py +304 -0
  16. package/assets/adapters/codex/commands/plan.md +63 -0
  17. package/assets/adapters/codex/commands/review.md +53 -0
  18. package/assets/adapters/codex/commands/session-close.md +53 -0
  19. package/assets/adapters/codex/commands/session-start.md +49 -0
  20. package/assets/adapters/codex/commands/ship.md +53 -0
  21. package/assets/adapters/codex/commands/work.md +53 -0
  22. package/assets/adapters/codex/generate-codex-config.py +269 -0
  23. package/assets/adapters/codex/hooks/codex.yaml.tpl +43 -0
  24. package/assets/adapters/codex/hooks/forge-codex-finish.sh +158 -0
  25. package/assets/adapters/codex/hooks/forge-codex-start.sh +186 -0
  26. package/assets/adapters/kiro/generate-steering.py +367 -0
  27. package/assets/adapters/opencode/HOOKS.md +123 -0
  28. package/assets/adapters/opencode/commands/plan.md +119 -0
  29. package/assets/adapters/opencode/commands/review.md +164 -0
  30. package/assets/adapters/opencode/commands/session-close.md +111 -0
  31. package/assets/adapters/opencode/commands/session-start.md +62 -0
  32. package/assets/adapters/opencode/commands/ship.md +135 -0
  33. package/assets/adapters/opencode/commands/work.md +82 -0
  34. package/assets/adapters/opencode/generate-agents-md.py +262 -0
  35. package/assets/core/agents/backend-engineer.md +61 -0
  36. package/assets/core/agents/compliance-reviewer.md +83 -0
  37. package/assets/core/agents/docs-writer.md +77 -0
  38. package/assets/core/agents/frontend-engineer.md +70 -0
  39. package/assets/core/agents/orchestrator.md +104 -0
  40. package/assets/core/agents/security-auditor.md +54 -0
  41. package/assets/core/agents/test-engineer.md +57 -0
  42. package/assets/core/hooks/hooks-registry.yaml +48 -0
  43. package/assets/core/hooks/post-turn-check.sh +139 -0
  44. package/assets/core/hooks/pre-bash-check.py +202 -0
  45. package/assets/core/hooks/pre-edit-check.py +317 -0
  46. package/assets/core/hooks/session-start.sh +184 -0
  47. package/assets/core/schemas/project.schema.json +503 -0
  48. package/assets/core/skills/README.md +88 -0
  49. package/assets/core/skills/aitmpl-search/SKILL.md +74 -0
  50. package/assets/core/skills/browser-test/SKILL.md +177 -0
  51. package/assets/core/skills/db-migrate/SKILL.md +163 -0
  52. package/assets/core/skills/local2prod/SKILL.md +147 -0
  53. package/assets/core/skills/new-feature/SKILL.md +155 -0
  54. package/assets/core/skills/obsidian-sync/SKILL.md +152 -0
  55. package/assets/core/skills/phase-kickoff/SKILL.md +69 -0
  56. package/assets/core/skills/security-audit/SKILL.md +125 -0
  57. package/assets/core/skills/spec/SKILL.md +72 -0
  58. package/assets/core/skills/wiki-ingest/SKILL.md +183 -0
  59. package/assets/core/skills/wiki-lint/SKILL.md +109 -0
  60. package/assets/core/skills/wiki-query/SKILL.md +100 -0
  61. package/assets/core/templates/claude-md/architecture.rules +20 -0
  62. package/assets/core/templates/claude-md/global.md +30 -0
  63. package/assets/core/templates/claude-md/project.md +36 -0
  64. package/assets/core/templates/daily-note.md +38 -0
  65. package/assets/core/templates/spec-template.md +43 -0
  66. package/assets/core/workflows/sdd.md +69 -0
  67. package/assets/core/workflows/sprint.md +59 -0
  68. package/assets/forge.py +1265 -0
  69. package/assets/hooks/pre-commit +43 -0
  70. package/assets/manifest.json +274 -0
  71. package/assets/profiles/astro/README.md +24 -0
  72. package/assets/profiles/astro/agents/frontend-engineer.md +74 -0
  73. package/assets/profiles/django/agents/api-engineer.md +83 -0
  74. package/assets/profiles/expo/README.md +24 -0
  75. package/assets/profiles/expo/agents/mobile-engineer.md +69 -0
  76. package/assets/profiles/express/agents/api-engineer.md +60 -0
  77. package/assets/profiles/fastapi/README.md +32 -0
  78. package/assets/profiles/fastapi/agents/api-engineer.md +87 -0
  79. package/assets/profiles/go-gin/agents/api-engineer.md +98 -0
  80. package/assets/profiles/hono-drizzle/README.md +31 -0
  81. package/assets/profiles/hono-drizzle/agents/api-engineer.md +82 -0
  82. package/assets/profiles/laravel/README.md +32 -0
  83. package/assets/profiles/laravel/agents/api-engineer.md +114 -0
  84. package/assets/profiles/laravel/agents/fullstack-engineer.md +67 -0
  85. package/assets/profiles/laravel/agents/migration-specialist.md +420 -0
  86. package/assets/profiles/nestjs/agents/api-engineer.md +79 -0
  87. package/assets/profiles/nextjs-admin/README.md +32 -0
  88. package/assets/profiles/nextjs-admin/agents/admin-engineer.md +78 -0
  89. package/assets/profiles/playwright-crawler/agents/scanner-engineer.md +51 -0
  90. package/assets/profiles/rails/agents/fullstack-engineer.md +61 -0
  91. package/assets/profiles/sveltekit/agents/frontend-engineer.md +96 -0
  92. package/assets/profiles/vuenuxt/agents/frontend-engineer.md +82 -0
  93. package/assets/profiles/wordpress/README.md +30 -0
  94. package/assets/profiles/wordpress/agents/divi-engineer.md +273 -0
  95. package/assets/profiles/wordpress/agents/elementor-engineer.md +310 -0
  96. package/assets/profiles/wordpress/agents/wp-engineer.md +216 -0
  97. package/assets/requirements.txt +2 -0
  98. package/assets/scripts/aitmpl-search.py +808 -0
  99. package/assets/scripts/forge-add-opportunities.py +92 -0
  100. package/assets/scripts/forge-audit.py +1061 -0
  101. package/assets/scripts/forge-generate-all.py +283 -0
  102. package/assets/scripts/forge-init.py +900 -0
  103. package/assets/scripts/forge-migrate-project-yaml.py +397 -0
  104. package/assets/scripts/forge-scaffold-profile.py +181 -0
  105. package/assets/scripts/forge-teardown.py +193 -0
  106. package/assets/scripts/forge-validate-project-yaml.py +457 -0
  107. package/assets/scripts/forge-wizard.py +1003 -0
  108. package/assets/scripts/setup-codex.sh +229 -0
  109. package/assets/scripts/team-install.sh +147 -0
  110. package/assets/scripts/token-stats.py +201 -0
  111. package/assets/templates/modes/enterprise.yaml.tpl +114 -0
  112. package/assets/templates/modes/multi-runtime.yaml.tpl +89 -0
  113. package/assets/templates/modes/new-stack.yaml.tpl +101 -0
  114. package/assets/templates/modes/startup.yaml.tpl +74 -0
  115. package/assets/templates/project.yaml.tpl +185 -0
  116. package/assets/templates/wiki/concepts/_template.md +22 -0
  117. package/assets/templates/wiki/entities/_template.md +19 -0
  118. package/assets/templates/wiki/index.md +32 -0
  119. package/assets/templates/wiki/log.md +6 -0
  120. package/assets/templates/wiki/sources/_template.md +25 -0
  121. package/dist/cli.d.ts +3 -0
  122. package/dist/cli.d.ts.map +1 -0
  123. package/dist/cli.js +64 -0
  124. package/dist/cli.js.map +1 -0
  125. package/dist/commands/audit.d.ts +2 -0
  126. package/dist/commands/audit.d.ts.map +1 -0
  127. package/dist/commands/audit.js +21 -0
  128. package/dist/commands/audit.js.map +1 -0
  129. package/dist/commands/doctor.d.ts +2 -0
  130. package/dist/commands/doctor.d.ts.map +1 -0
  131. package/dist/commands/doctor.js +58 -0
  132. package/dist/commands/doctor.js.map +1 -0
  133. package/dist/commands/generate.d.ts +2 -0
  134. package/dist/commands/generate.d.ts.map +1 -0
  135. package/dist/commands/generate.js +27 -0
  136. package/dist/commands/generate.js.map +1 -0
  137. package/dist/commands/init.d.ts +2 -0
  138. package/dist/commands/init.d.ts.map +1 -0
  139. package/dist/commands/init.js +22 -0
  140. package/dist/commands/init.js.map +1 -0
  141. package/dist/commands/validate.d.ts +2 -0
  142. package/dist/commands/validate.d.ts.map +1 -0
  143. package/dist/commands/validate.js +20 -0
  144. package/dist/commands/validate.js.map +1 -0
  145. package/dist/lib/paths.d.ts +10 -0
  146. package/dist/lib/paths.d.ts.map +1 -0
  147. package/dist/lib/paths.js +49 -0
  148. package/dist/lib/paths.js.map +1 -0
  149. package/dist/lib/python.d.ts +4 -0
  150. package/dist/lib/python.d.ts.map +1 -0
  151. package/dist/lib/python.js +46 -0
  152. package/dist/lib/python.js.map +1 -0
  153. 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