sscli 2.2.0__tar.gz → 2.5.1__tar.gz

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 (123) hide show
  1. {sscli-2.2.0 → sscli-2.5.1}/PKG-INFO +13 -23
  2. {sscli-2.2.0 → sscli-2.5.1}/README.md +7 -21
  3. sscli-2.5.1/foundry/actions/ast_grep_runner.py +374 -0
  4. {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/development.py +71 -44
  5. sscli-2.5.1/foundry/actions/examples/__init__.py +8 -0
  6. sscli-2.5.1/foundry/actions/examples/codemod_examples.py +204 -0
  7. {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/features.py +338 -45
  8. {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/generator.py +47 -2
  9. {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/interactive_config.py +10 -1
  10. sscli-2.5.1/foundry/actions/transformation.py +207 -0
  11. sscli-2.5.1/foundry/actions/upgrade.py +709 -0
  12. {sscli-2.2.0 → sscli-2.5.1}/foundry/cli.py +57 -66
  13. sscli-2.5.1/foundry/codemods/__init__.py +48 -0
  14. sscli-2.5.1/foundry/codemods/base.py +241 -0
  15. sscli-2.5.1/foundry/codemods/delta_store.py +499 -0
  16. sscli-2.5.1/foundry/codemods/toml_codemods.py +273 -0
  17. sscli-2.5.1/foundry/commands/dev.py +83 -0
  18. sscli-2.5.1/foundry/features/features.json +36 -0
  19. sscli-2.5.1/foundry/features/react-client/merchant-dashboard/DEPENDENCIES.md +47 -0
  20. sscli-2.5.1/foundry/metadata/__init__.py +20 -0
  21. sscli-2.5.1/foundry/metadata/metadata_tracker.py +511 -0
  22. {sscli-2.2.0 → sscli-2.5.1}/foundry/ops/version_manager.py +101 -31
  23. sscli-2.5.1/foundry/safety/__init__.py +51 -0
  24. sscli-2.5.1/foundry/safety/dry_run_engine.py +484 -0
  25. sscli-2.5.1/foundry/safety/git_guard.py +267 -0
  26. sscli-2.5.1/foundry/safety/integration.py +183 -0
  27. sscli-2.5.1/foundry/safety/verification_hook.py +563 -0
  28. sscli-2.5.1/foundry/tools/synvert_adapter.py +363 -0
  29. {sscli-2.2.0 → sscli-2.5.1}/pyproject.toml +6 -3
  30. sscli-2.5.1/setup.py +29 -0
  31. {sscli-2.2.0 → sscli-2.5.1}/sscli.egg-info/PKG-INFO +13 -23
  32. {sscli-2.2.0 → sscli-2.5.1}/sscli.egg-info/SOURCES.txt +30 -1
  33. {sscli-2.2.0 → sscli-2.5.1}/sscli.egg-info/requires.txt +3 -0
  34. sscli-2.5.1/tests/test_ast_grep_injections.py +301 -0
  35. sscli-2.5.1/tests/test_delta_store.py +447 -0
  36. sscli-2.5.1/tests/test_dry_run_engine.py +532 -0
  37. sscli-2.5.1/tests/test_git_guard.py +470 -0
  38. sscli-2.5.1/tests/test_metadata_tracker.py +660 -0
  39. sscli-2.5.1/tests/test_rails_ast_injection.py +351 -0
  40. sscli-2.5.1/tests/test_upgrade_command.py +587 -0
  41. sscli-2.5.1/tests/test_verification_hook.py +719 -0
  42. {sscli-2.2.0 → sscli-2.5.1}/foundry/__init__.py +0 -0
  43. {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/__init__.py +0 -0
  44. {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/account_info.py +0 -0
  45. {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/documentation.py +0 -0
  46. {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/explore.py +0 -0
  47. {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/generation_utils.py +0 -0
  48. {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/health.py +0 -0
  49. {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/metadata.py +0 -0
  50. {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/repo_utils.py +0 -0
  51. {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/setup.py +0 -0
  52. {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/validation.py +0 -0
  53. {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/verification/__init__.py +0 -0
  54. {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/verification/models.py +0 -0
  55. {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/verification/reports.py +0 -0
  56. {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/verification/verifier.py +0 -0
  57. {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/verify.py +0 -0
  58. {sscli-2.2.0 → sscli-2.5.1}/foundry/admin_tools.py +0 -0
  59. {sscli-2.2.0 → sscli-2.5.1}/foundry/alpha/__init__.py +0 -0
  60. {sscli-2.2.0 → sscli-2.5.1}/foundry/alpha/access_manager.py +0 -0
  61. {sscli-2.2.0 → sscli-2.5.1}/foundry/alpha/manager.py +0 -0
  62. {sscli-2.2.0 → sscli-2.5.1}/foundry/alpha/models.py +0 -0
  63. {sscli-2.2.0 → sscli-2.5.1}/foundry/alpha/view.py +0 -0
  64. {sscli-2.2.0 → sscli-2.5.1}/foundry/alpha_expiration.py +0 -0
  65. {sscli-2.2.0 → sscli-2.5.1}/foundry/animated_experience.py +0 -0
  66. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/__init__.py +0 -0
  67. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/animation.py +0 -0
  68. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/assets.py +0 -0
  69. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/base.py +0 -0
  70. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/canvas.py +0 -0
  71. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/components.py +0 -0
  72. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/composites.py +0 -0
  73. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/core.py +0 -0
  74. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/engine/__init__.py +0 -0
  75. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/engine/animation_engine.py +0 -0
  76. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/engine/clock.py +0 -0
  77. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/engine/frame_buffer.py +0 -0
  78. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/engine/state.py +0 -0
  79. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/rendering.py +0 -0
  80. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/scenes/__init__.py +0 -0
  81. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/scenes/entrance.py +0 -0
  82. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/scenes/forge.py +0 -0
  83. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/scenes/info_slide.py +0 -0
  84. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/scenes/logo_disperse.py +0 -0
  85. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/scenes/logo_dissolve.py +0 -0
  86. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/scenes/logo_reveal.py +0 -0
  87. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/scenes/logo_slide.py +0 -0
  88. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/scenes/rain.py +0 -0
  89. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/scenes/tranquility.py +0 -0
  90. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/sections/__init__.py +0 -0
  91. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/ui/__init__.py +0 -0
  92. {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/utils.py +0 -0
  93. {sscli-2.2.0 → sscli-2.5.1}/foundry/auth.py +0 -0
  94. {sscli-2.2.0 → sscli-2.5.1}/foundry/client_access_manager.py +0 -0
  95. {sscli-2.2.0 → sscli-2.5.1}/foundry/commands/__init__.py +0 -0
  96. {sscli-2.2.0 → sscli-2.5.1}/foundry/commands/auth.py +0 -0
  97. {sscli-2.2.0 → sscli-2.5.1}/foundry/commands/interactive.py +0 -0
  98. {sscli-2.2.0 → sscli-2.5.1}/foundry/constants.py +0 -0
  99. {sscli-2.2.0 → sscli-2.5.1}/foundry/development_experience.py +0 -0
  100. {sscli-2.2.0 → sscli-2.5.1}/foundry/gate.py +0 -0
  101. {sscli-2.2.0 → sscli-2.5.1}/foundry/interactive.py +0 -0
  102. {sscli-2.2.0 → sscli-2.5.1}/foundry/interactive_utils.py +0 -0
  103. {sscli-2.2.0 → sscli-2.5.1}/foundry/internal_experience.py +0 -0
  104. {sscli-2.2.0 → sscli-2.5.1}/foundry/manage.py +0 -0
  105. {sscli-2.2.0 → sscli-2.5.1}/foundry/ops/__init__.py +0 -0
  106. {sscli-2.2.0 → sscli-2.5.1}/foundry/ops/cli.py +0 -0
  107. {sscli-2.2.0 → sscli-2.5.1}/foundry/ops/licenses.py +0 -0
  108. {sscli-2.2.0 → sscli-2.5.1}/foundry/ops/manager.py +0 -0
  109. {sscli-2.2.0 → sscli-2.5.1}/foundry/ops/menus.py +0 -0
  110. {sscli-2.2.0 → sscli-2.5.1}/foundry/ops/reporter.py +0 -0
  111. {sscli-2.2.0 → sscli-2.5.1}/foundry/ops/runner.py +0 -0
  112. {sscli-2.2.0 → sscli-2.5.1}/foundry/patch.py +0 -0
  113. {sscli-2.2.0 → sscli-2.5.1}/foundry/telemetry.py +0 -0
  114. {sscli-2.2.0 → sscli-2.5.1}/foundry/tools/__init__.py +0 -0
  115. {sscli-2.2.0 → sscli-2.5.1}/foundry/tools/client/__init__.py +0 -0
  116. {sscli-2.2.0 → sscli-2.5.1}/foundry/tools/development/__init__.py +0 -0
  117. {sscli-2.2.0 → sscli-2.5.1}/foundry/tools/development/build_tools.py +0 -0
  118. {sscli-2.2.0 → sscli-2.5.1}/foundry/tools/internal/__init__.py +0 -0
  119. {sscli-2.2.0 → sscli-2.5.1}/foundry/utils.py +0 -0
  120. {sscli-2.2.0 → sscli-2.5.1}/setup.cfg +0 -0
  121. {sscli-2.2.0 → sscli-2.5.1}/sscli.egg-info/dependency_links.txt +0 -0
  122. {sscli-2.2.0 → sscli-2.5.1}/sscli.egg-info/entry_points.txt +0 -0
  123. {sscli-2.2.0 → sscli-2.5.1}/sscli.egg-info/top_level.txt +0 -0
@@ -1,7 +1,7 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sscli
3
- Version: 2.2.0
4
- Summary: Seed & Source CLI - Multi-tenant SaaS scaffolding with Seed & Source Core
3
+ Version: 2.5.1
4
+ Summary: Seed & Source CLI - Multi-tenant SaaS scaffolding with Merchant Dashboard
5
5
  Requires-Python: >=3.9
6
6
  Description-Content-Type: text/markdown
7
7
  Requires-Dist: typer[all]>=0.13.0
@@ -12,14 +12,18 @@ Requires-Dist: pathlib
12
12
  Requires-Dist: python-dotenv
13
13
  Requires-Dist: pydantic>=2.0.0
14
14
  Requires-Dist: httpx>=0.27.0
15
+ Requires-Dist: tomlkit>=0.14.0
16
+ Requires-Dist: pyyaml>=6.0
17
+ Requires-Dist: packaging
18
+ Dynamic: requires-python
15
19
 
16
- # Seed & Source CLI v2.2.0
20
+ # Seed & Source CLI v2.3.0
17
21
 
18
22
  Unified interface for Seed & Source templates. Now with **Seed & Source Core** and **The Central Vault**.
19
23
 
20
24
  ## 🎯 What is sscli?
21
25
 
22
- `sscli` scaffolds complete, enterprise-grade SaaS stacks. As part of the **Great Decoupling**, our templates are now "Lean Cores", with high-value proprietary features (Commerce, Admin, Tunnels) managed via a central vault and injected at setup time.
26
+ `sscli` scaffolds complete, enterprise-grade SaaS stacks. As part of the **Great Decoupling**, our templates are now "Lean Cores", with high-value proprietary features (Commerce, Admin, Tunnels, Merchant Dashboard) managed via a central vault and injected at setup time.
23
27
 
24
28
  - ✅ **Lean Cores**: Free, high-performance base templates for Rails, Python, and Astro.
25
29
  - ✅ **The Vault**: Proprietary high-value features (Commerce, Admin, Tunnels) injected on-demand.
@@ -30,14 +34,14 @@ Unified interface for Seed & Source templates. Now with **Seed & Source Core** a
30
34
 
31
35
  ```bash
32
36
  # Via pip
33
- pip install sscli==2.1.0
37
+ pip install sscli==2.3.0
34
38
 
35
39
  # Via pipx (recommended)
36
- pipx install sscli==2.1.0
40
+ pipx install sscli==2.3.0
37
41
 
38
42
  # Verify
39
43
  sscli --version
40
- # Output: sscli 2.1.0
44
+ # Output: sscli 2.3.0
41
45
  ```
42
46
 
43
47
  ## 🚀 Quick Start
@@ -104,6 +108,7 @@ These modules are managed in the central private vault and injected on-demand in
104
108
  | Feature | Tier | Description |
105
109
  |---------|------|-------------|
106
110
  | `commerce` | **PRO** | Shopify/Stripe adapters, webhooks, and commerce models |
111
+ | `merchant-dashboard` | **PRO** | Complete order/analytics UI for merchants (requires commerce) |
107
112
  | `admin` | **PRO** | Standalone NiceGUI-based admin dashboard |
108
113
  | `tunnel` | **PRO** | Automated ngrok config for local webhook testing |
109
114
  | `sqlite` | ALPHA | Local persistence adapter (SQLAlchemy/Alembic) |
@@ -118,10 +123,6 @@ sscli list # List available templates
118
123
  sscli verify # Verify template integrity
119
124
  ```
120
125
 
121
- ## 🔐 Seed & Source Core (NEW in v1.1.0)
122
-
123
- The `rails-api` template now uses **Seed & Source Core** — a Hexagonal Architecture pattern with:
124
-
125
126
  ### Domain Models
126
127
 
127
128
  ```ruby
@@ -158,13 +159,6 @@ External System (Shopify) → HMAC Validator → ShopifyAdapter →
158
159
  Provisioning::IssueResource → CommercialAgreement + SecurityToken
159
160
  ```
160
161
 
161
- ## 📖 Documentation
162
-
163
- - **Installation**: See this README
164
- - **Rails API Setup**: `rails-api/README.md`
165
- - **v1.1.0 Changelog**: `stack-cli/CHANGELOG_v1.1.0.md`
166
- - **PactaPay Case Study**: `PACTAPAY_IMPLEMENTATION.md`
167
-
168
162
  ## 🎯 Use Cases
169
163
 
170
164
  ### Use Case: SaaS with Shopify Integration
@@ -279,7 +273,7 @@ sscli --version
279
273
 
280
274
  # If not found, reinstall
281
275
  pipx uninstall sscli
282
- pipx install sscli==1.1.0
276
+ pipx install sscli
283
277
  ```
284
278
 
285
279
  ### Template not creating correctly
@@ -301,7 +295,3 @@ sscli new --template rails-api --name test --verbose
301
295
  ## 📄 License
302
296
 
303
297
  MIT
304
-
305
- ---
306
-
307
- **v1.1.0 - Now with Seed & Source Core** 🚀
@@ -1,10 +1,10 @@
1
- # Seed & Source CLI v2.2.0
1
+ # Seed & Source CLI v2.3.0
2
2
 
3
3
  Unified interface for Seed & Source templates. Now with **Seed & Source Core** and **The Central Vault**.
4
4
 
5
5
  ## 🎯 What is sscli?
6
6
 
7
- `sscli` scaffolds complete, enterprise-grade SaaS stacks. As part of the **Great Decoupling**, our templates are now "Lean Cores", with high-value proprietary features (Commerce, Admin, Tunnels) managed via a central vault and injected at setup time.
7
+ `sscli` scaffolds complete, enterprise-grade SaaS stacks. As part of the **Great Decoupling**, our templates are now "Lean Cores", with high-value proprietary features (Commerce, Admin, Tunnels, Merchant Dashboard) managed via a central vault and injected at setup time.
8
8
 
9
9
  - ✅ **Lean Cores**: Free, high-performance base templates for Rails, Python, and Astro.
10
10
  - ✅ **The Vault**: Proprietary high-value features (Commerce, Admin, Tunnels) injected on-demand.
@@ -15,14 +15,14 @@ Unified interface for Seed & Source templates. Now with **Seed & Source Core** a
15
15
 
16
16
  ```bash
17
17
  # Via pip
18
- pip install sscli==2.1.0
18
+ pip install sscli==2.3.0
19
19
 
20
20
  # Via pipx (recommended)
21
- pipx install sscli==2.1.0
21
+ pipx install sscli==2.3.0
22
22
 
23
23
  # Verify
24
24
  sscli --version
25
- # Output: sscli 2.1.0
25
+ # Output: sscli 2.3.0
26
26
  ```
27
27
 
28
28
  ## 🚀 Quick Start
@@ -89,6 +89,7 @@ These modules are managed in the central private vault and injected on-demand in
89
89
  | Feature | Tier | Description |
90
90
  |---------|------|-------------|
91
91
  | `commerce` | **PRO** | Shopify/Stripe adapters, webhooks, and commerce models |
92
+ | `merchant-dashboard` | **PRO** | Complete order/analytics UI for merchants (requires commerce) |
92
93
  | `admin` | **PRO** | Standalone NiceGUI-based admin dashboard |
93
94
  | `tunnel` | **PRO** | Automated ngrok config for local webhook testing |
94
95
  | `sqlite` | ALPHA | Local persistence adapter (SQLAlchemy/Alembic) |
@@ -103,10 +104,6 @@ sscli list # List available templates
103
104
  sscli verify # Verify template integrity
104
105
  ```
105
106
 
106
- ## 🔐 Seed & Source Core (NEW in v1.1.0)
107
-
108
- The `rails-api` template now uses **Seed & Source Core** — a Hexagonal Architecture pattern with:
109
-
110
107
  ### Domain Models
111
108
 
112
109
  ```ruby
@@ -143,13 +140,6 @@ External System (Shopify) → HMAC Validator → ShopifyAdapter →
143
140
  Provisioning::IssueResource → CommercialAgreement + SecurityToken
144
141
  ```
145
142
 
146
- ## 📖 Documentation
147
-
148
- - **Installation**: See this README
149
- - **Rails API Setup**: `rails-api/README.md`
150
- - **v1.1.0 Changelog**: `stack-cli/CHANGELOG_v1.1.0.md`
151
- - **PactaPay Case Study**: `PACTAPAY_IMPLEMENTATION.md`
152
-
153
143
  ## 🎯 Use Cases
154
144
 
155
145
  ### Use Case: SaaS with Shopify Integration
@@ -264,7 +254,7 @@ sscli --version
264
254
 
265
255
  # If not found, reinstall
266
256
  pipx uninstall sscli
267
- pipx install sscli==1.1.0
257
+ pipx install sscli
268
258
  ```
269
259
 
270
260
  ### Template not creating correctly
@@ -286,7 +276,3 @@ sscli new --template rails-api --name test --verbose
286
276
  ## 📄 License
287
277
 
288
278
  MIT
289
-
290
- ---
291
-
292
- **v1.1.0 - Now with Seed & Source Core** 🚀
@@ -0,0 +1,374 @@
1
+ """
2
+ AST-Grep Integration Wrapper for Stack CLI
3
+ ===========================================
4
+
5
+ Provides a high-level Python interface for executing ast-grep rules
6
+ in the context of feature injection for Astro landing templates.
7
+
8
+ Usage:
9
+ from foundry.actions.ast_grep_runner import AstGrepRunner
10
+
11
+ runner = AstGrepRunner()
12
+ runner.inject_rule(
13
+ target_path=Path("src/pages/index.astro"),
14
+ rule_name="astro-inject-pricing-component",
15
+ content_vars={"PRICING_COMPONENT": "<PricingTiers />"}
16
+ )
17
+ """
18
+
19
+ import subprocess
20
+ import json
21
+ from pathlib import Path
22
+ from typing import Dict, Optional, List, Tuple
23
+ import shutil
24
+
25
+ from foundry.constants import console, FOUNDRY_ROOT
26
+
27
+
28
+ class AstGrepError(Exception):
29
+ """Raised when ast-grep execution fails."""
30
+ pass
31
+
32
+
33
+ class AstGrepRunner:
34
+ """
35
+ Wrapper around ast-grep CLI for code transformation.
36
+
37
+ Handles:
38
+ - Rule file discovery and validation
39
+ - Variable substitution for dynamic content injection
40
+ - Execution with appropriate flags
41
+ - Error handling and fallback mechanisms
42
+ - Verification of transformations
43
+ """
44
+
45
+ def __init__(self, rule_dir: Optional[Path] = None):
46
+ """
47
+ Initialize AST-Grep runner.
48
+
49
+ Args:
50
+ rule_dir: Path to directory containing YAML rules.
51
+ Defaults to foundry/rules/astro-injections/
52
+ """
53
+ self.rule_dir = rule_dir or (
54
+ FOUNDRY_ROOT / "tooling" / "stack-cli" / "foundry" / "rules" / "astro-injections"
55
+ )
56
+ self.sg_config = self.rule_dir.parent / "sgconfig.yml"
57
+ self._ast_grep_path = self._locate_ast_grep()
58
+
59
+ def _locate_ast_grep(self) -> Optional[str]:
60
+ """
61
+ Find ast-grep binary in system PATH.
62
+
63
+ Returns:
64
+ Path to ast-grep executable, or None if not found.
65
+ """
66
+ return shutil.which("ast-grep") or shutil.which("sg")
67
+
68
+ def is_available(self) -> bool:
69
+ """
70
+ Check if ast-grep is installed and available.
71
+
72
+ Returns:
73
+ True if ast-grep can be executed.
74
+ """
75
+ return self._ast_grep_path is not None
76
+
77
+ def get_version(self) -> Optional[str]:
78
+ """
79
+ Get installed ast-grep version.
80
+
81
+ Returns:
82
+ Version string, or None if ast-grep not found.
83
+ """
84
+ if not self.is_available():
85
+ return None
86
+
87
+ try:
88
+ result = subprocess.run(
89
+ [self._ast_grep_path, "--version"],
90
+ capture_output=True,
91
+ text=True,
92
+ timeout=5
93
+ )
94
+ return result.stdout.strip() if result.returncode == 0 else None
95
+ except Exception:
96
+ return None
97
+
98
+ def validate_rule(self, rule_name: str) -> Tuple[bool, Optional[str]]:
99
+ """
100
+ Validate that a rule file exists and contains valid YAML.
101
+
102
+ Args:
103
+ rule_name: ID of the rule (e.g., "astro-inject-pricing-component")
104
+
105
+ Returns:
106
+ Tuple of (valid: bool, error_message: Optional[str])
107
+ """
108
+ rule_path = self.rule_dir / f"{rule_name}.yml"
109
+
110
+ if not rule_path.exists():
111
+ return False, f"Rule file not found: {rule_path}"
112
+
113
+ try:
114
+ import yaml
115
+ with open(rule_path) as f:
116
+ content = yaml.safe_load(f)
117
+ if not content or not isinstance(content, dict):
118
+ return False, "Rule is empty or not a valid YAML object"
119
+ return True, None
120
+ except Exception as e:
121
+ return False, f"Invalid YAML in rule: {str(e)}"
122
+
123
+ def inject_rule(
124
+ self,
125
+ target_path: Path,
126
+ rule_name: str,
127
+ content_vars: Optional[Dict[str, str]] = None,
128
+ dry_run: bool = False,
129
+ auto_accept: bool = True
130
+ ) -> bool:
131
+ """
132
+ Execute an ast-grep rule on a target file to inject/transform code.
133
+
134
+ Args:
135
+ target_path: File to transform (e.g., src/pages/index.astro)
136
+ rule_name: ID of the rule to apply
137
+ content_vars: Dictionary mapping variable names to injection content
138
+ e.g., {"PRICING_COMPONENT": "<PricingComponent />"}
139
+ dry_run: If True, show what would happen without modifying file
140
+ auto_accept: If True, auto-apply changes (--update-all flag)
141
+
142
+ Returns:
143
+ True if successful, False otherwise.
144
+
145
+ Raises:
146
+ AstGrepError: If ast-grep is not available or execution fails critically.
147
+ """
148
+ # Validation
149
+ if not self.is_available():
150
+ raise AstGrepError(
151
+ "ast-grep is not installed. "
152
+ "Install via: brew install ast-grep or cargo install ast-grep-cli"
153
+ )
154
+
155
+ if not target_path.exists():
156
+ console.print(f"[red]✗ Target file not found: {target_path}[/red]")
157
+ return False
158
+
159
+ valid, error = self.validate_rule(rule_name)
160
+ if not valid:
161
+ console.print(f"[red]✗ {error}[/red]")
162
+ return False
163
+
164
+ # Prepare target content with variable substitution
165
+ try:
166
+ if content_vars:
167
+ content = target_path.read_text(encoding="utf-8")
168
+ for var_name, var_value in content_vars.items():
169
+ # Replace placeholders like $$$THEME_CONTENT with actual content
170
+ placeholder = f"$$$/{var_name}"
171
+ content = content.replace(placeholder, var_value)
172
+ target_path.write_text(content, encoding="utf-8")
173
+ except Exception as e:
174
+ console.print(f"[red]✗ Failed to prepare target file: {str(e)}[/red]")
175
+ return False
176
+
177
+ # Build ast-grep command
178
+ try:
179
+ cmd = [
180
+ self._ast_grep_path,
181
+ "scan",
182
+ "--rule", str(self.rule_dir / f"{rule_name}.yml"),
183
+ ]
184
+
185
+ if auto_accept and not dry_run:
186
+ cmd.append("--update-all")
187
+ elif dry_run:
188
+ # Dry run shows diff without applying
189
+ pass # ast-grep shows diff by default
190
+ else:
191
+ cmd.append("--interactive")
192
+
193
+ cmd.append(str(target_path.parent))
194
+
195
+ if dry_run:
196
+ console.print(f"[yellow]DRY RUN:[/yellow] {' '.join(cmd)}")
197
+
198
+ # Execute rule
199
+ result = subprocess.run(
200
+ cmd,
201
+ capture_output=True,
202
+ text=True,
203
+ timeout=30
204
+ )
205
+
206
+ if result.returncode not in (0, 1): # 1 = matched but didn't auto-apply
207
+ console.print(f"[red]✗ ast-grep error:[/red] {result.stderr}")
208
+ return False
209
+
210
+ if result.stdout:
211
+ console.print(f"[cyan]{result.stdout}[/cyan]")
212
+
213
+ console.print(f"[green]✓ Applied rule: {rule_name}[/green]")
214
+ return True
215
+
216
+ except subprocess.TimeoutExpired:
217
+ console.print(
218
+ f"[red]✗ ast-grep execution timed out for {target_path}[/red]"
219
+ )
220
+ return False
221
+ except Exception as e:
222
+ console.print(f"[red]✗ ast-grep execution failed: {str(e)}[/red]")
223
+ return False
224
+
225
+ def inject_rule_batch(
226
+ self,
227
+ target_files: List[Path],
228
+ rule_name: str,
229
+ content_vars: Optional[Dict[str, str]] = None,
230
+ auto_accept: bool = True
231
+ ) -> Tuple[int, int]:
232
+ """
233
+ Apply a rule to multiple files.
234
+
235
+ Args:
236
+ target_files: List of files to transform
237
+ rule_name: Rule ID to apply
238
+ content_vars: Shared variables for all files
239
+ auto_accept: Auto-apply transformations
240
+
241
+ Returns:
242
+ Tuple of (success_count, failure_count)
243
+ """
244
+ successes = 0
245
+ failures = 0
246
+
247
+ for target in target_files:
248
+ try:
249
+ if self.inject_rule(target, rule_name, content_vars, auto_accept=auto_accept):
250
+ successes += 1
251
+ else:
252
+ failures += 1
253
+ except Exception as e:
254
+ console.print(f"[red]✗ Error processing {target}: {str(e)}[/red]")
255
+ failures += 1
256
+
257
+ return successes, failures
258
+
259
+ def parse_file(self, file_path: Path) -> Optional[Dict]:
260
+ """
261
+ Parse a file and return its AST structure.
262
+
263
+ Useful for understanding the syntax tree for rule development.
264
+
265
+ Args:
266
+ file_path: File to parse
267
+
268
+ Returns:
269
+ AST structure as dict, or None if parsing fails.
270
+ """
271
+ if not self.is_available():
272
+ console.print("[red]✗ ast-grep not available[/red]")
273
+ return None
274
+
275
+ try:
276
+ result = subprocess.run(
277
+ [self._ast_grep_path, "parse", str(file_path), "--format", "json"],
278
+ capture_output=True,
279
+ text=True,
280
+ timeout=10
281
+ )
282
+
283
+ if result.returncode == 0:
284
+ return json.loads(result.stdout)
285
+ else:
286
+ console.print(f"[red]✗ Parse error: {result.stderr}[/red]")
287
+ return None
288
+
289
+ except Exception as e:
290
+ console.print(f"[red]✗ Failed to parse file: {str(e)}[/red]")
291
+ return None
292
+
293
+ def test_rule(self, rule_name: str, target_path: Path) -> bool:
294
+ """
295
+ Test a rule without applying changes (dry run mode).
296
+
297
+ Args:
298
+ rule_name: Rule ID to test
299
+ target_path: File to test against
300
+
301
+ Returns:
302
+ True if rule matches code in target.
303
+ """
304
+ return self.inject_rule(target_path, rule_name, dry_run=True)
305
+
306
+ def get_rule_info(self, rule_name: str) -> Optional[Dict]:
307
+ """
308
+ Extract metadata from a rule file.
309
+
310
+ Args:
311
+ rule_name: Rule ID
312
+
313
+ Returns:
314
+ Dict with rule metadata (id, language, message, severity) or None.
315
+ """
316
+ rule_path = self.rule_dir / f"{rule_name}.yml"
317
+
318
+ if not rule_path.exists():
319
+ return None
320
+
321
+ try:
322
+ import yaml
323
+ with open(rule_path) as f:
324
+ data = yaml.safe_load(f)
325
+ if isinstance(data, dict):
326
+ return {
327
+ "id": data.get("id"),
328
+ "language": data.get("language"),
329
+ "message": data.get("message"),
330
+ "severity": data.get("severity", "info")
331
+ }
332
+ except Exception:
333
+ pass
334
+
335
+ return None
336
+
337
+ def list_available_rules(self) -> List[str]:
338
+ """
339
+ List all available rule files in the rules directory.
340
+
341
+ Returns:
342
+ List of rule IDs (filenames without .yml extension).
343
+ """
344
+ if not self.rule_dir.exists():
345
+ return []
346
+
347
+ rules = []
348
+ for rule_file in self.rule_dir.glob("*.yml"):
349
+ rules.append(rule_file.stem)
350
+
351
+ return sorted(rules)
352
+
353
+
354
+ def ensure_ast_grep_available() -> bool:
355
+ """
356
+ Check if ast-grep is available; offer installation instructions if not.
357
+
358
+ Returns:
359
+ True if ast-grep is installed and available.
360
+ """
361
+ runner = AstGrepRunner()
362
+
363
+ if runner.is_available():
364
+ version = runner.get_version()
365
+ console.print(f"[green]✓ ast-grep available[/green] ({version})")
366
+ return True
367
+
368
+ console.print("[yellow]⚠ ast-grep is not installed[/yellow]")
369
+ console.print("[cyan]Installation options:[/cyan]")
370
+ console.print(" macOS: [bold]brew install ast-grep[/bold]")
371
+ console.print(" Rust: [bold]cargo install ast-grep-cli[/bold]")
372
+ console.print(" GitHub: https://github.com/ast-grep/ast-grep/releases")
373
+
374
+ return False