aerocoding 0.1.28 → 0.1.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/dist/index.js CHANGED
@@ -1115,8 +1115,12 @@ var projectConfigSchema = z.object({
1115
1115
  $schema: z.string().optional().default("https://aerocoding.dev/schema.json"),
1116
1116
  /** Project UUID from aerocoding.dev */
1117
1117
  projectId: z.string().uuid(),
1118
- /** Template ID for architecture generation */
1118
+ /** Template ID for architecture generation (legacy, prefer backendTemplateId/frontendTemplateId) */
1119
1119
  templateId: z.string().min(1),
1120
+ /** Backend template ID */
1121
+ backendTemplateId: z.string().min(1).optional(),
1122
+ /** Frontend template ID */
1123
+ frontendTemplateId: z.string().min(1).optional(),
1120
1124
  /** Template version at project creation */
1121
1125
  templateVersion: z.string().optional(),
1122
1126
  /** Root namespace/package name */
@@ -1149,6 +1153,8 @@ function createProjectConfig(options) {
1149
1153
  $schema: "https://aerocoding.dev/schema.json",
1150
1154
  projectId: options.projectId,
1151
1155
  templateId: options.templateId,
1156
+ backendTemplateId: options.backendTemplateId,
1157
+ frontendTemplateId: options.frontendTemplateId,
1152
1158
  templateVersion: options.templateVersion,
1153
1159
  namespace: options.namespace,
1154
1160
  organizationId: options.organizationId,
@@ -1316,32 +1322,72 @@ async function createCommand(projectName, options) {
1316
1322
  }
1317
1323
  } catch {
1318
1324
  }
1319
- let templateId = options.template;
1320
- if (!templateId) {
1325
+ let backendTemplateId;
1326
+ let frontendTemplateId;
1327
+ const hasBackend = !!project.backendFramework;
1328
+ const hasFrontend = !!project.frontendFramework;
1329
+ if (options.template) {
1330
+ if (hasBackend) {
1331
+ backendTemplateId = options.template;
1332
+ } else if (hasFrontend) {
1333
+ frontendTemplateId = options.template;
1334
+ }
1335
+ }
1336
+ if (hasBackend && !backendTemplateId) {
1321
1337
  const templateSpinner = p.spinner();
1322
- templateSpinner.start("Loading templates...");
1338
+ templateSpinner.start("Loading backend templates...");
1323
1339
  const templateResult = await apiClient.getTemplates({
1324
1340
  category: "backend",
1325
1341
  language: project.backendFramework || void 0
1326
1342
  });
1327
- templateSpinner.stop("Templates loaded");
1343
+ templateSpinner.stop("Backend templates loaded");
1328
1344
  if (templateResult.templates.length === 0) {
1329
- p.cancel("No templates available for this project's framework.");
1330
- process.exit(1);
1345
+ p.log.warn("No backend templates available for this project's framework.");
1346
+ } else {
1347
+ const selectedTemplate = await p.select({
1348
+ message: "Select backend architecture template",
1349
+ options: templateResult.templates.map((tmpl) => ({
1350
+ value: tmpl.id,
1351
+ label: tmpl.name,
1352
+ hint: tmpl.description || `${tmpl.tier} tier`
1353
+ }))
1354
+ });
1355
+ if (p.isCancel(selectedTemplate)) {
1356
+ p.cancel("Operation cancelled.");
1357
+ process.exit(0);
1358
+ }
1359
+ backendTemplateId = selectedTemplate;
1331
1360
  }
1332
- const selectedTemplate = await p.select({
1333
- message: "Select architecture template",
1334
- options: templateResult.templates.map((tmpl) => ({
1335
- value: tmpl.id,
1336
- label: tmpl.name,
1337
- hint: tmpl.description || `${tmpl.tier} tier`
1338
- }))
1361
+ }
1362
+ if (hasFrontend && !frontendTemplateId) {
1363
+ const frontendSpinner = p.spinner();
1364
+ frontendSpinner.start("Loading frontend templates...");
1365
+ const frontendResult = await apiClient.getTemplates({
1366
+ category: "frontend",
1367
+ framework: project.frontendFramework || void 0
1339
1368
  });
1340
- if (p.isCancel(selectedTemplate)) {
1341
- p.cancel("Operation cancelled.");
1342
- process.exit(0);
1369
+ frontendSpinner.stop("Frontend templates loaded");
1370
+ if (frontendResult.templates.length === 0) {
1371
+ p.log.warn("No frontend templates available for this project's framework.");
1372
+ } else {
1373
+ const selectedFrontend = await p.select({
1374
+ message: "Select frontend architecture template",
1375
+ options: frontendResult.templates.map((tmpl) => ({
1376
+ value: tmpl.id,
1377
+ label: tmpl.name,
1378
+ hint: tmpl.description || `${tmpl.tier} tier`
1379
+ }))
1380
+ });
1381
+ if (p.isCancel(selectedFrontend)) {
1382
+ p.cancel("Operation cancelled.");
1383
+ process.exit(0);
1384
+ }
1385
+ frontendTemplateId = selectedFrontend;
1343
1386
  }
1344
- templateId = selectedTemplate;
1387
+ }
1388
+ if (!backendTemplateId && !frontendTemplateId) {
1389
+ p.cancel("No templates selected. At least one backend or frontend template is required.");
1390
+ process.exit(1);
1345
1391
  }
1346
1392
  const diagrams = project.schema?.diagrams || [];
1347
1393
  const hasMultipleDiagrams = diagrams.length > 1;
@@ -1401,50 +1447,73 @@ async function createCommand(projectName, options) {
1401
1447
  const useContexts = archStyleChoice === "bounded-contexts";
1402
1448
  const estimateSpinner = p.spinner();
1403
1449
  estimateSpinner.start("Calculating credit cost...");
1404
- let estimatedCredits = 0;
1405
1450
  let creditsRemaining = 0;
1406
- let estimatedFiles = [];
1407
- let estimatedEntities = 0;
1451
+ let backendEstimatedCredits = 0;
1452
+ let backendEstimatedFiles = [];
1453
+ let backendEstimatedEntities = 0;
1454
+ let frontendEstimatedCredits = 0;
1455
+ let frontendEstimatedFiles = [];
1456
+ let frontendEstimatedEntities = 0;
1408
1457
  const estimateDiagramIds = selectedDiagramIds.length < diagrams.length ? selectedDiagramIds : void 0;
1458
+ const featureFlags = {
1459
+ includeDtos: true,
1460
+ includeUseCases: true,
1461
+ includeMappers: true,
1462
+ includeControllers: true,
1463
+ includeEfConfig: true,
1464
+ includeValidation: true,
1465
+ includeDtoValidation: true,
1466
+ includeUnitTests: true,
1467
+ includeIntegrationTests: true,
1468
+ includeStarterFiles: true,
1469
+ includeReactions: true,
1470
+ includeOutbox: true,
1471
+ includeInbox: true
1472
+ };
1409
1473
  try {
1410
- const estimate = await apiClient.estimateCreditCost({
1411
- projectId,
1412
- templateId,
1413
- options: {
1414
- featureFlags: {
1415
- includeDtos: true,
1416
- includeUseCases: true,
1417
- includeMappers: true,
1418
- includeControllers: true,
1419
- includeEfConfig: true,
1420
- includeValidation: true,
1421
- includeDtoValidation: true,
1422
- includeUnitTests: true,
1423
- includeIntegrationTests: true,
1424
- includeStarterFiles: true,
1425
- includeReactions: true,
1426
- includeOutbox: true,
1427
- includeInbox: true
1428
- },
1429
- useContexts,
1430
- diagramIds: estimateDiagramIds
1431
- }
1432
- });
1433
- estimatedCredits = estimate.estimatedCredits;
1434
- estimatedFiles = estimate.files || [];
1435
- estimatedEntities = estimate.entities || 0;
1474
+ if (backendTemplateId) {
1475
+ const backendEstimate = await apiClient.estimateCreditCost({
1476
+ projectId,
1477
+ templateId: backendTemplateId,
1478
+ options: {
1479
+ featureFlags,
1480
+ useContexts,
1481
+ diagramIds: estimateDiagramIds
1482
+ }
1483
+ });
1484
+ backendEstimatedCredits = backendEstimate.estimatedCredits;
1485
+ backendEstimatedFiles = backendEstimate.files || [];
1486
+ backendEstimatedEntities = backendEstimate.entities || 0;
1487
+ }
1488
+ if (frontendTemplateId) {
1489
+ const frontendEstimate = await apiClient.estimateCreditCost({
1490
+ projectId,
1491
+ templateId: frontendTemplateId,
1492
+ options: {
1493
+ featureFlags,
1494
+ useContexts,
1495
+ diagramIds: estimateDiagramIds
1496
+ }
1497
+ });
1498
+ frontendEstimatedCredits = frontendEstimate.estimatedCredits;
1499
+ frontendEstimatedFiles = frontendEstimate.files || [];
1500
+ frontendEstimatedEntities = frontendEstimate.entities || 0;
1501
+ }
1436
1502
  const creditUsage = await apiClient.getCreditUsage(organizationId);
1437
1503
  creditsRemaining = creditUsage.remaining;
1438
1504
  estimateSpinner.stop("Credit estimate calculated");
1439
1505
  } catch {
1440
1506
  estimateSpinner.stop("Could not estimate credits (will be calculated on generation)");
1441
1507
  }
1442
- const hasEnoughCredits = estimatedCredits === 0 || creditsRemaining >= estimatedCredits;
1508
+ const totalEstimatedCredits = backendEstimatedCredits + frontendEstimatedCredits;
1509
+ const totalEstimatedFiles = backendEstimatedFiles.length + frontendEstimatedFiles.length;
1510
+ const totalEstimatedEntities = backendEstimatedEntities + frontendEstimatedEntities;
1511
+ const hasEnoughCredits = totalEstimatedCredits === 0 || creditsRemaining >= totalEstimatedCredits;
1512
+ const divider = ` ${"-".repeat(33)}`;
1443
1513
  console.log("");
1444
1514
  console.log(chalk7.bold(" Generation Summary"));
1445
- console.log(chalk7.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1515
+ console.log(chalk7.gray(divider));
1446
1516
  console.log(chalk7.gray(" Project:"), chalk7.white(project.name));
1447
- console.log(chalk7.gray(" Template:"), chalk7.cyan(templateId));
1448
1517
  console.log(chalk7.gray(" Namespace:"), chalk7.cyan(namespace));
1449
1518
  console.log(chalk7.gray(" Directory:"), chalk7.cyan(`${safeName}/`));
1450
1519
  console.log(
@@ -1461,36 +1530,89 @@ async function createCommand(projectName, options) {
1461
1530
  console.log(chalk7.gray(" Bounded Contexts:"), chalk7.cyan(`${selectedCount}/${totalCount} (${selectedNames})`));
1462
1531
  }
1463
1532
  }
1464
- if (estimatedFiles.length > 0) {
1533
+ console.log("");
1534
+ console.log(chalk7.bold(" Templates"));
1535
+ console.log(chalk7.gray(divider));
1536
+ if (backendTemplateId) {
1537
+ console.log(chalk7.gray(" Backend:"), chalk7.cyan(backendTemplateId));
1538
+ }
1539
+ if (frontendTemplateId) {
1540
+ console.log(chalk7.gray(" Frontend:"), chalk7.cyan(frontendTemplateId));
1541
+ }
1542
+ if (backendEstimatedFiles.length > 0) {
1543
+ console.log("");
1544
+ console.log(chalk7.bold(` Backend Files (${backendTemplateId})`));
1545
+ console.log(chalk7.gray(divider));
1546
+ const backendCategories = categorizeFilePaths(backendEstimatedFiles);
1547
+ const backendMaxNameLength = Math.max(...backendCategories.map((c) => c.name.length), 8);
1548
+ for (const category of backendCategories) {
1549
+ const padding = " ".repeat(backendMaxNameLength - category.name.length + 2);
1550
+ const countStr = category.count.toString().padStart(3, " ");
1551
+ console.log(
1552
+ chalk7.gray(` ${category.name}${padding}`),
1553
+ chalk7.cyan(`${countStr} files`)
1554
+ );
1555
+ }
1556
+ console.log(chalk7.gray(divider));
1557
+ const subtotalPadding = " ".repeat(backendMaxNameLength - 8 + 2);
1558
+ const subtotalStr = backendEstimatedFiles.length.toString().padStart(3, " ");
1559
+ console.log(
1560
+ chalk7.white(` Subtotal${subtotalPadding}`),
1561
+ chalk7.bold.cyan(`${subtotalStr} files`)
1562
+ );
1563
+ }
1564
+ if (frontendEstimatedFiles.length > 0) {
1465
1565
  console.log("");
1466
- console.log(chalk7.bold(" Files to Generate"));
1467
- console.log(chalk7.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1468
- const categories = categorizeFilePaths(estimatedFiles);
1469
- const maxNameLength = Math.max(...categories.map((c) => c.name.length));
1470
- for (const category of categories) {
1471
- const padding = " ".repeat(maxNameLength - category.name.length + 2);
1566
+ console.log(chalk7.bold(` Frontend Files (${frontendTemplateId})`));
1567
+ console.log(chalk7.gray(divider));
1568
+ const frontendCategories = categorizeFilePaths(frontendEstimatedFiles);
1569
+ const frontendMaxNameLength = Math.max(...frontendCategories.map((c) => c.name.length), 8);
1570
+ for (const category of frontendCategories) {
1571
+ const padding = " ".repeat(frontendMaxNameLength - category.name.length + 2);
1472
1572
  const countStr = category.count.toString().padStart(3, " ");
1473
1573
  console.log(
1474
1574
  chalk7.gray(` ${category.name}${padding}`),
1475
1575
  chalk7.cyan(`${countStr} files`)
1476
1576
  );
1477
1577
  }
1478
- console.log(chalk7.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1479
- const totalPadding = " ".repeat(maxNameLength - 5 + 2);
1480
- const totalStr = estimatedFiles.length.toString().padStart(3, " ");
1578
+ console.log(chalk7.gray(divider));
1579
+ const subtotalPadding = " ".repeat(frontendMaxNameLength - 8 + 2);
1580
+ const subtotalStr = frontendEstimatedFiles.length.toString().padStart(3, " ");
1581
+ console.log(
1582
+ chalk7.white(` Subtotal${subtotalPadding}`),
1583
+ chalk7.bold.cyan(`${subtotalStr} files`)
1584
+ );
1585
+ }
1586
+ if (backendEstimatedFiles.length > 0 && frontendEstimatedFiles.length > 0) {
1587
+ console.log("");
1588
+ console.log(chalk7.gray(divider));
1481
1589
  console.log(
1482
- chalk7.white(` Total${totalPadding}`),
1483
- chalk7.bold.cyan(`${totalStr} files`)
1590
+ chalk7.bold.white(" TOTAL "),
1591
+ chalk7.bold.green(`${totalEstimatedFiles.toString().padStart(3, " ")} files`)
1484
1592
  );
1485
- console.log(chalk7.gray(" Entities:"), chalk7.cyan(estimatedEntities));
1593
+ console.log(chalk7.gray(" Entities:"), chalk7.cyan(totalEstimatedEntities));
1594
+ } else if (totalEstimatedFiles > 0) {
1595
+ console.log(chalk7.gray(" Entities:"), chalk7.cyan(totalEstimatedEntities));
1486
1596
  }
1487
1597
  console.log("");
1488
1598
  console.log(chalk7.bold(" Credits"));
1489
- console.log(chalk7.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1490
- if (estimatedCredits > 0) {
1599
+ console.log(chalk7.gray(divider));
1600
+ if (backendEstimatedCredits > 0) {
1601
+ console.log(
1602
+ chalk7.gray(" Backend:"),
1603
+ chalk7.yellow(`${backendEstimatedCredits} credits`)
1604
+ );
1605
+ }
1606
+ if (frontendEstimatedCredits > 0) {
1491
1607
  console.log(
1492
- chalk7.white(" Cost:"),
1493
- hasEnoughCredits ? chalk7.bold.yellow(`${estimatedCredits} credits`) : chalk7.bold.red(`${estimatedCredits} credits`)
1608
+ chalk7.gray(" Frontend:"),
1609
+ chalk7.yellow(`${frontendEstimatedCredits} credits`)
1610
+ );
1611
+ }
1612
+ if (totalEstimatedCredits > 0) {
1613
+ console.log(
1614
+ chalk7.white(" Total Cost:"),
1615
+ hasEnoughCredits ? chalk7.bold.yellow(`${totalEstimatedCredits} credits`) : chalk7.bold.red(`${totalEstimatedCredits} credits`)
1494
1616
  );
1495
1617
  }
1496
1618
  console.log(
@@ -1499,13 +1621,13 @@ async function createCommand(projectName, options) {
1499
1621
  );
1500
1622
  console.log("");
1501
1623
  if (!hasEnoughCredits) {
1502
- console.log(chalk7.red(" \u26A0 Not enough credits for this generation."));
1503
- console.log(chalk7.gray(` Need ${estimatedCredits - creditsRemaining} more credits.
1624
+ console.log(chalk7.red(" ! Not enough credits for this generation."));
1625
+ console.log(chalk7.gray(` Need ${totalEstimatedCredits - creditsRemaining} more credits.
1504
1626
  `));
1505
1627
  process.exit(1);
1506
1628
  }
1507
1629
  const proceed = await p.confirm({
1508
- message: estimatedCredits > 0 ? `Proceed with generation? (~${estimatedCredits} credits)` : "Proceed with generation?",
1630
+ message: totalEstimatedCredits > 0 ? `Proceed with generation? (~${totalEstimatedCredits} credits)` : "Proceed with generation?",
1509
1631
  initialValue: true
1510
1632
  });
1511
1633
  if (p.isCancel(proceed) || !proceed) {
@@ -1516,47 +1638,68 @@ async function createCommand(projectName, options) {
1516
1638
  dirSpinner.start(`Creating ${safeName}/...`);
1517
1639
  await mkdir(projectDir, { recursive: true });
1518
1640
  dirSpinner.stop(`Created ${safeName}/`);
1519
- const genSpinner = ora2({ text: "Generating architecture...", color: "cyan" }).start();
1520
- const result = await apiClient.generateCode({
1521
- projectId,
1522
- templateId,
1523
- options: {
1524
- includeValidations: true,
1525
- includeComments: true,
1526
- featureFlags: {
1527
- includeDtos: true,
1528
- includeUseCases: true,
1529
- includeMappers: true,
1530
- includeControllers: true,
1531
- includeEfConfig: true,
1532
- includeValidation: true,
1533
- includeDtoValidation: true,
1534
- includeUnitTests: true,
1535
- includeIntegrationTests: true,
1536
- includeStarterFiles: true,
1537
- includeReactions: true,
1538
- includeOutbox: true,
1539
- includeInbox: true
1540
- },
1541
- useContexts,
1542
- diagramIds: estimateDiagramIds
1543
- // Filter by selected bounded contexts
1641
+ const allGeneratedFiles = [];
1642
+ let totalCreditsUsed = 0;
1643
+ let totalCreditsRemaining = creditsRemaining;
1644
+ if (backendTemplateId) {
1645
+ const backendSpinner = ora2({ text: "Generating backend architecture...", color: "cyan" }).start();
1646
+ const backendResult = await apiClient.generateCode({
1647
+ projectId,
1648
+ templateId: backendTemplateId,
1649
+ options: {
1650
+ includeValidations: true,
1651
+ includeComments: true,
1652
+ featureFlags,
1653
+ useContexts,
1654
+ diagramIds: estimateDiagramIds
1655
+ }
1656
+ });
1657
+ const backendFiles = (backendResult.files || []).map((file) => ({
1658
+ ...file,
1659
+ path: `backend/${file.path}`
1660
+ }));
1661
+ allGeneratedFiles.push(...backendFiles);
1662
+ if (backendResult.creditsUsed !== void 0) {
1663
+ totalCreditsUsed += backendResult.creditsUsed;
1664
+ totalCreditsRemaining = backendResult.creditsRemaining ?? totalCreditsRemaining;
1544
1665
  }
1545
- });
1546
- genSpinner.succeed(chalk7.green(`Generated ${result.files?.length || 0} files`));
1666
+ backendSpinner.succeed(chalk7.green(`Backend: ${backendFiles.length} files generated`));
1667
+ }
1668
+ if (frontendTemplateId) {
1669
+ const frontendSpinner = ora2({ text: "Generating frontend architecture...", color: "cyan" }).start();
1670
+ const frontendResult = await apiClient.generateCode({
1671
+ projectId,
1672
+ templateId: frontendTemplateId,
1673
+ options: {
1674
+ includeValidations: true,
1675
+ includeComments: true,
1676
+ featureFlags,
1677
+ useContexts,
1678
+ diagramIds: estimateDiagramIds
1679
+ }
1680
+ });
1681
+ const frontendFiles = (frontendResult.files || []).map((file) => ({
1682
+ ...file,
1683
+ path: `frontend/${file.path}`
1684
+ }));
1685
+ allGeneratedFiles.push(...frontendFiles);
1686
+ if (frontendResult.creditsUsed !== void 0) {
1687
+ totalCreditsUsed += frontendResult.creditsUsed;
1688
+ totalCreditsRemaining = frontendResult.creditsRemaining ?? totalCreditsRemaining;
1689
+ }
1690
+ frontendSpinner.succeed(chalk7.green(`Frontend: ${frontendFiles.length} files generated`));
1691
+ }
1547
1692
  const writeSpinner = p.spinner();
1548
1693
  writeSpinner.start("Writing files...");
1549
- const organizedFiles = result.files.map((file) => ({
1550
- ...file,
1551
- path: `backend/${file.path}`
1552
- }));
1553
- await writeGeneratedFiles(organizedFiles, projectDir, false);
1554
- writeSpinner.stop(`Wrote ${organizedFiles.length} files`);
1694
+ await writeGeneratedFiles(allGeneratedFiles, projectDir, false);
1695
+ writeSpinner.stop(`Wrote ${allGeneratedFiles.length} files`);
1555
1696
  const configSpinner = p.spinner();
1556
1697
  configSpinner.start("Creating config files...");
1557
1698
  const projectConfig = createProjectConfig({
1558
1699
  projectId,
1559
- templateId,
1700
+ templateId: backendTemplateId || frontendTemplateId || "",
1701
+ backendTemplateId,
1702
+ frontendTemplateId,
1560
1703
  templateVersion: "1.0.0",
1561
1704
  // TODO: get from template
1562
1705
  namespace,
@@ -1565,7 +1708,7 @@ async function createCommand(projectName, options) {
1565
1708
  });
1566
1709
  await saveProjectConfig(projectConfig, projectDir);
1567
1710
  let manifest = createEmptyManifest("1.0.0");
1568
- for (const file of organizedFiles) {
1711
+ for (const file of allGeneratedFiles) {
1569
1712
  const hash = hashString(file.content);
1570
1713
  manifest = setManifestFile(manifest, file.path, {
1571
1714
  hash,
@@ -1580,23 +1723,31 @@ async function createCommand(projectName, options) {
1580
1723
  const syncResult = await syncToCloud(apiClient, projectId, manifest);
1581
1724
  if (syncResult.success) {
1582
1725
  console.log(
1583
- chalk7.gray(" \u2713 Manifest synced to cloud") + chalk7.gray(` (${syncResult.fileCount} files)`)
1726
+ chalk7.gray(" OK Manifest synced to cloud") + chalk7.gray(` (${syncResult.fileCount} files)`)
1584
1727
  );
1585
1728
  }
1586
1729
  } catch {
1587
- console.log(chalk7.yellow(" \u26A0 Could not sync manifest to cloud"));
1730
+ console.log(chalk7.yellow(" ! Could not sync manifest to cloud"));
1588
1731
  }
1589
1732
  console.log("");
1590
1733
  console.log(chalk7.bold(" Project Created Successfully!"));
1591
- console.log(chalk7.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1734
+ console.log(chalk7.gray(divider));
1592
1735
  console.log(chalk7.gray(" Directory:"), chalk7.cyan(safeName + "/"));
1593
- console.log(chalk7.gray(" Files:"), chalk7.cyan(organizedFiles.length));
1736
+ const backendFileCount = allGeneratedFiles.filter((f) => f.path.startsWith("backend/")).length;
1737
+ const frontendFileCount = allGeneratedFiles.filter((f) => f.path.startsWith("frontend/")).length;
1738
+ if (backendFileCount > 0 && frontendFileCount > 0) {
1739
+ console.log(chalk7.gray(" Backend:"), chalk7.cyan(`${backendFileCount} files`));
1740
+ console.log(chalk7.gray(" Frontend:"), chalk7.cyan(`${frontendFileCount} files`));
1741
+ console.log(chalk7.gray(" Total:"), chalk7.bold.cyan(`${allGeneratedFiles.length} files`));
1742
+ } else {
1743
+ console.log(chalk7.gray(" Files:"), chalk7.cyan(allGeneratedFiles.length));
1744
+ }
1594
1745
  console.log(chalk7.gray(" Config:"), chalk7.cyan(PROJECT_CONFIG_FILENAME));
1595
1746
  console.log(chalk7.gray(" Manifest:"), chalk7.cyan(MANIFEST_FILENAME));
1596
- if (result.creditsUsed !== void 0) {
1747
+ if (totalCreditsUsed > 0) {
1597
1748
  console.log("");
1598
- console.log(chalk7.gray(" Credits used:"), chalk7.yellow(result.creditsUsed));
1599
- console.log(chalk7.gray(" Credits remaining:"), chalk7.green(result.creditsRemaining));
1749
+ console.log(chalk7.gray(" Credits used:"), chalk7.yellow(totalCreditsUsed));
1750
+ console.log(chalk7.gray(" Credits remaining:"), chalk7.green(totalCreditsRemaining));
1600
1751
  }
1601
1752
  p.outro(
1602
1753
  chalk7.green("Project ready!") + "\n\n" + chalk7.gray(" Next steps:\n") + chalk7.cyan(` cd ${safeName}
@@ -1714,59 +1865,124 @@ async function updateCommand(options) {
1714
1865
  projectSpinner.start("Fetching project details...");
1715
1866
  const project = await apiClient.getProject(config.projectId);
1716
1867
  projectSpinner.stop(`Project: ${project.name}`);
1717
- p2.log.step(chalk8.bold("Update Configuration:"));
1718
- p2.log.info(` Project: ${project.name}`);
1719
- p2.log.info(` Template: ${config.templateId}`);
1720
- p2.log.info(` Namespace: ${config.namespace}`);
1721
- p2.log.info(` Output: ${config.output.backend}`);
1868
+ const divider = ` ${"-".repeat(33)}`;
1869
+ const hasBackend = !!project.backendFramework;
1870
+ const hasFrontend = !!project.frontendFramework;
1871
+ let backendTemplateId = config.backendTemplateId;
1872
+ let frontendTemplateId = config.frontendTemplateId;
1873
+ if (!backendTemplateId && !frontendTemplateId) {
1874
+ if (hasBackend) {
1875
+ backendTemplateId = config.templateId;
1876
+ } else if (hasFrontend) {
1877
+ frontendTemplateId = config.templateId;
1878
+ }
1879
+ }
1880
+ if (!backendTemplateId && !frontendTemplateId) {
1881
+ p2.cancel("No templates configured for this project.");
1882
+ process.exit(1);
1883
+ }
1884
+ const backendOutput = config.output?.backend || "./backend";
1885
+ const frontendOutput = config.output?.frontend || "./frontend";
1886
+ const featureFlags = {
1887
+ includeDtos: true,
1888
+ includeUseCases: true,
1889
+ includeMappers: true,
1890
+ includeControllers: true,
1891
+ includeEfConfig: true,
1892
+ includeValidation: true,
1893
+ includeDtoValidation: true,
1894
+ includeUnitTests: true,
1895
+ includeIntegrationTests: true,
1896
+ includeStarterFiles: false,
1897
+ // Don't regenerate starter files on update
1898
+ includeReactions: true,
1899
+ includeOutbox: true,
1900
+ includeInbox: true
1901
+ };
1902
+ console.log("");
1903
+ console.log(chalk8.bold(" Update Configuration"));
1904
+ console.log(chalk8.gray(divider));
1905
+ console.log(chalk8.gray(" Project:"), chalk8.white(project.name));
1906
+ console.log(chalk8.gray(" Namespace:"), chalk8.cyan(config.namespace));
1907
+ if (backendTemplateId) {
1908
+ console.log(chalk8.gray(" Backend:"), chalk8.cyan(backendTemplateId));
1909
+ console.log(chalk8.gray(" Output:"), chalk8.cyan(backendOutput));
1910
+ }
1911
+ if (frontendTemplateId) {
1912
+ console.log(chalk8.gray(" Frontend:"), chalk8.cyan(frontendTemplateId));
1913
+ console.log(chalk8.gray(" Output:"), chalk8.cyan(frontendOutput));
1914
+ }
1722
1915
  if (options.dryRun) {
1723
- p2.log.info(chalk8.yellow(" Mode: DRY RUN (no files will be written)"));
1916
+ console.log(chalk8.yellow(" Mode: DRY RUN (no files will be written)"));
1724
1917
  }
1725
1918
  if (options.force) {
1726
- p2.log.info(chalk8.yellow(" Mode: FORCE (will overwrite modified files)"));
1919
+ console.log(chalk8.yellow(" Mode: FORCE (will overwrite modified files)"));
1727
1920
  }
1728
- let estimatedCredits = 0;
1921
+ let backendEstimatedCredits = 0;
1922
+ let frontendEstimatedCredits = 0;
1729
1923
  let creditsRemaining = 0;
1730
1924
  const estimateSpinner = p2.spinner();
1731
1925
  estimateSpinner.start("Calculating credit cost...");
1732
1926
  try {
1733
- const estimate = await apiClient.estimateCreditCost({
1734
- projectId: config.projectId,
1735
- templateId: config.templateId,
1736
- options: {
1737
- featureFlags: {
1738
- includeDtos: true,
1739
- includeUseCases: true,
1740
- includeMappers: true,
1741
- includeControllers: true,
1742
- includeEfConfig: true,
1743
- includeValidation: true,
1744
- includeDtoValidation: true,
1745
- includeUnitTests: true,
1746
- includeIntegrationTests: true,
1747
- includeStarterFiles: false
1748
- },
1749
- useContexts: true
1750
- }
1751
- });
1752
- estimatedCredits = estimate.estimatedCredits;
1927
+ if (backendTemplateId) {
1928
+ const backendEstimate = await apiClient.estimateCreditCost({
1929
+ projectId: config.projectId,
1930
+ templateId: backendTemplateId,
1931
+ options: {
1932
+ featureFlags,
1933
+ useContexts: true
1934
+ }
1935
+ });
1936
+ backendEstimatedCredits = backendEstimate.estimatedCredits;
1937
+ }
1938
+ if (frontendTemplateId) {
1939
+ const frontendEstimate = await apiClient.estimateCreditCost({
1940
+ projectId: config.projectId,
1941
+ templateId: frontendTemplateId,
1942
+ options: {
1943
+ featureFlags,
1944
+ useContexts: true
1945
+ }
1946
+ });
1947
+ frontendEstimatedCredits = frontendEstimate.estimatedCredits;
1948
+ }
1753
1949
  const creditUsage = await apiClient.getCreditUsage(project.organizationId);
1754
1950
  creditsRemaining = creditUsage.remaining;
1755
1951
  estimateSpinner.stop("Credit estimate calculated");
1756
1952
  } catch {
1757
1953
  estimateSpinner.stop("Could not estimate credits (will be calculated on generation)");
1758
1954
  }
1759
- if (estimatedCredits > 0) {
1760
- console.log("");
1761
- p2.log.info(chalk8.yellow(` Estimated Credits: ~${estimatedCredits}`));
1762
- p2.log.info(chalk8.gray(` Your Balance: ${creditsRemaining}`));
1763
- if (creditsRemaining < estimatedCredits) {
1764
- p2.log.warn(chalk8.red(" \u26A0 You may not have enough credits for this generation."));
1765
- }
1955
+ const totalEstimatedCredits = backendEstimatedCredits + frontendEstimatedCredits;
1956
+ const hasEnoughCredits = totalEstimatedCredits === 0 || creditsRemaining >= totalEstimatedCredits;
1957
+ console.log("");
1958
+ console.log(chalk8.bold(" Credits"));
1959
+ console.log(chalk8.gray(divider));
1960
+ if (backendEstimatedCredits > 0) {
1961
+ console.log(chalk8.gray(" Backend:"), chalk8.yellow(`${backendEstimatedCredits} credits`));
1962
+ }
1963
+ if (frontendEstimatedCredits > 0) {
1964
+ console.log(chalk8.gray(" Frontend:"), chalk8.yellow(`${frontendEstimatedCredits} credits`));
1965
+ }
1966
+ if (totalEstimatedCredits > 0) {
1967
+ console.log(
1968
+ chalk8.white(" Total Cost:"),
1969
+ hasEnoughCredits ? chalk8.bold.yellow(`${totalEstimatedCredits} credits`) : chalk8.bold.red(`${totalEstimatedCredits} credits`)
1970
+ );
1971
+ }
1972
+ console.log(
1973
+ chalk8.white(" Available:"),
1974
+ hasEnoughCredits ? chalk8.bold.green(`${creditsRemaining} credits`) : chalk8.bold.red(`${creditsRemaining} credits (insufficient)`)
1975
+ );
1976
+ console.log("");
1977
+ if (!hasEnoughCredits) {
1978
+ console.log(chalk8.red(" ! Not enough credits for this generation."));
1979
+ console.log(chalk8.gray(` Need ${totalEstimatedCredits - creditsRemaining} more credits.
1980
+ `));
1981
+ process.exit(1);
1766
1982
  }
1767
1983
  if (!options.dryRun) {
1768
1984
  const proceed = await p2.confirm({
1769
- message: estimatedCredits > 0 ? `Proceed with update (~${estimatedCredits} credits)?` : "Proceed with update?",
1985
+ message: totalEstimatedCredits > 0 ? `Proceed with update? (~${totalEstimatedCredits} credits)` : "Proceed with update?",
1770
1986
  initialValue: true
1771
1987
  });
1772
1988
  if (p2.isCancel(proceed) || !proceed) {
@@ -1774,56 +1990,70 @@ async function updateCommand(options) {
1774
1990
  process.exit(0);
1775
1991
  }
1776
1992
  }
1777
- const genSpinner = ora3({
1778
- text: "Generating code from latest schema...",
1779
- color: "cyan"
1780
- }).start();
1781
- const result = await apiClient.generateCode({
1782
- projectId: config.projectId,
1783
- templateId: config.templateId,
1784
- options: {
1785
- includeValidations: true,
1786
- includeComments: true,
1787
- featureFlags: {
1788
- includeDtos: true,
1789
- includeUseCases: true,
1790
- includeMappers: true,
1791
- includeControllers: true,
1792
- includeEfConfig: true,
1793
- includeValidation: true,
1794
- includeDtoValidation: true,
1795
- includeUnitTests: true,
1796
- includeIntegrationTests: true,
1797
- includeStarterFiles: false
1798
- // Don't regenerate starter files
1799
- },
1800
- useContexts: true
1993
+ const allGeneratedFiles = [];
1994
+ let totalCreditsUsed = 0;
1995
+ let totalCreditsRemaining = creditsRemaining;
1996
+ if (backendTemplateId) {
1997
+ const backendSpinner = ora3({ text: "Generating backend code...", color: "cyan" }).start();
1998
+ const backendResult = await apiClient.generateCode({
1999
+ projectId: config.projectId,
2000
+ templateId: backendTemplateId,
2001
+ options: {
2002
+ includeValidations: true,
2003
+ includeComments: true,
2004
+ featureFlags,
2005
+ useContexts: true
2006
+ }
2007
+ });
2008
+ const backendFiles = (backendResult.files || []).map((file) => ({
2009
+ ...file,
2010
+ path: `${backendOutput.replace(/^\.\//, "")}/${file.path}`
2011
+ }));
2012
+ allGeneratedFiles.push(...backendFiles);
2013
+ if (backendResult.creditsUsed !== void 0) {
2014
+ totalCreditsUsed += backendResult.creditsUsed;
2015
+ totalCreditsRemaining = backendResult.creditsRemaining ?? totalCreditsRemaining;
1801
2016
  }
1802
- });
1803
- genSpinner.succeed(
1804
- chalk8.green(`Generated ${result.files?.length || 0} files from schema`)
1805
- );
2017
+ backendSpinner.succeed(chalk8.green(`Backend: ${backendFiles.length} files generated`));
2018
+ }
2019
+ if (frontendTemplateId) {
2020
+ const frontendSpinner = ora3({ text: "Generating frontend code...", color: "cyan" }).start();
2021
+ const frontendResult = await apiClient.generateCode({
2022
+ projectId: config.projectId,
2023
+ templateId: frontendTemplateId,
2024
+ options: {
2025
+ includeValidations: true,
2026
+ includeComments: true,
2027
+ featureFlags,
2028
+ useContexts: true
2029
+ }
2030
+ });
2031
+ const frontendFiles = (frontendResult.files || []).map((file) => ({
2032
+ ...file,
2033
+ path: `${frontendOutput.replace(/^\.\//, "")}/${file.path}`
2034
+ }));
2035
+ allGeneratedFiles.push(...frontendFiles);
2036
+ if (frontendResult.creditsUsed !== void 0) {
2037
+ totalCreditsUsed += frontendResult.creditsUsed;
2038
+ totalCreditsRemaining = frontendResult.creditsRemaining ?? totalCreditsRemaining;
2039
+ }
2040
+ frontendSpinner.succeed(chalk8.green(`Frontend: ${frontendFiles.length} files generated`));
2041
+ }
1806
2042
  if (options.dryRun) {
1807
- p2.log.info("");
1808
- p2.log.info(chalk8.bold(" Dry Run Results:"));
1809
- p2.log.info(chalk8.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1810
- p2.log.info(` Files to process: ${result.files?.length || 0}`);
1811
- p2.log.info(` Tracked files: ${fileCount}`);
1812
- p2.log.info("");
1813
- p2.log.info(chalk8.gray(" Run without --dry-run to apply changes."));
2043
+ console.log("");
2044
+ console.log(chalk8.bold(" Dry Run Results:"));
2045
+ console.log(chalk8.gray(divider));
2046
+ console.log(` Files to process: ${allGeneratedFiles.length}`);
2047
+ console.log(` Tracked files: ${fileCount}`);
2048
+ console.log("");
2049
+ console.log(chalk8.gray(" Run without --dry-run to apply changes."));
1814
2050
  p2.outro(chalk8.green("Dry run complete!"));
1815
2051
  return;
1816
2052
  }
1817
2053
  const updateSpinner = p2.spinner();
1818
2054
  updateSpinner.start("Applying updates...");
1819
- const organizedFiles = result.files.map(
1820
- (file) => ({
1821
- ...file,
1822
- path: `${config.output.backend.replace(/^\.\//, "")}/${file.path}`
1823
- })
1824
- );
1825
2055
  const writeResult = await writeGeneratedFilesWithManifest(
1826
- organizedFiles,
2056
+ allGeneratedFiles,
1827
2057
  process.cwd(),
1828
2058
  config.templateVersion || "1.0.0",
1829
2059
  {
@@ -1832,13 +2062,10 @@ async function updateCommand(options) {
1832
2062
  }
1833
2063
  );
1834
2064
  updateSpinner.stop("Update complete");
1835
- if (result.creditsUsed !== void 0) {
2065
+ if (totalCreditsUsed > 0) {
1836
2066
  console.log("");
1837
- console.log(chalk8.gray(" Credits used:"), chalk8.yellow(result.creditsUsed));
1838
- console.log(
1839
- chalk8.gray(" Credits remaining:"),
1840
- chalk8.green(result.creditsRemaining)
1841
- );
2067
+ console.log(chalk8.gray(" Credits used:"), chalk8.yellow(totalCreditsUsed));
2068
+ console.log(chalk8.gray(" Credits remaining:"), chalk8.green(totalCreditsRemaining));
1842
2069
  }
1843
2070
  try {
1844
2071
  const syncResult = await syncToCloud(