@aifabrix/builder 2.0.0 → 2.0.2

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.
Files changed (58) hide show
  1. package/README.md +6 -2
  2. package/bin/aifabrix.js +9 -3
  3. package/jest.config.integration.js +30 -0
  4. package/lib/app-config.js +157 -0
  5. package/lib/app-deploy.js +233 -82
  6. package/lib/app-dockerfile.js +112 -0
  7. package/lib/app-prompts.js +244 -0
  8. package/lib/app-push.js +172 -0
  9. package/lib/app-run.js +334 -133
  10. package/lib/app.js +208 -274
  11. package/lib/audit-logger.js +2 -0
  12. package/lib/build.js +209 -98
  13. package/lib/cli.js +76 -86
  14. package/lib/commands/app.js +414 -0
  15. package/lib/commands/login.js +304 -0
  16. package/lib/config.js +78 -0
  17. package/lib/deployer.js +225 -81
  18. package/lib/env-reader.js +45 -30
  19. package/lib/generator.js +308 -191
  20. package/lib/github-generator.js +67 -7
  21. package/lib/infra.js +156 -61
  22. package/lib/push.js +105 -10
  23. package/lib/schema/application-schema.json +30 -2
  24. package/lib/schema/infrastructure-schema.json +589 -0
  25. package/lib/secrets.js +229 -24
  26. package/lib/template-validator.js +205 -0
  27. package/lib/templates.js +305 -170
  28. package/lib/utils/api.js +329 -0
  29. package/lib/utils/cli-utils.js +97 -0
  30. package/lib/utils/dockerfile-utils.js +131 -0
  31. package/lib/utils/environment-checker.js +125 -0
  32. package/lib/utils/error-formatter.js +61 -0
  33. package/lib/utils/health-check.js +187 -0
  34. package/lib/utils/logger.js +53 -0
  35. package/lib/utils/template-helpers.js +223 -0
  36. package/lib/utils/variable-transformer.js +271 -0
  37. package/lib/validator.js +27 -112
  38. package/package.json +13 -10
  39. package/templates/README.md +75 -3
  40. package/templates/applications/keycloak/Dockerfile +36 -0
  41. package/templates/applications/keycloak/env.template +32 -0
  42. package/templates/applications/keycloak/rbac.yaml +37 -0
  43. package/templates/applications/keycloak/variables.yaml +56 -0
  44. package/templates/applications/miso-controller/Dockerfile +125 -0
  45. package/templates/applications/miso-controller/env.template +129 -0
  46. package/templates/applications/miso-controller/rbac.yaml +168 -0
  47. package/templates/applications/miso-controller/variables.yaml +56 -0
  48. package/templates/github/release.yaml.hbs +5 -26
  49. package/templates/github/steps/npm.hbs +24 -0
  50. package/templates/infra/compose.yaml +6 -6
  51. package/templates/python/docker-compose.hbs +19 -12
  52. package/templates/python/main.py +80 -0
  53. package/templates/python/requirements.txt +4 -0
  54. package/templates/typescript/Dockerfile.hbs +2 -2
  55. package/templates/typescript/docker-compose.hbs +19 -12
  56. package/templates/typescript/index.ts +116 -0
  57. package/templates/typescript/package.json +26 -0
  58. package/templates/typescript/tsconfig.json +24 -0
