archforge-x 1.0.2 → 1.0.3

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.
@@ -0,0 +1,22 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.syncCommand = syncCommand;
7
+ const validator_1 = require("../../core/architecture/validator");
8
+ const chalk_1 = __importDefault(require("chalk"));
9
+ async function syncCommand() {
10
+ const projectRoot = process.cwd();
11
+ const validator = new validator_1.ArchitectureValidator();
12
+ console.log(chalk_1.default.blue("šŸ”„ Syncing architecture with archforge.yaml..."));
13
+ const result = await validator.validate(projectRoot);
14
+ if (result.success) {
15
+ console.log(chalk_1.default.green.bold("āœ… Architecture is valid and in sync!"));
16
+ }
17
+ else {
18
+ console.log(chalk_1.default.red.bold("āŒ Architecture violations found:"));
19
+ result.errors.forEach(err => console.log(chalk_1.default.red(` - ${err}`)));
20
+ process.exit(1);
21
+ }
22
+ }
@@ -52,13 +52,47 @@ async function interactiveCLI() {
52
52
  { title: "Hexagonal Architecture", value: "hexagonal" },
53
53
  ],
54
54
  });
55
+ const ormResponse = await (0, prompts_1.default)({
56
+ type: "select",
57
+ name: "orm",
58
+ message: "Select ORM:",
59
+ choices: langResponse.language === "ts" || langResponse.language === "js"
60
+ ? [
61
+ { title: "Prisma", value: "prisma" },
62
+ { title: "TypeORM", value: "typeorm" },
63
+ { title: "Sequelize", value: "sequelize" },
64
+ { title: "Drizzle", value: "drizzle" },
65
+ { title: "None", value: "none" },
66
+ ]
67
+ : langResponse.language === "py"
68
+ ? [
69
+ { title: "SQLAlchemy", value: "sqlalchemy" },
70
+ { title: "Django ORM", value: "django" },
71
+ { title: "None", value: "none" },
72
+ ]
73
+ : [
74
+ { title: "GORM", value: "gorm" },
75
+ { title: "SQLC", value: "sqlc" },
76
+ { title: "None", value: "none" },
77
+ ],
78
+ });
79
+ const dbResponse = await (0, prompts_1.default)({
80
+ type: ormResponse.orm === "none" ? null : "select",
81
+ name: "database",
82
+ message: "Select Database:",
83
+ choices: [
84
+ { title: "PostgreSQL", value: "postgresql" },
85
+ { title: "MySQL", value: "mysql" },
86
+ { title: "SQLite", value: "sqlite" },
87
+ { title: "MongoDB", value: "mongodb" },
88
+ ],
89
+ });
55
90
  const modulesResponse = await (0, prompts_1.default)({
56
91
  type: "multiselect",
57
92
  name: "modules",
58
93
  message: "Select optional modules:",
59
94
  choices: [
60
95
  { title: "Authentication (JWT)", value: "auth" },
61
- { title: "Database (ORM setup)", value: "db" },
62
96
  { title: "Caching (Redis)", value: "cache" },
63
97
  { title: "Docker Support", value: "docker" },
64
98
  { title: "CI/CD (GitHub Actions)", value: "ci" },
@@ -73,12 +107,14 @@ async function interactiveCLI() {
73
107
  });
74
108
  console.log(chalk_1.default.yellow("\nšŸ”§ Generating project..."));
75
109
  const options = {
76
- mode: 'professional',
110
+ // mode: 'professional',
77
111
  language: langResponse.language,
78
112
  framework: frameworkResponse.framework,
79
113
  architecture: archResponse.architecture,
80
114
  modules: modulesResponse.modules,
81
115
  projectName: projectNameResp.projectName,
116
+ orm: ormResponse.orm,
117
+ database: dbResponse.database,
82
118
  };
83
119
  // Create a dummy arch definition for the generator
84
120
  const dummyArch = {
@@ -92,7 +128,9 @@ async function interactiveCLI() {
92
128
  type: archResponse.architecture,
93
129
  language: langResponse.language,
94
130
  framework: frameworkResponse.framework,
95
- modules: modulesResponse.modules
131
+ modules: modulesResponse.modules,
132
+ orm: ormResponse.orm,
133
+ database: dbResponse.database
96
134
  },
97
135
  layers: []
98
136
  };
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.architectureSchema = exports.metadataSchema = exports.layerSchema = void 0;
3
+ exports.architectureSchema = exports.customStructureSchema = exports.rulesSchema = exports.namingSchema = exports.metadataSchema = exports.layerSchema = void 0;
4
4
  // src/core/architecture/schema.ts
5
5
  const zod_1 = require("zod");
6
6
  exports.layerSchema = zod_1.z.object({
@@ -9,15 +9,33 @@ exports.layerSchema = zod_1.z.object({
9
9
  description: zod_1.z.string().optional(),
10
10
  allowedImports: zod_1.z.array(zod_1.z.string()).optional(),
11
11
  forbiddenImports: zod_1.z.array(zod_1.z.string()).optional(),
12
+ canImport: zod_1.z.array(zod_1.z.string()).optional(),
12
13
  strict: zod_1.z.boolean().optional(),
13
14
  });
14
15
  exports.metadataSchema = zod_1.z.object({
15
- type: zod_1.z.enum(['clean', 'ddd', 'layered', 'hexagonal', 'microservices']),
16
+ type: zod_1.z.enum(['clean', 'ddd', 'layered', 'hexagonal', 'microservices', 'mvc']),
16
17
  language: zod_1.z.enum(['ts', 'js', 'py', 'go']),
17
18
  framework: zod_1.z.string().optional(),
19
+ orm: zod_1.z.string().optional(),
20
+ database: zod_1.z.string().optional(),
18
21
  modules: zod_1.z.array(zod_1.z.string()).optional(),
19
22
  version: zod_1.z.string().optional(),
20
23
  });
24
+ exports.namingSchema = zod_1.z.object({
25
+ services: zod_1.z.string().optional(),
26
+ repositories: zod_1.z.string().optional(),
27
+ controllers: zod_1.z.string().optional(),
28
+ entities: zod_1.z.string().optional(),
29
+ });
30
+ exports.rulesSchema = zod_1.z.object({
31
+ forbidDirectDbAccess: zod_1.z.boolean().optional(),
32
+ forbidFrameworkInDomain: zod_1.z.boolean().optional(),
33
+ enforceLayerBoundaries: zod_1.z.boolean().optional(),
34
+ });
35
+ exports.customStructureSchema = zod_1.z.object({
36
+ enabled: zod_1.z.boolean(),
37
+ folders: zod_1.z.array(zod_1.z.object({ path: zod_1.z.string() })),
38
+ });
21
39
  exports.architectureSchema = zod_1.z.object({
22
40
  version: zod_1.z.string().default("1.0"),
23
41
  name: zod_1.z.string(),
@@ -28,5 +46,8 @@ exports.architectureSchema = zod_1.z.object({
28
46
  }),
29
47
  metadata: exports.metadataSchema,
30
48
  layers: zod_1.z.array(exports.layerSchema),
49
+ naming: exports.namingSchema.optional(),
50
+ rules: exports.rulesSchema.optional(),
51
+ customStructure: exports.customStructureSchema.optional(),
31
52
  strict: zod_1.z.boolean().optional(),
32
53
  });
@@ -0,0 +1,112 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.ArchitectureValidator = void 0;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const js_yaml_1 = __importDefault(require("js-yaml"));
10
+ class ArchitectureValidator {
11
+ async validate(projectRoot) {
12
+ const configPath = path_1.default.join(projectRoot, "archforge.yaml");
13
+ if (!fs_extra_1.default.existsSync(configPath)) {
14
+ return { success: false, errors: ["archforge.yaml not found"], warnings: [] };
15
+ }
16
+ const config = js_yaml_1.default.load(fs_extra_1.default.readFileSync(configPath, "utf8"));
17
+ const errors = [];
18
+ const warnings = [];
19
+ // 1. Validate Folder Structure
20
+ if (config.layers) {
21
+ for (const [name, layer] of Object.entries(config.layers)) {
22
+ const layerPath = path_1.default.join(projectRoot, layer.path);
23
+ if (!fs_extra_1.default.existsSync(layerPath)) {
24
+ errors.push(`Layer "${name}" path not found: ${layer.path}`);
25
+ }
26
+ }
27
+ }
28
+ // 2. Validate Custom Folders
29
+ if (config.customStructure?.enabled && config.customStructure.folders) {
30
+ for (const folder of config.customStructure.folders) {
31
+ const folderPath = path_1.default.join(projectRoot, folder.path);
32
+ if (!fs_extra_1.default.existsSync(folderPath)) {
33
+ errors.push(`Custom folder not found: ${folder.path}`);
34
+ }
35
+ }
36
+ }
37
+ // 3. Validate Naming Conventions
38
+ if (config.naming) {
39
+ this.validateNamingConventions(projectRoot, config.naming, errors);
40
+ }
41
+ // 4. Validate Layer Imports (Basic check for forbidden imports in files)
42
+ if (config.layers && config.rules?.enforceLayerBoundaries) {
43
+ this.validateLayerImports(projectRoot, config.layers, errors);
44
+ }
45
+ return {
46
+ success: errors.length === 0,
47
+ errors,
48
+ warnings
49
+ };
50
+ }
51
+ validateNamingConventions(root, naming, errors) {
52
+ // Simple check: scan src and verify file suffixes
53
+ const scanDir = (dir) => {
54
+ if (!fs_extra_1.default.existsSync(dir))
55
+ return;
56
+ const files = fs_extra_1.default.readdirSync(dir);
57
+ for (const file of files) {
58
+ const fullPath = path_1.default.join(dir, file);
59
+ if (fs_extra_1.default.statSync(fullPath).isDirectory()) {
60
+ scanDir(fullPath);
61
+ }
62
+ else {
63
+ const lowerFile = file.toLowerCase();
64
+ if (dir.includes("services") && naming.services) {
65
+ const pattern = naming.services.replace("*", "").toLowerCase();
66
+ if (!lowerFile.includes(pattern)) {
67
+ errors.push(`Naming violation: Service file "${file}" does not match pattern "${naming.services}"`);
68
+ }
69
+ }
70
+ if (dir.includes("repositories") && naming.repositories) {
71
+ const pattern = naming.repositories.replace("*", "").toLowerCase();
72
+ if (!lowerFile.includes(pattern)) {
73
+ errors.push(`Naming violation: Repository file "${file}" does not match pattern "${naming.repositories}"`);
74
+ }
75
+ }
76
+ }
77
+ }
78
+ };
79
+ scanDir(path_1.default.join(root, "src"));
80
+ }
81
+ validateLayerImports(root, layers, errors) {
82
+ // This is a complex task for a simple validator, but we can do a basic grep-like check
83
+ // for forbidden layer paths in files of other layers.
84
+ for (const [name, layer] of Object.entries(layers)) {
85
+ const layerPath = path_1.default.join(root, layer.path);
86
+ if (!fs_extra_1.default.existsSync(layerPath))
87
+ continue;
88
+ const canImport = layer.canImport || [];
89
+ const forbiddenLayers = Object.keys(layers).filter(l => l !== name && !canImport.includes(l));
90
+ const scanFiles = (dir) => {
91
+ const files = fs_extra_1.default.readdirSync(dir);
92
+ for (const file of files) {
93
+ const fullPath = path_1.default.join(dir, file);
94
+ if (fs_extra_1.default.statSync(fullPath).isDirectory()) {
95
+ scanFiles(fullPath);
96
+ }
97
+ else if (file.endsWith(".ts") || file.endsWith(".py") || file.endsWith(".go")) {
98
+ const content = fs_extra_1.default.readFileSync(fullPath, "utf8");
99
+ for (const forbidden of forbiddenLayers) {
100
+ const forbiddenPath = layers[forbidden].path.replace("src/", "");
101
+ if (content.includes(forbiddenPath) && !content.includes(`from .`)) {
102
+ errors.push(`Architecture violation: Layer "${name}" (${file}) imports from forbidden layer "${forbidden}"`);
103
+ }
104
+ }
105
+ }
106
+ }
107
+ };
108
+ scanFiles(layerPath);
109
+ }
110
+ }
111
+ }
112
+ exports.ArchitectureValidator = ArchitectureValidator;
@@ -0,0 +1,166 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.BaseGenerator = void 0;
7
+ const fs_extra_1 = __importDefault(require("fs-extra"));
8
+ const path_1 = __importDefault(require("path"));
9
+ const chalk_1 = __importDefault(require("chalk"));
10
+ const js_yaml_1 = __importDefault(require("js-yaml"));
11
+ class BaseGenerator {
12
+ async generate(arch, options) {
13
+ const projectName = options.projectName || arch.project.name || "generated-project";
14
+ const root = path_1.default.resolve(projectName);
15
+ console.log(chalk_1.default.blueBright(`šŸš€ Starting ${this.getGeneratorName()} generation for ${projectName}...`));
16
+ if (!fs_extra_1.default.existsSync(root)) {
17
+ fs_extra_1.default.mkdirpSync(root);
18
+ }
19
+ await this.generateProjectStructure(root, arch, options);
20
+ this.generateArchForgeConfig(root, arch, options);
21
+ // Ensure all layer and custom folders exist
22
+ const config = js_yaml_1.default.load(fs_extra_1.default.readFileSync(path_1.default.join(root, "archforge.yaml"), "utf8"));
23
+ if (config.layers) {
24
+ for (const layer of Object.values(config.layers)) {
25
+ this.createDir(root, layer.path);
26
+ }
27
+ }
28
+ if (config.customStructure?.enabled && config.customStructure.folders) {
29
+ for (const folder of config.customStructure.folders) {
30
+ this.createDir(root, folder.path);
31
+ }
32
+ }
33
+ if (options.modules?.includes("docker")) {
34
+ console.log(chalk_1.default.blue(" 🐳 Generating Docker configuration..."));
35
+ this.generateDocker(root, options);
36
+ }
37
+ if (options.modules?.includes("ci")) {
38
+ console.log(chalk_1.default.blue(" šŸ‘· Generating CI/CD workflows..."));
39
+ this.generateCI(root, options);
40
+ }
41
+ this.generateProductionDocs(root, arch, options);
42
+ this.ensureNoEmptyFolders(root);
43
+ console.log(chalk_1.default.green.bold(`\nāœ… ${this.getGeneratorName()} project ${projectName} successfully generated!`));
44
+ }
45
+ generateProductionDocs(root, arch, options) {
46
+ this.writeFile(root, "docs/architecture.md", `
47
+ # Architecture Guide
48
+
49
+ This project follows the **${options.architecture || arch.metadata.type}** architecture style.
50
+
51
+ ## Layers
52
+ ${arch.layers.map(l => `- **${l.name}**: ${l.description || 'Located at ' + l.path}`).join('\n')}
53
+
54
+ ## Rules
55
+ - **Layer Boundaries**: Enforced via \`archforge.yaml\`.
56
+ - **Naming**: Services must end with \`Service\`, Repositories with \`Repository\`.
57
+ `);
58
+ this.writeFile(root, "docs/database.md", `
59
+ # Database Guide
60
+
61
+ - **ORM**: ${options.orm || arch.metadata.orm}
62
+ - **Database**: ${options.database || arch.metadata.database}
63
+
64
+ ## Setup
65
+ 1. Ensure Docker is running.
66
+ 2. Run \`docker-compose up -d\`.
67
+ 3. Migrations are handled automatically on startup.
68
+ `);
69
+ this.writeFile(root, "docs/caching.md", `
70
+ # Caching Guide
71
+
72
+ This project implements a first-class caching layer.
73
+
74
+ ## Strategies
75
+ - **Redis**: Used in production for distributed caching.
76
+ - **In-Memory**: Used for local development or as a fallback.
77
+
78
+ ## Usage
79
+ Inject the \`ICache\` interface into your services.
80
+ `);
81
+ this.writeFile(root, "docs/rules.md", `
82
+ # ArchForge Rules
83
+
84
+ This project uses \`archforge.yaml\` to enforce architectural standards.
85
+
86
+ ## How to Sync
87
+ Run \`npx archforge sync\` to validate the structure.
88
+ `);
89
+ }
90
+ ensureNoEmptyFolders(root) {
91
+ const scan = (dir) => {
92
+ const files = fs_extra_1.default.readdirSync(dir);
93
+ if (files.length === 0) {
94
+ this.writeFile(dir, "README.md", `# ${path_1.default.basename(dir)}\n\nThis folder is part of the architectural structure.`);
95
+ }
96
+ else {
97
+ for (const file of files) {
98
+ const fullPath = path_1.default.join(dir, file);
99
+ if (fs_extra_1.default.statSync(fullPath).isDirectory()) {
100
+ scan(fullPath);
101
+ }
102
+ }
103
+ }
104
+ };
105
+ scan(path_1.default.join(root, "src"));
106
+ }
107
+ generateArchForgeConfig(root, arch, options) {
108
+ const config = {
109
+ architecture: options.architecture || arch.metadata.type,
110
+ stack: options.framework || arch.metadata.framework,
111
+ orm: options.orm || arch.metadata.orm || "none",
112
+ database: options.database || arch.metadata.database || "none",
113
+ layers: arch.layers.length > 0 ? arch.layers.reduce((acc, layer) => {
114
+ acc[layer.name] = {
115
+ path: layer.path,
116
+ canImport: layer.canImport || layer.allowedImports || []
117
+ };
118
+ return acc;
119
+ }, {}) : this.getDefaultLayers(options.architecture || "clean"),
120
+ naming: arch.naming || {
121
+ services: "*Service",
122
+ repositories: "*Repository",
123
+ controllers: "*Controller"
124
+ },
125
+ rules: arch.rules || {
126
+ forbidDirectDbAccess: true,
127
+ forbidFrameworkInDomain: true,
128
+ enforceLayerBoundaries: true
129
+ },
130
+ customStructure: arch.customStructure || {
131
+ enabled: true,
132
+ folders: [
133
+ { path: "src/modules" },
134
+ { path: "src/shared" }
135
+ ]
136
+ }
137
+ };
138
+ this.writeFile(root, "archforge.yaml", `
139
+ # ArchForge X Configuration
140
+ # This file is the single source of truth for your project architecture.
141
+ ${require('js-yaml').dump(config)}
142
+ `.trim());
143
+ }
144
+ getDefaultLayers(arch) {
145
+ if (arch === "clean") {
146
+ return {
147
+ domain: { path: "src/domain", canImport: [] },
148
+ application: { path: "src/application", canImport: ["domain"] },
149
+ infrastructure: { path: "src/infrastructure", canImport: ["application", "domain"] },
150
+ presentation: { path: "src/presentation", canImport: ["application"] }
151
+ };
152
+ }
153
+ // Add other defaults as needed
154
+ return {};
155
+ }
156
+ writeFile(root, filePath, content) {
157
+ const fullPath = path_1.default.join(root, filePath);
158
+ fs_extra_1.default.mkdirpSync(path_1.default.dirname(fullPath));
159
+ fs_extra_1.default.writeFileSync(fullPath, content.trim());
160
+ }
161
+ createDir(root, dirPath) {
162
+ const fullPath = path_1.default.join(root, dirPath);
163
+ fs_extra_1.default.mkdirpSync(fullPath);
164
+ }
165
+ }
166
+ exports.BaseGenerator = BaseGenerator;