aerocoding 0.1.23 → 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
@@ -520,13 +548,266 @@ import ora2 from "ora";
520
548
  import * as p from "@clack/prompts";
521
549
 
522
550
  // src/utils/file-writer.ts
523
- import fs from "fs/promises";
524
- import path from "path";
551
+ import fs3 from "fs/promises";
552
+ import path2 from "path";
525
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
526
807
  function isPathSafe(outputDir, filePath) {
527
- const resolvedOutput = path.resolve(outputDir);
528
- const resolvedFile = path.resolve(outputDir, filePath);
529
- 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;
530
811
  }
531
812
  function getCategoryFromPath(filePath) {
532
813
  const lowerPath = filePath.toLowerCase();
@@ -601,19 +882,19 @@ async function writeGeneratedFiles(files, outputDir, verbose = false) {
601
882
  );
602
883
  continue;
603
884
  }
604
- const fullPath = path.resolve(outputDir, file.path);
605
- const dir = path.dirname(fullPath);
885
+ const fullPath = path2.resolve(outputDir, file.path);
886
+ const dir = path2.dirname(fullPath);
606
887
  if (file.generateOnce) {
607
888
  try {
608
- await fs.access(fullPath);
889
+ await fs3.access(fullPath);
609
890
  skippedOnceFiles.push(file.path);
610
891
  continue;
611
892
  } catch {
612
893
  }
613
894
  }
614
895
  try {
615
- await fs.mkdir(dir, { recursive: true });
616
- await fs.writeFile(fullPath, file.content, "utf-8");
896
+ await fs3.mkdir(dir, { recursive: true });
897
+ await fs3.writeFile(fullPath, file.content, "utf-8");
617
898
  if (verbose) {
618
899
  console.log(chalk6.gray(` ${file.path}`));
619
900
  }
@@ -636,12 +917,198 @@ async function writeGeneratedFiles(files, outputDir, verbose = false) {
636
917
  }
637
918
  }
638
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
+ }
1105
+ }
639
1106
 
640
1107
  // src/utils/prompt.ts
641
1108
  import readline from "readline";
642
1109
  import chalk7 from "chalk";
