@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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * CLI secrets and security command setup (secrets set, secure).
2
+ * CLI secret and security command setup (secret set, secure).
3
3
  *
4
4
  * @fileoverview Secrets command definitions for AI Fabrix Builder CLI
5
5
  * @author AI Fabrix Team
@@ -8,18 +8,50 @@
8
8
 
9
9
  const { handleCommandError } = require('../utils/cli-utils');
10
10
  const { handleSecretsSet } = require('../commands/secrets-set');
11
+ const { handleSecretsList } = require('../commands/secrets-list');
12
+ const { handleSecretsRemove } = require('../commands/secrets-remove');
13
+ const { handleSecretsValidate } = require('../commands/secrets-validate');
11
14
  const { handleSecure } = require('../commands/secure');
12
15
 
16
+ function setupSecretValidateCommand(secretCmd) {
17
+ secretCmd
18
+ .command('validate [path]')
19
+ .description('Validate secrets file (YAML structure and optional naming convention)')
20
+ .option('--naming', 'Check key names against *KeyVault convention')
21
+ .action(async(pathArg, options) => {
22
+ try {
23
+ const result = await handleSecretsValidate(pathArg, options);
24
+ if (!result.valid) process.exit(1);
25
+ } catch (error) {
26
+ handleCommandError(error, 'secret validate');
27
+ process.exit(1);
28
+ }
29
+ });
30
+ }
31
+
13
32
  /**
14
33
  * Sets up secrets and security commands
15
34
  * @param {Command} program - Commander program instance
16
35
  */
17
36
  function setupSecretsCommands(program) {
18
- const secretsCmd = program
19
- .command('secrets')
37
+ const secretCmd = program
38
+ .command('secret')
20
39
  .description('Manage secrets in secrets files');
21
40
 
22
- secretsCmd
41
+ secretCmd
42
+ .command('list')
43
+ .description('List secret keys (user or shared; use --shared for shared)')
44
+ .option('--shared', 'List shared secrets (from config aifabrix-secrets or remote API)')
45
+ .action(async(options) => {
46
+ try {
47
+ await handleSecretsList(options);
48
+ } catch (error) {
49
+ handleCommandError(error, 'secret list');
50
+ process.exit(1);
51
+ }
52
+ });
53
+
54
+ secretCmd
23
55
  .command('set <key> <value>')
24
56
  .description('Set a secret value in secrets file')
25
57
  .option('--shared', 'Save to general secrets file (from config.yaml aifabrix-secrets) instead of user secrets')
@@ -27,11 +59,26 @@ function setupSecretsCommands(program) {
27
59
  try {
28
60
  await handleSecretsSet(key, value, options);
29
61
  } catch (error) {
30
- handleCommandError(error, 'secrets set');
62
+ handleCommandError(error, 'secret set');
63
+ process.exit(1);
64
+ }
65
+ });
66
+
67
+ secretCmd
68
+ .command('remove <key>')
69
+ .description('Remove a secret by key')
70
+ .option('--shared', 'Remove from shared secrets (file or remote API)')
71
+ .action(async(key, options) => {
72
+ try {
73
+ await handleSecretsRemove(key, options);
74
+ } catch (error) {
75
+ handleCommandError(error, 'secret remove');
31
76
  process.exit(1);
32
77
  }
33
78
  });
34
79
 
80
+ setupSecretValidateCommand(secretCmd);
81
+
35
82
  program.command('secure')
36
83
  .description('Encrypt secrets in secrets.local.yaml files for ISO 27001 compliance')
37
84
  .option('--secrets-encryption <key>', 'Encryption key (32 bytes, hex or base64)')
@@ -13,7 +13,7 @@ const secrets = require('../core/secrets');
13
13
  const generator = require('../generator');
14
14
  const logger = require('../utils/logger');
15
15
  const { handleCommandError, logOfflinePathWhenType } = require('../utils/cli-utils');
