@aifabrix/builder 2.38.0 → 2.39.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.
Files changed (48) hide show
  1. package/.cursor/rules/project-rules.mdc +3 -0
  2. package/integration/hubspot/hubspot-deploy.json +0 -3
  3. package/integration/hubspot/hubspot-system.json +0 -3
  4. package/lib/api/applications.api.js +8 -2
  5. package/lib/api/auth.api.js +14 -0
  6. package/lib/api/credentials.api.js +1 -1
  7. package/lib/api/datasources-core.api.js +16 -1
  8. package/lib/api/datasources-extended.api.js +18 -1
  9. package/lib/api/deployments.api.js +6 -1
  10. package/lib/api/environments.api.js +11 -0
  11. package/lib/api/external-systems.api.js +16 -1
  12. package/lib/api/pipeline.api.js +12 -4
  13. package/lib/api/service-users.api.js +41 -0
  14. package/lib/api/types/service-users.types.js +24 -0
  15. package/lib/api/wizard.api.js +19 -0
  16. package/lib/app/deploy.js +86 -21
  17. package/lib/app/rotate-secret.js +3 -1
  18. package/lib/app/run-helpers.js +7 -2
  19. package/lib/app/show-display.js +30 -11
  20. package/lib/app/show.js +34 -8
  21. package/lib/cli/index.js +2 -0
  22. package/lib/cli/setup-app.js +8 -0
  23. package/lib/cli/setup-infra.js +3 -3
  24. package/lib/cli/setup-service-user.js +52 -0
  25. package/lib/commands/app.js +2 -1
  26. package/lib/commands/service-user.js +193 -0
  27. package/lib/commands/up-common.js +74 -5
  28. package/lib/commands/up-dataplane.js +13 -7
  29. package/lib/commands/up-miso.js +17 -24
  30. package/lib/core/templates.js +0 -1
  31. package/lib/external-system/deploy.js +79 -15
  32. package/lib/generator/builders.js +0 -24
  33. package/lib/schema/application-schema.json +0 -12
  34. package/lib/schema/external-system.schema.json +0 -16
  35. package/lib/utils/app-register-config.js +10 -12
  36. package/lib/utils/deployment-errors.js +10 -0
  37. package/lib/utils/environment-checker.js +25 -6
  38. package/lib/utils/variable-transformer.js +6 -14
  39. package/package.json +1 -1
  40. package/templates/applications/dataplane/README.md +23 -7
  41. package/templates/applications/dataplane/env.template +31 -2
  42. package/templates/applications/dataplane/rbac.yaml +1 -1
  43. package/templates/applications/dataplane/variables.yaml +2 -1
  44. package/templates/applications/keycloak/env.template +6 -3
  45. package/templates/applications/keycloak/variables.yaml +1 -0
  46. package/templates/applications/miso-controller/env.template +22 -15
  47. package/templates/applications/miso-controller/rbac.yaml +15 -0
  48. package/templates/applications/miso-controller/variables.yaml +24 -23
