@aifabrix/builder 2.40.2 → 2.41.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 (103) hide show
  1. package/README.md +6 -4
  2. package/integration/hubspot/test.js +1 -1
  3. package/lib/api/credential.api.js +40 -0
  4. package/lib/api/dev.api.js +423 -0
  5. package/lib/api/types/credential.types.js +23 -0
  6. package/lib/api/types/dev.types.js +140 -0
  7. package/lib/app/config.js +21 -0
  8. package/lib/app/down.js +2 -1
  9. package/lib/app/index.js +9 -0
  10. package/lib/app/push.js +36 -12
  11. package/lib/app/readme.js +1 -3
  12. package/lib/app/run-env-compose.js +201 -0
  13. package/lib/app/run-helpers.js +121 -118
  14. package/lib/app/run.js +148 -28
  15. package/lib/app/show.js +5 -2
  16. package/lib/build/index.js +11 -3
  17. package/lib/cli/setup-app.js +140 -14
  18. package/lib/cli/setup-dev.js +180 -17
  19. package/lib/cli/setup-environment.js +4 -2
  20. package/lib/cli/setup-external-system.js +71 -21
  21. package/lib/cli/setup-infra.js +29 -2
  22. package/lib/cli/setup-secrets.js +52 -5
  23. package/lib/cli/setup-utility.js +12 -3
  24. package/lib/commands/app-install.js +172 -0
  25. package/lib/commands/app-shell.js +75 -0
  26. package/lib/commands/app-test.js +282 -0
  27. package/lib/commands/app.js +1 -1
  28. package/lib/commands/dev-cli-handlers.js +141 -0
  29. package/lib/commands/dev-down.js +114 -0
  30. package/lib/commands/dev-init.js +309 -0
  31. package/lib/commands/secrets-list.js +118 -0
  32. package/lib/commands/secrets-remove.js +97 -0
  33. package/lib/commands/secrets-set.js +30 -17
  34. package/lib/commands/secrets-validate.js +50 -0
  35. package/lib/commands/up-dataplane.js +2 -2
  36. package/lib/commands/up-miso.js +0 -25
  37. package/lib/commands/upload.js +26 -1
  38. package/lib/core/admin-secrets.js +96 -0
  39. package/lib/core/secrets-ensure.js +378 -0
  40. package/lib/core/secrets-env-write.js +157 -0
  41. package/lib/core/secrets.js +147 -81
  42. package/lib/datasource/field-reference-validator.js +91 -0
  43. package/lib/datasource/validate.js +21 -3
  44. package/lib/deployment/environment-config.js +137 -0
  45. package/lib/deployment/environment.js +21 -98
  46. package/lib/deployment/push.js +32 -2
  47. package/lib/external-system/download.js +7 -0
  48. package/lib/external-system/test-auth.js +7 -3
  49. package/lib/external-system/test.js +5 -1
  50. package/lib/generator/index.js +174 -25
  51. package/lib/generator/wizard.js +8 -0
  52. package/lib/infrastructure/helpers.js +103 -20
  53. package/lib/infrastructure/index.js +88 -10
  54. package/lib/infrastructure/services.js +70 -15
  55. package/lib/schema/application-schema.json +24 -3
  56. package/lib/schema/external-system.schema.json +435 -413
  57. package/lib/utils/api.js +3 -3
  58. package/lib/utils/app-register-auth.js +25 -3
  59. package/lib/utils/cli-utils.js +20 -0
  60. package/lib/utils/compose-generator.js +76 -75
  61. package/lib/utils/compose-handlebars-helpers.js +43 -0
  62. package/lib/utils/compose-vector-helper.js +18 -0
  63. package/lib/utils/config-paths.js +127 -2
  64. package/lib/utils/credential-secrets-env.js +267 -0
  65. package/lib/utils/dev-cert-helper.js +122 -0
  66. package/lib/utils/device-code-helpers.js +224 -0
  67. package/lib/utils/device-code.js +37 -336
  68. package/lib/utils/docker-build.js +40 -8
  69. package/lib/utils/env-copy.js +83 -13
  70. package/lib/utils/env-map.js +35 -5
  71. package/lib/utils/env-template.js +6 -5
  72. package/lib/utils/error-formatters/http-status-errors.js +20 -1
  73. package/lib/utils/help-builder.js +15 -2
  74. package/lib/utils/infra-status.js +30 -1
  75. package/lib/utils/local-secrets.js +7 -52
  76. package/lib/utils/mutagen-install.js +195 -0
  77. package/lib/utils/mutagen.js +146 -0
  78. package/lib/utils/paths.js +43 -33
  79. package/lib/utils/port-resolver.js +28 -16
  80. package/lib/utils/remote-dev-auth.js +38 -0
  81. package/lib/utils/remote-docker-env.js +43 -0
  82. package/lib/utils/remote-secrets-loader.js +60 -0
  83. package/lib/utils/secrets-generator.js +94 -6
  84. package/lib/utils/secrets-helpers.js +33 -25
  85. package/lib/utils/secrets-path.js +2 -2
  86. package/lib/utils/secrets-utils.js +52 -1
  87. package/lib/utils/secrets-validation.js +84 -0
  88. package/lib/utils/ssh-key-helper.js +116 -0
  89. package/lib/utils/token-manager-messages.js +90 -0
  90. package/lib/utils/token-manager.js +5 -4
  91. package/lib/utils/variable-transformer.js +3 -3
  92. package/lib/validation/validator.js +65 -0
  93. package/package.json +2 -2
  94. package/scripts/install-local.js +34 -15
  95. package/templates/README.md +0 -1
  96. package/templates/applications/README.md.hbs +4 -4
  97. package/templates/applications/dataplane/application.yaml +5 -4
  98. package/templates/applications/dataplane/env.template +12 -7
  99. package/templates/applications/keycloak/env.template +2 -0
  100. package/templates/applications/miso-controller/application.yaml +1 -0
  101. package/templates/applications/miso-controller/env.template +11 -9
  102. package/templates/python/docker-compose.hbs +49 -23
  103. package/templates/typescript/docker-compose.hbs +48 -22
