sherpa-skill 2.0.5 → 2.0.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/.agents/plugins/marketplace.json +11 -0
- package/.agents/skills/sherpa/SKILL.md +161 -0
- package/.agents/skills/sherpa/commands/README.md +43 -0
- package/.agents/skills/sherpa/commands/manifest.json +589 -0
- package/.agents/skills/sherpa/commands/sherpa-research.md +147 -0
- package/.agents/skills/sherpa/core/commands/router.py +171 -0
- package/.agents/skills/sherpa/core/configuration/manager.py +504 -0
- package/.agents/skills/sherpa/core/ideas/engine.py +564 -0
- package/.agents/skills/sherpa/docs/commands/README.md +96 -0
- package/.agents/skills/sherpa/docs/configuration/runtime-verification.md +64 -0
- package/.agents/skills/sherpa/rules/demo-preparation-rules.md +543 -0
- package/.agents/skills/sherpa/rules/engineering-rules.md +221 -0
- package/.agents/skills/sherpa/rules/final-audit-rules.md +824 -0
- package/.agents/skills/sherpa/rules/implementation-rules.md +127 -0
- package/.agents/skills/sherpa/rules/judge-rules.md +443 -0
- package/.agents/skills/sherpa/rules/mentor-rules.md +109 -0
- package/.agents/skills/sherpa/rules/postmortem-rules.md +69 -0
- package/.agents/skills/sherpa/rules/presentation-rules.md +783 -0
- package/.agents/skills/sherpa/rules/red-team-rules.md +185 -0
- package/.agents/skills/sherpa/rules/research-rules.md +184 -0
- package/.agents/skills/sherpa/rules/score-optimization-rules.md +355 -0
- package/.agents/skills/sherpa/rules/scoring-rules.md +216 -0
- package/.agents/skills/sherpa/rules/submission-rules.md +41 -0
- package/.agents/skills/sherpa/rules/testing-security-rules.md +87 -0
- package/.agents/skills/sherpa/rules/ux-rules.md +89 -0
- package/.agents/skills/sherpa/src/config_manager.py +474 -0
- package/.agents/skills/sherpa/src/verification.py +458 -0
- package/.agents/skills/sherpa/templates/audit-finding.md +20 -0
- package/.agents/skills/sherpa/templates/claim-audit.md +15 -0
- package/.agents/skills/sherpa/templates/configuration/hackathon.json +21 -0
- package/.agents/skills/sherpa/templates/configuration/integrations.env.example +3 -0
- package/.agents/skills/sherpa/templates/configuration/project.env.example +12 -0
- package/.agents/skills/sherpa/templates/configuration/providers.env.example +4 -0
- package/.agents/skills/sherpa/templates/configuration/settings.json +22 -0
- package/.agents/skills/sherpa/templates/consistency-audit.md +17 -0
- package/.agents/skills/sherpa/templates/decision-log.md +18 -0
- package/.agents/skills/sherpa/templates/demo-checkpoint.md +42 -0
- package/.agents/skills/sherpa/templates/demo-evidence.md +32 -0
- package/.agents/skills/sherpa/templates/demo-finding.md +112 -0
- package/.agents/skills/sherpa/templates/demo-flow.md +52 -0
- package/.agents/skills/sherpa/templates/demo-plan.md +265 -0
- package/.agents/skills/sherpa/templates/demo-verification.md +57 -0
- package/.agents/skills/sherpa/templates/final-audit.md +387 -0
- package/.agents/skills/sherpa/templates/final-risk.md +17 -0
- package/.agents/skills/sherpa/templates/final-verification.md +57 -0
- package/.agents/skills/sherpa/templates/gap-analysis.md +113 -0
- package/.agents/skills/sherpa/templates/idea-comparison.md +129 -0
- package/.agents/skills/sherpa/templates/idea-evaluation.md +251 -0
- package/.agents/skills/sherpa/templates/implementation-plan.md +96 -0
- package/.agents/skills/sherpa/templates/implementation-task.md +57 -0
- package/.agents/skills/sherpa/templates/improvement-candidate.md +135 -0
- package/.agents/skills/sherpa/templates/improvement.md +18 -0
- package/.agents/skills/sherpa/templates/incident.md +20 -0
- package/.agents/skills/sherpa/templates/judge-criterion.md +41 -0
- package/.agents/skills/sherpa/templates/judge-question.md +41 -0
- package/.agents/skills/sherpa/templates/judge-session.md +125 -0
- package/.agents/skills/sherpa/templates/judge-simulation.md +264 -0
- package/.agents/skills/sherpa/templates/lesson.md +14 -0
- package/.agents/skills/sherpa/templates/postmortem.md +87 -0
- package/.agents/skills/sherpa/templates/presentation-evidence.md +33 -0
- package/.agents/skills/sherpa/templates/presentation-plan.md +349 -0
- package/.agents/skills/sherpa/templates/presentation-qna.md +29 -0
- package/.agents/skills/sherpa/templates/presentation-risk.md +42 -0
- package/.agents/skills/sherpa/templates/presentation-verification.md +57 -0
- package/.agents/skills/sherpa/templates/project-plan.md +242 -0
- package/.agents/skills/sherpa/templates/project-state.md +84 -0
- package/.agents/skills/sherpa/templates/red-team-finding.md +73 -0
- package/.agents/skills/sherpa/templates/red-team-review.md +105 -0
- package/.agents/skills/sherpa/templates/red-team-scenario.md +43 -0
- package/.agents/skills/sherpa/templates/red-team-verification.md +31 -0
- package/.agents/skills/sherpa/templates/repo-analysis.md +204 -0
- package/.agents/skills/sherpa/templates/requirement-audit.md +15 -0
- package/.agents/skills/sherpa/templates/research-comparison.md +124 -0
- package/.agents/skills/sherpa/templates/research-plan.md +93 -0
- package/.agents/skills/sherpa/templates/research-report.md +92 -0
- package/.agents/skills/sherpa/templates/research-source.md +77 -0
- package/.agents/skills/sherpa/templates/score-optimization-plan.md +197 -0
- package/.agents/skills/sherpa/templates/security-finding.md +45 -0
- package/.agents/skills/sherpa/templates/security-review.md +64 -0
- package/.agents/skills/sherpa/templates/slide-claim.md +47 -0
- package/.agents/skills/sherpa/templates/slide-plan.md +127 -0
- package/.agents/skills/sherpa/templates/submission-artifact.md +15 -0
- package/.agents/skills/sherpa/templates/submission-checklist.md +24 -0
- package/.agents/skills/sherpa/templates/submission-field.md +14 -0
- package/.agents/skills/sherpa/templates/submission-link.md +14 -0
- package/.agents/skills/sherpa/templates/submission-plan.md +31 -0
- package/.agents/skills/sherpa/templates/submission-readiness.md +18 -0
- package/.agents/skills/sherpa/templates/submission-requirement.md +12 -0
- package/.agents/skills/sherpa/templates/submission-risk.md +14 -0
- package/.agents/skills/sherpa/templates/submission-snapshot.md +20 -0
- package/.agents/skills/sherpa/templates/submission-verification.md +12 -0
- package/.agents/skills/sherpa/templates/test-plan.md +59 -0
- package/.agents/skills/sherpa/templates/test-result.md +46 -0
- package/.agents/skills/sherpa/templates/timeline.md +11 -0
- package/.agents/skills/sherpa/templates/ux-finding.md +56 -0
- package/.agents/skills/sherpa/templates/ux-flow.md +46 -0
- package/.agents/skills/sherpa/templates/ux-review.md +113 -0
- package/.agents/skills/sherpa/templates/ux-verification.md +32 -0
- package/.agents/skills/sherpa/workflows/configuration.md +23 -0
- package/.agents/skills/sherpa/workflows/demo-preparation.md +399 -0
- package/.agents/skills/sherpa/workflows/final-audit.md +713 -0
- package/.agents/skills/sherpa/workflows/idea-evaluation.md +239 -0
- package/.agents/skills/sherpa/workflows/idea-intelligence.md +69 -0
- package/.agents/skills/sherpa/workflows/implementation.md +42 -0
- package/.agents/skills/sherpa/workflows/init.md +102 -0
- package/.agents/skills/sherpa/workflows/judge-simulation.md +408 -0
- package/.agents/skills/sherpa/workflows/lifecycle.md +51 -0
- package/.agents/skills/sherpa/workflows/next.md +96 -0
- package/.agents/skills/sherpa/workflows/postmortem.md +76 -0
- package/.agents/skills/sherpa/workflows/presentation.md +522 -0
- package/.agents/skills/sherpa/workflows/project-planning.md +327 -0
- package/.agents/skills/sherpa/workflows/red-team.md +373 -0
- package/.agents/skills/sherpa/workflows/research.md +142 -0
- package/.agents/skills/sherpa/workflows/score-optimization.md +276 -0
- package/.agents/skills/sherpa/workflows/status.md +107 -0
- package/.agents/skills/sherpa/workflows/submission.md +46 -0
- package/.agents/skills/sherpa/workflows/testing-security.md +44 -0
- package/.agents/skills/sherpa/workflows/ux-review.md +59 -0
- package/.claude-plugin/marketplace.json +21 -0
- package/.claude-plugin/plugin.json +13 -0
- package/.codex-plugin/plugin.json +13 -0
- package/GEMINI.md +3 -0
- package/README.md +14 -8
- package/SKILL.md +161 -0
- package/distribution/cli/sherpa +7 -2
- package/distribution/npm/sherpa.mjs +15 -8
- package/gemini-extension.json +7 -0
- package/package.json +31 -3
- package/plugin.json +20 -0
- package/qwen-extension.json +6 -0
- package/skills/sherpa/SKILL.md +161 -0
- package/skills/sherpa/commands/README.md +43 -0
- package/skills/sherpa/commands/manifest.json +589 -0
- package/skills/sherpa/commands/sherpa-research.md +147 -0
- package/skills/sherpa/core/commands/router.py +171 -0
- package/skills/sherpa/core/configuration/manager.py +504 -0
- package/skills/sherpa/core/ideas/engine.py +564 -0
- package/skills/sherpa/docs/commands/README.md +96 -0
- package/skills/sherpa/docs/configuration/runtime-verification.md +64 -0
- package/skills/sherpa/rules/demo-preparation-rules.md +543 -0
- package/skills/sherpa/rules/engineering-rules.md +221 -0
- package/skills/sherpa/rules/final-audit-rules.md +824 -0
- package/skills/sherpa/rules/implementation-rules.md +127 -0
- package/skills/sherpa/rules/judge-rules.md +443 -0
- package/skills/sherpa/rules/mentor-rules.md +109 -0
- package/skills/sherpa/rules/postmortem-rules.md +69 -0
- package/skills/sherpa/rules/presentation-rules.md +783 -0
- package/skills/sherpa/rules/red-team-rules.md +185 -0
- package/skills/sherpa/rules/research-rules.md +184 -0
- package/skills/sherpa/rules/score-optimization-rules.md +355 -0
- package/skills/sherpa/rules/scoring-rules.md +216 -0
- package/skills/sherpa/rules/submission-rules.md +41 -0
- package/skills/sherpa/rules/testing-security-rules.md +87 -0
- package/skills/sherpa/rules/ux-rules.md +89 -0
- package/skills/sherpa/src/config_manager.py +474 -0
- package/skills/sherpa/src/verification.py +458 -0
- package/skills/sherpa/templates/audit-finding.md +20 -0
- package/skills/sherpa/templates/claim-audit.md +15 -0
- package/skills/sherpa/templates/configuration/hackathon.json +21 -0
- package/skills/sherpa/templates/configuration/integrations.env.example +3 -0
- package/skills/sherpa/templates/configuration/project.env.example +12 -0
- package/skills/sherpa/templates/configuration/providers.env.example +4 -0
- package/skills/sherpa/templates/configuration/settings.json +22 -0
- package/skills/sherpa/templates/consistency-audit.md +17 -0
- package/skills/sherpa/templates/decision-log.md +18 -0
- package/skills/sherpa/templates/demo-checkpoint.md +42 -0
- package/skills/sherpa/templates/demo-evidence.md +32 -0
- package/skills/sherpa/templates/demo-finding.md +112 -0
- package/skills/sherpa/templates/demo-flow.md +52 -0
- package/skills/sherpa/templates/demo-plan.md +265 -0
- package/skills/sherpa/templates/demo-verification.md +57 -0
- package/skills/sherpa/templates/final-audit.md +387 -0
- package/skills/sherpa/templates/final-risk.md +17 -0
- package/skills/sherpa/templates/final-verification.md +57 -0
- package/skills/sherpa/templates/gap-analysis.md +113 -0
- package/skills/sherpa/templates/idea-comparison.md +129 -0
- package/skills/sherpa/templates/idea-evaluation.md +251 -0
- package/skills/sherpa/templates/implementation-plan.md +96 -0
- package/skills/sherpa/templates/implementation-task.md +57 -0
- package/skills/sherpa/templates/improvement-candidate.md +135 -0
- package/skills/sherpa/templates/improvement.md +18 -0
- package/skills/sherpa/templates/incident.md +20 -0
- package/skills/sherpa/templates/judge-criterion.md +41 -0
- package/skills/sherpa/templates/judge-question.md +41 -0
- package/skills/sherpa/templates/judge-session.md +125 -0
- package/skills/sherpa/templates/judge-simulation.md +264 -0
- package/skills/sherpa/templates/lesson.md +14 -0
- package/skills/sherpa/templates/postmortem.md +87 -0
- package/skills/sherpa/templates/presentation-evidence.md +33 -0
- package/skills/sherpa/templates/presentation-plan.md +349 -0
- package/skills/sherpa/templates/presentation-qna.md +29 -0
- package/skills/sherpa/templates/presentation-risk.md +42 -0
- package/skills/sherpa/templates/presentation-verification.md +57 -0
- package/skills/sherpa/templates/project-plan.md +242 -0
- package/skills/sherpa/templates/project-state.md +84 -0
- package/skills/sherpa/templates/red-team-finding.md +73 -0
- package/skills/sherpa/templates/red-team-review.md +105 -0
- package/skills/sherpa/templates/red-team-scenario.md +43 -0
- package/skills/sherpa/templates/red-team-verification.md +31 -0
- package/skills/sherpa/templates/repo-analysis.md +204 -0
- package/skills/sherpa/templates/requirement-audit.md +15 -0
- package/skills/sherpa/templates/research-comparison.md +124 -0
- package/skills/sherpa/templates/research-plan.md +93 -0
- package/skills/sherpa/templates/research-report.md +92 -0
- package/skills/sherpa/templates/research-source.md +77 -0
- package/skills/sherpa/templates/score-optimization-plan.md +197 -0
- package/skills/sherpa/templates/security-finding.md +45 -0
- package/skills/sherpa/templates/security-review.md +64 -0
- package/skills/sherpa/templates/slide-claim.md +47 -0
- package/skills/sherpa/templates/slide-plan.md +127 -0
- package/skills/sherpa/templates/submission-artifact.md +15 -0
- package/skills/sherpa/templates/submission-checklist.md +24 -0
- package/skills/sherpa/templates/submission-field.md +14 -0
- package/skills/sherpa/templates/submission-link.md +14 -0
- package/skills/sherpa/templates/submission-plan.md +31 -0
- package/skills/sherpa/templates/submission-readiness.md +18 -0
- package/skills/sherpa/templates/submission-requirement.md +12 -0
- package/skills/sherpa/templates/submission-risk.md +14 -0
- package/skills/sherpa/templates/submission-snapshot.md +20 -0
- package/skills/sherpa/templates/submission-verification.md +12 -0
- package/skills/sherpa/templates/test-plan.md +59 -0
- package/skills/sherpa/templates/test-result.md +46 -0
- package/skills/sherpa/templates/timeline.md +11 -0
- package/skills/sherpa/templates/ux-finding.md +56 -0
- package/skills/sherpa/templates/ux-flow.md +46 -0
- package/skills/sherpa/templates/ux-review.md +113 -0
- package/skills/sherpa/templates/ux-verification.md +32 -0
- package/skills/sherpa/workflows/configuration.md +23 -0
- package/skills/sherpa/workflows/demo-preparation.md +399 -0
- package/skills/sherpa/workflows/final-audit.md +713 -0
- package/skills/sherpa/workflows/idea-evaluation.md +239 -0
- package/skills/sherpa/workflows/idea-intelligence.md +69 -0
- package/skills/sherpa/workflows/implementation.md +42 -0
- package/skills/sherpa/workflows/init.md +102 -0
- package/skills/sherpa/workflows/judge-simulation.md +408 -0
- package/skills/sherpa/workflows/lifecycle.md +51 -0
- package/skills/sherpa/workflows/next.md +96 -0
- package/skills/sherpa/workflows/postmortem.md +76 -0
- package/skills/sherpa/workflows/presentation.md +522 -0
- package/skills/sherpa/workflows/project-planning.md +327 -0
- package/skills/sherpa/workflows/red-team.md +373 -0
- package/skills/sherpa/workflows/research.md +142 -0
- package/skills/sherpa/workflows/score-optimization.md +276 -0
- package/skills/sherpa/workflows/status.md +107 -0
- package/skills/sherpa/workflows/submission.md +46 -0
- package/skills/sherpa/workflows/testing-security.md +44 -0
- package/skills/sherpa/workflows/ux-review.md +59 -0
|
@@ -0,0 +1,504 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""sherpa project configuration utility (Python standard library only).
|
|
3
|
+
Never prints, logs, or persists secret values. Use from the target project root.
|
|
4
|
+
"""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
import argparse
|
|
7
|
+
import datetime as dt
|
|
8
|
+
import json
|
|
9
|
+
import os
|
|
10
|
+
import re
|
|
11
|
+
import subprocess
|
|
12
|
+
import sys
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
from urllib.parse import urlparse
|
|
16
|
+
from zoneinfo import ZoneInfo, ZoneInfoNotFoundError
|
|
17
|
+
|
|
18
|
+
UNKNOWN = "UNKNOWN"
|
|
19
|
+
STATE_DIRECTORY = ".sherpa"
|
|
20
|
+
LEGACY_STATE_DIRECTORY = ".hackathon"
|
|
21
|
+
BOOLEAN_KEYS = {
|
|
22
|
+
"ENABLE_WEB_RESEARCH", "ENABLE_AUTO_VERIFY", "ENABLE_RED_TEAM", "ENABLE_VIDEO_ASSIST",
|
|
23
|
+
"REQUIRE_HUMAN_APPROVAL",
|
|
24
|
+
}
|
|
25
|
+
INTEGER_LIMITS = {"MAX_AUTO_ITERATIONS": (1, 20), "MAX_AUTO_FIXES": (0, 50)}
|
|
26
|
+
SECRET_ENV_KEYS = ("AI_API_KEY", "OPENAI_API_KEY", "ANTHROPIC_API_KEY", "GOOGLE_API_KEY", "GITHUB_TOKEN")
|
|
27
|
+
SECRET_FILE_NAMES = {"credentials.json", "service-account.json", "secrets.json"}
|
|
28
|
+
SKIP_DIRS = {".git", ".venv", "venv", "node_modules", "dist", "build", "coverage", "__pycache__"}
|
|
29
|
+
SECRET_PATTERNS = (
|
|
30
|
+
("private key", re.compile(r"-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----")),
|
|
31
|
+
("AWS access key", re.compile(r"\bAKIA[0-9A-Z]{16}\b")),
|
|
32
|
+
("OpenAI-style API key", re.compile(r"\bsk-[A-Za-z0-9_-]{20,}\b")),
|
|
33
|
+
("GitHub token", re.compile(r"\bgh[pousr]_[A-Za-z0-9]{30,}\b")),
|
|
34
|
+
("Google API key", re.compile(r"\bAIza[0-9A-Za-z_-]{30,}\b")),
|
|
35
|
+
("Bearer token", re.compile(r"(?i)\bBearer\s+[A-Za-z0-9._~+/=-]{20,}")),
|
|
36
|
+
("password or secret assignment", re.compile(r"(?i)\b(?:password|passwd|client_secret|api[_-]?key)\s*[:=]\s*[\"']?[^\s\"'<>]{8,}")),
|
|
37
|
+
("database URL with password", re.compile(r"(?i)\b(?:postgres|mysql|mongodb)(?:ql)?://[^:\s/]+:[^@\s/]{4,}@")),
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def parse_env_file(path: Path) -> tuple[dict[str, str], list[str]]:
|
|
42
|
+
"""Parse a small dotenv subset; diagnostics never include source values."""
|
|
43
|
+
values: dict[str, str] = {}
|
|
44
|
+
issues: list[str] = []
|
|
45
|
+
if not path.exists():
|
|
46
|
+
return values, issues
|
|
47
|
+
try:
|
|
48
|
+
lines = path.read_text(encoding="utf-8-sig").splitlines()
|
|
49
|
+
except (OSError, UnicodeError):
|
|
50
|
+
return values, [".env could not be read as UTF-8"]
|
|
51
|
+
key_re = re.compile(r"^[A-Z_][A-Z0-9_]*$")
|
|
52
|
+
for number, raw in enumerate(lines, 1):
|
|
53
|
+
line = raw.strip()
|
|
54
|
+
if not line or line.startswith("#"):
|
|
55
|
+
continue
|
|
56
|
+
if line.startswith("export "):
|
|
57
|
+
line = line[7:].lstrip()
|
|
58
|
+
if "=" not in line:
|
|
59
|
+
issues.append(f".env line {number}: expected KEY=value")
|
|
60
|
+
continue
|
|
61
|
+
key, value = line.split("=", 1)
|
|
62
|
+
key = key.strip()
|
|
63
|
+
if not key_re.fullmatch(key):
|
|
64
|
+
issues.append(f".env line {number}: invalid variable name")
|
|
65
|
+
continue
|
|
66
|
+
if key in values:
|
|
67
|
+
issues.append(f".env line {number}: duplicate variable {key}")
|
|
68
|
+
continue
|
|
69
|
+
value = value.strip()
|
|
70
|
+
if value.startswith(('"', "'")):
|
|
71
|
+
quote = value[0]
|
|
72
|
+
if len(value) < 2 or not value.endswith(quote):
|
|
73
|
+
issues.append(f".env line {number}: unterminated quoted value for {key}")
|
|
74
|
+
continue
|
|
75
|
+
value = value[1:-1]
|
|
76
|
+
elif " #" in value:
|
|
77
|
+
value = value.split(" #", 1)[0].rstrip()
|
|
78
|
+
values[key] = value
|
|
79
|
+
return values, issues
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def resolve_value(explicit: Any, project: Any, process_env: Any, dotenv: Any, default: Any = None) -> Any:
|
|
83
|
+
"""Precedence: explicit > project > process env > .env > default > UNKNOWN.
|
|
84
|
+
Empty and UNKNOWN values are unresolved and fall through; literal UNKNOWN is
|
|
85
|
+
returned when no lower-precedence source supplies a value.
|
|
86
|
+
"""
|
|
87
|
+
for value in (explicit, project, process_env, dotenv, default):
|
|
88
|
+
if value is None:
|
|
89
|
+
continue
|
|
90
|
+
if isinstance(value, str) and value.strip() == "":
|
|
91
|
+
continue
|
|
92
|
+
if isinstance(value, str) and value.strip().upper() == UNKNOWN:
|
|
93
|
+
continue
|
|
94
|
+
return value
|
|
95
|
+
return UNKNOWN
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def load_json(path: Path) -> tuple[dict[str, Any], list[str]]:
|
|
99
|
+
if not path.exists():
|
|
100
|
+
return {}, [f"Missing {path.name}"]
|
|
101
|
+
try:
|
|
102
|
+
value = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
103
|
+
if not isinstance(value, dict):
|
|
104
|
+
return {}, [f"{path.name}: top level must be a JSON object"]
|
|
105
|
+
return value, []
|
|
106
|
+
except (json.JSONDecodeError, UnicodeError, OSError) as exc:
|
|
107
|
+
if isinstance(exc, json.JSONDecodeError):
|
|
108
|
+
return {}, [f"{path.name}:{exc.lineno}: malformed JSON"]
|
|
109
|
+
return {}, [f"{path.name}: could not be read"]
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def nonempty(value: Any) -> bool:
|
|
113
|
+
return value is not None and not (isinstance(value, str) and value.strip() in ("", UNKNOWN))
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def validate_url(value: Any) -> str:
|
|
117
|
+
if not nonempty(value):
|
|
118
|
+
return "UNKNOWN"
|
|
119
|
+
try:
|
|
120
|
+
parsed = urlparse(str(value))
|
|
121
|
+
return "CONFIGURED" if parsed.scheme in ("http", "https") and parsed.hostname else "INVALID"
|
|
122
|
+
except ValueError:
|
|
123
|
+
return "INVALID"
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def validate_timezone(value: Any) -> str:
|
|
127
|
+
if not nonempty(value):
|
|
128
|
+
return "UNKNOWN"
|
|
129
|
+
try:
|
|
130
|
+
ZoneInfo(str(value))
|
|
131
|
+
return "CONFIGURED"
|
|
132
|
+
except ZoneInfoNotFoundError:
|
|
133
|
+
return "INVALID"
|
|
134
|
+
except (ValueError, OSError):
|
|
135
|
+
return "INVALID"
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def validate_deadline(value: Any) -> str:
|
|
139
|
+
if not nonempty(value):
|
|
140
|
+
return "UNKNOWN"
|
|
141
|
+
try:
|
|
142
|
+
dt.datetime.fromisoformat(str(value).replace("Z", "+00:00"))
|
|
143
|
+
return "CONFIGURED"
|
|
144
|
+
except (ValueError, TypeError):
|
|
145
|
+
return "INVALID"
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def git_output(root: Path, *args: str) -> subprocess.CompletedProcess[str]:
|
|
149
|
+
try:
|
|
150
|
+
return subprocess.run(["git", *args], cwd=root, capture_output=True, text=True, check=False)
|
|
151
|
+
except (OSError, subprocess.SubprocessError):
|
|
152
|
+
return subprocess.CompletedProcess(["git", *args], 127, "", "git unavailable")
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def is_git_root(root: Path) -> bool:
|
|
156
|
+
result = git_output(root, "rev-parse", "--show-toplevel")
|
|
157
|
+
if result.returncode:
|
|
158
|
+
return False
|
|
159
|
+
try:
|
|
160
|
+
return Path(result.stdout.strip()).resolve() == root.resolve()
|
|
161
|
+
except OSError:
|
|
162
|
+
return False
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def is_env_ignored(root: Path) -> bool:
|
|
166
|
+
result = git_output(root, "check-ignore", "--quiet", ".env")
|
|
167
|
+
if result.returncode == 0:
|
|
168
|
+
return True
|
|
169
|
+
ignore = root / ".gitignore"
|
|
170
|
+
if not ignore.is_file():
|
|
171
|
+
return False
|
|
172
|
+
patterns = [x.strip() for x in ignore.read_text(encoding="utf-8-sig").splitlines() if x.strip() and not x.lstrip().startswith("#")]
|
|
173
|
+
ignored = ".env" in patterns or ".env.*" in patterns
|
|
174
|
+
if "!.env" in patterns:
|
|
175
|
+
ignored = False
|
|
176
|
+
return ignored
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def tracked_env(root: Path) -> bool:
|
|
180
|
+
if not is_git_root(root):
|
|
181
|
+
return False
|
|
182
|
+
result = git_output(root, "ls-files", "--error-unmatch", ".env")
|
|
183
|
+
return result.returncode == 0
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def scan_secrets(root: Path) -> list[str]:
|
|
187
|
+
"""Return redacted type/path/line indicators; never return matched text."""
|
|
188
|
+
findings: list[str] = []
|
|
189
|
+
for directory, dirs, files in os.walk(root):
|
|
190
|
+
dirs[:] = [d for d in dirs if d not in SKIP_DIRS]
|
|
191
|
+
base = Path(directory)
|
|
192
|
+
for name in files:
|
|
193
|
+
path = base / name
|
|
194
|
+
rel = path.relative_to(root).as_posix()
|
|
195
|
+
if name in SECRET_FILE_NAMES or path.suffix.lower() in (".pem", ".key", ".p12", ".pfx", ".jks"):
|
|
196
|
+
findings.append(f"sensitive credential file: {rel}")
|
|
197
|
+
continue
|
|
198
|
+
if name == ".env" or (name.startswith(".env.") and not name.endswith(".example")):
|
|
199
|
+
continue
|
|
200
|
+
if path.suffix.lower() not in (".md", ".txt", ".json", ".jsonc", ".yaml", ".yml", ".toml", ".py", ".js", ".ts", ".tsx", ".jsx", ".html", ".xml", ".sh", ".ps1", ".env", ".example"):
|
|
201
|
+
continue
|
|
202
|
+
try:
|
|
203
|
+
if path.stat().st_size > 1_000_000:
|
|
204
|
+
continue
|
|
205
|
+
lines = path.read_text(encoding="utf-8-sig").splitlines()
|
|
206
|
+
except (OSError, UnicodeError):
|
|
207
|
+
continue
|
|
208
|
+
for number, line in enumerate(lines, 1):
|
|
209
|
+
for label, pattern in SECRET_PATTERNS:
|
|
210
|
+
if pattern.search(line):
|
|
211
|
+
findings.append(f"{label}: {rel}:{number}")
|
|
212
|
+
break
|
|
213
|
+
return findings
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def env_value(key: str, dotenv: dict[str, str], process_env: dict[str, str]) -> Any:
|
|
217
|
+
return resolve_value(None, None, process_env.get(key), dotenv.get(key))
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def project_state_dir(root: Path) -> Path:
|
|
221
|
+
"""Prefer Sherpa state; read the former directory as a one-release fallback."""
|
|
222
|
+
current = root / STATE_DIRECTORY
|
|
223
|
+
legacy = root / LEGACY_STATE_DIRECTORY
|
|
224
|
+
if current.exists():
|
|
225
|
+
return current
|
|
226
|
+
if legacy.exists():
|
|
227
|
+
return legacy
|
|
228
|
+
return current
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def legacy_state_warning(root: Path) -> str | None:
|
|
232
|
+
if not (root / STATE_DIRECTORY).exists() and (root / LEGACY_STATE_DIRECTORY).exists():
|
|
233
|
+
return ("Deprecated .hackathon/ state is being read. Run `sherpa migrate` to copy it "
|
|
234
|
+
"to .sherpa/; the original directory will be preserved.")
|
|
235
|
+
return None
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def config_snapshot(root: Path, process_env: dict[str, str] | None = None) -> dict[str, Any]:
|
|
239
|
+
process_env = dict(os.environ if process_env is None else process_env)
|
|
240
|
+
state_root = project_state_dir(root)
|
|
241
|
+
hackathon, h_errors = load_json(state_root / "hackathon.json")
|
|
242
|
+
settings, s_errors = load_json(state_root / "settings.json")
|
|
243
|
+
dotenv, env_errors = parse_env_file(root / ".env")
|
|
244
|
+
# Missing configuration is an expected pre-INIT state, not a malformed one.
|
|
245
|
+
errors = [e for e in h_errors + s_errors + env_errors if not e.startswith("Missing ")]
|
|
246
|
+
result: dict[str, Any] = {"errors": errors, "warnings": [], "fields": {}, "secret_findings": [], "secret_configured": False}
|
|
247
|
+
warning = legacy_state_warning(root)
|
|
248
|
+
if warning:
|
|
249
|
+
result["warnings"].append(warning)
|
|
250
|
+
|
|
251
|
+
def value(path: tuple[str, ...], env_key: str | None = None, default: Any = None) -> Any:
|
|
252
|
+
current: Any = hackathon
|
|
253
|
+
for segment in path:
|
|
254
|
+
current = current.get(segment) if isinstance(current, dict) else None
|
|
255
|
+
return resolve_value(None, current, process_env.get(env_key) if env_key else None,
|
|
256
|
+
dotenv.get(env_key) if env_key else None, default)
|
|
257
|
+
|
|
258
|
+
if not (state_root / "project-state.md").exists():
|
|
259
|
+
result["warnings"].append("Missing project-state.md")
|
|
260
|
+
result["fields"]["Current phase"] = UNKNOWN
|
|
261
|
+
else:
|
|
262
|
+
try:
|
|
263
|
+
state_text = (state_root / "project-state.md").read_text(encoding="utf-8-sig")
|
|
264
|
+
phase_match = re.search(r"(?im)^\|\s*Phase\s*\|\s*([^|]+?)\s*\|", state_text)
|
|
265
|
+
result["fields"]["Current phase"] = phase_match.group(1).strip() if phase_match and phase_match.group(1).strip().upper() != UNKNOWN else UNKNOWN
|
|
266
|
+
except (OSError, UnicodeError):
|
|
267
|
+
result["fields"]["Current phase"] = UNKNOWN
|
|
268
|
+
result["fields"]["Hackathon name"] = value(("name",), "HACKATHON_NAME")
|
|
269
|
+
result["fields"]["Hackathon URL status"] = validate_url(value(("url",), "HACKATHON_URL"))
|
|
270
|
+
result["fields"]["Platform"] = value(("platform",), "HACKATHON_PLATFORM")
|
|
271
|
+
result["fields"]["Edition"] = value(("edition",), "HACKATHON_EDITION")
|
|
272
|
+
result["fields"]["Deadline status"] = validate_deadline(value(("deadline", "datetime"), "HACKATHON_DEADLINE"))
|
|
273
|
+
result["fields"]["Timezone status"] = validate_timezone(value(("deadline", "timezone"), "HACKATHON_TIMEZONE"))
|
|
274
|
+
for check_name in ("Hackathon URL status", "Deadline status", "Timezone status"):
|
|
275
|
+
if result["fields"][check_name] == "INVALID": result["errors"].append(f"{check_name}: invalid value")
|
|
276
|
+
for label, field, env_key in (("Project name", ("project", "name"), "PROJECT_NAME"),
|
|
277
|
+
("Project path", ("project", "path"), "PROJECT_PATH"),
|
|
278
|
+
("Repository", ("project", "repository"), "GITHUB_REPOSITORY"),
|
|
279
|
+
("Deployment URL status", ("project", "deployment"), "DEPLOYMENT_URL")):
|
|
280
|
+
val = value(field, env_key)
|
|
281
|
+
if label == "Project path" and nonempty(val):
|
|
282
|
+
path = Path(str(val)); path = path if path.is_absolute() else (root / path)
|
|
283
|
+
result["fields"][label] = "CONFIGURED" if path.exists() else "INVALID"
|
|
284
|
+
elif label == "Repository" and nonempty(val):
|
|
285
|
+
result["fields"][label] = "CONFIGURED" if (validate_url(val) == "CONFIGURED" or re.fullmatch(r"[\w.-]+/[\w.-]+", str(val))) else "INVALID"
|
|
286
|
+
elif label == "Deployment URL status":
|
|
287
|
+
result["fields"][label] = validate_url(val)
|
|
288
|
+
else:
|
|
289
|
+
result["fields"][label] = "CONFIGURED" if nonempty(val) else "UNKNOWN"
|
|
290
|
+
|
|
291
|
+
setting_fields = (
|
|
292
|
+
("Agent platform", ("agent_platform",), "AGENT_PLATFORM", None),
|
|
293
|
+
("AI provider", ("ai_provider",), "AI_PROVIDER", None),
|
|
294
|
+
("AI model", ("ai_model",), "AI_MODEL", None),
|
|
295
|
+
("Verification", ("verification", "enabled"), "ENABLE_AUTO_VERIFY", True),
|
|
296
|
+
("Red team", ("red_team", "enabled"), "ENABLE_RED_TEAM", True),
|
|
297
|
+
("Research", ("research", "enabled"), "ENABLE_WEB_RESEARCH", True),
|
|
298
|
+
("Video assist", ("video", "enabled"), "ENABLE_VIDEO_ASSIST", True),
|
|
299
|
+
("Max iterations", ("autonomous_loop", "max_iterations"), "MAX_AUTO_ITERATIONS", 5),
|
|
300
|
+
("Max auto fixes", ("autonomous_loop", "max_auto_fixes"), "MAX_AUTO_FIXES", 10),
|
|
301
|
+
("Human approval", ("autonomous_loop", "require_human_approval"), "REQUIRE_HUMAN_APPROVAL", True),
|
|
302
|
+
)
|
|
303
|
+
for label, path, env_key, default in setting_fields:
|
|
304
|
+
current: Any = settings
|
|
305
|
+
for segment in path:
|
|
306
|
+
current = current.get(segment) if isinstance(current, dict) else None
|
|
307
|
+
resolved = resolve_value(None, current, process_env.get(env_key), dotenv.get(env_key), default)
|
|
308
|
+
if label in ("Max iterations", "Max auto fixes"):
|
|
309
|
+
try:
|
|
310
|
+
number = int(resolved)
|
|
311
|
+
low, high = INTEGER_LIMITS[env_key]
|
|
312
|
+
result["fields"][label] = number if low <= number <= high else "INVALID"
|
|
313
|
+
except (ValueError, TypeError): result["fields"][label] = "INVALID" if resolved != UNKNOWN else UNKNOWN
|
|
314
|
+
elif label in ("Verification", "Red team", "Research", "Video assist", "Human approval"):
|
|
315
|
+
if isinstance(resolved, bool): result["fields"][label] = resolved
|
|
316
|
+
elif str(resolved).lower() in ("true", "false"): result["fields"][label] = str(resolved).lower() == "true"
|
|
317
|
+
else: result["fields"][label] = "INVALID" if resolved != UNKNOWN else UNKNOWN
|
|
318
|
+
else:
|
|
319
|
+
result["fields"][label] = resolved if nonempty(resolved) else UNKNOWN
|
|
320
|
+
if result["fields"].get(label) == "INVALID": result["errors"].append(f"{label}: invalid value")
|
|
321
|
+
|
|
322
|
+
result["fields"]["Git repository"] = "CONFIGURED" if is_git_root(root) else "MISSING"
|
|
323
|
+
result["secret_configured"] = any(nonempty(process_env.get(k)) or nonempty(dotenv.get(k)) for k in SECRET_ENV_KEYS)
|
|
324
|
+
|
|
325
|
+
for key in BOOLEAN_KEYS:
|
|
326
|
+
raw = process_env.get(key, dotenv.get(key))
|
|
327
|
+
if raw is not None and str(raw).strip() and str(raw).lower() not in ("true", "false"):
|
|
328
|
+
result["errors"].append(f"{key}: expected true or false")
|
|
329
|
+
for key, (low, high) in INTEGER_LIMITS.items():
|
|
330
|
+
raw = process_env.get(key, dotenv.get(key))
|
|
331
|
+
if raw is not None and str(raw).strip():
|
|
332
|
+
try: number = int(raw)
|
|
333
|
+
except (ValueError, TypeError): result["errors"].append(f"{key}: expected an integer"); continue
|
|
334
|
+
if not low <= number <= high: result["errors"].append(f"{key}: must be between {low} and {high}")
|
|
335
|
+
if result["fields"].get("Human approval") is False:
|
|
336
|
+
result["warnings"].append("WARNING: Human approval is disabled for autonomous operations.")
|
|
337
|
+
if (root / ".env").exists() and not is_env_ignored(root):
|
|
338
|
+
result["errors"].append(".env exists but is not ignored by Git")
|
|
339
|
+
if tracked_env(root):
|
|
340
|
+
result["errors"].append(".env is tracked by Git; remove it from the index and rotate any exposed credentials")
|
|
341
|
+
result["secret_findings"] = scan_secrets(root)
|
|
342
|
+
if result["secret_findings"]:
|
|
343
|
+
result["errors"].append(f"Secret scan found {len(result['secret_findings'])} indicator(s) in scanned files")
|
|
344
|
+
if result["errors"]:
|
|
345
|
+
result["status"] = "INVALID"
|
|
346
|
+
elif not (state_root / "hackathon.json").exists() and not (state_root / "settings.json").exists():
|
|
347
|
+
result["status"] = "MISSING"
|
|
348
|
+
else:
|
|
349
|
+
unknowns = sum(1 for val in result["fields"].values() if val == UNKNOWN)
|
|
350
|
+
result["status"] = "CONFIGURED" if unknowns == 0 else "PARTIALLY_CONFIGURED"
|
|
351
|
+
return result
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def print_overview(snapshot: dict[str, Any]) -> None:
|
|
355
|
+
f = snapshot["fields"]
|
|
356
|
+
print("Sherpa CONFIGURATION")
|
|
357
|
+
print(f"Status: {snapshot['status']}")
|
|
358
|
+
print(f"Current phase: {f.get('Current phase', UNKNOWN)}")
|
|
359
|
+
for label in ("Hackathon name", "Platform", "Edition", "Project name", "Project path"):
|
|
360
|
+
value = f.get(label, UNKNOWN)
|
|
361
|
+
print(f"{label}: {value if nonempty(value) else UNKNOWN}")
|
|
362
|
+
for label in ("Hackathon URL status", "Deadline status", "Timezone status", "Repository", "Deployment URL status"):
|
|
363
|
+
print(f"{label}: {f.get(label, UNKNOWN)}")
|
|
364
|
+
print(f"AI provider: {f.get('AI provider', UNKNOWN)}")
|
|
365
|
+
print(f"AI credential: {'Configured' if snapshot.get('secret_configured') else 'Missing'}")
|
|
366
|
+
for label in ("Verification", "Red team", "Research", "Video assist"):
|
|
367
|
+
value=f.get(label, UNKNOWN); print(f"{label}: {value if value is not UNKNOWN else 'UNKNOWN'}")
|
|
368
|
+
print(f"Max iterations: {f.get('Max iterations', UNKNOWN)}")
|
|
369
|
+
print(f"Max auto fixes: {f.get('Max auto fixes', UNKNOWN)}")
|
|
370
|
+
print(f"Human approval: {'REQUIRED' if f.get('Human approval') is True else 'DISABLED' if f.get('Human approval') is False else UNKNOWN}")
|
|
371
|
+
for warning in snapshot["warnings"]: print(warning)
|
|
372
|
+
|
|
373
|
+
|
|
374
|
+
def print_check(snapshot: dict[str, Any], root: Path) -> None:
|
|
375
|
+
print(f"Configuration: {snapshot['status']}")
|
|
376
|
+
for label, value in snapshot["fields"].items(): print(f"{label}: {value}")
|
|
377
|
+
dotenv, _ = parse_env_file(root / ".env")
|
|
378
|
+
for key in SECRET_ENV_KEYS: print(f"{key}: {'Configured' if nonempty(os.environ.get(key)) or nonempty(dotenv.get(key)) else 'Missing'}")
|
|
379
|
+
print(f".env ignored: {'YES' if is_env_ignored(root) else 'NO / NOT APPLICABLE'}")
|
|
380
|
+
print(f".env tracked: {'YES' if tracked_env(root) else 'NO'}")
|
|
381
|
+
if snapshot["secret_findings"]:
|
|
382
|
+
print("Secret scan: FAIL — indicators found (values redacted)")
|
|
383
|
+
for finding in snapshot["secret_findings"]: print(f" {finding}")
|
|
384
|
+
else:
|
|
385
|
+
print("Secret scan: PASS — no known secret patterns detected in the scanned scope")
|
|
386
|
+
for warning in snapshot["warnings"]: print(warning)
|
|
387
|
+
for error in snapshot["errors"]: print(f"ERROR: {error}")
|
|
388
|
+
if not snapshot["errors"]:
|
|
389
|
+
print("No configuration errors detected. External provider connectivity was not tested.")
|
|
390
|
+
elif snapshot["status"] == "MISSING":
|
|
391
|
+
print("Configuration files are missing; run INIT to create them without overwriting existing data.")
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
def ensure_gitignore(root: Path) -> list[str]:
|
|
395
|
+
path = root / ".gitignore"
|
|
396
|
+
original = path.read_text(encoding="utf-8-sig") if path.exists() else ""
|
|
397
|
+
required = (".env", ".env.*", "!.env.sherpa.example", "!.env.*.example", "*.key", "*.pem",
|
|
398
|
+
"credentials.json", "service-account.json", "secrets.json")
|
|
399
|
+
present = set(line.strip() for line in original.splitlines())
|
|
400
|
+
additions = [line for line in required if line not in present]
|
|
401
|
+
if additions:
|
|
402
|
+
prefix = original
|
|
403
|
+
if prefix and not prefix.endswith(("\n", "\r")): prefix += "\n"
|
|
404
|
+
if prefix and not prefix.endswith("\n\n"): prefix += "\n"
|
|
405
|
+
path.write_text(prefix + "# sherpa local environment and credentials\n" + "\n".join(additions) + "\n", encoding="utf-8")
|
|
406
|
+
return additions
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
def init_project(root: Path, create_env: bool = False) -> tuple[list[str], list[str]]:
|
|
410
|
+
root = root.resolve()
|
|
411
|
+
if not root.is_dir(): raise ValueError("project root does not exist")
|
|
412
|
+
if ((root / "SKILL.md").exists() and (root / "workflows" / "lifecycle.md").exists()
|
|
413
|
+
and (root / "commands" / "manifest.json").exists()):
|
|
414
|
+
raise ValueError("refusing to initialize the sherpa framework repository; run from a hackathon project root")
|
|
415
|
+
template_root = Path(__file__).resolve().parents[2]
|
|
416
|
+
created: list[str] = []; preserved: list[str] = []
|
|
417
|
+
state_root = project_state_dir(root)
|
|
418
|
+
warning = legacy_state_warning(root)
|
|
419
|
+
if warning:
|
|
420
|
+
raise ValueError(warning + " Run `sherpa migrate` before initializing or writing state.")
|
|
421
|
+
state_root = root / STATE_DIRECTORY
|
|
422
|
+
destinations = {
|
|
423
|
+
".env.sherpa.example": template_root / ".env.sherpa.example",
|
|
424
|
+
f"{state_root.name}/hackathon.json": template_root / "templates/configuration/hackathon.json",
|
|
425
|
+
f"{state_root.name}/settings.json": template_root / "templates/configuration/settings.json",
|
|
426
|
+
f"{state_root.name}/config/project.env.example": template_root / "templates/configuration/project.env.example",
|
|
427
|
+
f"{state_root.name}/config/providers.env.example": template_root / "templates/configuration/providers.env.example",
|
|
428
|
+
f"{state_root.name}/config/integrations.env.example": template_root / "templates/configuration/integrations.env.example",
|
|
429
|
+
f"{state_root.name}/project-state.md": template_root / "templates/project-state.md",
|
|
430
|
+
}
|
|
431
|
+
for relative, source in destinations.items():
|
|
432
|
+
target = root / relative
|
|
433
|
+
if target.exists():
|
|
434
|
+
preserved.append(relative)
|
|
435
|
+
continue
|
|
436
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
437
|
+
content = source.read_text(encoding="utf-8-sig")
|
|
438
|
+
if relative == f"{state_root.name}/hackathon.json":
|
|
439
|
+
data=json.loads(content); data["project"]["path"]=str(root); content=json.dumps(data,indent=2)+"\n"
|
|
440
|
+
if relative == f"{state_root.name}/project-state.md":
|
|
441
|
+
content=content.replace("| Phase | UNKNOWN |", "| Phase | Understand Rules |", 1)
|
|
442
|
+
content=content.replace("| State last updated | UNKNOWN |", f"| State last updated | {dt.datetime.now(dt.timezone.utc).isoformat(timespec='seconds')} |", 1)
|
|
443
|
+
target.write_text(content,encoding="utf-8")
|
|
444
|
+
created.append(relative)
|
|
445
|
+
if create_env:
|
|
446
|
+
target=root/".env"
|
|
447
|
+
if target.exists(): preserved.append(".env")
|
|
448
|
+
else:
|
|
449
|
+
target.write_text((template_root/".env.sherpa.example").read_text(encoding="utf-8-sig"),encoding="utf-8")
|
|
450
|
+
created.append(".env")
|
|
451
|
+
additions=ensure_gitignore(root)
|
|
452
|
+
if additions: created.append(".gitignore rules: "+", ".join(additions))
|
|
453
|
+
else: preserved.append(".gitignore (required protection already present)")
|
|
454
|
+
return created,preserved
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def parse_overrides(values: list[str]) -> dict[str, str]:
|
|
458
|
+
result={}
|
|
459
|
+
for item in values:
|
|
460
|
+
if "=" not in item: raise ValueError("--override must use KEY=VALUE; value omitted from diagnostics")
|
|
461
|
+
key,value=item.split("=",1)
|
|
462
|
+
if not re.fullmatch(r"[A-Z_][A-Z0-9_]*",key): raise ValueError("invalid --override variable name")
|
|
463
|
+
result[key]=value
|
|
464
|
+
return result
|
|
465
|
+
|
|
466
|
+
|
|
467
|
+
def main(argv: list[str] | None = None) -> int:
|
|
468
|
+
parser=argparse.ArgumentParser(description="Sherpa project configuration utility")
|
|
469
|
+
parser.add_argument("--project-root",default=".",help="hackathon project root (default: current directory)")
|
|
470
|
+
sub=parser.add_subparsers(dest="command",required=True)
|
|
471
|
+
sub.add_parser("config",help="show redacted configuration overview")
|
|
472
|
+
check=sub.add_parser("check",help="validate config, environment, Git protection, and secrets")
|
|
473
|
+
check.add_argument("--override",action="append",default=[],metavar="KEY=VALUE",help="transient non-secret precedence override")
|
|
474
|
+
init=sub.add_parser("init",help="create missing project configuration without overwriting")
|
|
475
|
+
init.add_argument("--create-env",action="store_true",help="also create an empty-placeholder .env from .env.example")
|
|
476
|
+
args=parser.parse_args(argv)
|
|
477
|
+
root=Path(args.project_root).expanduser().resolve()
|
|
478
|
+
if not root.is_dir(): print("ERROR: project root does not exist"); return 2
|
|
479
|
+
if args.command=="init":
|
|
480
|
+
try:
|
|
481
|
+
created,preserved=init_project(root,args.create_env)
|
|
482
|
+
print("Sherpa configuration initialization")
|
|
483
|
+
print("Created: "+(", ".join(created) if created else "nothing; all configuration already existed"))
|
|
484
|
+
print("Preserved: "+(", ".join(preserved) if preserved else "none"))
|
|
485
|
+
return 0
|
|
486
|
+
except (OSError,ValueError,json.JSONDecodeError) as exc:
|
|
487
|
+
print(f"ERROR: {exc}"); return 2
|
|
488
|
+
snapshot=config_snapshot(root)
|
|
489
|
+
if args.command=="config": print_overview(snapshot); return 0
|
|
490
|
+
try: overrides=parse_overrides(args.override)
|
|
491
|
+
except ValueError as exc: print(f"ERROR: {exc}"); return 2
|
|
492
|
+
# Overrides affect validation only and are never persisted or printed.
|
|
493
|
+
dotenv,env_errors=parse_env_file(root/".env")
|
|
494
|
+
for key,value in overrides.items():
|
|
495
|
+
if key in SECRET_ENV_KEYS: print("ERROR: secret overrides are not allowed on the command line"); return 2
|
|
496
|
+
dotenv[key]=value
|
|
497
|
+
if overrides:
|
|
498
|
+
isolated=config_snapshot(root,process_env={**os.environ,**overrides})
|
|
499
|
+
snapshot=isolated
|
|
500
|
+
print_check(snapshot, root)
|
|
501
|
+
return 1 if snapshot["errors"] else 0
|
|
502
|
+
|
|
503
|
+
if __name__=="__main__":
|
|
504
|
+
raise SystemExit(main())
|