aerocoding 0.1.22 → 0.1.24

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
@@ -81,7 +81,7 @@ var DeviceFlow = class {
81
81
  * Step 3: Poll for token
82
82
  */
83
83
  async pollForToken(auth) {
84
- const spinner2 = ora({
84
+ const spinner5 = ora({
85
85
  text: "Waiting for authorization...",
86
86
  color: "cyan"
87
87
  }).start();
@@ -89,7 +89,7 @@ var DeviceFlow = class {
89
89
  let currentInterval = auth.interval * 1e3;
90
90
  while (true) {
91
91
  if (Date.now() - startTime > MAX_POLL_TIME) {
92
- spinner2.fail(chalk.red("Authorization timeout"));
92
+ spinner5.fail(chalk.red("Authorization timeout"));
93
93
  throw new Error("Device authorization timed out");
94
94
  }
95
95
  try {
@@ -97,7 +97,7 @@ var DeviceFlow = class {
97
97
  device_code: auth.device_code,
98
98
  client_id: "aerocoding-cli"
99
99
  });
100
- spinner2.succeed(chalk.green("Successfully authenticated!"));
100
+ spinner5.succeed(chalk.green("Successfully authenticated!"));
101
101
  return response.data;
102
102
  } catch (error) {
103
103
  const errorCode = error.response?.data?.error;
@@ -108,19 +108,19 @@ var DeviceFlow = class {
108
108
  }
109
109
  if (errorCode === "slow_down") {
110
110
  currentInterval += 5e3;
111
- spinner2.text = "Polling... (slowed down to avoid spam)";
111
+ spinner5.text = "Polling... (slowed down to avoid spam)";
112
112
  await this.sleep(currentInterval);
113
113
  continue;
114
114
  }
115
115
  if (errorCode === "expired_token") {
116
- spinner2.fail(chalk.red("Authorization code expired"));
116
+ spinner5.fail(chalk.red("Authorization code expired"));
117
117
  throw new Error("Device code expired. Please try again.");
118
118
  }
119
119
  if (errorCode === "access_denied") {
120
- spinner2.fail(chalk.red("Authorization denied"));
120
+ spinner5.fail(chalk.red("Authorization denied"));
121
121
  throw new Error("You denied the authorization request");
122
122
  }
123
- spinner2.fail(chalk.red("Authorization failed"));
123
+ spinner5.fail(chalk.red("Authorization failed"));
124
124
  console.error(
125
125
  chalk.red(`Error: ${errorDescription || "Unknown error"}`)
126
126
  );
@@ -132,7 +132,7 @@ var DeviceFlow = class {
132
132
  * Helper: Sleep for specified milliseconds
133
133
  */
134
134
  sleep(ms) {
135
- return new Promise((resolve) => setTimeout(resolve, ms));
135
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
136
136
  }
137
137
  };
138
138
 
@@ -391,6 +391,34 @@ var ApiClient = class {
391
391
  const response = await this.client.get(`/api/templates/${templateId}?compatible=true`);
392
392
  return response.data.compatible || [];
393
393
  }
394
+ // ============================================
395
+ // Manifest Cloud Sync API
396
+ // ============================================
397
+ /**
398
+ * Get manifest from cloud storage
399
+ * @returns Manifest or null if not found
400
+ */
401
+ async getManifest(projectId) {
402
+ try {
403
+ const response = await this.client.get(`/api/projects/${projectId}/manifest`);
404
+ return response.data;
405
+ } catch (error) {
406
+ if (error?.response?.status === 404) {
407
+ return null;
408
+ }
409
+ throw error;
410
+ }
411
+ }
412
+ /**
413
+ * Save manifest to cloud storage
414
+ */
415
+ async saveManifest(projectId, manifest) {
416
+ const response = await this.client.put(
417
+ `/api/projects/${projectId}/manifest`,
418
+ manifest
419
+ );
420
+ return response.data;
421
+ }
394
422
  };
395
423
 
396
424
  // src/commands/_shared/create-api-client.ts
@@ -517,15 +545,269 @@ async function whoamiCommand() {
517
545
  // src/commands/generate.ts
518
546
  import chalk8 from "chalk";
519
547
  import ora2 from "ora";
548
+ import * as p from "@clack/prompts";
520
549
 
521
550
  // src/utils/file-writer.ts
522
- import fs from "fs/promises";
523
- import path from "path";
551
+ import fs3 from "fs/promises";
552
+ import path2 from "path";
524
553
  import chalk6 from "chalk";
554
+
555
+ // src/manifest/index.ts
556
+ import * as fs2 from "fs/promises";
557
+ import * as path from "path";
558
+
559
+ // src/manifest/types.ts
560
+ var MANIFEST_VERSION = "1.0.0";
561
+ var MANIFEST_FILENAME = "aerocoding-manifest.json";
562
+
563
+ // src/manifest/hash-utils.ts
564
+ import * as crypto from "crypto";
565
+ import * as fs from "fs/promises";
566
+ import { createReadStream } from "fs";
567
+ function hashString(content) {
568
+ return crypto.createHash("sha256").update(content, "utf-8").digest("hex");
569
+ }
570
+ async function hashFile(filePath) {
571
+ return new Promise((resolve2, reject) => {
572
+ const hash = crypto.createHash("sha256");
573
+ const stream = createReadStream(filePath);
574
+ stream.on("data", (chunk) => hash.update(chunk));
575
+ stream.on("end", () => resolve2(hash.digest("hex")));
576
+ stream.on("error", reject);
577
+ });
578
+ }
579
+ async function getFileStats(filePath) {
580
+ try {
581
+ const stats = await fs.stat(filePath);
582
+ return {
583
+ mtime: Math.floor(stats.mtimeMs),
584
+ size: stats.size
585
+ };
586
+ } catch {
587
+ return null;
588
+ }
589
+ }
590
+ function mightHaveChanged(currentStats, manifestEntry) {
591
+ return currentStats.mtime !== manifestEntry.mtime || currentStats.size !== manifestEntry.size;
592
+ }
593
+ async function detectFileChange(filePath, manifestEntry) {
594
+ const stats = await getFileStats(filePath);
595
+ if (!stats) {
596
+ if (manifestEntry) {
597
+ return { status: "deleted" };
598
+ }
599
+ return { status: "new" };
600
+ }
601
+ if (!manifestEntry) {
602
+ return { status: "unknown" };
603
+ }
604
+ if (!mightHaveChanged(stats, manifestEntry)) {
605
+ return { status: "unchanged" };
606
+ }
607
+ const currentHash = await hashFile(filePath);
608
+ if (currentHash === manifestEntry.hash) {
609
+ return { status: "unchanged" };
610
+ }
611
+ return { status: "modified", currentHash };
612
+ }
613
+
614
+ // src/manifest/index.ts
615
+ async function readManifest(projectDir) {
616
+ const manifestPath = path.join(projectDir, MANIFEST_FILENAME);
617
+ try {
618
+ const content = await fs2.readFile(manifestPath, "utf-8");
619
+ return JSON.parse(content);
620
+ } catch (error) {
621
+ if (error.code === "ENOENT") {
622
+ return null;
623
+ }
624
+ throw error;
625
+ }
626
+ }
627
+ async function writeManifest(projectDir, manifest) {
628
+ const manifestPath = path.join(projectDir, MANIFEST_FILENAME);
629
+ await fs2.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
630
+ }
631
+ function createEmptyManifest(templateVersion) {
632
+ return {
633
+ version: MANIFEST_VERSION,
634
+ lastSync: (/* @__PURE__ */ new Date()).toISOString(),
635
+ templateVersion,
636
+ files: {},
637
+ entities: []
638
+ };
639
+ }
640
+ function setManifestFile(manifest, filePath, entry) {
641
+ return {
642
+ ...manifest,
643
+ files: {
644
+ ...manifest.files,
645
+ [filePath]: entry
646
+ }
647
+ };
648
+ }
649
+
650
+ // src/merge/merger.ts
651
+ import diff3Merge from "diff3";
652
+ function performMerge(base, current, generated) {
653
+ const baseLines = base.split("\n");
654
+ const currentLines = current.split("\n");
655
+ const generatedLines = generated.split("\n");
656
+ const merged = diff3Merge(currentLines, baseLines, generatedLines);
657
+ const conflicts = [];
658
+ const outputLines = [];
659
+ let lineNumber = 1;
660
+ for (const region of merged) {
661
+ if (Array.isArray(region)) {
662
+ outputLines.push(...region);
663
+ lineNumber += region.length;
664
+ } else if ("ok" in region && region.ok) {
665
+ const okLines = region.ok;
666
+ outputLines.push(...okLines);
667
+ lineNumber += okLines.length;
668
+ } else if ("conflict" in region && region.conflict) {
669
+ const conflictData = region.conflict;
670
+ const yoursLines = conflictData.a || [];
671
+ const generatedLines2 = conflictData.b || [];
672
+ const yours = yoursLines.join("\n");
673
+ const generatedStr = generatedLines2.join("\n");
674
+ const conflictLines = [
675
+ "<<<<<<< YOURS",
676
+ ...yoursLines,
677
+ "=======",
678
+ ...generatedLines2,
679
+ ">>>>>>> GENERATED"
680
+ ];
681
+ const startLine = lineNumber;
682
+ outputLines.push(...conflictLines);
683
+ lineNumber += conflictLines.length;
684
+ conflicts.push({
685
+ startLine,
686
+ endLine: lineNumber - 1,
687
+ yours,
688
+ generated: generatedStr
689
+ });
690
+ }
691
+ }
692
+ return {
693
+ success: conflicts.length === 0,
694
+ content: outputLines.join("\n"),
695
+ conflicts
696
+ };
697
+ }
698
+
699
+ // src/merge/conflict-writer.ts
700
+ import { writeFile as writeFile2, unlink, access } from "fs/promises";
701
+ import { basename, extname } from "path";
702
+ var CONFLICT_NEW_EXT = ".new";
703
+ var CONFLICT_DIFF_EXT = ".conflict";
704
+ async function writeConflictFiles(filePath, generatedContent, conflicts, currentContent) {
705
+ const newPath = `${filePath}${CONFLICT_NEW_EXT}`;
706
+ const conflictPath = `${filePath}${CONFLICT_DIFF_EXT}`;
707
+ await writeFile2(newPath, generatedContent, "utf-8");
708
+ const conflictContent = generateConflictFile(
709
+ filePath,
710
+ currentContent,
711
+ generatedContent,
712
+ conflicts
713
+ );
714
+ await writeFile2(conflictPath, conflictContent, "utf-8");
715
+ return { newPath, conflictPath };
716
+ }
717
+ function generateConflictFile(filePath, _currentContent, _generatedContent, conflicts) {
718
+ const fileName = basename(filePath);
719
+ const ext = extname(filePath);
720
+ const commentStyle = getCommentStyle(ext);
721
+ const header = `${commentStyle.start}
722
+ ${commentStyle.prefix} ${fileName} - MERGE CONFLICT
723
+ ${commentStyle.prefix}
724
+ ${commentStyle.prefix} This file has ${conflicts.length} conflict(s) that need manual resolution.
725
+ ${commentStyle.prefix}
726
+ ${commentStyle.prefix} Resolution options:
727
+ ${commentStyle.prefix} 1. Edit ${fileName} to include both your changes and the generated changes
728
+ ${commentStyle.prefix} 2. Copy desired parts from ${fileName}${CONFLICT_NEW_EXT}
729
+ ${commentStyle.prefix} 3. Run 'aerocoding resolve' when done
730
+ ${commentStyle.prefix}
731
+ ${commentStyle.prefix} Your file: ${fileName} (unchanged)
732
+ ${commentStyle.prefix} Generated: ${fileName}${CONFLICT_NEW_EXT} (new version)
733
+ ${commentStyle.end}
734
+
735
+ `;
736
+ const conflictSections = conflicts.map((conflict, index) => {
737
+ return `${commentStyle.start}
738
+ ${commentStyle.prefix} CONFLICT ${index + 1} of ${conflicts.length} (lines ${conflict.startLine}-${conflict.endLine})
739
+ ${commentStyle.end}
740
+
741
+ <<<<<<< YOURS (keep your changes)
742
+ ${conflict.yours}
743
+ =======
744
+ ${conflict.generated}
745
+ >>>>>>> GENERATED (from template)
746
+ `;
747
+ }).join("\n");
748
+ return header + conflictSections;
749
+ }
750
+ function getCommentStyle(ext) {
751
+ switch (ext.toLowerCase()) {
752
+ case ".cs":
753
+ case ".ts":
754
+ case ".tsx":
755
+ case ".js":
756
+ case ".jsx":
757
+ case ".java":
758
+ case ".kt":
759
+ case ".dart":
760
+ case ".go":
761
+ case ".swift":
762
+ case ".rs":
763
+ case ".c":
764
+ case ".cpp":
765
+ case ".h":
766
+ return { start: "/*", prefix: " *", end: " */" };
767
+ case ".py":
768
+ case ".rb":
769
+ case ".sh":
770
+ case ".yaml":
771
+ case ".yml":
772
+ return { start: "#", prefix: "#", end: "#" };
773
+ case ".html":
774
+ case ".xml":
775
+ case ".xaml":
776
+ case ".svg":
777
+ return { start: "<!--", prefix: " ", end: "-->" };
778
+ case ".sql":
779
+ return { start: "--", prefix: "--", end: "--" };
780
+ case ".css":
781
+ case ".scss":
782
+ case ".less":
783
+ return { start: "/*", prefix: " *", end: " */" };
784
+ default:
785
+ return { start: "//", prefix: "//", end: "//" };
786
+ }
787
+ }
788
+ async function removeConflictFiles(filePath) {
789
+ const newPath = `${filePath}${CONFLICT_NEW_EXT}`;
790
+ const conflictPath = `${filePath}${CONFLICT_DIFF_EXT}`;
791
+ let removedNew = false;
792
+ let removedConflict = false;
793
+ try {
794
+ await unlink(newPath);
795
+ removedNew = true;
796
+ } catch {
797
+ }
798
+ try {
799
+ await unlink(conflictPath);
800
+ removedConflict = true;
801
+ } catch {
802
+ }
803
+ return { removedNew, removedConflict };
804
+ }
805
+
806
+ // src/utils/file-writer.ts
525
807
  function isPathSafe(outputDir, filePath) {
526
- const resolvedOutput = path.resolve(outputDir);
527
- const resolvedFile = path.resolve(outputDir, filePath);
528
- return resolvedFile.startsWith(resolvedOutput + path.sep) || resolvedFile === resolvedOutput;
808
+ const resolvedOutput = path2.resolve(outputDir);
809
+ const resolvedFile = path2.resolve(outputDir, filePath);
810
+ return resolvedFile.startsWith(resolvedOutput + path2.sep) || resolvedFile === resolvedOutput;
529
811
  }
530
812
  function getCategoryFromPath(filePath) {
531
813
  const lowerPath = filePath.toLowerCase();
@@ -589,6 +871,7 @@ function displayCategorizedSummary(files) {
589
871
  }
590
872
  async function writeGeneratedFiles(files, outputDir, verbose = false) {
591
873
  const writtenFiles = [];
874
+ const skippedOnceFiles = [];
592
875
  for (const file of files) {
593
876
  if (!isPathSafe(outputDir, file.path)) {
594
877
  console.error(chalk6.red(` Skipping unsafe path: ${file.path}`));
@@ -599,11 +882,19 @@ async function writeGeneratedFiles(files, outputDir, verbose = false) {
599
882
  );
600
883
  continue;
601
884
  }
602
- const fullPath = path.resolve(outputDir, file.path);
603
- const dir = path.dirname(fullPath);
885
+ const fullPath = path2.resolve(outputDir, file.path);
886
+ const dir = path2.dirname(fullPath);
887
+ if (file.generateOnce) {
888
+ try {
889
+ await fs3.access(fullPath);
890
+ skippedOnceFiles.push(file.path);
891
+ continue;
892
+ } catch {
893
+ }
894
+ }
604
895
  try {
605
- await fs.mkdir(dir, { recursive: true });
606
- await fs.writeFile(fullPath, file.content, "utf-8");
896
+ await fs3.mkdir(dir, { recursive: true });
897
+ await fs3.writeFile(fullPath, file.content, "utf-8");
607
898
  if (verbose) {
608
899
  console.log(chalk6.gray(` ${file.path}`));
609
900
  }
@@ -616,13 +907,208 @@ async function writeGeneratedFiles(files, outputDir, verbose = false) {
616
907
  if (!verbose && writtenFiles.length > 0) {
617
908
  displayCategorizedSummary(writtenFiles);
618
909
  }
910
+ if (skippedOnceFiles.length > 0) {
911
+ console.log("");
912
+ console.log(
913
+ chalk6.gray(` Skipped ${skippedOnceFiles.length} existing file(s) (generateOnce):`)
914
+ );
915
+ for (const filePath of skippedOnceFiles) {
916
+ console.log(chalk6.gray(` - ${filePath}`));
917
+ }
918
+ }
919
+ }
920
+ function inferFileType(filePath) {
921
+ const lowerPath = filePath.toLowerCase();
922
+ if (lowerPath.includes("/entities/")) return "entity";
923
+ if (lowerPath.includes("/usecases/") || lowerPath.includes("/use-cases/")) return "usecase";
924
+ if (lowerPath.includes("/repositories/")) return "repository";
925
+ if (lowerPath.includes("/controllers/")) return "controller";
926
+ if (lowerPath.includes("/dtos/") || lowerPath.includes("/dto/")) return "dto";
927
+ if (lowerPath.includes("/tests/") || lowerPath.includes(".test.") || lowerPath.includes(".spec.")) return "test";
928
+ if (lowerPath.includes("/config") || lowerPath.includes("appsettings")) return "config";
929
+ return "other";
930
+ }
931
+ async function writeGeneratedFilesWithManifest(files, outputDir, templateVersion, options = {}) {
932
+ const { verbose = false, forceOverwrite = false } = options;
933
+ let manifest = await readManifest(outputDir) || createEmptyManifest(templateVersion);
934
+ const result = {
935
+ created: [],
936
+ updated: [],
937
+ merged: [],
938
+ skipped: [],
939
+ conflicts: [],
940
+ manifest
941
+ };
942
+ for (const file of files) {
943
+ if (!isPathSafe(outputDir, file.path)) {
944
+ console.error(chalk6.red(` Skipping unsafe path: ${file.path}`));
945
+ continue;
946
+ }
947
+ const fullPath = path2.resolve(outputDir, file.path);
948
+ const dir = path2.dirname(fullPath);
949
+ const manifestEntry = manifest.files[file.path];
950
+ const changeStatus = await detectFileChange(fullPath, manifestEntry);
951
+ let shouldWrite = false;
952
+ let action = "skip";
953
+ switch (changeStatus.status) {
954
+ case "new":
955
+ shouldWrite = true;
956
+ action = "create";
957
+ break;
958
+ case "unchanged":
959
+ shouldWrite = true;
960
+ action = "update";
961
+ break;
962
+ case "modified":
963
+ if (forceOverwrite) {
964
+ shouldWrite = true;
965
+ action = "update";
966
+ } else {
967
+ const currentContent = await fs3.readFile(fullPath, "utf-8");
968
+ const mergeResult = performMerge(
969
+ manifestEntry?.hash ? "" : "",
970
+ // We don't have base content stored
971
+ currentContent,
972
+ file.content
973
+ );
974
+ if (mergeResult.success) {
975
+ shouldWrite = true;
976
+ action = "update";
977
+ file._mergedContent = mergeResult.content;
978
+ result.merged.push(file.path);
979
+ } else {
980
+ action = "conflict";
981
+ await fs3.mkdir(dir, { recursive: true });
982
+ const { newPath, conflictPath } = await writeConflictFiles(
983
+ fullPath,
984
+ file.content,
985
+ mergeResult.conflicts,
986
+ currentContent
987
+ );
988
+ result.conflicts.push({
989
+ path: file.path,
990
+ reason: "Merge conflict - manual resolution required",
991
+ newPath: path2.relative(outputDir, newPath),
992
+ conflictPath: path2.relative(outputDir, conflictPath)
993
+ });
994
+ }
995
+ }
996
+ break;
997
+ case "deleted":
998
+ action = "skip";
999
+ result.skipped.push(file.path);
1000
+ break;
1001
+ case "unknown":
1002
+ action = "skip";
1003
+ result.skipped.push(file.path);
1004
+ break;
1005
+ }
1006
+ if (file.generateOnce && changeStatus.status !== "new") {
1007
+ shouldWrite = false;
1008
+ action = "skip";
1009
+ if (!result.skipped.includes(file.path)) {
1010
+ result.skipped.push(file.path);
1011
+ }
1012
+ }
1013
+ if (shouldWrite) {
1014
+ try {
1015
+ await fs3.mkdir(dir, { recursive: true });
1016
+ const contentToWrite = file._mergedContent || file.content;
1017
+ await fs3.writeFile(fullPath, contentToWrite, "utf-8");
1018
+ const stats = await fs3.stat(fullPath);
1019
+ const entry = {
1020
+ hash: hashString(contentToWrite),
1021
+ mtime: Math.floor(stats.mtimeMs),
1022
+ size: stats.size,
1023
+ entityId: file.entityId,
1024
+ type: file.type || inferFileType(file.path),
1025
+ contextName: file.contextName
1026
+ };
1027
+ manifest = setManifestFile(manifest, file.path, entry);
1028
+ const isMerged = result.merged.includes(file.path);
1029
+ if (action === "create") {
1030
+ result.created.push(file.path);
1031
+ if (verbose) {
1032
+ console.log(chalk6.green(` \u2713 Created ${file.path}`));
1033
+ }
1034
+ } else if (isMerged) {
1035
+ if (verbose) {
1036
+ console.log(chalk6.magenta(` \u2713 Merged ${file.path}`));
1037
+ }
1038
+ } else {
1039
+ result.updated.push(file.path);
1040
+ if (verbose) {
1041
+ console.log(chalk6.blue(` \u2713 Updated ${file.path}`));
1042
+ }
1043
+ }
1044
+ } catch (error) {
1045
+ console.error(chalk6.red(` Failed to write ${file.path}`));
1046
+ console.error(chalk6.gray(` ${error.message}`));
1047
+ }
1048
+ } else if (action === "conflict" && verbose) {
1049
+ console.log(chalk6.yellow(` \u26A0 Conflict ${file.path}`));
1050
+ }
1051
+ }
1052
+ manifest = {
1053
+ ...manifest,
1054
+ lastSync: (/* @__PURE__ */ new Date()).toISOString(),
1055
+ templateVersion
1056
+ };
1057
+ await writeManifest(outputDir, manifest);
1058
+ result.manifest = manifest;
1059
+ if (!verbose) {
1060
+ displayIncrementalSummary(result);
1061
+ }
1062
+ return result;
1063
+ }
1064
+ function displayIncrementalSummary(result) {
1065
+ console.log("");
1066
+ console.log(chalk6.bold(" Update Results"));
1067
+ console.log(chalk6.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"));
1068
+ if (result.created.length > 0) {
1069
+ console.log(chalk6.green(` \u2713 Created: ${result.created.length} files`));
1070
+ }
1071
+ if (result.updated.length > 0) {
1072
+ console.log(chalk6.blue(` \u2713 Updated: ${result.updated.length} files`));
1073
+ }
1074
+ if (result.merged.length > 0) {
1075
+ console.log(chalk6.magenta(` \u2713 Merged: ${result.merged.length} files`));
1076
+ }
1077
+ if (result.skipped.length > 0) {
1078
+ console.log(chalk6.gray(` \u25CB Skipped: ${result.skipped.length} files`));
1079
+ }
1080
+ if (result.conflicts.length > 0) {
1081
+ console.log(chalk6.yellow(` \u26A0 Conflicts: ${result.conflicts.length} files`));
1082
+ }
1083
+ console.log(chalk6.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"));
1084
+ const total = result.created.length + result.updated.length + result.merged.length;
1085
+ console.log(chalk6.white(` Total written: ${total} files`));
1086
+ if (result.conflicts.length > 0) {
1087
+ console.log("");
1088
+ console.log(chalk6.yellow(" \u26A0 Conflicts need manual resolution:"));
1089
+ console.log("");
1090
+ for (const conflict of result.conflicts.slice(0, 5)) {
1091
+ console.log(chalk6.yellow(` ${conflict.path}`));
1092
+ if (conflict.newPath) {
1093
+ console.log(chalk6.gray(` \u2192 ${conflict.newPath}`));
1094
+ }
1095
+ if (conflict.conflictPath) {
1096
+ console.log(chalk6.gray(` \u2192 ${conflict.conflictPath}`));
1097
+ }
1098
+ }
1099
+ if (result.conflicts.length > 5) {
1100
+ console.log(chalk6.gray(` ... and ${result.conflicts.length - 5} more`));
1101
+ }
1102
+ console.log("");
1103
+ console.log(chalk6.gray(" Run 'aerocoding resolve' after fixing conflicts."));
1104
+ }
619
1105
  }
620
1106
 
621
1107
  // src/utils/prompt.ts
622
1108
  import readline from "readline";
623
1109
  import chalk7 from "chalk";
624
1110
  function promptConfirm(message) {
625
- return new Promise((resolve) => {
1111
+ return new Promise((resolve2) => {
626
1112
  const rl = readline.createInterface({
627
1113
  input: process.stdin,
628
1114
  output: process.stdout
@@ -630,14 +1116,14 @@ function promptConfirm(message) {
630
1116
  rl.question(chalk7.yellow(`${message} [Y/n] `), (answer) => {
631
1117
  rl.close();
632
1118
  const normalized = answer.trim().toLowerCase();
633
- resolve(normalized !== "n" && normalized !== "no");
1119
+ resolve2(normalized !== "n" && normalized !== "no");
634
1120
  });
635
1121
  });
636
1122
  }
637
1123
 
638
1124
  // src/config/loader.ts
639
- import fs2 from "fs/promises";
640
- import path2 from "path";
1125
+ import fs4 from "fs/promises";
1126
+ import path3 from "path";
641
1127
 
642
1128
  // src/config/schema.ts
643
1129
  import { z } from "zod";
@@ -648,12 +1134,10 @@ var configSchema = z.object({
648
1134
  // Architecture style: how to organize generated code
649
1135
  architectureStyle: z.enum(["bounded-contexts", "flat"]).optional().default("bounded-contexts"),
650
1136
  backend: z.object({
651
- preset: z.string(),
652
- layers: z.array(z.string())
1137
+ preset: z.string()
653
1138
  }).optional(),
654
1139
  frontend: z.object({
655
- preset: z.string(),
656
- layers: z.array(z.string())
1140
+ preset: z.string()
657
1141
  }).optional(),
658
1142
  codeStyle: z.object({
659
1143
  includeValidations: z.boolean().default(true),
@@ -671,9 +1155,9 @@ var CONFIG_FILENAME = ".aerocodingrc.json";
671
1155
 
672
1156
  // src/config/loader.ts
673
1157
  async function loadConfig(dir = process.cwd()) {
674
- const configPath = path2.join(dir, CONFIG_FILENAME);
1158
+ const configPath = path3.join(dir, CONFIG_FILENAME);
675
1159
  try {
676
- const content = await fs2.readFile(configPath, "utf-8");
1160
+ const content = await fs4.readFile(configPath, "utf-8");
677
1161
  const parsed = JSON.parse(content);
678
1162
  return configSchema.parse(parsed);
679
1163
  } catch (error) {
@@ -684,7 +1168,7 @@ async function loadConfig(dir = process.cwd()) {
684
1168
  }
685
1169
  }
686
1170
  async function saveConfig(config, dir = process.cwd()) {
687
- const configPath = path2.join(dir, CONFIG_FILENAME);
1171
+ const configPath = path3.join(dir, CONFIG_FILENAME);
688
1172
  const content = JSON.stringify(
689
1173
  {
690
1174
  $schema: "https://aerocoding.dev/schemas/aerocodingrc.json",
@@ -693,12 +1177,12 @@ async function saveConfig(config, dir = process.cwd()) {
693
1177
  null,
694
1178
  2
695
1179
  );
696
- await fs2.writeFile(configPath, content, "utf-8");
1180
+ await fs4.writeFile(configPath, content, "utf-8");
697
1181
  }
698
1182
  async function configExists(dir = process.cwd()) {
699
- const configPath = path2.join(dir, CONFIG_FILENAME);
1183
+ const configPath = path3.join(dir, CONFIG_FILENAME);
700
1184
  try {
701
- await fs2.access(configPath);
1185
+ await fs4.access(configPath);
702
1186
  return true;
703
1187
  } catch {
704
1188
  return false;
@@ -735,35 +1219,6 @@ function mapPresetToTarget(preset) {
735
1219
  }
736
1220
  return null;
737
1221
  }
738
- function buildTemplateIdFromConfig(config) {
739
- if (!config) return null;
740
- if (config.backend?.preset) {
741
- const templateId = mapPresetToTemplateId(config.backend.preset);
742
- if (templateId) return templateId;
743
- }
744
- if (config.frontend?.preset) {
745
- const templateId = mapPresetToTemplateId(config.frontend.preset);
746
- if (templateId) return templateId;
747
- }
748
- return null;
749
- }
750
- function buildTargetsFromConfig(config) {
751
- if (!config) return [];
752
- const targets = [];
753
- if (config.backend?.preset) {
754
- const target = mapPresetToTarget(config.backend.preset);
755
- if (target) {
756
- targets.push(target);
757
- }
758
- }
759
- if (config.frontend?.preset) {
760
- const target = mapPresetToTarget(config.frontend.preset);
761
- if (target) {
762
- targets.push(target);
763
- }
764
- }
765
- return targets;
766
- }
767
1222
  function buildFeatureFlagsFromConfig(config) {
768
1223
  const flags = {
769
1224
  // Default all feature flags to true for full architecture
@@ -791,6 +1246,91 @@ function buildFeatureFlagsFromConfig(config) {
791
1246
  }
792
1247
  return flags;
793
1248
  }
1249
+ function categorizeFilesByTarget(files) {
1250
+ const backend = [];
1251
+ const frontend = [];
1252
+ for (const file of files) {
1253
+ const ext = file.split(".").pop()?.toLowerCase();
1254
+ if (ext === "cs" || ext === "csproj" || ext === "sln") {
1255
+ backend.push(file);
1256
+ } else if (ext === "dart" || file.includes("pubspec.yaml")) {
1257
+ frontend.push(file);
1258
+ } else if (ext === "ts" || ext === "tsx" || ext === "js" || ext === "jsx") {
1259
+ if (file.toLowerCase().includes("frontend") || file.includes(".tsx")) {
1260
+ frontend.push(file);
1261
+ } else {
1262
+ backend.push(file);
1263
+ }
1264
+ } else {
1265
+ if (file.toLowerCase().includes("flutter") || file.toLowerCase().includes("dart")) {
1266
+ frontend.push(file);
1267
+ } else {
1268
+ backend.push(file);
1269
+ }
1270
+ }
1271
+ }
1272
+ return { backend, frontend };
1273
+ }
1274
+ function displayFileCategories(files) {
1275
+ const categories = categorizeFilePaths(files);
1276
+ const maxNameLength = Math.max(...categories.map((c) => c.name.length));
1277
+ for (const category of categories) {
1278
+ const padding = " ".repeat(maxNameLength - category.name.length + 2);
1279
+ const countStr = category.count.toString().padStart(3, " ");
1280
+ console.log(
1281
+ chalk8.gray(` ${category.name}${padding}`),
1282
+ chalk8.cyan(`${countStr} files`)
1283
+ );
1284
+ }
1285
+ }
1286
+ function isBackendFile(filePath) {
1287
+ const ext = filePath.split(".").pop()?.toLowerCase();
1288
+ if (ext === "cs" || ext === "csproj" || ext === "sln") {
1289
+ return true;
1290
+ }
1291
+ if (ext === "ts" || ext === "js") {
1292
+ if (filePath.toLowerCase().includes("frontend") || filePath.includes(".tsx")) {
1293
+ return false;
1294
+ }
1295
+ return true;
1296
+ }
1297
+ return false;
1298
+ }
1299
+ function isFrontendFile(filePath) {
1300
+ const ext = filePath.split(".").pop()?.toLowerCase();
1301
+ if (ext === "dart" || filePath.includes("pubspec.yaml")) {
1302
+ return true;
1303
+ }
1304
+ if (ext === "tsx" || ext === "jsx") {
1305
+ return true;
1306
+ }
1307
+ if ((ext === "ts" || ext === "js") && filePath.toLowerCase().includes("frontend")) {
1308
+ return true;
1309
+ }
1310
+ if (filePath.toLowerCase().includes("flutter") || filePath.toLowerCase().includes("dart")) {
1311
+ return true;
1312
+ }
1313
+ return false;
1314
+ }
1315
+ function organizeFilesIntoFolders(files, selectedTarget) {
1316
+ return files.map((file) => {
1317
+ let newPath = file.path;
1318
+ if (selectedTarget === "both") {
1319
+ if (isBackendFile(file.path)) {
1320
+ newPath = `backend/${file.path}`;
1321
+ } else if (isFrontendFile(file.path)) {
1322
+ newPath = `frontend/${file.path}`;
1323
+ } else {
1324
+ newPath = `backend/${file.path}`;
1325
+ }
1326
+ } else if (selectedTarget === "backend") {
1327
+ newPath = `backend/${file.path}`;
1328
+ } else if (selectedTarget === "frontend") {
1329
+ newPath = `frontend/${file.path}`;
1330
+ }
1331
+ return { ...file, path: newPath };
1332
+ });
1333
+ }
794
1334
  async function generateCommand(options) {
795
1335
  const tokenManager = new TokenManager();
796
1336
  const token = await tokenManager.getAccessToken();
@@ -807,27 +1347,119 @@ async function generateCommand(options) {
807
1347
  console.log(chalk8.gray(" Or use --project <id> to specify a project\n"));
808
1348
  process.exit(1);
809
1349
  }
810
- const templateId = buildTemplateIdFromConfig(config);
811
- const featureFlags = templateId ? buildFeatureFlagsFromConfig(config) : void 0;
812
- const targets = !templateId ? options.targets || buildTargetsFromConfig(config) : void 0;
813
1350
  const output = options.output || config?.output || "./.aerocoding";
814
- const backendPreset = options.backendPreset || config?.backend?.preset;
815
- const frontendPreset = options.frontendPreset || config?.frontend?.preset;
816
- const backendLayers = options.backendLayers || config?.backend?.layers;
817
- const frontendLayers = options.frontendLayers || config?.frontend?.layers;
818
1351
  const validationLib = options.validationLib || config?.libraries?.validation;
819
1352
  const includeValidations = options.validations ?? config?.codeStyle?.includeValidations ?? true;
820
1353
  const includeComments = options.comments ?? config?.codeStyle?.includeComments ?? true;
821
1354
  const includeLogging = options.logging ?? config?.codeStyle?.includeLogging ?? true;
822
1355
  const includeTesting = options.testing ?? config?.codeStyle?.includeTesting ?? true;
823
1356
  const apiClient = createApiClientWithAutoLogout(token, tokenManager);
824
- let spinner2 = ora2({ text: "Fetching project...", color: "cyan" }).start();
1357
+ let spinner5 = ora2({ text: "Fetching project...", color: "cyan" }).start();
825
1358
  try {
826
1359
  const project = await apiClient.getProject(projectId);
827
- spinner2.succeed(chalk8.gray("Project loaded"));
828
- spinner2 = ora2({ text: "Checking credits...", color: "cyan" }).start();
1360
+ spinner5.succeed(chalk8.gray("Project loaded"));
1361
+ const hasBackendConfig = !!config?.backend?.preset;
1362
+ const hasFrontendConfig = !!config?.frontend?.preset;
1363
+ const projectHasBackend = !!project.backendFramework;
1364
+ const projectHasFrontend = !!project.frontendFramework;
1365
+ if (hasBackendConfig && !projectHasBackend) {
1366
+ console.log("");
1367
+ console.log(chalk8.yellow(" \u26A0 Backend is configured but disabled in project settings."));
1368
+ console.log(chalk8.gray(" Enable backend framework at aerocoding.dev or run 'aerocoding init' to reconfigure."));
1369
+ }
1370
+ if (hasFrontendConfig && !projectHasFrontend) {
1371
+ console.log("");
1372
+ console.log(chalk8.yellow(" \u26A0 Frontend is configured but disabled in project settings."));
1373
+ console.log(chalk8.gray(" Enable frontend framework at aerocoding.dev or run 'aerocoding init' to reconfigure."));
1374
+ }
1375
+ const backendAvailable = hasBackendConfig && projectHasBackend;
1376
+ const frontendAvailable = hasFrontendConfig && projectHasFrontend;
1377
+ if (!backendAvailable && !frontendAvailable) {
1378
+ console.log(chalk8.red("\n No valid targets available."));
1379
+ console.log(chalk8.gray(" Check your project settings at aerocoding.dev and run 'aerocoding init'."));
1380
+ console.log("");
1381
+ process.exit(1);
1382
+ }
1383
+ const hasBothTargets = backendAvailable && frontendAvailable;
1384
+ let selectedTarget = "both";
1385
+ if (hasBothTargets && !options.all) {
1386
+ const targetChoice = await p.select({
1387
+ message: "Which target do you want to generate?",
1388
+ options: [
1389
+ {
1390
+ value: "both",
1391
+ label: "Both"
1392
+ },
1393
+ {
1394
+ value: "backend",
1395
+ label: `Backend (${config?.backend?.preset})`
1396
+ },
1397
+ {
1398
+ value: "frontend",
1399
+ label: `Frontend (${config?.frontend?.preset})`
1400
+ }
1401
+ ],
1402
+ initialValue: "both"
1403
+ });
1404
+ if (p.isCancel(targetChoice)) {
1405
+ console.log(chalk8.yellow("\n Generation cancelled\n"));
1406
+ process.exit(0);
1407
+ }
1408
+ selectedTarget = targetChoice;
1409
+ } else if (backendAvailable && !frontendAvailable) {
1410
+ selectedTarget = "backend";
1411
+ } else if (frontendAvailable && !backendAvailable) {
1412
+ selectedTarget = "frontend";
1413
+ }
1414
+ const diagrams = project.schema?.diagrams || [];
1415
+ const hasMultipleDiagrams = diagrams.length > 1;
1416
+ let selectedDiagramIds = diagrams.map((d) => d.id || d.name || "unknown");
1417
+ if (hasMultipleDiagrams && !options.all) {
1418
+ const diagramChoices = await p.multiselect({
1419
+ message: "Which bounded contexts do you want to generate?",
1420
+ options: diagrams.map((d) => ({
1421
+ value: d.id || d.name || "unknown",
1422
+ label: `${d.name || "Unnamed"} (${d.entities?.length || 0} entities)`
1423
+ })),
1424
+ initialValues: diagrams.map((d) => d.id || d.name || "unknown"),
1425
+ required: true
1426
+ });
1427
+ if (p.isCancel(diagramChoices)) {
1428
+ console.log(chalk8.yellow("\n Generation cancelled\n"));
1429
+ process.exit(0);
1430
+ }
1431
+ selectedDiagramIds = diagramChoices;
1432
+ }
1433
+ let templateId = null;
1434
+ let targets = [];
1435
+ let backendPreset;
1436
+ let frontendPreset;
1437
+ if (selectedTarget === "backend" || selectedTarget === "both") {
1438
+ backendPreset = options.backendPreset || config?.backend?.preset;
1439
+ if (backendPreset) {
1440
+ const tid = mapPresetToTemplateId(backendPreset);
1441
+ if (tid) templateId = tid;
1442
+ const target = mapPresetToTarget(backendPreset);
1443
+ if (target) targets.push(target);
1444
+ }
1445
+ }
1446
+ if (selectedTarget === "frontend" || selectedTarget === "both") {
1447
+ frontendPreset = options.frontendPreset || config?.frontend?.preset;
1448
+ if (frontendPreset) {
1449
+ const tid = mapPresetToTemplateId(frontendPreset);
1450
+ if (tid && !templateId) templateId = tid;
1451
+ const target = mapPresetToTarget(frontendPreset);
1452
+ if (target) targets.push(target);
1453
+ }
1454
+ }
1455
+ if (!templateId && (!targets || targets.length === 0)) {
1456
+ targets = options.targets || [];
1457
+ }
1458
+ const featureFlags = buildFeatureFlagsFromConfig(config);
1459
+ spinner5 = ora2({ text: "Checking credits...", color: "cyan" }).start();
829
1460
  const credits = await apiClient.getCreditUsage(project.organizationId);
830
1461
  const useContexts = config?.architectureStyle !== "flat";
1462
+ const estimateDiagramIds = selectedDiagramIds.length < diagrams.length ? selectedDiagramIds : void 0;
831
1463
  let estimate = null;
832
1464
  try {
833
1465
  if (templateId) {
@@ -836,7 +1468,8 @@ async function generateCommand(options) {
836
1468
  templateId,
837
1469
  options: {
838
1470
  featureFlags,
839
- useContexts
1471
+ useContexts,
1472
+ diagramIds: estimateDiagramIds
840
1473
  }
841
1474
  });
842
1475
  } else if (targets && targets.length > 0) {
@@ -847,15 +1480,14 @@ async function generateCommand(options) {
847
1480
  outputDir: output,
848
1481
  backendPreset,
849
1482
  frontendPreset,
850
- backendLayers,
851
- frontendLayers,
852
- useContexts
1483
+ useContexts,
1484
+ diagramIds: estimateDiagramIds
853
1485
  }
854
1486
  });
855
1487
  }
856
1488
  } catch {
857
1489
  }
858
- spinner2.succeed(chalk8.gray("Credits verified"));
1490
+ spinner5.succeed(chalk8.gray("Credits verified"));
859
1491
  if (estimate && estimate.entities === 0) {
860
1492
  console.log(chalk8.yellow("\n \u26A0 No entities found in this project."));
861
1493
  console.log(chalk8.gray(" Create entities in the diagram editor and save (Ctrl+S) before generating.\n"));
@@ -878,12 +1510,26 @@ async function generateCommand(options) {
878
1510
  } else {
879
1511
  console.log(chalk8.gray(" Mode:"), chalk8.gray("default"));
880
1512
  }
881
- if (backendPreset) {
1513
+ if (hasBothTargets) {
1514
+ const targetLabel = selectedTarget === "backend" ? "Backend only" : selectedTarget === "frontend" ? "Frontend only" : "Backend + Frontend";
1515
+ console.log(chalk8.gray(" Target:"), chalk8.cyan(targetLabel));
1516
+ }
1517
+ if (backendPreset && (selectedTarget === "backend" || selectedTarget === "both")) {
882
1518
  console.log(chalk8.gray(" Backend Preset:"), chalk8.cyan(backendPreset));
883
1519
  }
884
- if (frontendPreset) {
1520
+ if (frontendPreset && (selectedTarget === "frontend" || selectedTarget === "both")) {
885
1521
  console.log(chalk8.gray(" Frontend Preset:"), chalk8.cyan(frontendPreset));
886
1522
  }
1523
+ if (hasMultipleDiagrams) {
1524
+ const selectedCount = selectedDiagramIds.length;
1525
+ const totalCount = diagrams.length;
1526
+ if (selectedCount === totalCount) {
1527
+ console.log(chalk8.gray(" Bounded Contexts:"), chalk8.cyan(`All (${totalCount})`));
1528
+ } else {
1529
+ const selectedNames = diagrams.filter((d) => selectedDiagramIds.includes(d.id || d.name || "unknown")).map((d) => d.name || "Unnamed").join(", ");
1530
+ console.log(chalk8.gray(" Bounded Contexts:"), chalk8.cyan(`${selectedCount}/${totalCount} (${selectedNames})`));
1531
+ }
1532
+ }
887
1533
  const archStyle = config?.architectureStyle || "bounded-contexts";
888
1534
  console.log(
889
1535
  chalk8.gray(" Architecture:"),
@@ -902,26 +1548,32 @@ async function generateCommand(options) {
902
1548
  }
903
1549
  console.log(chalk8.gray(" Output:"), chalk8.cyan(output));
904
1550
  if (estimate && estimate.files && estimate.files.length > 0) {
905
- console.log("");
906
- console.log(chalk8.bold(" Files to Generate"));
907
- console.log(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"));
908
- const categories = categorizeFilePaths(estimate.files);
909
- const maxNameLength = Math.max(...categories.map((c) => c.name.length));
910
- for (const category of categories) {
911
- const padding = " ".repeat(maxNameLength - category.name.length + 2);
912
- const countStr = category.count.toString().padStart(3, " ");
913
- console.log(
914
- chalk8.gray(` ${category.name}${padding}`),
915
- chalk8.cyan(`${countStr} files`)
916
- );
1551
+ const { backend: backendFiles, frontend: frontendFiles } = categorizeFilesByTarget(estimate.files);
1552
+ if ((selectedTarget === "backend" || selectedTarget === "both") && backendFiles.length > 0) {
1553
+ console.log("");
1554
+ console.log(chalk8.bold(" Files to Generate (Backend)"));
1555
+ console.log(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"));
1556
+ displayFileCategories(backendFiles);
1557
+ console.log(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"));
1558
+ console.log(chalk8.gray(" Subtotal:"), chalk8.cyan(`${backendFiles.length} files`));
1559
+ }
1560
+ if ((selectedTarget === "frontend" || selectedTarget === "both") && frontendFiles.length > 0) {
1561
+ console.log("");
1562
+ console.log(chalk8.bold(" Files to Generate (Frontend)"));
1563
+ console.log(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"));
1564
+ displayFileCategories(frontendFiles);
1565
+ console.log(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"));
1566
+ console.log(chalk8.gray(" Subtotal:"), chalk8.cyan(`${frontendFiles.length} files`));
1567
+ }
1568
+ if (selectedTarget === "both" && backendFiles.length > 0 && frontendFiles.length > 0) {
1569
+ console.log("");
1570
+ console.log(chalk8.bold(" Total"));
1571
+ console.log(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"));
1572
+ console.log(chalk8.gray(" Backend:"), chalk8.cyan(`${backendFiles.length} files`));
1573
+ console.log(chalk8.gray(" Frontend:"), chalk8.cyan(`${frontendFiles.length} files`));
1574
+ console.log(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"));
1575
+ console.log(chalk8.white(" Total:"), chalk8.bold.cyan(`${estimate.totalFiles} files`));
917
1576
  }
918
- console.log(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"));
919
- const totalPadding = " ".repeat(maxNameLength - 5 + 2);
920
- const totalStr = estimate.totalFiles.toString().padStart(3, " ");
921
- console.log(
922
- chalk8.white(` Total${totalPadding}`),
923
- chalk8.bold.cyan(`${totalStr} files`)
924
- );
925
1577
  console.log(chalk8.gray(` Entities:`), chalk8.cyan(estimate.entities));
926
1578
  }
927
1579
  console.log("");
@@ -952,7 +1604,8 @@ async function generateCommand(options) {
952
1604
  }
953
1605
  }
954
1606
  console.log("");
955
- spinner2 = ora2({ text: "Generating code...", color: "cyan" }).start();
1607
+ spinner5 = ora2({ text: "Generating code...", color: "cyan" }).start();
1608
+ const diagramIds = selectedDiagramIds.length < diagrams.length ? selectedDiagramIds : void 0;
956
1609
  const generatePayload = templateId ? {
957
1610
  // NEW: Full architecture mode using template
958
1611
  projectId,
@@ -961,7 +1614,9 @@ async function generateCommand(options) {
961
1614
  includeValidations,
962
1615
  includeComments,
963
1616
  featureFlags,
964
- useContexts
1617
+ useContexts,
1618
+ diagramIds
1619
+ // Filter by selected bounded contexts
965
1620
  }
966
1621
  } : {
967
1622
  // Legacy mode using targets
@@ -975,14 +1630,14 @@ async function generateCommand(options) {
975
1630
  outputDir: output,
976
1631
  backendPreset,
977
1632
  frontendPreset,
978
- backendLayers,
979
- frontendLayers,
980
1633
  validationLib,
981
- useContexts
1634
+ useContexts,
1635
+ diagramIds
1636
+ // Filter by selected bounded contexts
982
1637
  }
983
1638
  };
984
1639
  const result = await apiClient.generateCode(generatePayload);
985
- spinner2.succeed(chalk8.green("Code generated successfully!"));
1640
+ spinner5.succeed(chalk8.green("Code generated successfully!"));
986
1641
  console.log("");
987
1642
  console.log(chalk8.bold(" Results"));
988
1643
  console.log(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"));
@@ -1014,11 +1669,16 @@ async function generateCommand(options) {
1014
1669
  }
1015
1670
  }
1016
1671
  console.log("");
1017
- await writeGeneratedFiles(result.files, output, options.verbose);
1018
- console.log(chalk8.green(` Files written to ${chalk8.white(output)}`));
1672
+ const organizedFiles = organizeFilesIntoFolders(result.files, selectedTarget);
1673
+ await writeGeneratedFiles(organizedFiles, output, options.verbose);
1674
+ if (selectedTarget === "both") {
1675
+ console.log(chalk8.green(` Files written to ${chalk8.white(output)}/backend and ${chalk8.white(output)}/frontend`));
1676
+ } else {
1677
+ console.log(chalk8.green(` Files written to ${chalk8.white(output)}/${selectedTarget}`));
1678
+ }
1019
1679
  console.log("");
1020
1680
  } catch (error) {
1021
- spinner2.fail(chalk8.red("Generation failed"));
1681
+ spinner5.fail(chalk8.red("Generation failed"));
1022
1682
  if (error.response?.status === 401) {
1023
1683
  await handleUnauthorized(tokenManager);
1024
1684
  } else if (error.response?.status === 403) {
@@ -1049,42 +1709,42 @@ async function generateCommand(options) {
1049
1709
  }
1050
1710
 
1051
1711
  // src/commands/init.ts
1052
- import * as p from "@clack/prompts";
1712
+ import * as p2 from "@clack/prompts";
1053
1713
  import chalk9 from "chalk";
1054
1714
  async function initCommand(options) {
1055
- p.intro(chalk9.bgCyan.black(" AeroCoding CLI "));
1715
+ p2.intro(chalk9.bgCyan.black(" AeroCoding CLI "));
1056
1716
  if (!options.force && await configExists()) {
1057
- const overwrite = await p.confirm({
1717
+ const overwrite = await p2.confirm({
1058
1718
  message: "Config file already exists. Overwrite?",
1059
1719
  initialValue: false
1060
1720
  });
1061
- if (p.isCancel(overwrite) || !overwrite) {
1062
- p.cancel("Operation cancelled.");
1721
+ if (p2.isCancel(overwrite) || !overwrite) {
1722
+ p2.cancel("Operation cancelled.");
1063
1723
  process.exit(0);
1064
1724
  }
1065
1725
  }
1066
1726
  const tokenManager = new TokenManager();
1067
1727
  const token = await tokenManager.getAccessToken();
1068
1728
  if (!token) {
1069
- p.cancel("Not logged in. Run 'aerocoding login' first.");
1729
+ p2.cancel("Not logged in. Run 'aerocoding login' first.");
1070
1730
  process.exit(1);
1071
1731
  }
1072
1732
  const apiClient = createApiClientWithAutoLogout(token, tokenManager);
1073
1733
  try {
1074
- const orgSpinner = p.spinner();
1734
+ const orgSpinner = p2.spinner();
1075
1735
  orgSpinner.start("Loading organizations...");
1076
1736
  const organizations = await apiClient.listOrganizations();
1077
1737
  orgSpinner.stop("Organizations loaded");
1078
1738
  if (organizations.length === 0) {
1079
- p.cancel("No organizations found. Create one on aerocoding.dev first.");
1739
+ p2.cancel("No organizations found. Create one on aerocoding.dev first.");
1080
1740
  process.exit(1);
1081
1741
  }
1082
1742
  let organizationId;
1083
1743
  if (organizations.length === 1 && organizations[0]) {
1084
1744
  organizationId = organizations[0].id;
1085
- p.log.info(`Organization: ${organizations[0].name}`);
1745
+ p2.log.info(`Organization: ${organizations[0].name}`);
1086
1746
  } else {
1087
- const selectedOrg = await p.select({
1747
+ const selectedOrg = await p2.select({
1088
1748
  message: "Select organization",
1089
1749
  options: organizations.map((org) => ({
1090
1750
  value: org.id,
@@ -1092,23 +1752,23 @@ async function initCommand(options) {
1092
1752
  hint: org.planTier.toUpperCase()
1093
1753
  }))
1094
1754
  });
1095
- if (p.isCancel(selectedOrg)) {
1096
- p.cancel("Operation cancelled.");
1755
+ if (p2.isCancel(selectedOrg)) {
1756
+ p2.cancel("Operation cancelled.");
1097
1757
  process.exit(0);
1098
1758
  }
1099
1759
  organizationId = selectedOrg;
1100
1760
  }
1101
1761
  let projectId = options.project;
1102
1762
  if (!projectId) {
1103
- const spinner3 = p.spinner();
1104
- spinner3.start("Loading projects...");
1763
+ const spinner6 = p2.spinner();
1764
+ spinner6.start("Loading projects...");
1105
1765
  const projects = await apiClient.listProjects(organizationId);
1106
- spinner3.stop("Projects loaded");
1766
+ spinner6.stop("Projects loaded");
1107
1767
  if (projects.length === 0) {
1108
- p.cancel("No projects in this organization. Create one on aerocoding.dev first.");
1768
+ p2.cancel("No projects in this organization. Create one on aerocoding.dev first.");
1109
1769
  process.exit(1);
1110
1770
  }
1111
- const selectedProject = await p.select({
1771
+ const selectedProject = await p2.select({
1112
1772
  message: "Select project",
1113
1773
  options: projects.map((proj) => ({
1114
1774
  value: proj.id,
@@ -1116,43 +1776,28 @@ async function initCommand(options) {
1116
1776
  hint: [proj.backendFramework, proj.frontendFramework].filter(Boolean).join(" + ")
1117
1777
  }))
1118
1778
  });
1119
- if (p.isCancel(selectedProject)) {
1120
- p.cancel("Operation cancelled.");
1779
+ if (p2.isCancel(selectedProject)) {
1780
+ p2.cancel("Operation cancelled.");
1121
1781
  process.exit(0);
1122
1782
  }
1123
1783
  projectId = selectedProject;
1124
1784
  }
1125
- const spinner2 = p.spinner();
1126
- spinner2.start("Fetching project details...");
1785
+ const spinner5 = p2.spinner();
1786
+ spinner5.start("Fetching project details...");
1127
1787
  const project = await apiClient.getProject(projectId);
1128
- spinner2.stop(`Project: ${project.name}`);
1129
- const targetOptions = [];
1130
- if (project.backendFramework) {
1131
- targetOptions.push({
1132
- value: "backend",
1133
- label: `Backend (${project.backendFramework})`
1134
- });
1135
- }
1136
- if (project.frontendFramework) {
1137
- targetOptions.push({
1138
- value: "frontend",
1139
- label: `Frontend (${project.frontendFramework})`
1140
- });
1141
- }
1142
- if (targetOptions.length === 0) {
1143
- p.cancel("Project has no frameworks configured. Update it on aerocoding.dev first.");
1788
+ spinner5.stop(`Project: ${project.name}`);
1789
+ const hasBackend = !!project.backendFramework;
1790
+ const hasFrontend = !!project.frontendFramework;
1791
+ if (!hasBackend && !hasFrontend) {
1792
+ p2.cancel("Project has no frameworks configured. Update it on aerocoding.dev first.");
1144
1793
  process.exit(1);
1145
1794
  }
1146
- const targets = await p.multiselect({
1147
- message: "What do you want to generate?",
1148
- options: targetOptions,
1149
- required: true
1150
- });
1151
- if (p.isCancel(targets)) {
1152
- p.cancel("Operation cancelled.");
1153
- process.exit(0);
1795
+ if (hasBackend) {
1796
+ p2.log.success(`Backend: ${project.backendFramework?.toUpperCase()} detected`);
1797
+ }
1798
+ if (hasFrontend) {
1799
+ p2.log.success(`Frontend: ${project.frontendFramework?.toUpperCase()} detected`);
1154
1800
  }
1155
- const selectedTargets = targets;
1156
1801
  const config = {
1157
1802
  project: projectId,
1158
1803
  output: "./.aerocoding",
@@ -1167,8 +1812,8 @@ async function initCommand(options) {
1167
1812
  };
1168
1813
  let selectedBackendTemplate = null;
1169
1814
  let selectedFrontendTemplate = null;
1170
- if (selectedTargets.includes("backend") && project.backendFramework) {
1171
- const archSpinner = p.spinner();
1815
+ if (hasBackend && project.backendFramework) {
1816
+ const archSpinner = p2.spinner();
1172
1817
  archSpinner.start("Loading backend templates...");
1173
1818
  const templateResult = await apiClient.getTemplates({
1174
1819
  category: "backend",
@@ -1176,7 +1821,7 @@ async function initCommand(options) {
1176
1821
  });
1177
1822
  archSpinner.stop("Templates loaded");
1178
1823
  if (templateResult.templates.length > 0) {
1179
- const preset = await p.select({
1824
+ const preset = await p2.select({
1180
1825
  message: "Backend template",
1181
1826
  options: templateResult.templates.map((tmpl) => ({
1182
1827
  value: tmpl.id,
@@ -1184,40 +1829,19 @@ async function initCommand(options) {
1184
1829
  hint: tmpl.description || `${tmpl.tier} tier`
1185
1830
  }))
1186
1831
  });
1187
- if (p.isCancel(preset)) {
1188
- p.cancel("Operation cancelled.");
1832
+ if (p2.isCancel(preset)) {
1833
+ p2.cancel("Operation cancelled.");
1189
1834
  process.exit(0);
1190
1835
  }
1191
1836
  const fullTemplate = await apiClient.getTemplate(preset);
1192
1837
  selectedBackendTemplate = fullTemplate;
1193
- if (fullTemplate.layers && fullTemplate.layers.length > 0) {
1194
- const layers = await p.multiselect({
1195
- message: "Select backend layers to include",
1196
- options: fullTemplate.layers.map((layer) => ({
1197
- value: layer.id,
1198
- label: layer.name,
1199
- hint: layer.category
1200
- })),
1201
- initialValues: fullTemplate.layers.filter((l) => l.enabled).map((l) => l.id)
1202
- });
1203
- if (p.isCancel(layers)) {
1204
- p.cancel("Operation cancelled.");
1205
- process.exit(0);
1206
- }
1207
- config.backend = {
1208
- preset,
1209
- layers
1210
- };
1211
- } else {
1212
- config.backend = {
1213
- preset,
1214
- layers: []
1215
- };
1216
- }
1838
+ config.backend = {
1839
+ preset
1840
+ };
1217
1841
  }
1218
1842
  }
1219
- if (selectedTargets.includes("frontend") && project.frontendFramework) {
1220
- const archSpinner = p.spinner();
1843
+ if (hasFrontend && project.frontendFramework) {
1844
+ const archSpinner = p2.spinner();
1221
1845
  archSpinner.start("Loading frontend templates...");
1222
1846
  const templateResult = await apiClient.getTemplates({
1223
1847
  category: "frontend",
@@ -1225,7 +1849,7 @@ async function initCommand(options) {
1225
1849
  });
1226
1850
  archSpinner.stop("Templates loaded");
1227
1851
  if (templateResult.templates.length > 0) {
1228
- const preset = await p.select({
1852
+ const preset = await p2.select({
1229
1853
  message: "Frontend template",
1230
1854
  options: templateResult.templates.map((tmpl) => ({
1231
1855
  value: tmpl.id,
@@ -1233,79 +1857,52 @@ async function initCommand(options) {
1233
1857
  hint: tmpl.description || `${tmpl.tier} tier`
1234
1858
  }))
1235
1859
  });
1236
- if (p.isCancel(preset)) {
1237
- p.cancel("Operation cancelled.");
1860
+ if (p2.isCancel(preset)) {
1861
+ p2.cancel("Operation cancelled.");
1238
1862
  process.exit(0);
1239
1863
  }
1240
1864
  const fullTemplate = await apiClient.getTemplate(preset);
1241
1865
  selectedFrontendTemplate = fullTemplate;
1242
- if (fullTemplate.layers && fullTemplate.layers.length > 0) {
1243
- const layers = await p.multiselect({
1244
- message: "Select frontend layers to include",
1245
- options: fullTemplate.layers.map((layer) => ({
1246
- value: layer.id,
1247
- label: layer.name,
1248
- hint: layer.category
1249
- })),
1250
- initialValues: fullTemplate.layers.filter((l) => l.enabled).map((l) => l.id)
1251
- });
1252
- if (p.isCancel(layers)) {
1253
- p.cancel("Operation cancelled.");
1254
- process.exit(0);
1255
- }
1256
- config.frontend = {
1257
- preset,
1258
- layers
1259
- };
1260
- } else {
1261
- config.frontend = {
1262
- preset,
1263
- layers: []
1264
- };
1265
- }
1866
+ config.frontend = {
1867
+ preset
1868
+ };
1266
1869
  }
1267
1870
  }
1268
1871
  const templateArchStyle = selectedBackendTemplate?.architectureStyle || selectedFrontendTemplate?.architectureStyle;
1269
- if (templateArchStyle) {
1270
- config.architectureStyle = templateArchStyle;
1271
- p.log.info(
1272
- `Architecture: ${templateArchStyle === "bounded-contexts" ? "Bounded Contexts (defined by template)" : "Flat Structure (defined by template)"}`
1273
- );
1274
- } else {
1275
- const architectureStyle = await p.select({
1276
- message: "How would you like to organize your code?",
1277
- options: [
1278
- {
1279
- value: "flat",
1280
- label: "Flat Structure (Recommended)",
1281
- hint: "Single Domain, Application, Infrastructure for the entire project"
1282
- },
1283
- {
1284
- value: "bounded-contexts",
1285
- label: "Bounded Contexts",
1286
- hint: "Each module has its own Domain, Application, Infrastructure"
1287
- }
1288
- ],
1289
- initialValue: "flat"
1290
- });
1291
- if (p.isCancel(architectureStyle)) {
1292
- p.cancel("Operation cancelled.");
1293
- process.exit(0);
1294
- }
1295
- config.architectureStyle = architectureStyle;
1872
+ const recommendedStyle = templateArchStyle || "flat";
1873
+ const architectureStyle = await p2.select({
1874
+ message: "How would you like to organize your code?",
1875
+ options: [
1876
+ {
1877
+ value: "bounded-contexts",
1878
+ label: recommendedStyle === "bounded-contexts" ? "Bounded Contexts (Recommended)" : "Bounded Contexts",
1879
+ hint: "Each module has its own Domain, Application, Infrastructure"
1880
+ },
1881
+ {
1882
+ value: "flat",
1883
+ label: recommendedStyle === "flat" ? "Flat Structure (Recommended)" : "Flat Structure",
1884
+ hint: "Single Domain, Application, Infrastructure for the entire project"
1885
+ }
1886
+ ],
1887
+ initialValue: recommendedStyle
1888
+ });
1889
+ if (p2.isCancel(architectureStyle)) {
1890
+ p2.cancel("Operation cancelled.");
1891
+ process.exit(0);
1296
1892
  }
1297
- const codeStyleOptions = await p.multiselect({
1893
+ config.architectureStyle = architectureStyle;
1894
+ const codeStyleOptions = await p2.multiselect({
1298
1895
  message: "Code style options",
1299
1896
  options: [
1300
- { value: "validations", label: "Include validations", hint: "Add validation rules" },
1301
- { value: "comments", label: "Include comments", hint: "Add code documentation" },
1302
- { value: "logging", label: "Include logging", hint: "Add log statements" },
1303
- { value: "testing", label: "Include tests", hint: "Generate test files" }
1897
+ { value: "validations", label: "Include validations (Recommended)", hint: "Add validation rules" },
1898
+ { value: "comments", label: "Include comments (Recommended)", hint: "Add code documentation" },
1899
+ { value: "logging", label: "Include logging (Recommended)", hint: "Add log statements" },
1900
+ { value: "testing", label: "Include tests (Recommended)", hint: "Generate test files" }
1304
1901
  ],
1305
1902
  initialValues: ["validations", "comments", "logging", "testing"]
1306
1903
  });
1307
- if (p.isCancel(codeStyleOptions)) {
1308
- p.cancel("Operation cancelled.");
1904
+ if (p2.isCancel(codeStyleOptions)) {
1905
+ p2.cancel("Operation cancelled.");
1309
1906
  process.exit(0);
1310
1907
  }
1311
1908
  const selectedStyles = codeStyleOptions;
@@ -1315,62 +1912,848 @@ async function initCommand(options) {
1315
1912
  includeLogging: selectedStyles.includes("logging"),
1316
1913
  includeTesting: selectedStyles.includes("testing")
1317
1914
  };
1318
- const output = await p.text({
1319
- message: "Output directory",
1320
- initialValue: "./.aerocoding",
1915
+ config.output = "./.aerocoding";
1916
+ p2.log.step(chalk9.bold("Configuration Summary:"));
1917
+ if (config.backend) {
1918
+ p2.log.info(` Backend: ${config.backend.preset}`);
1919
+ }
1920
+ if (config.frontend) {
1921
+ p2.log.info(` Frontend: ${config.frontend.preset}`);
1922
+ }
1923
+ p2.log.info(
1924
+ ` Architecture: ${config.architectureStyle === "bounded-contexts" ? "Bounded Contexts" : "Flat Structure"}`
1925
+ );
1926
+ p2.log.info(` Output: ${config.output}`);
1927
+ await saveConfig(config);
1928
+ p2.outro(
1929
+ chalk9.green("Config saved to .aerocodingrc.json") + "\n\n" + chalk9.gray(" Run ") + chalk9.cyan("aerocoding generate") + chalk9.gray(" to generate code!")
1930
+ );
1931
+ } catch (error) {
1932
+ if (error.response?.status === 401) {
1933
+ await handleUnauthorized(tokenManager);
1934
+ } else if (error.response?.data?.message) {
1935
+ p2.cancel(error.response.data.message);
1936
+ } else {
1937
+ p2.cancel(error.message || "An unexpected error occurred");
1938
+ }
1939
+ process.exit(1);
1940
+ }
1941
+ }
1942
+
1943
+ // src/commands/create.ts
1944
+ import * as p3 from "@clack/prompts";
1945
+ import chalk10 from "chalk";
1946
+ import ora3 from "ora";
1947
+ import { mkdir, access as access3 } from "fs/promises";
1948
+ import { resolve } from "path";
1949
+
1950
+ // src/config/project-config.ts
1951
+ import { z as z2 } from "zod";
1952
+ import { readFile as readFile2, writeFile as writeFile3, access as access2 } from "fs/promises";
1953
+ import { join as join2 } from "path";
1954
+ var PROJECT_CONFIG_FILENAME = "aerocoding.json";
1955
+ var projectConfigSchema = z2.object({
1956
+ $schema: z2.string().optional().default("https://aerocoding.dev/schema.json"),
1957
+ /** Project UUID from aerocoding.dev */
1958
+ projectId: z2.string().uuid(),
1959
+ /** Template ID for architecture generation */
1960
+ templateId: z2.string().min(1),
1961
+ /** Template version at project creation */
1962
+ templateVersion: z2.string().optional(),
1963
+ /** Root namespace/package name */
1964
+ namespace: z2.string().min(1),
1965
+ /** Output directories */
1966
+ output: z2.object({
1967
+ backend: z2.string().default("./backend"),
1968
+ frontend: z2.string().default("./frontend")
1969
+ }).optional().default({ backend: "./backend", frontend: "./frontend" }),
1970
+ /** Organization ID (for cloud sync) */
1971
+ organizationId: z2.string().uuid().optional()
1972
+ });
1973
+ async function loadProjectConfig(dir = process.cwd()) {
1974
+ try {
1975
+ const configPath = join2(dir, PROJECT_CONFIG_FILENAME);
1976
+ const content = await readFile2(configPath, "utf-8");
1977
+ const parsed = JSON.parse(content);
1978
+ return projectConfigSchema.parse(parsed);
1979
+ } catch {
1980
+ return null;
1981
+ }
1982
+ }
1983
+ async function saveProjectConfig(config, dir = process.cwd()) {
1984
+ const configPath = join2(dir, PROJECT_CONFIG_FILENAME);
1985
+ const content = JSON.stringify(config, null, 2);
1986
+ await writeFile3(configPath, content, "utf-8");
1987
+ }
1988
+ function createProjectConfig(options) {
1989
+ return {
1990
+ $schema: "https://aerocoding.dev/schema.json",
1991
+ projectId: options.projectId,
1992
+ templateId: options.templateId,
1993
+ templateVersion: options.templateVersion,
1994
+ namespace: options.namespace,
1995
+ organizationId: options.organizationId,
1996
+ output: {
1997
+ backend: options.output?.backend || "./backend",
1998
+ frontend: options.output?.frontend || "./frontend"
1999
+ }
2000
+ };
2001
+ }
2002
+
2003
+ // src/utils/cloud-sync.ts
2004
+ function toCloudManifest(manifest) {
2005
+ return {
2006
+ version: manifest.version,
2007
+ lastSync: manifest.lastSync,
2008
+ templateVersion: manifest.templateVersion,
2009
+ files: manifest.files
2010
+ };
2011
+ }
2012
+ function fromCloudManifest(cloudManifest, templateVersion) {
2013
+ return {
2014
+ version: cloudManifest.version,
2015
+ lastSync: cloudManifest.lastSync || (/* @__PURE__ */ new Date()).toISOString(),
2016
+ templateVersion: cloudManifest.templateVersion || templateVersion,
2017
+ files: cloudManifest.files,
2018
+ entities: []
2019
+ // Will be rebuilt on next generation
2020
+ };
2021
+ }
2022
+ async function syncToCloud(apiClient, projectId, manifest) {
2023
+ const cloudManifest = toCloudManifest(manifest);
2024
+ const result = await apiClient.saveManifest(projectId, cloudManifest);
2025
+ return {
2026
+ success: result.success,
2027
+ fileCount: result.fileCount
2028
+ };
2029
+ }
2030
+ async function fetchFromCloud(apiClient, projectId, templateVersion) {
2031
+ const cloudManifest = await apiClient.getManifest(projectId);
2032
+ if (!cloudManifest) {
2033
+ return null;
2034
+ }
2035
+ return fromCloudManifest(cloudManifest, templateVersion);
2036
+ }
2037
+ async function compareWithCloud(apiClient, projectId, localManifest) {
2038
+ const cloudManifest = await apiClient.getManifest(projectId);
2039
+ const cloudLastSync = cloudManifest?.lastSync || null;
2040
+ const localLastSync = localManifest?.lastSync || null;
2041
+ const cloudFileCount = cloudManifest ? Object.keys(cloudManifest.files).length : 0;
2042
+ const localFileCount = localManifest ? Object.keys(localManifest.files).length : 0;
2043
+ let isCloudNewer = false;
2044
+ if (cloudLastSync && localLastSync) {
2045
+ isCloudNewer = new Date(cloudLastSync) > new Date(localLastSync);
2046
+ } else if (cloudLastSync && !localLastSync) {
2047
+ isCloudNewer = true;
2048
+ }
2049
+ return {
2050
+ hasCloudBackup: cloudManifest !== null,
2051
+ cloudLastSync,
2052
+ localLastSync,
2053
+ isCloudNewer,
2054
+ cloudFileCount,
2055
+ localFileCount
2056
+ };
2057
+ }
2058
+ function formatSyncDate(isoDate) {
2059
+ if (!isoDate) return "never";
2060
+ const date = new Date(isoDate);
2061
+ const now = /* @__PURE__ */ new Date();
2062
+ const diffMs = now.getTime() - date.getTime();
2063
+ const diffMins = Math.floor(diffMs / 6e4);
2064
+ const diffHours = Math.floor(diffMs / 36e5);
2065
+ const diffDays = Math.floor(diffMs / 864e5);
2066
+ if (diffMins < 1) return "just now";
2067
+ if (diffMins < 60) return `${diffMins}m ago`;
2068
+ if (diffHours < 24) return `${diffHours}h ago`;
2069
+ if (diffDays < 7) return `${diffDays}d ago`;
2070
+ return date.toLocaleDateString();
2071
+ }
2072
+
2073
+ // src/commands/create.ts
2074
+ async function createCommand(projectName, options) {
2075
+ p3.intro(chalk10.bgCyan.black(" AeroCoding Create "));
2076
+ if (!projectName || projectName.trim() === "") {
2077
+ p3.cancel("Project name is required");
2078
+ process.exit(1);
2079
+ }
2080
+ const safeName = projectName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
2081
+ if (!safeName) {
2082
+ p3.cancel("Invalid project name. Use alphanumeric characters and hyphens.");
2083
+ process.exit(1);
2084
+ }
2085
+ const projectDir = resolve(process.cwd(), safeName);
2086
+ try {
2087
+ await access3(projectDir);
2088
+ if (!options.force) {
2089
+ const overwrite = await p3.confirm({
2090
+ message: `Directory '${safeName}' already exists. Overwrite?`,
2091
+ initialValue: false
2092
+ });
2093
+ if (p3.isCancel(overwrite) || !overwrite) {
2094
+ p3.cancel("Operation cancelled.");
2095
+ process.exit(0);
2096
+ }
2097
+ }
2098
+ } catch {
2099
+ }
2100
+ const tokenManager = new TokenManager();
2101
+ const token = await tokenManager.getAccessToken();
2102
+ if (!token) {
2103
+ p3.cancel("Not logged in. Run 'aerocoding login' first.");
2104
+ process.exit(1);
2105
+ }
2106
+ const apiClient = createApiClientWithAutoLogout(token, tokenManager);
2107
+ try {
2108
+ const orgSpinner = p3.spinner();
2109
+ orgSpinner.start("Loading organizations...");
2110
+ const organizations = await apiClient.listOrganizations();
2111
+ orgSpinner.stop("Organizations loaded");
2112
+ if (organizations.length === 0) {
2113
+ p3.cancel("No organizations found. Create one on aerocoding.dev first.");
2114
+ process.exit(1);
2115
+ }
2116
+ let organizationId;
2117
+ if (organizations.length === 1 && organizations[0]) {
2118
+ organizationId = organizations[0].id;
2119
+ p3.log.info(`Organization: ${organizations[0].name}`);
2120
+ } else {
2121
+ const selectedOrg = await p3.select({
2122
+ message: "Select organization",
2123
+ options: organizations.map((org) => ({
2124
+ value: org.id,
2125
+ label: org.name,
2126
+ hint: org.planTier.toUpperCase()
2127
+ }))
2128
+ });
2129
+ if (p3.isCancel(selectedOrg)) {
2130
+ p3.cancel("Operation cancelled.");
2131
+ process.exit(0);
2132
+ }
2133
+ organizationId = selectedOrg;
2134
+ }
2135
+ let projectId = options.project;
2136
+ if (!projectId) {
2137
+ const spinner5 = p3.spinner();
2138
+ spinner5.start("Loading projects...");
2139
+ const projects = await apiClient.listProjects(organizationId);
2140
+ spinner5.stop("Projects loaded");
2141
+ if (projects.length === 0) {
2142
+ p3.cancel("No projects in this organization. Create one on aerocoding.dev first.");
2143
+ process.exit(1);
2144
+ }
2145
+ const selectedProject = await p3.select({
2146
+ message: "Select project to generate from",
2147
+ options: projects.map((proj) => ({
2148
+ value: proj.id,
2149
+ label: proj.name,
2150
+ hint: [proj.backendFramework, proj.frontendFramework].filter(Boolean).join(" + ")
2151
+ }))
2152
+ });
2153
+ if (p3.isCancel(selectedProject)) {
2154
+ p3.cancel("Operation cancelled.");
2155
+ process.exit(0);
2156
+ }
2157
+ projectId = selectedProject;
2158
+ }
2159
+ const projectSpinner = p3.spinner();
2160
+ projectSpinner.start("Fetching project details...");
2161
+ const project = await apiClient.getProject(projectId);
2162
+ projectSpinner.stop(`Project: ${project.name}`);
2163
+ let templateId = options.template;
2164
+ if (!templateId) {
2165
+ const templateSpinner = p3.spinner();
2166
+ templateSpinner.start("Loading templates...");
2167
+ const templateResult = await apiClient.getTemplates({
2168
+ category: "backend",
2169
+ language: project.backendFramework || void 0
2170
+ });
2171
+ templateSpinner.stop("Templates loaded");
2172
+ if (templateResult.templates.length === 0) {
2173
+ p3.cancel("No templates available for this project's framework.");
2174
+ process.exit(1);
2175
+ }
2176
+ const selectedTemplate = await p3.select({
2177
+ message: "Select architecture template",
2178
+ options: templateResult.templates.map((tmpl) => ({
2179
+ value: tmpl.id,
2180
+ label: tmpl.name,
2181
+ hint: tmpl.description || `${tmpl.tier} tier`
2182
+ }))
2183
+ });
2184
+ if (p3.isCancel(selectedTemplate)) {
2185
+ p3.cancel("Operation cancelled.");
2186
+ process.exit(0);
2187
+ }
2188
+ templateId = selectedTemplate;
2189
+ }
2190
+ const namespace = await p3.text({
2191
+ message: "Root namespace/package name",
2192
+ placeholder: "MegaStore",
2193
+ initialValue: toPascalCase(project.name),
1321
2194
  validate: (value) => {
1322
- if (!value) return "Output directory is required";
2195
+ if (!value || value.trim() === "") return "Namespace is required";
2196
+ if (!/^[A-Za-z][A-Za-z0-9]*$/.test(value)) {
2197
+ return "Namespace must start with a letter and contain only alphanumeric characters";
2198
+ }
1323
2199
  return void 0;
1324
2200
  }
1325
2201
  });
1326
- if (p.isCancel(output)) {
1327
- p.cancel("Operation cancelled.");
2202
+ if (p3.isCancel(namespace)) {
2203
+ p3.cancel("Operation cancelled.");
1328
2204
  process.exit(0);
1329
2205
  }
1330
- config.output = output;
1331
- await saveConfig(config);
1332
- p.outro(
1333
- chalk9.green("Config saved to .aerocodingrc.json") + "\n\n" + chalk9.gray(" Run ") + chalk9.cyan("aerocoding generate") + chalk9.gray(" to generate code!")
2206
+ p3.log.step(chalk10.bold("Configuration Summary:"));
2207
+ p3.log.info(` Directory: ${safeName}/`);
2208
+ p3.log.info(` Project: ${project.name}`);
2209
+ p3.log.info(` Template: ${templateId}`);
2210
+ p3.log.info(` Namespace: ${namespace}`);
2211
+ const proceed = await p3.confirm({
2212
+ message: "Create project with these settings?",
2213
+ initialValue: true
2214
+ });
2215
+ if (p3.isCancel(proceed) || !proceed) {
2216
+ p3.cancel("Operation cancelled.");
2217
+ process.exit(0);
2218
+ }
2219
+ const dirSpinner = p3.spinner();
2220
+ dirSpinner.start(`Creating ${safeName}/...`);
2221
+ await mkdir(projectDir, { recursive: true });
2222
+ dirSpinner.stop(`Created ${safeName}/`);
2223
+ const genSpinner = ora3({ text: "Generating architecture...", color: "cyan" }).start();
2224
+ const result = await apiClient.generateCode({
2225
+ projectId,
2226
+ templateId,
2227
+ options: {
2228
+ includeValidations: true,
2229
+ includeComments: true,
2230
+ featureFlags: {
2231
+ includeDtos: true,
2232
+ includeUseCases: true,
2233
+ includeMappers: true,
2234
+ includeControllers: true,
2235
+ includeEfConfig: true,
2236
+ includeValidation: true,
2237
+ includeDtoValidation: true,
2238
+ includeUnitTests: true,
2239
+ includeIntegrationTests: true,
2240
+ includeStarterFiles: false
2241
+ },
2242
+ useContexts: true
2243
+ }
2244
+ });
2245
+ genSpinner.succeed(chalk10.green(`Generated ${result.files?.length || 0} files`));
2246
+ const writeSpinner = p3.spinner();
2247
+ writeSpinner.start("Writing files...");
2248
+ const organizedFiles = result.files.map((file) => ({
2249
+ ...file,
2250
+ path: `backend/${file.path}`
2251
+ }));
2252
+ await writeGeneratedFiles(organizedFiles, projectDir, false);
2253
+ writeSpinner.stop(`Wrote ${organizedFiles.length} files`);
2254
+ const configSpinner = p3.spinner();
2255
+ configSpinner.start("Creating config files...");
2256
+ const projectConfig = createProjectConfig({
2257
+ projectId,
2258
+ templateId,
2259
+ templateVersion: "1.0.0",
2260
+ // TODO: get from template
2261
+ namespace,
2262
+ organizationId,
2263
+ output: { backend: "./backend", frontend: "./frontend" }
2264
+ });
2265
+ await saveProjectConfig(projectConfig, projectDir);
2266
+ let manifest = createEmptyManifest("1.0.0");
2267
+ for (const file of organizedFiles) {
2268
+ const hash = hashString(file.content);
2269
+ manifest = setManifestFile(manifest, file.path, {
2270
+ hash,
2271
+ mtime: Date.now(),
2272
+ size: Buffer.byteLength(file.content, "utf-8"),
2273
+ type: detectFileType(file.path)
2274
+ });
2275
+ }
2276
+ await writeManifest(projectDir, manifest);
2277
+ configSpinner.stop("Config files created");
2278
+ try {
2279
+ const syncResult = await syncToCloud(apiClient, projectId, manifest);
2280
+ if (syncResult.success) {
2281
+ console.log(
2282
+ chalk10.gray(" \u2713 Manifest synced to cloud") + chalk10.gray(` (${syncResult.fileCount} files)`)
2283
+ );
2284
+ }
2285
+ } catch {
2286
+ console.log(chalk10.yellow(" \u26A0 Could not sync manifest to cloud"));
2287
+ }
2288
+ console.log("");
2289
+ console.log(chalk10.bold(" Project Created Successfully!"));
2290
+ console.log(chalk10.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"));
2291
+ console.log(chalk10.gray(" Directory:"), chalk10.cyan(safeName + "/"));
2292
+ console.log(chalk10.gray(" Files:"), chalk10.cyan(organizedFiles.length));
2293
+ console.log(chalk10.gray(" Config:"), chalk10.cyan(PROJECT_CONFIG_FILENAME));
2294
+ console.log(chalk10.gray(" Manifest:"), chalk10.cyan(MANIFEST_FILENAME));
2295
+ if (result.creditsUsed !== void 0) {
2296
+ console.log("");
2297
+ console.log(chalk10.gray(" Credits used:"), chalk10.yellow(result.creditsUsed));
2298
+ console.log(chalk10.gray(" Credits remaining:"), chalk10.green(result.creditsRemaining));
2299
+ }
2300
+ p3.outro(
2301
+ chalk10.green("Project ready!") + "\n\n" + chalk10.gray(" Next steps:\n") + chalk10.cyan(` cd ${safeName}
2302
+ `) + chalk10.gray(" # Make changes in the diagram editor\n") + chalk10.cyan(" aerocoding update") + chalk10.gray(" # Sync changes incrementally")
1334
2303
  );
1335
2304
  } catch (error) {
1336
- if (error.response?.status === 401) {
2305
+ const err = error;
2306
+ if (err.response?.status === 401) {
1337
2307
  await handleUnauthorized(tokenManager);
1338
- } else if (error.response?.data?.message) {
1339
- p.cancel(error.response.data.message);
2308
+ } else if (err.response?.data?.message) {
2309
+ p3.cancel(err.response.data.message);
2310
+ } else {
2311
+ p3.cancel(err.message || "An unexpected error occurred");
2312
+ }
2313
+ process.exit(1);
2314
+ }
2315
+ }
2316
+ function toPascalCase(str) {
2317
+ return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("").replace(/[^a-zA-Z0-9]/g, "");
2318
+ }
2319
+ function detectFileType(filePath) {
2320
+ const lower = filePath.toLowerCase();
2321
+ if (lower.includes("/entities/") || lower.includes("/domain/")) return "entity";
2322
+ if (lower.includes("/usecases/") || lower.includes("/application/")) return "usecase";
2323
+ if (lower.includes("/repositories/")) return "repository";
2324
+ if (lower.includes("/controllers/") || lower.includes("/api/")) return "controller";
2325
+ if (lower.includes("/dtos/") || lower.includes("/dto/")) return "dto";
2326
+ if (lower.includes(".test.") || lower.includes(".spec.") || lower.includes("/tests/")) return "test";
2327
+ if (lower.includes("/config/") || lower.includes("appsettings") || lower.includes(".csproj")) return "config";
2328
+ return "other";
2329
+ }
2330
+
2331
+ // src/commands/update.ts
2332
+ import * as p4 from "@clack/prompts";
2333
+ import chalk11 from "chalk";
2334
+ import ora4 from "ora";
2335
+ async function updateCommand(options) {
2336
+ p4.intro(chalk11.bgCyan.black(" AeroCoding Update "));
2337
+ const config = await loadProjectConfig();
2338
+ if (!config) {
2339
+ p4.cancel(
2340
+ `No ${PROJECT_CONFIG_FILENAME} found. Run 'aerocoding create' first.`
2341
+ );
2342
+ process.exit(1);
2343
+ }
2344
+ const tokenManager = new TokenManager();
2345
+ const token = await tokenManager.getAccessToken();
2346
+ if (!token) {
2347
+ p4.cancel("Not logged in. Run 'aerocoding login' first.");
2348
+ process.exit(1);
2349
+ }
2350
+ const apiClient = createApiClientWithAutoLogout(token, tokenManager);
2351
+ try {
2352
+ let manifest = await readManifest(process.cwd());
2353
+ let fileCount = manifest ? Object.keys(manifest.files).length : 0;
2354
+ if (manifest) {
2355
+ p4.log.info(
2356
+ `Found manifest with ${fileCount} tracked files (last sync: ${manifest.lastSync || "unknown"})`
2357
+ );
2358
+ } else {
2359
+ p4.log.warn("No local manifest found.");
2360
+ const cloudCheck = await compareWithCloud(apiClient, config.projectId, null);
2361
+ if (cloudCheck.hasCloudBackup) {
2362
+ p4.log.info(
2363
+ chalk11.cyan(
2364
+ ` Cloud backup available (${cloudCheck.cloudFileCount} files, synced ${formatSyncDate(cloudCheck.cloudLastSync)})`
2365
+ )
2366
+ );
2367
+ const recovery = await p4.select({
2368
+ message: "How would you like to proceed?",
2369
+ options: [
2370
+ {
2371
+ value: "restore",
2372
+ label: "Restore from cloud backup",
2373
+ hint: "recommended"
2374
+ },
2375
+ {
2376
+ value: "continue",
2377
+ label: "Continue anyway",
2378
+ hint: "all files will be treated as new"
2379
+ },
2380
+ { value: "cancel", label: "Cancel" }
2381
+ ]
2382
+ });
2383
+ if (p4.isCancel(recovery) || recovery === "cancel") {
2384
+ p4.cancel("Update cancelled.");
2385
+ process.exit(0);
2386
+ }
2387
+ if (recovery === "restore") {
2388
+ const restoreSpinner = p4.spinner();
2389
+ restoreSpinner.start("Restoring manifest from cloud...");
2390
+ const cloudManifest = await fetchFromCloud(
2391
+ apiClient,
2392
+ config.projectId,
2393
+ config.templateVersion || "1.0.0"
2394
+ );
2395
+ if (cloudManifest) {
2396
+ await writeManifest(process.cwd(), cloudManifest);
2397
+ manifest = cloudManifest;
2398
+ fileCount = Object.keys(manifest.files).length;
2399
+ restoreSpinner.stop(
2400
+ chalk11.green(`Restored ${fileCount} files from cloud backup`)
2401
+ );
2402
+ } else {
2403
+ restoreSpinner.stop(chalk11.yellow("Cloud backup not found"));
2404
+ }
2405
+ }
2406
+ } else {
2407
+ p4.log.info(
2408
+ chalk11.gray(" No cloud backup available. All generated files will be created as new.")
2409
+ );
2410
+ }
2411
+ }
2412
+ const projectSpinner = p4.spinner();
2413
+ projectSpinner.start("Fetching project details...");
2414
+ const project = await apiClient.getProject(config.projectId);
2415
+ projectSpinner.stop(`Project: ${project.name}`);
2416
+ p4.log.step(chalk11.bold("Update Configuration:"));
2417
+ p4.log.info(` Project: ${project.name}`);
2418
+ p4.log.info(` Template: ${config.templateId}`);
2419
+ p4.log.info(` Namespace: ${config.namespace}`);
2420
+ p4.log.info(` Output: ${config.output.backend}`);
2421
+ if (options.dryRun) {
2422
+ p4.log.info(chalk11.yellow(" Mode: DRY RUN (no files will be written)"));
2423
+ }
2424
+ if (options.force) {
2425
+ p4.log.info(chalk11.yellow(" Mode: FORCE (will overwrite modified files)"));
2426
+ }
2427
+ if (!options.dryRun) {
2428
+ const proceed = await p4.confirm({
2429
+ message: "Proceed with update?",
2430
+ initialValue: true
2431
+ });
2432
+ if (p4.isCancel(proceed) || !proceed) {
2433
+ p4.cancel("Update cancelled.");
2434
+ process.exit(0);
2435
+ }
2436
+ }
2437
+ const genSpinner = ora4({
2438
+ text: "Generating code from latest schema...",
2439
+ color: "cyan"
2440
+ }).start();
2441
+ const result = await apiClient.generateCode({
2442
+ projectId: config.projectId,
2443
+ templateId: config.templateId,
2444
+ options: {
2445
+ includeValidations: true,
2446
+ includeComments: true,
2447
+ featureFlags: {
2448
+ includeDtos: true,
2449
+ includeUseCases: true,
2450
+ includeMappers: true,
2451
+ includeControllers: true,
2452
+ includeEfConfig: true,
2453
+ includeValidation: true,
2454
+ includeDtoValidation: true,
2455
+ includeUnitTests: true,
2456
+ includeIntegrationTests: true,
2457
+ includeStarterFiles: false
2458
+ // Don't regenerate starter files
2459
+ },
2460
+ useContexts: true
2461
+ }
2462
+ });
2463
+ genSpinner.succeed(
2464
+ chalk11.green(`Generated ${result.files?.length || 0} files from schema`)
2465
+ );
2466
+ if (options.dryRun) {
2467
+ p4.log.info("");
2468
+ p4.log.info(chalk11.bold(" Dry Run Results:"));
2469
+ p4.log.info(chalk11.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"));
2470
+ p4.log.info(` Files to process: ${result.files?.length || 0}`);
2471
+ p4.log.info(` Tracked files: ${fileCount}`);
2472
+ p4.log.info("");
2473
+ p4.log.info(chalk11.gray(" Run without --dry-run to apply changes."));
2474
+ p4.outro(chalk11.green("Dry run complete!"));
2475
+ return;
2476
+ }
2477
+ const updateSpinner = p4.spinner();
2478
+ updateSpinner.start("Applying updates...");
2479
+ const organizedFiles = result.files.map(
2480
+ (file) => ({
2481
+ ...file,
2482
+ path: `${config.output.backend.replace(/^\.\//, "")}/${file.path}`
2483
+ })
2484
+ );
2485
+ const writeResult = await writeGeneratedFilesWithManifest(
2486
+ organizedFiles,
2487
+ process.cwd(),
2488
+ config.templateVersion || "1.0.0",
2489
+ {
2490
+ verbose: options.verbose,
2491
+ forceOverwrite: options.force
2492
+ }
2493
+ );
2494
+ updateSpinner.stop("Update complete");
2495
+ if (result.creditsUsed !== void 0) {
2496
+ console.log("");
2497
+ console.log(chalk11.gray(" Credits used:"), chalk11.yellow(result.creditsUsed));
2498
+ console.log(
2499
+ chalk11.gray(" Credits remaining:"),
2500
+ chalk11.green(result.creditsRemaining)
2501
+ );
2502
+ }
2503
+ try {
2504
+ const syncResult = await syncToCloud(
2505
+ apiClient,
2506
+ config.projectId,
2507
+ writeResult.manifest
2508
+ );
2509
+ if (syncResult.success) {
2510
+ console.log(
2511
+ chalk11.gray(" \u2713 Manifest synced to cloud") + chalk11.gray(` (${syncResult.fileCount} files)`)
2512
+ );
2513
+ }
2514
+ } catch (syncError) {
2515
+ console.log(chalk11.yellow(" \u26A0 Could not sync manifest to cloud"));
2516
+ if (options.verbose) {
2517
+ console.log(chalk11.gray(` ${syncError.message}`));
2518
+ }
2519
+ }
2520
+ if (writeResult.conflicts.length > 0) {
2521
+ p4.outro(
2522
+ chalk11.yellow(`Update complete with ${writeResult.conflicts.length} conflict(s).`) + "\n" + chalk11.gray(" Run 'aerocoding resolve' after fixing conflicts.")
2523
+ );
2524
+ } else {
2525
+ const totalWritten = writeResult.created.length + writeResult.updated.length + writeResult.merged.length;
2526
+ p4.outro(
2527
+ chalk11.green(`Update complete! ${totalWritten} file(s) written.`)
2528
+ );
2529
+ }
2530
+ } catch (error) {
2531
+ const err = error;
2532
+ if (err.response?.status === 401) {
2533
+ await handleUnauthorized(tokenManager);
2534
+ } else if (err.response?.data?.message) {
2535
+ p4.cancel(err.response.data.message);
1340
2536
  } else {
1341
- p.cancel(error.message || "An unexpected error occurred");
2537
+ p4.cancel(err.message || "An unexpected error occurred");
1342
2538
  }
1343
2539
  process.exit(1);
1344
2540
  }
1345
2541
  }
1346
2542
 
2543
+ // src/commands/resolve.ts
2544
+ import * as p5 from "@clack/prompts";
2545
+ import chalk12 from "chalk";
2546
+ import { readdir, stat as stat2 } from "fs/promises";
2547
+ import { join as join3, relative } from "path";
2548
+ async function resolveCommand(options) {
2549
+ p5.intro(chalk12.bgCyan.black(" AeroCoding Resolve "));
2550
+ const config = await loadProjectConfig();
2551
+ if (!config) {
2552
+ p5.cancel(
2553
+ `No ${PROJECT_CONFIG_FILENAME} found. Run 'aerocoding create' first.`
2554
+ );
2555
+ process.exit(1);
2556
+ }
2557
+ const spinner5 = p5.spinner();
2558
+ spinner5.start("Scanning for conflict files...");
2559
+ const conflicts = await findConflictFiles(process.cwd());
2560
+ spinner5.stop(`Found ${conflicts.length} conflict(s)`);
2561
+ if (conflicts.length === 0) {
2562
+ p5.log.success("No conflicts to resolve!");
2563
+ p5.outro(chalk12.green("All clear!"));
2564
+ return;
2565
+ }
2566
+ p5.log.info("");
2567
+ p5.log.info(chalk12.bold(" Pending Conflicts:"));
2568
+ p5.log.info(chalk12.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"));
2569
+ for (const conflict of conflicts) {
2570
+ const relPath = relative(process.cwd(), conflict.originalPath);
2571
+ p5.log.info(chalk12.yellow(` \u26A0 ${relPath}`));
2572
+ if (options.verbose) {
2573
+ p5.log.info(chalk12.gray(` \u2192 ${relative(process.cwd(), conflict.newPath)}`));
2574
+ p5.log.info(
2575
+ chalk12.gray(` \u2192 ${relative(process.cwd(), conflict.conflictPath)}`)
2576
+ );
2577
+ }
2578
+ }
2579
+ p5.log.info(chalk12.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"));
2580
+ p5.log.info("");
2581
+ if (!options.all) {
2582
+ const proceed = await p5.confirm({
2583
+ message: `Remove ${conflicts.length} conflict file(s)? (This assumes you've resolved them)`,
2584
+ initialValue: true
2585
+ });
2586
+ if (p5.isCancel(proceed) || !proceed) {
2587
+ p5.cancel("Resolution cancelled.");
2588
+ process.exit(0);
2589
+ }
2590
+ }
2591
+ let manifest = await readManifest(process.cwd());
2592
+ if (!manifest) {
2593
+ p5.cancel("No manifest found. Cannot update file hashes.");
2594
+ process.exit(1);
2595
+ }
2596
+ const resolveSpinner = p5.spinner();
2597
+ resolveSpinner.start("Resolving conflicts...");
2598
+ let resolved = 0;
2599
+ let failed = 0;
2600
+ for (const conflict of conflicts) {
2601
+ try {
2602
+ const relPath = relative(process.cwd(), conflict.originalPath);
2603
+ try {
2604
+ await stat2(conflict.originalPath);
2605
+ } catch {
2606
+ if (options.verbose) {
2607
+ console.log(
2608
+ chalk12.red(` \u2717 ${relPath} - original file not found`)
2609
+ );
2610
+ }
2611
+ failed++;
2612
+ continue;
2613
+ }
2614
+ const { removedNew, removedConflict } = await removeConflictFiles(
2615
+ conflict.originalPath
2616
+ );
2617
+ const newHash = await hashFile(conflict.originalPath);
2618
+ const stats = await stat2(conflict.originalPath);
2619
+ const manifestPath = relPath.replace(/\\/g, "/");
2620
+ const existingEntry = manifest.files[manifestPath];
2621
+ manifest = setManifestFile(manifest, manifestPath, {
2622
+ hash: newHash,
2623
+ mtime: Math.floor(stats.mtimeMs),
2624
+ size: stats.size,
2625
+ entityId: existingEntry?.entityId,
2626
+ type: existingEntry?.type || "other",
2627
+ contextName: existingEntry?.contextName
2628
+ });
2629
+ if (options.verbose) {
2630
+ const removed = [];
2631
+ if (removedNew) removed.push(".new");
2632
+ if (removedConflict) removed.push(".conflict");
2633
+ console.log(
2634
+ chalk12.green(` \u2713 ${relPath}`) + chalk12.gray(` (removed ${removed.join(", ")})`)
2635
+ );
2636
+ }
2637
+ resolved++;
2638
+ } catch (error) {
2639
+ failed++;
2640
+ if (options.verbose) {
2641
+ const relPath = relative(process.cwd(), conflict.originalPath);
2642
+ console.log(chalk12.red(` \u2717 ${relPath} - ${error}`));
2643
+ }
2644
+ }
2645
+ }
2646
+ manifest = {
2647
+ ...manifest,
2648
+ lastSync: (/* @__PURE__ */ new Date()).toISOString()
2649
+ };
2650
+ await writeManifest(process.cwd(), manifest);
2651
+ resolveSpinner.stop("Conflicts resolved");
2652
+ console.log("");
2653
+ console.log(chalk12.bold(" Resolution Results"));
2654
+ console.log(chalk12.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"));
2655
+ if (resolved > 0) {
2656
+ console.log(chalk12.green(` \u2713 Resolved: ${resolved} files`));
2657
+ }
2658
+ if (failed > 0) {
2659
+ console.log(chalk12.red(` \u2717 Failed: ${failed} files`));
2660
+ }
2661
+ console.log(chalk12.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"));
2662
+ if (failed > 0) {
2663
+ p5.outro(
2664
+ chalk12.yellow(
2665
+ `Resolved ${resolved} conflict(s), ${failed} failed.`
2666
+ )
2667
+ );
2668
+ } else {
2669
+ p5.outro(chalk12.green(`All ${resolved} conflict(s) resolved!`));
2670
+ }
2671
+ }
2672
+ async function findConflictFiles(dir) {
2673
+ const conflicts = [];
2674
+ async function scan(currentDir) {
2675
+ try {
2676
+ const entries = await readdir(currentDir, { withFileTypes: true });
2677
+ for (const entry of entries) {
2678
+ const fullPath = join3(currentDir, entry.name);
2679
+ if (entry.isDirectory()) {
2680
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name.startsWith(".")) {
2681
+ continue;
2682
+ }
2683
+ await scan(fullPath);
2684
+ } else if (entry.isFile()) {
2685
+ if (entry.name.endsWith(CONFLICT_NEW_EXT)) {
2686
+ const originalPath = fullPath.slice(
2687
+ 0,
2688
+ -CONFLICT_NEW_EXT.length
2689
+ );
2690
+ const conflictPath = originalPath + CONFLICT_DIFF_EXT;
2691
+ const existing = conflicts.find(
2692
+ (c) => c.originalPath === originalPath
2693
+ );
2694
+ if (!existing) {
2695
+ conflicts.push({
2696
+ originalPath,
2697
+ newPath: fullPath,
2698
+ conflictPath
2699
+ });
2700
+ }
2701
+ } else if (entry.name.endsWith(CONFLICT_DIFF_EXT)) {
2702
+ const originalPath = fullPath.slice(
2703
+ 0,
2704
+ -CONFLICT_DIFF_EXT.length
2705
+ );
2706
+ const newPath = originalPath + CONFLICT_NEW_EXT;
2707
+ const existing = conflicts.find(
2708
+ (c) => c.originalPath === originalPath
2709
+ );
2710
+ if (!existing) {
2711
+ conflicts.push({
2712
+ originalPath,
2713
+ newPath,
2714
+ conflictPath: fullPath
2715
+ });
2716
+ }
2717
+ }
2718
+ }
2719
+ }
2720
+ } catch {
2721
+ }
2722
+ }
2723
+ await scan(dir);
2724
+ return conflicts;
2725
+ }
2726
+
1347
2727
  // src/commands/pull.ts
1348
- import chalk10 from "chalk";
2728
+ import chalk13 from "chalk";
1349
2729
  async function pullCommand(options) {
1350
- console.log(chalk10.yellow("Command not yet implemented"));
2730
+ console.log(chalk13.yellow("Command not yet implemented"));
1351
2731
  if (options.project) {
1352
- console.log(chalk10.gray(` Requested project: ${options.project}`));
2732
+ console.log(chalk13.gray(` Requested project: ${options.project}`));
1353
2733
  }
1354
- console.log(chalk10.gray(" Coming soon in v0.2.0\n"));
2734
+ console.log(chalk13.gray(" Coming soon in v0.2.0\n"));
1355
2735
  }
1356
2736
 
1357
2737
  // src/commands/status.ts
1358
- import chalk11 from "chalk";
2738
+ import chalk14 from "chalk";
1359
2739
  async function statusCommand() {
1360
- console.log(chalk11.yellow("Command not yet implemented"));
1361
- console.log(chalk11.gray(" Coming soon in v0.2.0\n"));
2740
+ console.log(chalk14.yellow("Command not yet implemented"));
2741
+ console.log(chalk14.gray(" Coming soon in v0.2.0\n"));
1362
2742
  }
1363
2743
 
1364
2744
  // src/index.ts
1365
2745
  import "dotenv/config";
1366
2746
  var program = new Command();
1367
- program.name("aerocoding").description("AeroCoding CLI - Generate production-ready code from UML diagrams").version("0.1.21");
2747
+ program.name("aerocoding").description("AeroCoding CLI - Generate production-ready code from UML diagrams").version("0.1.24");
1368
2748
  program.command("login").description("Authenticate with AeroCoding").action(loginCommand);
1369
2749
  program.command("logout").description("Logout and clear stored credentials").action(logoutCommand);
1370
2750
  program.command("whoami").description("Show current authenticated user").action(whoamiCommand);
1371
2751
  program.command("init").description("Initialize AeroCoding in current directory").option("-p, --project <id>", "Project ID (skip selection)").option("-f, --force", "Overwrite existing config without asking").action(initCommand);
2752
+ program.command("create <name>").description("Create a new AeroCoding project with full architecture").option("-t, --template <id>", "Template ID (skip selection)").option("-p, --project <id>", "Project ID (skip selection)").option("-f, --force", "Overwrite existing directory without asking").action(createCommand);
2753
+ program.command("update").description("Update generated code incrementally (preserves your changes)").option("-f, --force", "Overwrite modified files without merging").option("-v, --verbose", "Show detailed file operations").option("--dry-run", "Preview changes without writing files").action(updateCommand);
2754
+ program.command("resolve").description("Clean up conflict files after manual resolution").option("-v, --verbose", "Show detailed resolution info").option("-a, --all", "Resolve all conflicts without confirmation").action(resolveCommand);
1372
2755
  program.command("pull").description("Pull schema from cloud").option("-p, --project <id>", "Project ID").action(pullCommand);
1373
2756
  program.command("status").description("Show local schema status").action(statusCommand);
1374
- program.command("generate").alias("gen").description("Generate code from schema").option("-p, --project <id>", "Project ID").option("-t, --targets <targets...>", "Generation targets (e.g., dotnet-entity)").option("-e, --entities <entities...>", "Filter by entity names").option("-o, --output <dir>", "Output directory", "./.aerocoding").option("--backend-preset <preset>", "Backend architecture preset").option("--frontend-preset <preset>", "Frontend architecture preset").option("--backend-layers <layers...>", "Backend layers to generate").option("--frontend-layers <layers...>", "Frontend layers to generate").option("--no-validations", "Exclude validations").option("--no-comments", "Exclude comments").option("--no-annotations", "Exclude annotations").option("--no-logging", "Exclude logging statements").option("--no-testing", "Exclude test files").option("--validation-lib <framework>", "Validation library (e.g., fluentvalidation, zod, formz)").option("-v, --verbose", "Show all generated file paths").option("-y, --yes", "Skip confirmation prompt").action(generateCommand);
2757
+ program.command("generate").alias("gen").description("Generate code from schema").option("-p, --project <id>", "Project ID").option("-t, --targets <targets...>", "Generation targets (e.g., dotnet-entity)").option("-e, --entities <entities...>", "Filter by entity names").option("-o, --output <dir>", "Output directory", "./.aerocoding").option("--backend-preset <preset>", "Backend architecture preset").option("--frontend-preset <preset>", "Frontend architecture preset").option("--backend-layers <layers...>", "Backend layers to generate").option("--frontend-layers <layers...>", "Frontend layers to generate").option("--no-validations", "Exclude validations").option("--no-comments", "Exclude comments").option("--no-annotations", "Exclude annotations").option("--no-logging", "Exclude logging statements").option("--no-testing", "Exclude test files").option("--validation-lib <framework>", "Validation library (e.g., fluentvalidation, zod, formz)").option("-v, --verbose", "Show all generated file paths").option("-y, --yes", "Skip confirmation prompt").option("-a, --all", "Generate all bounded contexts and targets without prompting").action(generateCommand);
1375
2758
  program.parse();
1376
2759
  //# sourceMappingURL=index.js.map