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