delimit-cli 3.14.28 → 3.14.30
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/bin/delimit-setup.js +3 -6
- package/gateway/ai/backends/deploy_bridge.py +56 -2
- package/gateway/ai/backends/gateway_core.py +212 -1
- package/gateway/ai/backends/generate_bridge.py +84 -13
- package/gateway/ai/backends/governance_bridge.py +63 -16
- package/gateway/ai/backends/memory_bridge.py +77 -76
- package/gateway/ai/backends/ops_bridge.py +76 -6
- package/gateway/ai/backends/os_bridge.py +23 -3
- package/gateway/ai/backends/repo_bridge.py +156 -17
- package/gateway/ai/backends/tools_design.py +116 -9
- package/gateway/ai/backends/tools_infra.py +200 -72
- package/gateway/ai/backends/tools_real.py +8 -0
- package/gateway/ai/backends/ui_bridge.py +115 -5
- package/gateway/ai/backends/vault_bridge.py +69 -114
- package/gateway/ai/content_engine.py +1276 -0
- package/gateway/ai/context_fs.py +193 -0
- package/gateway/ai/daemon.py +500 -0
- package/gateway/ai/data_plane.py +291 -0
- package/gateway/ai/deliberation.py +1033 -6
- package/gateway/ai/events.py +39 -0
- package/gateway/ai/founding_users.py +162 -0
- package/gateway/ai/governance.py +698 -4
- package/gateway/ai/inbox_daemon.py +78 -17
- package/gateway/ai/integrations/__init__.py +1 -0
- package/gateway/ai/integrations/opensage_wrapper.py +288 -0
- package/gateway/ai/key_resolver.py +95 -0
- package/gateway/ai/ledger_manager.py +289 -1
- package/gateway/ai/license.py +62 -4
- package/gateway/ai/license_core.py +208 -7
- package/gateway/ai/local_server.py +215 -0
- package/gateway/ai/loop_engine.py +408 -0
- package/gateway/ai/mcp_bridge.py +178 -0
- package/gateway/ai/release_sync.py +2 -2
- package/gateway/ai/screen_record.py +374 -0
- package/gateway/ai/secrets_broker.py +235 -0
- package/gateway/ai/social.py +189 -27
- package/gateway/ai/social_target.py +1635 -0
- package/gateway/ai/supabase_sync.py +190 -0
- package/gateway/ai/tracing.py +195 -0
- package/gateway/core/contract_ledger.py +1 -1
- package/gateway/core/dependency_graph.py +1 -1
- package/gateway/core/dependency_manifest.py +1 -1
- package/gateway/core/diff_engine_v2.py +272 -78
- package/gateway/core/event_backbone.py +2 -2
- package/gateway/core/event_schema.py +1 -1
- package/gateway/core/impact_analyzer.py +1 -1
- package/gateway/core/policy_engine.py +4 -0
- package/package.json +1 -1
|
@@ -194,10 +194,13 @@ def design_extract_tokens(
|
|
|
194
194
|
) -> Dict[str, Any]:
|
|
195
195
|
"""Extract design tokens from project CSS/SCSS/Tailwind config.
|
|
196
196
|
|
|
197
|
-
If
|
|
197
|
+
If a Figma token is available and figma_file_key provided, fetches from Figma API.
|
|
198
198
|
Otherwise scans local project files for CSS variables, Tailwind config, etc.
|
|
199
|
+
|
|
200
|
+
Token resolution order: FIGMA_TOKEN env var -> ~/.delimit/secrets/figma.json -> free fallback.
|
|
199
201
|
"""
|
|
200
|
-
|
|
202
|
+
from ai.key_resolver import get_figma_token
|
|
203
|
+
figma_token, _token_source = get_figma_token()
|
|
201
204
|
if figma_token and figma_file_key:
|
|
202
205
|
return _figma_extract_tokens(figma_file_key, figma_token, token_types)
|
|
203
206
|
|
|
@@ -255,7 +258,7 @@ def design_extract_tokens(
|
|
|
255
258
|
all_tokens["breakpoints"] = unique_bp
|
|
256
259
|
|
|
257
260
|
total = sum(len(v) for v in all_tokens.values())
|
|
258
|
-
|
|
261
|
+
result = {
|
|
259
262
|
"tool": "design.extract_tokens",
|
|
260
263
|
"status": "ok",
|
|
261
264
|
"tokens": all_tokens,
|
|
@@ -263,6 +266,14 @@ def design_extract_tokens(
|
|
|
263
266
|
"source_files": sorted(set(source_files)),
|
|
264
267
|
"figma_used": False,
|
|
265
268
|
}
|
|
269
|
+
# If user passed a figma_file_key but no token is available, add a hint
|
|
270
|
+
if figma_file_key and not figma_token:
|
|
271
|
+
result["hint"] = (
|
|
272
|
+
"Figma integration available -- set FIGMA_TOKEN env var "
|
|
273
|
+
"or store your token with `delimit_secret_store key=figma value=<token>`. "
|
|
274
|
+
"Local CSS/Tailwind tokens were extracted instead."
|
|
275
|
+
)
|
|
276
|
+
return result
|
|
266
277
|
|
|
267
278
|
|
|
268
279
|
def _figma_extract_tokens(file_key: str, token: str, token_types: Optional[List[str]]) -> Dict[str, Any]:
|
|
@@ -647,6 +658,16 @@ def story_generate(
|
|
|
647
658
|
) -> Dict[str, Any]:
|
|
648
659
|
"""Generate a .stories.tsx file for a component (no Storybook required)."""
|
|
649
660
|
comp = Path(component_path)
|
|
661
|
+
if not comp.exists() and "/" not in component_path and "\\" not in component_path:
|
|
662
|
+
# Bare name like "Button" — search current directory for matching files
|
|
663
|
+
search_root = Path.cwd()
|
|
664
|
+
name = comp.stem
|
|
665
|
+
for pattern in [f"**/{name}.tsx", f"**/{name}.jsx", f"**/{name}.ts", f"**/{name}.js"]:
|
|
666
|
+
matches = [p for p in search_root.glob(pattern)
|
|
667
|
+
if "node_modules" not in str(p) and ".next" not in str(p)]
|
|
668
|
+
if matches:
|
|
669
|
+
comp = matches[0]
|
|
670
|
+
break
|
|
650
671
|
if not comp.exists():
|
|
651
672
|
return {"tool": "story.generate", "error": f"Component file not found: {comp}"}
|
|
652
673
|
|
|
@@ -714,6 +735,55 @@ type Story = StoryObj<typeof {comp_name}>;
|
|
|
714
735
|
}
|
|
715
736
|
|
|
716
737
|
|
|
738
|
+
# ---------------------------------------------------------------------------
|
|
739
|
+
# 25a. Puppeteer fallback for screenshots
|
|
740
|
+
# ---------------------------------------------------------------------------
|
|
741
|
+
|
|
742
|
+
def _puppeteer_screenshot_fallback(url: str, baselines_dir: Path) -> Dict[str, Any]:
|
|
743
|
+
"""Take a screenshot via puppeteer (npx) when Playwright is not available."""
|
|
744
|
+
try:
|
|
745
|
+
baselines_dir.mkdir(parents=True, exist_ok=True)
|
|
746
|
+
safe_name = re.sub(r"[^a-zA-Z0-9]", "_", url)[:100]
|
|
747
|
+
screenshot_path = baselines_dir / f"{safe_name}.png"
|
|
748
|
+
|
|
749
|
+
# Inline JS script for puppeteer
|
|
750
|
+
script = (
|
|
751
|
+
"const puppeteer = require('puppeteer');"
|
|
752
|
+
"(async () => {"
|
|
753
|
+
" const browser = await puppeteer.launch({headless: 'new', args: ['--no-sandbox']});"
|
|
754
|
+
" const page = await browser.newPage();"
|
|
755
|
+
" await page.setViewport({width: 1280, height: 720});"
|
|
756
|
+
f" await page.goto('{url}', {{waitUntil: 'networkidle2', timeout: 15000}});"
|
|
757
|
+
f" await page.screenshot({{path: '{screenshot_path}'}});"
|
|
758
|
+
" await browser.close();"
|
|
759
|
+
"})();"
|
|
760
|
+
)
|
|
761
|
+
result = subprocess.run(
|
|
762
|
+
["node", "-e", script],
|
|
763
|
+
capture_output=True,
|
|
764
|
+
timeout=30,
|
|
765
|
+
)
|
|
766
|
+
if result.returncode != 0:
|
|
767
|
+
return {
|
|
768
|
+
"tool": "story.visual_test",
|
|
769
|
+
"status": "puppeteer_error",
|
|
770
|
+
"error": result.stderr.decode(errors="replace")[:500],
|
|
771
|
+
"hint": "Puppeteer fallback failed. Install Playwright for better support: pip install playwright && python -m playwright install chromium",
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
return {
|
|
775
|
+
"tool": "story.visual_test",
|
|
776
|
+
"status": "ok",
|
|
777
|
+
"screenshot_path": str(screenshot_path),
|
|
778
|
+
"baseline_exists": False,
|
|
779
|
+
"diff_percent": None,
|
|
780
|
+
"engine": "puppeteer_fallback",
|
|
781
|
+
"hint": "Screenshot taken with puppeteer (fallback). Install Playwright for full visual regression with baseline comparison.",
|
|
782
|
+
}
|
|
783
|
+
except Exception as e:
|
|
784
|
+
return {"tool": "story.visual_test", "status": "error", "error": str(e)}
|
|
785
|
+
|
|
786
|
+
|
|
717
787
|
# ---------------------------------------------------------------------------
|
|
718
788
|
# 25. story_visual_test
|
|
719
789
|
# ---------------------------------------------------------------------------
|
|
@@ -723,19 +793,39 @@ def story_visual_test(
|
|
|
723
793
|
project_path: Optional[str] = None,
|
|
724
794
|
threshold: float = 0.05,
|
|
725
795
|
) -> Dict[str, Any]:
|
|
726
|
-
"""Take a screenshot with Playwright and compare against baseline.
|
|
796
|
+
"""Take a screenshot with Playwright and compare against baseline.
|
|
797
|
+
|
|
798
|
+
Falls back to puppeteer (npx) if Playwright is not installed,
|
|
799
|
+
and returns install guidance if neither is available.
|
|
800
|
+
"""
|
|
801
|
+
from ai.key_resolver import get_playwright, get_puppeteer
|
|
802
|
+
|
|
727
803
|
root = Path(project_path) if project_path else Path.cwd()
|
|
728
804
|
baselines_dir = root / ".delimit" / "visual-baselines"
|
|
729
805
|
|
|
730
|
-
|
|
806
|
+
pw_available, _ = get_playwright()
|
|
807
|
+
|
|
808
|
+
if not pw_available:
|
|
809
|
+
# Try puppeteer fallback via npx
|
|
810
|
+
pup_available, _ = get_puppeteer()
|
|
811
|
+
if pup_available:
|
|
812
|
+
return _puppeteer_screenshot_fallback(url, baselines_dir)
|
|
813
|
+
|
|
731
814
|
return {
|
|
732
815
|
"tool": "story.visual_test",
|
|
733
|
-
"status": "
|
|
734
|
-
"message":
|
|
816
|
+
"status": "no_screenshot_tool",
|
|
817
|
+
"message": (
|
|
818
|
+
"No screenshot tool available. Install one of the following:\n"
|
|
819
|
+
" - Playwright (recommended): pip install playwright && python -m playwright install chromium\n"
|
|
820
|
+
" - Puppeteer (fallback): npm install -g puppeteer"
|
|
821
|
+
),
|
|
735
822
|
"screenshot_path": None,
|
|
736
823
|
"baseline_exists": False,
|
|
737
824
|
"diff_percent": None,
|
|
738
|
-
"next_steps_hint":
|
|
825
|
+
"next_steps_hint": (
|
|
826
|
+
"Install Playwright for full visual regression testing, "
|
|
827
|
+
"or use `delimit_story_accessibility` for static checks that require no browser."
|
|
828
|
+
),
|
|
739
829
|
}
|
|
740
830
|
|
|
741
831
|
try:
|
|
@@ -795,7 +885,24 @@ def story_visual_test(
|
|
|
795
885
|
}
|
|
796
886
|
|
|
797
887
|
except Exception as e:
|
|
798
|
-
|
|
888
|
+
pup_available, _ = get_puppeteer()
|
|
889
|
+
if pup_available:
|
|
890
|
+
return _puppeteer_screenshot_fallback(url, baselines_dir)
|
|
891
|
+
|
|
892
|
+
return {
|
|
893
|
+
"tool": "story.visual_test",
|
|
894
|
+
"status": "playwright_error",
|
|
895
|
+
"error": str(e),
|
|
896
|
+
"screenshot_path": None,
|
|
897
|
+
"baseline_exists": False,
|
|
898
|
+
"diff_percent": None,
|
|
899
|
+
"engine": "playwright",
|
|
900
|
+
"hint": (
|
|
901
|
+
"Playwright is installed but could not launch a browser in this environment. "
|
|
902
|
+
"Install/configure a browser runtime that works in the current sandbox, or "
|
|
903
|
+
"use Puppeteer as a fallback."
|
|
904
|
+
),
|
|
905
|
+
}
|
|
799
906
|
|
|
800
907
|
|
|
801
908
|
# ---------------------------------------------------------------------------
|
|
@@ -34,8 +34,8 @@ DEPLOYS_DIR = Path(os.environ.get("DELIMIT_DEPLOYS_DIR", os.path.expanduser("~/.
|
|
|
34
34
|
SECRET_PATTERNS = {
|
|
35
35
|
"aws_access_key": r"(?:AKIA[0-9A-Z]{16})",
|
|
36
36
|
"aws_secret_key": r"(?:aws_secret_access_key|AWS_SECRET_ACCESS_KEY)\s*[=:]\s*['\"]?[A-Za-z0-9/+=]{40}",
|
|
37
|
-
"generic_api_key": r"(?:api[_-]?key|apikey)\s*[=:]\s*['\"]?[A-Za-z0-9_\-]{20,}",
|
|
38
|
-
"generic_secret": r"(?:secret|password|passwd|token)\s*[=:]\s*['\"]?[^\s'\"]{8,}",
|
|
37
|
+
"generic_api_key": r"\b(?:api[_-]?key|apikey)\b\s*[=:]\s*['\"]?[A-Za-z0-9_\-]{20,}",
|
|
38
|
+
"generic_secret": r"\b(?:secret|password|passwd|token)\b\s*[=:]\s*['\"]?[^\s'\"]{8,}",
|
|
39
39
|
"private_key_header": r"-----BEGIN (?:RSA |EC |DSA )?PRIVATE KEY-----",
|
|
40
40
|
"github_token": r"gh[pousr]_[A-Za-z0-9_]{36,}",
|
|
41
41
|
"slack_token": r"xox[baprs]-[0-9A-Za-z\-]{10,}",
|
|
@@ -62,7 +62,17 @@ SKIP_DIRS = {"node_modules", ".git", "__pycache__", ".venv", "venv", ".tox", "di
|
|
|
62
62
|
|
|
63
63
|
|
|
64
64
|
def _run_cmd(cmd: List[str], timeout: int = 30, cwd: Optional[str] = None) -> Dict[str, Any]:
|
|
65
|
-
"""Run a command and return stdout, stderr, returncode.
|
|
65
|
+
"""Run a command and return stdout, stderr, returncode.
|
|
66
|
+
|
|
67
|
+
Security: always uses list-form args (never shell=True).
|
|
68
|
+
Validates cwd if provided and rejects null bytes in arguments.
|
|
69
|
+
"""
|
|
70
|
+
# Defense-in-depth: reject null bytes in any argument
|
|
71
|
+
for i, arg in enumerate(cmd):
|
|
72
|
+
if "\x00" in str(arg):
|
|
73
|
+
return {"stdout": "", "stderr": f"Argument {i} contains null bytes", "returncode": -4}
|
|
74
|
+
if cwd and "\x00" in cwd:
|
|
75
|
+
return {"stdout": "", "stderr": "cwd contains null bytes", "returncode": -4}
|
|
66
76
|
try:
|
|
67
77
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, cwd=cwd)
|
|
68
78
|
return {"stdout": result.stdout, "stderr": result.stderr, "returncode": result.returncode}
|
|
@@ -74,6 +84,28 @@ def _run_cmd(cmd: List[str], timeout: int = 30, cwd: Optional[str] = None) -> Di
|
|
|
74
84
|
return {"stdout": "", "stderr": str(e), "returncode": -3}
|
|
75
85
|
|
|
76
86
|
|
|
87
|
+
def _bump_semver(version: str, bump: str) -> str:
|
|
88
|
+
"""Compute the next semver version without mutating files."""
|
|
89
|
+
try:
|
|
90
|
+
major_s, minor_s, patch_s = version.split(".", 2)
|
|
91
|
+
major = int(major_s)
|
|
92
|
+
minor = int(minor_s)
|
|
93
|
+
patch = int(patch_s)
|
|
94
|
+
except Exception as exc:
|
|
95
|
+
raise ValueError(f"Invalid semver version '{version}'") from exc
|
|
96
|
+
|
|
97
|
+
if bump == "patch":
|
|
98
|
+
patch += 1
|
|
99
|
+
elif bump == "minor":
|
|
100
|
+
minor += 1
|
|
101
|
+
patch = 0
|
|
102
|
+
elif bump == "major":
|
|
103
|
+
major += 1
|
|
104
|
+
minor = 0
|
|
105
|
+
patch = 0
|
|
106
|
+
return f"{major}.{minor}.{patch}"
|
|
107
|
+
|
|
108
|
+
|
|
77
109
|
def _scan_files(target: str) -> List[Path]:
|
|
78
110
|
"""Collect scannable source files under target."""
|
|
79
111
|
root = Path(target).resolve()
|
|
@@ -82,14 +114,15 @@ def _scan_files(target: str) -> List[Path]:
|
|
|
82
114
|
return [root]
|
|
83
115
|
if not root.is_dir():
|
|
84
116
|
return []
|
|
85
|
-
for
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
117
|
+
for dirpath, dirnames, filenames in os.walk(root, onerror=lambda _err: None):
|
|
118
|
+
dirnames[:] = [d for d in dirnames if d not in SKIP_DIRS]
|
|
119
|
+
for filename in filenames:
|
|
120
|
+
p = Path(dirpath) / filename
|
|
121
|
+
if p.suffix in SCAN_EXTENSIONS:
|
|
122
|
+
files.append(p)
|
|
123
|
+
# Cap to avoid scanning massive repos
|
|
124
|
+
if len(files) >= 5000:
|
|
125
|
+
return files
|
|
93
126
|
return files
|
|
94
127
|
|
|
95
128
|
|
|
@@ -892,36 +925,27 @@ def deploy_site(project_path: str = ".", message: str = "", env_vars: dict = Non
|
|
|
892
925
|
except Exception as e:
|
|
893
926
|
return {"error": f"Git status failed: {e}"}
|
|
894
927
|
|
|
895
|
-
# 2.
|
|
896
|
-
commit_msg = message or "deploy: site update"
|
|
928
|
+
# 2. Preflight the git remote before creating a commit.
|
|
897
929
|
try:
|
|
898
|
-
subprocess.run(["git", "add", "-A"], cwd=str(p), timeout=10, capture_output=True)
|
|
899
930
|
result = subprocess.run(
|
|
900
|
-
["git", "
|
|
901
|
-
cwd=str(p), timeout=10, capture_output=True, text=True
|
|
902
|
-
)
|
|
903
|
-
if result.returncode == 0:
|
|
904
|
-
results["steps"].append({"step": "commit", "status": "ok", "message": commit_msg})
|
|
905
|
-
else:
|
|
906
|
-
results["steps"].append({"step": "commit", "status": "skipped", "detail": "nothing to commit"})
|
|
907
|
-
except Exception as e:
|
|
908
|
-
results["steps"].append({"step": "commit", "status": "error", "detail": str(e)})
|
|
909
|
-
|
|
910
|
-
# 3. Git push
|
|
911
|
-
try:
|
|
912
|
-
result = subprocess.run(
|
|
913
|
-
["git", "push", "origin", "HEAD"],
|
|
931
|
+
["git", "push", "--dry-run", "origin", "HEAD"],
|
|
914
932
|
cwd=str(p), timeout=30, capture_output=True, text=True
|
|
915
933
|
)
|
|
916
|
-
|
|
917
|
-
"
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
934
|
+
if result.returncode != 0:
|
|
935
|
+
results["steps"].append({
|
|
936
|
+
"step": "push_precheck",
|
|
937
|
+
"status": "error",
|
|
938
|
+
"detail": (result.stderr.strip() or result.stdout.strip())[:200],
|
|
939
|
+
})
|
|
940
|
+
results["status"] = "push_precheck_failed"
|
|
941
|
+
return results
|
|
942
|
+
results["steps"].append({"step": "push_precheck", "status": "ok"})
|
|
921
943
|
except Exception as e:
|
|
922
|
-
results["steps"].append({"step": "
|
|
944
|
+
results["steps"].append({"step": "push_precheck", "status": "error", "detail": str(e)})
|
|
945
|
+
results["status"] = "push_precheck_error"
|
|
946
|
+
return results
|
|
923
947
|
|
|
924
|
-
#
|
|
948
|
+
# 3. Vercel build
|
|
925
949
|
env = {**os.environ}
|
|
926
950
|
if env_vars:
|
|
927
951
|
# Whitelist safe env var prefixes — block LD_PRELOAD, PATH overrides, etc.
|
|
@@ -952,7 +976,68 @@ def deploy_site(project_path: str = ".", message: str = "", env_vars: dict = Non
|
|
|
952
976
|
results["status"] = "build_error"
|
|
953
977
|
return results
|
|
954
978
|
|
|
955
|
-
#
|
|
979
|
+
# 4. Git add + commit
|
|
980
|
+
commit_msg = message or "deploy: site update"
|
|
981
|
+
try:
|
|
982
|
+
result = subprocess.run(["git", "add", "-A"], cwd=str(p), timeout=10, capture_output=True, text=True)
|
|
983
|
+
if result.returncode != 0:
|
|
984
|
+
results["steps"].append({
|
|
985
|
+
"step": "git_add",
|
|
986
|
+
"status": "error",
|
|
987
|
+
"detail": (result.stderr.strip() or result.stdout.strip())[:200],
|
|
988
|
+
})
|
|
989
|
+
results["status"] = "git_add_failed"
|
|
990
|
+
return results
|
|
991
|
+
results["steps"].append({"step": "git_add", "status": "ok"})
|
|
992
|
+
except Exception as e:
|
|
993
|
+
results["steps"].append({"step": "git_add", "status": "error", "detail": str(e)})
|
|
994
|
+
results["status"] = "git_add_error"
|
|
995
|
+
return results
|
|
996
|
+
|
|
997
|
+
try:
|
|
998
|
+
result = subprocess.run(
|
|
999
|
+
["git", "commit", "-m", commit_msg],
|
|
1000
|
+
cwd=str(p), timeout=10, capture_output=True, text=True
|
|
1001
|
+
)
|
|
1002
|
+
commit_output = f"{result.stdout}\n{result.stderr}".lower()
|
|
1003
|
+
if result.returncode == 0:
|
|
1004
|
+
results["steps"].append({"step": "commit", "status": "ok", "message": commit_msg})
|
|
1005
|
+
elif "nothing to commit" in commit_output or "working tree clean" in commit_output:
|
|
1006
|
+
results["steps"].append({"step": "commit", "status": "skipped", "detail": "nothing to commit"})
|
|
1007
|
+
else:
|
|
1008
|
+
results["steps"].append({
|
|
1009
|
+
"step": "commit",
|
|
1010
|
+
"status": "error",
|
|
1011
|
+
"detail": (result.stderr.strip() or result.stdout.strip())[:200],
|
|
1012
|
+
})
|
|
1013
|
+
results["status"] = "commit_failed"
|
|
1014
|
+
return results
|
|
1015
|
+
except Exception as e:
|
|
1016
|
+
results["steps"].append({"step": "commit", "status": "error", "detail": str(e)})
|
|
1017
|
+
results["status"] = "commit_error"
|
|
1018
|
+
return results
|
|
1019
|
+
|
|
1020
|
+
# 5. Git push
|
|
1021
|
+
try:
|
|
1022
|
+
result = subprocess.run(
|
|
1023
|
+
["git", "push", "origin", "HEAD"],
|
|
1024
|
+
cwd=str(p), timeout=30, capture_output=True, text=True
|
|
1025
|
+
)
|
|
1026
|
+
if result.returncode != 0:
|
|
1027
|
+
results["steps"].append({
|
|
1028
|
+
"step": "push",
|
|
1029
|
+
"status": "error",
|
|
1030
|
+
"detail": (result.stderr.strip() or result.stdout.strip())[:200],
|
|
1031
|
+
})
|
|
1032
|
+
results["status"] = "push_failed"
|
|
1033
|
+
return results
|
|
1034
|
+
results["steps"].append({"step": "push", "status": "ok", "detail": "pushed"})
|
|
1035
|
+
except Exception as e:
|
|
1036
|
+
results["steps"].append({"step": "push", "status": "error", "detail": str(e)})
|
|
1037
|
+
results["status"] = "push_error"
|
|
1038
|
+
return results
|
|
1039
|
+
|
|
1040
|
+
# 6. Vercel deploy
|
|
956
1041
|
try:
|
|
957
1042
|
result = subprocess.run(
|
|
958
1043
|
["npx", "vercel", "deploy", "--prebuilt", "--prod"],
|
|
@@ -970,9 +1055,14 @@ def deploy_site(project_path: str = ".", message: str = "", env_vars: dict = Non
|
|
|
970
1055
|
"status": "ok" if result.returncode == 0 else "error",
|
|
971
1056
|
"url": deploy_url
|
|
972
1057
|
})
|
|
1058
|
+
if result.returncode != 0:
|
|
1059
|
+
results["status"] = "deploy_failed"
|
|
1060
|
+
return results
|
|
973
1061
|
results["deploy_url"] = deploy_url
|
|
974
1062
|
except Exception as e:
|
|
975
1063
|
results["steps"].append({"step": "deploy", "status": "error", "detail": str(e)})
|
|
1064
|
+
results["status"] = "deploy_error"
|
|
1065
|
+
return results
|
|
976
1066
|
|
|
977
1067
|
results["status"] = "deployed"
|
|
978
1068
|
return results
|
|
@@ -1036,32 +1126,71 @@ def deploy_npm(project_path: str = ".", bump: str = "patch", tag: str = "latest"
|
|
|
1036
1126
|
except Exception:
|
|
1037
1127
|
pass
|
|
1038
1128
|
|
|
1129
|
+
# ── Dry-run: simulate without touching the filesystem ──
|
|
1130
|
+
if dry_run:
|
|
1131
|
+
# Compute what the next version would be without actually bumping
|
|
1132
|
+
parts = current_version.split(".")
|
|
1133
|
+
if len(parts) == 3 and bump in ("patch", "minor", "major"):
|
|
1134
|
+
major, minor, patch_v = int(parts[0]), int(parts[1]), int(parts[2])
|
|
1135
|
+
if bump == "patch":
|
|
1136
|
+
patch_v += 1
|
|
1137
|
+
elif bump == "minor":
|
|
1138
|
+
minor += 1
|
|
1139
|
+
patch_v = 0
|
|
1140
|
+
elif bump == "major":
|
|
1141
|
+
major += 1
|
|
1142
|
+
minor = 0
|
|
1143
|
+
patch_v = 0
|
|
1144
|
+
simulated_version = f"{major}.{minor}.{patch_v}"
|
|
1145
|
+
else:
|
|
1146
|
+
simulated_version = current_version
|
|
1147
|
+
results["new_version"] = simulated_version
|
|
1148
|
+
results["steps"].append({"step": "version_bump", "status": "dry_run", "from": current_version, "to": simulated_version, "bump": bump})
|
|
1149
|
+
results["steps"].append({"step": "publish", "status": "dry_run", "tag": tag, "output": f"Would publish {pkg_name}@{simulated_version} with tag {tag}"})
|
|
1150
|
+
results["steps"].append({"step": "verify", "status": "dry_run"})
|
|
1151
|
+
results["status"] = "dry_run_complete"
|
|
1152
|
+
return results
|
|
1153
|
+
|
|
1039
1154
|
# 4. Version bump
|
|
1040
1155
|
if bump in ("patch", "minor", "major"):
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
bump_cmd, capture_output=True, text=True, timeout=10, cwd=str(p)
|
|
1045
|
-
)
|
|
1046
|
-
if result.returncode == 0:
|
|
1047
|
-
new_version = result.stdout.strip().lstrip("v")
|
|
1156
|
+
if dry_run:
|
|
1157
|
+
try:
|
|
1158
|
+
new_version = _bump_semver(current_version, bump)
|
|
1048
1159
|
results["new_version"] = new_version
|
|
1049
|
-
results["steps"].append({
|
|
1050
|
-
|
|
1051
|
-
|
|
1160
|
+
results["steps"].append({
|
|
1161
|
+
"step": "version_bump",
|
|
1162
|
+
"status": "dry_run",
|
|
1163
|
+
"from": current_version,
|
|
1164
|
+
"to": new_version,
|
|
1165
|
+
"bump": bump,
|
|
1166
|
+
})
|
|
1167
|
+
except Exception as e:
|
|
1168
|
+
results["steps"].append({"step": "version_bump", "status": "error", "detail": str(e)})
|
|
1169
|
+
results["status"] = "bump_failed"
|
|
1170
|
+
return results
|
|
1171
|
+
else:
|
|
1172
|
+
try:
|
|
1173
|
+
bump_cmd = ["npm", "version", bump, "--no-git-tag-version"]
|
|
1174
|
+
result = subprocess.run(
|
|
1175
|
+
bump_cmd, capture_output=True, text=True, timeout=10, cwd=str(p)
|
|
1176
|
+
)
|
|
1177
|
+
if result.returncode == 0:
|
|
1178
|
+
new_version = result.stdout.strip().lstrip("v")
|
|
1179
|
+
results["new_version"] = new_version
|
|
1180
|
+
results["steps"].append({"step": "version_bump", "status": "ok", "from": current_version, "to": new_version, "bump": bump})
|
|
1181
|
+
else:
|
|
1182
|
+
results["steps"].append({"step": "version_bump", "status": "error", "detail": result.stderr.strip()[:200]})
|
|
1183
|
+
results["status"] = "bump_failed"
|
|
1184
|
+
return results
|
|
1185
|
+
except Exception as e:
|
|
1186
|
+
results["steps"].append({"step": "version_bump", "status": "error", "detail": str(e)})
|
|
1052
1187
|
results["status"] = "bump_failed"
|
|
1053
1188
|
return results
|
|
1054
|
-
except Exception as e:
|
|
1055
|
-
results["steps"].append({"step": "version_bump", "status": "error", "detail": str(e)})
|
|
1056
|
-
results["status"] = "bump_failed"
|
|
1057
|
-
return results
|
|
1058
1189
|
else:
|
|
1059
1190
|
results["new_version"] = current_version
|
|
1060
1191
|
|
|
1061
1192
|
# 5. Publish
|
|
1062
1193
|
publish_cmd = ["npm", "publish", "--tag", tag]
|
|
1063
|
-
if dry_run:
|
|
1064
|
-
publish_cmd.append("--dry-run")
|
|
1065
1194
|
|
|
1066
1195
|
try:
|
|
1067
1196
|
result = subprocess.run(
|
|
@@ -1070,7 +1199,7 @@ def deploy_npm(project_path: str = ".", bump: str = "patch", tag: str = "latest"
|
|
|
1070
1199
|
if result.returncode == 0:
|
|
1071
1200
|
results["steps"].append({
|
|
1072
1201
|
"step": "publish",
|
|
1073
|
-
"status": "ok"
|
|
1202
|
+
"status": "ok",
|
|
1074
1203
|
"tag": tag,
|
|
1075
1204
|
"output": result.stdout.strip()[-300:]
|
|
1076
1205
|
})
|
|
@@ -1091,27 +1220,26 @@ def deploy_npm(project_path: str = ".", bump: str = "patch", tag: str = "latest"
|
|
|
1091
1220
|
results["status"] = "publish_failed"
|
|
1092
1221
|
return results
|
|
1093
1222
|
|
|
1094
|
-
# 6. Verify on registry
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
results["steps"].append({"step": "verify", "status": "skipped"})
|
|
1223
|
+
# 6. Verify on registry
|
|
1224
|
+
try:
|
|
1225
|
+
import time
|
|
1226
|
+
time.sleep(2) # brief wait for registry propagation
|
|
1227
|
+
result = subprocess.run(
|
|
1228
|
+
["npm", "view", pkg_name, "version"],
|
|
1229
|
+
capture_output=True, text=True, timeout=15
|
|
1230
|
+
)
|
|
1231
|
+
registry_version = result.stdout.strip()
|
|
1232
|
+
verified = registry_version == results.get("new_version", current_version)
|
|
1233
|
+
results["steps"].append({
|
|
1234
|
+
"step": "verify",
|
|
1235
|
+
"status": "ok" if verified else "mismatch",
|
|
1236
|
+
"registry_version": registry_version
|
|
1237
|
+
})
|
|
1238
|
+
except Exception:
|
|
1239
|
+
results["steps"].append({"step": "verify", "status": "skipped"})
|
|
1112
1240
|
|
|
1113
1241
|
# 7. Git commit the version bump
|
|
1114
|
-
if bump in ("patch", "minor", "major")
|
|
1242
|
+
if bump in ("patch", "minor", "major"):
|
|
1115
1243
|
try:
|
|
1116
1244
|
new_ver = results.get("new_version", current_version)
|
|
1117
1245
|
subprocess.run(["git", "add", "package.json"], cwd=str(p), timeout=10, capture_output=True)
|
|
@@ -551,6 +551,8 @@ def docs_generate(target: str = ".", options: Optional[Dict] = None) -> Dict[str
|
|
|
551
551
|
|
|
552
552
|
# Scan Python files
|
|
553
553
|
for py_file in sorted(project.rglob("*.py")):
|
|
554
|
+
if not py_file.is_file():
|
|
555
|
+
continue
|
|
554
556
|
if any(d in py_file.parts for d in skip_dirs):
|
|
555
557
|
continue
|
|
556
558
|
if py_file.name.startswith("test_") or py_file.name == "conftest.py":
|
|
@@ -569,6 +571,8 @@ def docs_generate(target: str = ".", options: Optional[Dict] = None) -> Dict[str
|
|
|
569
571
|
# Scan JS/TS files
|
|
570
572
|
for ext in ("*.js", "*.ts", "*.jsx", "*.tsx"):
|
|
571
573
|
for js_file in sorted(project.rglob(ext)):
|
|
574
|
+
if not js_file.is_file():
|
|
575
|
+
continue
|
|
572
576
|
if any(d in js_file.parts for d in skip_dirs):
|
|
573
577
|
continue
|
|
574
578
|
if ".test." in js_file.name or ".spec." in js_file.name:
|
|
@@ -715,6 +719,8 @@ def docs_validate(target: str = ".", options: Optional[Dict] = None) -> Dict[str
|
|
|
715
719
|
missing_docs = []
|
|
716
720
|
|
|
717
721
|
for py_file in sorted(project.rglob("*.py")):
|
|
722
|
+
if not py_file.is_file():
|
|
723
|
+
continue
|
|
718
724
|
if any(d in py_file.parts for d in skip_dirs):
|
|
719
725
|
continue
|
|
720
726
|
if py_file.name.startswith("test_") or py_file.name == "conftest.py":
|
|
@@ -731,6 +737,8 @@ def docs_validate(target: str = ".", options: Optional[Dict] = None) -> Dict[str
|
|
|
731
737
|
# 3. Check broken internal links in all markdown files
|
|
732
738
|
broken_links = []
|
|
733
739
|
for md_file in sorted(project.rglob("*.md")):
|
|
740
|
+
if not md_file.is_file():
|
|
741
|
+
continue
|
|
734
742
|
if any(d in md_file.parts for d in skip_dirs):
|
|
735
743
|
continue
|
|
736
744
|
broken_links.extend(_check_broken_links(md_file, project))
|