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