@aifabrix/builder 2.0.0 → 2.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.
Files changed (61) hide show
  1. package/README.md +5 -3
  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 +235 -144
  10. package/lib/app.js +208 -274
  11. package/lib/audit-logger.js +2 -0
  12. package/lib/build.js +177 -125
  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/env-config.yaml +9 -1
  25. package/lib/schema/infrastructure-schema.json +589 -0
  26. package/lib/secrets.js +229 -24
  27. package/lib/template-validator.js +205 -0
  28. package/lib/templates.js +305 -170
  29. package/lib/utils/api.js +329 -0
  30. package/lib/utils/cli-utils.js +97 -0
  31. package/lib/utils/compose-generator.js +185 -0
  32. package/lib/utils/docker-build.js +173 -0
  33. package/lib/utils/dockerfile-utils.js +131 -0
  34. package/lib/utils/environment-checker.js +125 -0
  35. package/lib/utils/error-formatter.js +61 -0
  36. package/lib/utils/health-check.js +187 -0
  37. package/lib/utils/logger.js +53 -0
  38. package/lib/utils/template-helpers.js +223 -0
  39. package/lib/utils/variable-transformer.js +271 -0
  40. package/lib/validator.js +27 -112
  41. package/package.json +14 -10
  42. package/templates/README.md +75 -3
  43. package/templates/applications/keycloak/Dockerfile +36 -0
  44. package/templates/applications/keycloak/env.template +32 -0
  45. package/templates/applications/keycloak/rbac.yaml +37 -0
  46. package/templates/applications/keycloak/variables.yaml +56 -0
  47. package/templates/applications/miso-controller/Dockerfile +125 -0
  48. package/templates/applications/miso-controller/env.template +129 -0
  49. package/templates/applications/miso-controller/rbac.yaml +214 -0
  50. package/templates/applications/miso-controller/variables.yaml +56 -0
  51. package/templates/github/release.yaml.hbs +5 -26
  52. package/templates/github/steps/npm.hbs +24 -0
  53. package/templates/infra/compose.yaml +6 -6
  54. package/templates/python/docker-compose.hbs +19 -12
  55. package/templates/python/main.py +80 -0
  56. package/templates/python/requirements.txt +4 -0
  57. package/templates/typescript/Dockerfile.hbs +2 -2
  58. package/templates/typescript/docker-compose.hbs +19 -12
  59. package/templates/typescript/index.ts +116 -0
  60. package/templates/typescript/package.json +26 -0
  61. package/templates/typescript/tsconfig.json +24 -0
package/lib/infra.js CHANGED
@@ -15,9 +15,24 @@ const path = require('path');
15
15
  const fs = require('fs');
16
16
  const os = require('os');
17
17
  const secrets = require('./secrets');
18
+ const logger = require('./utils/logger');
18
19
 
19
20
  const execAsync = promisify(exec);
20
21
 