@@ -0,0 +1,271 @@
1
+ /**
2
+ * Variable Transformation Utilities
3
+ *
4
+ * Transforms nested variables.yaml structure to flat schema format
5
+ * Converts app.*, image.*, requires.* to schema-compatible structure
6
+ *
7
+ * @fileoverview Variable transformation utilities for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ /**
13
+ * Sanitizes authentication type - map keycloak to azure (schema allows: azure, local, none)
14
+ * @function sanitizeAuthType
15
+ * @param {string} authType - Authentication type
16
+ * @returns {string} Sanitized authentication type
17
+ */
18
+ function sanitizeAuthType(authType) {
19
+ if (authType === 'keycloak') {
20
+ return 'azure';
21
+ }
22
+ if (authType && !['azure', 'local', 'none'].includes(authType)) {
23
+ return 'azure'; // Default to azure if invalid type
24
+ }
25
+ return authType;
26
+ }
27
+
28
+ /**
29
+ * Transforms flat structure to schema-compatible format
30
+ * @function transformFlatStructure
31
+ * @param {Object} variables - Raw variables from YAML
32
+ * @param {string} appName - Application name (fallback)
33
+ * @returns {Object} Transformed variables matching schema
34
+ */
35
+ function transformFlatStructure(variables, appName) {
36
+ const result = {
37
+ key: variables.key || appName,
38
+ displayName: variables.displayName || appName,
39
+ description: variables.description || '',
40
+ type: variables.type || 'webapp',
41
+ image: variables.image,
42
+ registryMode: variables.registryMode || 'external',
43
+ port: variables.port || 3000,
44
+ requiresDatabase: variables.requiresDatabase || false,
45
+ requiresRedis: variables.requiresRedis || false,
46
+ requiresStorage: variables.requiresStorage || false,
47
+ databases: variables.databases || [],
48
+ ...variables
49
+ };
50
+
51
+ // Sanitize authentication if present
52
+ if (result.authentication && result.authentication.type) {
53
+ result.authentication = {
54
+ ...result.authentication,
55
+ type: sanitizeAuthType(result.authentication.type)
56
+ };
57
+ }
58
+
59
+ return result;
60
+ }
61
+
62
+ /**
63
+ * Builds image reference string from variables
64
+ * @function buildImageReference
65
+ * @param {Object} variables - Variables configuration
66
+ * @param {string} appName - Application name (fallback)
67
+ * @returns {string} Image reference string
68
+ */
69
+ function buildImageReference(variables, appName) {
70
+ const imageName = variables.image?.name || variables.app?.key || appName;
71
+ const registry = variables.image?.registry;
72
+ const tag = variables.image?.tag || 'latest';
73
+ return registry ? `${registry}/${imageName}:${tag}` : `${imageName}:${tag}`;
74
+ }
75
+
76
+ /**
77
+ * Validates repository URL format
78
+ * @function validateRepositoryUrl
79
+ * @param {string} url - Repository URL
80
+ * @returns {boolean} True if valid
81
+ */
82
+ function validateRepositoryUrl(url) {
83
+ if (!url || url.trim() === '') {
84
+ return false;
85
+ }
86
+ const repoPattern = /^(https:\/\/github\.com\/[^/]+\/[^/]+|https:\/\/gitlab\.com\/[^/]+\/[^/]+|https:\/\/dev\.azure\.com\/[^/]+\/[^/]+\/[^/]+)$/;
87
+ return repoPattern.test(url);
88
+ }
89
+
90
+ /**
91
+ * Validates and transforms repository configuration
92
+ * @function validateRepositoryConfig
93
+ * @param {Object} repository - Repository configuration
94
+ * @returns {Object|null} Validated repository config or null
95
+ */
96
+ function validateRepositoryConfig(repository) {
97
+ if (!repository) {
98
+ return null;
99
+ }
100
+
101
+ const repo = { enabled: repository.enabled || false };
102
+ if (validateRepositoryUrl(repository.repositoryUrl)) {
103
+ repo.repositoryUrl = repository.repositoryUrl;
104
+ }
105
+
106
+ if (repo.enabled || repo.repositoryUrl) {
107
+ return repo;
108
+ }
109
+
110
+ return null;
111
+ }
112
+
113
+ /**
114
+ * Validates and transforms build configuration
115
+ * @function validateBuildConfig
116
+ * @param {Object} build - Build configuration
117
+ * @returns {Object|null} Validated build config or null
118
+ */
119
+ function validateBuildConfig(build) {
120
+ if (!build) {
121
+ return null;
122
+ }
123
+
124
+ const buildConfig = {};
125
+ if (build.envOutputPath) {
126
+ buildConfig.envOutputPath = build.envOutputPath;
127
+ }
128
+ if (build.secrets !== null && build.secrets !== undefined && build.secrets !== '') {
129
+ buildConfig.secrets = build.secrets;
130
+ }
131
+ if (build.localPort) {
132
+ buildConfig.localPort = build.localPort;
133
+ }
134
+ if (build.language) {
135
+ buildConfig.language = build.language;
136
+ }
137
+ if (build.context) {
138
+ buildConfig.context = build.context;
139
+ }
140
+ if (build.dockerfile && build.dockerfile.trim() !== '') {
141
+ buildConfig.dockerfile = build.dockerfile;
142
+ }
143
+
144
+ return Object.keys(buildConfig).length > 0 ? buildConfig : null;
145
+ }
146
+
147
+ /**
148
+ * Validates and transforms deployment configuration
149
+ * @function validateDeploymentConfig
150
+ * @param {Object} deployment - Deployment configuration
151
+ * @returns {Object|null} Validated deployment config or null
152
+ */
153
+ function validateDeploymentConfig(deployment) {
154
+ if (!deployment) {
155
+ return null;
156
+ }
157
+
158
+ const deploymentConfig = {};
159
+ if (deployment.controllerUrl && deployment.controllerUrl.trim() !== '' && /^https:\/\/.*$/.test(deployment.controllerUrl)) {
160
+ deploymentConfig.controllerUrl = deployment.controllerUrl;
161
+ }
162
+ if (deployment.clientId && deployment.clientId.trim() !== '' && /^[a-z0-9-]+$/.test(deployment.clientId)) {
163
+ deploymentConfig.clientId = deployment.clientId;
164
+ }
165
+ if (deployment.clientSecret && deployment.clientSecret.trim() !== '' && /^(kv:\/\/.*|.+)$/.test(deployment.clientSecret)) {
166
+ deploymentConfig.clientSecret = deployment.clientSecret;
167
+ }
168
+
169
+ return Object.keys(deploymentConfig).length > 0 ? deploymentConfig : null;
170
+ }
171
+
172
+ /**
173
+ * Transforms optional fields from variables
174
+ * @function transformOptionalFields
175
+ * @param {Object} variables - Raw variables from YAML
176
+ * @param {Object} transformed - Base transformed object
177
+ * @returns {Object} Transformed object with optional fields added
178
+ */
179
+ function transformOptionalFields(variables, transformed) {
180
+ if (variables.healthCheck) {
181
+ transformed.healthCheck = variables.healthCheck;
182
+ }
183
+
184
+ if (variables.authentication) {
185
+ transformed.authentication = {
186
+ ...variables.authentication,
187
+ type: sanitizeAuthType(variables.authentication.type)
188
+ };
189
+ }
190
+
191
+ const repository = validateRepositoryConfig(variables.repository);
192
+ if (repository) {
193
+ transformed.repository = repository;
194
+ }
195
+
196
+ const build = validateBuildConfig(variables.build);
197
+ if (build) {
198
+ transformed.build = build;
199
+ }
200
+
201
+ const deployment = validateDeploymentConfig(variables.deployment);
202
+ if (deployment) {
203
+ transformed.deployment = deployment;
204
+ }
205
+
206
+ if (variables.startupCommand) {
207
+ transformed.startupCommand = variables.startupCommand;
208
+ }
209
+ if (variables.runtimeVersion) {
210
+ transformed.runtimeVersion = variables.runtimeVersion;
211
+ }
212
+ if (variables.scaling) {
213
+ transformed.scaling = variables.scaling;
214
+ }
215
+ if (variables.frontDoorRouting) {
216
+ transformed.frontDoorRouting = variables.frontDoorRouting;
217
+ }
218
+ if (variables.roles) {
219
+ transformed.roles = variables.roles;
220
+ }
221
+ if (variables.permissions) {
222
+ transformed.permissions = variables.permissions;
223
+ }
224
+
225
+ return transformed;
226
+ }
227
+
228
+ /**
229
+ * Transforms nested variables.yaml structure to flat schema format
230
+ * Converts app.*, image.*, requires.* to schema-compatible structure
231
+ * Handles both flat and nested structures
232
+ *
233
+ * @function transformVariablesForValidation
234
+ * @param {Object} variables - Raw variables from YAML
235
+ * @param {string} appName - Application name (fallback)
236
+ * @returns {Object} Transformed variables matching schema
237
+ */
238
+ function transformVariablesForValidation(variables, appName) {
239
+ // Check if structure is already flat (has key, displayName, image as string)
240
+ const isFlat = variables.key && variables.image && typeof variables.image === 'string';
241
+
242
+ if (isFlat) {
243
+ return transformFlatStructure(variables, appName);
244
+ }
245
+
246
+ // Nested structure - transform it
247
+ const requires = variables.requires || {};
248
+ const imageRef = buildImageReference(variables, appName);
249
+
250
+ // Transform to flat schema structure
251
+ const transformed = {
252
+ key: variables.app?.key || appName,
253
+ displayName: variables.app?.displayName || appName,
254
+ description: variables.app?.description || '',
255
+ type: variables.app?.type || 'webapp',
256
+ image: imageRef,
257
+ registryMode: variables.image?.registryMode || 'external',
258
+ port: variables.port || 3000,
259
+ requiresDatabase: requires.database || false,
260
+ requiresRedis: requires.redis || false,
261
+ requiresStorage: requires.storage || false,
262
+ databases: requires.databases || (requires.database ? [{ name: variables.app?.key || appName }] : [])
263
+ };
264
+
265
+ return transformOptionalFields(variables, transformed);
266
+ }
267
+
268
+ module.exports = {
269
+ transformVariablesForValidation
270
+ };
271
+
package/lib/validator.js CHANGED
@@ -14,6 +14,9 @@ const path = require('path');
14
14
  const yaml = require('js-yaml');
