@aifabrix/builder 2.37.9 → 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 (74) hide show
  1. package/.cursor/rules/project-rules.mdc +3 -0
  2. package/README.md +19 -0
  3. package/integration/hubspot/hubspot-deploy.json +1 -5
  4. package/integration/hubspot/hubspot-system.json +0 -3
  5. package/lib/api/applications.api.js +29 -1
  6. package/lib/api/auth.api.js +14 -0
  7. package/lib/api/credentials.api.js +34 -0
  8. package/lib/api/datasources-core.api.js +16 -1
  9. package/lib/api/datasources-extended.api.js +18 -1
  10. package/lib/api/deployments.api.js +32 -0
  11. package/lib/api/environments.api.js +11 -0
  12. package/lib/api/external-systems.api.js +16 -1
  13. package/lib/api/pipeline.api.js +12 -4
  14. package/lib/api/service-users.api.js +41 -0
  15. package/lib/api/types/applications.types.js +1 -1
  16. package/lib/api/types/deployments.types.js +1 -1
  17. package/lib/api/types/pipeline.types.js +1 -1
  18. package/lib/api/types/service-users.types.js +24 -0
  19. package/lib/api/wizard.api.js +40 -1
  20. package/lib/app/deploy.js +86 -21
  21. package/lib/app/rotate-secret.js +3 -1
  22. package/lib/app/run-helpers.js +35 -2
  23. package/lib/app/show-display.js +30 -11
  24. package/lib/app/show.js +34 -8
  25. package/lib/cli/index.js +4 -0
  26. package/lib/cli/setup-app.js +40 -0
  27. package/lib/cli/setup-credential-deployment.js +72 -0
  28. package/lib/cli/setup-infra.js +3 -3
  29. package/lib/cli/setup-service-user.js +52 -0
  30. package/lib/cli/setup-utility.js +1 -25
  31. package/lib/commands/app-down.js +80 -0
  32. package/lib/commands/app-logs.js +146 -0
  33. package/lib/commands/app.js +24 -1
  34. package/lib/commands/credential-list.js +104 -0
  35. package/lib/commands/deployment-list.js +184 -0
  36. package/lib/commands/service-user.js +193 -0
  37. package/lib/commands/up-common.js +74 -5
  38. package/lib/commands/up-dataplane.js +13 -7
  39. package/lib/commands/up-miso.js +17 -24
  40. package/lib/core/templates.js +2 -2
  41. package/lib/external-system/deploy.js +79 -15
  42. package/lib/generator/builders.js +8 -27
  43. package/lib/generator/external-controller-manifest.js +5 -4
  44. package/lib/generator/index.js +16 -14
  45. package/lib/generator/split.js +1 -0
  46. package/lib/generator/wizard.js +4 -1
  47. package/lib/schema/application-schema.json +6 -14
  48. package/lib/schema/deployment-rules.yaml +121 -0
  49. package/lib/schema/external-system.schema.json +0 -16
  50. package/lib/utils/app-register-config.js +10 -12
  51. package/lib/utils/app-run-containers.js +2 -1
  52. package/lib/utils/compose-generator.js +2 -1
  53. package/lib/utils/deployment-errors.js +10 -0
  54. package/lib/utils/environment-checker.js +25 -6
  55. package/lib/utils/help-builder.js +0 -1
  56. package/lib/utils/image-version.js +209 -0
  57. package/lib/utils/schema-loader.js +1 -1
  58. package/lib/utils/variable-transformer.js +7 -33
  59. package/lib/validation/external-manifest-validator.js +1 -1
  60. package/package.json +1 -1
  61. package/templates/applications/README.md.hbs +1 -3
  62. package/templates/applications/dataplane/Dockerfile +2 -2
  63. package/templates/applications/dataplane/README.md +20 -6
  64. package/templates/applications/dataplane/env.template +31 -2
  65. package/templates/applications/dataplane/rbac.yaml +1 -1
  66. package/templates/applications/dataplane/variables.yaml +7 -4
  67. package/templates/applications/keycloak/Dockerfile +3 -3
  68. package/templates/applications/keycloak/README.md +14 -4
  69. package/templates/applications/keycloak/env.template +17 -2
  70. package/templates/applications/keycloak/variables.yaml +2 -1
  71. package/templates/applications/miso-controller/README.md +1 -3
  72. package/templates/applications/miso-controller/env.template +85 -25
  73. package/templates/applications/miso-controller/rbac.yaml +15 -0
  74. package/templates/applications/miso-controller/variables.yaml +24 -23
