aerocoding 0.1.23 → 0.1.25

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 spinner4 = 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
+ spinner4.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
+ spinner4.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
+ spinner4.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
+ spinner4.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
+ spinner4.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
+ spinner4.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
@@ -514,19 +542,274 @@ async function whoamiCommand() {
514
542
  }
515
543
  }
516
544
 
517
- // src/commands/generate.ts
518
- import chalk8 from "chalk";
519
- import ora2 from "ora";
545
+ // src/commands/create.ts
520
546
  import * as p from "@clack/prompts";
547
+ import chalk7 from "chalk";
548
+ import ora2 from "ora";
549
+ import { mkdir, access as access3 } from "fs/promises";
550
+ import { resolve } from "path";
521
551
 
522
552
  // src/utils/file-writer.ts
523
- import fs from "fs/promises";
524
- import path from "path";
553
+ import fs3 from "fs/promises";
554
+ import path2 from "path";
525
555
  import chalk6 from "chalk";
556
+
557
+ // src/manifest/index.ts
558
+ import * as fs2 from "fs/promises";
559
+ import * as path from "path";
560
+
561
+ // src/manifest/types.ts
562
+ var MANIFEST_VERSION = "1.0.0";
563
+ var MANIFEST_FILENAME = "aerocoding-manifest.json";
564
+
565
+ // src/manifest/hash-utils.ts
566
+ import * as crypto from "crypto";
567
+ import * as fs from "fs/promises";
568
+ import { createReadStream } from "fs";
569
+ function hashString(content) {
570
+ return crypto.createHash("sha256").update(content, "utf-8").digest("hex");
571
+ }
572
+ async function hashFile(filePath) {
573
+ return new Promise((resolve2, reject) => {
574
+ const hash = crypto.createHash("sha256");
575
+ const stream = createReadStream(filePath);
576
+ stream.on("data", (chunk) => hash.update(chunk));
577
+ stream.on("end", () => resolve2(hash.digest("hex")));
578
+ stream.on("error", reject);
579
+ });
580
+ }
581
+ async function getFileStats(filePath) {
582
+ try {
583
+ const stats = await fs.stat(filePath);
584
+ return {
585
+ mtime: Math.floor(stats.mtimeMs),
586
+ size: stats.size
587
+ };
588
+ } catch {
589
+ return null;
590
+ }
591
+ }
592
+ function mightHaveChanged(currentStats, manifestEntry) {
593
+ return currentStats.mtime !== manifestEntry.mtime || currentStats.size !== manifestEntry.size;
594
+ }
595
+ async function detectFileChange(filePath, manifestEntry) {
596
+ const stats = await getFileStats(filePath);
597
+ if (!stats) {
598
+ if (manifestEntry) {
599
+ return { status: "deleted" };
600
+ }
601
+ return { status: "new" };
602
+ }
603
+ if (!manifestEntry) {
604
+ return { status: "unknown" };
605
+ }
606
+ if (!mightHaveChanged(stats, manifestEntry)) {
607
+ return { status: "unchanged" };
608
+ }
609
+ const currentHash = await hashFile(filePath);
610
+ if (currentHash === manifestEntry.hash) {
611
+ return { status: "unchanged" };
612
+ }
613
+ return { status: "modified", currentHash };
614
+ }
615
+
616
+ // src/manifest/index.ts
617
+ async function readManifest(projectDir) {
618
+ const manifestPath = path.join(projectDir, MANIFEST_FILENAME);
619
+ try {
620
+ const content = await fs2.readFile(manifestPath, "utf-8");
621
+ return JSON.parse(content);
622
+ } catch (error) {
623
+ if (error.code === "ENOENT") {
624
+ return null;
625
+ }
626
+ throw error;
627
+ }
628
+ }
629
+ async function writeManifest(projectDir, manifest) {
630
+ const manifestPath = path.join(projectDir, MANIFEST_FILENAME);
631
+ await fs2.writeFile(manifestPath, JSON.stringify(manifest, null, 2) + "\n", "utf-8");
632
+ }
633
+ function createEmptyManifest(templateVersion) {
634
+ return {
635
+ version: MANIFEST_VERSION,
636
+ lastSync: (/* @__PURE__ */ new Date()).toISOString(),
637
+ templateVersion,
638
+ files: {},
639
+ entities: []
640
+ };
641
+ }
642
+ function setManifestFile(manifest, filePath, entry) {
643
+ return {
644
+ ...manifest,
645
+ files: {
646
+ ...manifest.files,
647
+ [filePath]: entry
648
+ }
649
+ };
650
+ }
651
+
652
+ // src/merge/merger.ts
653
+ import diff3Merge from "diff3";
654
+ function performMerge(base, current, generated) {
655
+ const baseLines = base.split("\n");
656
+ const currentLines = current.split("\n");
657
+ const generatedLines = generated.split("\n");
658
+ const merged = diff3Merge(currentLines, baseLines, generatedLines);
659
+ const conflicts = [];
660
+ const outputLines = [];
661
+ let lineNumber = 1;
662
+ for (const region of merged) {
663
+ if (Array.isArray(region)) {
664
+ outputLines.push(...region);
665
+ lineNumber += region.length;
666
+ } else if ("ok" in region && region.ok) {
667
+ const okLines = region.ok;
668
+ outputLines.push(...okLines);
669
+ lineNumber += okLines.length;
670
+ } else if ("conflict" in region && region.conflict) {
671
+ const conflictData = region.conflict;
672
+ const yoursLines = conflictData.a || [];
673
+ const generatedLines2 = conflictData.b || [];
674
+ const yours = yoursLines.join("\n");
675
+ const generatedStr = generatedLines2.join("\n");
676
+ const conflictLines = [
677
+ "<<<<<<< YOURS",
678
+ ...yoursLines,
679
+ "=======",
680
+ ...generatedLines2,
681
+ ">>>>>>> GENERATED"
682
+ ];
683
+ const startLine = lineNumber;
684
+ outputLines.push(...conflictLines);
685
+ lineNumber += conflictLines.length;
686
+ conflicts.push({
687
+ startLine,
688
+ endLine: lineNumber - 1,
689
+ yours,
690
+ generated: generatedStr
691
+ });
692
+ }
693
+ }
694
+ return {
695
+ success: conflicts.length === 0,
696
+ content: outputLines.join("\n"),
697
+ conflicts
698
+ };
699
+ }
700
+
701
+ // src/merge/conflict-writer.ts
702
+ import { writeFile as writeFile2, unlink, access } from "fs/promises";
703
+ import { basename, extname } from "path";
704
+ var CONFLICT_NEW_EXT = ".new";
705
+ var CONFLICT_DIFF_EXT = ".conflict";
706
+ async function writeConflictFiles(filePath, generatedContent, conflicts, currentContent) {
707
+ const newPath = `${filePath}${CONFLICT_NEW_EXT}`;
708
+ const conflictPath = `${filePath}${CONFLICT_DIFF_EXT}`;
709
+ await writeFile2(newPath, generatedContent, "utf-8");
710
+ const conflictContent = generateConflictFile(
711
+ filePath,
712
+ currentContent,
713
+ generatedContent,
714
+ conflicts
715
+ );
716
+ await writeFile2(conflictPath, conflictContent, "utf-8");
717
+ return { newPath, conflictPath };
718
+ }
719
+ function generateConflictFile(filePath, _currentContent, _generatedContent, conflicts) {
720
+ const fileName = basename(filePath);
721
+ const ext = extname(filePath);
722
+ const commentStyle = getCommentStyle(ext);
723
+ const header = `${commentStyle.start}
724
+ ${commentStyle.prefix} ${fileName} - MERGE CONFLICT
725
+ ${commentStyle.prefix}
726
+ ${commentStyle.prefix} This file has ${conflicts.length} conflict(s) that need manual resolution.
727
+ ${commentStyle.prefix}
728
+ ${commentStyle.prefix} Resolution options:
729
+ ${commentStyle.prefix} 1. Edit ${fileName} to include both your changes and the generated changes
730
+ ${commentStyle.prefix} 2. Copy desired parts from ${fileName}${CONFLICT_NEW_EXT}
731
+ ${commentStyle.prefix} 3. Run 'aerocoding resolve' when done
732
+ ${commentStyle.prefix}
733
+ ${commentStyle.prefix} Your file: ${fileName} (unchanged)
734
+ ${commentStyle.prefix} Generated: ${fileName}${CONFLICT_NEW_EXT} (new version)
735
+ ${commentStyle.end}
736
+
737
+ `;
738
+ const conflictSections = conflicts.map((conflict, index) => {
739
+ return `${commentStyle.start}
740
+ ${commentStyle.prefix} CONFLICT ${index + 1} of ${conflicts.length} (lines ${conflict.startLine}-${conflict.endLine})
741
+ ${commentStyle.end}
742
+
743
+ <<<<<<< YOURS (keep your changes)
744
+ ${conflict.yours}
745
+ =======
746
+ ${conflict.generated}
747
+ >>>>>>> GENERATED (from template)
748
+ `;
749
+ }).join("\n");
750
+ return header + conflictSections;
751
+ }
752
+ function getCommentStyle(ext) {
753
+ switch (ext.toLowerCase()) {
754
+ case ".cs":
755
+ case ".ts":
756
+ case ".tsx":
757
+ case ".js":
758
+ case ".jsx":
759
+ case ".java":
760
+ case ".kt":
761
+ case ".dart":
762
+ case ".go":
763
+ case ".swift":
764
+ case ".rs":
765
+ case ".c":
766
+ case ".cpp":
767
+ case ".h":
768
+ return { start: "/*", prefix: " *", end: " */" };
769
+ case ".py":
770
+ case ".rb":
771
+ case ".sh":
772
+ case ".yaml":
773
+ case ".yml":
774
+ return { start: "#", prefix: "#", end: "#" };
775
+ case ".html":
776
+ case ".xml":
777
+ case ".xaml":
778
+ case ".svg":
779
+ return { start: "<!--", prefix: " ", end: "-->" };
780
+ case ".sql":
781
+ return { start: "--", prefix: "--", end: "--" };
782
+ case ".css":
783
+ case ".scss":
784
+ case ".less":
785
+ return { start: "/*", prefix: " *", end: " */" };
786
+ default:
787
+ return { start: "//", prefix: "//", end: "//" };
788
+ }
789
+ }
790
+ async function removeConflictFiles(filePath) {
791
+ const newPath = `${filePath}${CONFLICT_NEW_EXT}`;
792
+ const conflictPath = `${filePath}${CONFLICT_DIFF_EXT}`;
793
+ let removedNew = false;
794
+ let removedConflict = false;
795
+ try {
796
+ await unlink(newPath);
797
+ removedNew = true;
798
+ } catch {
799
+ }
800
+ try {
801
+ await unlink(conflictPath);
802
+ removedConflict = true;
803
+ } catch {
804
+ }
805
+ return { removedNew, removedConflict };
806
+ }
807
+
808
+ // src/utils/file-writer.ts
526
809
  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;
