@aifabrix/builder 2.36.2 → 2.37.5

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 (43) hide show
  1. package/.cursor/rules/project-rules.mdc +19 -0
  2. package/README.md +68 -104
  3. package/integration/hubspot/test.js +1 -1
  4. package/lib/api/wizard.api.js +24 -1
  5. package/lib/app/deploy.js +43 -7
  6. package/lib/app/display.js +1 -1
  7. package/lib/app/list.js +3 -1
  8. package/lib/app/run-helpers.js +1 -1
  9. package/lib/build/index.js +3 -4
  10. package/lib/cli/index.js +45 -0
  11. package/lib/cli/setup-app.js +230 -0
  12. package/lib/cli/setup-auth.js +88 -0
  13. package/lib/cli/setup-dev.js +101 -0
  14. package/lib/cli/setup-environment.js +53 -0
  15. package/lib/cli/setup-external-system.js +87 -0
  16. package/lib/cli/setup-infra.js +219 -0
  17. package/lib/cli/setup-secrets.js +48 -0
  18. package/lib/cli/setup-utility.js +202 -0
  19. package/lib/cli.js +7 -961
  20. package/lib/commands/up-common.js +31 -1
  21. package/lib/commands/up-miso.js +6 -2
  22. package/lib/commands/wizard-core.js +32 -7
  23. package/lib/core/config.js +10 -0
  24. package/lib/core/ensure-encryption-key.js +56 -0
  25. package/lib/deployment/deployer-status.js +101 -0
  26. package/lib/deployment/deployer.js +62 -110
  27. package/lib/deployment/environment.js +133 -34
  28. package/lib/external-system/deploy.js +5 -1
  29. package/lib/external-system/test-auth.js +14 -7
  30. package/lib/generator/wizard.js +37 -41
  31. package/lib/infrastructure/helpers.js +1 -1
  32. package/lib/schema/environment-deploy-request.schema.json +64 -0
  33. package/lib/utils/help-builder.js +5 -2
  34. package/lib/utils/paths.js +22 -4
  35. package/lib/utils/secrets-generator.js +23 -8
  36. package/lib/utils/secrets-helpers.js +46 -21
  37. package/package.json +1 -1
  38. package/scripts/install-local.js +11 -2
  39. package/templates/applications/README.md.hbs +3 -3
  40. package/templates/applications/dataplane/variables.yaml +0 -2
  41. package/templates/applications/miso-controller/variables.yaml +0 -2
  42. package/templates/external-system/deploy.js.hbs +69 -0
  43. package/templates/infra/environment-dev.json +10 -0