643
1110
  function promptConfirm(message) {
644
- return new Promise((resolve) => {
1111
+ return new Promise((resolve2) => {
645
1112
  const rl = readline.createInterface({
646
1113
  input: process.stdin,
647
1114
  output: process.stdout
@@ -649,14 +1116,14 @@ function promptConfirm(message) {
649
1116
  rl.question(chalk7.yellow(`${message} [Y/n] `), (answer) => {
650
1117
  rl.close();
651
1118
  const normalized = answer.trim().toLowerCase();
652
- resolve(normalized !== "n" && normalized !== "no");
1119
+ resolve2(normalized !== "n" && normalized !== "no");
653
1120
  });
654
1121
  });
655
1122
  }
656
1123
 
657
1124
  // src/config/loader.ts
658
- import fs2 from "fs/promises";
659
- import path2 from "path";
1125
+ import fs4 from "fs/promises";
1126
+ import path3 from "path";
660
1127
 
661
1128
  // src/config/schema.ts
662
1129
  import { z } from "zod";
@@ -688,9 +1155,9 @@ var CONFIG_FILENAME = ".aerocodingrc.json";
688
1155
 
689
1156
  // src/config/loader.ts
690
1157
  async function loadConfig(dir = process.cwd()) {
691
- const configPath = path2.join(dir, CONFIG_FILENAME);
1158
+ const configPath = path3.join(dir, CONFIG_FILENAME);
692
1159
  try {
693
- const content = await fs2.readFile(configPath, "utf-8");
1160
+ const content = await fs4.readFile(configPath, "utf-8");
694
1161
  const parsed = JSON.parse(content);
695
1162
  return configSchema.parse(parsed);
696
1163
  } catch (error) {
@@ -701,7 +1168,7 @@ async function loadConfig(dir = process.cwd()) {
701
1168
  }
702
1169
  }
703
1170
  async function saveConfig(config, dir = process.cwd()) {
704
- const configPath = path2.join(dir, CONFIG_FILENAME);
1171
+ const configPath = path3.join(dir, CONFIG_FILENAME);
705
1172
  const content = JSON.stringify(
706
1173
  {
707
1174
  $schema: "https://aerocoding.dev/schemas/aerocodingrc.json",
@@ -710,12 +1177,12 @@ async function saveConfig(config, dir = process.cwd()) {
710
1177
  null,
711
1178
  2
712
1179
  );
713
- await fs2.writeFile(configPath, content, "utf-8");
1180
+ await fs4.writeFile(configPath, content, "utf-8");
714
1181
  }
715
1182
  async function configExists(dir = process.cwd()) {
716
- const configPath = path2.join(dir, CONFIG_FILENAME);
1183
+ const configPath = path3.join(dir, CONFIG_FILENAME);
717
1184
  try {
718
- await fs2.access(configPath);
1185
+ await fs4.access(configPath);
719
1186
  return true;
720
1187
  } catch {
721
1188
  return false;
@@ -779,6 +1246,91 @@ function buildFeatureFlagsFromConfig(config) {
779
1246
  }
780
1247
  return flags;
781
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
+ }
782
1334
  async function generateCommand(options) {
783
1335
  const tokenManager = new TokenManager();
784
1336
  const token = await tokenManager.getAccessToken();
@@ -802,42 +1354,61 @@ async function generateCommand(options) {
802
1354
  const includeLogging = options.logging ?? config?.codeStyle?.includeLogging ?? true;
803
1355
  const includeTesting = options.testing ?? config?.codeStyle?.includeTesting ?? true;
804
1356
  const apiClient = createApiClientWithAutoLogout(token, tokenManager);
805
- let spinner2 = ora2({ text: "Fetching project...", color: "cyan" }).start();
1357
+ let spinner5 = ora2({ text: "Fetching project...", color: "cyan" }).start();
806
1358
  try {
807
1359
  const project = await apiClient.getProject(projectId);
808
- spinner2.succeed(chalk8.gray("Project loaded"));
1360
+ spinner5.succeed(chalk8.gray("Project loaded"));
809
1361
  const hasBackendConfig = !!config?.backend?.preset;
810
1362
  const hasFrontendConfig = !!config?.frontend?.preset;
811
- const hasBothTargets = hasBackendConfig && hasFrontendConfig;
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;
812
1384
  let selectedTarget = "both";
813
1385
  if (hasBothTargets && !options.all) {
814
1386
  const targetChoice = await p.select({
815
1387
  message: "Which target do you want to generate?",
816
1388
  options: [
1389
+ {
1390
+ value: "both",
1391
+ label: "Both"
1392
+ },
817
1393
  {
818
1394
  value: "backend",
819
- label: `Backend (${config?.backend?.preset})`,
820
- hint: "Recommended"
1395
+ label: `Backend (${config?.backend?.preset})`
821
1396
  },
822
1397
  {
823
1398
  value: "frontend",
824
1399
  label: `Frontend (${config?.frontend?.preset})`
825
- },
826
- {
827
- value: "both",
828
- label: "Both"
829
1400
  }
830
1401
  ],
831
- initialValue: "backend"
1402
+ initialValue: "both"
832
1403
  });
833
1404
  if (p.isCancel(targetChoice)) {
834
1405
  console.log(chalk8.yellow("\n Generation cancelled\n"));
835
1406
  process.exit(0);
836
1407
  }
837
1408
  selectedTarget = targetChoice;
838
- } else if (hasBackendConfig && !hasFrontendConfig) {
1409
+ } else if (backendAvailable && !frontendAvailable) {
839
1410
  selectedTarget = "backend";
840
- } else if (hasFrontendConfig && !hasBackendConfig) {
1411
+ } else if (frontendAvailable && !backendAvailable) {
841
1412
  selectedTarget = "frontend";
842
1413
  }
843
1414
  const diagrams = project.schema?.diagrams || [];
@@ -885,7 +1456,7 @@ async function generateCommand(options) {
885
1456
  targets = options.targets || [];
886
1457
  }
887
1458
  const featureFlags = buildFeatureFlagsFromConfig(config);
888
- spinner2 = ora2({ text: "Checking credits...", color: "cyan" }).start();
1459
+ spinner5 = ora2({ text: "Checking credits...", color: "cyan" }).start();
889
1460
  const credits = await apiClient.getCreditUsage(project.organizationId);
890
1461
  const useContexts = config?.architectureStyle !== "flat";
891
1462
  const estimateDiagramIds = selectedDiagramIds.length < diagrams.length ? selectedDiagramIds : void 0;
@@ -916,7 +1487,7 @@ async function generateCommand(options) {
916
1487
  }
917
1488
  } catch {
918
1489
  }
919
- spinner2.succeed(chalk8.gray("Credits verified"));
1490
+ spinner5.succeed(chalk8.gray("Credits verified"));
920
1491
  if (estimate && estimate.entities === 0) {
921
1492
  console.log(chalk8.yellow("\n \u26A0 No entities found in this project."));
922
1493
  console.log(chalk8.gray(" Create entities in the diagram editor and save (Ctrl+S) before generating.\n"));
@@ -977,26 +1548,32 @@ async function generateCommand(options) {
977
1548
  }
978
1549
  console.log(chalk8.gray(" Output:"), chalk8.cyan(output));
979
1550
  if (estimate && estimate.files && estimate.files.length > 0) {
980
- console.log("");
981
- console.log(chalk8.bold(" Files to Generate"));
982
- 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"));
983
- const categories = categorizeFilePaths(estimate.files);
984
- const maxNameLength = Math.max(...categories.map((c) => c.name.length));
985
- for (const category of categories) {
986
- const padding = " ".repeat(maxNameLength - category.name.length + 2);
987
- const countStr = category.count.toString().padStart(3, " ");
988
- console.log(
989
- chalk8.gray(` ${category.name}${padding}`),
990
- chalk8.cyan(`${countStr} files`)
991
- );
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`));
992
1576
  }
993
- 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"));
994
- const totalPadding = " ".repeat(maxNameLength - 5 + 2);
995
- const totalStr = estimate.totalFiles.toString().padStart(3, " ");
996
- console.log(
997
- chalk8.white(` Total${totalPadding}`),
998
- chalk8.bold.cyan(`${totalStr} files`)
999
- );
1000
1577
  console.log(chalk8.gray(` Entities:`), chalk8.cyan(estimate.entities));
1001
1578
  }
1002
1579
  console.log("");
@@ -1027,7 +1604,7 @@ async function generateCommand(options) {
1027
1604
  }
1028
1605
  }
1029
1606
  console.log("");
1030
- spinner2 = ora2({ text: "Generating code...", color: "cyan" }).start();
1607
+ spinner5 = ora2({ text: "Generating code...", color: "cyan" }).start();
1031
1608
  const diagramIds = selectedDiagramIds.length < diagrams.length ? selectedDiagramIds : void 0;
1032
1609
  const generatePayload = templateId ? {
1033
1610
  // NEW: Full architecture mode using template
@@ -1060,7 +1637,7 @@ async function generateCommand(options) {
1060
1637
  }
1061
1638
  };
1062
1639
  const result = await apiClient.generateCode(generatePayload);
1063
- spinner2.succeed(chalk8.green("Code generated successfully!"));
1640
+ spinner5.succeed(chalk8.green("Code generated successfully!"));
1064
1641
  console.log("");
1065
1642
  console.log(chalk8.bold(" Results"));
1066
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"));
@@ -1092,11 +1669,16 @@ async function generateCommand(options) {
1092
1669
  }
1093
1670
  }
1094
1671
  console.log("");
1095
- await writeGeneratedFiles(result.files, output, options.verbose);
1096
- 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
+ }
1097
1679
  console.log("");
1098
1680
  } catch (error) {
1099
- spinner2.fail(chalk8.red("Generation failed"));
1681
+ spinner5.fail(chalk8.red("Generation failed"));
1100
1682
  if (error.response?.status === 401) {
1101
1683
  await handleUnauthorized(tokenManager);
1102
1684
  } else if (error.response?.status === 403) {
@@ -1178,10 +1760,10 @@ async function initCommand(options) {
1178
1760
  }
1179
1761
  let projectId = options.project;
1180
1762
  if (!projectId) {
1181
- const spinner3 = p2.spinner();
1182
- spinner3.start("Loading projects...");
1763
+ const spinner6 = p2.spinner();
1764
+ spinner6.start("Loading projects...");
1183
1765
  const projects = await apiClient.listProjects(organizationId);
1184
- spinner3.stop("Projects loaded");
1766
+ spinner6.stop("Projects loaded");
1185
1767
  if (projects.length === 0) {
1186
1768
  p2.cancel("No projects in this organization. Create one on aerocoding.dev first.");
1187
1769
  process.exit(1);
@@ -1200,10 +1782,10 @@ async function initCommand(options) {
1200
1782
  }
1201
1783
  projectId = selectedProject;
1202
1784
  }
1203
- const spinner2 = p2.spinner();
1204
- spinner2.start("Fetching project details...");
1785
+ const spinner5 = p2.spinner();
1786
+ spinner5.start("Fetching project details...");
1205
1787
  const project = await apiClient.getProject(projectId);
1206
- spinner2.stop(`Project: ${project.name}`);
1788
+ spinner5.stop(`Project: ${project.name}`);
1207
1789
  const hasBackend = !!project.backendFramework;
1208
1790
  const hasFrontend = !!project.frontendFramework;
1209
1791
  if (!hasBackend && !hasFrontend) {
@@ -1358,31 +1940,818 @@ async function initCommand(options) {
1358
1940
  }
1359
1941
  }
1360
1942
 
1361
- // src/commands/pull.ts
1943
+ // src/commands/create.ts
1944
+ import * as p3 from "@clack/prompts";
1362
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),
2194
+ validate: (value) => {
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
+ }
2199
+ return void 0;
2200
+ }
2201
+ });
2202
+ if (p3.isCancel(namespace)) {
2203
+ p3.cancel("Operation cancelled.");
2204
+ process.exit(0);
2205
+ }
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")
2303
+ );
2304
+ } catch (error) {
2305
+ const err = error;
2306
+ if (err.response?.status === 401) {
2307
+ await handleUnauthorized(tokenManager);
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);
2536
+ } else {
2537
+ p4.cancel(err.message || "An unexpected error occurred");
2538
+ }
2539
+ process.exit(1);
2540
+ }
2541
+ }
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
+
2727
+ // src/commands/pull.ts
2728
+ import chalk13 from "chalk";
1363
2729
  async function pullCommand(options) {
1364
- console.log(chalk10.yellow("Command not yet implemented"));
2730
+ console.log(chalk13.yellow("Command not yet implemented"));
1365
2731
  if (options.project) {
1366
- console.log(chalk10.gray(` Requested project: ${options.project}`));
2732
+ console.log(chalk13.gray(` Requested project: ${options.project}`));
1367
2733
  }
1368
- console.log(chalk10.gray(" Coming soon in v0.2.0\n"));
2734
+ console.log(chalk13.gray(" Coming soon in v0.2.0\n"));
1369
2735
  }
1370
2736
 
1371
2737
  // src/commands/status.ts
1372
- import chalk11 from "chalk";
2738
+ import chalk14 from "chalk";
1373
2739
  async function statusCommand() {
1374
- console.log(chalk11.yellow("Command not yet implemented"));
1375
- 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"));
1376
2742
  }
1377
2743
 
1378
2744
  // src/index.ts
1379
2745
  import "dotenv/config";
1380
2746
  var program = new Command();
1381
- program.name("aerocoding").description("AeroCoding CLI - Generate production-ready code from UML diagrams").version("0.1.23");
2747
+ program.name("aerocoding").description("AeroCoding CLI - Generate production-ready code from UML diagrams").version("0.1.24");
1382
2748
  program.command("login").description("Authenticate with AeroCoding").action(loginCommand);
1383
2749
  program.command("logout").description("Logout and clear stored credentials").action(logoutCommand);
1384
2750
  program.command("whoami").description("Show current authenticated user").action(whoamiCommand);
1385
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);
1386
2755
  program.command("pull").description("Pull schema from cloud").option("-p, --project <id>", "Project ID").action(pullCommand);
1387
2756
  program.command("status").description("Show local schema status").action(statusCommand);
1388
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);