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.
- {sscli-2.2.0 → sscli-2.5.1}/PKG-INFO +13 -23
- {sscli-2.2.0 → sscli-2.5.1}/README.md +7 -21
- sscli-2.5.1/foundry/actions/ast_grep_runner.py +374 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/development.py +71 -44
- sscli-2.5.1/foundry/actions/examples/__init__.py +8 -0
- sscli-2.5.1/foundry/actions/examples/codemod_examples.py +204 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/features.py +338 -45
- {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/generator.py +47 -2
- {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/interactive_config.py +10 -1
- sscli-2.5.1/foundry/actions/transformation.py +207 -0
- sscli-2.5.1/foundry/actions/upgrade.py +709 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/cli.py +57 -66
- sscli-2.5.1/foundry/codemods/__init__.py +48 -0
- sscli-2.5.1/foundry/codemods/base.py +241 -0
- sscli-2.5.1/foundry/codemods/delta_store.py +499 -0
- sscli-2.5.1/foundry/codemods/toml_codemods.py +273 -0
- sscli-2.5.1/foundry/commands/dev.py +83 -0
- sscli-2.5.1/foundry/features/features.json +36 -0
- sscli-2.5.1/foundry/features/react-client/merchant-dashboard/DEPENDENCIES.md +47 -0
- sscli-2.5.1/foundry/metadata/__init__.py +20 -0
- sscli-2.5.1/foundry/metadata/metadata_tracker.py +511 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/ops/version_manager.py +101 -31
- sscli-2.5.1/foundry/safety/__init__.py +51 -0
- sscli-2.5.1/foundry/safety/dry_run_engine.py +484 -0
- sscli-2.5.1/foundry/safety/git_guard.py +267 -0
- sscli-2.5.1/foundry/safety/integration.py +183 -0
- sscli-2.5.1/foundry/safety/verification_hook.py +563 -0
- sscli-2.5.1/foundry/tools/synvert_adapter.py +363 -0
- {sscli-2.2.0 → sscli-2.5.1}/pyproject.toml +6 -3
- sscli-2.5.1/setup.py +29 -0
- {sscli-2.2.0 → sscli-2.5.1}/sscli.egg-info/PKG-INFO +13 -23
- {sscli-2.2.0 → sscli-2.5.1}/sscli.egg-info/SOURCES.txt +30 -1
- {sscli-2.2.0 → sscli-2.5.1}/sscli.egg-info/requires.txt +3 -0
- sscli-2.5.1/tests/test_ast_grep_injections.py +301 -0
- sscli-2.5.1/tests/test_delta_store.py +447 -0
- sscli-2.5.1/tests/test_dry_run_engine.py +532 -0
- sscli-2.5.1/tests/test_git_guard.py +470 -0
- sscli-2.5.1/tests/test_metadata_tracker.py +660 -0
- sscli-2.5.1/tests/test_rails_ast_injection.py +351 -0
- sscli-2.5.1/tests/test_upgrade_command.py +587 -0
- sscli-2.5.1/tests/test_verification_hook.py +719 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/__init__.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/__init__.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/account_info.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/documentation.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/explore.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/generation_utils.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/health.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/metadata.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/repo_utils.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/setup.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/validation.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/verification/__init__.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/verification/models.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/verification/reports.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/verification/verifier.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/actions/verify.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/admin_tools.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/alpha/__init__.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/alpha/access_manager.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/alpha/manager.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/alpha/models.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/alpha/view.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/alpha_expiration.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animated_experience.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/__init__.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/animation.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/assets.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/base.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/canvas.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/components.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/composites.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/core.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/engine/__init__.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/engine/animation_engine.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/engine/clock.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/engine/frame_buffer.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/engine/state.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/rendering.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/scenes/__init__.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/scenes/entrance.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/scenes/forge.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/scenes/info_slide.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/scenes/logo_disperse.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/scenes/logo_dissolve.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/scenes/logo_reveal.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/scenes/logo_slide.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/scenes/rain.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/scenes/tranquility.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/sections/__init__.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/ui/__init__.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/animations/utils.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/auth.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/client_access_manager.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/commands/__init__.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/commands/auth.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/commands/interactive.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/constants.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/development_experience.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/gate.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/interactive.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/interactive_utils.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/internal_experience.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/manage.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/ops/__init__.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/ops/cli.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/ops/licenses.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/ops/manager.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/ops/menus.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/ops/reporter.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/ops/runner.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/patch.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/telemetry.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/tools/__init__.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/tools/client/__init__.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/tools/development/__init__.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/tools/development/build_tools.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/tools/internal/__init__.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/foundry/utils.py +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/setup.cfg +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/sscli.egg-info/dependency_links.txt +0 -0
- {sscli-2.2.0 → sscli-2.5.1}/sscli.egg-info/entry_points.txt +0 -0
- {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.
|
|
4
|
-
Summary: Seed & Source CLI - Multi-tenant SaaS scaffolding with
|
|
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.
|
|
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.
|
|
37
|
+
pip install sscli==2.3.0
|
|
34
38
|
|
|
35
39
|
# Via pipx (recommended)
|
|
36
|
-
pipx install sscli==2.
|
|
40
|
+
pipx install sscli==2.3.0
|
|
37
41
|
|
|
38
42
|
# Verify
|
|
39
43
|
sscli --version
|
|
40
|
-
# Output: sscli 2.
|
|
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
|
|
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.
|
|
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.
|
|
18
|
+
pip install sscli==2.3.0
|
|
19
19
|
|
|
20
20
|
# Via pipx (recommended)
|
|
21
|
-
pipx install sscli==2.
|
|
21
|
+
pipx install sscli==2.3.0
|
|
22
22
|
|
|
23
23
|
# Verify
|
|
24
24
|
sscli --version
|
|
25
|
-
# Output: sscli 2.
|
|
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
|
|
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
|