@@ -0,0 +1,230 @@
1
+ /**
2
+ * CLI application lifecycle command setup (create, wizard, build, run, push, deploy, dockerfile).
3
+ *
4
+ * @fileoverview Application command definitions for AI Fabrix Builder CLI
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const chalk = require('chalk');
10
+ const path = require('path');
11
+ const app = require('../app');
12
+ const logger = require('../utils/logger');
13
+ const { handleCommandError } = require('../utils/cli-utils');
14
+
15
+ /**
16
+ * Normalize options for external system creation
17
+ * @param {Object} options - Raw CLI options
18
+ * @returns {Object} Normalized options
19
+ */
20
+ function normalizeExternalOptions(options) {
21
+ const normalized = { ...options };
22
+ if (options.displayName) normalized.systemDisplayName = options.displayName;
23
+ if (options.description) normalized.systemDescription = options.description;
24
+ if (options.systemType) normalized.systemType = options.systemType;
25
+ if (options.authType) normalized.authType = options.authType;
26
+ if (options.datasources !== undefined) {
27
+ const parsedCount = parseInt(options.datasources, 10);
28
+ if (Number.isNaN(parsedCount) || parsedCount < 1 || parsedCount > 10) {
29
+ throw new Error('Datasources count must be a number between 1 and 10');
30
+ }
31
+ normalized.datasourceCount = parsedCount;
32
+ }
33
+ if (options.controller) {
34
+ normalized.controller = true;
35
+ normalized.controllerUrl = options.controller;
36
+ }
37
+ return normalized;
38
+ }
39
+
40
+ /**
41
+ * Validate required options for non-interactive external creation
42
+ * @param {Object} normalizedOptions - Normalized options
43
+ * @throws {Error} If required options are missing
44
+ */
45
+ function validateNonInteractiveExternalOptions(normalizedOptions) {
46
+ const missing = [];
47
+ if (!normalizedOptions.systemDisplayName) missing.push('--display-name');
48
+ if (!normalizedOptions.systemDescription) missing.push('--description');
49
+ if (!normalizedOptions.systemType) missing.push('--system-type');
50
+ if (!normalizedOptions.authType) missing.push('--auth-type');
51
+ if (!normalizedOptions.datasourceCount) missing.push('--datasources');
52
+ if (missing.length > 0) {
53
+ throw new Error(`Missing required options for non-interactive external create: ${missing.join(', ')}`);
54
+ }
55
+ if (!Object.prototype.hasOwnProperty.call(normalizedOptions, 'github')) {
56
+ normalizedOptions.github = false;
57
+ }
58
+ if (!Object.prototype.hasOwnProperty.call(normalizedOptions, 'controller')) {
59
+ normalizedOptions.controller = false;
60
+ }
61
+ }
62
+
63
+ /**
64
+ * Handle create command execution
65
+ * @async
66
+ * @param {string} appName - Application name
67
+ * @param {Object} options - CLI options
68
+ */
69
+ async function handleCreateCommand(appName, options) {
70
+ const validTypes = ['webapp', 'api', 'service', 'functionapp', 'external'];
71
+ if (options.type && !validTypes.includes(options.type)) {
72
+ throw new Error(`Invalid type: ${options.type}. Must be one of: ${validTypes.join(', ')}`);
73
+ }
74
+
75
+ const wizardOptions = { app: appName, ...options };
76
+ const normalizedOptions = normalizeExternalOptions(options);
77
+
78
+ const isExternalType = options.type === 'external';
79
+ const isNonInteractive = process.stdin && process.stdin.isTTY === false;
80
+
81
+ if (isExternalType && !options.wizard && isNonInteractive) {
82
+ validateNonInteractiveExternalOptions(normalizedOptions);
83
+ }
84
+
85
+ const shouldUseWizard = options.wizard && (options.type === 'external' || (!options.type && validTypes.includes('external')));
86
+ if (shouldUseWizard) {
87
+ const { handleWizard } = require('../commands/wizard');
88
+ await handleWizard(wizardOptions);
89
+ } else {
90
+ await app.createApp(appName, normalizedOptions);
91
+ }
92
+ }
93
+
94
+ /**
95
+ * Sets up application lifecycle commands
96
+ * @param {Command} program - Commander program instance
97
+ */
98
+ function setupAppCommands(program) {
99
+ program.command('create <app>')
100
+ .description('Create new application with configuration files')
101
+ .option('-p, --port <port>', 'Application port', '3000')
102
+ .option('-d, --database', 'Requires database')
103
+ .option('-r, --redis', 'Requires Redis')
104
+ .option('-s, --storage', 'Requires file storage')
105
+ .option('-a, --authentication', 'Requires authentication/RBAC')
106
+ .option('-l, --language <lang>', 'Runtime language (typescript/python)')
107
+ .option('-t, --template <name>', 'Template to use (e.g., miso-controller, keycloak)')
108
+ .option('--type <type>', 'Application type (webapp, api, service, functionapp, external)', 'webapp')
109
+ .option('--app', 'Generate minimal application files (package.json, index.ts or requirements.txt, main.py)')
110
+ .option('-g, --github', 'Generate GitHub Actions workflows')
111
+ .option('--github-steps <steps>', 'Extra GitHub workflow steps (comma-separated, e.g., npm,test)')
112
+ .option('--main-branch <branch>', 'Main branch name for workflows', 'main')
113
+ .option('--wizard', 'Use interactive wizard for external system creation')
114
+ .option('--display-name <name>', 'External system display name')
115
+ .option('--description <desc>', 'External system description')
116
+ .option('--system-type <type>', 'External system type (openapi, mcp, custom)')
117
+ .option('--auth-type <type>', 'External system auth type (oauth2, apikey, basic)')
118
+ .option('--datasources <count>', 'Number of datasources to create')
119
+ .action(async(appName, options) => {
120
+ try {
121
+ await handleCreateCommand(appName, options);
122
+ } catch (error) {
123
+ handleCommandError(error, 'create');
124
+ process.exit(1);
125
+ }
126
+ });
127
+
128
+ program.command('wizard [appName]')
129
+ .description('Create or extend external systems (OpenAPI, MCP, or known platforms like HubSpot) via guided steps or a config file')
130
+ .option('-a, --app <app>', 'Application name (synonym for positional appName)')
131
+ .option('--config <file>', 'Run headless using a wizard.yaml file (appName, mode, source, credential, preferences)')
132
+ .option('--silent', 'Run with saved integration/<app>/wizard.yaml only; no prompts (requires app name and existing wizard.yaml)')
133
+ .addHelpText('after', `
134
+ Examples:
135
+ $ aifabrix wizard Run interactively (mode first, then prompts)
136
+ $ aifabrix wizard my-integration Load wizard.yaml if present → show summary → "Run with saved config?" or start from step 1
137
+ $ aifabrix wizard my-integration --silent Run headless with integration/my-integration/wizard.yaml (no prompts)
138
+ $ aifabrix wizard -a my-integration Same as above (app name set)
139
+ $ aifabrix wizard --config wizard.yaml Run headless from a wizard config file
140
+
141
+ Config path: When appName is provided, integration/<appName>/wizard.yaml is used for load/save and error.log.
142
+ To change settings after a run, edit that file and run "aifabrix wizard <app>" again.
143
+ Headless config must include: appName, mode (create-system|add-datasource), source (type + filePath/url/platform).
144
+ See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`)
145
+ .action(async(positionalAppName, options) => {
146
+ try {
147
+ const appName = positionalAppName || options.app;
148
+ const configPath = appName ? path.join(process.cwd(), 'integration', appName, 'wizard.yaml') : null;
149
+ const { handleWizard } = require('../commands/wizard');
150
+ await handleWizard({ ...options, app: appName, config: options.config, configPath });
151
+ } catch (error) {
152
+ handleCommandError(error, 'wizard');
153
+ process.exit(1);
154
+ }
155
+ });
156
+
157
+ program.command('build <app>')
158
+ .description('Build container image (auto-detects runtime)')
159
+ .option('-l, --language <lang>', 'Override language detection')
160
+ .option('-f, --force-template', 'Force rebuild from template')
161
+ .option('-t, --tag <tag>', 'Image tag (default: latest). Set image.tag in variables.yaml to match for deploy.')
162
+ .action(async(appName, options) => {
163
+ try {
164
+ const imageTag = await app.buildApp(appName, options);
165
+ logger.log(`✅ Built image: ${imageTag}`);
166
+ } catch (error) {
167
+ handleCommandError(error, 'build');
168
+ process.exit(1);
169
+ }
170
+ });
171
+
172
+ program.command('run <app>')
173
+ .description('Run application locally')
174
+ .option('-p, --port <port>', 'Override local port')
175
+ .option('-d, --debug', 'Enable debug output with detailed container information')
176
+ .action(async(appName, options) => {
177
+ try {
178
+ await app.runApp(appName, options);
179
+ } catch (error) {
180
+ handleCommandError(error, 'run');
181
+ process.exit(1);
182
+ }
183
+ });
184
+
185
+ program.command('push <app>')
186
+ .description('Push image to Azure Container Registry')
187
+ .option('-r, --registry <registry>', 'ACR registry URL (overrides variables.yaml)')
188
+ .option('-t, --tag <tag>', 'Image tag(s) - comma-separated for multiple (default: latest)')
189
+ .action(async(appName, options) => {
190
+ try {
191
+ await app.pushApp(appName, options);
192
+ } catch (error) {
193
+ handleCommandError(error, 'push');
194
+ process.exit(1);
195
+ }
196
+ });
197
+
198
+ program.command('deploy <app>')
199
+ .description('Deploy to Azure via Miso Controller')
200
+ .option('--type <type>', 'Application type: external to deploy from integration/<app> (no app register needed)')
201
+ .option('--client-id <id>', 'Client ID (overrides config)')
202
+ .option('--client-secret <secret>', 'Client Secret (overrides config)')
203
+ .option('--poll', 'Poll for deployment status', true)
204
+ .option('--no-poll', 'Do not poll for status')
205
+ .action(async(appName, options) => {
206
+ try {
207
+ await app.deployApp(appName, options);
208
+ } catch (error) {
209
+ handleCommandError(error, 'deploy');
210
+ process.exit(1);
211
+ }
212
+ });
213
+
214
+ program.command('dockerfile <app>')
215
+ .description('Generate Dockerfile for an application')
216
+ .option('-l, --language <lang>', 'Override language detection')
217
+ .option('-f, --force', 'Overwrite existing Dockerfile')
218
+ .action(async(appName, options) => {
219
+ try {
220
+ const dockerfilePath = await app.generateDockerfileForApp(appName, options);
221
+ logger.log(chalk.green('\n✅ Dockerfile generated successfully!'));
222
+ logger.log(chalk.gray(`Location: ${dockerfilePath}`));
223
+ } catch (error) {
224
+ handleCommandError(error, 'dockerfile');
225
+ process.exit(1);
226
+ }
227
+ });
228
+ }
229
+
230
+ module.exports = { setupAppCommands };
@@ -0,0 +1,88 @@
1
+ /**
2
+ * CLI authentication command setup (login, logout, auth status/config).
3
+ *
4
+ * @fileoverview Authentication command definitions for AI Fabrix Builder CLI
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const chalk = require('chalk');
10
+ const logger = require('../utils/logger');
11
+ const { handleCommandError } = require('../utils/cli-utils');
12
+ const { handleLogin } = require('../commands/login');
13
+ const { handleLogout } = require('../commands/logout');
14
+ const { handleAuthStatus } = require('../commands/auth-status');
15
+ const { handleAuthConfig } = require('../commands/auth-config');
16
+
17
+ /**
18
+ * Sets up authentication commands
19
+ * @param {Command} program - Commander program instance
20
+ */
21
+ function setupAuthCommands(program) {
22
+ program.command('login')
23
+ .description('Authenticate with Miso Controller')
24
+ .option('-c, --controller <url>', 'Controller URL (default: from config or developer ID, e.g. http://localhost:3000)')
25
+ .option('-m, --method <method>', 'Authentication method (device|credentials)', 'device')
26
+ .option('-a, --app <app>', 'Application name (required for credentials method, reads from secrets.local.yaml)')
27
+ .option('--client-id <id>', 'Client ID (for credentials method, overrides secrets.local.yaml)')
28
+ .option('--client-secret <secret>', 'Client Secret (for credentials method, overrides secrets.local.yaml)')
29
+ .option('-e, --environment <env>', 'Environment key (updates root-level environment in config.yaml, e.g., miso, dev, tst, pro)')
30
+ .option('--online', 'Request online-only token (excludes offline_access scope, device flow only)')
31
+ .option('--scope <scopes>', 'Custom OAuth2 scope string (device flow only, default: "openid profile email offline_access")')
32
+ .action(async(options) => {
33
+ try {
34
+ await handleLogin(options);
35
+ } catch (error) {
36
+ logger.error(chalk.red('\n❌ Login failed:'), error.message);
37
+ process.exit(1);
38
+ }
39
+ });
40
+
41
+ program.command('logout')
42
+ .description('Clear authentication tokens')
43
+ .option('-c, --controller <url>', 'Clear device tokens for specific controller')
44
+ .option('-e, --environment <env>', 'Clear client tokens for specific environment')
45
+ .option('-a, --app <app>', 'Clear client tokens for specific app (requires --environment)')
46
+ .action(async(options) => {
47
+ try {
48
+ await handleLogout(options);
49
+ } catch (error) {
50
+ handleCommandError(error, 'logout');
51
+ process.exit(1);
52
+ }
53
+ });
54
+
55
+ const authStatusHandler = async(options) => {
56
+ try {
57
+ await handleAuthStatus(options);
58
+ } catch (error) {
59
+ handleCommandError(error, 'auth status');
60
+ process.exit(1);
61
+ }
62
+ };
63
+
64
+ const auth = program
65
+ .command('auth')
66
+ .description('Authentication commands');
67
+
68
+ auth
69
+ .command('status')
70
+ .description('Display authentication status for current controller and environment')
71
+ .action(authStatusHandler);
72
+
73
+ auth
74
+ .command('config')
75
+ .description('Configure authentication settings (controller, environment)')
76
+ .option('--set-controller <url>', 'Set default controller URL')
77
+ .option('--set-environment <env>', 'Set default environment')
78
+ .action(async(options) => {
79
+ try {
80
+ await handleAuthConfig(options);
81
+ } catch (error) {
82
+ handleCommandError(error, 'auth config');
83
+ process.exit(1);
84
+ }
85
+ });
86
+ }
87
+
88
+ module.exports = { setupAuthCommands };
@@ -0,0 +1,101 @@
1
+ /**
2
+ * CLI developer configuration command setup (dev config, dev set-id).
3
+ *
4
+ * @fileoverview Developer command definitions for AI Fabrix Builder CLI
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const chalk = require('chalk');
10
+ const config = require('../core/config');
11
+ const devConfig = require('../utils/dev-config');
12
+ const logger = require('../utils/logger');
13
+ const { handleCommandError } = require('../utils/cli-utils');
14
+
15
+ /**
16
+ * Display developer configuration
17
+ * @param {string} devId - Developer ID
18
+ * @returns {Promise<void>}
19
+ */
20
+ async function displayDevConfig(devId) {
21
+ const devIdNum = parseInt(devId, 10);
22
+ const ports = devConfig.getDevPorts(devIdNum);
23
+ const configVars = [
24
+ { key: 'aifabrix-home', value: await config.getAifabrixHomeOverride() },
25
+ { key: 'aifabrix-secrets', value: await config.getAifabrixSecretsPath() },
26
+ { key: 'aifabrix-env-config', value: await config.getAifabrixEnvConfigPath() }
27
+ ].filter(v => v.value);
28
+
29
+ logger.log('\n🔧 Developer Configuration\n');
30
+ logger.log(`Developer ID: ${devId}`);
31
+ logger.log('\nPorts:');
32
+ logger.log(` App: ${ports.app}`);
33
+ logger.log(` Postgres: ${ports.postgres}`);
34
+ logger.log(` Redis: ${ports.redis}`);
35
+ logger.log(` pgAdmin: ${ports.pgadmin}`);
36
+ logger.log(` Redis Commander: ${ports.redisCommander}`);
37
+
38
+ if (configVars.length > 0) {
39
+ logger.log('\nConfiguration:');
40
+ configVars.forEach(v => logger.log(` ${v.key}: ${v.value}`));
41
+ }
42
+ logger.log('');
43
+ }
44
+
45
+ /**
46
+ * Sets up developer configuration commands
47
+ * @param {Command} program - Commander program instance
48
+ */
49
+ function setupDevCommands(program) {
50
+ const dev = program
51
+ .command('dev')
52
+ .description('Developer configuration and isolation');
53
+
54
+ dev
55
+ .command('config')
56
+ .description('Show or set developer configuration')
57
+ .option('--set-id <id>', 'Set developer ID')
58
+ .action(async(options) => {
59
+ try {
60
+ const setIdValue = options.setId || options['set-id'];
61
+ if (setIdValue) {
62
+ const digitsOnly = /^[0-9]+$/.test(setIdValue);
63
+ if (!digitsOnly) {
64
+ throw new Error('Developer ID must be a non-negative digit string (0 = default infra, > 0 = developer-specific)');
65
+ }
66
+ await config.setDeveloperId(setIdValue);
67
+ process.env.AIFABRIX_DEVELOPERID = setIdValue;
68
+ logger.log(chalk.green(`✓ Developer ID set to ${setIdValue}`));
69
+ await displayDevConfig(setIdValue);
70
+ return;
71
+ }
72
+
73
+ const devId = await config.getDeveloperId();
74
+ await displayDevConfig(devId);
75
+ } catch (error) {
76
+ handleCommandError(error, 'dev config');
77
+ process.exit(1);
78
+ }
79
+ });
80
+
81
+ dev
82
+ .command('set-id <id>')
83
+ .description('Set developer ID (convenience alias for "dev config --set-id")')
84
+ .action(async(id) => {
85
+ try {
86
+ const digitsOnly = /^[0-9]+$/.test(id);
87
+ if (!digitsOnly) {
88
+ throw new Error('Developer ID must be a non-negative digit string (0 = default infra, > 0 = developer-specific)');
89
+ }
90
+ await config.setDeveloperId(id);
91
+ process.env.AIFABRIX_DEVELOPERID = id;
92
+ logger.log(chalk.green(`✓ Developer ID set to ${id}`));
93
+ await displayDevConfig(id);
94
+ } catch (error) {
95
+ handleCommandError(error, 'dev set-id');
96
+ process.exit(1);
97
+ }
98
+ });
99
+ }
100
+
101
+ module.exports = { setupDevCommands };
@@ -0,0 +1,53 @@
1
+ /**
2
+ * CLI environment deployment command setup (environment deploy, env deploy).
3
+ *
4
+ * @fileoverview Environment command definitions for AI Fabrix Builder CLI
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const { handleCommandError } = require('../utils/cli-utils');
10
+
11
+ /**
12
+ * Sets up environment deployment commands
13
+ * @param {Command} program - Commander program instance
14
+ */
15
+ function setupEnvironmentCommands(program) {
16
+ const deployEnvHandler = async(envKey, options) => {
17
+ try {
18
+ const environmentDeploy = require('../deployment/environment');
19
+ await environmentDeploy.deployEnvironment(envKey, options);
20
+ } catch (error) {
21
+ handleCommandError(error, 'environment deploy');
22
+ process.exit(1);
23
+ }
24
+ };
25
+
26
+ const environment = program
27
+ .command('environment')
28
+ .description('Manage environments');
29
+
30
+ environment
31
+ .command('deploy <env>')
32
+ .description('Deploy/setup environment in Miso Controller')
33
+ .option('--config <file>', 'Environment configuration file')
34
+ .option('--skip-validation', 'Skip environment validation')
35
+ .option('--poll', 'Poll for deployment status', true)
36
+ .option('--no-poll', 'Do not poll for status')
37
+ .action(deployEnvHandler);
38
+
39
+ const env = program
40
+ .command('env')
41
+ .description('Environment management (alias for environment)');
42
+
43
+ env
44
+ .command('deploy <env>')
45
+ .description('Deploy/setup environment in Miso Controller')
46
+ .option('--config <file>', 'Environment configuration file')
47
+ .option('--skip-validation', 'Skip environment validation')
48
+ .option('--poll', 'Poll for deployment status', true)
49
+ .option('--no-poll', 'Do not poll for status')
50
+ .action(deployEnvHandler);
51
+ }
52
+
53
+ module.exports = { setupEnvironmentCommands };
@@ -0,0 +1,87 @@
1
+ /**
2
+ * CLI external system command setup (download, delete, test, test-integration).
3
+ *
4
+ * @fileoverview External system command definitions for AI Fabrix Builder CLI
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const { handleCommandError } = require('../utils/cli-utils');
10
+
11
+ /**
12
+ * Sets up external system commands
13
+ * @param {Command} program - Commander program instance
14
+ */
15
+ function setupExternalSystemCommands(program) {
16
+ program.command('download <system-key>')
17
+ .description('Download external system from dataplane to local development structure')
18
+ .option('--dry-run', 'Show what would be downloaded without actually downloading')
19
+ .action(async(systemKey, options) => {
20
+ try {
21
+ const download = require('../external-system/download');
22
+ await download.downloadExternalSystem(systemKey, options);
23
+ } catch (error) {
24
+ handleCommandError(error, 'download');
25
+ process.exit(1);
26
+ }
27
+ });
28
+
29
+ program.command('delete <system-key>')
30
+ .description('Delete external system from dataplane (also deletes all associated datasources)')
31
+ .option('--type <type>', 'Application type (external) - required for external systems')
32
+ .option('--yes', 'Skip confirmation prompt')
33
+ .option('--force', 'Skip confirmation prompt (alias for --yes)')
34
+ .action(async(systemKey, options) => {
35
+ try {
36
+ if (options.type !== 'external') {
37
+ throw new Error('Delete command for external systems requires --type external');
38
+ }
39
+ const externalDelete = require('../external-system/delete');
40
+ await externalDelete.deleteExternalSystem(systemKey, options);
41
+ } catch (error) {
42
+ handleCommandError(error, 'delete');
43
+ process.exit(1);
44
+ }
45
+ });
46
+
47
+ program.command('test <app>')
48
+ .description('Run unit tests for external system (local validation, no API calls)')
49
+ .option('-d, --datasource <key>', 'Test specific datasource only')
50
+ .option('-v, --verbose', 'Show detailed validation output')
51
+ .action(async(appName, options) => {
52
+ try {
53
+ const test = require('../external-system/test');
54
+ const results = await test.testExternalSystem(appName, options);
55
+ test.displayTestResults(results, options.verbose);
56
+ if (!results.valid) {
57
+ process.exit(1);
58
+ }
59
+ } catch (error) {
60
+ handleCommandError(error, 'test');
61
+ process.exit(1);
62
+ }
63
+ });
64
+
65
+ program.command('test-integration <app>')
66
+ .description('Run integration tests via dataplane pipeline API')
67
+ .option('-d, --datasource <key>', 'Test specific datasource only')
68
+ .option('-p, --payload <file>', 'Path to custom test payload file')
69
+ .option('--dataplane <url>', 'Dataplane URL (default: discovered from controller)')
70
+ .option('-v, --verbose', 'Show detailed test output')
71
+ .option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
72
+ .action(async(appName, options) => {
73
+ try {
74
+ const test = require('../external-system/test');
75
+ const results = await test.testExternalSystemIntegration(appName, options);
76
+ test.displayIntegrationTestResults(results, options.verbose);
77
+ if (!results.success) {
78
+ process.exit(1);
79
+ }
80
+ } catch (error) {
81
+ handleCommandError(error, 'test-integration');
82
+ process.exit(1);
83
+ }
84
+ });
85
+ }
86
+
87
+ module.exports = { setupExternalSystemCommands };