22
+ // Wrapper to support cwd option
23
+ function execAsyncWithCwd(command, options = {}) {
24
+ return new Promise((resolve, reject) => {
25
+ const { cwd, ...execOptions } = options;
26
+ exec(command, { ...execOptions, cwd }, (error, stdout, stderr) => {
27
+ if (error) {
28
+ reject(error);
29
+ } else {
30
+ resolve({ stdout, stderr });
31
+ }
32
+ });
33
+ });
34
+ }
35
+
21
36
  /**
22
37
  * Starts local infrastructure services
23
38
  * Launches Postgres, Redis, Keycloak, and Controller in Docker containers
@@ -43,7 +58,7 @@ async function checkDockerAvailability() {
43
58
  async function ensureAdminSecrets() {
44
59
  const adminSecretsPath = path.join(os.homedir(), '.aifabrix', 'admin-secrets.env');
45
60
  if (!fs.existsSync(adminSecretsPath)) {
46
- console.log('Generating admin-secrets.env...');
61
+ logger.log('Generating admin-secrets.env...');
47
62
  await secrets.generateAdminSecretsEnv();
48
63
  }
49
64
  return adminSecretsPath;
@@ -59,21 +74,26 @@ async function startInfra() {
59
74
  throw new Error(`Compose template not found: ${templatePath}`);
60
75
  }
61
76
 
62
- const template = fs.readFileSync(templatePath, 'utf8');
63
- const tempComposePath = path.join(os.tmpdir(), 'aifabrix-compose.yaml');
64
- fs.writeFileSync(tempComposePath, template);
77
+ // Create infra directory in ~/.aifabrix
78
+ const aifabrixDir = path.join(os.homedir(), '.aifabrix');
79
+ const infraDir = path.join(aifabrixDir, 'infra');
80
+ if (!fs.existsSync(infraDir)) {
81
+ fs.mkdirSync(infraDir, { recursive: true });
82
+ }
83
+
84
+ const composePath = path.join(infraDir, 'compose.yaml');
85
+ fs.writeFileSync(composePath, fs.readFileSync(templatePath, 'utf8'));
65
86
 
66
87
  try {
67
- console.log('Starting infrastructure services...');
68
- await execAsync(`docker-compose -f "${tempComposePath}" --env-file "${adminSecretsPath}" up -d`);
69
- console.log('Infrastructure services started successfully');
88
+ logger.log(`Using compose file: ${composePath}`);
89
+ logger.log('Starting infrastructure services...');
90
+ await execAsyncWithCwd(`docker-compose -f "${composePath}" -p infra --env-file "${adminSecretsPath}" up -d`, { cwd: infraDir });
91
+ logger.log('Infrastructure services started successfully');
70
92
 
71
93
  await waitForServices();
72
- console.log('All services are healthy and ready');
94
+ logger.log('All services are healthy and ready');
73
95
  } finally {
74
- if (fs.existsSync(tempComposePath)) {
75
- fs.unlinkSync(tempComposePath);
76
- }
96
+ // Keep the compose file for stop commands
77
97
  }
78
98
  }
79
99
 
@@ -91,25 +111,23 @@ async function startInfra() {
91
111
  * // All infrastructure containers are stopped and removed
92
112
  */