15
15
  const Ajv = require('ajv');
16
16
  const applicationSchema = require('./schema/application-schema.json');
17
+ const { transformVariablesForValidation } = require('./utils/variable-transformer');
18
+ const { checkEnvironment } = require('./utils/environment-checker');
19
+ const { formatValidationErrors } = require('./utils/error-formatter');
17
20
 
18
21
  /**
19
22
  * Validates variables.yaml file against application schema
@@ -49,9 +52,12 @@ async function validateVariables(appName) {
49
52
  throw new Error(`Invalid YAML syntax in variables.yaml: ${error.message}`);
50
53
  }
51
54
 
55
+ // Transform nested structure to flat schema format
56
+ const transformed = transformVariablesForValidation(variables, appName);
57
+
52
58
  const ajv = new Ajv({ allErrors: true, strict: false });
53
59
  const validate = ajv.compile(applicationSchema);
54
- const valid = validate(variables);
60
+ const valid = validate(transformed);
55
61
 
56
62
  return {
57
63
  valid,
@@ -209,125 +215,33 @@ async function validateEnvTemplate(appName) {
209
215
  }
210
216
 
211
217
  /**
212
- * Checks the development environment for common issues
213
- * Validates Docker, ports, secrets, and other requirements
218
+ * Validates deployment JSON against application schema
219
+ * Ensures generated aifabrix-deploy.json matches the schema structure
214
220
  *
215
- * @async
216
- * @function checkEnvironment
217
- * @returns {Promise<Object>} Environment check result
218
- * @throws {Error} If critical issues are found
221
+ * @function validateDeploymentJson
222
+ * @param {Object} deployment - Deployment JSON object to validate
223
+ * @returns {Object} Validation result with errors
219
224
  *
220
225
  * @example
221
- * const result = await checkEnvironment();
222
- * // Returns: { docker: 'ok', ports: 'ok', secrets: 'missing', recommendations: [...] }
226
+ * const result = validateDeploymentJson(deployment);
227
+ * // Returns: { valid: true, errors: [] }
223
228
  */
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');
229
+ function validateDeploymentJson(deployment) {
230
+ if (!deployment || typeof deployment !== 'object') {
231
+ return {
232
+ valid: false,
233
+ errors: ['Deployment must be an object']
234
+ };
290
235
  }
291
236
 
292
- return result;
293
- }
237
+ const ajv = new Ajv({ allErrors: true, strict: false });
238
+ const validate = ajv.compile(applicationSchema);
239
+ const valid = validate(deployment);
294
240
 
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'}`
241
+ return {
242
+ valid,
243
+ errors: valid ? [] : formatValidationErrors(validate.errors)
320
244
  };
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
245
  }