package/lib/app/show.js CHANGED
@@ -418,6 +418,7 @@ function buildApplicationFromAppCfg(app, cfg, portalInputConfigurations) {
418
418
  build: formatBuildForDisplay(cfg.build ?? app.build),
419
419
  status: pickAppCfg('status', app, cfg, '—'),
420
420
  url: pickAppCfg('url', app, cfg, '—'),
421
+ internalUrl: pickAppCfg('internalUrl', app, cfg, '—'),
421
422
  roles: cfg.roles ?? app.roles,
422
423
  permissions: cfg.permissions ?? app.permissions,
423
424
  authentication: cfg.authentication ?? app.authentication,
@@ -485,9 +486,10 @@ function buildOnlineSummary(apiApp, controllerUrl, externalSystem) {
485
486
  * Run show in offline mode: generate manifest (same as aifabrix json) and use it; else fall back to variables.yaml.
486
487
  * @param {string} appKey - Application key
487
488
  * @param {boolean} json - Output as JSON
489
+ * @param {boolean} [permissionsOnly] - When true, output only permissions
488
490
  * @throws {Error} If variables.yaml not found or invalid YAML
489
491
  */
490
- async function runOffline(appKey, json) {
492
+ async function runOffline(appKey, json, permissionsOnly) {
491
493
  let summary;
492
494
 
493
495
  try {
@@ -503,6 +505,16 @@ async function runOffline(appKey, json) {
503
505
  }
504
506
 
505
507
  if (json) {
508
+ if (permissionsOnly) {
509
+ const out = {
510
+ source: summary.source,
511
+ path: summary.path,
512
+ appKey: summary.appKey,
513
+ permissions: summary.permissions || []
514
+ };
515
+ logger.log(JSON.stringify(out, null, 2));
516
+ return;
517
+ }
506
518
  const out = {
507
519
  source: summary.source,
508
520
  path: summary.path,
@@ -519,7 +531,7 @@ async function runOffline(appKey, json) {
519
531
  logger.log(JSON.stringify(out, null, 2));
520
532
  return;
521
533
  }
522
- displayShow(summary);
534
+ displayShow(summary, { permissionsOnly: !!permissionsOnly });
523
535
  }
524
536
 
525
537
  async function resolveOnlineAuth(controllerUrl) {
@@ -550,7 +562,17 @@ async function fetchExternalSystemForOnline(controllerUrl, appKey, authConfig) {
550
562
  }
551
563
  }
552
564
 
553
- function outputOnlineJson(summary) {
565
+ function outputOnlineJson(summary, permissionsOnly) {
566
+ if (permissionsOnly) {
567
+ const out = {
568
+ source: summary.source,
569
+ controllerUrl: summary.controllerUrl,
570
+ appKey: summary.appKey,
571
+ permissions: summary.permissions || []
572
+ };
573
+ logger.log(JSON.stringify(out, null, 2));
574
+ return;
575
+ }
554
576
  const out = {
555
577
  source: summary.source,
556
578
  controllerUrl: summary.controllerUrl,
@@ -562,6 +584,7 @@ function outputOnlineJson(summary) {
562
584
  type: summary.application.type,
563
585
  status: summary.application.status,
564
586
  url: summary.application.url,
587
+ internalUrl: summary.application.internalUrl,
565
588
  port: summary.application.port,
566
589
  configuration: summary.application.configuration,
567
590
  roles: summary.application.roles,
@@ -583,9 +606,10 @@ function outputOnlineJson(summary) {
583
606
  * Run show in online mode: getApplication, optionally dataplane for external, display or JSON.
584
607
  * @param {string} appKey - Application key
585
608
  * @param {boolean} json - Output as JSON
609
+ * @param {boolean} [permissionsOnly] - When true, output only permissions
586
610
  * @throws {Error} On auth failure, 404, or API error
587
611
  */
588
- async function runOnline(appKey, json) {
612
+ async function runOnline(appKey, json, permissionsOnly) {
589
613
  const controllerUrl = await resolveControllerUrl();
590
614
  if (!controllerUrl) {
591
615
  throw new Error('Controller URL is required for --online. Run aifabrix login to set the controller URL in config.yaml.');
@@ -599,10 +623,10 @@ async function runOnline(appKey, json) {
599
623
  : null;
600
624
  const summary = buildOnlineSummary(apiApp, authResult.actualControllerUrl, externalSystem);
601
625
  if (json) {
602
- outputOnlineJson(summary);
626
+ outputOnlineJson(summary, permissionsOnly);
603
627
  return;
604
628
  }
605
- displayShow(summary);
629
+ displayShow(summary, { permissionsOnly: !!permissionsOnly });
606
630
  }
607
631
 
608
632
  /**
@@ -612,6 +636,7 @@ async function runOnline(appKey, json) {
612
636
  * @param {Object} options - Options
613
637
  * @param {boolean} [options.online] - Fetch from controller
614
638
  * @param {boolean} [options.json] - Output as JSON
639
+ * @param {boolean} [options.permissions] - When true, output only permissions (app show --permissions)
615
640
  * @throws {Error} If file missing/invalid (offline) or API/auth error (online)
616
641
  */
617
642
  async function showApp(appKey, options = {}) {
@@ -621,11 +646,12 @@ async function showApp(appKey, options = {}) {
621
646
 
622
647
  const online = Boolean(options.online);
623
648
  const json = Boolean(options.json);
649
+ const permissions = Boolean(options.permissions);
624
650
 
625
651
  if (online) {
626
- await runOnline(appKey, json);
652
+ await runOnline(appKey, json, permissions);
627
653
  } else {
628
- await runOffline(appKey, json);
654
+ await runOffline(appKey, json, permissions);
629
655
  }
630
656
  }
631
657
 
package/lib/cli/index.js CHANGED
@@ -21,6 +21,7 @@ const { setupExternalSystemCommands } = require('./setup-external-system');
21
21
  const { setupAppCommands: setupAppManagementCommands } = require('../commands/app');
22
22
  const { setupDatasourceCommands } = require('../commands/datasource');
23
23
  const { setupCredentialDeploymentCommands } = require('./setup-credential-deployment');
24
+ const { setupServiceUserCommands } = require('./setup-service-user');
24
25
 
25
26
  /**
26
27
  * Sets up all CLI commands on the Commander program instance
@@ -35,6 +36,7 @@ function setupCommands(program) {
35
36
  setupDatasourceCommands(program);
36
37
  setupUtilityCommands(program);
37
38
  setupCredentialDeploymentCommands(program);
39
+ setupServiceUserCommands(program);
38
40
  setupExternalSystemCommands(program);
39
41
  setupDevCommands(program);
40
42
  setupSecretsCommands(program);
@@ -229,6 +229,7 @@ See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`)
229
229
 
230
230
  program.command('deploy <app>')
231
231
  .description('Deploy to Azure via Miso Controller')
232
+ .option('--deployment <target>', 'Deployment target: \'local\' (send manifest to controller, then run app locally) or \'cloud\' (deploy via Miso Controller only)', 'cloud')
232
233
  .option('--type <type>', 'Application type: external to deploy from integration/<app> (no app register needed)')
233
234
  .option('--client-id <id>', 'Client ID (overrides config)')
234
235
  .option('--client-secret <secret>', 'Client Secret (overrides config)')
@@ -236,7 +237,14 @@ See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`)
236
237
  .option('--no-poll', 'Do not poll for status')
237
238
  .action(async(appName, options) => {
238
239
  try {
240
+ const target = (options.deployment || 'cloud').toLowerCase();
241
+ if (target !== 'local' && target !== 'cloud') {
242
+ throw new Error('Deployment target must be \'local\' or \'cloud\'');
243
+ }
239
244
  await app.deployApp(appName, options);
245
+ if (target === 'local') {
246
+ await app.runApp(appName, options);
247
+ }
240
248
  } catch (error) {
241
249
  handleCommandError(error, 'deploy');
242
250
  process.exit(1);
@@ -83,10 +83,10 @@ function setupInfraCommands(program) {
83
83
  });
84
84
 
85
85
  program.command('up-miso')
86
- .description('Install keycloak, miso-controller, and dataplane from images (no build). Infra must be up. Uses auto-generated secrets for testing.')
86
+ .description('Install keycloak and miso-controller from images (no build). Infra must be up. For dataplane use up-dataplane. Uses auto-generated secrets for testing.')
87
87
  .option('-r, --registry <url>', 'Override registry for all apps (e.g. myacr.azurecr.io)')
88
88
  .option('--registry-mode <mode>', 'Override registry mode (acr|external)')
89
- .option('-i, --image <key>=<value>', 'Override image (e.g. keycloak=myreg/k:v1, miso-controller=myreg/m:v1, dataplane=myreg/d:v1); can be repeated', (v, prev) => (prev || []).concat([v]))
89
+ .option('-i, --image <key>=<value>', 'Override image (e.g. keycloak=myreg/k:v1, miso-controller=myreg/m:v1); can be repeated', (v, prev) => (prev || []).concat([v]))
90
90
  .action(async(options) => {
91
91
  try {
92
92
  await handleUpMiso(options);
@@ -97,7 +97,7 @@ function setupInfraCommands(program) {
97
97
  });
98
98
 
99
99
  program.command('up-dataplane')
100
- .description('Register and deploy dataplane app in dev (requires login, environment must be dev)')
100
+ .description('Register, deploy, then run dataplane app locally in dev (always local deployment; requires login, environment must be dev)')
101
101
  .option('-r, --registry <url>', 'Override registry for dataplane image')
102
102
  .option('--registry-mode <mode>', 'Override registry mode (acr|external)')
103
103
  .option('-i, --image <ref>', 'Override dataplane image reference (e.g. myreg/dataplane:latest)')
@@ -0,0 +1,52 @@
1
+ /**
2
+ * CLI service-user command setup.
3
+ * Command: service-user create (create service user, get one-time secret).
4
+ *
5
+ * @fileoverview Service user CLI definitions
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ const chalk = require('chalk');
11
+ const logger = require('../utils/logger');
12
+ const { handleCommandError } = require('../utils/cli-utils');
13
+ const { runServiceUserCreate } = require('../commands/service-user');
14
+
15
+ /**
16
+ * Sets up service-user commands
17
+ * @param {Command} program - Commander program instance
18
+ */
19
+ function setupServiceUserCommands(program) {
20
+ const serviceUser = program
21
+ .command('service-user')
22
+ .description('Create service users for integrations (one-time secret on create)');
23
+
24
+ serviceUser
25
+ .command('create')
26
+ .description('Create a service user (username, email, redirectUris, groupIds); receive one-time clientSecret (save it now)')
27
+ .option('--controller <url>', 'Controller URL (default: from config)')
28
+ .option('-u, --username <username>', 'Service user username (required)')
29
+ .option('-e, --email <email>', 'Email address (required)')
30
+ .option('--redirect-uris <uris>', 'Comma-separated redirect URIs for OAuth2 (required, e.g. https://app.example.com/callback)')
31
+ .option('--group-names <names>', 'Comma-separated group names (required, e.g. AI-Fabrix-Developers)')
32
+ .option('-d, --description <description>', 'Optional description')
33
+ .action(async(options) => {
34
+ try {
35
+ const opts = {
36
+ controller: options.controller,
37
+ username: options.username,
38
+ email: options.email,
39
+ redirectUris: options.redirectUris,
40
+ groupNames: options.groupNames,
41
+ description: options.description
42
+ };
43
+ await runServiceUserCreate(opts);
44
+ } catch (error) {
45
+ logger.error(chalk.red(`Error: ${error.message}`));
46
+ handleCommandError(error, 'service-user create');
47
+ process.exit(1);
48
+ }
49
+ });
50
+ }
51
+
52
+ module.exports = { setupServiceUserCommands };
@@ -74,9 +74,10 @@ function setupAppCommands(program) {
74
74
  .command('show <appKey>')
75
75
  .description('Show application from controller (online). Same as aifabrix show <appKey> --online')
76
76
  .option('--json', 'Output as JSON')
77
+ .option('--permissions', 'Show only list of permissions')
77
78
  .action(async(appKey, options) => {
78
79
  try {
79
- await showApp(appKey, { online: true, json: !!options.json });
80
+ await showApp(appKey, { online: true, json: !!options.json, permissions: !!options.permissions });
80
81
  } catch (error) {
81
82
  logger.error(chalk.red(`Error: ${error.message}`));
82
83
  process.exit(1);
@@ -0,0 +1,193 @@
1
+ /**
2
+ * Service user create command – create service user and get one-time secret
3
+ * POST /api/v1/service-users. Used by `aifabrix service-user create`.
4
+ *
5
+ * @fileoverview Service user create command implementation
6
+ * @author AI Fabrix Team
7
+ * @version 2.0.0
8
+ */
9
+
10
+ const chalk = require('chalk');
11
+ const logger = require('../utils/logger');
12
+ const { resolveControllerUrl } = require('../utils/controller-url');
13
+ const { getOrRefreshDeviceToken } = require('../utils/token-manager');
14
+ const { normalizeControllerUrl } = require('../core/config');
15
+ const { createServiceUser } = require('../api/service-users.api');
16
+
17
+ const ONE_TIME_WARNING =
18
+ 'Save this secret now; it will not be shown again.';
19
+
20
+ /**
21
+ * Get auth token for service-user (device token from config)
22
+ * @async
23
+ * @param {string} controllerUrl - Controller base URL
24
+ * @returns {Promise<{token: string, controllerUrl: string}|null>}
25
+ */
26
+ async function getServiceUserAuth(controllerUrl) {
27
+ const normalizedUrl = normalizeControllerUrl(controllerUrl);
28
+ const deviceToken = await getOrRefreshDeviceToken(normalizedUrl);
29
+ if (deviceToken && deviceToken.token) {
30
+ return {
31
+ token: deviceToken.token,
32
+ controllerUrl: deviceToken.controller || normalizedUrl
33
+ };
34
+ }
35
+ return null;
36
+ }
37
+
38
+ /**
39
+ * Extract clientId and clientSecret from API response (response may be wrapped in data)
40
+ * @param {Object} response - API response
41
+ * @returns {{ clientId: string, clientSecret: string }}
42
+ */
43
+ function extractCreateResponse(response) {
44
+ const data = response?.data ?? response;
45
+ return {
46
+ clientId: data?.clientId ?? '',
47
+ clientSecret: data?.clientSecret ?? ''
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Log error for failed create response and exit
53
+ * @param {Object} response - API response with success: false
54
+ */
55
+ function handleCreateError(response) {
56
+ const status = response.status;
57
+ const msg = response.formattedError || response.error || 'Request failed';
58
+ if (status === 400) {
59
+ logger.error(chalk.red(`❌ Validation error: ${msg}`));
60
+ } else if (status === 401) {
61
+ logger.error(chalk.red('❌ Unauthorized. Run "aifabrix login" and try again.'));
62
+ } else if (status === 403) {
63
+ logger.error(chalk.red('❌ Missing permission: service-user:create'));
64
+ logger.error(chalk.gray('Your account needs the service-user:create permission on the controller.'));
65
+ } else {
66
+ logger.error(chalk.red(`❌ Failed to create service user: ${msg}`));
67
+ }
68
+ process.exit(1);
69
+ }
70
+
71
+ /**
72
+ * Display success output with clientId, clientSecret and one-time warning
73
+ * @param {string} clientId - Service user client ID
74
+ * @param {string} clientSecret - One-time client secret
75
+ */
76
+ function displayCreateSuccess(clientId, clientSecret) {
77
+ logger.log(chalk.bold('\n✓ Service user created\n'));
78
+ logger.log(chalk.cyan(' clientId: ') + clientId);
79
+ logger.log(chalk.cyan(' clientSecret: ') + clientSecret);
80
+ logger.log('');
81
+ logger.log(chalk.yellow('⚠ ' + ONE_TIME_WARNING));
82
+ logger.log('');
83
+ }
84
+
85
+ /**
86
+ * Parse comma-separated string into non-empty trimmed array
87
+ * @param {string} [val] - Comma-separated value
88
+ * @returns {string[]}
89
+ */
90
+ function parseList(val) {
91
+ if (val === undefined || val === null || String(val).trim() === '') {
92
+ return [];
93
+ }
94
+ return String(val)
95
+ .split(',')
96
+ .map(s => s.trim())
97
+ .filter(Boolean);
98
+ }
99
+
100
+ /**
101
+ * Validate username, email, redirectUris, groupIds; exit on failure
102
+ * @param {Object} options - CLI options
103
+ * @returns {{ username: string, email: string, redirectUris: string[], groupNames: string[], description?: string }}
104
+ */
105
+ function validateServiceUserOptions(options) {
106
+ const username = options.username?.trim();
107
+ const email = options.email?.trim();
108
+ const redirectUris = parseList(options.redirectUris);
109
+ const groupNames = parseList(options.groupNames);
110
+ if (!username) {
111
+ logger.error(chalk.red('❌ Username is required. Use --username <username>.'));
112
+ process.exit(1);
113
+ }
114
+ if (!email) {
115
+ logger.error(chalk.red('❌ Email is required. Use --email <email>.'));
116
+ process.exit(1);
117
+ }
118
+ if (redirectUris.length === 0) {
119
+ logger.error(chalk.red('❌ At least one redirect URI is required. Use --redirect-uris <uri1,uri2,...>.'));
120
+ process.exit(1);
121
+ }
122
+ if (groupNames.length === 0) {
123
+ logger.error(chalk.red('❌ At least one group name is required. Use --group-names <name1,name2,...>.'));
124
+ process.exit(1);
125
+ }
126
+ return {
127
+ username,
128
+ email,
129
+ redirectUris,
130
+ groupNames,
131
+ description: options.description?.trim() || undefined
132
+ };
133
+ }
134
+
135
+ /**
136
+ * Resolve controller URL and auth; exit on failure
137
+ * @async
138
+ * @param {Object} options - CLI options
139
+ * @returns {Promise<{ username: string, email: string, redirectUris: string[], groupNames: string[], description?: string, controllerUrl: string, authConfig: Object }>}
140
+ */
141
+ async function resolveOptionsAndAuth(options) {
142
+ const validated = validateServiceUserOptions(options);
143
+ const controllerUrl = options.controller || (await resolveControllerUrl());
144
+ if (!controllerUrl) {
145
+ logger.error(chalk.red('❌ Controller URL is required. Run "aifabrix login" first.'));
146
+ process.exit(1);
147
+ }
148
+ const authResult = await getServiceUserAuth(controllerUrl);
149
+ if (!authResult || !authResult.token) {
150
+ logger.error(chalk.red(`❌ No authentication token for controller: ${controllerUrl}`));
151
+ logger.error(chalk.gray('Run: aifabrix login'));
152
+ process.exit(1);
153
+ }
154
+ return {
155
+ ...validated,
156
+ controllerUrl: authResult.controllerUrl,
157
+ authConfig: { type: 'bearer', token: authResult.token }
158
+ };
159
+ }
160
+
161
+ /**
162
+ * Run service-user create: call POST /api/v1/service-users and display one-time secret with warning
163
+ * @async
164
+ * @param {Object} options - CLI options
165
+ * @param {string} [options.controller] - Controller URL override
166
+ * @param {string} options.username - Username (required)
167
+ * @param {string} options.email - Email (required)
168
+ * @param {string} options.redirectUris - Comma-separated redirect URIs (required, min 1)
169
+ * @param {string} options.groupNames - Comma-separated group names (required, e.g. AI-Fabrix-Developers)
170
+ * @param {string} [options.description] - Optional description
171
+ * @returns {Promise<void>}
172
+ */
173
+ async function runServiceUserCreate(options = {}) {
174
+ const ctx = await resolveOptionsAndAuth(options);
175
+ const body = {
176
+ username: ctx.username,
177
+ email: ctx.email,
178
+ redirectUris: ctx.redirectUris,
179
+ groupNames: ctx.groupNames,
180
+ description: ctx.description
181
+ };
182
+ const response = await createServiceUser(ctx.controllerUrl, ctx.authConfig, body);
183
+ if (!response.success) {
184
+ handleCreateError(response);
185
+ return;
186
+ }
187
+ const { clientId, clientSecret } = extractCreateResponse(response);
188
+ displayCreateSuccess(clientId, clientSecret);
189
+ }
190
+
191
+ module.exports = {
192
+ runServiceUserCreate
193
+ };
@@ -10,6 +10,7 @@
10
10
 
11
11
  const path = require('path');
12
12
  const fs = require('fs');
13
+ const yaml = require('js-yaml');
13
14
  const chalk = require('chalk');
14
15
  const logger = require('../utils/logger');
15
16
  const pathsUtil = require('../utils/paths');
@@ -30,8 +31,74 @@ async function ensureTemplateAtPath(appName, targetAppPath) {
30
31
  return true;
31
32
  }
32
33
 
34
+ /**
35
+ * Resolve the directory (folder) that would contain the .env file for envOutputPath.
36
+ * @param {string} envOutputPath - Value from build.envOutputPath (e.g. ../../.env)
37
+ * @param {string} variablesPath - Path to variables.yaml
38
+ * @returns {string} Absolute path to the folder that would contain the output .env file
39
+ */
40
+ function getEnvOutputPathFolder(envOutputPath, variablesPath) {
41
+ const variablesDir = path.dirname(variablesPath);
42
+ const resolvedFile = path.resolve(variablesDir, envOutputPath);
43
+ return path.dirname(resolvedFile);
44
+ }
45
+
46
+ /**
47
+ * Validates envOutputPath: if the target folder does not exist, patches variables.yaml to set envOutputPath to null.
48
+ * Used by up-platform, up-miso, up-dataplane so we do not keep a path that points outside an existing tree.
49
+ *
50
+ * @param {string} appName - Application name (e.g. keycloak, miso-controller, dataplane)
51
+ */
52
+ function validateEnvOutputPathFolderOrNull(appName) {
53
+ if (!appName || typeof appName !== 'string') return;
54
+ const pathsToPatch = [pathsUtil.getBuilderPath(appName)];
55
+ const cwdBuilderPath = path.join(process.cwd(), 'builder', appName);
56
+ if (path.resolve(cwdBuilderPath) !== path.resolve(pathsToPatch[0])) {
57
+ pathsToPatch.push(cwdBuilderPath);
58
+ }
59
+ const envOutputPathLine = /^(\s*envOutputPath:)\s*.*$/m;
60
+ const replacement = '$1 null # deploy only, no copy';
61
+ for (const appPath of pathsToPatch) {
62
+ const variablesPath = path.join(appPath, 'variables.yaml');
63
+ if (!fs.existsSync(variablesPath)) continue;
64
+ try {
65
+ const content = fs.readFileSync(variablesPath, 'utf8');
66
+ const variables = yaml.load(content);
67
+ const value = variables?.build?.envOutputPath;
68
+ if (value === null || value === undefined || value === '') continue;
69
+ const folder = getEnvOutputPathFolder(String(value).trim(), variablesPath);
70
+ if (fs.existsSync(folder)) continue;
71
+ const newContent = content.replace(envOutputPathLine, replacement);
72
+ fs.writeFileSync(variablesPath, newContent, 'utf8');
73
+ } catch (err) {
74
+ logger.warn(chalk.yellow(`Could not validate envOutputPath in ${variablesPath}: ${err.message}`));
75
+ }
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Patches a single variables.yaml to set build.envOutputPath to null for deploy-only.
81
+ *
82
+ * @param {string} variablesPath - Path to variables.yaml
83
+ * @param {RegExp} envOutputPathLine - Regex for envOutputPath line
84
+ * @param {string} replacement - Replacement string
85
+ */
86
+ function patchOneVariablesFileForDeployOnly(variablesPath, envOutputPathLine, replacement) {
87
+ const content = fs.readFileSync(variablesPath, 'utf8');
88
+ if (!envOutputPathLine.test(content)) return;
89
+ const variables = yaml.load(content);
90
+ const value = variables?.build?.envOutputPath;
91
+ if (value !== null && value !== undefined && value !== '') {
92
+ const folder = getEnvOutputPathFolder(String(value).trim(), variablesPath);
93
+ if (fs.existsSync(folder)) return;
94
+ }
95
+ const newContent = content.replace(envOutputPathLine, replacement);
96
+ fs.writeFileSync(variablesPath, newContent, 'utf8');
97
+ }
98
+
33
99
  /**
34
100
  * Patches variables.yaml to set build.envOutputPath to null for deploy-only (no local code).
101
+ * Only patches when the target folder does NOT exist; when folder exists, keeps the value.
35
102
  * Use when running up-miso/up-platform so we do not copy .env to repo paths or show that message.
36
103
  * Patches both primary builder path and cwd/builder if different.
37
104
  *
@@ -50,10 +117,7 @@ function patchEnvOutputPathForDeployOnly(appName) {
50
117
  const variablesPath = path.join(appPath, 'variables.yaml');
51
118
  if (!fs.existsSync(variablesPath)) continue;
52
119
  try {
53
- let content = fs.readFileSync(variablesPath, 'utf8');
54
- if (!envOutputPathLine.test(content)) continue;
55
- content = content.replace(envOutputPathLine, replacement);
56
- fs.writeFileSync(variablesPath, content, 'utf8');
120
+ patchOneVariablesFileForDeployOnly(variablesPath, envOutputPathLine, replacement);
57
121
  } catch (err) {
58
122
  logger.warn(chalk.yellow(`Could not patch envOutputPath in ${variablesPath}: ${err.message}`));
59
123
  }
@@ -99,4 +163,9 @@ async function ensureAppFromTemplate(appName) {
99
163
  return primaryCopied;
100
164
  }
101
165
 
102
- module.exports = { ensureAppFromTemplate, patchEnvOutputPathForDeployOnly };
166
+ module.exports = {
167
+ ensureAppFromTemplate,
168
+ patchEnvOutputPathForDeployOnly,
169
+ validateEnvOutputPathFolderOrNull,
170
+ getEnvOutputPathFolder
171
+ };
@@ -1,9 +1,10 @@
1
1
  /**
2
2
  * AI Fabrix Builder - Up Dataplane Command
3
3
  *
4
- * Registers or rotates dataplane app in dev, then deploys. Miso-controller runs
5
- * the dataplane container; this command does not run the image locally.
6
- * If app is already registered, uses rotate-secret; otherwise registers.
4
+ * Always local deployment: registers or rotates dataplane app in dev, sends
5
+ * deployment manifest to Miso Controller, then runs the dataplane app locally
6
+ * (same as aifabrix deploy dataplane --deployment=local). If app is already
7
+ * registered, uses rotate-secret; otherwise registers.
7
8
  *
8
9
  * @fileoverview up-dataplane command implementation
9
10
  * @author AI Fabrix Team
@@ -23,7 +24,7 @@ const { registerApplication } = require('../app/register');
23
24
  const { rotateSecret } = require('../app/rotate-secret');
24
25
  const { checkApplicationExists } = require('../utils/app-existence');
25
26
  const app = require('../app');
26
- const { ensureAppFromTemplate } = require('./up-common');
27
+ const { ensureAppFromTemplate, validateEnvOutputPathFolderOrNull } = require('./up-common');
27
28
 
28
29
  /**
29
30
  * Register or rotate dataplane: if app exists in controller, rotate secret; otherwise register.
@@ -64,7 +65,8 @@ function buildDataplaneImageRef(registry) {
64
65
 
65
66
  /**
66
67
  * Handle up-dataplane command: ensure logged in, environment dev, ensure dataplane,
67
- * register or rotate (if already registered), then deploy (miso-controller runs the container).
68
+ * register or rotate (if already registered), deploy (send manifest to controller),
69
+ * then run dataplane app locally (always local deployment).
68
70
  *
69
71
  * @async
70
72
  * @function handleUpDataplane
@@ -80,7 +82,7 @@ async function handleUpDataplane(options = {}) {
80
82
  if (builderDir) {
81
83
  process.env.AIFABRIX_BUILDER_DIR = builderDir;
82
84
  }
83
- logger.log(chalk.blue('Starting up-dataplane (register/rotate, deploy dataplane in dev)...\n'));
85
+ logger.log(chalk.blue('Starting up-dataplane (register/rotate, deploy, then run dataplane locally)...\n'));
84
86
 
85
87
  const [controllerUrl, environmentKey] = await Promise.all([resolveControllerUrl(), resolveEnvironment()]);
86
88
  const authConfig = await checkAuthentication(controllerUrl, environmentKey);
@@ -95,6 +97,8 @@ async function handleUpDataplane(options = {}) {
95
97
  logger.log(chalk.green('✓ Logged in and environment is dev'));
96
98
 
97
99
  await ensureAppFromTemplate('dataplane');
100
+ // If envOutputPath target folder does not exist, set envOutputPath to null
101
+ validateEnvOutputPathFolderOrNull('dataplane');
98
102
 
99
103
  await registerOrRotateDataplane(options, controllerUrl, environmentKey, authConfig);
100
104
 
@@ -102,8 +106,10 @@ async function handleUpDataplane(options = {}) {
102
106
  const deployOpts = { imageOverride, image: imageOverride, registryMode: options.registryMode };
103
107
 
104
108
  await app.deployApp('dataplane', deployOpts);
109
+ logger.log('');
110
+ await app.runApp('dataplane', {});
105
111
 
106
- logger.log(chalk.green('\n✓ up-dataplane complete. Dataplane is registered and deployed in dev (miso-controller runs the container).'));
112
+ logger.log(chalk.green('\n✓ up-dataplane complete. Dataplane is registered, deployed in dev, and running locally.'));
107
113
  }
108
114
 
109
115
  module.exports = { handleUpDataplane, buildDataplaneImageRef };