storm-cli 1.0.0__py3-none-any.whl

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.
main.py ADDED
@@ -0,0 +1,753 @@
1
+ import argparse
2
+ import json
3
+ import os
4
+ import re
5
+ import shutil
6
+ import socket
7
+ import subprocess
8
+ import sys
9
+
10
+ from src.template import Template
11
+ from src.interpreter import Interpreter
12
+
13
+
14
+ def check_network():
15
+ try:
16
+ socket.create_connection(("8.8.8.8", 53), timeout=3)
17
+ except OSError:
18
+ print("ERROR: No internet connection. This script requires network access.")
19
+ sys.exit(1)
20
+ print(" [ok] internet connection")
21
+
22
+
23
+ def check_dotnet_prerequisites():
24
+ missing = []
25
+
26
+ if shutil.which("dotnet") is None:
27
+ missing.append("dotnet-sdk")
28
+ else:
29
+ version_result = subprocess.run(["dotnet", "--version"], capture_output=True, text=True)
30
+ if version_result.returncode == 0:
31
+ version = version_result.stdout.strip()
32
+ parts = version.split(".")
33
+ major = int(parts[0])
34
+ if major < 8:
35
+ missing.append(f"dotnet-sdk 8+ (found {version})")
36
+ else:
37
+ missing.append("dotnet-sdk (unable to determine version)")
38
+
39
+ result = subprocess.run(["dotnet", "ef", "--version"], capture_output=True, text=True)
40
+ if result.returncode != 0:
41
+ missing.append("ef-core (dotnet-ef global tool)")
42
+
43
+ if missing:
44
+ print("ERROR: Missing prerequisites:")
45
+ for item in missing:
46
+ print(f" - {item}")
47
+ print("\nInstall the missing dependencies and try again.")
48
+ sys.exit(1)
49
+
50
+ print(f" [ok] dotnet-sdk ({version})")
51
+ print(" [ok] ef-core")
52
+
53
+
54
+ def check_laravel_prerequisites():
55
+ missing = []
56
+
57
+ php_path = shutil.which("php") or shutil.which("php-win")
58
+ if php_path is None:
59
+ missing.append("php / php-win")
60
+ else:
61
+ result = subprocess.run([php_path, "-v"], capture_output=True, text=True)
62
+ if result.returncode == 0:
63
+ first_line = result.stdout.splitlines()[0]
64
+ match = re.search(r'PHP\s+(\d+)\.(\d+)', first_line)
65
+ if match:
66
+ major = int(match.group(1))
67
+ minor = int(match.group(2))
68
+ if major < 8 or (major == 8 and minor < 5):
69
+ missing.append("php 8.5+ (found php " + first_line.split()[1] if len(first_line.split()) > 1 else "older version)")
70
+ else:
71
+ missing.append("php (unable to determine version)")
72
+ else:
73
+ missing.append("php (not working)")
74
+
75
+ if resolve_composer() is None:
76
+ missing.append("composer")
77
+
78
+ if missing:
79
+ print("ERROR: Missing prerequisites:")
80
+ for item in missing:
81
+ print(f" - {item}")
82
+ print("\nInstall the missing dependencies and try again.")
83
+ sys.exit(1)
84
+
85
+ print(" [ok] php 8.5+")
86
+ print(" [ok] composer")
87
+
88
+
89
+ SCHEMA_STORM = """\
90
+ // ============================================================================
91
+ // Schema Definition — define your data model here
92
+ // Run `python main.py --generate` after editing to rebuild all source files.
93
+ // ============================================================================
94
+
95
+ // ── Enums ──────────────────────────────────────────────────────────────────
96
+
97
+ enum Status {
98
+ Active = "active",
99
+ Inactive = "inactive",
100
+ Pending = "pending"
101
+ }
102
+
103
+ enum Role {
104
+ Admin = "admin",
105
+ User = "user"
106
+ }
107
+
108
+ // ── Tables ─────────────────────────────────────────────────────────────────
109
+
110
+ table User {
111
+ id: uuid pk;
112
+ name: string(min=2,max=100);
113
+ email: string(max=255) unique;
114
+ password: string(max=255);
115
+ role: Role;
116
+ status: Status;
117
+ createdAt:datetime;
118
+ updatedAt:datetime;
119
+ }
120
+
121
+ table Product {
122
+ id: int? pk;
123
+ name: string(min=1,max=200);
124
+ price: double(min=0) = 0;
125
+ stock: int(min=0) = 0;
126
+ status: Status;
127
+ ownerId: uuid;
128
+ owner: User;
129
+ createdAt:datetime;
130
+ updatedAt:datetime;
131
+ }
132
+ """
133
+
134
+
135
+ CONFIGS = {
136
+ Template.DOTNETCSHARP.value: {
137
+ "Template": Template.DOTNETCSHARP.value,
138
+ "EnumPath": "./Static",
139
+ "ModelPath": "./Models",
140
+ "ControllerPath": "./Controllers",
141
+ "DtoPath": "./Dtos",
142
+ "IServicePath": "./IServices",
143
+ "ServicePath": "./Services",
144
+ "MapperPath": "./Mappers",
145
+ "DbContextPath": "./Data",
146
+ },
147
+ Template.DOTNETCSHARP_CLEANARCHITECTURE.value: {
148
+ "Template": Template.DOTNETCSHARP_CLEANARCHITECTURE.value,
149
+ "EnumPath": "./DOMAIN/Static",
150
+ "ModelPath": "./DOMAIN/Models",
151
+ "ControllerPath": "./API/Controllers",
152
+ "DtoPath": "./APPLICATION/Dtos",
153
+ "IServicesPath": "./APPLICATION/IServices",
154
+ "ServicePath": "./INFRASTRUCTURE/Services",
155
+ "MapperPath": "./INFRASTRUCTURE/Mappers",
156
+ "DbContextPath": "./INFRASTRUCTURE/Data",
157
+ },
158
+ Template.LARAVELPHP.value: {
159
+ "Template": Template.LARAVELPHP.value,
160
+ "EnumPath": "./app/Static",
161
+ "ModelPath": "./app/Models",
162
+ "ControllerPath": "./app/Controllers",
163
+ "DtoPath": "./app/Dtos",
164
+ "IServicePath": "./app/IServices",
165
+ "ServicePath": "./app/Services",
166
+ "MapperPath": "./app/Mappers",
167
+ "MigrationsPath": "./database/migrations",
168
+ },
169
+ }
170
+
171
+
172
+ def write_config(config, target_dir):
173
+ os.makedirs(target_dir, exist_ok=True)
174
+ config_path = os.path.join(target_dir, "storm.config.json")
175
+ with open(config_path, "w") as f:
176
+ json.dump(config, f, indent=4)
177
+ print(f" [ok] storm.config.json written")
178
+
179
+
180
+ def create_config_dirs(config, base_dir):
181
+ for path in config.values():
182
+ dir_path = os.path.join(base_dir, path.lstrip("./"))
183
+ os.makedirs(dir_path, exist_ok=True)
184
+ print(" [ok] config directories created")
185
+
186
+
187
+ def run_dotnet(args, cwd=None):
188
+ print(f" [>] dotnet {' '.join(args)}")
189
+ result = subprocess.run(["dotnet"] + args, cwd=cwd, capture_output=True, text=True)
190
+ if result.returncode != 0:
191
+ print(f"ERROR: dotnet command failed:")
192
+ print(result.stderr)
193
+ sys.exit(1)
194
+
195
+
196
+ def resolve_composer():
197
+ """Find the composer executable path."""
198
+ for name in ["composer", "composer.bat", "composer.phar"]:
199
+ path = shutil.which(name)
200
+ if path:
201
+ return path
202
+ return None
203
+
204
+
205
+ def run_composer(args, cwd=None, ignore_platform_reqs=None):
206
+ composer_exe = resolve_composer()
207
+ if composer_exe is None:
208
+ print("ERROR: composer not found. Install Composer and try again.")
209
+ sys.exit(1)
210
+ cmd = [composer_exe] + args
211
+ if ignore_platform_reqs:
212
+ for req in ignore_platform_reqs:
213
+ cmd.append(f"--ignore-platform-req={req}")
214
+ print(f" [>] composer {' '.join(args)}")
215
+ result = subprocess.run(cmd, cwd=cwd, capture_output=True, text=True)
216
+ if result.returncode != 0:
217
+ print(f"ERROR: composer command failed:")
218
+ print(result.stderr)
219
+ sys.exit(1)
220
+
221
+
222
+ def get_dotnet_version():
223
+ result = subprocess.run(["dotnet", "--version"], capture_output=True, text=True)
224
+ if result.returncode != 0:
225
+ print("ERROR: Could not determine dotnet SDK version")
226
+ sys.exit(1)
227
+ version = result.stdout.strip()
228
+ parts = version.split(".")
229
+ return f"{parts[0]}.{parts[1]}"
230
+
231
+
232
+ def add_nuget_package(project_csproj, package_name, version=None, cwd=None):
233
+ args = ["add", project_csproj, "package", package_name]
234
+ if version:
235
+ args.extend(["--version", version])
236
+ result = subprocess.run(["dotnet"] + args, cwd=cwd, capture_output=True, text=True)
237
+ if result.returncode != 0:
238
+ print(f" [!!] {package_name} skipped (already present or not found)")
239
+ else:
240
+ print(f" [ok] {package_name}")
241
+
242
+
243
+ NUGET_SIMPLE = [
244
+ ("Microsoft.EntityFrameworkCore", True),
245
+ ("Microsoft.EntityFrameworkCore.SqlServer", True),
246
+ ("Microsoft.EntityFrameworkCore.Design", True),
247
+ ("Microsoft.AspNetCore.Identity.EntityFrameworkCore", True),
248
+ ("AutoMapper", False),
249
+ ("Newtonsoft.Json", False),
250
+ ("Scrutor", False),
251
+ ("Swashbuckle.AspNetCore", False),
252
+ ("Scalar.AspNetCore", False),
253
+ ]
254
+
255
+ NUGET_CLEAN_ARCH = {
256
+ "DOMAIN": [
257
+ ("Microsoft.AspNetCore.Identity.EntityFrameworkCore", True),
258
+ ],
259
+ "APPLICATION": [
260
+ ("AutoMapper", False),
261
+ ],
262
+ "INFRASTRUCTURE": [
263
+ ("Microsoft.EntityFrameworkCore", True),
264
+ ("Microsoft.EntityFrameworkCore.SqlServer", True),
265
+ ("Microsoft.AspNetCore.Identity.EntityFrameworkCore", True),
266
+ ],
267
+ "API": [
268
+ ("Microsoft.EntityFrameworkCore.Design", True),
269
+ ("Newtonsoft.Json", False),
270
+ ("Scrutor", False),
271
+ ("Swashbuckle.AspNetCore", False),
272
+ ("Scalar.AspNetCore", False),
273
+ ],
274
+ }
275
+
276
+
277
+ def install_nuget_packages(project_csproj, packages, sdk_version, cwd=None):
278
+ for package_name, is_sdk_versioned in packages:
279
+ version = sdk_version if is_sdk_versioned else None
280
+ add_nuget_package(project_csproj, package_name, version=version, cwd=cwd)
281
+
282
+
283
+ def init_dotnet_project(config, project_dir, name):
284
+ print("\nCreating dotnet webapi project...")
285
+ run_dotnet(["new", "webapi", "-n", name, "-o", project_dir, "--force"])
286
+ create_config_dirs(config, project_dir)
287
+
288
+ sdk_version = get_dotnet_version()
289
+ csproj = f"{name}.csproj"
290
+ print(f"\nInstalling NuGet packages (SDK {sdk_version})...")
291
+ install_nuget_packages(csproj, NUGET_SIMPLE, sdk_version, cwd=project_dir)
292
+
293
+ write_config(config, project_dir)
294
+ write_program_cs(PROGRAM_CS_SIMPLE, project_dir, name)
295
+
296
+ # Update appsettings.json with ConnectionStrings
297
+ appsettings = os.path.join(project_dir, "appsettings.json")
298
+ if os.path.exists(appsettings):
299
+ with open(appsettings) as f:
300
+ cfg = json.load(f)
301
+ cfg.setdefault("ConnectionStrings", {})["DefaultConnection"] = \
302
+ "Server=(localdb)\\mssqllocaldb;Database=" + name + ";Trusted_Connection=True;MultipleActiveResultSets=true"
303
+ with open(appsettings, "w") as f:
304
+ json.dump(cfg, f, indent=2)
305
+ print(" [ok] appsettings.json patched")
306
+
307
+
308
+ def init_dotnet_clean_arch_project(config, solution_dir, name):
309
+ print("\nCreating clean architecture solution...")
310
+
311
+ layers = [
312
+ ("DOMAIN", "classlib"),
313
+ ("APPLICATION", "classlib"),
314
+ ("INFRASTRUCTURE", "classlib"),
315
+ ("API", "webapi"),
316
+ ]
317
+
318
+ for layer, project_type in layers:
319
+ run_dotnet(["new", project_type, "-n", layer, "-o", layer, "--force"], cwd=solution_dir)
320
+
321
+ run_dotnet(["new", "sln", "-n", name, "--force"], cwd=solution_dir)
322
+
323
+ sln_name = f"{name}.sln"
324
+ for layer, _ in layers:
325
+ run_dotnet(["sln", sln_name, "add", f"{layer}/{layer}.csproj"], cwd=solution_dir)
326
+
327
+ run_dotnet(["add", "APPLICATION/APPLICATION.csproj", "reference", "DOMAIN/DOMAIN.csproj"], cwd=solution_dir)
328
+ run_dotnet(["add", "INFRASTRUCTURE/INFRASTRUCTURE.csproj", "reference", "APPLICATION/APPLICATION.csproj"], cwd=solution_dir)
329
+ run_dotnet(["add", "API/API.csproj", "reference", "APPLICATION/APPLICATION.csproj"], cwd=solution_dir)
330
+ run_dotnet(["add", "API/API.csproj", "reference", "INFRASTRUCTURE/INFRASTRUCTURE.csproj"], cwd=solution_dir)
331
+
332
+ sdk_version = get_dotnet_version()
333
+ print(f"\nInstalling NuGet packages (SDK {sdk_version})...")
334
+ for layer, packages in NUGET_CLEAN_ARCH.items():
335
+ csproj = f"{layer}/{layer}.csproj"
336
+ install_nuget_packages(csproj, packages, sdk_version, cwd=solution_dir)
337
+
338
+ create_config_dirs(config, solution_dir)
339
+ write_config(config, solution_dir)
340
+ write_program_cs(PROGRAM_CS_CLEAN_ARCH, os.path.join(solution_dir, "API"), name)
341
+
342
+
343
+ PROGRAM_CS_SIMPLE = """\
344
+ using System.Reflection;
345
+ using Microsoft.AspNetCore.Identity;
346
+ using Microsoft.EntityFrameworkCore;
347
+ using Scrutor;
348
+ using Scalar.AspNetCore;
349
+ using $$NAMESPACE$$.Data;
350
+ using $$NAMESPACE$$.IServices;
351
+ using $$NAMESPACE$$.Services;
352
+ using $$NAMESPACE$$.Mappers;
353
+ using $$NAMESPACE$$.Models;
354
+
355
+ var builder = WebApplication.CreateBuilder(args);
356
+
357
+ // ── Database ─────────────────────────────────────────────────────────
358
+ builder.Services.AddDbContext<AppDbContext>(options =>
359
+ options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
360
+
361
+ // ── Identity ────────────────────────────────────────────────────────
362
+ builder.Services.AddIdentity<User, IdentityRole<Guid>>()
363
+ .AddEntityFrameworkStores<AppDbContext>()
364
+ .AddTokenProvider<DataProtectorTokenProvider<User>>(TokenOptions.DefaultProvider)
365
+ .AddDefaultTokenProviders();
366
+
367
+ builder.Services.ConfigureApplicationCookie(options =>
368
+ {
369
+ options.Cookie.Name = "authToken";
370
+ options.Cookie.HttpOnly = true;
371
+ options.Cookie.IsEssential = true;
372
+ options.Cookie.MaxAge = TimeSpan.FromDays(7);
373
+ options.Cookie.Path = "/";
374
+ options.Cookie.SameSite = SameSiteMode.None;
375
+ var isProduction = string.Equals(
376
+ Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development",
377
+ "Production", StringComparison.OrdinalIgnoreCase);
378
+ options.Cookie.SecurePolicy = isProduction
379
+ ? CookieSecurePolicy.Always
380
+ : CookieSecurePolicy.SameAsRequest;
381
+ });
382
+
383
+ builder.Services.Configure<IdentityOptions>(options =>
384
+ {
385
+ options.Password.RequireDigit = false;
386
+ options.Password.RequireLowercase = false;
387
+ options.Password.RequireNonAlphanumeric = false;
388
+ options.Password.RequireUppercase = false;
389
+ options.Password.RequiredLength = 3;
390
+ options.Password.RequiredUniqueChars = 0;
391
+ options.User.RequireUniqueEmail = true;
392
+ });
393
+
394
+ // ── AutoMapper ──────────────────────────────────────────────────────
395
+ builder.Services.AddAutoMapper(cfg =>
396
+ {
397
+ cfg.AddMaps(Assembly.GetExecutingAssembly());
398
+ });
399
+
400
+ // ── Scrutor — register all I*Service → *Service ────────────────────
401
+ builder.Services.Scan(scan => scan
402
+ .FromAssemblyOf<UserService>()
403
+ .AddClasses(classes => classes
404
+ .InNamespaces("$$NAMESPACE$$.Services")
405
+ .AssignableTo(typeof(GenericService<,,,>)))
406
+ .AsImplementedInterfaces()
407
+ .WithScopedLifetime());
408
+
409
+ // ── Controllers ─────────────────────────────────────────────────────
410
+ builder.Services.AddControllers();
411
+
412
+ // ── OpenAPI / Scalar ────────────────────────────────────────────────
413
+ builder.Services.AddOpenApi();
414
+
415
+ var app = builder.Build();
416
+
417
+ // ── Swagger / Scalar ────────────────────────────────────────────────
418
+ app.UseSwagger(options =>
419
+ {
420
+ options.RouteTemplate = "openapi/{documentName}.json";
421
+ });
422
+ app.MapScalarApiReference(options =>
423
+ {
424
+ options.WithTitle("$$NAMESPACE$$");
425
+ options.WithTheme(ScalarTheme.BluePlanet);
426
+ options.WithDefaultHttpClient(ScalarTarget.JavaScript, ScalarClient.Axios);
427
+ options.WithPreferredScheme("Bearer");
428
+ });
429
+
430
+ app.UseHttpsRedirection();
431
+ app.UseAuthentication();
432
+ app.UseAuthorization();
433
+ app.MapControllers();
434
+ app.Run();
435
+ """
436
+
437
+ PROGRAM_CS_CLEAN_ARCH = """\
438
+ using System.Reflection;
439
+ using Microsoft.AspNetCore.Identity;
440
+ using Microsoft.EntityFrameworkCore;
441
+ using Scrutor;
442
+ using Scalar.AspNetCore;
443
+ using $$NAMESPACE$$.DOMAIN.Models;
444
+ using $$NAMESPACE$$.APPLICATION.IServices;
445
+ using $$NAMESPACE$$.APPLICATION.Dtos.Shared;
446
+ using $$NAMESPACE$$.INFRASTRUCTURE.Data;
447
+ using $$NAMESPACE$$.INFRASTRUCTURE.Services;
448
+ using $$NAMESPACE$$.INFRASTRUCTURE.Mappers;
449
+
450
+ var builder = WebApplication.CreateBuilder(args);
451
+
452
+ // ── Database ─────────────────────────────────────────────────────────
453
+ builder.Services.AddDbContext<AppDbContext>(options =>
454
+ options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));
455
+
456
+ // ── Identity ────────────────────────────────────────────────────────
457
+ builder.Services.AddIdentity<User, IdentityRole<Guid>>()
458
+ .AddEntityFrameworkStores<AppDbContext>()
459
+ .AddTokenProvider<DataProtectorTokenProvider<User>>(TokenOptions.DefaultProvider)
460
+ .AddDefaultTokenProviders();
461
+
462
+ builder.Services.ConfigureApplicationCookie(options =>
463
+ {
464
+ options.Cookie.Name = "authToken";
465
+ options.Cookie.HttpOnly = true;
466
+ options.Cookie.IsEssential = true;
467
+ options.Cookie.MaxAge = TimeSpan.FromDays(7);
468
+ options.Cookie.Path = "/";
469
+ options.Cookie.SameSite = SameSiteMode.None;
470
+ var isProduction = string.Equals(
471
+ Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT") ?? "Development",
472
+ "Production", StringComparison.OrdinalIgnoreCase);
473
+ options.Cookie.SecurePolicy = isProduction
474
+ ? CookieSecurePolicy.Always
475
+ : CookieSecurePolicy.SameAsRequest;
476
+ });
477
+
478
+ builder.Services.Configure<IdentityOptions>(options =>
479
+ {
480
+ options.Password.RequireDigit = false;
481
+ options.Password.RequireLowercase = false;
482
+ options.Password.RequireNonAlphanumeric = false;
483
+ options.Password.RequireUppercase = false;
484
+ options.Password.RequiredLength = 3;
485
+ options.Password.RequiredUniqueChars = 0;
486
+ options.User.RequireUniqueEmail = true;
487
+ });
488
+
489
+ // ── AutoMapper ──────────────────────────────────────────────────────
490
+ builder.Services.AddAutoMapper(cfg =>
491
+ {
492
+ cfg.AddMaps(Assembly.GetExecutingAssembly());
493
+ });
494
+
495
+ // ── Scrutor — register all I*Service → *Service ────────────────────
496
+ builder.Services.Scan(scan => scan
497
+ .FromAssemblyOf<UserService>()
498
+ .AddClasses(classes => classes
499
+ .InNamespaces("$$NAMESPACE$$.INFRASTRUCTURE.Services")
500
+ .AssignableTo(typeof(GenericService<,,,>)))
501
+ .AsImplementedInterfaces()
502
+ .WithScopedLifetime());
503
+
504
+ // ── Controllers ─────────────────────────────────────────────────────
505
+ builder.Services.AddControllers();
506
+
507
+ // ── OpenAPI / Scalar ────────────────────────────────────────────────
508
+ builder.Services.AddOpenApi();
509
+
510
+ var app = builder.Build();
511
+
512
+ // ── Swagger / Scalar ────────────────────────────────────────────────
513
+ app.UseSwagger(options =>
514
+ {
515
+ options.RouteTemplate = "openapi/{documentName}.json";
516
+ });
517
+ app.MapScalarApiReference(options =>
518
+ {
519
+ options.WithTitle("$$NAMESPACE$$");
520
+ options.WithTheme(ScalarTheme.BluePlanet);
521
+ options.WithDefaultHttpClient(ScalarTarget.JavaScript, ScalarClient.Axios);
522
+ options.WithPreferredScheme("Bearer");
523
+ });
524
+
525
+ app.UseHttpsRedirection();
526
+ app.UseAuthentication();
527
+ app.UseAuthorization();
528
+ app.MapControllers();
529
+ app.Run();
530
+ """
531
+
532
+
533
+ def write_program_cs(template: str, project_dir: str, namespace_name: str):
534
+ content = template.replace("$$NAMESPACE$$", namespace_name)
535
+ path = os.path.join(project_dir, "Program.cs")
536
+ with open(path, "w", encoding="utf-8") as f:
537
+ f.write(content)
538
+ print(f" [ok] Program.cs updated")
539
+
540
+
541
+ COMPOSER_PACKAGES = [
542
+ "zircote/swagger-php",
543
+ "spatie/laravel-query-builder",
544
+ "spatie/laravel-medialibrary",
545
+ ]
546
+
547
+
548
+ def set_laravel_php_requirements(project_dir):
549
+ """Set PHP ^8.5 and Laravel ^12.0 requirements in composer.json."""
550
+ composer_path = os.path.join(project_dir, "composer.json")
551
+ if not os.path.exists(composer_path):
552
+ print(" [!!] composer.json not found, skipping version requirements")
553
+ return
554
+
555
+ with open(composer_path, "r", encoding="utf-8") as f:
556
+ composer = json.load(f)
557
+
558
+ composer.setdefault("require", {})
559
+ composer["require"]["php"] = "^8.5"
560
+
561
+ # Detect the installed Laravel version from composer.lock, fall back to ^13.0
562
+ lock_path = os.path.join(project_dir, "composer.lock")
563
+ laravel_constraint = "^13.0"
564
+ if os.path.exists(lock_path):
565
+ with open(lock_path, "r", encoding="utf-8") as f:
566
+ lock = json.load(f)
567
+ for pkg in lock.get("packages", []):
568
+ if pkg.get("name") == "laravel/framework":
569
+ version = pkg.get("version", "")
570
+ match = re.search(r'^(\d+)\.', version)
571
+ if match:
572
+ laravel_constraint = f"^{match.group(1)}.0"
573
+ break
574
+ composer["require"]["laravel/framework"] = laravel_constraint
575
+
576
+ with open(composer_path, "w", encoding="utf-8") as f:
577
+ json.dump(composer, f, indent=4)
578
+ print(f" [ok] composer.json updated: php ^8.5, laravel {laravel_constraint}")
579
+
580
+
581
+ def merge_temp_project(temp_dir, target_dir):
582
+ """Move all files from temp_dir into target_dir, then remove temp_dir."""
583
+ for item in os.listdir(temp_dir):
584
+ src = os.path.join(temp_dir, item)
585
+ dst = os.path.join(target_dir, item)
586
+ if os.path.isdir(src):
587
+ if os.path.exists(dst):
588
+ shutil.rmtree(dst)
589
+ shutil.move(src, dst)
590
+ else:
591
+ if os.path.exists(dst):
592
+ os.remove(dst)
593
+ shutil.move(src, dst)
594
+ os.rmdir(temp_dir)
595
+
596
+
597
+ def init_laravel_project(config, project_dir):
598
+ temp_dir = os.path.join(project_dir, "__laravel_temp__")
599
+ print("\nCreating Laravel project in temporary directory...")
600
+ run_composer([
601
+ "create-project", "laravel/laravel", temp_dir, "--prefer-dist",
602
+ "--ignore-platform-req=ext-fileinfo",
603
+ ])
604
+ print(" [ok] Laravel project created")
605
+
606
+ merge_temp_project(temp_dir, project_dir)
607
+ print(" [ok] project files merged into root")
608
+
609
+ create_config_dirs(config, project_dir)
610
+
611
+ set_laravel_php_requirements(project_dir)
612
+
613
+ print(f"\nInstalling Composer packages...")
614
+ run_composer(
615
+ ["require"] + COMPOSER_PACKAGES,
616
+ cwd=project_dir,
617
+ ignore_platform_reqs=["ext-fileinfo", "ext-exif", "ext-gd", "ext-imagick"],
618
+ )
619
+
620
+ write_config(config, project_dir)
621
+
622
+
623
+ BANNER = r"""
624
+ ███████ ████████ ██████ ██████ ███ ███
625
+ ██ ██ ██ ██ ██ ██ ████ ████
626
+ ███████ ██ ██ ██ ██████ ██ ████ ██
627
+ ██ ██ ██ ██ ██ ██ ██ ██ ██
628
+ ███████ ██ ██████ ██ ██ ██ ██
629
+
630
+
631
+ ██████ ██ ██
632
+ ██ ██ ██
633
+ ██ ██ ██
634
+ ██ ██ ██
635
+ ██████ ███████ ██
636
+
637
+
638
+ """
639
+
640
+ YEL = "\033[33m"
641
+ RST = "\033[0m"
642
+
643
+ HELP_BANNER = f"""{YEL}
644
+ ███████ ████████ ██████ ██████ ███ ███
645
+ ██ ██ ██ ██ ██ ██ ████ ████
646
+ ███████ ██ ██ ██ ██████ ██ ████ ██
647
+ ██ ██ ██ ██ ██ ██ ██ ██ ██
648
+ ███████ ██ ██████ ██ ██ ██ ██
649
+
650
+
651
+ ██████ ██ ██
652
+ ██ ██ ██
653
+ ██ ██ ██
654
+ ██ ██ ██
655
+ ██████ ███████ ██
656
+
657
+
658
+ API Starter Kit -- schema to source{RST}
659
+ """
660
+
661
+
662
+ def main():
663
+ parser = argparse.ArgumentParser(
664
+ description=HELP_BANNER,
665
+ formatter_class=argparse.RawDescriptionHelpFormatter,
666
+ )
667
+
668
+ parser.add_argument(
669
+ "--init", "-i",
670
+ action="store_true",
671
+ help="Initialize a new project"
672
+ )
673
+
674
+ parser.add_argument(
675
+ "--generate", "-g",
676
+ action="store_true",
677
+ help="Generate code from schema.storm"
678
+ )
679
+
680
+ template_choices = [t.value for t in Template]
681
+ parser.add_argument(
682
+ "--template", "-t",
683
+ choices=template_choices,
684
+ help=f"Project template to use (default: {template_choices[0]})"
685
+ )
686
+
687
+ parser.add_argument(
688
+ "--name", "-n",
689
+ help="Project name (defaults to current directory name)"
690
+ )
691
+
692
+ args = parser.parse_args()
693
+
694
+ if args.init:
695
+ print(YEL + BANNER + RST)
696
+ template = args.template or template_choices[0]
697
+ project_name = args.name or os.path.basename(os.getcwd())
698
+ print(f"Initializing project with template: {template}")
699
+ print(f"Project name: {project_name}")
700
+
701
+ check_network()
702
+
703
+ if template in (Template.DOTNETCSHARP.value, Template.DOTNETCSHARP_CLEANARCHITECTURE.value):
704
+ check_dotnet_prerequisites()
705
+ elif template == Template.LARAVELPHP.value:
706
+ check_laravel_prerequisites()
707
+
708
+ config = CONFIGS[template]
709
+
710
+ if template == Template.DOTNETCSHARP.value:
711
+ init_dotnet_project(config, ".", project_name)
712
+ elif template == Template.DOTNETCSHARP_CLEANARCHITECTURE.value:
713
+ init_dotnet_clean_arch_project(config, ".", project_name)
714
+ elif template == Template.LARAVELPHP.value:
715
+ init_laravel_project(config, ".")
716
+
717
+ schema_path = os.path.join(".", "schema.storm")
718
+ if not os.path.exists(schema_path):
719
+ with open(schema_path, "w", encoding="utf-8") as f:
720
+ f.write(SCHEMA_STORM)
721
+ print(" [ok] schema.storm created")
722
+ else:
723
+ print(" [--] schema.storm already exists, skipped")
724
+
725
+ elif args.generate:
726
+ print(YEL + BANNER + RST)
727
+ config_path = "storm.config.json"
728
+ schema_path = args.name or "schema.storm"
729
+
730
+ if not os.path.exists(config_path):
731
+ print(f"ERROR: {config_path} not found. Run --init first.")
732
+ sys.exit(1)
733
+
734
+ if not os.path.exists(schema_path):
735
+ print(f"ERROR: {schema_path} not found.")
736
+ sys.exit(1)
737
+
738
+ with open(config_path) as f:
739
+ config = json.load(f)
740
+
741
+ project_name = os.path.basename(os.getcwd())
742
+ print(f"Generating code for: {project_name}")
743
+ print(f" config: {config_path}")
744
+ print(f" schema: {schema_path}")
745
+
746
+ interpreter = Interpreter(schema_path)
747
+ interpreter.generate(config, project_name, ".")
748
+ else:
749
+ parser.print_help()
750
+
751
+
752
+ if __name__ == "__main__":
753
+ main()