@aifabrix/builder 2.33.1 → 2.33.4

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 (42) hide show
  1. package/README.md +13 -0
  2. package/lib/app/deploy-config.js +161 -0
  3. package/lib/app/deploy.js +28 -153
  4. package/lib/app/register.js +6 -5
  5. package/lib/app/run-helpers.js +23 -17
  6. package/lib/cli.js +31 -1
  7. package/lib/commands/logout.js +3 -4
  8. package/lib/commands/up-common.js +72 -0
  9. package/lib/commands/up-dataplane.js +109 -0
  10. package/lib/commands/up-miso.js +134 -0
  11. package/lib/core/config.js +32 -9
  12. package/lib/core/secrets-docker-env.js +88 -0
  13. package/lib/core/secrets.js +142 -115
  14. package/lib/infrastructure/helpers.js +82 -1
  15. package/lib/infrastructure/index.js +2 -0
  16. package/lib/schema/env-config.yaml +7 -0
  17. package/lib/utils/compose-generator.js +13 -13
  18. package/lib/utils/config-paths.js +13 -0
  19. package/lib/utils/device-code.js +2 -2
  20. package/lib/utils/env-endpoints.js +2 -5
  21. package/lib/utils/env-map.js +18 -14
  22. package/lib/utils/parse-image-ref.js +27 -0
  23. package/lib/utils/paths.js +28 -4
  24. package/lib/utils/secrets-generator.js +34 -12
  25. package/lib/utils/secrets-helpers.js +1 -2
  26. package/lib/utils/token-manager-refresh.js +5 -0
  27. package/package.json +1 -1
  28. package/templates/applications/dataplane/Dockerfile +16 -0
  29. package/templates/applications/dataplane/README.md +205 -0
  30. package/templates/applications/dataplane/env.template +143 -0
  31. package/templates/applications/dataplane/rbac.yaml +283 -0
  32. package/templates/applications/dataplane/variables.yaml +143 -0
  33. package/templates/applications/keycloak/Dockerfile +1 -1
  34. package/templates/applications/keycloak/README.md +193 -0
  35. package/templates/applications/keycloak/variables.yaml +5 -6
  36. package/templates/applications/miso-controller/Dockerfile +8 -8
  37. package/templates/applications/miso-controller/README.md +369 -0
  38. package/templates/applications/miso-controller/env.template +114 -6
  39. package/templates/applications/miso-controller/rbac.yaml +74 -0
  40. package/templates/applications/miso-controller/variables.yaml +93 -5
  41. package/templates/infra/compose.yaml.hbs +2 -1
  42. package/templates/applications/miso-controller/test.yaml +0 -1
package/README.md CHANGED
@@ -36,6 +36,13 @@ aifabrix down myapp
36
36
 
37
37
  Want authentication or deployment controller?
38
38
 
39
+ **Quick install from images (no build):**
40
+ ```bash
41
+ aifabrix up # Start Postgres + Redis first
42
+ aifabrix up-miso # Install Keycloak + Miso Controller from images (auto-generated secrets for testing)
43
+ ```
44
+
45
+ **Or create and build from templates:**
39
46
  ```bash
40
47
  # Keycloak for authentication
41
48
  aifabrix create keycloak --port 8082 --database --template keycloak
@@ -48,6 +55,12 @@ aifabrix build miso-controller
48
55
  aifabrix run miso-controller
49
56
  ```
50
57
 
58
+ **Dataplane in dev (after login):**
59
+ ```bash
60
+ aifabrix login --environment dev
61
+ aifabrix up-dataplane # Register or rotate, run, and deploy dataplane in dev
62
+ ```
63
+
51
64
  → [Infrastructure Guide](docs/infrastructure.md)
52
65
 
53
66
  ## Documentation