810
+ const resolvedOutput = path2.resolve(outputDir);
811
+ const resolvedFile = path2.resolve(outputDir, filePath);
812
+ return resolvedFile.startsWith(resolvedOutput + path2.sep) || resolvedFile === resolvedOutput;
530
813
  }
531
814
  function getCategoryFromPath(filePath) {
532
815
  const lowerPath = filePath.toLowerCase();
@@ -601,19 +884,19 @@ async function writeGeneratedFiles(files, outputDir, verbose = false) {
601
884
  );
602
885
  continue;
603
886
  }
604
- const fullPath = path.resolve(outputDir, file.path);
605
- const dir = path.dirname(fullPath);
887
+ const fullPath = path2.resolve(outputDir, file.path);
888
+ const dir = path2.dirname(fullPath);
606
889
  if (file.generateOnce) {
607
890
  try {
608
- await fs.access(fullPath);
891
+ await fs3.access(fullPath);
609
892
  skippedOnceFiles.push(file.path);
610
893
  continue;
611
894
  } catch {
612
895
  }
613
896
  }
614
897
  try {
615
- await fs.mkdir(dir, { recursive: true });
616
- await fs.writeFile(fullPath, file.content, "utf-8");
898
+ await fs3.mkdir(dir, { recursive: true });
899
+ await fs3.writeFile(fullPath, file.content, "utf-8");
617
900
  if (verbose) {
618
901
  console.log(chalk6.gray(` ${file.path}`));
619
902
  }
@@ -636,533 +919,341 @@ async function writeGeneratedFiles(files, outputDir, verbose = false) {
636
919
  }
637
920
  }
638
921
  }