16
- const { detectAppType, getDeployJsonPath } = require('../utils/paths');
16
+ const { detectAppType, getDeployJsonPath, getResolveAppPath } = require('../utils/paths');
17
17
 
18
18
  /**
19
19
  * Resolve app path and type for split-json (integration first, then builder).
@@ -89,9 +89,18 @@ function setupResolveCommand(program) {
89
89
  .option('--skip-validation', 'Skip file validation after generating .env')
90
90
  .action(async(appName, options) => {
91
91
  try {
92
- const envPath = await secrets.generateEnvFile(appName, undefined, 'docker', options.force);
92
+ const { appPath, envOnly } = await getResolveAppPath(appName);
93
+ const envPath = await secrets.generateEnvFile(
94
+ appName,
95
+ undefined,
96
+ 'docker',
97
+ options.force,
98
+ { appPath, envOnly, skipOutputPath: false, preserveFromPath: null }
99
+ );
93
100
  logger.log(`✓ Generated .env file: ${envPath}`);
94
- if (!options.skipValidation) {
101
+ if (envOnly) {
102
+ logger.log(chalk.gray(' (env-only mode: validation skipped; no application.yaml)'));
103
+ } else if (!options.skipValidation) {
95
104
  const validate = require('../validation/validate');
96
105
  const result = await validate.validateAppOrFile(appName);
97
106
  validate.displayValidationResults(result);
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Install command – run install (dependencies) inside app container (dev: running; tst: ephemeral with .env).
3
+ *
4
+ * @fileoverview App install command for builder apps (plan 73)
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const { spawn } = require('child_process');
10
+ const chalk = require('chalk');
11
+ const logger = require('../utils/logger');
12
+ const config = require('../core/config');
13
+ const containerHelpers = require('../utils/app-run-containers');
14
+ const composeGenerator = require('../utils/compose-generator');
15
+ const pathsUtil = require('../utils/paths');
16
+ const { loadConfigFile } = require('../utils/config-format');
17
+ const secretsEnvWrite = require('../core/secrets-env-write');
18
+
19
+ /**
20
+ * Load application config for builder app.
21
+ * @param {string} appName - Application name
22
+ * @returns {Object} Application config
23
+ */
24
+ function loadAppConfig(appName) {
25
+ const builderPath = pathsUtil.getBuilderPath(appName);
26
+ const configPath = pathsUtil.resolveApplicationConfigPath(builderPath);
27
+ return loadConfigFile(configPath);
28
+ }
29
+
30
+ /** Env set so install/test/lint use a writable temp dir in the container (e.g. when /app is read-only). */
31
+ const TMPDIR_VALUE = '/tmp';
32
+
33
+ /** pnpm store in /tmp when /app is read-only (avoids EACCES on _tmp_ files in project root). */
34
+ const PNPM_STORE_DIR = '/tmp/.pnpm-store';
35
+
36
+ /**
37
+ * Ensure install command can run when /app is read-only: use TMPDIR and pnpm --store-dir.
38
+ * @param {string} cmd - Raw install command (e.g. "pnpm install")
39
+ * @returns {string} Command (possibly with --store-dir for pnpm)
40
+ */
41
+ function installCommandForReadOnlyApp(cmd) {
42
+ const t = typeof cmd === 'string' ? cmd.trim() : '';
43
+ if (t === 'pnpm install' || t.startsWith('pnpm install ') ||
44
+ t === 'pnpm i' || t.startsWith('pnpm i ')) {
45
+ return t.includes('--store-dir') ? t : `${t} --store-dir ${PNPM_STORE_DIR}`;
46
+ }
47
+ return t;
48
+ }
49
+
50
+ /**
51
+ * Resolve install command from application config (language or build.scripts).
52
+ * @param {Object} appConfig - Application config
53
+ * @returns {string} Shell command to run install (e.g. "pnpm install" or "make install")
54
+ */
55
+ function getInstallCommand(appConfig) {
56
+ const build = appConfig.build;
57
+ const scripts = (build && build.scripts) || appConfig.scripts;
58
+ if (scripts && typeof scripts.install === 'string' && scripts.install.trim()) {
59
+ return scripts.install.trim();
60
+ }
61
+ const lang = (build && build.language || appConfig.language || 'typescript').toLowerCase();
62
+ return lang === 'python' ? 'make install' : 'pnpm install';
63
+ }
64
+
65
+ /**
66
+ * Run install in dev (exec in running container).
67
+ * Resolves app .env (including NPM_TOKEN/PYPI_TOKEN from kv://) and passes it so install has registry tokens.
68
+ * @param {string} appName - Application name
69
+ * @param {string|number} developerId - Developer ID
70
+ * @param {string} installCmd - Install command
71
+ * @returns {Promise<number>} Exit code
72
+ */
73
+ async function runInstallInDev(appName, developerId, installCmd) {
74
+ const containerName = containerHelpers.getContainerName(appName, developerId);
75
+ const isRunning = await containerHelpers.checkContainerRunning(appName, developerId);
76
+ if (!isRunning) {
77
+ throw new Error(
78
+ `Container ${containerName} is not running.\nRun 'aifabrix run ${appName}' first.`
79
+ );
80
+ }
81
+ logger.log(chalk.blue(`Running install in container ${containerName}: ${installCmd}\n`));
82
+ const cmd = installCommandForReadOnlyApp(installCmd);
83
+ const envFilePath = await secretsEnvWrite.resolveAndWriteEnvFile(appName, {});
84
+ return runDockerExec(containerName, cmd, envFilePath);
85
+ }
86
+
87
+ /**
88
+ * Run command in container via docker exec; stream output. Returns exit code.
89
+ * Uses -e TMPDIR, pnpm store, CI; when envFilePath is set passes --env-file so NPM_TOKEN/PYPI_TOKEN (from kv://) are available.
90
+ * @param {string} containerName - Container name
91
+ * @param {string} cmd - Shell command
92
+ * @param {string|null} [envFilePath] - Path to resolved .env for --env-file (optional; when set, install has registry tokens)
93
+ * @returns {Promise<number>} Exit code
94
+ */
95
+ function runDockerExec(containerName, cmd, envFilePath = null) {
96
+ return new Promise((resolve) => {
97
+ const args = [
98
+ 'exec',
99
+ '-e', `TMPDIR=${TMPDIR_VALUE}`,
100
+ '-e', `npm_config_store_dir=${PNPM_STORE_DIR}`,
101
+ '-e', 'CI=true'
102
+ ];
103
+ if (envFilePath) {
104
+ args.push('--env-file', envFilePath);
105
+ }
106
+ args.push(containerName, 'sh', '-c', cmd);
107
+ const proc = spawn('docker', args, {
108
+ stdio: 'inherit',
109
+ shell: false
110
+ });
111
+ proc.on('close', code => resolve(code !== null ? code : 1));
112
+ proc.on('error', () => resolve(1));
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Run command in ephemeral container with optional env file; stream output. Returns exit code.
118
+ * @param {string} fullImage - Image:tag
119
+ * @param {string} cmd - Shell command
120
+ * @param {string|null} [envFilePath] - Path to .env file for --env-file (optional)
121
+ * @returns {Promise<number>} Exit code
122
+ */
123
+ function runDockerRunEphemeral(fullImage, cmd, envFilePath = null) {
124
+ return new Promise((resolve) => {
125
+ const args = ['run', '--rm', '-e', `TMPDIR=${TMPDIR_VALUE}`, '-e', `npm_config_store_dir=${PNPM_STORE_DIR}`, '-e', 'CI=true'];
126
+ if (envFilePath) {
127
+ args.push('--env-file', envFilePath);
128
+ }
129
+ args.push(fullImage, 'sh', '-c', cmd);
130
+ const proc = spawn('docker', args, {
131
+ stdio: 'inherit',
132
+ shell: false
133
+ });
134
+ proc.on('close', code => resolve(code !== null ? code : 1));
135
+ proc.on('error', () => resolve(1));
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Run install for a builder app. DEV: exec in running container; TST: ephemeral with resolved .env.
141
+ * @param {string} appName - Application name
142
+ * @param {Object} [options] - { env: 'dev'|'tst' }
143
+ * @returns {Promise<void>}
144
+ */
145
+ async function runAppInstall(appName, options = {}) {
146
+ const env = (options.env || 'dev').toLowerCase();
147
+ if (env !== 'dev' && env !== 'tst') {
148
+ throw new Error('--env must be dev or tst');
149
+ }
150
+ const appConfig = loadAppConfig(appName);
151
+ const installCmd = getInstallCommand(appConfig);
152
+ const developerId = await config.getDeveloperId();
153
+ const imageName = composeGenerator.getImageName(appConfig, appName);
154
+ const imageTag = (appConfig.image && appConfig.image.tag) ? appConfig.image.tag : 'latest';
155
+ const fullImage = `${imageName}:${imageTag}`;
156
+
157
+ if (env === 'dev') {
158
+ const code = await runInstallInDev(appName, developerId, installCmd);
159
+ if (code !== 0) process.exit(code);
160
+ logger.log(chalk.green('✓ Install completed'));
161
+ return;
162
+ }
163
+
164
+ logger.log(chalk.blue(`Running install in ephemeral container (${fullImage}): ${installCmd}\n`));
165
+ const envFilePath = await secretsEnvWrite.resolveAndWriteEnvFile(appName, {});
166
+ const cmd = installCommandForReadOnlyApp(installCmd);
167
+ const code = await runDockerRunEphemeral(fullImage, cmd, envFilePath);
168
+ if (code !== 0) process.exit(code);
169
+ logger.log(chalk.green('✓ Install completed'));
170
+ }
171
+
172
+ module.exports = { runAppInstall, getInstallCommand };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Shell command – open an interactive shell in the app container (docker exec -it).
3
+ *
4
+ * @fileoverview App shell command implementation
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const { spawn } = require('child_process');
10
+ const chalk = require('chalk');
11
+ const logger = require('../utils/logger');
12
+ const config = require('../core/config');
13
+ const containerHelpers = require('../utils/app-run-containers');
14
+ const pathsUtil = require('../utils/paths');
15
+ const secretsEnvWrite = require('../core/secrets-env-write');
16
+
17
+ /**
18
+ * Load application config for builder app (used to ensure app exists).
19
+ * @param {string} appName - Application name
20
+ * @returns {Object} Application config
21
+ */
22
+ function loadAppConfig(appName) {
23
+ const { loadConfigFile } = require('../utils/config-format');
24
+ const builderPath = pathsUtil.getBuilderPath(appName);
25
+ const configPath = pathsUtil.resolveApplicationConfigPath(builderPath);
26
+ return loadConfigFile(configPath);
27
+ }
28
+
29
+ /**
30
+ * Run interactive shell in the application container. Uses sh.
31
+ * Resolves app .env (including NPM_TOKEN/PYPI_TOKEN from kv://) and passes it via --env-file so the shell has registry tokens.
32
+ * @param {string} appName - Application name
33
+ * @param {Object} [options] - Options (e.g. --env for future remote)
34
+ * @returns {Promise<void>} Resolves when shell exits
35
+ */
36
+ async function runAppShell(appName, _options = {}) {
37
+ loadAppConfig(appName);
38
+
39
+ const developerId = await config.getDeveloperId();
40
+ const containerName = containerHelpers.getContainerName(appName, developerId);
41
+ const isRunning = await containerHelpers.checkContainerRunning(appName, developerId);
42
+
43
+ if (!isRunning) {
44
+ throw new Error(
45
+ `Container ${containerName} is not running.\nRun 'aifabrix run ${appName}' first.`
46
+ );
47
+ }
48
+
49
+ logger.log(chalk.blue(`Opening shell in ${containerName} (exit with 'exit' or Ctrl+D)...\n`));
50
+
51
+ const envFilePath = await secretsEnvWrite.resolveAndWriteEnvFile(appName, {});
52
+ const proc = spawn('docker', [
53
+ 'exec', '-it',
54
+ '--env-file', envFilePath,
55
+ '-e', 'TMPDIR=/tmp',
56
+ containerName,
57
+ 'sh'
58
+ ], {
59
+ stdio: 'inherit',
60
+ shell: false
61
+ });
62
+
63
+ return new Promise((resolve, reject) => {
64
+ proc.on('close', code => {
65
+ if (code !== 0 && code !== null) {
66
+ reject(new Error(`Shell exited with code ${code}`));
67
+ } else {
68
+ resolve();
69
+ }
70
+ });
71
+ proc.on('error', err => reject(err));
72
+ });
73
+ }
74
+
75
+ module.exports = { runAppShell };
@@ -0,0 +1,282 @@
1
+ /**
2
+ * Test command – run tests inside app container (dev: running container; tst: ephemeral).
3
+ *
4
+ * @fileoverview App test command for builder apps (plan 65: dev = in container, tst = ephemeral)
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const { spawn } = require('child_process');
10
+ const chalk = require('chalk');
11
+ const logger = require('../utils/logger');
12
+ const config = require('../core/config');
13
+ const containerHelpers = require('../utils/app-run-containers');
14
+ const composeGenerator = require('../utils/compose-generator');
15
+ const pathsUtil = require('../utils/paths');
16
+ const { loadConfigFile } = require('../utils/config-format');
17
+ const secretsEnvWrite = require('../core/secrets-env-write');
18
+
19
+ /**
20
+ * Load application config for builder app.
21
+ * @param {string} appName - Application name
22
+ * @returns {Object} Application config
23
+ */
24
+ function loadAppConfig(appName) {
25
+ const builderPath = pathsUtil.getBuilderPath(appName);
26
+ const configPath = pathsUtil.resolveApplicationConfigPath(builderPath);
27
+ return loadConfigFile(configPath);
28
+ }
29
+
30
+ /**
31
+ * Resolve test command from application config (language or build.scripts).
32
+ * @param {Object} appConfig - Application config
33
+ * @returns {string} Shell command to run tests (e.g. "pnpm test" or "make test")
34
+ */
35
+ function getTestCommand(appConfig) {
36
+ const scripts = appConfig.build?.scripts || appConfig.scripts;
37
+ if (scripts && typeof scripts.test === 'string' && scripts.test.trim()) {
38
+ return scripts.test.trim();
39
+ }
40
+ const lang = (appConfig.build?.language || appConfig.language || 'typescript').toLowerCase();
41
+ return lang === 'python' ? 'make test' : 'pnpm test';
42
+ }
43
+
44
+ /**
45
+ * Resolve test:e2e command from application config.
46
+ * @param {Object} appConfig - Application config
47
+ * @returns {string} Shell command (e.g. "pnpm test:e2e" or "make test:e2e")
48
+ */
49
+ function getTestE2eCommand(appConfig) {
50
+ const scripts = appConfig.build?.scripts || appConfig.scripts;
51
+ const cmd = scripts?.['test:e2e'] || scripts?.testE2e;
52
+ if (typeof cmd === 'string' && cmd.trim()) return cmd.trim();
53
+ const lang = (appConfig.build?.language || appConfig.language || 'typescript').toLowerCase();
54
+ return lang === 'python' ? 'make test:e2e' : 'pnpm test:e2e';
55
+ }
56
+
57
+ /**
58
+ * Resolve test:integration command from application config (aifabrix test-integration <app>).
59
+ * Defaults to test:e2e when not set so builder apps can run integration-style tests.
60
+ * @param {Object} appConfig - Application config
61
+ * @returns {string} Shell command (e.g. "pnpm test:integration" or "make test-integration")
62
+ */
63
+ function getTestIntegrationCommand(appConfig) {
64
+ const scripts = appConfig.build?.scripts || appConfig.scripts;
65
+ const cmd = scripts?.['test:integration'] || scripts?.testIntegration;
66
+ if (typeof cmd === 'string' && cmd.trim()) return cmd.trim();
67
+ return getTestE2eCommand(appConfig);
68
+ }
69
+
70
+ /**
71
+ * Resolve lint command from application config.
72
+ * @param {Object} appConfig - Application config
73
+ * @returns {string} Shell command (e.g. "pnpm lint" or "make lint")
74
+ */
75
+ function getLintCommand(appConfig) {
76
+ const scripts = appConfig.build?.scripts || appConfig.scripts;
77
+ if (scripts && typeof scripts.lint === 'string' && scripts.lint.trim()) {
78
+ return scripts.lint.trim();
79
+ }
80
+ const lang = (appConfig.build?.language || appConfig.language || 'typescript').toLowerCase();
81
+ return lang === 'python' ? 'make lint' : 'pnpm lint';
82
+ }
83
+
84
+ /**
85
+ * Run tests in dev (exec in running container).
86
+ * Resolves app .env (including NPM_TOKEN/PYPI_TOKEN) and passes it so tests have registry tokens.
87
+ * @param {string} appName - Application name
88
+ * @param {string|number} developerId - Developer ID
89
+ * @param {string} testCmd - Test command
90
+ * @returns {Promise<number>} Exit code
91
+ */
92
+ async function runTestsInDev(appName, developerId, testCmd) {
93
+ const containerName = containerHelpers.getContainerName(appName, developerId);
94
+ const isRunning = await containerHelpers.checkContainerRunning(appName, developerId);
95
+ if (!isRunning) {
96
+ throw new Error(
97
+ `Container ${containerName} is not running.\nRun 'aifabrix run ${appName}' first.`
98
+ );
99
+ }
100
+ logger.log(chalk.blue(`Running tests in container ${containerName}: ${testCmd}\n`));
101
+ const envFilePath = await secretsEnvWrite.resolveAndWriteEnvFile(appName, {});
102
+ return runDockerExec(containerName, testCmd, envFilePath);
103
+ }
104
+
105
+ /**
106
+ * Run tests for a builder app. DEV: exec in running container; TST: ephemeral container.
107
+ * @param {string} appName - Application name
108
+ * @param {Object} [options] - { env: 'dev'|'tst' }
109
+ * @returns {Promise<void>}
110
+ */
111
+ async function runAppTest(appName, options = {}) {
112
+ const env = (options.env || 'dev').toLowerCase();
113
+ if (env !== 'dev' && env !== 'tst') {
114
+ throw new Error('--env must be dev or tst');
115
+ }
116
+ const appConfig = loadAppConfig(appName);
117
+ const testCmd = getTestCommand(appConfig);
118
+ const developerId = await config.getDeveloperId();
119
+ const imageName = composeGenerator.getImageName(appConfig, appName);
120
+ const imageTag = appConfig.image?.tag || 'latest';
121
+ const fullImage = `${imageName}:${imageTag}`;
122
+
123
+ if (env === 'dev') {
124
+ const code = await runTestsInDev(appName, developerId, testCmd);
125
+ if (code !== 0) process.exit(code);
126
+ logger.log(chalk.green('✓ Tests completed'));
127
+ return;
128
+ }
129
+ logger.log(chalk.blue(`Running tests in ephemeral container (${fullImage}): ${testCmd}\n`));
130
+ const envFilePath = await secretsEnvWrite.resolveAndWriteEnvFile(appName, {});
131
+ const code = await runDockerRunEphemeral(fullImage, testCmd, envFilePath);
132
+ if (code !== 0) process.exit(code);
133
+ logger.log(chalk.green('✓ Tests completed'));
134
+ }
135
+
136
+ /**
137
+ * Run command in container via docker exec; stream output. Returns exit code.
138
+ * When envFilePath is set, passes --env-file so NPM_TOKEN/PYPI_TOKEN (from kv://) are available.
139
+ * @param {string} containerName - Container name
140
+ * @param {string} testCmd - Shell command
141
+ * @param {string|null} [envFilePath] - Path to resolved .env for --env-file (optional)
142
+ * @returns {Promise<number>} Exit code
143
+ */
144
+ function runDockerExec(containerName, testCmd, envFilePath = null) {
145
+ return new Promise((resolve) => {
146
+ const args = ['exec'];
147
+ if (envFilePath) args.push('--env-file', envFilePath);
148
+ args.push(containerName, 'sh', '-c', testCmd);
149
+ const proc = spawn('docker', args, {
150
+ stdio: 'inherit',
151
+ shell: false
152
+ });
153
+ proc.on('close', code => resolve(code !== null ? code : 1));
154
+ proc.on('error', () => resolve(1));
155
+ });
156
+ }
157
+
158
+ /**
159
+ * Run command in ephemeral container; optional --env-file. Returns exit code.
160
+ * @param {string} fullImage - Image:tag
161
+ * @param {string} testCmd - Shell command
162
+ * @param {string|null} [envFilePath] - Path to .env for docker run --env-file (optional)
163
+ * @returns {Promise<number>} Exit code
164
+ */
165
+ function runDockerRunEphemeral(fullImage, testCmd, envFilePath = null) {
166
+ return new Promise((resolve) => {
167
+ const args = ['run', '--rm'];
168
+ if (envFilePath) args.push('--env-file', envFilePath);
169
+ args.push(fullImage, 'sh', '-c', testCmd);
170
+ const proc = spawn('docker', args, {
171
+ stdio: 'inherit',
172
+ shell: false
173
+ });
174
+ proc.on('close', code => resolve(code !== null ? code : 1));
175
+ proc.on('error', () => resolve(1));
176
+ });
177
+ }
178
+
179
+ /**
180
+ * Run test-e2e for a builder app. DEV: exec in running container; TST: ephemeral with .env.
181
+ * @param {string} appName - Application name
182
+ * @param {Object} [options] - { env: 'dev'|'tst' }
183
+ * @returns {Promise<void>}
184
+ */
185
+ async function runAppTestE2e(appName, options = {}) {
186
+ const env = (options.env || 'dev').toLowerCase();
187
+ if (env !== 'dev' && env !== 'tst') {
188
+ throw new Error('--env must be dev or tst');
189
+ }
190
+ const appConfig = loadAppConfig(appName);
191
+ const cmd = getTestE2eCommand(appConfig);
192
+ const developerId = await config.getDeveloperId();
193
+ const imageName = composeGenerator.getImageName(appConfig, appName);
194
+ const imageTag = appConfig.image?.tag || 'latest';
195
+ const fullImage = `${imageName}:${imageTag}`;
196
+
197
+ if (env === 'dev') {
198
+ const code = await runTestsInDev(appName, developerId, cmd);
199
+ if (code !== 0) process.exit(code);
200
+ logger.log(chalk.green('✓ Test-e2e completed'));
201
+ return;
202
+ }
203
+ logger.log(chalk.blue(`Running test-e2e in ephemeral container (${fullImage}): ${cmd}\n`));
204
+ const envFilePath = await secretsEnvWrite.resolveAndWriteEnvFile(appName, {});
205
+ const code = await runDockerRunEphemeral(fullImage, cmd, envFilePath);
206
+ if (code !== 0) process.exit(code);
207
+ logger.log(chalk.green('✓ Test-e2e completed'));
208
+ }
209
+
210
+ /**
211
+ * Run test-integration for a builder app (integration tests in container). DEV: exec in running container; TST: ephemeral with .env.
212
+ * Uses build.scripts.testIntegration or test:integration; falls back to test:e2e when not set.
213
+ * @param {string} appName - Application name
214
+ * @param {Object} [options] - { env: 'dev'|'tst' }
215
+ * @returns {Promise<void>}
216
+ */
217
+ async function runAppTestIntegration(appName, options = {}) {
218
+ const env = (options.env || 'dev').toLowerCase();
219
+ if (env !== 'dev' && env !== 'tst') {
220
+ throw new Error('--env must be dev or tst');
221
+ }
222
+ const appConfig = loadAppConfig(appName);
223
+ const cmd = getTestIntegrationCommand(appConfig);
224
+ const developerId = await config.getDeveloperId();
225
+ const imageName = composeGenerator.getImageName(appConfig, appName);
226
+ const imageTag = appConfig.image?.tag || 'latest';
227
+ const fullImage = `${imageName}:${imageTag}`;
228
+
229
+ if (env === 'dev') {
230
+ const code = await runTestsInDev(appName, developerId, cmd);
231
+ if (code !== 0) process.exit(code);
232
+ logger.log(chalk.green('✓ Test-integration completed'));
233
+ return;
234
+ }
235
+ logger.log(chalk.blue(`Running test-integration in ephemeral container (${fullImage}): ${cmd}\n`));
236
+ const envFilePath = await secretsEnvWrite.resolveAndWriteEnvFile(appName, {});
237
+ const code = await runDockerRunEphemeral(fullImage, cmd, envFilePath);
238
+ if (code !== 0) process.exit(code);
239
+ logger.log(chalk.green('✓ Test-integration completed'));
240
+ }
241
+
242
+ /**
243
+ * Run lint for a builder app. DEV: exec in running container; TST: ephemeral with .env.
244
+ * @param {string} appName - Application name
245
+ * @param {Object} [options] - { env: 'dev'|'tst' }
246
+ * @returns {Promise<void>}
247
+ */
248
+ async function runAppLint(appName, options = {}) {
249
+ const env = (options.env || 'dev').toLowerCase();
250
+ if (env !== 'dev' && env !== 'tst') {
251
+ throw new Error('--env must be dev or tst');
252
+ }
253
+ const appConfig = loadAppConfig(appName);
254
+ const cmd = getLintCommand(appConfig);
255
+ const developerId = await config.getDeveloperId();
256
+ const imageName = composeGenerator.getImageName(appConfig, appName);
257
+ const imageTag = appConfig.image?.tag || 'latest';
258
+ const fullImage = `${imageName}:${imageTag}`;
259
+
260
+ if (env === 'dev') {
261
+ const code = await runTestsInDev(appName, developerId, cmd);
262
+ if (code !== 0) process.exit(code);
263
+ logger.log(chalk.green('✓ Lint completed'));
264
+ return;
265
+ }
266
+ logger.log(chalk.blue(`Running lint in ephemeral container (${fullImage}): ${cmd}\n`));
267
+ const envFilePath = await secretsEnvWrite.resolveAndWriteEnvFile(appName, {});
268
+ const code = await runDockerRunEphemeral(fullImage, cmd, envFilePath);
269
+ if (code !== 0) process.exit(code);
270
+ logger.log(chalk.green('✓ Lint completed'));
271
+ }
272
+
273
+ module.exports = {
274
+ runAppTest,
275
+ getTestCommand,
276
+ getTestE2eCommand,
277
+ getTestIntegrationCommand,
278
+ getLintCommand,
279
+ runAppTestE2e,
280
+ runAppTestIntegration,
281
+ runAppLint
282
+ };
@@ -21,7 +21,7 @@ function setupAppRegisterCommand(app) {
21
21
  app.command('register <appKey>')
22
22
  .description('Register application and get pipeline credentials')
23
23
  .option('-p, --port <port>', 'Application port (default: from application.yaml)')
24
- .option('-u, --url <url>', 'Application URL. If omitted: app.url, deployment.dataplaneUrl or deployment.appUrl in application.yaml; else http://localhost:{build.localPort or port}')
24
+ .option('-u, --url <url>', 'Application URL. If omitted: app.url, deployment.dataplaneUrl or deployment.appUrl in application.yaml; else http://localhost:{port})')
25
25
  .option('-n, --name <name>', 'Override display name')
26
26
  .option('-d, --description <desc>', 'Override description')
27
27
  .action(async(appKey, options) => {