332
246
 
333
247
  /**
@@ -371,6 +285,7 @@ module.exports = {
371
285
  validateVariables,
372
286
  validateRbac,
373
287
  validateEnvTemplate,
288
+ validateDeploymentJson,
374
289
  checkEnvironment,
375
290
  formatValidationErrors,
376
291
  validateApplication
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.0.0",
3
+ "version": "2.0.2",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -10,6 +10,9 @@
10
10
  "test": "jest --coverage",
11
11
  "test:watch": "jest --watch",
12
12
  "test:ci": "jest --ci --coverage --watchAll=false",
13
+ "test:integration": "jest --config jest.config.integration.js --runInBand",
14
+ "test:integration:python": "cross-env TEST_LANGUAGE=python jest --config jest.config.integration.js --runInBand",
15
+ "test:integration:typescript": "cross-env TEST_LANGUAGE=typescript jest --config jest.config.integration.js --runInBand",
13
16
  "lint": "eslint . --ext .js",
14
17
  "lint:fix": "eslint . --ext .js --fix",
15
18
  "lint:ci": "eslint . --ext .js --format json --output-file eslint-report.json",
@@ -17,8 +20,7 @@
17
20
  "build": "npm run lint && npm run test:ci",
18
21
  "validate": "npm run build",
19
22
  "prepublishOnly": "npm run validate",
20
- "precommit": "npm run lint:fix && npm run test",
21
- "posttest": "npm run lint"
23
+ "precommit": "npm run lint:fix && npm run test"
22
24
  },
23
25
  "keywords": [
24
26
  "aifabrix",
@@ -34,19 +36,20 @@
34
36
  "node": ">=18.0.0"
35
37
  },
36
38
  "dependencies": {
37
- "commander": "^11.1.0",
38
- "js-yaml": "^4.1.0",
39
39
  "ajv": "^8.12.0",
40
- "handlebars": "^4.7.8",
41
40
  "axios": "^1.6.0",
42
41
  "chalk": "^4.1.2",
43
- "ora": "^5.4.1",
44
- "inquirer": "^8.2.5"
42
+ "commander": "^11.1.0",
43
+ "handlebars": "^4.7.8",
44
+ "inquirer": "^8.2.5",
45
+ "js-yaml": "^4.1.0",
46
+ "ora": "^5.4.1"
45
47
  },
46
48
  "devDependencies": {
47
- "jest": "^29.7.0",
49
+ "@types/node": "^20.10.0",
50
+ "cross-env": "^10.1.0",
48
51
  "eslint": "^8.55.0",
49
- "@types/node": "^20.10.0"
52
+ "jest": "^29.7.0"
50
53
  },
51
54
  "repository": {
52
55
  "type": "git",
@@ -1,15 +1,56 @@
1
1
  # AI Fabrix Builder Templates
2
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.
3
+ These are Handlebars (.hbs) template files that generate Docker files and application configurations for AI Fabrix applications. They should NOT be linted as Dockerfiles since they contain template variables that will be replaced during generation.
4
4
 
5
- ## Template Files
5
+ ## Template Structure
6
+
7
+ The templates directory is organized as follows:
8
+
9
+ ### Application Templates (for `--template` flag)
10
+
11
+ Application templates are folder-based and located under `templates/applications/`. When you use `--template <name>`, the tool looks for `templates/applications/<name>/` and copies all files from that folder to `builder/<app>/`.
12
+
13
+ **Example:**
14
+ - `templates/applications/miso-controller/` - Miso Controller application template
15
+ - `templates/applications/keycloak/` - Keycloak application template
16
+
17
+ **Template Validation:**
18
+ - Template folder must exist in `templates/applications/<name>/`
19
+ - Template folder must contain at least one file
20
+ - Hidden files (starting with `.`) are skipped
21
+ - If a template includes a `Dockerfile`, it will be copied to `builder/<app>/Dockerfile` along with other files
22
+
23
+ ### Language Templates
6
24
 
7
25
  - `python/Dockerfile.hbs` - Python application Dockerfile template
8
26
  - `python/docker-compose.hbs` - Python application docker-compose template
9
27
  - `typescript/Dockerfile.hbs` - TypeScript/Node.js application Dockerfile template
10
28
  - `typescript/docker-compose.hbs` - TypeScript/Node.js application docker-compose template
29
+
30
+ ### Infrastructure Templates
31
+
11
32
  - `infra/compose.yaml` - Infrastructure services docker-compose template
12
33
 
34
+ ### GitHub Workflow Templates
35
+
36
+ - `github/ci.yaml.hbs` - Continuous Integration workflow
37
+ - `github/pr-checks.yaml.hbs` - Pull Request checks workflow
38
+ - `github/release.yaml.hbs` - Release and publish workflow
39
+ - `github/test.yaml.hbs` - Test workflow
40
+
41
+ ### GitHub Workflow Step Templates (for `--github-steps` flag)
42
+
43
+ Extra workflow steps are located in `templates/github/steps/`. When you use `--github-steps <steps>`, the tool loads step templates from `templates/github/steps/{step}.hbs` and includes them in the generated workflows.
44
+
45
+ **Example:**
46
+ - `github/steps/npm.hbs` - NPM publishing step
47
+ - `github/steps/test.hbs` - Custom test step
48
+
49
+ **Step Templates:**
50
+ - Step templates are Handlebars templates that generate workflow job definitions
51
+ - They receive the same context as the main workflow templates
52
+ - Step templates are rendered and included in workflow files based on the `githubSteps` array
53
+
13
54
  ## Template Variables
14
55
 
15
56
  ### Application Configuration
@@ -34,7 +75,38 @@ These are Handlebars (.hbs) template files that generate Docker files for AI Fab
34
75
 
35
76
  ## Usage
36
77
 
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.
78
+ ### Application Templates
79
+
80
+ Use `--template <name>` when creating an application:
81
+
82
+ ```bash
83
+ aifabrix create myapp --template miso-controller --port 3000
84
+ ```
85
+
86
+ This validates that `templates/applications/miso-controller/` exists and copies all files (including Dockerfile if present) from it to `builder/myapp/`.
87
+
88
+ ### GitHub Workflow Steps
89
+
90
+ Use `--github-steps <steps>` when creating an application with GitHub workflows:
91
+
92
+ ```bash
93
+ aifabrix create myapp --github --github-steps npm
94
+ ```
95
+
96
+ This loads step templates from:
97
+ - `templates/github/steps/npm.hbs`
98
+
99
+ And includes them in the generated workflow files (e.g., `release.yaml`).
100
+
101
+ **Currently available step templates:**
102
+ - `npm.hbs` - Adds NPM publishing job to release workflow
103
+
104
+ **Creating custom step templates:**
105
+ Create your own step templates in `templates/github/steps/{your-step}.hbs` and reference them in `--github-steps`.
106
+
107
+ ### Language and Infrastructure Templates
108
+
109
+ These templates are processed automatically 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
110
 
39
111
  ## VS Code Configuration
40
112
 
@@ -0,0 +1,36 @@
1
+ # Keycloak Identity and Access Management with Custom Themes
2
+ # This Dockerfile extends Keycloak 24.0 with custom themes
3
+
4
+ FROM quay.io/keycloak/keycloak:24.0
5
+
6
+ # Set working directory
7
+ WORKDIR /opt/keycloak
8
+
9
+ # Copy custom themes from share directory (only if themes/ folder exists)
10
+ USER root
11
+ # Use a shell script approach: copy themes if it exists, otherwise skip
12
+ # First, copy everything from build context to a staging area
13
+ COPY . /tmp/build-context/
14
+ # Then conditionally copy themes if the directory exists
15
+ RUN if [ -d "/tmp/build-context/themes" ] && [ "$(ls -A /tmp/build-context/themes)" ]; then \
16
+ cp -r /tmp/build-context/themes/* /opt/keycloak/themes/ && \
17
+ chown -R keycloak:keycloak /opt/keycloak/themes/ && \
18
+ echo "Themes copied successfully"; \
19
+ else \
20
+ echo "No themes directory found, skipping theme copy"; \
21
+ fi && \
22
+ rm -rf /tmp/build-context
23
+
24
+ # Build Keycloak with health checks enabled
25
+ # This enables the /health, /health/live, /health/ready, and /health/started endpoints
26
+ RUN /opt/keycloak/bin/kc.sh build --health-enabled=true
27
+
28
+ # Switch back to keycloak user
29
+ USER keycloak
30
+
31
+ # Keycloak runs on port 8080 internally, exposed as 8082
32
+ EXPOSE 8080
33
+
34
+ # Default Keycloak command (can be overridden in docker-compose.yaml)
35
+ # Health checks are enabled via the build step above
36
+ CMD ["start-dev"]
@@ -0,0 +1,32 @@
1
+ # Environment Variables Template
2
+ # Use kv:// references for secrets (resolved from .aifabrix/secrets.yaml)
3
+ # Use ${VAR} for environment-specific values
4
+
5
+ # =============================================================================
6
+ # APPLICATION ENVIRONMENT
7
+ # =============================================================================
8
+
9
+ KEYCLOAK_ADMIN=admin
10
+ KEYCLOAK_ADMIN_PASSWORD=kv://keycloak-admin-passwordKeyVault
11
+ KC_HOSTNAME_STRICT=false
12
+ KC_HTTP_ENABLED=true
13
+
14
+ # =============================================================================
15
+ # HEALTH CHECK CONFIGURATION
16
+ # =============================================================================
17
+ # Enable health check endpoints: /health, /health/live, /health/ready, /health/started
18
+
19
+ KC_HEALTH_ENABLED=true
20
+
21
+ # =============================================================================
22
+ # DATABASE CONFIGURATION
23
+ # =============================================================================
24
+ # Connects to postgres service in Docker network (postgres) or localhost (local)
25
+
26
+ KC_DB=postgres
27
+ KC_DB_URL_HOST=${DB_HOST}
28
+ KC_DB_URL_PORT=5432
29
+ KC_DB_URL_DATABASE=keycloak
30
+ KC_DB_USERNAME=keycloak_user
31
+ KC_DB_PASSWORD=kv://databases-keycloak-0-passwordKeyVault
32
+