639
-
640
- // src/utils/prompt.ts
641
- import readline from "readline";
642
- import chalk7 from "chalk";
643
- function promptConfirm(message) {
644
- return new Promise((resolve) => {
645
- const rl = readline.createInterface({
646
- input: process.stdin,
647
- output: process.stdout
648
- });
649
- rl.question(chalk7.yellow(`${message} [Y/n] `), (answer) => {
650
- rl.close();
651
- const normalized = answer.trim().toLowerCase();
652
- resolve(normalized !== "n" && normalized !== "no");
653
- });
654
- });
655
- }
656
-
657
- // src/config/loader.ts
658
- import fs2 from "fs/promises";
659
- import path2 from "path";
660
-
661
- // src/config/schema.ts
662
- import { z } from "zod";
663
- var configSchema = z.object({
664
- $schema: z.string().optional(),
665
- project: z.string().uuid(),
666
- output: z.string().default("./.aerocoding"),
667
- // Architecture style: how to organize generated code
668
- architectureStyle: z.enum(["bounded-contexts", "flat"]).optional().default("bounded-contexts"),
669
- backend: z.object({
670
- preset: z.string()
671
- }).optional(),
672
- frontend: z.object({
673
- preset: z.string()
674
- }).optional(),
675
- codeStyle: z.object({
676
- includeValidations: z.boolean().default(true),
677
- includeComments: z.boolean().default(true),
678
- includeLogging: z.boolean().default(true),
679
- includeTesting: z.boolean().default(true)
680
- }).optional(),
681
- libraries: z.object({
682
- validation: z.string().optional(),
683
- logging: z.string().optional()
684
- }).optional(),
685
- excludePatterns: z.array(z.string()).optional()
686
- });
687
- var CONFIG_FILENAME = ".aerocodingrc.json";
688
-
689
- // src/config/loader.ts
690
- async function loadConfig(dir = process.cwd()) {
691
- const configPath = path2.join(dir, CONFIG_FILENAME);
692
- try {
693
- const content = await fs2.readFile(configPath, "utf-8");
694
- const parsed = JSON.parse(content);
695
- return configSchema.parse(parsed);
696
- } catch (error) {
697
- if (error.code === "ENOENT") {
698
- return null;
699
- }
700
- throw error;
701
- }
702
- }
703
- async function saveConfig(config, dir = process.cwd()) {
704
- const configPath = path2.join(dir, CONFIG_FILENAME);
705
- const content = JSON.stringify(
706
- {
707
- $schema: "https://aerocoding.dev/schemas/aerocodingrc.json",
708
- ...config
709
- },
710
- null,
711
- 2
712
- );
713
- await fs2.writeFile(configPath, content, "utf-8");
714
- }
715
- async function configExists(dir = process.cwd()) {
716
- const configPath = path2.join(dir, CONFIG_FILENAME);
717
- try {
718
- await fs2.access(configPath);
719
- return true;
720
- } catch {
721
- return false;
722
- }
723
- }
724
-
725
- // src/commands/generate.ts
726
- function mapPresetToTemplateId(preset) {
727
- const presetLower = preset.toLowerCase();
728
- if (presetLower.includes("clean") && presetLower.includes("dotnet")) {
729
- return "clean-arch-dotnet";
730
- }
731
- if (presetLower.includes("clean") && (presetLower.includes("dart") || presetLower.includes("flutter"))) {
732
- return "flutter-clean-dart-test";
733
- }
734
- if (presetLower.includes("clean") && presetLower.includes("typescript")) {
735
- return "minimal-typescript-test";
736
- }
737
- if (presetLower.startsWith("clean-arch-")) {
738
- return preset;
739
- }
740
- return null;
741
- }
742
- function mapPresetToTarget(preset) {
743
- const presetLower = preset.toLowerCase();
744
- if (presetLower.includes("dotnet") || presetLower.includes("aspnet") || presetLower.includes("csharp")) {
745
- return "dotnet-entity";
746
- }
747
- if (presetLower.includes("dart") || presetLower.includes("flutter")) {
748
- return "dart-entity";
749
- }
750
- if (presetLower.includes("typescript") || presetLower.includes("nextjs") || presetLower.includes("react")) {
751
- return "typescript-interface";
752
- }
753
- return null;
922
+ function inferFileType(filePath) {
923
+ const lowerPath = filePath.toLowerCase();
924
+ if (lowerPath.includes("/entities/")) return "entity";
925
+ if (lowerPath.includes("/usecases/") || lowerPath.includes("/use-cases/")) return "usecase";
926
+ if (lowerPath.includes("/repositories/")) return "repository";
927
+ if (lowerPath.includes("/controllers/")) return "controller";
928
+ if (lowerPath.includes("/dtos/") || lowerPath.includes("/dto/")) return "dto";
929
+ if (lowerPath.includes("/tests/") || lowerPath.includes(".test.") || lowerPath.includes(".spec.")) return "test";
930
+ if (lowerPath.includes("/config") || lowerPath.includes("appsettings")) return "config";
931
+ return "other";
754
932
  }
755
- function buildFeatureFlagsFromConfig(config) {
756
- const flags = {
757
- // Default all feature flags to true for full architecture
758
- includeDtos: true,
759
- includeUseCases: true,
760
- includeMappers: true,
761
- includeControllers: true,
762
- includeEfConfig: true,
763
- includeValidation: true,
764
- includeDtoValidation: true,
765
- // Data Annotations on Input records ([Required], [MaxLength], etc.)
766
- // Test flags
767
- includeUnitTests: true,
768
- includeIntegrationTests: true,
769
- // Starter files (disabled by default - user adds their own Program.cs)
770
- includeStarterFiles: false
933
+ async function writeGeneratedFilesWithManifest(files, outputDir, templateVersion, options = {}) {
934
+ const { verbose = false, forceOverwrite = false } = options;
935
+ let manifest = await readManifest(outputDir) || createEmptyManifest(templateVersion);
936
+ const result = {
937
+ created: [],
938
+ updated: [],
939
+ merged: [],
940
+ skipped: [],
941
+ conflicts: [],
942
+ manifest
771
943
  };
772
- if (!config) return flags;
773
- if (config.codeStyle?.includeValidations !== void 0) {
774
- flags.includeValidation = config.codeStyle.includeValidations;
775
- }
776
- if (config.codeStyle?.includeTesting !== void 0) {
777
- flags.includeUnitTests = config.codeStyle.includeTesting;
778
- flags.includeIntegrationTests = config.codeStyle.includeTesting;
779
- }
780
- return flags;
781
- }
782
- async function generateCommand(options) {
783
- const tokenManager = new TokenManager();
784
- const token = await tokenManager.getAccessToken();
785
- if (!token) {
786
- console.log(chalk8.red("\n Not authenticated"));
787
- console.log(chalk8.gray(" Run 'aerocoding login' to get started\n"));
788
- process.exit(1);
789
- }
790
- const config = await loadConfig();
791
- const projectId = options.project || config?.project;
792
- if (!projectId) {
793
- console.log(chalk8.red("\n Project ID required"));
794
- console.log(chalk8.gray(" Run 'aerocoding init' to create a config file"));
795
- console.log(chalk8.gray(" Or use --project <id> to specify a project\n"));
796
- process.exit(1);
797
- }
798
- const output = options.output || config?.output || "./.aerocoding";
799
- const validationLib = options.validationLib || config?.libraries?.validation;
800
- const includeValidations = options.validations ?? config?.codeStyle?.includeValidations ?? true;
801
- const includeComments = options.comments ?? config?.codeStyle?.includeComments ?? true;
802
- const includeLogging = options.logging ?? config?.codeStyle?.includeLogging ?? true;
803
- const includeTesting = options.testing ?? config?.codeStyle?.includeTesting ?? true;
804
- const apiClient = createApiClientWithAutoLogout(token, tokenManager);
805
- let spinner2 = ora2({ text: "Fetching project...", color: "cyan" }).start();
806
- try {
807
- const project = await apiClient.getProject(projectId);
808
- spinner2.succeed(chalk8.gray("Project loaded"));
809
- const hasBackendConfig = !!config?.backend?.preset;
810
- const hasFrontendConfig = !!config?.frontend?.preset;
811
- const hasBothTargets = hasBackendConfig && hasFrontendConfig;
812
- let selectedTarget = "both";
813
- if (hasBothTargets && !options.all) {
814
- const targetChoice = await p.select({
815
- message: "Which target do you want to generate?",
816
- options: [
817
- {
818
- value: "backend",
819
- label: `Backend (${config?.backend?.preset})`,
820
- hint: "Recommended"
821
- },
822
- {
823
- value: "frontend",
824
- label: `Frontend (${config?.frontend?.preset})`
825
- },
826
- {
827
- value: "both",
828
- label: "Both"
829
- }
830
- ],
831
- initialValue: "backend"
832
- });
833
- if (p.isCancel(targetChoice)) {
834
- console.log(chalk8.yellow("\n Generation cancelled\n"));
835
- process.exit(0);
836
- }
837
- selectedTarget = targetChoice;
838
- } else if (hasBackendConfig && !hasFrontendConfig) {
839
- selectedTarget = "backend";
840
- } else if (hasFrontendConfig && !hasBackendConfig) {
841
- selectedTarget = "frontend";
842
- }
843
- const diagrams = project.schema?.diagrams || [];
844
- const hasMultipleDiagrams = diagrams.length > 1;
845
- let selectedDiagramIds = diagrams.map((d) => d.id || d.name || "unknown");
846
- if (hasMultipleDiagrams && !options.all) {
847
- const diagramChoices = await p.multiselect({
848
- message: "Which bounded contexts do you want to generate?",
849
- options: diagrams.map((d) => ({
850
- value: d.id || d.name || "unknown",
851
- label: `${d.name || "Unnamed"} (${d.entities?.length || 0} entities)`
852
- })),
853
- initialValues: diagrams.map((d) => d.id || d.name || "unknown"),
854
- required: true
855
- });
856
- if (p.isCancel(diagramChoices)) {
857
- console.log(chalk8.yellow("\n Generation cancelled\n"));
858
- process.exit(0);
859
- }
860
- selectedDiagramIds = diagramChoices;
944
+ for (const file of files) {
945
+ if (!isPathSafe(outputDir, file.path)) {
946
+ console.error(chalk6.red(` Skipping unsafe path: ${file.path}`));
947
+ continue;
861
948
  }
862
- let templateId = null;
863
- let targets = [];
864
- let backendPreset;
865
- let frontendPreset;
866
- if (selectedTarget === "backend" || selectedTarget === "both") {
867
- backendPreset = options.backendPreset || config?.backend?.preset;
868
- if (backendPreset) {
869
- const tid = mapPresetToTemplateId(backendPreset);
870
- if (tid) templateId = tid;
871
- const target = mapPresetToTarget(backendPreset);
872
- if (target) targets.push(target);
873
- }
949
+ const fullPath = path2.resolve(outputDir, file.path);
950
+ const dir = path2.dirname(fullPath);
951
+ const manifestEntry = manifest.files[file.path];
952
+ const changeStatus = await detectFileChange(fullPath, manifestEntry);
953
+ let shouldWrite = false;
954
+ let action = "skip";
955
+ switch (changeStatus.status) {
956
+ case "new":
957
+ shouldWrite = true;
958
+ action = "create";
959
+ break;
960
+ case "unchanged":
961
+ shouldWrite = true;
962
+ action = "update";
963
+ break;
964
+ case "modified":
965
+ if (forceOverwrite) {
966
+ shouldWrite = true;
967
+ action = "update";
968
+ } else {
969
+ const currentContent = await fs3.readFile(fullPath, "utf-8");
970
+ const mergeResult = performMerge(
971
+ manifestEntry?.hash ? "" : "",
972
+ // We don't have base content stored
973
+ currentContent,
974
+ file.content
975
+ );
976
+ if (mergeResult.success) {
977
+ shouldWrite = true;
978
+ action = "update";
979
+ file._mergedContent = mergeResult.content;
980
+ result.merged.push(file.path);
981
+ } else {
982
+ action = "conflict";
983
+ await fs3.mkdir(dir, { recursive: true });
984
+ const { newPath, conflictPath } = await writeConflictFiles(
985
+ fullPath,
986
+ file.content,
987
+ mergeResult.conflicts,
988
+ currentContent
989
+ );
990
+ result.conflicts.push({
991
+ path: file.path,
992
+ reason: "Merge conflict - manual resolution required",
993
+ newPath: path2.relative(outputDir, newPath),
994
+ conflictPath: path2.relative(outputDir, conflictPath)
995
+ });
996
+ }
997
+ }
998
+ break;
999
+ case "deleted":
1000
+ action = "skip";
1001
+ result.skipped.push(file.path);
1002
+ break;
1003
+ case "unknown":
1004
+ action = "skip";
1005
+ result.skipped.push(file.path);
1006
+ break;
874
1007
  }
875
- if (selectedTarget === "frontend" || selectedTarget === "both") {
876
- frontendPreset = options.frontendPreset || config?.frontend?.preset;
877
- if (frontendPreset) {
878
- const tid = mapPresetToTemplateId(frontendPreset);
879
- if (tid && !templateId) templateId = tid;
880
- const target = mapPresetToTarget(frontendPreset);
881
- if (target) targets.push(target);
1008
+ if (file.generateOnce && changeStatus.status !== "new") {
1009
+ shouldWrite = false;
1010
+ action = "skip";
1011
+ if (!result.skipped.includes(file.path)) {
1012
+ result.skipped.push(file.path);
882
1013
  }
883
1014
  }
884
- if (!templateId && (!targets || targets.length === 0)) {
885
- targets = options.targets || [];
886
- }
887
- const featureFlags = buildFeatureFlagsFromConfig(config);
888
- spinner2 = ora2({ text: "Checking credits...", color: "cyan" }).start();
889
- const credits = await apiClient.getCreditUsage(project.organizationId);
890
- const useContexts = config?.architectureStyle !== "flat";
891
- const estimateDiagramIds = selectedDiagramIds.length < diagrams.length ? selectedDiagramIds : void 0;
892
- let estimate = null;
893
- try {
894
- if (templateId) {
895
- estimate = await apiClient.estimateCreditCost({
896
- projectId,
897
- templateId,
898
- options: {
899
- featureFlags,
900
- useContexts,
901
- diagramIds: estimateDiagramIds
1015
+ if (shouldWrite) {
1016
+ try {
1017
+ await fs3.mkdir(dir, { recursive: true });
1018
+ const contentToWrite = file._mergedContent || file.content;
1019
+ await fs3.writeFile(fullPath, contentToWrite, "utf-8");
1020
+ const stats = await fs3.stat(fullPath);
1021
+ const entry = {
1022
+ hash: hashString(contentToWrite),
1023
+ mtime: Math.floor(stats.mtimeMs),
1024
+ size: stats.size,
1025
+ entityId: file.entityId,
1026
+ type: file.type || inferFileType(file.path),
1027
+ contextName: file.contextName
1028
+ };
1029
+ manifest = setManifestFile(manifest, file.path, entry);
1030
+ const isMerged = result.merged.includes(file.path);
1031
+ if (action === "create") {
1032
+ result.created.push(file.path);
1033
+ if (verbose) {
1034
+ console.log(chalk6.green(` \u2713 Created ${file.path}`));
902
1035
  }
903
- });
904
- } else if (targets && targets.length > 0) {
905
- estimate = await apiClient.estimateCreditCost({
906
- projectId,
907
- targets,
908
- options: {
909
- outputDir: output,
910
- backendPreset,
911
- frontendPreset,
912
- useContexts,
913
- diagramIds: estimateDiagramIds
1036
+ } else if (isMerged) {
1037
+ if (verbose) {
1038
+ console.log(chalk6.magenta(` \u2713 Merged ${file.path}`));
914
1039
  }
915
- });
916
- }
917
- } catch {
918
- }
919
- spinner2.succeed(chalk8.gray("Credits verified"));
920
- if (estimate && estimate.entities === 0) {
921
- console.log(chalk8.yellow("\n \u26A0 No entities found in this project."));
922
- console.log(chalk8.gray(" Create entities in the diagram editor and save (Ctrl+S) before generating.\n"));
923
- process.exit(1);
924
- }
925
- const hasEnoughCredits = estimate ? credits.remaining >= estimate.estimatedCredits : true;
926
- console.log("");
927
- console.log(chalk8.bold(" Generation Summary"));
928
- 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"));
929
- console.log(chalk8.gray(" Project:"), chalk8.white(project.name));
930
- if (config) {
931
- console.log(chalk8.gray(" Config:"), chalk8.cyan(".aerocodingrc.json"));
932
- }
933
- if (templateId) {
934
- console.log(chalk8.gray(" Mode:"), chalk8.green("Full Architecture"));
935
- console.log(chalk8.gray(" Template:"), chalk8.cyan(templateId));
936
- } else if (targets && targets.length > 0) {
937
- console.log(chalk8.gray(" Mode:"), chalk8.yellow("Simple (legacy)"));
938
- console.log(chalk8.gray(" Targets:"), chalk8.cyan(targets.join(", ")));
939
- } else {
940
- console.log(chalk8.gray(" Mode:"), chalk8.gray("default"));
941
- }
942
- if (hasBothTargets) {
943
- const targetLabel = selectedTarget === "backend" ? "Backend only" : selectedTarget === "frontend" ? "Frontend only" : "Backend + Frontend";
944
- console.log(chalk8.gray(" Target:"), chalk8.cyan(targetLabel));
945
- }
946
- if (backendPreset && (selectedTarget === "backend" || selectedTarget === "both")) {
947
- console.log(chalk8.gray(" Backend Preset:"), chalk8.cyan(backendPreset));
948
- }
949
- if (frontendPreset && (selectedTarget === "frontend" || selectedTarget === "both")) {
950
- console.log(chalk8.gray(" Frontend Preset:"), chalk8.cyan(frontendPreset));
951
- }
952
- if (hasMultipleDiagrams) {
953
- const selectedCount = selectedDiagramIds.length;
954
- const totalCount = diagrams.length;
955
- if (selectedCount === totalCount) {
956
- console.log(chalk8.gray(" Bounded Contexts:"), chalk8.cyan(`All (${totalCount})`));
957
- } else {
958
- const selectedNames = diagrams.filter((d) => selectedDiagramIds.includes(d.id || d.name || "unknown")).map((d) => d.name || "Unnamed").join(", ");
959
- console.log(chalk8.gray(" Bounded Contexts:"), chalk8.cyan(`${selectedCount}/${totalCount} (${selectedNames})`));
960
- }
961
- }
962
- const archStyle = config?.architectureStyle || "bounded-contexts";
963
- console.log(
964
- chalk8.gray(" Architecture:"),
965
- archStyle === "bounded-contexts" ? chalk8.cyan("Bounded Contexts") : chalk8.yellow("Flat Structure")
966
- );
967
- const disabledOptions = [];
968
- if (!includeValidations) disabledOptions.push("validations");
969
- if (!includeComments) disabledOptions.push("comments");
970
- if (!includeLogging) disabledOptions.push("logging");
971
- if (!includeTesting) disabledOptions.push("testing");
972
- if (disabledOptions.length > 0) {
973
- console.log(chalk8.gray(" Disabled:"), chalk8.yellow(disabledOptions.join(", ")));
974
- }
975
- if (validationLib) {
976
- console.log(chalk8.gray(" Validation Lib:"), chalk8.cyan(validationLib));
977
- }
978
- console.log(chalk8.gray(" Output:"), chalk8.cyan(output));
979
- 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
- );
1040
+ } else {
1041
+ result.updated.push(file.path);
1042
+ if (verbose) {
1043
+ console.log(chalk6.blue(` \u2713 Updated ${file.path}`));
1044
+ }
1045
+ }
1046
+ } catch (error) {
1047
+ console.error(chalk6.red(` Failed to write ${file.path}`));
1048
+ console.error(chalk6.gray(` ${error.message}`));
992
1049
  }
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
- console.log(chalk8.gray(` Entities:`), chalk8.cyan(estimate.entities));
1050
+ } else if (action === "conflict" && verbose) {
1051
+ console.log(chalk6.yellow(` \u26A0 Conflict ${file.path}`));
1001
1052
  }
1053
+ }
1054
+ manifest = {
1055
+ ...manifest,
1056
+ lastSync: (/* @__PURE__ */ new Date()).toISOString(),
1057
+ templateVersion
1058
+ };
1059
+ await writeManifest(outputDir, manifest);
1060
+ result.manifest = manifest;
1061
+ if (!verbose) {
1062
+ displayIncrementalSummary(result);
1063
+ }
1064
+ return result;
1065
+ }
1066
+ function displayIncrementalSummary(result) {
1067
+ console.log("");
1068
+ console.log(chalk6.bold(" Update Results"));
1069
+ 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"));
1070
+ if (result.created.length > 0) {
1071
+ console.log(chalk6.green(` \u2713 Created: ${result.created.length} files`));
1072
+ }
1073
+ if (result.updated.length > 0) {
1074
+ console.log(chalk6.blue(` \u2713 Updated: ${result.updated.length} files`));
1075
+ }
1076
+ if (result.merged.length > 0) {
1077
+ console.log(chalk6.magenta(` \u2713 Merged: ${result.merged.length} files`));
1078
+ }
1079
+ if (result.skipped.length > 0) {
1080
+ console.log(chalk6.gray(` \u25CB Skipped: ${result.skipped.length} files`));
1081
+ }
1082
+ if (result.conflicts.length > 0) {
1083
+ console.log(chalk6.yellow(` \u26A0 Conflicts: ${result.conflicts.length} files`));
1084
+ }
1085
+ 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"));
1086
+ const total = result.created.length + result.updated.length + result.merged.length;
1087
+ console.log(chalk6.white(` Total written: ${total} files`));
1088
+ if (result.conflicts.length > 0) {
1002
1089
  console.log("");
1003
- console.log(chalk8.bold(" Credits"));
1004
- 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"));
1005
- if (estimate) {
1006
- console.log(
1007
- chalk8.white(" Cost:"),
1008
- hasEnoughCredits ? chalk8.bold.yellow(`${estimate.estimatedCredits} credits`) : chalk8.bold.red(`${estimate.estimatedCredits} credits`)
1009
- );
1010
- }
1011
- console.log(
1012
- chalk8.white(" Available:"),
1013
- hasEnoughCredits ? chalk8.bold.green(`${credits.remaining} credits`) : chalk8.bold.red(`${credits.remaining} credits (insufficient)`)
1014
- );
1015
- console.log("");
1016
- if (!hasEnoughCredits && estimate) {
1017
- console.log(chalk8.red(" \u26A0 Not enough credits for this generation."));
1018
- console.log(chalk8.gray(` Need ${estimate.estimatedCredits - credits.remaining} more credits.
1019
- `));
1020
- process.exit(1);
1021
- }
1022
- if (!options.yes) {
1023
- const confirmed = await promptConfirm(" Proceed with generation?");
1024
- if (!confirmed) {
1025
- console.log(chalk8.yellow("\n Generation cancelled\n"));
1026
- process.exit(0);
1027
- }
1028
- }
1090
+ console.log(chalk6.yellow(" \u26A0 Conflicts need manual resolution:"));
1029
1091
  console.log("");
1030
- spinner2 = ora2({ text: "Generating code...", color: "cyan" }).start();
1031
- const diagramIds = selectedDiagramIds.length < diagrams.length ? selectedDiagramIds : void 0;
1032
- const generatePayload = templateId ? {
1033
- // NEW: Full architecture mode using template
1034
- projectId,
1035
- templateId,
1036
- options: {
1037
- includeValidations,
1038
- includeComments,
1039
- featureFlags,
1040
- useContexts,
1041
- diagramIds
1042
- // Filter by selected bounded contexts
1092
+ for (const conflict of result.conflicts.slice(0, 5)) {
1093
+ console.log(chalk6.yellow(` ${conflict.path}`));
1094
+ if (conflict.newPath) {
1095
+ console.log(chalk6.gray(` \u2192 ${conflict.newPath}`));
1043
1096
  }
1044
- } : {
1045
- // Legacy mode using targets
1046
- projectId,
1047
- targets,
1048
- options: {
1049
- includeValidations,
1050
- includeComments,
1051
- includeLogging,
1052
- includeTesting,
1053
- outputDir: output,
1054
- backendPreset,
1055
- frontendPreset,
1056
- validationLib,
1057
- useContexts,
1058
- diagramIds
1059
- // Filter by selected bounded contexts
1097
+ if (conflict.conflictPath) {
1098
+ console.log(chalk6.gray(` \u2192 ${conflict.conflictPath}`));
1060
1099
  }
1061
- };
1062
- const result = await apiClient.generateCode(generatePayload);
1063
- spinner2.succeed(chalk8.green("Code generated successfully!"));
1064
- console.log("");
1065
- console.log(chalk8.bold(" Results"));
1066
- 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"));
1067
- console.log(chalk8.gray(" Files:"), chalk8.cyan(result.stats?.totalFiles ?? result.files?.length ?? 0));
1068
- if (result.stats?.totalEntities) {
1069
- console.log(chalk8.gray(" Entities:"), chalk8.cyan(result.stats.totalEntities));
1070
- }
1071
- if (result.stats?.languages && result.stats.languages.length > 0) {
1072
- console.log(
1073
- chalk8.gray(" Languages:"),
1074
- chalk8.cyan(result.stats.languages.join(", "))
1075
- );
1076
1100
  }
1077
- if (result.creditsUsed !== void 0) {
1078
- console.log("");
1079
- console.log(chalk8.bold(" Credits"));
1080
- 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"));
1081
- console.log(chalk8.gray(" Used:"), chalk8.yellow(result.creditsUsed));
1082
- console.log(
1083
- chalk8.gray(" Remaining:"),
1084
- result.creditsRemaining !== void 0 && result.creditsRemaining > 50 ? chalk8.green(result.creditsRemaining) : chalk8.yellow(result.creditsRemaining)
1085
- );
1086
- }
1087
- if (result.warnings && result.warnings.length > 0) {
1088
- console.log("");
1089
- console.log(chalk8.yellow(" Warnings:"));
1090
- for (const warning of result.warnings) {
1091
- console.log(chalk8.yellow(` - ${warning}`));
1092
- }
1101
+ if (result.conflicts.length > 5) {
1102
+ console.log(chalk6.gray(` ... and ${result.conflicts.length - 5} more`));
1093
1103
  }
1094
1104
  console.log("");
1095
- await writeGeneratedFiles(result.files, output, options.verbose);
1096
- console.log(chalk8.green(` Files written to ${chalk8.white(output)}`));
1097
- console.log("");
1098
- } catch (error) {
1099
- spinner2.fail(chalk8.red("Generation failed"));
1100
- if (error.response?.status === 401) {
1101
- await handleUnauthorized(tokenManager);
1102
- } else if (error.response?.status === 403) {
1103
- console.log(chalk8.yellow("\n You don't have permission to access this project."));
1104
- console.log(chalk8.gray(" Check if you're part of the organization.\n"));
1105
- } else if (error.response?.status === 404) {
1106
- console.log(chalk8.yellow("\n Project not found."));
1107
- console.log(chalk8.gray(" Check if the project ID is correct.\n"));
1108
- } else if (error.response?.status === 429) {
1109
- const data = error.response.data;
1110
- console.log(chalk8.red("\n Insufficient credits"));
1111
- if (data.requiredCredits) {
1112
- console.log(chalk8.yellow(` Required: ${data.requiredCredits} credits`));
1113
- }
1114
- console.log(chalk8.yellow(` Available: ${data.remaining ?? 0} credits`));
1115
- console.log(chalk8.gray("\n Upgrade your plan or wait for credit reset.\n"));
1116
- } else if (error.response?.data?.message) {
1117
- console.log(chalk8.red(`
1118
- ${error.response.data.message}
1119
- `));
1120
- } else {
1121
- console.log(chalk8.red(`
1122
- ${error.message}
1123
- `));
1124
- }
1125
- process.exit(1);
1105
+ console.log(chalk6.gray(" Run 'aerocoding resolve' after fixing conflicts."));
1126
1106
  }
1127
1107
  }
1128
1108
 
1129
- // src/commands/init.ts
1130
- import * as p2 from "@clack/prompts";
1131
- import chalk9 from "chalk";
1132
- async function initCommand(options) {
1133
- p2.intro(chalk9.bgCyan.black(" AeroCoding CLI "));
1134
- if (!options.force && await configExists()) {
1135
- const overwrite = await p2.confirm({
1136
- message: "Config file already exists. Overwrite?",
1137
- initialValue: false
1138
- });
1139
- if (p2.isCancel(overwrite) || !overwrite) {
1140
- p2.cancel("Operation cancelled.");
1141
- process.exit(0);
1109
+ // src/config/project-config.ts
1110
+ import { z } from "zod";
1111
+ import { readFile as readFile2, writeFile as writeFile3, access as access2 } from "fs/promises";
1112
+ import { join as join2 } from "path";
1113
+ var PROJECT_CONFIG_FILENAME = "aerocoding.json";
1114
+ var projectConfigSchema = z.object({
1115
+ $schema: z.string().optional().default("https://aerocoding.dev/schema.json"),
1116
+ /** Project UUID from aerocoding.dev */
1117
+ projectId: z.string().uuid(),
1118
+ /** Template ID for architecture generation */
1119
+ templateId: z.string().min(1),
1120
+ /** Template version at project creation */
1121
+ templateVersion: z.string().optional(),
1122
+ /** Root namespace/package name */
1123
+ namespace: z.string().min(1),
1124
+ /** Output directories */
1125
+ output: z.object({
1126
+ backend: z.string().default("./backend"),
1127
+ frontend: z.string().default("./frontend")
1128
+ }).optional().default({ backend: "./backend", frontend: "./frontend" }),
1129
+ /** Organization ID (for cloud sync) */
1130
+ organizationId: z.string().uuid().optional()
1131
+ });
1132
+ async function loadProjectConfig(dir = process.cwd()) {
1133
+ try {
1134
+ const configPath = join2(dir, PROJECT_CONFIG_FILENAME);
1135
+ const content = await readFile2(configPath, "utf-8");
1136
+ const parsed = JSON.parse(content);
1137
+ return projectConfigSchema.parse(parsed);
1138
+ } catch {
1139
+ return null;
1140
+ }
1141
+ }
1142
+ async function saveProjectConfig(config, dir = process.cwd()) {
1143
+ const configPath = join2(dir, PROJECT_CONFIG_FILENAME);
1144
+ const content = JSON.stringify(config, null, 2);
1145
+ await writeFile3(configPath, content, "utf-8");
1146
+ }
1147
+ function createProjectConfig(options) {
1148
+ return {
1149
+ $schema: "https://aerocoding.dev/schema.json",
1150
+ projectId: options.projectId,
1151
+ templateId: options.templateId,
1152
+ templateVersion: options.templateVersion,
1153
+ namespace: options.namespace,
1154
+ organizationId: options.organizationId,
1155
+ output: {
1156
+ backend: options.output?.backend || "./backend",
1157
+ frontend: options.output?.frontend || "./frontend"
1142
1158
  }
1159
+ };
1160
+ }
1161
+
1162
+ // src/utils/cloud-sync.ts
1163
+ function toCloudManifest(manifest) {
1164
+ return {
1165
+ version: manifest.version,
1166
+ lastSync: manifest.lastSync,
1167
+ templateVersion: manifest.templateVersion,
1168
+ files: manifest.files
1169
+ };
1170
+ }
1171
+ function fromCloudManifest(cloudManifest, templateVersion) {
1172
+ return {
1173
+ version: cloudManifest.version,
1174
+ lastSync: cloudManifest.lastSync || (/* @__PURE__ */ new Date()).toISOString(),
1175
+ templateVersion: cloudManifest.templateVersion || templateVersion,
1176
+ files: cloudManifest.files,
1177
+ entities: []
1178
+ // Will be rebuilt on next generation
1179
+ };
1180
+ }
1181
+ async function syncToCloud(apiClient, projectId, manifest) {
1182
+ const cloudManifest = toCloudManifest(manifest);
1183
+ const result = await apiClient.saveManifest(projectId, cloudManifest);
1184
+ return {
1185
+ success: result.success,
1186
+ fileCount: result.fileCount
1187
+ };
1188
+ }
1189
+ async function fetchFromCloud(apiClient, projectId, templateVersion) {
1190
+ const cloudManifest = await apiClient.getManifest(projectId);
1191
+ if (!cloudManifest) {
1192
+ return null;
1193
+ }
1194
+ return fromCloudManifest(cloudManifest, templateVersion);
1195
+ }
1196
+ async function compareWithCloud(apiClient, projectId, localManifest) {
1197
+ const cloudManifest = await apiClient.getManifest(projectId);
1198
+ const cloudLastSync = cloudManifest?.lastSync || null;
1199
+ const localLastSync = localManifest?.lastSync || null;
1200
+ const cloudFileCount = cloudManifest ? Object.keys(cloudManifest.files).length : 0;
1201
+ const localFileCount = localManifest ? Object.keys(localManifest.files).length : 0;
1202
+ let isCloudNewer = false;
1203
+ if (cloudLastSync && localLastSync) {
1204
+ isCloudNewer = new Date(cloudLastSync) > new Date(localLastSync);
1205
+ } else if (cloudLastSync && !localLastSync) {
1206
+ isCloudNewer = true;
1143
1207
  }
1208
+ return {
1209
+ hasCloudBackup: cloudManifest !== null,
1210
+ cloudLastSync,
1211
+ localLastSync,
1212
+ isCloudNewer,
1213
+ cloudFileCount,
1214
+ localFileCount
1215
+ };
1216
+ }
1217
+ function formatSyncDate(isoDate) {
1218
+ if (!isoDate) return "never";
1219
+ const date = new Date(isoDate);
1220
+ const now = /* @__PURE__ */ new Date();
1221
+ const diffMs = now.getTime() - date.getTime();
1222
+ const diffMins = Math.floor(diffMs / 6e4);
1223
+ const diffHours = Math.floor(diffMs / 36e5);
1224
+ const diffDays = Math.floor(diffMs / 864e5);
1225
+ if (diffMins < 1) return "just now";
1226
+ if (diffMins < 60) return `${diffMins}m ago`;
1227
+ if (diffHours < 24) return `${diffHours}h ago`;
1228
+ if (diffDays < 7) return `${diffDays}d ago`;
1229
+ return date.toLocaleDateString();
1230
+ }
1231
+
1232
+ // src/commands/create.ts
1233
+ async function createCommand(projectName, options) {
1234
+ p.intro(chalk7.bgCyan.black(" AeroCoding Create "));
1144
1235
  const tokenManager = new TokenManager();
1145
1236
  const token = await tokenManager.getAccessToken();
1146
1237
  if (!token) {
1147
- p2.cancel("Not logged in. Run 'aerocoding login' first.");
1238
+ p.cancel("Not logged in. Run 'aerocoding login' first.");
1148
1239
  process.exit(1);
1149
1240
  }
1150
1241
  const apiClient = createApiClientWithAutoLogout(token, tokenManager);
1151
1242
  try {
1152
- const orgSpinner = p2.spinner();
1243
+ const orgSpinner = p.spinner();
1153
1244
  orgSpinner.start("Loading organizations...");
1154
1245
  const organizations = await apiClient.listOrganizations();
1155
1246
  orgSpinner.stop("Organizations loaded");
1156
1247
  if (organizations.length === 0) {
1157
- p2.cancel("No organizations found. Create one on aerocoding.dev first.");
1248
+ p.cancel("No organizations found. Create one on aerocoding.dev first.");
1158
1249
  process.exit(1);
1159
1250
  }
1160
1251
  let organizationId;
1161
1252
  if (organizations.length === 1 && organizations[0]) {
1162
1253
  organizationId = organizations[0].id;
1163
- p2.log.info(`Organization: ${organizations[0].name}`);
1254
+ p.log.info(`Organization: ${organizations[0].name}`);
1164
1255
  } else {
1165
- const selectedOrg = await p2.select({
1256
+ const selectedOrg = await p.select({
1166
1257
  message: "Select organization",
1167
1258
  options: organizations.map((org) => ({
1168
1259
  value: org.id,
@@ -1170,221 +1261,634 @@ async function initCommand(options) {
1170
1261
  hint: org.planTier.toUpperCase()
1171
1262
  }))
1172
1263
  });
1173
- if (p2.isCancel(selectedOrg)) {
1174
- p2.cancel("Operation cancelled.");
1264
+ if (p.isCancel(selectedOrg)) {
1265
+ p.cancel("Operation cancelled.");
1175
1266
  process.exit(0);
1176
1267
  }
1177
1268
  organizationId = selectedOrg;
1178
1269
  }
1179
1270
  let projectId = options.project;
1180
1271
  if (!projectId) {
1181
- const spinner3 = p2.spinner();
1182
- spinner3.start("Loading projects...");
1272
+ const spinner4 = p.spinner();
1273
+ spinner4.start("Loading projects...");
1183
1274
  const projects = await apiClient.listProjects(organizationId);
1184
- spinner3.stop("Projects loaded");
1275
+ spinner4.stop("Projects loaded");
1185
1276
  if (projects.length === 0) {
1186
- p2.cancel("No projects in this organization. Create one on aerocoding.dev first.");
1277
+ p.cancel("No projects in this organization. Create one on aerocoding.dev first.");
1187
1278
  process.exit(1);
1188
1279
  }
1189
- const selectedProject = await p2.select({
1190
- message: "Select project",
1280
+ const selectedProject = await p.select({
1281
+ message: "Select project to generate from",
1191
1282
  options: projects.map((proj) => ({
1192
1283
  value: proj.id,
1193
1284
  label: proj.name,
1194
1285
  hint: [proj.backendFramework, proj.frontendFramework].filter(Boolean).join(" + ")
1195
1286
  }))
1196
1287
  });
1197
- if (p2.isCancel(selectedProject)) {
1198
- p2.cancel("Operation cancelled.");
1288
+ if (p.isCancel(selectedProject)) {
1289
+ p.cancel("Operation cancelled.");
1199
1290
  process.exit(0);
1200
1291
  }
1201
1292
  projectId = selectedProject;
1202
1293
  }
1203
- const spinner2 = p2.spinner();
1204
- spinner2.start("Fetching project details...");
1294
+ const projectSpinner = p.spinner();
1295
+ projectSpinner.start("Fetching project details...");
1205
1296
  const project = await apiClient.getProject(projectId);
1206
- spinner2.stop(`Project: ${project.name}`);
1207
- const hasBackend = !!project.backendFramework;
1208
- const hasFrontend = !!project.frontendFramework;
1209
- if (!hasBackend && !hasFrontend) {
1210
- p2.cancel("Project has no frameworks configured. Update it on aerocoding.dev first.");
1297
+ projectSpinner.stop(`Project: ${project.name}`);
1298
+ const dirName = projectName || project.name;
1299
+ const safeName = dirName.toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
1300
+ if (!safeName) {
1301
+ p.cancel("Invalid project name. Use alphanumeric characters and hyphens.");
1211
1302
  process.exit(1);
1212
1303
  }
1213
- if (hasBackend) {
1214
- p2.log.success(`Backend: ${project.backendFramework?.toUpperCase()} detected`);
1215
- }
1216
- if (hasFrontend) {
1217
- p2.log.success(`Frontend: ${project.frontendFramework?.toUpperCase()} detected`);
1218
- }
1219
- const config = {
1220
- project: projectId,
1221
- output: "./.aerocoding",
1222
- architectureStyle: "flat",
1223
- // Default, will be updated based on template or user choice
1224
- codeStyle: {
1225
- includeValidations: true,
1226
- includeComments: true,
1227
- includeLogging: true,
1228
- includeTesting: true
1229
- }
1230
- };
1231
- let selectedBackendTemplate = null;
1232
- let selectedFrontendTemplate = null;
1233
- if (hasBackend && project.backendFramework) {
1234
- const archSpinner = p2.spinner();
1235
- archSpinner.start("Loading backend templates...");
1236
- const templateResult = await apiClient.getTemplates({
1237
- category: "backend",
1238
- language: project.backendFramework
1239
- });
1240
- archSpinner.stop("Templates loaded");
1241
- if (templateResult.templates.length > 0) {
1242
- const preset = await p2.select({
1243
- message: "Backend template",
1244
- options: templateResult.templates.map((tmpl) => ({
1245
- value: tmpl.id,
1246
- label: tmpl.name,
1247
- hint: tmpl.description || `${tmpl.tier} tier`
1248
- }))
1304
+ const projectDir = resolve(process.cwd(), safeName);
1305
+ try {
1306
+ await access3(projectDir);
1307
+ if (!options.force) {
1308
+ const overwrite = await p.confirm({
1309
+ message: `Directory '${safeName}' already exists. Overwrite?`,
1310
+ initialValue: false
1249
1311
  });
1250
- if (p2.isCancel(preset)) {
1251
- p2.cancel("Operation cancelled.");
1312
+ if (p.isCancel(overwrite) || !overwrite) {
1313
+ p.cancel("Operation cancelled.");
1252
1314
  process.exit(0);
1253
1315
  }
1254
- const fullTemplate = await apiClient.getTemplate(preset);
1255
- selectedBackendTemplate = fullTemplate;
1256
- config.backend = {
1257
- preset
1258
- };
1259
1316
  }
1317
+ } catch {
1260
1318
  }
1261
- if (hasFrontend && project.frontendFramework) {
1262
- const archSpinner = p2.spinner();
1263
- archSpinner.start("Loading frontend templates...");
1319
+ let templateId = options.template;
1320
+ if (!templateId) {
1321
+ const templateSpinner = p.spinner();
1322
+ templateSpinner.start("Loading templates...");
1264
1323
  const templateResult = await apiClient.getTemplates({
1265
- category: "frontend",
1266
- framework: project.frontendFramework
1324
+ category: "backend",
1325
+ language: project.backendFramework || void 0
1267
1326
  });
1268
- archSpinner.stop("Templates loaded");
1269
- if (templateResult.templates.length > 0) {
1270
- const preset = await p2.select({
1271
- message: "Frontend template",
1272
- options: templateResult.templates.map((tmpl) => ({
1273
- value: tmpl.id,
1274
- label: tmpl.name,
1275
- hint: tmpl.description || `${tmpl.tier} tier`
1276
- }))
1277
- });
1278
- if (p2.isCancel(preset)) {
1279
- p2.cancel("Operation cancelled.");
1280
- process.exit(0);
1281
- }
1282
- const fullTemplate = await apiClient.getTemplate(preset);
1283
- selectedFrontendTemplate = fullTemplate;
1284
- config.frontend = {
1285
- preset
1286
- };
1327
+ templateSpinner.stop("Templates loaded");
1328
+ if (templateResult.templates.length === 0) {
1329
+ p.cancel("No templates available for this project's framework.");
1330
+ process.exit(1);
1287
1331
  }
1332
+ const selectedTemplate = await p.select({
1333
+ message: "Select architecture template",
1334
+ options: templateResult.templates.map((tmpl) => ({
1335
+ value: tmpl.id,
1336
+ label: tmpl.name,
1337
+ hint: tmpl.description || `${tmpl.tier} tier`
1338
+ }))
1339
+ });
1340
+ if (p.isCancel(selectedTemplate)) {
1341
+ p.cancel("Operation cancelled.");
1342
+ process.exit(0);
1343
+ }
1344
+ templateId = selectedTemplate;
1288
1345
  }
1289
- const templateArchStyle = selectedBackendTemplate?.architectureStyle || selectedFrontendTemplate?.architectureStyle;
1290
- const recommendedStyle = templateArchStyle || "flat";
1291
- const architectureStyle = await p2.select({
1292
- message: "How would you like to organize your code?",
1293
- options: [
1294
- {
1295
- value: "bounded-contexts",
1296
- label: recommendedStyle === "bounded-contexts" ? "Bounded Contexts (Recommended)" : "Bounded Contexts",
1297
- hint: "Each module has its own Domain, Application, Infrastructure"
1298
- },
1299
- {
1300
- value: "flat",
1301
- label: recommendedStyle === "flat" ? "Flat Structure (Recommended)" : "Flat Structure",
1302
- hint: "Single Domain, Application, Infrastructure for the entire project"
1346
+ const namespace = await p.text({
1347
+ message: "Root namespace/package name",
1348
+ placeholder: "MegaStore",
1349
+ initialValue: toPascalCase(project.name),
1350
+ validate: (value) => {
1351
+ if (!value || value.trim() === "") return "Namespace is required";
1352
+ if (!/^[A-Za-z][A-Za-z0-9]*$/.test(value)) {
1353
+ return "Namespace must start with a letter and contain only alphanumeric characters";
1303
1354
  }
1304
- ],
1305
- initialValue: recommendedStyle
1355
+ return void 0;
1356
+ }
1306
1357
  });
1307
- if (p2.isCancel(architectureStyle)) {
1308
- p2.cancel("Operation cancelled.");
1358
+ if (p.isCancel(namespace)) {
1359
+ p.cancel("Operation cancelled.");
1309
1360
  process.exit(0);
1310
1361
  }
1311
- config.architectureStyle = architectureStyle;
1312
- const codeStyleOptions = await p2.multiselect({
1313
- message: "Code style options",
1314
- options: [
1315
- { value: "validations", label: "Include validations (Recommended)", hint: "Add validation rules" },
1316
- { value: "comments", label: "Include comments (Recommended)", hint: "Add code documentation" },
1317
- { value: "logging", label: "Include logging (Recommended)", hint: "Add log statements" },
1318
- { value: "testing", label: "Include tests (Recommended)", hint: "Generate test files" }
1319
- ],
1320
- initialValues: ["validations", "comments", "logging", "testing"]
1362
+ p.log.step(chalk7.bold("Configuration Summary:"));
1363
+ p.log.info(` Directory: ${safeName}/`);
1364
+ p.log.info(` Project: ${project.name}`);
1365
+ p.log.info(` Template: ${templateId}`);
1366
+ p.log.info(` Namespace: ${namespace}`);
1367
+ const proceed = await p.confirm({
1368
+ message: "Create project with these settings?",
1369
+ initialValue: true
1321
1370
  });
1322
- if (p2.isCancel(codeStyleOptions)) {
1323
- p2.cancel("Operation cancelled.");
1371
+ if (p.isCancel(proceed) || !proceed) {
1372
+ p.cancel("Operation cancelled.");
1324
1373
  process.exit(0);
1325
1374
  }
1326
- const selectedStyles = codeStyleOptions;
1327
- config.codeStyle = {
1328
- includeValidations: selectedStyles.includes("validations"),
1329
- includeComments: selectedStyles.includes("comments"),
1330
- includeLogging: selectedStyles.includes("logging"),
1331
- includeTesting: selectedStyles.includes("testing")
1332
- };
1333
- config.output = "./.aerocoding";
1334
- p2.log.step(chalk9.bold("Configuration Summary:"));
1335
- if (config.backend) {
1336
- p2.log.info(` Backend: ${config.backend.preset}`);
1375
+ const dirSpinner = p.spinner();
1376
+ dirSpinner.start(`Creating ${safeName}/...`);
1377
+ await mkdir(projectDir, { recursive: true });
1378
+ dirSpinner.stop(`Created ${safeName}/`);
1379
+ const genSpinner = ora2({ text: "Generating architecture...", color: "cyan" }).start();
1380
+ const result = await apiClient.generateCode({
1381
+ projectId,
1382
+ templateId,
1383
+ options: {
1384
+ includeValidations: true,
1385
+ includeComments: true,
1386
+ featureFlags: {
1387
+ includeDtos: true,
1388
+ includeUseCases: true,
1389
+ includeMappers: true,
1390
+ includeControllers: true,
1391
+ includeEfConfig: true,
1392
+ includeValidation: true,
1393
+ includeDtoValidation: true,
1394
+ includeUnitTests: true,
1395
+ includeIntegrationTests: true,
1396
+ includeStarterFiles: false
1397
+ },
1398
+ useContexts: true
1399
+ }
1400
+ });
1401
+ genSpinner.succeed(chalk7.green(`Generated ${result.files?.length || 0} files`));
1402
+ const writeSpinner = p.spinner();
1403
+ writeSpinner.start("Writing files...");
1404
+ const organizedFiles = result.files.map((file) => ({
1405
+ ...file,
1406
+ path: `backend/${file.path}`
1407
+ }));
1408
+ await writeGeneratedFiles(organizedFiles, projectDir, false);
1409
+ writeSpinner.stop(`Wrote ${organizedFiles.length} files`);
1410
+ const configSpinner = p.spinner();
1411
+ configSpinner.start("Creating config files...");
1412
+ const projectConfig = createProjectConfig({
1413
+ projectId,
1414
+ templateId,
1415
+ templateVersion: "1.0.0",
1416
+ // TODO: get from template
1417
+ namespace,
1418
+ organizationId,
1419
+ output: { backend: "./backend", frontend: "./frontend" }
1420
+ });
1421
+ await saveProjectConfig(projectConfig, projectDir);
1422
+ let manifest = createEmptyManifest("1.0.0");
1423
+ for (const file of organizedFiles) {
1424
+ const hash = hashString(file.content);
1425
+ manifest = setManifestFile(manifest, file.path, {
1426
+ hash,
1427
+ mtime: Date.now(),
1428
+ size: Buffer.byteLength(file.content, "utf-8"),
1429
+ type: detectFileType(file.path)
1430
+ });
1337
1431
  }
1338
- if (config.frontend) {
1339
- p2.log.info(` Frontend: ${config.frontend.preset}`);
1432
+ await writeManifest(projectDir, manifest);
1433
+ configSpinner.stop("Config files created");
1434
+ try {
1435
+ const syncResult = await syncToCloud(apiClient, projectId, manifest);
1436
+ if (syncResult.success) {
1437
+ console.log(
1438
+ chalk7.gray(" \u2713 Manifest synced to cloud") + chalk7.gray(` (${syncResult.fileCount} files)`)
1439
+ );
1440
+ }
1441
+ } catch {
1442
+ console.log(chalk7.yellow(" \u26A0 Could not sync manifest to cloud"));
1340
1443
  }
1341
- p2.log.info(
1342
- ` Architecture: ${config.architectureStyle === "bounded-contexts" ? "Bounded Contexts" : "Flat Structure"}`
1343
- );
1344
- p2.log.info(` Output: ${config.output}`);
1345
- await saveConfig(config);
1346
- p2.outro(
1347
- chalk9.green("Config saved to .aerocodingrc.json") + "\n\n" + chalk9.gray(" Run ") + chalk9.cyan("aerocoding generate") + chalk9.gray(" to generate code!")
1444
+ console.log("");
1445
+ console.log(chalk7.bold(" Project Created Successfully!"));
1446
+ console.log(chalk7.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1447
+ console.log(chalk7.gray(" Directory:"), chalk7.cyan(safeName + "/"));
1448
+ console.log(chalk7.gray(" Files:"), chalk7.cyan(organizedFiles.length));
1449
+ console.log(chalk7.gray(" Config:"), chalk7.cyan(PROJECT_CONFIG_FILENAME));
1450
+ console.log(chalk7.gray(" Manifest:"), chalk7.cyan(MANIFEST_FILENAME));
1451
+ if (result.creditsUsed !== void 0) {
1452
+ console.log("");
1453
+ console.log(chalk7.gray(" Credits used:"), chalk7.yellow(result.creditsUsed));
1454
+ console.log(chalk7.gray(" Credits remaining:"), chalk7.green(result.creditsRemaining));
1455
+ }
1456
+ p.outro(
1457
+ chalk7.green("Project ready!") + "\n\n" + chalk7.gray(" Next steps:\n") + chalk7.cyan(` cd ${safeName}
1458
+ `) + chalk7.gray(" # Make changes in the diagram editor\n") + chalk7.cyan(" aerocoding update") + chalk7.gray(" # Sync changes incrementally")
1348
1459
  );
1349
1460
  } catch (error) {
1350
- if (error.response?.status === 401) {
1461
+ const err = error;
1462
+ if (err.response?.status === 401) {
1351
1463
  await handleUnauthorized(tokenManager);
1352
- } else if (error.response?.data?.message) {
1353
- p2.cancel(error.response.data.message);
1464
+ } else if (err.response?.data?.message) {
1465
+ p.cancel(err.response.data.message);
1354
1466
  } else {
1355
- p2.cancel(error.message || "An unexpected error occurred");
1467
+ p.cancel(err.message || "An unexpected error occurred");
1356
1468
  }
1357
1469
  process.exit(1);
1358
1470
  }
1359
1471
  }
1472
+ function toPascalCase(str) {
1473
+ return str.split(/[-_\s]+/).map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()).join("").replace(/[^a-zA-Z0-9]/g, "");
1474
+ }
1475
+ function detectFileType(filePath) {
1476
+ const lower = filePath.toLowerCase();
1477
+ if (lower.includes("/entities/") || lower.includes("/domain/")) return "entity";
1478
+ if (lower.includes("/usecases/") || lower.includes("/application/")) return "usecase";
1479
+ if (lower.includes("/repositories/")) return "repository";
1480
+ if (lower.includes("/controllers/") || lower.includes("/api/")) return "controller";
1481
+ if (lower.includes("/dtos/") || lower.includes("/dto/")) return "dto";
1482
+ if (lower.includes(".test.") || lower.includes(".spec.") || lower.includes("/tests/")) return "test";
1483
+ if (lower.includes("/config/") || lower.includes("appsettings") || lower.includes(".csproj")) return "config";
1484
+ return "other";
1485
+ }
1360
1486
 
1361
- // src/commands/pull.ts
1362
- import chalk10 from "chalk";
1363
- async function pullCommand(options) {
1364
- console.log(chalk10.yellow("Command not yet implemented"));
1365
- if (options.project) {
1366
- console.log(chalk10.gray(` Requested project: ${options.project}`));
1367
- }
1368
- console.log(chalk10.gray(" Coming soon in v0.2.0\n"));
1487
+ // src/commands/update.ts
1488
+ import * as p2 from "@clack/prompts";
1489
+ import chalk8 from "chalk";
1490
+ import ora3 from "ora";
1491
+ async function updateCommand(options) {
1492
+ p2.intro(chalk8.bgCyan.black(" AeroCoding Update "));
1493
+ const config = await loadProjectConfig();
1494
+ if (!config) {
1495
+ p2.cancel(
1496
+ `No ${PROJECT_CONFIG_FILENAME} found. Run 'aerocoding create' first.`
1497
+ );
1498
+ process.exit(1);
1499
+ }
1500
+ const tokenManager = new TokenManager();
1501
+ const token = await tokenManager.getAccessToken();
1502
+ if (!token) {
1503
+ p2.cancel("Not logged in. Run 'aerocoding login' first.");
1504
+ process.exit(1);
1505
+ }
1506
+ const apiClient = createApiClientWithAutoLogout(token, tokenManager);
1507
+ try {
1508
+ let manifest = await readManifest(process.cwd());
1509
+ let fileCount = manifest ? Object.keys(manifest.files).length : 0;
1510
+ if (manifest) {
1511
+ p2.log.info(
1512
+ `Found manifest with ${fileCount} tracked files (last sync: ${manifest.lastSync || "unknown"})`
1513
+ );
1514
+ } else {
1515
+ p2.log.warn("No local manifest found.");
1516
+ const cloudCheck = await compareWithCloud(apiClient, config.projectId, null);
1517
+ if (cloudCheck.hasCloudBackup) {
1518
+ p2.log.info(
1519
+ chalk8.cyan(
1520
+ ` Cloud backup available (${cloudCheck.cloudFileCount} files, synced ${formatSyncDate(cloudCheck.cloudLastSync)})`
1521
+ )
1522
+ );
1523
+ const recovery = await p2.select({
1524
+ message: "How would you like to proceed?",
1525
+ options: [
1526
+ {
1527
+ value: "restore",
1528
+ label: "Restore from cloud backup",
1529
+ hint: "recommended"
1530
+ },
1531
+ {
1532
+ value: "continue",
1533
+ label: "Continue anyway",
1534
+ hint: "all files will be treated as new"
1535
+ },
1536
+ { value: "cancel", label: "Cancel" }
1537
+ ]
1538
+ });
1539
+ if (p2.isCancel(recovery) || recovery === "cancel") {
1540
+ p2.cancel("Update cancelled.");
1541
+ process.exit(0);
1542
+ }
1543
+ if (recovery === "restore") {
1544
+ const restoreSpinner = p2.spinner();
1545
+ restoreSpinner.start("Restoring manifest from cloud...");
1546
+ const cloudManifest = await fetchFromCloud(
1547
+ apiClient,
1548
+ config.projectId,
1549
+ config.templateVersion || "1.0.0"
1550
+ );
1551
+ if (cloudManifest) {
1552
+ await writeManifest(process.cwd(), cloudManifest);
1553
+ manifest = cloudManifest;
1554
+ fileCount = Object.keys(manifest.files).length;
1555
+ restoreSpinner.stop(
1556
+ chalk8.green(`Restored ${fileCount} files from cloud backup`)
1557
+ );
1558
+ } else {
1559
+ restoreSpinner.stop(chalk8.yellow("Cloud backup not found"));
1560
+ }
1561
+ }
1562
+ } else {
1563
+ p2.log.info(
1564
+ chalk8.gray(" No cloud backup available. All generated files will be created as new.")
1565
+ );
1566
+ }
1567
+ }
1568
+ const projectSpinner = p2.spinner();
1569
+ projectSpinner.start("Fetching project details...");
1570
+ const project = await apiClient.getProject(config.projectId);
1571
+ projectSpinner.stop(`Project: ${project.name}`);
1572
+ p2.log.step(chalk8.bold("Update Configuration:"));
1573
+ p2.log.info(` Project: ${project.name}`);
1574
+ p2.log.info(` Template: ${config.templateId}`);
1575
+ p2.log.info(` Namespace: ${config.namespace}`);
1576
+ p2.log.info(` Output: ${config.output.backend}`);
1577
+ if (options.dryRun) {
1578
+ p2.log.info(chalk8.yellow(" Mode: DRY RUN (no files will be written)"));
1579
+ }
1580
+ if (options.force) {
1581
+ p2.log.info(chalk8.yellow(" Mode: FORCE (will overwrite modified files)"));
1582
+ }
1583
+ if (!options.dryRun) {
1584
+ const proceed = await p2.confirm({
1585
+ message: "Proceed with update?",
1586
+ initialValue: true
1587
+ });
1588
+ if (p2.isCancel(proceed) || !proceed) {
1589
+ p2.cancel("Update cancelled.");
1590
+ process.exit(0);
1591
+ }
1592
+ }
1593
+ const genSpinner = ora3({
1594
+ text: "Generating code from latest schema...",
1595
+ color: "cyan"
1596
+ }).start();
1597
+ const result = await apiClient.generateCode({
1598
+ projectId: config.projectId,
1599
+ templateId: config.templateId,
1600
+ options: {
1601
+ includeValidations: true,
1602
+ includeComments: true,
1603
+ featureFlags: {
1604
+ includeDtos: true,
1605
+ includeUseCases: true,
1606
+ includeMappers: true,
1607
+ includeControllers: true,
1608
+ includeEfConfig: true,
1609
+ includeValidation: true,
1610
+ includeDtoValidation: true,
1611
+ includeUnitTests: true,
1612
+ includeIntegrationTests: true,
1613
+ includeStarterFiles: false
1614
+ // Don't regenerate starter files
1615
+ },
1616
+ useContexts: true
1617
+ }
1618
+ });
1619
+ genSpinner.succeed(
1620
+ chalk8.green(`Generated ${result.files?.length || 0} files from schema`)
1621
+ );
1622
+ if (options.dryRun) {
1623
+ p2.log.info("");
1624
+ p2.log.info(chalk8.bold(" Dry Run Results:"));
1625
+ p2.log.info(chalk8.gray(" \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500"));
1626
+ p2.log.info(` Files to process: ${result.files?.length || 0}`);
1627
+ p2.log.info(` Tracked files: ${fileCount}`);
1628
+ p2.log.info("");
1629
+ p2.log.info(chalk8.gray(" Run without --dry-run to apply changes."));
1630
+ p2.outro(chalk8.green("Dry run complete!"));
1631
+ return;
1632
+ }
1633
+ const updateSpinner = p2.spinner();
1634
+ updateSpinner.start("Applying updates...");
1635
+ const organizedFiles = result.files.map(
1636
+ (file) => ({
1637
+ ...file,
1638
+ path: `${config.output.backend.replace(/^\.\//, "")}/${file.path}`
1639
+ })
1640
+ );
1641
+ const writeResult = await writeGeneratedFilesWithManifest(
1642
+ organizedFiles,
1643
+ process.cwd(),
1644
+ config.templateVersion || "1.0.0",
1645
+ {
1646
+ verbose: options.verbose,
1647
+ forceOverwrite: options.force
1648
+ }
1649
+ );
1650
+ updateSpinner.stop("Update complete");
1651
+ if (result.creditsUsed !== void 0) {
1652
+ console.log("");
1653
+ console.log(chalk8.gray(" Credits used:"), chalk8.yellow(result.creditsUsed));
1654
+ console.log(
1655
+ chalk8.gray(" Credits remaining:"),
1656
+ chalk8.green(result.creditsRemaining)
1657
+ );
1658
+ }
1659
+ try {
1660
+ const syncResult = await syncToCloud(
1661
+ apiClient,
1662
+ config.projectId,
1663
+ writeResult.manifest
1664
+ );
1665
+ if (syncResult.success) {
1666
+ console.log(
1667
+ chalk8.gray(" \u2713 Manifest synced to cloud") + chalk8.gray(` (${syncResult.fileCount} files)`)
1668
+ );
1669
+ }
1670
+ } catch (syncError) {
1671
+ console.log(chalk8.yellow(" \u26A0 Could not sync manifest to cloud"));
1672
+ if (options.verbose) {
1673
+ console.log(chalk8.gray(` ${syncError.message}`));
1674
+ }
1675
+ }
1676
+ if (writeResult.conflicts.length > 0) {
1677
+ p2.outro(
1678
+ chalk8.yellow(`Update complete with ${writeResult.conflicts.length} conflict(s).`) + "\n" + chalk8.gray(" Run 'aerocoding resolve' after fixing conflicts.")
1679
+ );
1680
+ } else {
1681
+ const totalWritten = writeResult.created.length + writeResult.updated.length + writeResult.merged.length;
1682
+ p2.outro(
1683
+ chalk8.green(`Update complete! ${totalWritten} file(s) written.`)
1684
+ );
1685
+ }
1686
+ } catch (error) {
1687
+ const err = error;
1688
+ if (err.response?.status === 401) {
1689
+ await handleUnauthorized(tokenManager);
1690
+ } else if (err.response?.data?.message) {
1691
+ p2.cancel(err.response.data.message);
1692
+ } else {
1693
+ p2.cancel(err.message || "An unexpected error occurred");
1694
+ }
1695
+ process.exit(1);
1696
+ }
1369
1697
  }
1370
1698
 
1371
- // src/commands/status.ts
1372
- import chalk11 from "chalk";
1373
- async function statusCommand() {
1374
- console.log(chalk11.yellow("Command not yet implemented"));
1375
- console.log(chalk11.gray(" Coming soon in v0.2.0\n"));
1699
+ // src/commands/resolve.ts
1700
+ import * as p3 from "@clack/prompts";
1701
+ import chalk9 from "chalk";
1702
+ import { readdir, stat as stat2 } from "fs/promises";
1703
+ import { join as join3, relative } from "path";
1704
+ async function resolveCommand(options) {
1705
+ p3.intro(chalk9.bgCyan.black(" AeroCoding Resolve "));
1706
+ const config = await loadProjectConfig();
1707
+ if (!config) {
1708
+ p3.cancel(
1709
+ `No ${PROJECT_CONFIG_FILENAME} found. Run 'aerocoding create' first.`
1710
+ );
1711
+ process.exit(1);
1712
+ }
1713
+ const spinner4 = p3.spinner();
1714
+ spinner4.start("Scanning for conflict files...");
1715
+ const conflicts = await findConflictFiles(process.cwd());
1716
+ spinner4.stop(`Found ${conflicts.length} conflict(s)`);
1717
+ if (conflicts.length === 0) {
1718
+ p3.log.success("No conflicts to resolve!");
1719
+ p3.outro(chalk9.green("All clear!"));
1720
+ return;
1721
+ }
1722
+ p3.log.info("");
1723
+ p3.log.info(chalk9.bold(" Pending Conflicts:"));
1724
+ p3.log.info(chalk9.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"));
1725
+ for (const conflict of conflicts) {
1726
+ const relPath = relative(process.cwd(), conflict.originalPath);
1727
+ p3.log.info(chalk9.yellow(` \u26A0 ${relPath}`));
1728
+ if (options.verbose) {
1729
+ p3.log.info(chalk9.gray(` \u2192 ${relative(process.cwd(), conflict.newPath)}`));
1730
+ p3.log.info(
1731
+ chalk9.gray(` \u2192 ${relative(process.cwd(), conflict.conflictPath)}`)
1732
+ );
1733
+ }
1734
+ }
1735
+ p3.log.info(chalk9.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"));
1736
+ p3.log.info("");
1737
+ if (!options.all) {
1738
+ const proceed = await p3.confirm({
1739
+ message: `Remove ${conflicts.length} conflict file(s)? (This assumes you've resolved them)`,
1740
+ initialValue: true
1741
+ });
1742
+ if (p3.isCancel(proceed) || !proceed) {
1743
+ p3.cancel("Resolution cancelled.");
1744
+ process.exit(0);
1745
+ }
1746
+ }
1747
+ let manifest = await readManifest(process.cwd());
1748
+ if (!manifest) {
1749
+ p3.cancel("No manifest found. Cannot update file hashes.");
1750
+ process.exit(1);
1751
+ }
1752
+ const resolveSpinner = p3.spinner();
1753
+ resolveSpinner.start("Resolving conflicts...");
1754
+ let resolved = 0;
1755
+ let failed = 0;
1756
+ for (const conflict of conflicts) {
1757
+ try {
1758
+ const relPath = relative(process.cwd(), conflict.originalPath);
1759
+ try {
1760
+ await stat2(conflict.originalPath);
1761
+ } catch {
1762
+ if (options.verbose) {
1763
+ console.log(
1764
+ chalk9.red(` \u2717 ${relPath} - original file not found`)
1765
+ );
1766
+ }
1767
+ failed++;
1768
+ continue;
1769
+ }
1770
+ const { removedNew, removedConflict } = await removeConflictFiles(
1771
+ conflict.originalPath
1772
+ );
1773
+ const newHash = await hashFile(conflict.originalPath);
1774
+ const stats = await stat2(conflict.originalPath);
1775
+ const manifestPath = relPath.replace(/\\/g, "/");
1776
+ const existingEntry = manifest.files[manifestPath];
1777
+ manifest = setManifestFile(manifest, manifestPath, {
1778
+ hash: newHash,
1779
+ mtime: Math.floor(stats.mtimeMs),
1780
+ size: stats.size,
1781
+ entityId: existingEntry?.entityId,
1782
+ type: existingEntry?.type || "other",
1783
+ contextName: existingEntry?.contextName
1784
+ });
1785
+ if (options.verbose) {
1786
+ const removed = [];
1787
+ if (removedNew) removed.push(".new");
1788
+ if (removedConflict) removed.push(".conflict");
1789
+ console.log(
1790
+ chalk9.green(` \u2713 ${relPath}`) + chalk9.gray(` (removed ${removed.join(", ")})`)
1791
+ );
1792
+ }
1793
+ resolved++;
1794
+ } catch (error) {
1795
+ failed++;
1796
+ if (options.verbose) {
1797
+ const relPath = relative(process.cwd(), conflict.originalPath);
1798
+ console.log(chalk9.red(` \u2717 ${relPath} - ${error}`));
1799
+ }
1800
+ }
1801
+ }
1802
+ manifest = {
1803
+ ...manifest,
1804
+ lastSync: (/* @__PURE__ */ new Date()).toISOString()
1805
+ };
1806
+ await writeManifest(process.cwd(), manifest);
1807
+ resolveSpinner.stop("Conflicts resolved");
1808
+ console.log("");
1809
+ console.log(chalk9.bold(" Resolution Results"));
1810
+ console.log(chalk9.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"));
1811
+ if (resolved > 0) {
1812
+ console.log(chalk9.green(` \u2713 Resolved: ${resolved} files`));
1813
+ }
1814
+ if (failed > 0) {
1815
+ console.log(chalk9.red(` \u2717 Failed: ${failed} files`));
1816
+ }
1817
+ console.log(chalk9.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"));
1818
+ if (failed > 0) {
1819
+ p3.outro(
1820
+ chalk9.yellow(
1821
+ `Resolved ${resolved} conflict(s), ${failed} failed.`
1822
+ )
1823
+ );
1824
+ } else {
1825
+ p3.outro(chalk9.green(`All ${resolved} conflict(s) resolved!`));
1826
+ }
1827
+ }
1828
+ async function findConflictFiles(dir) {
1829
+ const conflicts = [];
1830
+ async function scan(currentDir) {
1831
+ try {
1832
+ const entries = await readdir(currentDir, { withFileTypes: true });
1833
+ for (const entry of entries) {
1834
+ const fullPath = join3(currentDir, entry.name);
1835
+ if (entry.isDirectory()) {
1836
+ if (entry.name === "node_modules" || entry.name === ".git" || entry.name.startsWith(".")) {
1837
+ continue;
1838
+ }
1839
+ await scan(fullPath);
1840
+ } else if (entry.isFile()) {
1841
+ if (entry.name.endsWith(CONFLICT_NEW_EXT)) {
1842
+ const originalPath = fullPath.slice(
1843
+ 0,
1844
+ -CONFLICT_NEW_EXT.length
1845
+ );
1846
+ const conflictPath = originalPath + CONFLICT_DIFF_EXT;
1847
+ const existing = conflicts.find(
1848
+ (c) => c.originalPath === originalPath
1849
+ );
1850
+ if (!existing) {
1851
+ conflicts.push({
1852
+ originalPath,
1853
+ newPath: fullPath,
1854
+ conflictPath
1855
+ });
1856
+ }
1857
+ } else if (entry.name.endsWith(CONFLICT_DIFF_EXT)) {
1858
+ const originalPath = fullPath.slice(
1859
+ 0,
1860
+ -CONFLICT_DIFF_EXT.length
1861
+ );
1862
+ const newPath = originalPath + CONFLICT_NEW_EXT;
1863
+ const existing = conflicts.find(
1864
+ (c) => c.originalPath === originalPath
1865
+ );
1866
+ if (!existing) {
1867
+ conflicts.push({
1868
+ originalPath,
1869
+ newPath,
1870
+ conflictPath: fullPath
1871
+ });
1872
+ }
1873
+ }
1874
+ }
1875
+ }
1876
+ } catch {
1877
+ }
1878
+ }
1879
+ await scan(dir);
1880
+ return conflicts;
1376
1881
  }
1377
1882
 
1378
1883
  // src/index.ts
1379
1884
  import "dotenv/config";
1380
1885
  var program = new Command();
1381
- program.name("aerocoding").description("AeroCoding CLI - Generate production-ready code from UML diagrams").version("0.1.23");
1886
+ program.name("aerocoding").description("AeroCoding CLI - Generate production-ready code from UML diagrams").version("0.1.25");
1382
1887
  program.command("login").description("Authenticate with AeroCoding").action(loginCommand);
1383
1888
  program.command("logout").description("Logout and clear stored credentials").action(logoutCommand);
1384
1889
  program.command("whoami").description("Show current authenticated user").action(whoamiCommand);
1385
- 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);
1386
- program.command("pull").description("Pull schema from cloud").option("-p, --project <id>", "Project ID").action(pullCommand);
1387
- program.command("status").description("Show local schema status").action(statusCommand);
1388
- 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);
1890
+ 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);
1891
+ 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);
1892
+ 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);
1389
1893
  program.parse();
1390
1894
  //# sourceMappingURL=index.js.map