@@ -0,0 +1,161 @@
1
+ /**
2
+ * Deployment configuration loading and validation for deploy flow.
3
+ * Extracted from deploy.js to keep file size within limits.
4
+ *
5
+ * @fileoverview Deployment config for AI Fabrix Builder
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ const fs = require('fs').promises;
11
+ const path = require('path');
12
+ const yaml = require('js-yaml');
13
+ const config = require('../core/config');
14
+ const { getDeploymentAuth } = require('../utils/token-manager');
15
+ const { detectAppType } = require('../utils/paths');
16
+ const { resolveControllerUrl } = require('../utils/controller-url');
17
+
18
+ /**
19
+ * Validates that the application directory exists
20
+ * @async
21
+ * @param {string} builderPath - Path to builder directory
22
+ * @param {string} appName - Application name
23
+ * @throws {Error} If directory doesn't exist
24
+ */
25
+ async function validateAppDirectory(builderPath, appName) {
26
+ try {
27
+ await fs.access(builderPath);
28
+ } catch (error) {
29
+ if (error.code === 'ENOENT') {
30
+ throw new Error(`Application '${appName}' not found in builder/. Run 'aifabrix create ${appName}' first`);
31
+ }
32
+ throw error;
33
+ }
34
+ }
35
+
36
+ /**
37
+ * Loads variables.yaml file
38
+ * @async
39
+ * @param {string} variablesPath - Path to variables.yaml
40
+ * @returns {Promise<Object>} Variables object
41
+ * @throws {Error} If file cannot be loaded
42
+ */
43
+ async function loadVariablesFile(variablesPath) {
44
+ try {
45
+ const variablesContent = await fs.readFile(variablesPath, 'utf8');
46
+ return yaml.load(variablesContent);
47
+ } catch (error) {
48
+ throw new Error(`Failed to load configuration from variables.yaml: ${error.message}`);
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Extracts deployment configuration from config.yaml
54
+ * Resolves controller URL using fallback chain: config.controller → logged-in user → developer ID default
55
+ * Resolves environment using fallback chain: config.environment → default 'dev'
56
+ * @async
57
+ * @param {Object} options - CLI options (for poll settings only)
58
+ * @param {Object} _variables - Variables from variables.yaml (unused, kept for compatibility)
59
+ * @returns {Promise<Object>} Extracted configuration with resolved controller URL
60
+ */
61
+ async function extractDeploymentConfig(options, _variables) {
62
+ const { resolveEnvironment } = require('../core/config');
63
+
64
+ const controllerUrl = await resolveControllerUrl();
65
+ const envKey = await resolveEnvironment();
66
+
67
+ return {
68
+ controllerUrl,
69
+ envKey,
70
+ poll: options.poll !== false,
71
+ pollInterval: options.pollInterval || 5000,
72
+ pollMaxAttempts: options.pollMaxAttempts || 60
73
+ };
74
+ }
75
+
76
+ /**
77
+ * Validates required deployment configuration
78
+ * @param {Object} deploymentConfig - Deployment configuration
79
+ * @throws {Error} If configuration is invalid
80
+ */
81
+ function validateDeploymentConfig(deploymentConfig) {
82
+ if (!deploymentConfig.controllerUrl) {
83
+ throw new Error('Controller URL is required. Run "aifabrix login" to set the controller URL in config.yaml');
84
+ }
85
+ if (!deploymentConfig.auth) {
86
+ throw new Error('Authentication is required. Run "aifabrix login" first or ensure credentials are in secrets.local.yaml');
87
+ }
88
+ }
89
+
90
+ /**
91
+ * Configure deployment environment settings from config.yaml
92
+ * @async
93
+ * @param {Object} _options - CLI options (unused, kept for compatibility)
94
+ * @param {Object} deploymentConfig - Deployment configuration to update
95
+ * @returns {Promise<void>}
96
+ */
97
+ async function configureDeploymentEnvironment(_options, deploymentConfig) {
98
+ const currentEnvironment = await config.getCurrentEnvironment();
99
+ deploymentConfig.envKey = deploymentConfig.envKey || currentEnvironment;
100
+ }
101
+
102
+ /**
103
+ * Refresh deployment token and configure authentication
104
+ * @async
105
+ * @param {string} appName - Application name
106
+ * @param {Object} deploymentConfig - Deployment configuration to update
107
+ * @returns {Promise<void>}
108
+ * @throws {Error} If authentication fails
109
+ */
110
+ async function refreshDeploymentToken(appName, deploymentConfig) {
111
+ if (!deploymentConfig.controllerUrl) {
112
+ throw new Error('Controller URL is required. Run "aifabrix login" to set the controller URL in config.yaml');
113
+ }
114
+
115
+ try {
116
+ const authConfig = await getDeploymentAuth(
117
+ deploymentConfig.controllerUrl,
118
+ deploymentConfig.envKey,
119
+ appName
120
+ );
121
+ if (!authConfig || !authConfig.controller) {
122
+ throw new Error('Invalid authentication configuration: missing controller URL');
123
+ }
124
+ if (!authConfig.token) {
125
+ throw new Error('Authentication is required');
126
+ }
127
+ deploymentConfig.auth = authConfig;
128
+ deploymentConfig.controllerUrl = authConfig.controller;
129
+ } catch (error) {
130
+ throw new Error(`Failed to get authentication: ${error.message}`);
131
+ }
132
+ }
133
+
134
+ /**
135
+ * Loads deployment configuration from variables.yaml and gets/refreshes token
136
+ * @async
137
+ * @param {string} appName - Application name
138
+ * @param {Object} options - CLI options
139
+ * @returns {Promise<Object>} Deployment configuration with token
140
+ * @throws {Error} If configuration is invalid
141
+ */
142
+ async function loadDeploymentConfig(appName, options) {
143
+ const { appPath } = await detectAppType(appName);
144
+ await validateAppDirectory(appPath, appName);
145
+
146
+ const variablesPath = path.join(appPath, 'variables.yaml');
147
+ const variables = await loadVariablesFile(variablesPath);
148
+
149
+ const deploymentConfig = await extractDeploymentConfig(options, variables);
150
+
151
+ await configureDeploymentEnvironment(options, deploymentConfig);
152
+ await refreshDeploymentToken(appName, deploymentConfig);
153
+
154
+ validateDeploymentConfig(deploymentConfig);
155
+
156
+ return deploymentConfig;
157
+ }
158
+
159
+ module.exports = {
160
+ loadDeploymentConfig
161
+ };
package/lib/app/deploy.js CHANGED
@@ -15,11 +15,9 @@ const yaml = require('js-yaml');
15
15
  const chalk = require('chalk');
16
16
  const pushUtils = require('../deployment/push');
17
17
  const logger = require('../utils/logger');
18
- const config = require('../core/config');
19
- const { getDeploymentAuth } = require('../utils/token-manager');
20
18
  const { detectAppType } = require('../utils/paths');
21
- const { resolveControllerUrl } = require('../utils/controller-url');
22
19
  const { checkApplicationExists } = require('../utils/app-existence');
20
+ const { loadDeploymentConfig } = require('./deploy-config');
23
21
 
24
22
  /**
25
23
  * Validate application name format
@@ -145,155 +143,6 @@ async function pushApp(appName, options = {}) {
145
143
  }
146
144
  }
147
145
 
148
- /**
149
- * Validates that the application directory exists
150
- * @async
151
- * @param {string} builderPath - Path to builder directory
152
- * @param {string} appName - Application name
153
- * @throws {Error} If directory doesn't exist
154
- */
155
- async function validateAppDirectory(builderPath, appName) {
156
- try {
157
- await fs.access(builderPath);
158
- } catch (error) {
159
- if (error.code === 'ENOENT') {
160
- throw new Error(`Application '${appName}' not found in builder/. Run 'aifabrix create ${appName}' first`);
161
- }
162
- throw error;
163
- }
164
- }
165
-
166
- /**
167
- * Loads variables.yaml file
168
- * @async
169
- * @param {string} variablesPath - Path to variables.yaml
170
- * @returns {Promise<Object>} Variables object
171
- * @throws {Error} If file cannot be loaded
172
- */
173
- async function loadVariablesFile(variablesPath) {
174
- try {
175
- const variablesContent = await fs.readFile(variablesPath, 'utf8');
176
- return yaml.load(variablesContent);
177
- } catch (error) {
178
- throw new Error(`Failed to load configuration from variables.yaml: ${error.message}`);
179
- }
180
- }
181
-
182
- /**
183
- * Extracts deployment configuration from config.yaml
184
- * Resolves controller URL using fallback chain: config.controller → logged-in user → developer ID default
185
- * Resolves environment using fallback chain: config.environment → default 'dev'
186
- * @async
187
- * @param {Object} options - CLI options (for poll settings only)
188
- * @param {Object} _variables - Variables from variables.yaml (unused, kept for compatibility)
189
- * @returns {Promise<Object>} Extracted configuration with resolved controller URL
190
- */
191
- async function extractDeploymentConfig(options, _variables) {
192
- const { resolveEnvironment } = require('../core/config');
193
-
194
- // Resolve controller URL from config.yaml (no flags, no options)
195
- const controllerUrl = await resolveControllerUrl();
196
-
197
- // Resolve environment from config.yaml (no flags, no options)
198
- const envKey = await resolveEnvironment();
199
-
200
- return {
201
- controllerUrl,
202
- envKey,
203
- poll: options.poll !== false,
204
- pollInterval: options.pollInterval || 5000,
205
- pollMaxAttempts: options.pollMaxAttempts || 60
206
- };
207
- }
208
-
209
- /**
210
- * Validates required deployment configuration
211
- * @param {Object} deploymentConfig - Deployment configuration
212
- * @throws {Error} If configuration is invalid
213
- */
214
- function validateDeploymentConfig(deploymentConfig) {
215
- if (!deploymentConfig.controllerUrl) {
216
- throw new Error('Controller URL is required. Run "aifabrix login" to set the controller URL in config.yaml');
217
- }
218
- if (!deploymentConfig.auth) {
219
- throw new Error('Authentication is required. Run "aifabrix login" first or ensure credentials are in secrets.local.yaml');
220
- }
221
- }
222
-
223
- /**
224
- * Configure deployment environment settings from config.yaml
225
- * @async
226
- * @param {Object} _options - CLI options (unused, kept for compatibility)
227
- * @param {Object} deploymentConfig - Deployment configuration to update
228
- * @returns {Promise<void>}
229
- */
230
- async function configureDeploymentEnvironment(_options, deploymentConfig) {
231
- // Get current environment from root-level config (already resolved in extractDeploymentConfig)
232
- // This function is kept for compatibility but no longer updates environment from options
233
- const currentEnvironment = await config.getCurrentEnvironment();
234
- deploymentConfig.envKey = deploymentConfig.envKey || currentEnvironment;
235
- }
236
-
237
- /**
238
- * Refresh deployment token and configure authentication
239
- * @async
240
- * @param {string} appName - Application name
241
- * @param {Object} deploymentConfig - Deployment configuration to update
242
- * @returns {Promise<void>}
243
- * @throws {Error} If authentication fails
244
- */
245
- async function refreshDeploymentToken(appName, deploymentConfig) {
246
- // Get controller URL (should already be resolved by extractDeploymentConfig)
247
- if (!deploymentConfig.controllerUrl) {
248
- throw new Error('Controller URL is required. Run "aifabrix login" to set the controller URL in config.yaml');
249
- }
250
-
251
- // Get deployment authentication (device token → client token → credentials)
252
- try {
253
- const authConfig = await getDeploymentAuth(
254
- deploymentConfig.controllerUrl,
255
- deploymentConfig.envKey,
256
- appName
257
- );
258
- if (!authConfig || !authConfig.controller) {
259
- throw new Error('Invalid authentication configuration: missing controller URL');
260
- }
261
- if (!authConfig.token) {
262
- throw new Error('Authentication is required');
263
- }
264
- deploymentConfig.auth = authConfig;
265
- deploymentConfig.controllerUrl = authConfig.controller;
266
- } catch (error) {
267
- throw new Error(`Failed to get authentication: ${error.message}`);
268
- }
269
- }
270
-
271
- /**
272
- * Loads deployment configuration from variables.yaml and gets/refreshes token
273
- * @async
274
- * @param {string} appName - Application name
275
- * @param {Object} options - CLI options
276
- * @returns {Promise<Object>} Deployment configuration with token
277
- * @throws {Error} If configuration is invalid
278
- */
279
- async function loadDeploymentConfig(appName, options) {
280
- // Detect app type and get correct path (integration or builder)
281
- const { appPath } = await detectAppType(appName);
282
- await validateAppDirectory(appPath, appName);
283
-
284
- const variablesPath = path.join(appPath, 'variables.yaml');
285
- const variables = await loadVariablesFile(variablesPath);
286
-
287
- const deploymentConfig = await extractDeploymentConfig(options, variables);
288
-
289
- await configureDeploymentEnvironment(options, deploymentConfig);
290
- await refreshDeploymentToken(appName, deploymentConfig);
291
-
292
- validateDeploymentConfig(deploymentConfig);
293
-
294
- return deploymentConfig;
295
- }
296
-
297
146
  /**
298
147
  * Generates and validates deployment manifest
299
148
  * @async
@@ -406,6 +255,22 @@ async function handleDeploymentError(error, appName, controllerUrl, usedExternal
406
255
  await deployer.handleDeploymentErrors(error, appName, url, alreadyLogged);
407
256
  }
408
257
 
258
+ /**
259
+ * Validates that the deployment image reference is pullable (includes a registry).
260
+ * A local ref (name:tag) causes "docker: not found" on the controller host.
261
+ * @param {string} imageRef - Image reference from manifest
262
+ * @param {string} appName - Application name (for error hint)
263
+ * @throws {Error} If image is missing or has no registry
264
+ */
265
+ function validateImageIsPullable(imageRef, appName) {
266
+ if (!imageRef || !imageRef.includes('/')) {
267
+ const hint = `Set image.registry and image.tag in builder/${appName}/variables.yaml, or pass a full image ref (e.g. --image <registry>/${appName}:<tag>) when deploying`;
268
+ throw new Error(
269
+ `Deployed image must be pullable (include a registry). Current image: "${imageRef || 'none'}". ${hint}`
270
+ );
271
+ }
272
+ }
273
+
409
274
  /**
410
275
  * Execute standard application deployment flow
411
276
  * @async
@@ -422,6 +287,15 @@ async function executeStandardDeployment(appName, options) {
422
287
  const appExists = await checkApplicationExists(appName, controllerUrl, config.envKey, config.auth);
423
288
 
424
289
  const { manifest, manifestPath } = await generateAndValidateManifest(appName);
290
+ if (options.imageOverride || options.image) {
291
+ manifest.image = options.imageOverride || options.image;
292
+ }
293
+ if (options.registryMode) {
294
+ manifest.registryMode = options.registryMode;
295
+ }
296
+
297
+ validateImageIsPullable(manifest.image, appName);
298
+
425
299
  displayDeploymentInfo(manifest, manifestPath);
426
300
 
427
301
  try {
@@ -489,6 +363,7 @@ async function deployApp(appName, options = {}) {
489
363
  module.exports = {
490
364
  pushApp,
491
365
  deployApp,
492
- validateAppName
366
+ validateAppName,
367
+ validateImageIsPullable
493
368
  };
494
369
 
@@ -48,17 +48,18 @@ function buildRegistrationData(appConfig, options) {
48
48
  registrationData.externalIntegration = appConfig.externalIntegration;
49
49
  }
50
50
  } else {
51
- // For non-external types: include registryMode, port, image
52
- registrationData.registryMode = appConfig.registryMode;
51
+ // For non-external types: include registryMode, port, image (options override when provided)
52
+ registrationData.registryMode = options.registryMode ?? appConfig.registryMode;
53
53
 
54
54
  // Port is required for non-external types
55
55
  if (appConfig.port) {
56
56
  registrationData.port = appConfig.port;
57
57
  }
58
58
 
59
- // Image is required for non-external types
60
- if (appConfig.image) {
61
- registrationData.image = appConfig.image;
59
+ // Image is required for non-external types (options.imageOverride overrides appConfig.image)
60
+ const imageValue = options.imageOverride ?? options.image ?? appConfig.image;
61
+ if (imageValue) {
62
+ registrationData.image = imageValue;
62
63
  }
63
64
 
64
65
  // URL: always set when we have port so controller DB has it. Precedence: --url, variables (app.url, deployment.dataplaneUrl, deployment.appUrl), else http://localhost:{localPort|port}
@@ -26,6 +26,7 @@ const { waitForHealthCheck } = require('../utils/health-check');
26
26
  const composeGenerator = require('../utils/compose-generator');
27
27
  const dockerUtils = require('../utils/docker');
28
28
  const containerHelpers = require('../utils/app-run-containers');
29
+ const pathsUtil = require('../utils/paths');
29
30
 
30
31
  const execAsync = promisify(exec);
31
32
 
@@ -41,11 +42,12 @@ const stopAndRemoveContainer = containerHelpers.stopAndRemoveContainer;
41
42
  */
42
43
  function checkBuilderDirectory(appName) {
43
44
  const currentDir = process.cwd();
44
- const normalizedPath = currentDir.replace(/\\/g, '/');
45
- const expectedBuilderPath = `builder/${appName}`;
45
+ const expectedAppDir = pathsUtil.getBuilderPath(appName);
46
+ const normalizedCurrent = path.resolve(currentDir).replace(/\\/g, '/');
47
+ const normalizedExpected = path.resolve(expectedAppDir).replace(/\\/g, '/');
46
48
 
47
- if (normalizedPath.endsWith(expectedBuilderPath)) {
48
- const projectRoot = path.resolve(currentDir, '../..');
49
+ if (normalizedCurrent === normalizedExpected) {
50
+ const projectRoot = path.resolve(expectedAppDir, '../..');
49
51
  throw new Error(
50
52
  'You\'re running from inside the builder directory.\n' +
51
53
  `Current directory: ${currentDir}\n` +
@@ -58,19 +60,20 @@ function checkBuilderDirectory(appName) {
58
60
 
59
61
  /**
60
62
  * Load and validate config file exists
63
+ * Uses paths.getBuilderPath so AIFABRIX_BUILDER_DIR (e.g. from up-miso) is respected.
61
64
  * @param {string} appName - Application name
62
65
  * @returns {Object} Application configuration
63
66
  * @throws {Error} If config file not found
64
67
  */
65
68
  function loadAppConfig(appName) {
66
69
  const currentDir = process.cwd();
67
- const configPath = path.join(currentDir, 'builder', appName, 'variables.yaml');
70
+ const builderPath = pathsUtil.getBuilderPath(appName);
71
+ const configPath = path.join(builderPath, 'variables.yaml');
68
72
  if (!fsSync.existsSync(configPath)) {
69
- const expectedDir = path.join(currentDir, 'builder', appName);
70
73
  throw new Error(
71
74
  `Application configuration not found: ${configPath}\n` +
72
75
  `Current directory: ${currentDir}\n` +
73
- `Expected location: ${expectedDir}\n` +
76
+ `Expected location: ${builderPath}\n` +
74
77
  'Make sure you\'re running from the project root (where \'builder\' directory exists)\n' +
75
78
  `Run 'aifabrix create ${appName}' first if configuration doesn't exist`
76
79
  );
@@ -194,14 +197,15 @@ async function ensureDevDirectory(appName, developerId) {
194
197
  * @async
195
198
  * @param {string} appName - Application name
196
199
  * @param {string} builderEnvPath - Path to builder .env file
200
+ * @param {boolean} [skipOutputPath=false] - When true, skip copying to envOutputPath (e.g. up-miso/up-dataplane, no local code)
197
201
  */
198
- async function ensureEnvFile(appName, builderEnvPath) {
202
+ async function ensureEnvFile(appName, builderEnvPath, skipOutputPath = false) {
199
203
  if (!fsSync.existsSync(builderEnvPath)) {
200
204
  logger.log(chalk.yellow('Generating .env file from template...'));
201
- await secrets.generateEnvFile(appName, null, 'docker');
205
+ await secrets.generateEnvFile(appName, null, 'docker', false, skipOutputPath);
202
206
  } else {
203
207
  logger.log(chalk.blue('Updating .env file for Docker environment...'));
204
- await secrets.generateEnvFile(appName, null, 'docker');
208
+ await secrets.generateEnvFile(appName, null, 'docker', false, skipOutputPath);
205
209
  }
206
210
  }
207
211
 
@@ -224,9 +228,10 @@ async function copyEnvToDev(builderEnvPath, devEnvPath) {
224
228
  * @param {string} variablesPath - Path to variables.yaml
225
229
  * @param {string} builderEnvPath - Path to builder .env file
226
230
  * @param {string} devEnvPath - Path to dev .env file
231
+ * @param {boolean} [skipOutputPath=false] - When true, skip (e.g. up-miso/up-dataplane, no local code)
227
232
  */
228
- async function handleEnvOutputPath(appName, variablesPath, builderEnvPath, devEnvPath) {
229
- if (!fsSync.existsSync(variablesPath)) {
233
+ async function handleEnvOutputPath(appName, variablesPath, builderEnvPath, devEnvPath, skipOutputPath = false) {
234
+ if (skipOutputPath || !fsSync.existsSync(variablesPath)) {
230
235
  return;
231
236
  }
232
237
 
@@ -284,18 +289,19 @@ async function generateComposeFile(appName, appConfig, composeOptions, devDir) {
284
289
  async function prepareEnvironment(appName, appConfig, options) {
285
290
  const developerId = await config.getDeveloperId();
286
291
  const devDir = await ensureDevDirectory(appName, developerId);
292
+ const skipEnvOutputPath = options.skipEnvOutputPath === true;
287
293
 
288
- // Generate/update .env file
289
- const builderEnvPath = path.join(process.cwd(), 'builder', appName, '.env');
290
- await ensureEnvFile(appName, builderEnvPath);
294
+ // Generate/update .env file (respect AIFABRIX_BUILDER_DIR when set by up-miso/up-dataplane)
295
+ const builderEnvPath = path.join(pathsUtil.getBuilderPath(appName), '.env');
296
+ await ensureEnvFile(appName, builderEnvPath, skipEnvOutputPath);
291
297
 
292
298
  // Copy .env to dev directory
293
299
  const devEnvPath = path.join(devDir, '.env');
294
300
  await copyEnvToDev(builderEnvPath, devEnvPath);
295
301
 
296
- // Handle envOutputPath if configured
302
+ // Handle envOutputPath if configured (skipped when skipEnvOutputPath e.g. up-miso/up-dataplane)
297
303
  const variablesPath = path.join(devDir, 'variables.yaml');
298
- await handleEnvOutputPath(appName, variablesPath, builderEnvPath, devEnvPath);
304
+ await handleEnvOutputPath(appName, variablesPath, builderEnvPath, devEnvPath, skipEnvOutputPath);
299
305
 
300
306
  // Generate Docker Compose
301
307
  const composeOptions = { ...options };
package/lib/cli.js CHANGED
@@ -29,6 +29,8 @@ const { handleSecretsSet } = require('./commands/secrets-set');
29
29
  const { handleAuthConfig } = require('./commands/auth-config');
30
30
  const { setupAppCommands: setupAppManagementCommands } = require('./commands/app');
31
31
  const { setupDatasourceCommands } = require('./commands/datasource');
32
+ const { handleUpMiso } = require('./commands/up-miso');
33
+ const { handleUpDataplane } = require('./commands/up-dataplane');
32
34
 
33
35
  /**
34
36
  * Sets up authentication commands
@@ -152,6 +154,34 @@ function setupInfraCommands(program) {
152
154
  }
153
155
  });
154
156
 
157
+ program.command('up-miso')
158
+ .description('Install keycloak and miso-controller from images (no build). Infra must be up. Uses auto-generated secrets for testing.')
159
+ .option('-r, --registry <url>', 'Override registry for both apps (e.g. myacr.azurecr.io)')
160
+ .option('--registry-mode <mode>', 'Override registry mode (acr|external)')
161
+ .option('-i, --image <key>=<value>', 'Override image (e.g. keycloak=myreg/keycloak:v1, miso-controller=myreg/m:v1); can be repeated', (v, prev) => (prev || []).concat([v]))
162
+ .action(async(options) => {
163
+ try {
164
+ await handleUpMiso(options);
165
+ } catch (error) {
166
+ handleCommandError(error, 'up-miso');
167
+ process.exit(1);
168
+ }
169
+ });
170
+
171
+ program.command('up-dataplane')
172
+ .description('Register and deploy dataplane app in dev (requires login, environment must be dev)')
173
+ .option('-r, --registry <url>', 'Override registry for dataplane image')
174
+ .option('--registry-mode <mode>', 'Override registry mode (acr|external)')
175
+ .option('-i, --image <ref>', 'Override dataplane image reference (e.g. myreg/dataplane:latest)')
176
+ .action(async(options) => {
177
+ try {
178
+ await handleUpDataplane(options);
179
+ } catch (error) {
180
+ handleCommandError(error, 'up-dataplane');
181
+ process.exit(1);
182
+ }
183
+ });
184
+
155
185
  program.command('down [app]')
156
186
  .description('Stop and remove local infrastructure services or a specific application')
157
187
  .option('-v, --volumes', 'Remove volumes (deletes all data)')
@@ -397,7 +427,7 @@ function setupAppCommands(program) {
397
427
  .description('Build container image (auto-detects runtime)')
398
428
  .option('-l, --language <lang>', 'Override language detection')
399
429
  .option('-f, --force-template', 'Force rebuild from template')
400
- .option('-t, --tag <tag>', 'Image tag (default: latest)')
430
+ .option('-t, --tag <tag>', 'Image tag (default: latest). Set image.tag in variables.yaml to match for deploy.')
401
431
  .action(async(appName, options) => {
402
432
  try {
403
433
  const imageTag = await app.buildApp(appName, options);
@@ -16,11 +16,10 @@ const {
16
16
  clearClientToken,
17
17
  clearAllClientTokens,
18
18
  clearClientTokensForEnvironment,
19
- normalizeControllerUrl
19
+ normalizeControllerUrl,
20
+ CONFIG_FILE
20
21
  } = require('../core/config');
21
22
  const logger = require('../utils/logger');
22
- const os = require('os');
23
- const path = require('path');
24
23
 
25
24
  /**
26
25
  * Validate environment key format
@@ -144,7 +143,7 @@ async function clearClientTokens(options) {
144
143
  * @throws {Error} If logout fails or options are invalid
145
144
  */
146
145
  async function handleLogout(options) {
147
- const configPath = path.join(os.homedir(), '.aifabrix', 'config.yaml');
146
+ const configPath = CONFIG_FILE;
148
147
  logger.log(chalk.blue('\nšŸ”“ Clearing authentication tokens...\n'));
149
148
 
150
149
  // Validate options
@@ -0,0 +1,72 @@
1
+ /**
2
+ * AI Fabrix Builder - Up Commands Shared Helpers
3
+ *
4
+ * Shared logic for up-miso and up-dataplane (ensure app from template).
5
+ *
6
+ * @fileoverview Shared helpers for up-miso and up-dataplane commands
7
+ * @author AI Fabrix Team
8
+ * @version 2.0.0
9
+ */
10
+
11
+ const path = require('path');
12
+ const fs = require('fs');
13
+ const chalk = require('chalk');
14
+ const logger = require('../utils/logger');
15
+ const pathsUtil = require('../utils/paths');
16
+ const { copyTemplateFiles } = require('../validation/template');
17
+
18
+ /**
19
+ * Copy template to a target path if variables.yaml is missing there.
20
+ * @param {string} appName - Application name
21
+ * @param {string} targetAppPath - Target directory (e.g. builder/keycloak)
22
+ * @returns {Promise<boolean>} True if template was copied, false if already present
23
+ */
24
+ async function ensureTemplateAtPath(appName, targetAppPath) {
25
+ const variablesPath = path.join(targetAppPath, 'variables.yaml');
26
+ if (fs.existsSync(variablesPath)) {
27
+ return false;
28
+ }
29
+ await copyTemplateFiles(appName, targetAppPath);
30
+ return true;
31
+ }
32
+
33
+ /**
34
+ * Ensures builder app directory exists from template if variables.yaml is missing.
35
+ * If builder/<appName>/variables.yaml does not exist, copies from templates/applications/<appName>.
36
+ * Uses AIFABRIX_BUILDER_DIR when set (e.g. by up-miso/up-dataplane from config aifabrix-env-config).
37
+ * When using a custom builder dir, also populates cwd/builder/<appName> so the repo's builder/ is not empty.
38
+ *
39
+ * @async
40
+ * @function ensureAppFromTemplate
41
+ * @param {string} appName - Application name (keycloak, miso-controller, dataplane)
42
+ * @returns {Promise<boolean>} True if template was copied (in either location), false if both already existed
43
+ * @throws {Error} If template copy fails
44
+ *
45
+ * @example
46
+ * await ensureAppFromTemplate('keycloak');
47
+ */
48
+ async function ensureAppFromTemplate(appName) {
49
+ if (!appName || typeof appName !== 'string') {
50
+ throw new Error('Application name is required and must be a string');
51
+ }
52
+
53
+ const appPath = pathsUtil.getBuilderPath(appName);
54
+ const primaryCopied = await ensureTemplateAtPath(appName, appPath);
55
+ if (primaryCopied) {
56
+ logger.log(chalk.blue(`Creating builder/${appName} from template...`));
57
+ logger.log(chalk.green(`āœ“ Copied template for ${appName}`));
58
+ }
59
+
60
+ const cwdBuilderPath = path.join(process.cwd(), 'builder', appName);
61
+ if (path.resolve(cwdBuilderPath) !== path.resolve(appPath)) {
62
+ const cwdCopied = await ensureTemplateAtPath(appName, cwdBuilderPath);
63
+ if (cwdCopied) {
64
+ logger.log(chalk.blue(`Creating builder/${appName} in project (from template)...`));
65
+ logger.log(chalk.green(`āœ“ Copied template for ${appName} into builder/`));
66
+ }
67
+ }
68
+
69
+ return primaryCopied;
70
+ }
71
+
72
+ module.exports = { ensureAppFromTemplate };