93
113
  async function stopInfra() {
94
- const templatePath = path.join(__dirname, '..', 'templates', 'infra', 'compose.yaml');
95
- const adminSecretsPath = path.join(os.homedir(), '.aifabrix', 'admin-secrets.env');
114
+ const aifabrixDir = path.join(os.homedir(), '.aifabrix');
115
+ const composePath = path.join(aifabrixDir, 'infra', 'compose.yaml');
116
+ const adminSecretsPath = path.join(aifabrixDir, 'admin-secrets.env');
96
117
 
97
- if (!fs.existsSync(templatePath) || !fs.existsSync(adminSecretsPath)) {
98
- console.log('Infrastructure not running or not properly configured');
118
+ if (!fs.existsSync(composePath) || !fs.existsSync(adminSecretsPath)) {
119
+ logger.log('Infrastructure not running or not properly configured');
99
120
  return;
100
121
  }
101
122
 
102
- const tempComposePath = path.join(os.tmpdir(), 'aifabrix-compose.yaml');
103
- fs.writeFileSync(tempComposePath, fs.readFileSync(templatePath, 'utf8'));
123
+ const infraDir = path.join(aifabrixDir, 'infra');
104
124
 
105
125
  try {
106
- console.log('Stopping infrastructure services...');
107
- await execAsync(`docker-compose -f "${tempComposePath}" --env-file "${adminSecretsPath}" down`);
108
- console.log('Infrastructure services stopped');
126
+ logger.log('Stopping infrastructure services...');
127
+ await execAsyncWithCwd(`docker-compose -f "${composePath}" -p infra --env-file "${adminSecretsPath}" down`, { cwd: infraDir });
128
+ logger.log('Infrastructure services stopped');
109
129
  } finally {
110
- if (fs.existsSync(tempComposePath)) {
111
- fs.unlinkSync(tempComposePath);
112
- }
130
+ // Keep the compose file for future use
113
131
  }
114
132
  }
115
133
 
@@ -127,25 +145,90 @@ async function stopInfra() {
127
145
  * // All infrastructure containers and data are removed
128
146
  */
129
147
  async function stopInfraWithVolumes() {
130
- const templatePath = path.join(__dirname, '..', 'templates', 'infra', 'compose.yaml');
131
- const adminSecretsPath = path.join(os.homedir(), '.aifabrix', 'admin-secrets.env');
148
+ const aifabrixDir = path.join(os.homedir(), '.aifabrix');
149
+ const composePath = path.join(aifabrixDir, 'infra', 'compose.yaml');
150
+ const adminSecretsPath = path.join(aifabrixDir, 'admin-secrets.env');
132
151
 
133
- if (!fs.existsSync(templatePath) || !fs.existsSync(adminSecretsPath)) {
134
- console.log('Infrastructure not running or not properly configured');
152
+ if (!fs.existsSync(composePath) || !fs.existsSync(adminSecretsPath)) {
153
+ logger.log('Infrastructure not running or not properly configured');
135
154
  return;
136
155
  }
137
156
 
138
- const tempComposePath = path.join(os.tmpdir(), 'aifabrix-compose.yaml');
139
- fs.writeFileSync(tempComposePath, fs.readFileSync(templatePath, 'utf8'));
157
+ const infraDir = path.join(aifabrixDir, 'infra');
140
158
 
141
159
  try {
142
- console.log('Stopping infrastructure services and removing all data...');
143
- await execAsync(`docker-compose -f "${tempComposePath}" --env-file "${adminSecretsPath}" down -v`);
144
- console.log('Infrastructure services stopped and all data removed');
160
+ logger.log('Stopping infrastructure services and removing all data...');
161
+ await execAsyncWithCwd(`docker-compose -f "${composePath}" -p infra --env-file "${adminSecretsPath}" down -v`, { cwd: infraDir });
162
+ logger.log('Infrastructure services stopped and all data removed');
145
163
  } finally {
146
- if (fs.existsSync(tempComposePath)) {
147
- fs.unlinkSync(tempComposePath);
164
+ // Keep the compose file for future use
165
+ }
166
+ }
167
+
168
+ /**
169
+ * Finds container by name pattern
170
+ * @private
171
+ * @async
172
+ * @param {string} serviceName - Service name
173
+ * @returns {Promise<string|null>} Container name or null if not found
174
+ */
175
+ async function findContainer(serviceName) {
176
+ try {
177
+ // Try both naming patterns: infra-* (dynamic names) and aifabrix-* (hardcoded names)
178
+ let { stdout } = await execAsync(`docker ps --filter "name=infra-${serviceName}" --format "{{.Names}}"`);
179
+ let containerName = stdout.trim();
180
+ if (!containerName) {
181
+ // Fallback to hardcoded names
182
+ ({ stdout } = await execAsync(`docker ps --filter "name=aifabrix-${serviceName}" --format "{{.Names}}"`));
183
+ containerName = stdout.trim();
184
+ }
185
+ return containerName;
186
+ } catch (error) {
187
+ return null;
188
+ }
189
+ }
190
+
191
+ /**
192
+ * Checks health status for a service with health checks
193
+ * @private
194
+ * @async
195
+ * @param {string} serviceName - Service name
196
+ * @returns {Promise<string>} Health status
197
+ */
198
+ async function checkServiceWithHealthCheck(serviceName) {
199
+ try {
200
+ const containerName = await findContainer(serviceName);
201
+ if (!containerName) {
202
+ return 'unknown';
148
203
  }
204
+ const { stdout } = await execAsync(`docker inspect --format='{{.State.Health.Status}}' ${containerName}`);
205
+ const status = stdout.trim().replace(/['"]/g, '');
206
+ // Accept both 'healthy' and 'starting' as healthy (starting means it's initializing)
207
+ return (status === 'healthy' || status === 'starting') ? 'healthy' : status;
208
+ } catch (error) {
209
+ return 'unknown';
210
+ }
211
+ }
212
+
213
+ /**
214
+ * Checks health status for a service without health checks
215
+ * @private
216
+ * @async
217
+ * @param {string} serviceName - Service name
218
+ * @returns {Promise<string>} Health status
219
+ */
220
+ async function checkServiceWithoutHealthCheck(serviceName) {
221
+ try {
222
+ const containerName = await findContainer(serviceName);
223
+ if (!containerName) {
224
+ return 'unknown';
225
+ }
226
+ const { stdout } = await execAsync(`docker inspect --format='{{.State.Status}}' ${containerName}`);
227
+ const status = stdout.trim().replace(/['"]/g, '');
228
+ // Treat 'running' or 'healthy' as 'healthy' for services without health checks
229
+ return (status === 'running' || status === 'healthy') ? 'healthy' : 'unhealthy';
230
+ } catch (error) {
231
+ return 'unknown';
149
232
  }
150
233
  }
151
234
 
@@ -163,16 +246,18 @@ async function stopInfraWithVolumes() {
163
246
  * // Returns: { postgres: 'healthy', redis: 'healthy', keycloak: 'healthy', controller: 'healthy' }
164
247
  */
165
248
  async function checkInfraHealth() {
166
- const services = ['postgres', 'redis', 'pgadmin', 'redis-commander'];
249
+ const servicesWithHealthCheck = ['postgres', 'redis'];
250
+ const servicesWithoutHealthCheck = ['pgadmin', 'redis-commander'];
167
251
  const health = {};
168
252
 
169
- for (const service of services) {
170
- try {
171
- const { stdout } = await execAsync(`docker inspect --format='{{.State.Health.Status}}' aifabrix-${service}`);
172
- health[service] = stdout.trim();
173
- } catch (error) {
174
- health[service] = 'unknown';
175
- }
253
+ // Check health status for services with health checks
254
+ for (const service of servicesWithHealthCheck) {
255
+ health[service] = await checkServiceWithHealthCheck(service);
256
+ }
257
+
258
+ // Check if services without health checks are running
259
+ for (const service of servicesWithoutHealthCheck) {
260
+ health[service] = await checkServiceWithoutHealthCheck(service);
176
261
  }
177
262
 
178
263
  return health;
@@ -202,12 +287,21 @@ async function getInfraStatus() {
202
287
 
203
288
  for (const [serviceName, config] of Object.entries(services)) {
204
289
  try {
205
- const { stdout } = await execAsync(`docker inspect --format='{{.State.Status}}' aifabrix-${serviceName}`);
206
- status[serviceName] = {
207
- status: stdout.trim(),
208
- port: config.port,
209
- url: config.url
210
- };
290
+ const containerName = await findContainer(serviceName);
291
+ if (containerName) {
292
+ const { stdout } = await execAsync(`docker inspect --format='{{.State.Status}}' ${containerName}`);
293
+ status[serviceName] = {
294
+ status: stdout.trim(),
295
+ port: config.port,
296
+ url: config.url
297
+ };
298
+ } else {
299
+ status[serviceName] = {
300
+ status: 'not running',
301
+ port: config.port,
302
+ url: config.url
303
+ };
304
+ }
211
305
  } catch (error) {
212
306
  status[serviceName] = {
213
307
  status: 'not running',
@@ -244,24 +338,22 @@ async function restartService(serviceName) {
244
338
  throw new Error(`Invalid service name. Must be one of: ${validServices.join(', ')}`);
245
339
  }
246
340
 
247
- const templatePath = path.join(__dirname, '..', 'templates', 'infra', 'compose.yaml');
248
- const adminSecretsPath = path.join(os.homedir(), '.aifabrix', 'admin-secrets.env');
341
+ const aifabrixDir = path.join(os.homedir(), '.aifabrix');
342
+ const composePath = path.join(aifabrixDir, 'infra', 'compose.yaml');
343
+ const adminSecretsPath = path.join(aifabrixDir, 'admin-secrets.env');
249
344
 
250
- if (!fs.existsSync(templatePath) || !fs.existsSync(adminSecretsPath)) {
345
+ if (!fs.existsSync(composePath) || !fs.existsSync(adminSecretsPath)) {
251
346
  throw new Error('Infrastructure not properly configured');
252
347
  }
253
348
 
254
- const tempComposePath = path.join(os.tmpdir(), 'aifabrix-compose.yaml');
255
- fs.writeFileSync(tempComposePath, fs.readFileSync(templatePath, 'utf8'));
349
+ const infraDir = path.join(aifabrixDir, 'infra');
256
350
 
257
351
  try {
258
- console.log(`Restarting ${serviceName} service...`);
259
- await execAsync(`docker-compose -f "${tempComposePath}" --env-file "${adminSecretsPath}" restart ${serviceName}`);
260
- console.log(`${serviceName} service restarted successfully`);
352
+ logger.log(`Restarting ${serviceName} service...`);
353
+ await execAsyncWithCwd(`docker-compose -f "${composePath}" -p infra --env-file "${adminSecretsPath}" restart ${serviceName}`, { cwd: infraDir });
354
+ logger.log(`${serviceName} service restarted successfully`);
261
355
  } finally {
262
- if (fs.existsSync(tempComposePath)) {
263
- fs.unlinkSync(tempComposePath);
264
- }
356
+ // Keep the compose file for future use
265
357
  }
266
358
  }
267
359
 
@@ -281,8 +373,10 @@ async function waitForServices() {
281
373
  return;
282
374
  }
283
375
 
376
+ // Debug logging
377
+
284
378
  if (attempt < maxAttempts) {
285
- console.log(`Waiting for services to be healthy... (${attempt}/${maxAttempts})`);
379
+ logger.log(`Waiting for services to be healthy... (${attempt}/${maxAttempts})`);
286
380
  await new Promise(resolve => setTimeout(resolve, delay));
287
381
  }
288
382
  }
@@ -296,5 +390,6 @@ module.exports = {
296
390
  stopInfraWithVolumes,
297
391
  checkInfraHealth,
298
392
  getInfraStatus,
299
- restartService
393
+ restartService,
394
+ ensureAdminSecrets
300
395
  };
package/lib/push.js CHANGED
@@ -12,6 +12,7 @@
12
12
  const { exec } = require('child_process');
13
13
  const { promisify } = require('util');
14
14
  const chalk = require('chalk');
15
+ const logger = require('./utils/logger');
15
16
 
16
17
  const execAsync = promisify(exec);
17
18
 
@@ -43,12 +44,55 @@ function extractRegistryName(registryUrl) {
43
44
  }
44
45
 
45
46
  /**
46
- * Validate registry URL format
47
+ * Parses registry URL format
48
+ * @function parseRegistryUrl
49
+ * @param {string} registryUrl - Registry URL to parse
50
+ * @returns {Object|null} Parsed registry info or null if invalid
51
+ */
52
+ function parseRegistryUrl(registryUrl) {
53
+ if (!registryUrl || typeof registryUrl !== 'string') {
54
+ return null;
55
+ }
56
+
57
+ if (registryUrl.includes('://')) {
58
+ return null;
59
+ }
60
+
61
+ if (/^[^.]+\.azurecr\.io$/.test(registryUrl)) {
62
+ return { type: 'acr', valid: true };
63
+ }
64
+
65
+ if (registryUrl === 'docker.io' || registryUrl === 'index.docker.io') {
66
+ return { type: 'dockerhub', valid: true };
67
+ }
68
+
69
+ if (registryUrl === 'ghcr.io') {
70
+ return { type: 'ghcr', valid: true };
71
+ }
72
+
73
+ if (registryUrl === 'azurecr.io') {
74
+ return null;
75
+ }
76
+
77
+ if (/^[^.]+\.azurecr\.com$/.test(registryUrl)) {
78
+ return null;
79
+ }
80
+
81
+ if (/^[a-z0-9][a-z0-9.-]+\.[a-z]{2,}(?::[0-9]+)?$/.test(registryUrl)) {
82
+ return { type: 'custom', valid: true };
83
+ }
84
+
85
+ return null;
86
+ }
87
+
88
+ /**
89
+ * Validates registry URL format
47
90
  * @param {string} registryUrl - Registry URL to validate
48
91
  * @returns {boolean} True if valid
49
92
  */
50
93
  function validateRegistryURL(registryUrl) {
51
- return /^[^.]+\.azurecr\.io$/.test(registryUrl);
94
+ const parsed = parseRegistryUrl(registryUrl);
95
+ return parsed !== null && parsed.valid;
52
96
  }
53
97
 
54
98
  /**
@@ -74,14 +118,62 @@ async function checkACRAuthentication(registry) {
74
118
  async function authenticateACR(registry) {
75
119
  try {
76
120
  const registryName = extractRegistryName(registry);
77
- console.log(chalk.blue(`Authenticating with ${registry}...`));
121
+ logger.log(chalk.blue(`Authenticating with ${registry}...`));
78
122
  await execAsync(`az acr login --name ${registryName}`);
79
- console.log(chalk.green(`✓ Authenticated with ${registry}`));
123
+ logger.log(chalk.green(`✓ Authenticated with ${registry}`));
80
124
  } catch (error) {
81
125
  throw new Error(`Failed to authenticate with Azure Container Registry: ${error.message}`);
82
126
  }
83
127
  }
84
128
 
129
+ /**
130
+ * Authenticate with external registry
131
+ * @param {string} registry - Registry URL
132
+ * @param {string} username - Username for authentication
133
+ * @param {string} password - Password or token for authentication
134
+ * @throws {Error} If authentication fails
135
+ */
136
+ async function authenticateExternalRegistry(registry, username, password) {
137
+ try {
138
+ logger.log(chalk.blue(`Authenticating with ${registry}...`));
139
+
140
+ // Use cross-platform approach: write password to stdin directly
141
+ // This works on Windows, Linux, and macOS
142
+ const { spawn } = require('child_process');
143
+ const dockerLogin = spawn('docker', ['login', registry, '-u', username, '--password-stdin']);
144
+
145
+ return new Promise((resolve, reject) => {
146
+ let errorOutput = '';
147
+
148
+ dockerLogin.stdin.write(password);
149
+ dockerLogin.stdin.end();
150
+
151
+ dockerLogin.stdout.on('data', (_data) => {
152
+ // Authentication output (usually minimal)
153
+ });
154
+
155
+ dockerLogin.stderr.on('data', (data) => {
156
+ errorOutput += data.toString();
157
+ });
158
+
159
+ dockerLogin.on('close', (code) => {
160
+ if (code === 0) {
161
+ logger.log(chalk.green(`✓ Authenticated with ${registry}`));
162
+ resolve();
163
+ } else {
164
+ reject(new Error(`Docker login failed: ${errorOutput || `Exit code ${code}`}`));
165
+ }
166
+ });
167
+
168
+ dockerLogin.on('error', (error) => {
169
+ reject(new Error(`Failed to execute docker login: ${error.message}`));
170
+ });
171
+ });
172
+ } catch (error) {
173
+ throw new Error(`Failed to authenticate with external registry: ${error.message}`);
174
+ }
175
+ }
176
+
85
177
  /**
86
178
  * Check if Docker image exists locally
87
179
  * @param {string} imageName - Image name
@@ -90,8 +182,10 @@ async function authenticateACR(registry) {
90
182
  */
91
183
  async function checkLocalImageExists(imageName, tag = 'latest') {
92
184
  try {
93
- const { stdout } = await execAsync(`docker images --format "{{.Repository}}:{{.Tag}}" | grep "^${imageName}:${tag}$"`);
94
- return stdout.trim() === `${imageName}:${tag}`;
185
+ // Use Docker's native filtering for cross-platform compatibility (Windows-safe)
186
+ const { stdout } = await execAsync(`docker images --format "{{.Repository}}:{{.Tag}}" --filter "reference=${imageName}:${tag}"`);
187
+ const lines = stdout.trim().split('\n').filter(line => line.trim() !== '');
188
+ return lines.some(line => line.trim() === `${imageName}:${tag}`);
95
189
  } catch (error) {
96
190
  return false;
97
191
  }
@@ -105,9 +199,9 @@ async function checkLocalImageExists(imageName, tag = 'latest') {
105
199
  */
106
200
  async function tagImage(sourceImage, targetImage) {
107
201
  try {
108
- console.log(chalk.blue(`Tagging ${sourceImage} as ${targetImage}...`));
202
+ logger.log(chalk.blue(`Tagging ${sourceImage} as ${targetImage}...`));
109
203
  await execAsync(`docker tag ${sourceImage} ${targetImage}`);
110
- console.log(chalk.green(`✓ Tagged: ${targetImage}`));
204
+ logger.log(chalk.green(`✓ Tagged: ${targetImage}`));
111
205
  } catch (error) {
112
206
  throw new Error(`Failed to tag image: ${error.message}`);
113
207
  }
@@ -120,9 +214,9 @@ async function tagImage(sourceImage, targetImage) {
120
214
  */
121
215
  async function pushImage(imageWithTag) {
122
216
  try {
123
- console.log(chalk.blue(`Pushing ${imageWithTag}...`));
217
+ logger.log(chalk.blue(`Pushing ${imageWithTag}...`));
124
218
  await execAsync(`docker push ${imageWithTag}`);
125
- console.log(chalk.green(`✓ Pushed: ${imageWithTag}`));
219
+ logger.log(chalk.green(`✓ Pushed: ${imageWithTag}`));
126
220
  } catch (error) {
127
221
  throw new Error(`Failed to push image: ${error.message}`);
128
222
  }
@@ -134,6 +228,7 @@ module.exports = {
134
228
  validateRegistryURL,
135
229
  checkACRAuthentication,
136
230
  authenticateACR,
231
+ authenticateExternalRegistry,
137
232
  checkLocalImageExists,
138
233
  tagImage,
139
234
  pushImage
@@ -90,7 +90,7 @@
90
90
  "name": {
91
91
  "type": "string",
92
92
  "description": "Database name",
93
- "pattern": "^[a-z0-9_]+$"
93
+ "pattern": "^[a-z0-9-_]+$"
94
94
  }
95
95
  },
96
96
  "additionalProperties": false
@@ -506,7 +506,7 @@
506
506
  },
507
507
  "repositoryUrl": {
508
508
  "type": "string",
509
- "description": "Full repository URL for deployment validation (same as OAuth callback)",
509
+ "description": "Full repository URL for pipeline validation (same as OAuth callback)",
510
510
  "pattern": "^(https://github.com/[^/]+/[^/]+|https://gitlab.com/[^/]+/[^/]+|https://dev.azure.com/[^/]+/[^/]+/[^/]+)$"
511
511
  }
512
512
  },
@@ -586,6 +586,12 @@
586
586
  "minimum": 1000,
587
587
  "maximum": 65535
588
588
  },
589
+ "containerPort": {
590
+ "type": "integer",
591
+ "description": "Container internal port (defaults to port if not specified)",
592
+ "minimum": 1,
593
+ "maximum": 65535
594
+ },
589
595
  "language": {
590
596
  "type": "string",
591
597
  "description": "Runtime language for template selection",
@@ -603,6 +609,28 @@
603
609
  }
604
610
  },
605
611
  "additionalProperties": false
612
+ },
613
+ "deployment": {
614
+ "type": "object",
615
+ "description": "Deployment configuration for pipeline API",
616
+ "properties": {
617
+ "controllerUrl": {
618
+ "type": "string",
619
+ "description": "Controller API URL for deployment",
620
+ "pattern": "^https://.*$"
621
+ },
622
+ "clientId": {
623
+ "type": "string",
624
+ "description": "Pipeline ClientId for automated deployment",
625
+ "pattern": "^[a-z0-9-]+$"
626
+ },
627
+ "clientSecret": {
628
+ "type": "string",
629
+ "description": "Pipeline ClientSecret (use kv:// reference)",
630
+ "pattern": "^(kv://.*|.+)$"
631
+ }
632
+ },
633
+ "additionalProperties": false
606
634
  }
607
635
  },
608
636
  "additionalProperties": false,
@@ -7,9 +7,17 @@ environments:
7
7
  REDIS_HOST: redis
8
8
  MISO_HOST: miso-controller
9
9
  KEYCLOAK_HOST: keycloak
10
-
10
+ MORI_HOST: mori-controller
11
+ OPENWEBUI_HOST: openwebui
12
+ FLOWISE_HOST: flowise
13
+ DATAPLANE_HOST: dataplane
14
+
11
15
  local:
12
16
  DB_HOST: localhost
13
17
  REDIS_HOST: localhost
14
18
  MISO_HOST: localhost
15
19
  KEYCLOAK_HOST: localhost
20
+ MORI_HOST: localhost
21
+ OPENWEBUI_HOST: localhost
22
+ FLOWISE_HOST: localhost
23
+ DATAPLANE_HOST: localhost