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 +1449 -80
- package/dist/index.js.map +1 -1
- package/package.json +3 -1
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
120
|
+
spinner5.fail(chalk.red("Authorization denied"));
|
|
121
121
|
throw new Error("You denied the authorization request");
|
|
122
122
|
}
|
|
123
|
-
|
|
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((
|
|
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
|
|
524
|
-
import
|
|
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 =
|
|
528
|
-
const resolvedFile =
|
|
529
|
-
return resolvedFile.startsWith(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 =
|
|
605
|
-
const dir =
|
|
885
|
+
const fullPath = path2.resolve(outputDir, file.path);
|
|
886
|
+
const dir = path2.dirname(fullPath);
|
|
606
887
|
if (file.generateOnce) {
|
|
607
888
|
try {
|
|
608
|
-
await
|
|
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
|
|
616
|
-
await
|
|
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((
|
|
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
|
-
|
|
1119
|
+
resolve2(normalized !== "n" && normalized !== "no");
|
|
653
1120
|
});
|
|
654
1121
|
});
|
|
655
1122
|
}
|
|
656
1123
|
|
|
657
1124
|
// src/config/loader.ts
|
|
658
|
-
import
|
|
659
|
-
import
|
|
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 =
|
|
1158
|
+
const configPath = path3.join(dir, CONFIG_FILENAME);
|
|
692
1159
|
try {
|
|
693
|
-
const content = await
|
|
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 =
|
|
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
|
|
1180
|
+
await fs4.writeFile(configPath, content, "utf-8");
|
|
714
1181
|
}
|
|
715
1182
|
async function configExists(dir = process.cwd()) {
|
|
716
|
-
const configPath =
|
|
1183
|
+
const configPath = path3.join(dir, CONFIG_FILENAME);
|
|
717
1184
|
try {
|
|
718
|
-
await
|
|
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
|
|
1357
|
+
let spinner5 = ora2({ text: "Fetching project...", color: "cyan" }).start();
|
|
806
1358
|
try {
|
|
807
1359
|
const project = await apiClient.getProject(projectId);
|
|
808
|
-
|
|
1360
|
+
spinner5.succeed(chalk8.gray("Project loaded"));
|
|
809
1361
|
const hasBackendConfig = !!config?.backend?.preset;
|
|
810
1362
|
const hasFrontendConfig = !!config?.frontend?.preset;
|
|
811
|
-
const
|
|
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: "
|
|
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 (
|
|
1409
|
+
} else if (backendAvailable && !frontendAvailable) {
|
|
839
1410
|
selectedTarget = "backend";
|
|
840
|
-
} else if (
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1096
|
-
|
|
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
|
-
|
|
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
|
|
1182
|
-
|
|
1763
|
+
const spinner6 = p2.spinner();
|
|
1764
|
+
spinner6.start("Loading projects...");
|
|
1183
1765
|
const projects = await apiClient.listProjects(organizationId);
|
|
1184
|
-
|
|
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
|
|
1204
|
-
|
|
1785
|
+
const spinner5 = p2.spinner();
|
|
1786
|
+
spinner5.start("Fetching project details...");
|
|
1205
1787
|
const project = await apiClient.getProject(projectId);
|
|
1206
|
-
|
|
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/
|
|
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(
|
|
2730
|
+
console.log(chalk13.yellow("Command not yet implemented"));
|
|
1365
2731
|
if (options.project) {
|
|
1366
|
-
console.log(
|
|
2732
|
+
console.log(chalk13.gray(` Requested project: ${options.project}`));
|
|
1367
2733
|
}
|
|
1368
|
-
console.log(
|
|
2734
|
+
console.log(chalk13.gray(" Coming soon in v0.2.0\n"));
|
|
1369
2735
|
}
|
|
1370
2736
|
|
|
1371
2737
|
// src/commands/status.ts
|
|
1372
|
-
import
|
|
2738
|
+
import chalk14 from "chalk";
|
|
1373
2739
|
async function statusCommand() {
|
|
1374
|
-
console.log(
|
|
1375
|
-
console.log(
|
|
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.
|
|
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);
|