@aifabrix/builder 2.0.0

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,377 @@
1
+ /**
2
+ * AI Fabrix Builder Schema Validation
3
+ *
4
+ * This module provides schema validation with developer-friendly error messages.
5
+ * Validates variables.yaml, rbac.yaml, and env.template files.
6
+ *
7
+ * @fileoverview Schema validation with friendly error messages for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const fs = require('fs');
13
+ const path = require('path');
14
+ const yaml = require('js-yaml');
15
+ const Ajv = require('ajv');
16
+ const applicationSchema = require('./schema/application-schema.json');
17
+
18
+ /**
19
+ * Validates variables.yaml file against application schema
20
+ * Provides detailed error messages for configuration issues
21
+ *
22
+ * @async
23
+ * @function validateVariables
24
+ * @param {string} appName - Name of the application
25
+ * @returns {Promise<Object>} Validation result with errors and warnings
26
+ * @throws {Error} If file cannot be read or parsed
27
+ *
28
+ * @example
29
+ * const result = await validateVariables('myapp');
30
+ * // Returns: { valid: true, errors: [], warnings: [] }
31
+ */
32
+ async function validateVariables(appName) {
33
+ if (!appName || typeof appName !== 'string') {
34
+ throw new Error('App name is required and must be a string');
35
+ }
36
+
37
+ const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
38
+
39
+ if (!fs.existsSync(variablesPath)) {
40
+ throw new Error(`variables.yaml not found: ${variablesPath}`);
41
+ }
42
+
43
+ const content = fs.readFileSync(variablesPath, 'utf8');
44
+ let variables;
45
+
46
+ try {
47
+ variables = yaml.load(content);
48
+ } catch (error) {
49
+ throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
50
+ }
51
+
52
+ const ajv = new Ajv({ allErrors: true, strict: false });
53
+ const validate = ajv.compile(applicationSchema);
54
+ const valid = validate(variables);
55
+
56
+ return {
57
+ valid,
58
+ errors: valid ? [] : formatValidationErrors(validate.errors),
59
+ warnings: []
60
+ };
61
+ }
62
+
63
+ /**
64
+ * Validates rbac.yaml file structure and content
65
+ * Ensures roles and permissions are properly defined
66
+ *
67
+ * @async
68
+ * @function validateRbac
69
+ * @param {string} appName - Name of the application
70
+ * @returns {Promise<Object>} Validation result with errors and warnings
71
+ * @throws {Error} If file cannot be read or parsed
72
+ *
73
+ * @example
74
+ * const result = await validateRbac('myapp');
75
+ * // Returns: { valid: true, errors: [], warnings: [] }
76
+ */
77
+ function validateRoles(roles) {
78
+ const errors = [];
79
+ if (!roles || !Array.isArray(roles)) {
80
+ errors.push('rbac.yaml must contain a "roles" array');
81
+ return errors;
82
+ }
83
+
84
+ const roleNames = new Set();
85
+ roles.forEach((role, index) => {
86
+ if (!role.name || !role.value || !role.description) {
87
+ errors.push(`Role at index ${index} is missing required fields (name, value, description)`);
88
+ } else if (roleNames.has(role.value)) {
89
+ errors.push(`Duplicate role value: ${role.value}`);
90
+ } else {
91
+ roleNames.add(role.value);
92
+ }
93
+ });
94
+ return errors;
95
+ }
96
+
97
+ function validatePermissions(permissions) {
98
+ const errors = [];
99
+ if (!permissions || !Array.isArray(permissions)) {
100
+ errors.push('rbac.yaml must contain a "permissions" array');
101
+ return errors;
102
+ }
103
+
104
+ const permissionNames = new Set();
105
+ permissions.forEach((permission, index) => {
106
+ if (!permission.name || !permission.roles || !permission.description) {
107
+ errors.push(`Permission at index ${index} is missing required fields (name, roles, description)`);
108
+ } else if (permissionNames.has(permission.name)) {
109
+ errors.push(`Duplicate permission name: ${permission.name}`);
110
+ } else {
111
+ permissionNames.add(permission.name);
112
+ }
113
+ });
114
+ return errors;
115
+ }
116
+
117
+ async function validateRbac(appName) {
118
+ if (!appName || typeof appName !== 'string') {
119
+ throw new Error('App name is required and must be a string');
120
+ }
121
+
122
+ const rbacPath = path.join(process.cwd(), 'builder', appName, 'rbac.yaml');
123
+
124
+ if (!fs.existsSync(rbacPath)) {
125
+ return { valid: true, errors: [], warnings: ['rbac.yaml not found - authentication disabled'] };
126
+ }
127
+
128
+ const content = fs.readFileSync(rbacPath, 'utf8');
129
+ let rbac;
130
+
131
+ try {
132
+ rbac = yaml.load(content);
133
+ } catch (error) {
134
+ throw new Error(`Invalid YAML syntax in rbac.yaml: ${error.message}`);
135
+ }
136
+
137
+ const errors = [
138
+ ...validateRoles(rbac.roles),
139
+ ...validatePermissions(rbac.permissions)
140
+ ];
141
+
142
+ return {
143
+ valid: errors.length === 0,
144
+ errors,
145
+ warnings: []
146
+ };
147
+ }
148
+
149
+ /**
150
+ * Validates env.template file for proper kv:// references
151
+ * Checks for syntax errors and missing secret references
152
+ *
153
+ * @async
154
+ * @function validateEnvTemplate
155
+ * @param {string} appName - Name of the application
156
+ * @returns {Promise<Object>} Validation result with errors and warnings
157
+ * @throws {Error} If file cannot be read
158
+ *
159
+ * @example
160
+ * const result = await validateEnvTemplate('myapp');
161
+ * // Returns: { valid: true, errors: [], warnings: [] }
162
+ */
163
+ async function validateEnvTemplate(appName) {
164
+ if (!appName || typeof appName !== 'string') {
165
+ throw new Error('App name is required and must be a string');
166
+ }
167
+
168
+ const templatePath = path.join(process.cwd(), 'builder', appName, 'env.template');
169
+
170
+ if (!fs.existsSync(templatePath)) {
171
+ throw new Error(`env.template not found: ${templatePath}`);
172
+ }
173
+
174
+ const content = fs.readFileSync(templatePath, 'utf8');
175
+ const errors = [];
176
+ const warnings = [];
177
+
178
+ // Check for valid environment variable syntax
179
+ const lines = content.split('\n');
180
+ lines.forEach((line, index) => {
181
+ const trimmed = line.trim();
182
+ if (trimmed && !trimmed.startsWith('#')) {
183
+ if (!trimmed.includes('=')) {
184
+ errors.push(`Line ${index + 1}: Invalid environment variable format (missing =)`);
185
+ } else {
186
+ const [key, value] = trimmed.split('=', 2);
187
+ if (!key || !value) {
188
+ errors.push(`Line ${index + 1}: Invalid environment variable format`);
189
+ }
190
+ }
191
+ }
192
+ });
193
+
194
+ // Check for kv:// reference format
195
+ const kvPattern = /kv:\/\/([a-zA-Z0-9-_]+)/g;
196
+ let match;
197
+ while ((match = kvPattern.exec(content)) !== null) {
198
+ const secretKey = match[1];
199
+ if (!secretKey) {
200
+ errors.push('Invalid kv:// reference format');
201
+ }
202
+ }
203
+
204
+ return {
205
+ valid: errors.length === 0,
206
+ errors,
207
+ warnings
208
+ };
209
+ }
210
+
211
+ /**
212
+ * Checks the development environment for common issues
213
+ * Validates Docker, ports, secrets, and other requirements
214
+ *
215
+ * @async
216
+ * @function checkEnvironment
217
+ * @returns {Promise<Object>} Environment check result
218
+ * @throws {Error} If critical issues are found
219
+ *
220
+ * @example
221
+ * const result = await checkEnvironment();
222
+ * // Returns: { docker: 'ok', ports: 'ok', secrets: 'missing', recommendations: [...] }
223
+ */
224
+ async function checkDocker() {
225
+ try {
226
+ const { exec } = require('child_process');
227
+ const { promisify } = require('util');
228
+ const execAsync = promisify(exec);
229
+
230
+ await execAsync('docker --version');
231
+ await execAsync('docker-compose --version');
232
+ return 'ok';
233
+ } catch (error) {
234
+ return 'error';
235
+ }
236
+ }
237
+
238
+ async function checkPorts() {
239
+ const requiredPorts = [5432, 6379, 5050, 8081];
240
+ const netstat = require('net');
241
+ let portIssues = 0;
242
+
243
+ for (const port of requiredPorts) {
244
+ try {
245
+ await new Promise((resolve, reject) => {
246
+ const server = netstat.createServer();
247
+ server.listen(port, () => {
248
+ server.close(resolve);
249
+ });
250
+ server.on('error', reject);
251
+ });
252
+ } catch (error) {
253
+ portIssues++;
254
+ }
255
+ }
256
+
257
+ return portIssues === 0 ? 'ok' : 'warning';
258
+ }
259
+
260
+ function checkSecrets() {
261
+ const os = require('os');
262
+ const secretsPath = path.join(os.homedir(), '.aifabrix', 'secrets.yaml');
263
+ return fs.existsSync(secretsPath) ? 'ok' : 'missing';
264
+ }
265
+
266
+ async function checkEnvironment() {
267
+ const result = {
268
+ docker: 'unknown',
269
+ ports: 'unknown',
270
+ secrets: 'unknown',
271
+ recommendations: []
272
+ };
273
+
274
+ // Check Docker
275
+ result.docker = await checkDocker();
276
+ if (result.docker === 'error') {
277
+ result.recommendations.push('Install Docker and Docker Compose');
278
+ }
279
+
280
+ // Check ports
281
+ result.ports = await checkPorts();
282
+ if (result.ports === 'warning') {
283
+ result.recommendations.push('Some required ports (5432, 6379, 5050, 8081) are in use');
284
+ }
285
+
286
+ // Check secrets
287
+ result.secrets = checkSecrets();
288
+ if (result.secrets === 'missing') {
289
+ result.recommendations.push('Create secrets file: ~/.aifabrix/secrets.yaml');
290
+ }
291
+
292
+ return result;
293
+ }
294
+
295
+ /**
296
+ * Formats validation errors into developer-friendly messages
297
+ * Converts technical schema errors into actionable advice
298
+ *
299
+ * @function formatValidationErrors
300
+ * @param {Array} errors - Raw validation errors from Ajv
301
+ * @returns {Array} Formatted error messages
302
+ *
303
+ * @example
304
+ * const messages = formatValidationErrors(ajvErrors);
305
+ * // Returns: ['Port must be between 1 and 65535', 'Missing required field: displayName']
306
+ */
307
+ function formatSingleError(error) {
308
+ const path = error.instancePath ? error.instancePath.slice(1) : 'root';
309
+ const field = path ? `Field "${path}"` : 'Configuration';
310
+
311
+ const errorMessages = {
312
+ required: `${field}: Missing required property "${error.params.missingProperty}"`,
313
+ type: `${field}: Expected ${error.params.type}, got ${typeof error.data}`,
314
+ minimum: `${field}: Value must be at least ${error.params.limit}`,
315
+ maximum: `${field}: Value must be at most ${error.params.limit}`,
316
+ minLength: `${field}: Must be at least ${error.params.limit} characters`,
317
+ maxLength: `${field}: Must be at most ${error.params.limit} characters`,
318
+ pattern: `${field}: Invalid format`,
319
+ enum: `${field}: Must be one of: ${error.params.allowedValues?.join(', ') || 'unknown'}`
320
+ };
321
+
322
+ return errorMessages[error.keyword] || `${field}: ${error.message}`;
323
+ }
324
+
325
+ function formatValidationErrors(errors) {
326
+ if (!Array.isArray(errors)) {
327
+ return ['Unknown validation error'];
328
+ }
329
+
330
+ return errors.map(formatSingleError);
331
+ }
332
+
333
+ /**
334
+ * Validates all application configuration files
335
+ * Runs complete validation suite for an application
336
+ *
337
+ * @async
338
+ * @function validateApplication
339
+ * @param {string} appName - Name of the application
340
+ * @returns {Promise<Object>} Complete validation result
341
+ * @throws {Error} If validation fails
342
+ *
343
+ * @example
344
+ * const result = await validateApplication('myapp');
345
+ * // Returns: { valid: true, variables: {...}, rbac: {...}, env: {...} }
346
+ */
347
+ async function validateApplication(appName) {
348
+ if (!appName || typeof appName !== 'string') {
349
+ throw new Error('App name is required and must be a string');
350
+ }
351
+
352
+ const variables = await validateVariables(appName);
353
+ const rbac = await validateRbac(appName);
354
+ const env = await validateEnvTemplate(appName);
355
+
356
+ const valid = variables.valid && rbac.valid && env.valid;
357
+
358
+ return {
359
+ valid,
360
+ variables,
361
+ rbac,
362
+ env,
363
+ summary: {
364
+ totalErrors: variables.errors.length + rbac.errors.length + env.errors.length,
365
+ totalWarnings: variables.warnings.length + rbac.warnings.length + env.warnings.length
366
+ }
367
+ };
368
+ }
369
+
370
+ module.exports = {
371
+ validateVariables,
372
+ validateRbac,
373
+ validateEnvTemplate,
374
+ checkEnvironment,
375
+ formatValidationErrors,
376
+ validateApplication
377
+ };
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@aifabrix/builder",
3
+ "version": "2.0.0",
4
+ "description": "AI Fabrix Local Fabric & Deployment SDK",
5
+ "main": "lib/index.js",
6
+ "bin": {
7
+ "aifabrix": "bin/aifabrix.js"
8
+ },
9
+ "scripts": {
10
+ "test": "jest --coverage",
11
+ "test:watch": "jest --watch",
12
+ "test:ci": "jest --ci --coverage --watchAll=false",
13
+ "lint": "eslint . --ext .js",
14
+ "lint:fix": "eslint . --ext .js --fix",
15
+ "lint:ci": "eslint . --ext .js --format json --output-file eslint-report.json",
16
+ "dev": "node bin/aifabrix.js",
17
+ "build": "npm run lint && npm run test:ci",
18
+ "validate": "npm run build",
19
+ "prepublishOnly": "npm run validate",
20
+ "precommit": "npm run lint:fix && npm run test",
21
+ "posttest": "npm run lint"
22
+ },
23
+ "keywords": [
24
+ "aifabrix",
25
+ "docker",
26
+ "deployment",
27
+ "cli",
28
+ "azure",
29
+ "container"
30
+ ],
31
+ "author": "eSystems Nordic Ltd",
32
+ "license": "UNLICENSED",
33
+ "engines": {
34
+ "node": ">=18.0.0"
35
+ },
36
+ "dependencies": {
37
+ "commander": "^11.1.0",
38
+ "js-yaml": "^4.1.0",
39
+ "ajv": "^8.12.0",
40
+ "handlebars": "^4.7.8",
41
+ "axios": "^1.6.0",
42
+ "chalk": "^4.1.2",
43
+ "ora": "^5.4.1",
44
+ "inquirer": "^8.2.5"
45
+ },
46
+ "devDependencies": {
47
+ "jest": "^29.7.0",
48
+ "eslint": "^8.55.0",
49
+ "@types/node": "^20.10.0"
50
+ },
51
+ "repository": {
52
+ "type": "git",
53
+ "url": "https://github.com/esystemsdev/aifabrix-builder.git"
54
+ },
55
+ "bugs": {
56
+ "url": "https://github.com/esystemsdev/aifabrix-builder/issues"
57
+ },
58
+ "homepage": "https://github.com/esystemsdev/aifabrix-builder#readme"
59
+ }
@@ -0,0 +1,51 @@
1
+ # AI Fabrix Builder Templates
2
+
3
+ These are Handlebars (.hbs) template files that generate Docker files for AI Fabrix applications. They should NOT be linted as Dockerfiles since they contain template variables that will be replaced during generation.
4
+
5
+ ## Template Files
6
+
7
+ - `python/Dockerfile.hbs` - Python application Dockerfile template
8
+ - `python/docker-compose.hbs` - Python application docker-compose template
9
+ - `typescript/Dockerfile.hbs` - TypeScript/Node.js application Dockerfile template
10
+ - `typescript/docker-compose.hbs` - TypeScript/Node.js application docker-compose template
11
+ - `infra/compose.yaml` - Infrastructure services docker-compose template
12
+
13
+ ## Template Variables
14
+
15
+ ### Application Configuration
16
+ - `{{app.key}}` - Application key/identifier
17
+ - `{{image.name}}` - Container image name
18
+ - `{{image.tag}}` - Container image tag (defaults to "latest")
19
+ - `{{port}}` - Application port from schema
20
+ - `{{startupCommand}}` - Custom startup command (optional)
21
+
22
+ ### Health Check Configuration
23
+ - `{{healthCheck.path}}` - Health check endpoint path (e.g., "/health")
24
+ - `{{healthCheck.interval}}` - Health check interval in seconds
25
+
26
+ ### Service Requirements
27
+ - `{{requiresDatabase}}` - Database requirement flag (conditional db-init service)
28
+ - `{{requiresStorage}}` - Storage requirement flag (conditional volume mounting)
29
+ - `{{databases}}` - Array of database configurations
30
+
31
+ ### Build Configuration
32
+ - `{{build.localPort}}` - Local development port (different from Docker port)
33
+ - `{{mountVolume}}` - Volume mount path for local development
34
+
35
+ ## Usage
36
+
37
+ These templates are processed by the AI Fabrix Builder SDK based on the application schema defined in `variables.yaml`. The generated files will be valid Docker files after template processing.
38
+
39
+ ## VS Code Configuration
40
+
41
+ The `.vscode/settings.json` file is configured to:
42
+ - Treat `.hbs` files as Handlebars templates (not Dockerfiles)
43
+ - Ignore template files from Docker linting
44
+ - Prevent false linting errors on template variables
45
+
46
+ ## Generated Output
47
+
48
+ After processing, these templates generate:
49
+ - Valid Dockerfiles with proper syntax
50
+ - Docker-compose files with conditional services
51
+ - Infrastructure configurations with shared services only
@@ -0,0 +1,15 @@
1
+ name: CI/CD Pipeline
2
+ on:
3
+ push:
4
+ branches: [{{mainBranch}}]
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v4
10
+ - name: Setup Node.js
11
+ uses: actions/setup-node@v4
12
+ with:
13
+ node-version: '20'
14
+ - name: Run tests
15
+ run: npm test
@@ -0,0 +1,35 @@
1
+ name: Pull Request Checks
2
+
3
+ on:
4
+ pull_request:
5
+ types: [opened, synchronize, reopened]
6
+
7
+ jobs:
8
+ pr-validation:
9
+ name: PR Validation
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v4
13
+ with:
14
+ fetch-depth: 0
15
+
16
+ - name: Setup Node.js
17
+ uses: actions/setup-node@v4
18
+ with:
19
+ node-version: '20'
20
+ cache: 'npm'
21
+
22
+ - name: Install dependencies
23
+ run: npm ci
24
+
25
+ - name: Check file sizes
26
+ run: |
27
+ find {{sourceDir}} -name "*.{{fileExtension}}" -exec sh -c 'lines=$(wc -l < "$1"); if [ $lines -gt 500 ]; then echo "File $1 exceeds 500 lines ($lines)"; exit 1; fi' _ {} \;
28
+
29
+ - name: Check for TODOs in modified files
30
+ run: |
31
+ git diff origin/${{ github.base_ref }} --name-only | grep "\.{{fileExtension}}$" | xargs grep -n "TODO" || true
32
+
33
+ - name: Validate commit messages
34
+ run: |
35
+ git log origin/${{ github.base_ref }}..HEAD --format=%s | grep -E "^(feat|fix|docs|style|refactor|test|chore|perf)(\(.+\))?: .+" || echo "Warning: Some commits don't follow conventional commits"
@@ -0,0 +1,79 @@
1
+ name: Release and Publish
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*.*.*'
7
+
8
+ jobs:
9
+ validate:
10
+ name: Validate Release
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - name: Setup Node.js
16
+ uses: actions/setup-node@v4
17
+ with:
18
+ node-version: '20'
19
+ cache: 'npm'
20
+
21
+ - name: Install dependencies
22
+ run: npm ci
23
+
24
+ - name: Run all checks
25
+ run: npm run validate
26
+
27
+ - name: Verify version matches tag
28
+ run: |
29
+ TAG_VERSION=${GITHUB_REF#refs/tags/v}
30
+ PACKAGE_VERSION=$(node -p "require('./package.json').version")
31
+ if [ "$TAG_VERSION" != "$PACKAGE_VERSION" ]; then
32
+ echo "Tag version ($TAG_VERSION) does not match package.json version ($PACKAGE_VERSION)"
33
+ exit 1
34
+ fi
35
+
36
+ {{#if publishToNpm}}
37
+ publish-npm:
38
+ name: Publish to NPM
39
+ runs-on: ubuntu-latest
40
+ needs: validate
41
+ steps:
42
+ - uses: actions/checkout@v4
43
+
44
+ - name: Setup Node.js
45
+ uses: actions/setup-node@v4
46
+ with:
47
+ node-version: '20'
48
+ registry-url: 'https://registry.npmjs.org'
49
+ cache: 'npm'
50
+
51
+ - name: Install dependencies
52
+ run: npm ci
53
+
54
+ - name: Build package
55
+ run: npm run build
56
+
57
+ - name: Publish to NPM
58
+ run: npm publish --access public
59
+ env:
60
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
61
+ {{/if}}
62
+
63
+ create-release:
64
+ name: Create GitHub Release
65
+ runs-on: ubuntu-latest
66
+ needs: {{#if publishToNpm}}publish-npm{{else}}validate{{/if}}
67
+ steps:
68
+ - uses: actions/checkout@v4
69
+
70
+ - name: Create Release
71
+ uses: actions/create-release@v1
72
+ env:
73
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
74
+ with:
75
+ tag_name: ${{ github.ref }}
76
+ release_name: Release ${{ github.ref }}
77
+ body: Release {{appName}} ${{ github.ref }}
78
+ draft: false
79
+ prerelease: false
@@ -0,0 +1,11 @@
1
+ name: Test Workflow
2
+ on:
3
+ push:
4
+ branches: [{{mainBranch}}]
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v4
10
+ - name: Test {{appName}}
11
+ run: echo "Testing {{appName}}"
@@ -0,0 +1,11 @@
1
+ name: Test Workflow
2
+ on:
3
+ push:
4
+ branches: [{{mainBranch}}]
5
+ jobs:
6
+ test:
7
+ runs-on: ubuntu-latest
8
+ steps:
9
+ - uses: actions/checkout@v4
10
+ - name: Test {{appName}}
11
+ run: echo "Testing {{appName}}"