@@ -154,6 +154,31 @@ See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`;
154
154
  });
155
155
  }
156
156
 
157
+ function registerRunCommand(program) {
158
+ const runHelp = `
159
+ In dev: use --reload for sync and mount (requires remote server with Mutagen, or local Docker).
160
+ Examples:
161
+ $ aifabrix run myapp
162
+ $ aifabrix run myapp --env tst
163
+ $ aifabrix run myapp --reload`;
164
+ program.command('run <app>')
165
+ .description('Run application locally (or remotely on your Docker host)')
166
+ .option('-p, --port <port>', 'Override local port')
167
+ .option('-d, --debug', 'Enable debug output with detailed container information')
168
+ .option('-t, --tag <tag>', 'Image tag to run (e.g. v1.0.0); overrides application.yaml image.tag')
169
+ .option('-e, --env <env>', 'Environment: dev (default), tst, or pro', 'dev')
170
+ .option('--reload', 'In dev: use sync and mount (requires remote server; Mutagen or local Docker)')
171
+ .addHelpText('after', runHelp)
172
+ .action(async(appName, options) => {
173
+ try {
174
+ await app.runApp(appName, options);
175
+ } catch (error) {
176
+ handleCommandError(error, 'run');
177
+ process.exit(1);
178
+ }
179
+ });
180
+ }
181
+
157
182
  function setupBuildRunLogsDownCommands(program) {
158
183
  program.command('build <app>')
159
184
  .description('Build container image (auto-detects runtime)')
@@ -170,19 +195,7 @@ function setupBuildRunLogsDownCommands(program) {
170
195
  }
171
196
  });
172
197
 
173
- program.command('run <app>')
174
- .description('Run application locally')
175
- .option('-p, --port <port>', 'Override local port')
176
- .option('-d, --debug', 'Enable debug output with detailed container information')
177
- .option('-t, --tag <tag>', 'Image tag to run (e.g. v1.0.0); overrides application.yaml image.tag')
178
- .action(async(appName, options) => {
179
- try {
180
- await app.runApp(appName, options);
181
- } catch (error) {
182
- handleCommandError(error, 'run');
183
- process.exit(1);
184
- }
185
- });
198
+ registerRunCommand(program);
186
199
 
187
200
  program.command('logs <app>')
188
201
  .description('Show application container logs (and optional env summary with secrets masked)')
@@ -215,6 +228,117 @@ function setupBuildRunLogsDownCommands(program) {
215
228
  });
216
229
  }
217
230
 
231
+ function setupShellTestStopCommands(program) {
232
+ program.command('stop <app>')
233
+ .description('Stop and remove application container (alias for down-app)')
234
+ .option('--volumes', 'Remove application Docker volume')
235
+ .action(async(appName, options) => {
236
+ try {
237
+ const { runDownAppWithImageRemoval } = require('../commands/app-down');
238
+ await runDownAppWithImageRemoval(appName, { volumes: options.volumes });
239
+ } catch (error) {
240
+ handleCommandError(error, 'stop');
241
+ process.exit(1);
242
+ }
243
+ });
244
+
245
+ program.command('shell <app>')
246
+ .description('Open interactive shell in the application container')
247
+ .option('--env <env>', 'Environment (dev|tst); dev uses running container', 'dev')
248
+ .action(async(appName, options) => {
249
+ try {
250
+ const { runAppShell } = require('../commands/app-shell');
251
+ await runAppShell(appName, { env: options.env });
252
+ } catch (error) {
253
+ handleCommandError(error, 'shell');
254
+ process.exit(1);
255
+ }
256
+ });
257
+
258
+ program.command('test <app>')
259
+ .description('Run tests (builder app: in container; external system: local validation)')
260
+ .option('--env <env>', 'For builder app: dev (running container) or tst (ephemeral)', 'dev')
261
+ .option('-d, --datasource <key>', 'For external system: test specific datasource only')
262
+ .option('-v, --verbose', 'Verbose output')
263
+ .action(async(appName, options) => {
264
+ try {
265
+ const pathsUtil = require('../utils/paths');
266
+ const appType = await pathsUtil.detectAppType(appName).catch(() => null);
267
+ if (appType && appType.baseDir === 'integration') {
268
+ const test = require('../external-system/test');
269
+ const results = await test.testExternalSystem(appName, options);
270
+ test.displayTestResults(results, options.verbose);
271
+ if (!results.valid) process.exit(1);
272
+ } else {
273
+ const { runAppTest } = require('../commands/app-test');
274
+ await runAppTest(appName, { env: options.env });
275
+ }
276
+ } catch (error) {
277
+ handleCommandError(error, 'test');
278
+ process.exit(1);
279
+ }
280
+ });
281
+ }
282
+
283
+ function setupInstallTestE2eLintCommands(program) {
284
+ program.command('install <app>')
285
+ .description('Install dependencies in container (builder apps only)')
286
+ .option('--env <env>', 'dev (running container) or tst (ephemeral with .env)', 'dev')
287
+ .action(async(appName, options) => {
288
+ try {
289
+ const pathsUtil = require('../utils/paths');
290
+ const appType = await pathsUtil.detectAppType(appName).catch(() => null);
291
+ if (appType && appType.baseDir === 'integration') {
292
+ logger.log(chalk.gray('Install is for builder applications only. Use aifabrix shell <app> to run commands in external setups.'));
293
+ return;
294
+ }
295
+ const { runAppInstall } = require('../commands/app-install');
296
+ await runAppInstall(appName, { env: options.env });
297
+ } catch (error) {
298
+ handleCommandError(error, 'install');
299
+ process.exit(1);
300
+ }
301
+ });
302
+
303
+ program.command('test-e2e <app>')
304
+ .description('Run e2e tests in container (builder apps only)')
305
+ .option('--env <env>', 'dev (running container) or tst (ephemeral with .env)', 'dev')
306
+ .action(async(appName, options) => {
307
+ try {
308
+ const pathsUtil = require('../utils/paths');
309
+ const appType = await pathsUtil.detectAppType(appName).catch(() => null);
310
+ if (appType && appType.baseDir === 'integration') {
311
+ logger.log(chalk.gray('test-e2e is for builder applications only. Use aifabrix shell <app> then make test:e2e or pnpm test:e2e.'));
312
+ return;
313
+ }
314
+ const { runAppTestE2e } = require('../commands/app-test');
315
+ await runAppTestE2e(appName, { env: options.env });
316
+ } catch (error) {
317
+ handleCommandError(error, 'test-e2e');
318
+ process.exit(1);
319
+ }
320
+ });
321
+
322
+ program.command('lint <app>')
323
+ .description('Run lint in container (builder apps only)')
324
+ .option('--env <env>', 'dev (running container) or tst (ephemeral with .env)', 'dev')
325
+ .action(async(appName, options) => {
326
+ try {
327
+ const pathsUtil = require('../utils/paths');
328
+ const appType = await pathsUtil.detectAppType(appName).catch(() => null);
329
+ if (appType && appType.baseDir === 'integration') {
330
+ logger.log(chalk.gray('lint is for builder applications only. Use aifabrix shell <app> then make lint or pnpm lint.'));
331
+ return;
332
+ }
333
+ const { runAppLint } = require('../commands/app-test');
334
+ await runAppLint(appName, { env: options.env });
335
+ } catch (error) {
336
+ handleCommandError(error, 'lint');
337
+ process.exit(1);
338
+ }
339
+ });
340
+ }
341
+
218
342
  function setupPushDeployDockerfileCommands(program) {
219
343
  program.command('push <app>')
220
344
  .description('Push image to Azure Container Registry')
@@ -230,7 +354,7 @@ function setupPushDeployDockerfileCommands(program) {
230
354
  });
231
355
 
232
356
  program.command('deploy <app>')
233
- .description('Deploy to Azure via Miso Controller')
357
+ .description('Deploy to Azure or locally via Miso Controller')
234
358
  .option('--local', 'Send manifest to controller then run app locally (app: same as aifabrix run <app>; external: restart dataplane)')
235
359
  .option('--client-id <id>', 'Client ID (overrides config)')
236
360
  .option('--client-secret <secret>', 'Client Secret (overrides config)')
@@ -274,6 +398,8 @@ function setupAppCommands(program) {
274
398
  setupCreateCommand(program);
275
399
  setupWizardCommand(program);
276
400
  setupBuildRunLogsDownCommands(program);
401
+ setupShellTestStopCommands(program);
402
+ setupInstallTestE2eLintCommands(program);
277
403
  setupPushDeployDockerfileCommands(program);
278
404
  }
279
405
 
@@ -1,5 +1,5 @@
1
1
  /**
2
- * CLI developer configuration command setup (dev config, dev set-id).
2
+ * CLI developer configuration command setup (dev config, dev set-id, dev init, dev add/update/pin/delete/list).
3
3
  *
4
4
  * @fileoverview Developer command definitions for AI Fabrix Builder CLI
5
5
  * @author AI Fabrix Team
@@ -11,20 +11,37 @@ const config = require('../core/config');
11
11
  const devConfig = require('../utils/dev-config');
12
12
  const logger = require('../utils/logger');
13
13
  const { handleCommandError } = require('../utils/cli-utils');
14
+ const { runDevInit, runDevRefresh } = require('../commands/dev-init');
15
+ const {
16
+ handleDevList,
17
+ handleDevAdd,
18
+ handleDevUpdate,
19
+ handleDevPin,
20
+ handleDevDelete
21
+ } = require('../commands/dev-cli-handlers');
14
22
 
15
23
  /**
16
- * Display developer configuration
24
+ * Display developer configuration (local ports and config; remote/settings keys always shown).
25
+ * Always shows environment, controller, and remote keys (value or "(not set)").
17
26
  * @param {string} devId - Developer ID
18
27
  * @returns {Promise<void>}
19
28
  */
20
29
  async function displayDevConfig(devId) {
21
30
  const devIdNum = parseInt(devId, 10);
22
31
  const ports = devConfig.getDevPorts(devIdNum);
23
- const configVars = [
32
+ const environment = await config.getCurrentEnvironment();
33
+ const controller = await config.getControllerUrl();
34
+
35
+ const optionalConfigVars = [
24
36
  { key: 'aifabrix-home', value: await config.getAifabrixHomeOverride() },
25
37
  { key: 'aifabrix-secrets', value: await config.getAifabrixSecretsPath() },
26
- { key: 'aifabrix-env-config', value: await config.getAifabrixEnvConfigPath() }
27
- ].filter(v => v.value);
38
+ { key: 'aifabrix-env-config', value: await config.getAifabrixEnvConfigPath() },
39
+ { key: 'remote-server', value: await config.getRemoteServer() },
40
+ { key: 'docker-endpoint', value: await config.getDockerEndpoint() },
41
+ { key: 'user-mutagen-folder', value: await config.getUserMutagenFolder() },
42
+ { key: 'sync-ssh-user', value: await config.getSyncSshUser() },
43
+ { key: 'sync-ssh-host', value: await config.getSyncSshHost() }
44
+ ];
28
45
 
29
46
  logger.log('\n🔧 Developer Configuration\n');
30
47
  logger.log(`Developer ID: ${devId}`);
@@ -35,22 +52,18 @@ async function displayDevConfig(devId) {
35
52
  logger.log(` pgAdmin: ${ports.pgadmin}`);
36
53
  logger.log(` Redis Commander: ${ports.redisCommander}`);
37
54
 
38
- if (configVars.length > 0) {
39
- logger.log('\nConfiguration:');
40
- configVars.forEach(v => logger.log(` ${v.key}: ${v.value}`));
41
- }
55
+ logger.log('\nConfiguration:');
56
+ logger.log(` environment: '${environment}'`);
57
+ logger.log(controller ? ` controller: '${controller}'` : ' controller: (not set)');
58
+ optionalConfigVars.forEach(v => logger.log(` ${v.key}: ${v.value || '(not set)'}`));
42
59
  logger.log('');
43
60
  }
44
61
 
45
62
  /**
46
- * Sets up developer configuration commands
47
- * @param {Command} program - Commander program instance
63
+ * Register dev config and set-id commands.
64
+ * @param {Command} dev - dev subcommand group
48
65
  */
49
- function setupDevCommands(program) {
50
- const dev = program
51
- .command('dev')
52
- .description('Developer configuration and isolation');
53
-
66
+ function setupDevConfigCommands(dev) {
54
67
  dev
55
68
  .command('config')
56
69
  .description('Show or set developer configuration')
@@ -69,7 +82,6 @@ function setupDevCommands(program) {
69
82
  await displayDevConfig(setIdValue);
70
83
  return;
71
84
  }
72
-
73
85
  const devId = await config.getDeveloperId();
74
86
  await displayDevConfig(devId);
75
87
  } catch (error) {
@@ -98,4 +110,155 @@ function setupDevCommands(program) {
98
110
  });
99
111
  }
100
112
 
113
+ /**
114
+ * Register dev init and refresh commands.
115
+ * @param {Command} dev - dev subcommand group
116
+ */
117
+ function setupDevInitCommand(dev) {
118
+ dev
119
+ .command('init')
120
+ .description('Onboard with Builder Server (issue certificate, fetch settings, register SSH key for Mutagen)')
121
+ .requiredOption('--developer-id <id>', 'Developer ID (same as dev add; e.g. 01)')
122
+ .requiredOption('--server <url>', 'Builder Server base URL (e.g. https://dev.aifabrix.dev)')
123
+ .requiredOption('--pin <pin>', 'One-time PIN from your admin')
124
+ .action(async(options) => {
125
+ try {
126
+ await runDevInit(options);
127
+ } catch (error) {
128
+ handleCommandError(error, 'dev init');
129
+ process.exit(1);
130
+ }
131
+ });
132
+
133
+ dev
134
+ .command('refresh')
135
+ .description('Fetch settings from Builder Server and update config; refresh certificate if expiring within 14 days or --cert')
136
+ .option('--cert', 'Force certificate refresh (create PIN + issue-cert) even when cert is still valid')
137
+ .action(async(options) => {
138
+ try {
139
+ await runDevRefresh(options);
140
+ } catch (error) {
141
+ handleCommandError(error, 'dev refresh');
142
+ process.exit(1);
143
+ }
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Register dev list and add commands (remote only).
149
+ * @param {Command} dev - dev subcommand group
150
+ */
151
+ function setupDevListAddCommands(dev) {
152
+ dev
153
+ .command('list')
154
+ .description('List developer users (remote Builder Server only)')
155
+ .action(async() => {
156
+ try {
157
+ await handleDevList();
158
+ } catch (error) {
159
+ handleCommandError(error, 'dev list');
160
+ process.exit(1);
161
+ }
162
+ });
163
+
164
+ dev
165
+ .command('add')
166
+ .description('Register a new developer (remote Builder Server only; admin)')
167
+ .requiredOption('--developer-id <id>', 'Developer ID (unique, e.g. 01)')
168
+ .requiredOption('--name <name>', 'Display name')
169
+ .requiredOption('--email <email>', 'Email address')
170
+ .option('--groups <items>', 'Comma-separated groups (admin, secret-manager, developer)', 'developer')
171
+ .action(async(options) => {
172
+ try {
173
+ await handleDevAdd(options);
174
+ } catch (error) {
175
+ handleCommandError(error, 'dev add');
176
+ process.exit(1);
177
+ }
178
+ });
179
+ }
180
+
181
+ /**
182
+ * Register dev update/pin/delete commands (remote only).
183
+ * @param {Command} dev - dev subcommand group
184
+ */
185
+ function setupDevUpdatePinDeleteCommands(dev) {
186
+ dev
187
+ .command('update [developerId]')
188
+ .description('Update a developer (name, email, groups); use --developer-id like dev add')
189
+ .option('--developer-id <id>', 'Developer ID (same as dev add)')
190
+ .option('--name <name>', 'Display name')
191
+ .option('--email <email>', 'Email address')
192
+ .option('--groups <items>', 'Comma-separated groups (admin, secret-manager, developer)')
193
+ .action(async(developerId, options) => {
194
+ try {
195
+ await handleDevUpdate(developerId, options);
196
+ } catch (error) {
197
+ handleCommandError(error, 'dev update');
198
+ process.exit(1);
199
+ }
200
+ });
201
+
202
+ dev
203
+ .command('pin [developerId]')
204
+ .description('Create or regenerate one-time PIN for onboarding (admin; show once to developer)')
205
+ .action(async(developerId) => {
206
+ try {
207
+ await handleDevPin(developerId);
208
+ } catch (error) {
209
+ handleCommandError(error, 'dev pin');
210
+ process.exit(1);
211
+ }
212
+ });
213
+
214
+ dev
215
+ .command('delete <developerId>')
216
+ .description('Remove a developer (remote Builder Server only; admin)')
217
+ .action(async(developerId) => {
218
+ try {
219
+ await handleDevDelete(developerId);
220
+ } catch (error) {
221
+ handleCommandError(error, 'dev delete');
222
+ process.exit(1);
223
+ }
224
+ });
225
+
226
+ dev
227
+ .command('down')
228
+ .description('Stop Mutagen sync sessions for this developer (and optionally app containers)')
229
+ .option('--apps', 'Also stop running app containers for this developer')
230
+ .action(async(options) => {
231
+ try {
232
+ const { handleDevDown } = require('../commands/dev-down');
233
+ await handleDevDown(options);
234
+ } catch (error) {
235
+ handleCommandError(error, 'dev down');
236
+ process.exit(1);
237
+ }
238
+ });
239
+ }
240
+
241
+ /**
242
+ * Register dev list/add/update/pin/delete commands (remote only).
243
+ * @param {Command} dev - dev subcommand group
244
+ */
245
+ function setupDevUserCommands(dev) {
246
+ setupDevListAddCommands(dev);
247
+ setupDevUpdatePinDeleteCommands(dev);
248
+ }
249
+
250
+ /**
251
+ * Sets up developer configuration commands
252
+ * @param {Command} program - Commander program instance
253
+ */
254
+ function setupDevCommands(program) {
255
+ const dev = program
256
+ .command('dev')
257
+ .description('Developer configuration and isolation');
258
+
259
+ setupDevConfigCommands(dev);
260
+ setupDevInitCommand(dev);
261
+ setupDevUserCommands(dev);
262
+ }
263
+
101
264
  module.exports = { setupDevCommands };
@@ -30,7 +30,8 @@ function setupEnvironmentCommands(program) {
30
30
  environment
31
31
  .command('deploy <env>')
32
32
  .description('Deploy/setup environment in Miso Controller')
33
- .option('--config <file>', 'Environment configuration file')
33
+ .option('--config <file>', 'Environment configuration file (optional if --preset is used)')
34
+ .option('--preset <size>', 'Environment size preset: s, m, l, xl (default: s)', 's')
34
35
  .option('--skip-validation', 'Skip environment validation')
35
36
  .option('--poll', 'Poll for deployment status', true)
36
37
  .option('--no-poll', 'Do not poll for status')
@@ -43,7 +44,8 @@ function setupEnvironmentCommands(program) {
43
44
  env
44
45
  .command('deploy <env>')
45
46
  .description('Deploy/setup environment in Miso Controller')
46
- .option('--config <file>', 'Environment configuration file')
47
+ .option('--config <file>', 'Environment configuration file (optional if --preset is used)')
48
+ .option('--preset <size>', 'Environment size preset: s, m, l, xl (default: s)', 's')
47
49
  .option('--skip-validation', 'Skip environment validation')
48
50
  .option('--poll', 'Poll for deployment status', true)
49
51
  .option('--no-poll', 'Do not poll for status')
@@ -56,35 +56,85 @@ function setupDeleteCommand(program) {
56
56
  });
57
57
  }
58
58
 
59
+ /**
60
+ * Try to run builder-style test integration when app is not in builder or has no externalIntegration.
61
+ * @param {string} appName - App or system key
62
+ * @param {Object} options - CLI options
63
+ * @returns {Promise<boolean>} True if builder test was run, false otherwise
64
+ */
65
+ async function tryBuilderTestIntegration(appName, options) {
66
+ const fsSync = require('fs');
67
+ const pathsUtil = require('../utils/paths');
68
+ const { getIntegrationPath, getBuilderPath } = pathsUtil;
69
+ const { resolveApplicationConfigPath } = require('../utils/app-config-resolver');
70
+ const { loadConfigFile } = require('../utils/config-format');
71
+ const integrationPath = getIntegrationPath(appName);
72
+ let hasExternalIntegration = false;
73
+ try {
74
+ const integrationConfig = loadConfigFile(resolveApplicationConfigPath(integrationPath));
75
+ hasExternalIntegration = !!(integrationConfig && integrationConfig.externalIntegration);
76
+ } catch {
77
+ // integration path or config missing
78
+ }
79
+ if (!hasExternalIntegration) {
80
+ const builderPath = getBuilderPath(appName);
81
+ const builderConfigPath = resolveApplicationConfigPath(builderPath);
82
+ if (fsSync.existsSync(builderPath) && fsSync.existsSync(builderConfigPath)) {
83
+ const { runAppTestIntegration } = require('../commands/app-test');
84
+ const opts = { env: options.env || options.environment || 'dev' };
85
+ await runAppTestIntegration(appName, opts);
86
+ return true;
87
+ }
88
+ }
89
+ return false;
90
+ }
91
+
92
+ /**
93
+ * Run external system integration test via dataplane and exit on failure.
94
+ * @param {string} appName - App or system key
95
+ * @param {Object} options - CLI options
96
+ * @returns {Promise<void>}
97
+ */
98
+ async function runExternalSystemTestIntegration(appName, options) {
99
+ const test = require('../external-system/test');
100
+ const opts = { ...options, environment: options.env || options.environment };
101
+ const results = await test.testExternalSystemIntegration(appName, opts);
102
+ test.displayIntegrationTestResults(results, options.verbose);
103
+ if (!results.success) process.exit(1);
104
+ }
105
+
106
+ /**
107
+ * Run test-integration command: builder app in container or external system via dataplane.
108
+ * @param {string} appName - App or system key
109
+ * @param {Object} options - CLI options (datasource, payload, env, verbose, timeout)
110
+ * @returns {Promise<void>}
111
+ */
112
+ async function runTestIntegrationCommand(appName, options) {
113
+ const pathsUtil = require('../utils/paths');
114
+ const appType = await pathsUtil.detectAppType(appName).catch(() => null);
115
+ if (appType && appType.baseDir === 'builder') {
116
+ const { runAppTestIntegration } = require('../commands/app-test');
117
+ const opts = { env: options.env || options.environment || 'dev' };
118
+ await runAppTestIntegration(appName, opts);
119
+ return;
120
+ }
121
+ const ranBuilder = await tryBuilderTestIntegration(appName, options);
122
+ if (ranBuilder) return;
123
+ await runExternalSystemTestIntegration(appName, options);
124
+ }
125
+
59
126
  function setupExternalSystemTestCommands(program) {
60
- program.command('test <app>')
61
- .description('Run unit tests for external system (local validation, no API calls)')
62
- .option('-d, --datasource <key>', 'Test specific datasource only')
63
- .option('-v, --verbose', 'Show detailed validation output')
64
- .action(async(appName, options) => {
65
- try {
66
- const test = require('../external-system/test');
67
- const results = await test.testExternalSystem(appName, options);
68
- test.displayTestResults(results, options.verbose);
69
- if (!results.valid) process.exit(1);
70
- } catch (error) {
71
- handleCommandError(error, 'test');
72
- process.exit(1);
73
- }
74
- });
127
+ // 'test <app>' is registered in setup-app.js and dispatches by app type (builder vs external)
75
128
  program.command('test-integration <app>')
76
- .description('Run integration tests via dataplane pipeline API')
129
+ .description('Run integration tests (builder/docker app: in container; external system: via dataplane pipeline API)')
77
130
  .option('-d, --datasource <key>', 'Test specific datasource only')
78
131
  .option('-p, --payload <file>', 'Path to custom test payload file')
79
- .option('--dataplane <url>', 'Dataplane URL (default: discovered from controller)')
132
+ .option('-e, --env <env>', 'Environment: dev, tst, or pro (default: from aifabrix auth config)')
80
133
  .option('-v, --verbose', 'Show detailed test output')
81
134
  .option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
82
135
  .action(async(appName, options) => {
83
136
  try {
84
- const test = require('../external-system/test');
85
- const results = await test.testExternalSystemIntegration(appName, options);
86
- test.displayIntegrationTestResults(results, options.verbose);
87
- if (!results.success) process.exit(1);
137
+ await runTestIntegrationCommand(appName, options);
88
138
  } catch (error) {
89
139
  handleCommandError(error, 'test-integration');
90
140
  process.exit(1);
@@ -12,7 +12,9 @@ const appLib = require('../app');
12
12
  const validator = require('../validation/validator');
13
13
  const config = require('../core/config');
14
14
  const logger = require('../utils/logger');
15
- const { handleCommandError } = require('../utils/cli-utils');
15
+ const { handleCommandError, isAuthenticationError } = require('../utils/cli-utils');
16
+ const { resolveControllerUrl } = require('../utils/controller-url');
17
+ const { handleLogin } = require('../commands/login');
16
18
  const { handleUpMiso } = require('../commands/up-miso');
17
19
  const { handleUpDataplane } = require('../commands/up-dataplane');
18
20
 
@@ -45,13 +47,14 @@ async function runUpInfraCommand(options) {
45
47
  logger.log(chalk.green('✓ Traefik disabled and saved to config'));
46
48
  }
47
49
  const useTraefik = options.traefik === true ? true : (options.traefik === false ? false : !!(cfg.traefik));
48
- await infra.startInfra(developerId, { traefik: useTraefik });
50
+ await infra.startInfra(developerId, { traefik: useTraefik, adminPwd: options.adminPwd });
49
51
  }
50
52
 
51
53
  function setupUpInfraCommand(program) {
52
54
  program.command('up-infra')
53
55
  .description('Start local infrastructure: Postgres, Redis, optional Traefik')
54
56
  .option('-d, --developer <id>', 'Set developer ID and start infrastructure')
57
+ .option('--adminPwd <password>', 'Override default admin password for new install (Postgres, pgAdmin, Redis Commander)')
55
58
  .option('--traefik', 'Include Traefik reverse proxy and save to config')
56
59
  .option('--no-traefik', 'Exclude Traefik and save to config')
57
60
  .action(async(options) => {
@@ -75,6 +78,18 @@ function setupUpPlatformCommand(program) {
75
78
  await handleUpMiso(options);
76
79
  await handleUpDataplane(options);
77
80
  } catch (error) {
81
+ if (isAuthenticationError(error)) {
82
+ const controllerUrl = error.controllerUrl || await resolveControllerUrl();
83
+ logger.log(chalk.blue('\nAuthentication required. Running aifabrix login...\n'));
84
+ try {
85
+ await handleLogin({ method: 'device', controller: controllerUrl });
86
+ await handleUpDataplane(options);
87
+ return;
88
+ } catch (loginOrRetryError) {
89
+ handleCommandError(loginOrRetryError, 'up-platform');
90
+ process.exit(1);
91
+ }
92
+ }
78
93
  handleCommandError(error, 'up-platform');
79
94
  process.exit(1);
80
95
  }
@@ -107,6 +122,18 @@ function setupUpDataplaneCommand(program) {
107
122
  try {
108
123
  await handleUpDataplane(options);
109
124
  } catch (error) {
125
+ if (isAuthenticationError(error)) {
126
+ const controllerUrl = error.controllerUrl || await resolveControllerUrl();
127
+ logger.log(chalk.blue('\nAuthentication required. Running aifabrix login...\n'));
128
+ try {
129
+ await handleLogin({ method: 'device', controller: controllerUrl });
130
+ await handleUpDataplane(options);
131
+ return;
132
+ } catch (loginOrRetryError) {
133
+ handleCommandError(loginOrRetryError, 'up-dataplane');
134
+ process.exit(1);
135
+ }
136
+ }
110
137
  handleCommandError(error, 'up-dataplane');
111
138
  process.exit(1);
112
139
  }