@@ -27,9 +27,13 @@ const composeGenerator = require('../utils/compose-generator');
27
27
  const dockerUtils = require('../utils/docker');
28
28
  const containerHelpers = require('../utils/app-run-containers');
29
29
  const pathsUtil = require('../utils/paths');
30
+ const { resolveVersionForApp } = require('../utils/image-version');
30
31
 
31
32
  const execAsync = promisify(exec);
32
33
 
34
+ /** Template apps (keycloak, miso-controller, dataplane) - never update variables.yaml when running */
35
+ const TEMPLATE_APP_KEYS = ['keycloak', 'miso-controller', 'dataplane'];
36
+
33
37
  // Re-export container helper functions
34
38
  const checkImageExists = containerHelpers.checkImageExists;
35
39
  const checkContainerRunning = containerHelpers.checkContainerRunning;
@@ -138,6 +142,25 @@ async function validateAppConfiguration(appName) {
138
142
  return appConfig;
139
143
  }
140
144
 
145
+ /**
146
+ * Resolves version from image and updates builder/variables.yaml when running.
147
+ * Template apps (keycloak, miso-controller, dataplane) are never updated - variables.yaml stays pristine.
148
+ * @async
149
+ * @param {string} appName - Application name
150
+ * @param {Object} appConfig - Application configuration
151
+ * @param {boolean} debug - Enable debug logging
152
+ */
153
+ async function resolveAndUpdateVersion(appName, appConfig, debug) {
154
+ const isTemplateApp = TEMPLATE_APP_KEYS.includes(appName);
155
+ const resolved = await resolveVersionForApp(appName, appConfig, {
156
+ updateBuilder: !isTemplateApp,
157
+ builderPath: pathsUtil.getBuilderPath(appName)
158
+ });
159
+ if (resolved.fromImage && resolved.updated && debug) {
160
+ logger.log(chalk.gray(`[DEBUG] Updated app.version to ${resolved.version} from image`));
161
+ }
162
+ }
163
+
141
164
  /**
142
165
  * Checks prerequisites: Docker image and (optionally) infrastructure
143
166
  * @async
@@ -163,10 +186,20 @@ async function checkPrerequisites(appName, appConfig, debug = false, skipInfraCh
163
186
  }
164
187
  logger.log(chalk.green(`✓ Image ${fullImageName} found`));
165
188
 
166
- if (skipInfraCheck) {
167
- return;
189
+ await resolveAndUpdateVersion(appName, appConfig, debug);
190
+
191
+ if (!skipInfraCheck) {
192
+ await checkInfraHealthOrThrow(debug);
168
193
  }
194
+ }
169
195
 
196
+ /**
197
+ * Checks infrastructure health and throws if unhealthy
198
+ * @async
199
+ * @param {boolean} debug - Enable debug logging
200
+ * @throws {Error} If infrastructure is not healthy
201
+ */
202
+ async function checkInfraHealthOrThrow(debug) {
170
203
  logger.log(chalk.blue('Checking infrastructure health...'));
171
204
  const infraHealth = await infra.checkInfraHealth();
172
205
  if (debug) {
@@ -31,14 +31,21 @@ function logApplicationRequired(a) {
31
31
  logger.log(` Port: ${port}`);
32
32
  }
33
33
 
34
+ const APPLICATION_OPTIONAL_FIELDS = [
35
+ { key: 'deploymentKey', label: 'Deployment' },
36
+ { key: 'image', label: 'Image' },
37
+ { key: 'registryMode', label: 'Registry' },
38
+ { key: 'healthCheck', label: 'Health' },
39
+ { key: 'build', label: 'Build' },
40
+ { key: 'status', label: 'Status' },
41
+ { key: 'url', label: 'URL' },
42
+ { key: 'internalUrl', label: 'Internal URL' }
43
+ ];
44
+
34
45
  function logApplicationOptional(a) {
35
- if (a.deploymentKey !== undefined) logger.log(` Deployment: ${a.deploymentKey ?? '—'}`);
36
- if (a.image !== undefined) logger.log(` Image: ${a.image ?? '—'}`);
37
- if (a.registryMode !== undefined) logger.log(` Registry: ${a.registryMode ?? '—'}`);
38
- if (a.healthCheck !== undefined) logger.log(` Health: ${a.healthCheck ?? '—'}`);
39
- if (a.build !== undefined) logger.log(` Build: ${a.build ?? '—'}`);
40
- if (a.status !== undefined) logger.log(` Status: ${a.status ?? '—'}`);
41
- if (a.url !== undefined) logger.log(` URL: ${a.url ?? '—'}`);
46
+ APPLICATION_OPTIONAL_FIELDS.forEach(({ key, label }) => {
47
+ if (a[key] !== undefined) logger.log(` ${(label + ':').padEnd(16)} ${a[key] ?? '—'}`);
48
+ });
42
49
  }
43
50
 
44
51
  function logApplicationFields(a) {
@@ -75,10 +82,15 @@ function logRolesSection(roles) {
75
82
  });
76
83
  }
77
84
 
78
- function logPermissionsSection(permissions) {
79
- if (permissions.length === 0) return;
85
+ function logPermissionsSection(permissions, opts = {}) {
86
+ const showWhenEmpty = opts.showWhenEmpty || false;
87
+ if (permissions.length === 0 && !showWhenEmpty) return;
80
88
  logger.log('');
81
89
  logger.log('🛡️ Permissions');
90
+ if (permissions.length === 0) {
91
+ logger.log(' (none)');
92
+ return;
93
+ }
82
94
  permissions.forEach((p) => {
83
95
  const name = p.name ?? '—';
84
96
  const roleList = (p.roles || []).join(', ');
@@ -186,8 +198,10 @@ function logExternalSystemSection(ext) {
186
198
  /**
187
199
  * Format and print human-readable show output (offline or online).
188
200
  * @param {Object} summary - Unified summary (buildOfflineSummaryFromDeployJson or buildOnlineSummary)
201
+ * @param {Object} [options] - Display options
202
+ * @param {boolean} [options.permissionsOnly] - When true, output only source and Permissions section
189
203
  */
190
- function display(summary) {
204
+ function display(summary, options = {}) {
191
205
  const a = summary.application;
192
206
  const roles = summary.roles ?? a.roles ?? [];
193
207
  const permissions = summary.permissions ?? a.permissions ?? [];
@@ -197,9 +211,14 @@ function display(summary) {
197
211
  const dbNames = Array.isArray(databases) ? databases.map((d) => (d && d.name) || d).filter(Boolean) : [];
198
212
 
199
213
  logSourceAndHeader(summary);
214
+ if (options.permissionsOnly) {
215
+ logPermissionsSection(permissions, { showWhenEmpty: true });
216
+ logger.log('');
217
+ return;
218
+ }
200
219
  logApplicationSection(a, summary.isExternal);
201
220
  logRolesSection(roles);
202
- logPermissionsSection(permissions);
221
+ /* Permissions section shown only when --permissions is set (permissionsOnly mode) */
203
222
  logAuthSection(authentication);
204
223
  logConfigurationsSection(portalInputConfigurations);
205
224
  logDatabasesSection(dbNames);
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
@@ -20,6 +20,8 @@ const { setupSecretsCommands } = require('./setup-secrets');
20
20
  const { setupExternalSystemCommands } = require('./setup-external-system');
21
21
  const { setupAppCommands: setupAppManagementCommands } = require('../commands/app');
22
22
  const { setupDatasourceCommands } = require('../commands/datasource');
23
+ const { setupCredentialDeploymentCommands } = require('./setup-credential-deployment');
24
+ const { setupServiceUserCommands } = require('./setup-service-user');
23
25
 
24
26
  /**
25
27
  * Sets up all CLI commands on the Commander program instance
@@ -33,6 +35,8 @@ function setupCommands(program) {
33
35
  setupAppManagementCommands(program);
34
36
  setupDatasourceCommands(program);
35
37
  setupUtilityCommands(program);
38
+ setupCredentialDeploymentCommands(program);
39
+ setupServiceUserCommands(program);
36
40
  setupExternalSystemCommands(program);
37
41
  setupDevCommands(program);
38
42
  setupSecretsCommands(program);
@@ -173,6 +173,7 @@ See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`)
173
173
  .description('Run application locally')
174
174
  .option('-p, --port <port>', 'Override local port')
175
175
  .option('-d, --debug', 'Enable debug output with detailed container information')
176
+ .option('-t, --tag <tag>', 'Image tag to run (e.g. v1.0.0); overrides variables.yaml image.tag')
176
177
  .action(async(appName, options) => {
177
178
  try {
178
179
  await app.runApp(appName, options);
@@ -182,6 +183,37 @@ See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`)
182
183
  }
183
184
  });
184
185
 
186
+ program.command('logs <app>')
187
+ .description('Show application container logs (and optional env summary with secrets masked)')
188
+ .option('-f', 'Follow log stream')
189
+ .option('-t, --tail <lines>', 'Number of lines (default: 100); 0 = full list', '100')
190
+ .action(async(appName, options) => {
191
+ try {
192
+ const { runAppLogs } = require('../commands/app-logs');
193
+ const tailNum = parseInt(options.tail, 10);
194
+ await runAppLogs(appName, {
195
+ follow: options.f,
196
+ tail: Number.isNaN(tailNum) ? 100 : tailNum
197
+ });
198
+ } catch (error) {
199
+ handleCommandError(error, 'logs');
200
+ process.exit(1);
201
+ }
202
+ });
203
+
204
+ program.command('down-app <app>')
205
+ .description('Stop and remove application container; optionally remove volume and image')
206
+ .option('--volumes', 'Remove application Docker volume')
207
+ .action(async(appName, options) => {
208
+ try {
209
+ const { runDownAppWithImageRemoval } = require('../commands/app-down');
210
+ await runDownAppWithImageRemoval(appName, { volumes: options.volumes });
211
+ } catch (error) {
212
+ handleCommandError(error, 'down-app');
213
+ process.exit(1);
214
+ }
215
+ });
216
+
185
217
  program.command('push <app>')
186
218
  .description('Push image to Azure Container Registry')
187
219
  .option('-r, --registry <registry>', 'ACR registry URL (overrides variables.yaml)')
@@ -197,6 +229,7 @@ See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`)
197
229
 
198
230
  program.command('deploy <app>')
199
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')
200
233
  .option('--type <type>', 'Application type: external to deploy from integration/<app> (no app register needed)')
201
234
  .option('--client-id <id>', 'Client ID (overrides config)')
202
235
  .option('--client-secret <secret>', 'Client Secret (overrides config)')
@@ -204,7 +237,14 @@ See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`)
204
237
  .option('--no-poll', 'Do not poll for status')
205
238
  .action(async(appName, options) => {
206
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
+ }
207
244
  await app.deployApp(appName, options);
245
+ if (target === 'local') {
246
+ await app.runApp(appName, options);
247
+ }
208
248
  } catch (error) {
209
249
  handleCommandError(error, 'deploy');
210
250
  process.exit(1);
@@ -0,0 +1,72 @@
1
+ /**
2
+ * CLI credential and deployment list command setup.
3
+ * Commands: credential list, deployment list.
4
+ *
5
+ * @fileoverview Credential and deployment list 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 { runCredentialList } = require('../commands/credential-list');
14
+ const { runDeploymentList } = require('../commands/deployment-list');
15
+
16
+ /**
17
+ * Sets up credential and deployment list commands
18
+ * @param {Command} program - Commander program instance
19
+ */
20
+ function setupCredentialDeploymentCommands(program) {
21
+ const credential = program
22
+ .command('credential')
23
+ .description('Manage credentials');
24
+
25
+ credential
26
+ .command('list')
27
+ .description('List credentials from controller/dataplane (GET /api/v1/credential)')
28
+ .option('--controller <url>', 'Controller URL (default: from config)')
29
+ .option('--active-only', 'List only active credentials')
30
+ .option('--page-size <n>', 'Items per page', '50')
31
+ .action(async(options) => {
32
+ try {
33
+ const opts = {
34
+ controller: options.controller,
35
+ activeOnly: options.activeOnly,
36
+ pageSize: parseInt(options.pageSize, 10) || 50
37
+ };
38
+ await runCredentialList(opts);
39
+ } catch (error) {
40
+ logger.error(chalk.red(`Error: ${error.message}`));
41
+ handleCommandError(error, 'credential list');
42
+ process.exit(1);
43
+ }
44
+ });
45
+
46
+ const deployment = program
47
+ .command('deployment')
48
+ .description('List deployments');
49
+
50
+ deployment
51
+ .command('list')
52
+ .description('List last N deployments for current environment (default pageSize=50)')
53
+ .option('--controller <url>', 'Controller URL (default: from config)')
54
+ .option('--environment <env>', 'Environment key (default: from config)')
55
+ .option('--page-size <n>', 'Items per page', '50')
56
+ .action(async(options) => {
57
+ try {
58
+ const opts = {
59
+ controller: options.controller,
60
+ environment: options.environment,
61
+ pageSize: parseInt(options.pageSize, 10) || 50
62
+ };
63
+ await runDeploymentList(opts);
64
+ } catch (error) {
65
+ logger.error(chalk.red(`Error: ${error.message}`));
66
+ handleCommandError(error, 'deployment list');
67
+ process.exit(1);
68
+ }
69
+ });
70
+ }
71
+
72
+ module.exports = { setupCredentialDeploymentCommands };
@@ -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 };
@@ -1,5 +1,5 @@
1
1
  /**
2
- * CLI utility command setup (resolve, json, split-json, genkey, show, validate, diff).
2
+ * CLI utility command setup (resolve, json, split-json, show, validate, diff).
3
3
  *
4
4
  * @fileoverview Utility command definitions for AI Fabrix Builder CLI
5
5
  * @author AI Fabrix Team
@@ -127,30 +127,6 @@ function setupUtilityCommands(program) {
127
127
  }
128
128
  });
129
129
 
130
- program.command('genkey <app>')
131
- .description('Generate deployment key')
132
- .action(async(appName) => {
133
- try {
134
- const jsonPath = await generator.generateDeployJson(appName);
135
-
136
- const jsonContent = fs.readFileSync(jsonPath, 'utf8');
137
- const deployment = JSON.parse(jsonContent);
138
-
139
- const key = deployment.deploymentKey;
140
-
141
- if (!key) {
142
- throw new Error('deploymentKey not found in generated JSON');
143
- }
144
-
145
- logger.log(`\nDeployment key for ${appName}:`);
146
- logger.log(key);
147
- logger.log(chalk.gray(`\nGenerated from: ${jsonPath}`));
148
- } catch (error) {
149
- handleCommandError(error, 'genkey');
150
- process.exit(1);
151
- }
152
- });
153
-
154
130
  program.command('show <appKey>')
155
131
  .description('Show application info from local builder/ or integration/ (offline) or from controller (--online)')
156
132
  .option('--online', 'Fetch application data from the controller')
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Down-app command – stop container, optionally volumes, then remove image if unused
3
+ *
4
+ * @fileoverview Down-app command implementation
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const chalk = require('chalk');
10
+ const { exec } = require('child_process');
11
+ const { promisify } = require('util');
12
+ const logger = require('../utils/logger');
13
+ const config = require('../core/config');
14
+ const containerHelpers = require('../utils/app-run-containers');
15
+ const { downApp } = require('../app/down');
16
+
17
+ const execAsync = promisify(exec);
18
+
19
+ /**
20
+ * Get image ID of a running container (sha or name:tag)
21
+ * @async
22
+ * @param {string} containerName - Docker container name
23
+ * @returns {Promise<string|null>} Image ID or null if container not found / not running
24
+ */
25
+ async function getContainerImageId(containerName) {
26
+ try {
27
+ const { stdout } = await execAsync(
28
+ `docker inspect --format='{{.Image}}' ${containerName}`,
29
+ { encoding: 'utf8' }
30
+ );
31
+ const id = (stdout && stdout.trim()) || null;
32
+ return id || null;
33
+ } catch {
34
+ return null;
35
+ }
36
+ }
37
+
38
+ /**
39
+ * Remove Docker image by ID; ignore "in use" or "no such image"
40
+ * @async
41
+ * @param {string} imageId - Image ID (sha or name:tag)
42
+ */
43
+ async function removeImageIfUnused(imageId) {
44
+ if (!imageId) return;
45
+ try {
46
+ await execAsync(`docker rmi ${imageId}`);
47
+ logger.log(chalk.green(`✓ Image ${imageId} removed`));
48
+ } catch (err) {
49
+ const msg = (err && err.message) || '';
50
+ if (msg.includes('in use') || msg.includes('is being used')) {
51
+ logger.log(chalk.gray(`Image ${imageId} still in use by another container; not removed`));
52
+ } else if (msg.includes('No such image')) {
53
+ logger.log(chalk.gray(`Image ${imageId} not found (already removed)`));
54
+ } else {
55
+ logger.log(chalk.yellow(`Could not remove image: ${msg}`));
56
+ }
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Run down-app: get image from container, stop/remove container (and optionally volume), then remove image if unused
62
+ * @async
63
+ * @param {string} appName - Application name
64
+ * @param {Object} options - { volumes: boolean }
65
+ * @returns {Promise<void>}
66
+ */
67
+ async function runDownAppWithImageRemoval(appName, options = {}) {
68
+ const developerId = await config.getDeveloperId();
69
+ const containerName = containerHelpers.getContainerName(appName, developerId);
70
+ const imageId = await getContainerImageId(containerName);
71
+
72
+ await downApp(appName, options);
73
+ await removeImageIfUnused(imageId);
74
+ }
75
+
76
+ module.exports = {
77
+ runDownAppWithImageRemoval,
78
+ getContainerImageId,
79
+ removeImageIfUnused
80
+ };