@aifabrix/builder 2.36.2 → 2.37.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.
- package/README.md +68 -104
- package/lib/app/display.js +1 -1
- package/lib/app/run-helpers.js +1 -1
- package/lib/cli/index.js +45 -0
- package/lib/cli/setup-app.js +229 -0
- package/lib/cli/setup-auth.js +88 -0
- package/lib/cli/setup-dev.js +101 -0
- package/lib/cli/setup-environment.js +53 -0
- package/lib/cli/setup-external-system.js +86 -0
- package/lib/cli/setup-infra.js +219 -0
- package/lib/cli/setup-secrets.js +48 -0
- package/lib/cli/setup-utility.js +202 -0
- package/lib/cli.js +7 -961
- package/lib/commands/up-miso.js +1 -1
- package/lib/core/config.js +10 -0
- package/lib/core/ensure-encryption-key.js +56 -0
- package/lib/generator/wizard.js +19 -34
- package/lib/infrastructure/helpers.js +1 -1
- package/lib/utils/help-builder.js +5 -2
- package/package.json +1 -1
- package/templates/applications/README.md.hbs +3 -3
- package/templates/applications/dataplane/variables.yaml +0 -2
- package/templates/applications/miso-controller/variables.yaml +0 -2
- package/templates/external-system/deploy.js.hbs +58 -0
package/lib/cli.js
CHANGED
|
@@ -1,972 +1,18 @@
|
|
|
1
|
-
/* eslint-disable max-lines */
|
|
2
1
|
/**
|
|
3
2
|
* AI Fabrix Builder CLI Command Definitions
|
|
4
3
|
*
|
|
5
|
-
* This module
|
|
6
|
-
*
|
|
4
|
+
* This module re-exports CLI setup from lib/cli/ to keep the public API
|
|
5
|
+
* at require('./lib/cli') unchanged. All command definitions live in lib/cli/.
|
|
7
6
|
*
|
|
8
|
-
* @fileoverview
|
|
7
|
+
* @fileoverview Re-export for AI Fabrix Builder CLI (lib/cli.js)
|
|
9
8
|
* @author AI Fabrix Team
|
|
10
9
|
* @version 2.0.0
|
|
11
10
|
*/
|
|
12
11
|
|
|
13
|
-
const
|
|
14
|
-
const app = require('./app');
|
|
15
|
-
const secrets = require('./core/secrets');
|
|
16
|
-
const generator = require('./generator');
|
|
17
|
-
const validator = require('./validation/validator');
|
|
18
|
-
const config = require('./core/config');
|
|
19
|
-
const devConfig = require('./utils/dev-config');
|
|
20
|
-
const chalk = require('chalk');
|
|
21
|
-
const path = require('path');
|
|
22
|
-
const logger = require('./utils/logger');
|
|
23
|
-
const { validateCommand, handleCommandError } = require('./utils/cli-utils');
|
|
24
|
-
const { handleLogin } = require('./commands/login');
|
|
25
|
-
const { handleLogout } = require('./commands/logout');
|
|
26
|
-
const { handleAuthStatus } = require('./commands/auth-status');
|
|
27
|
-
const { handleSecure } = require('./commands/secure');
|
|
28
|
-
const { handleSecretsSet } = require('./commands/secrets-set');
|
|
29
|
-
const { handleAuthConfig } = require('./commands/auth-config');
|
|
30
|
-
const { setupAppCommands: setupAppManagementCommands } = require('./commands/app');
|
|
31
|
-
const { setupDatasourceCommands } = require('./commands/datasource');
|
|
32
|
-
const { handleUpMiso } = require('./commands/up-miso');
|
|
33
|
-
const { handleUpDataplane } = require('./commands/up-dataplane');
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* Sets up authentication commands
|
|
37
|
-
* @param {Command} program - Commander program instance
|
|
38
|
-
*/
|
|
39
|
-
function setupAuthCommands(program) {
|
|
40
|
-
program.command('login')
|
|
41
|
-
.description('Authenticate with Miso Controller')
|
|
42
|
-
.option('-c, --controller <url>', 'Controller URL (default: from config or developer ID, e.g. http://localhost:3000)')
|
|
43
|
-
.option('-m, --method <method>', 'Authentication method (device|credentials)', 'device')
|
|
44
|
-
.option('-a, --app <app>', 'Application name (required for credentials method, reads from secrets.local.yaml)')
|
|
45
|
-
.option('--client-id <id>', 'Client ID (for credentials method, overrides secrets.local.yaml)')
|
|
46
|
-
.option('--client-secret <secret>', 'Client Secret (for credentials method, overrides secrets.local.yaml)')
|
|
47
|
-
.option('-e, --environment <env>', 'Environment key (updates root-level environment in config.yaml, e.g., miso, dev, tst, pro)')
|
|
48
|
-
.option('--online', 'Request online-only token (excludes offline_access scope, device flow only)')
|
|
49
|
-
.option('--scope <scopes>', 'Custom OAuth2 scope string (device flow only, default: "openid profile email offline_access")')
|
|
50
|
-
.action(async(options) => {
|
|
51
|
-
try {
|
|
52
|
-
await handleLogin(options);
|
|
53
|
-
} catch (error) {
|
|
54
|
-
logger.error(chalk.red('\n❌ Login failed:'), error.message);
|
|
55
|
-
process.exit(1);
|
|
56
|
-
}
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
program.command('logout')
|
|
60
|
-
.description('Clear authentication tokens')
|
|
61
|
-
.option('-c, --controller <url>', 'Clear device tokens for specific controller')
|
|
62
|
-
.option('-e, --environment <env>', 'Clear client tokens for specific environment')
|
|
63
|
-
.option('-a, --app <app>', 'Clear client tokens for specific app (requires --environment)')
|
|
64
|
-
.action(async(options) => {
|
|
65
|
-
try {
|
|
66
|
-
await handleLogout(options);
|
|
67
|
-
} catch (error) {
|
|
68
|
-
handleCommandError(error, 'logout');
|
|
69
|
-
process.exit(1);
|
|
70
|
-
}
|
|
71
|
-
});
|
|
72
|
-
|
|
73
|
-
const authStatusHandler = async(options) => {
|
|
74
|
-
try {
|
|
75
|
-
await handleAuthStatus(options);
|
|
76
|
-
} catch (error) {
|
|
77
|
-
handleCommandError(error, 'auth status');
|
|
78
|
-
process.exit(1);
|
|
79
|
-
}
|
|
80
|
-
};
|
|
81
|
-
|
|
82
|
-
// Use nested command pattern for multi-word commands (like environment deploy)
|
|
83
|
-
const auth = program
|
|
84
|
-
.command('auth')
|
|
85
|
-
.description('Authentication commands');
|
|
86
|
-
|
|
87
|
-
auth
|
|
88
|
-
.command('status')
|
|
89
|
-
.description('Display authentication status for current controller and environment')
|
|
90
|
-
.action(authStatusHandler);
|
|
91
|
-
|
|
92
|
-
auth
|
|
93
|
-
.command('config')
|
|
94
|
-
.description('Configure authentication settings (controller, environment)')
|
|
95
|
-
.option('--set-controller <url>', 'Set default controller URL')
|
|
96
|
-
.option('--set-environment <env>', 'Set default environment')
|
|
97
|
-
.action(async(options) => {
|
|
98
|
-
try {
|
|
99
|
-
await handleAuthConfig(options);
|
|
100
|
-
} catch (error) {
|
|
101
|
-
handleCommandError(error, 'auth config');
|
|
102
|
-
process.exit(1);
|
|
103
|
-
}
|
|
104
|
-
});
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Runs the up command: resolves developer ID, traefik, and starts infra.
|
|
109
|
-
* @param {Object} options - Commander options (developer, traefik)
|
|
110
|
-
* @returns {Promise<void>}
|
|
111
|
-
*/
|
|
112
|
-
async function runUpCommand(options) {
|
|
113
|
-
let developerId = null;
|
|
114
|
-
if (options.developer) {
|
|
115
|
-
const id = parseInt(options.developer, 10);
|
|
116
|
-
if (isNaN(id) || id < 0) {
|
|
117
|
-
throw new Error('Developer ID must be a non-negative number (0 = default infra, > 0 = developer-specific)');
|
|
118
|
-
}
|
|
119
|
-
await config.setDeveloperId(id);
|
|
120
|
-
process.env.AIFABRIX_DEVELOPERID = id.toString();
|
|
121
|
-
developerId = id;
|
|
122
|
-
logger.log(chalk.green(`✓ Developer ID set to ${id}`));
|
|
123
|
-
}
|
|
124
|
-
const cfg = await config.getConfig();
|
|
125
|
-
if (options.traefik === true) {
|
|
126
|
-
cfg.traefik = true;
|
|
127
|
-
await config.saveConfig(cfg);
|
|
128
|
-
logger.log(chalk.green('✓ Traefik enabled and saved to config'));
|
|
129
|
-
} else if (options.traefik === false) {
|
|
130
|
-
cfg.traefik = false;
|
|
131
|
-
await config.saveConfig(cfg);
|
|
132
|
-
logger.log(chalk.green('✓ Traefik disabled and saved to config'));
|
|
133
|
-
}
|
|
134
|
-
const useTraefik = options.traefik === true ? true : (options.traefik === false ? false : !!(cfg.traefik));
|
|
135
|
-
await infra.startInfra(developerId, { traefik: useTraefik });
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Sets up infrastructure commands
|
|
140
|
-
* @param {Command} program - Commander program instance
|
|
141
|
-
*/
|
|
142
|
-
function setupInfraCommands(program) {
|
|
143
|
-
program.command('up')
|
|
144
|
-
.description('Start local infrastructure services (Postgres, Redis, pgAdmin, Redis Commander)')
|
|
145
|
-
.option('-d, --developer <id>', 'Set developer ID and start infrastructure')
|
|
146
|
-
.option('--traefik', 'Include Traefik reverse proxy and save to config')
|
|
147
|
-
.option('--no-traefik', 'Exclude Traefik and save to config')
|
|
148
|
-
.action(async(options) => {
|
|
149
|
-
try {
|
|
150
|
-
await runUpCommand(options);
|
|
151
|
-
} catch (error) {
|
|
152
|
-
handleCommandError(error, 'up');
|
|
153
|
-
process.exit(1);
|
|
154
|
-
}
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
program.command('up-miso')
|
|
158
|
-
.description('Install keycloak, miso-controller, and dataplane from images (no build). Infra must be up. Uses auto-generated secrets for testing.')
|
|
159
|
-
.option('-r, --registry <url>', 'Override registry for all apps (e.g. myacr.azurecr.io)')
|
|
160
|
-
.option('--registry-mode <mode>', 'Override registry mode (acr|external)')
|
|
161
|
-
.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]))
|
|
162
|
-
.action(async(options) => {
|
|
163
|
-
try {
|
|
164
|
-
await handleUpMiso(options);
|
|
165
|
-
} catch (error) {
|
|
166
|
-
handleCommandError(error, 'up-miso');
|
|
167
|
-
process.exit(1);
|
|
168
|
-
}
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
program.command('up-dataplane')
|
|
172
|
-
.description('Register and deploy dataplane app in dev (requires login, environment must be dev)')
|
|
173
|
-
.option('-r, --registry <url>', 'Override registry for dataplane image')
|
|
174
|
-
.option('--registry-mode <mode>', 'Override registry mode (acr|external)')
|
|
175
|
-
.option('-i, --image <ref>', 'Override dataplane image reference (e.g. myreg/dataplane:latest)')
|
|
176
|
-
.action(async(options) => {
|
|
177
|
-
try {
|
|
178
|
-
await handleUpDataplane(options);
|
|
179
|
-
} catch (error) {
|
|
180
|
-
handleCommandError(error, 'up-dataplane');
|
|
181
|
-
process.exit(1);
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
program.command('down [app]')
|
|
186
|
-
.description('Stop and remove local infrastructure services or a specific application')
|
|
187
|
-
.option('-v, --volumes', 'Remove volumes (deletes all data)')
|
|
188
|
-
.action(async(appName, options) => {
|
|
189
|
-
try {
|
|
190
|
-
// If app name is provided, stop/remove that application (optionally volumes)
|
|
191
|
-
if (typeof appName === 'string' && appName.trim().length > 0) {
|
|
192
|
-
await app.downApp(appName, { volumes: !!options.volumes });
|
|
193
|
-
} else {
|
|
194
|
-
// Otherwise, stop/remove infrastructure
|
|
195
|
-
if (options.volumes) {
|
|
196
|
-
await infra.stopInfraWithVolumes();
|
|
197
|
-
} else {
|
|
198
|
-
await infra.stopInfra();
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
} catch (error) {
|
|
202
|
-
handleCommandError(error, 'down');
|
|
203
|
-
process.exit(1);
|
|
204
|
-
}
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
program.command('doctor')
|
|
208
|
-
.description('Check environment and configuration')
|
|
209
|
-
.action(async() => {
|
|
210
|
-
try {
|
|
211
|
-
const result = await validator.checkEnvironment();
|
|
212
|
-
logger.log('\n🔍 AI Fabrix Environment Check\n');
|
|
213
|
-
|
|
214
|
-
logger.log(`Docker: ${result.docker === 'ok' ? '✅ Running' : '❌ Not available'}`);
|
|
215
|
-
logger.log(`Ports: ${result.ports === 'ok' ? '✅ Available' : '⚠️ Some ports in use'}`);
|
|
216
|
-
logger.log(`Secrets: ${result.secrets === 'ok' ? '✅ Configured' : '❌ Missing'}`);
|
|
217
|
-
|
|
218
|
-
if (result.recommendations.length > 0) {
|
|
219
|
-
logger.log('\n📋 Recommendations:');
|
|
220
|
-
result.recommendations.forEach(rec => logger.log(` • ${rec}`));
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
// Check infrastructure health if Docker is available
|
|
224
|
-
if (result.docker === 'ok') {
|
|
225
|
-
try {
|
|
226
|
-
const health = await infra.checkInfraHealth();
|
|
227
|
-
logger.log('\n🏥 Infrastructure Health:');
|
|
228
|
-
Object.entries(health).forEach(([service, status]) => {
|
|
229
|
-
const icon = status === 'healthy' ? '✅' : status === 'unknown' ? '❓' : '❌';
|
|
230
|
-
logger.log(` ${icon} ${service}: ${status}`);
|
|
231
|
-
});
|
|
232
|
-
} catch (error) {
|
|
233
|
-
logger.log('\n🏥 Infrastructure: Not running');
|
|
234
|
-
}
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
logger.log('');
|
|
238
|
-
} catch (error) {
|
|
239
|
-
handleCommandError(error, 'doctor');
|
|
240
|
-
process.exit(1);
|
|
241
|
-
}
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
program.command('status')
|
|
245
|
-
.description('Show detailed infrastructure service status and running applications')
|
|
246
|
-
.action(async() => {
|
|
247
|
-
try {
|
|
248
|
-
const status = await infra.getInfraStatus();
|
|
249
|
-
logger.log('\n📊 Infrastructure Status\n');
|
|
250
|
-
|
|
251
|
-
Object.entries(status).forEach(([service, info]) => {
|
|
252
|
-
// Normalize status value for comparison (handle edge cases)
|
|
253
|
-
const normalizedStatus = String(info.status).trim().toLowerCase();
|
|
254
|
-
const icon = normalizedStatus === 'running' ? '✅' : '❌';
|
|
255
|
-
logger.log(`${icon} ${service}:`);
|
|
256
|
-
logger.log(` Status: ${info.status}`);
|
|
257
|
-
logger.log(` Port: ${info.port}`);
|
|
258
|
-
logger.log(` URL: ${info.url}`);
|
|
259
|
-
logger.log('');
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
// Show running applications
|
|
263
|
-
const apps = await infra.getAppStatus();
|
|
264
|
-
if (apps.length > 0) {
|
|
265
|
-
logger.log('📱 Running Applications\n');
|
|
266
|
-
apps.forEach((app) => {
|
|
267
|
-
const normalizedStatus = String(app.status).trim().toLowerCase();
|
|
268
|
-
const icon = normalizedStatus.includes('running') || normalizedStatus.includes('up') ? '✅' : '❌';
|
|
269
|
-
logger.log(`${icon} ${app.name}:`);
|
|
270
|
-
logger.log(` Container: ${app.container}`);
|
|
271
|
-
logger.log(` Port: ${app.port}`);
|
|
272
|
-
logger.log(` Status: ${app.status}`);
|
|
273
|
-
logger.log(` URL: ${app.url}`);
|
|
274
|
-
logger.log('');
|
|
275
|
-
});
|
|
276
|
-
}
|
|
277
|
-
} catch (error) {
|
|
278
|
-
handleCommandError(error, 'status');
|
|
279
|
-
process.exit(1);
|
|
280
|
-
}
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
program.command('restart <service>')
|
|
284
|
-
.description('Restart a specific infrastructure service')
|
|
285
|
-
.action(async(service) => {
|
|
286
|
-
try {
|
|
287
|
-
await infra.restartService(service);
|
|
288
|
-
logger.log(`✅ ${service} service restarted successfully`);
|
|
289
|
-
} catch (error) {
|
|
290
|
-
handleCommandError(error, 'restart');
|
|
291
|
-
process.exit(1);
|
|
292
|
-
}
|
|
293
|
-
});
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* Normalize options for external system creation
|
|
298
|
-
* @function normalizeExternalOptions
|
|
299
|
-
* @param {Object} options - Raw CLI options
|
|
300
|
-
* @returns {Object} Normalized options
|
|
301
|
-
*/
|
|
302
|
-
function normalizeExternalOptions(options) {
|
|
303
|
-
const normalized = { ...options };
|
|
304
|
-
if (options.displayName) normalized.systemDisplayName = options.displayName;
|
|
305
|
-
if (options.description) normalized.systemDescription = options.description;
|
|
306
|
-
if (options.systemType) normalized.systemType = options.systemType;
|
|
307
|
-
if (options.authType) normalized.authType = options.authType;
|
|
308
|
-
if (options.datasources !== undefined) {
|
|
309
|
-
const parsedCount = parseInt(options.datasources, 10);
|
|
310
|
-
if (Number.isNaN(parsedCount) || parsedCount < 1 || parsedCount > 10) {
|
|
311
|
-
throw new Error('Datasources count must be a number between 1 and 10');
|
|
312
|
-
}
|
|
313
|
-
normalized.datasourceCount = parsedCount;
|
|
314
|
-
}
|
|
315
|
-
if (options.controller) {
|
|
316
|
-
normalized.controller = true;
|
|
317
|
-
normalized.controllerUrl = options.controller;
|
|
318
|
-
}
|
|
319
|
-
return normalized;
|
|
320
|
-
}
|
|
321
|
-
|
|
322
|
-
/**
|
|
323
|
-
* Validate required options for non-interactive external creation
|
|
324
|
-
* @function validateNonInteractiveExternalOptions
|
|
325
|
-
* @param {Object} normalizedOptions - Normalized options
|
|
326
|
-
* @throws {Error} If required options are missing
|
|
327
|
-
*/
|
|
328
|
-
function validateNonInteractiveExternalOptions(normalizedOptions) {
|
|
329
|
-
const missing = [];
|
|
330
|
-
if (!normalizedOptions.systemDisplayName) missing.push('--display-name');
|
|
331
|
-
if (!normalizedOptions.systemDescription) missing.push('--description');
|
|
332
|
-
if (!normalizedOptions.systemType) missing.push('--system-type');
|
|
333
|
-
if (!normalizedOptions.authType) missing.push('--auth-type');
|
|
334
|
-
if (!normalizedOptions.datasourceCount) missing.push('--datasources');
|
|
335
|
-
if (missing.length > 0) {
|
|
336
|
-
throw new Error(`Missing required options for non-interactive external create: ${missing.join(', ')}`);
|
|
337
|
-
}
|
|
338
|
-
if (!Object.prototype.hasOwnProperty.call(normalizedOptions, 'github')) {
|
|
339
|
-
normalizedOptions.github = false;
|
|
340
|
-
}
|
|
341
|
-
if (!Object.prototype.hasOwnProperty.call(normalizedOptions, 'controller')) {
|
|
342
|
-
normalizedOptions.controller = false;
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
/**
|
|
347
|
-
* Handle create command execution
|
|
348
|
-
* @async
|
|
349
|
-
* @function handleCreateCommand
|
|
350
|
-
* @param {string} appName - Application name
|
|
351
|
-
* @param {Object} options - CLI options
|
|
352
|
-
*/
|
|
353
|
-
async function handleCreateCommand(appName, options) {
|
|
354
|
-
const validTypes = ['webapp', 'api', 'service', 'functionapp', 'external'];
|
|
355
|
-
if (options.type && !validTypes.includes(options.type)) {
|
|
356
|
-
throw new Error(`Invalid type: ${options.type}. Must be one of: ${validTypes.join(', ')}`);
|
|
357
|
-
}
|
|
358
|
-
|
|
359
|
-
const wizardOptions = { app: appName, ...options };
|
|
360
|
-
const normalizedOptions = normalizeExternalOptions(options);
|
|
361
|
-
|
|
362
|
-
const isExternalType = options.type === 'external';
|
|
363
|
-
const isNonInteractive = process.stdin && process.stdin.isTTY === false;
|
|
364
|
-
|
|
365
|
-
if (isExternalType && !options.wizard && isNonInteractive) {
|
|
366
|
-
validateNonInteractiveExternalOptions(normalizedOptions);
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const shouldUseWizard = options.wizard && (options.type === 'external' || (!options.type && validTypes.includes('external')));
|
|
370
|
-
if (shouldUseWizard) {
|
|
371
|
-
const { handleWizard } = require('./commands/wizard');
|
|
372
|
-
await handleWizard(wizardOptions);
|
|
373
|
-
} else {
|
|
374
|
-
await app.createApp(appName, normalizedOptions);
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
/**
|
|
379
|
-
* Sets up application lifecycle commands
|
|
380
|
-
* @param {Command} program - Commander program instance
|
|
381
|
-
*/
|
|
382
|
-
function setupAppCommands(program) {
|
|
383
|
-
program.command('create <app>')
|
|
384
|
-
.description('Create new application with configuration files')
|
|
385
|
-
.option('-p, --port <port>', 'Application port', '3000')
|
|
386
|
-
.option('-d, --database', 'Requires database')
|
|
387
|
-
.option('-r, --redis', 'Requires Redis')
|
|
388
|
-
.option('-s, --storage', 'Requires file storage')
|
|
389
|
-
.option('-a, --authentication', 'Requires authentication/RBAC')
|
|
390
|
-
.option('-l, --language <lang>', 'Runtime language (typescript/python)')
|
|
391
|
-
.option('-t, --template <name>', 'Template to use (e.g., miso-controller, keycloak)')
|
|
392
|
-
.option('--type <type>', 'Application type (webapp, api, service, functionapp, external)', 'webapp')
|
|
393
|
-
.option('--app', 'Generate minimal application files (package.json, index.ts or requirements.txt, main.py)')
|
|
394
|
-
.option('-g, --github', 'Generate GitHub Actions workflows')
|
|
395
|
-
.option('--github-steps <steps>', 'Extra GitHub workflow steps (comma-separated, e.g., npm,test)')
|
|
396
|
-
.option('--main-branch <branch>', 'Main branch name for workflows', 'main')
|
|
397
|
-
.option('--wizard', 'Use interactive wizard for external system creation')
|
|
398
|
-
.option('--display-name <name>', 'External system display name')
|
|
399
|
-
.option('--description <desc>', 'External system description')
|
|
400
|
-
.option('--system-type <type>', 'External system type (openapi, mcp, custom)')
|
|
401
|
-
.option('--auth-type <type>', 'External system auth type (oauth2, apikey, basic)')
|
|
402
|
-
.option('--datasources <count>', 'Number of datasources to create')
|
|
403
|
-
.action(async(appName, options) => {
|
|
404
|
-
try {
|
|
405
|
-
await handleCreateCommand(appName, options);
|
|
406
|
-
} catch (error) {
|
|
407
|
-
handleCommandError(error, 'create');
|
|
408
|
-
process.exit(1);
|
|
409
|
-
}
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
program.command('wizard [appName]')
|
|
413
|
-
.description('Create or extend external systems (OpenAPI, MCP, or known platforms like HubSpot) via guided steps or a config file')
|
|
414
|
-
.option('-a, --app <app>', 'Application name (synonym for positional appName)')
|
|
415
|
-
.option('--config <file>', 'Run headless using a wizard.yaml file (appName, mode, source, credential, preferences)')
|
|
416
|
-
.option('--silent', 'Run with saved integration/<app>/wizard.yaml only; no prompts (requires app name and existing wizard.yaml)')
|
|
417
|
-
.addHelpText('after', `
|
|
418
|
-
Examples:
|
|
419
|
-
$ aifabrix wizard Run interactively (mode first, then prompts)
|
|
420
|
-
$ aifabrix wizard my-integration Load wizard.yaml if present → show summary → "Run with saved config?" or start from step 1
|
|
421
|
-
$ aifabrix wizard my-integration --silent Run headless with integration/my-integration/wizard.yaml (no prompts)
|
|
422
|
-
$ aifabrix wizard -a my-integration Same as above (app name set)
|
|
423
|
-
$ aifabrix wizard --config wizard.yaml Run headless from a wizard config file
|
|
424
|
-
|
|
425
|
-
Config path: When appName is provided, integration/<appName>/wizard.yaml is used for load/save and error.log.
|
|
426
|
-
To change settings after a run, edit that file and run "aifabrix wizard <app>" again.
|
|
427
|
-
Headless config must include: appName, mode (create-system|add-datasource), source (type + filePath/url/platform).
|
|
428
|
-
See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`)
|
|
429
|
-
.action(async(positionalAppName, options) => {
|
|
430
|
-
try {
|
|
431
|
-
const appName = positionalAppName || options.app;
|
|
432
|
-
const configPath = appName ? path.join(process.cwd(), 'integration', appName, 'wizard.yaml') : null;
|
|
433
|
-
const { handleWizard } = require('./commands/wizard');
|
|
434
|
-
await handleWizard({ ...options, app: appName, config: options.config, configPath });
|
|
435
|
-
} catch (error) {
|
|
436
|
-
handleCommandError(error, 'wizard');
|
|
437
|
-
process.exit(1);
|
|
438
|
-
}
|
|
439
|
-
});
|
|
440
|
-
|
|
441
|
-
program.command('build <app>')
|
|
442
|
-
.description('Build container image (auto-detects runtime)')
|
|
443
|
-
.option('-l, --language <lang>', 'Override language detection')
|
|
444
|
-
.option('-f, --force-template', 'Force rebuild from template')
|
|
445
|
-
.option('-t, --tag <tag>', 'Image tag (default: latest). Set image.tag in variables.yaml to match for deploy.')
|
|
446
|
-
.action(async(appName, options) => {
|
|
447
|
-
try {
|
|
448
|
-
const imageTag = await app.buildApp(appName, options);
|
|
449
|
-
logger.log(`✅ Built image: ${imageTag}`);
|
|
450
|
-
} catch (error) {
|
|
451
|
-
handleCommandError(error, 'build');
|
|
452
|
-
process.exit(1);
|
|
453
|
-
}
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
program.command('run <app>')
|
|
457
|
-
.description('Run application locally')
|
|
458
|
-
.option('-p, --port <port>', 'Override local port')
|
|
459
|
-
.option('-d, --debug', 'Enable debug output with detailed container information')
|
|
460
|
-
.action(async(appName, options) => {
|
|
461
|
-
try {
|
|
462
|
-
await app.runApp(appName, options);
|
|
463
|
-
} catch (error) {
|
|
464
|
-
handleCommandError(error, 'run');
|
|
465
|
-
process.exit(1);
|
|
466
|
-
}
|
|
467
|
-
});
|
|
468
|
-
|
|
469
|
-
program.command('push <app>')
|
|
470
|
-
.description('Push image to Azure Container Registry')
|
|
471
|
-
.option('-r, --registry <registry>', 'ACR registry URL (overrides variables.yaml)')
|
|
472
|
-
.option('-t, --tag <tag>', 'Image tag(s) - comma-separated for multiple (default: latest)')
|
|
473
|
-
.action(async(appName, options) => {
|
|
474
|
-
try {
|
|
475
|
-
await app.pushApp(appName, options);
|
|
476
|
-
} catch (error) {
|
|
477
|
-
handleCommandError(error, 'push');
|
|
478
|
-
process.exit(1);
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
|
-
|
|
482
|
-
program.command('deploy <app>')
|
|
483
|
-
.description('Deploy to Azure via Miso Controller')
|
|
484
|
-
.option('--client-id <id>', 'Client ID (overrides config)')
|
|
485
|
-
.option('--client-secret <secret>', 'Client Secret (overrides config)')
|
|
486
|
-
.option('--poll', 'Poll for deployment status', true)
|
|
487
|
-
.option('--no-poll', 'Do not poll for status')
|
|
488
|
-
.action(async(appName, options) => {
|
|
489
|
-
try {
|
|
490
|
-
await app.deployApp(appName, options);
|
|
491
|
-
} catch (error) {
|
|
492
|
-
handleCommandError(error, 'deploy');
|
|
493
|
-
process.exit(1);
|
|
494
|
-
}
|
|
495
|
-
});
|
|
496
|
-
|
|
497
|
-
program.command('dockerfile <app>')
|
|
498
|
-
.description('Generate Dockerfile for an application')
|
|
499
|
-
.option('-l, --language <lang>', 'Override language detection')
|
|
500
|
-
.option('-f, --force', 'Overwrite existing Dockerfile')
|
|
501
|
-
.action(async(appName, options) => {
|
|
502
|
-
try {
|
|
503
|
-
const dockerfilePath = await app.generateDockerfileForApp(appName, options);
|
|
504
|
-
logger.log(chalk.green('\n✅ Dockerfile generated successfully!'));
|
|
505
|
-
logger.log(chalk.gray(`Location: ${dockerfilePath}`));
|
|
506
|
-
} catch (error) {
|
|
507
|
-
handleCommandError(error, 'dockerfile');
|
|
508
|
-
process.exit(1);
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
/**
|
|
514
|
-
* Sets up environment deployment commands
|
|
515
|
-
* @param {Command} program - Commander program instance
|
|
516
|
-
*/
|
|
517
|
-
function setupEnvironmentCommands(program) {
|
|
518
|
-
const deployEnvHandler = async(envKey, options) => {
|
|
519
|
-
try {
|
|
520
|
-
const environmentDeploy = require('./deployment/environment');
|
|
521
|
-
await environmentDeploy.deployEnvironment(envKey, options);
|
|
522
|
-
} catch (error) {
|
|
523
|
-
handleCommandError(error, 'environment deploy');
|
|
524
|
-
process.exit(1);
|
|
525
|
-
}
|
|
526
|
-
};
|
|
527
|
-
|
|
528
|
-
const environment = program
|
|
529
|
-
.command('environment')
|
|
530
|
-
.description('Manage environments');
|
|
531
|
-
|
|
532
|
-
environment
|
|
533
|
-
.command('deploy <env>')
|
|
534
|
-
.description('Deploy/setup environment in Miso Controller')
|
|
535
|
-
.option('--config <file>', 'Environment configuration file')
|
|
536
|
-
.option('--skip-validation', 'Skip environment validation')
|
|
537
|
-
.option('--poll', 'Poll for deployment status', true)
|
|
538
|
-
.option('--no-poll', 'Do not poll for status')
|
|
539
|
-
.action(deployEnvHandler);
|
|
540
|
-
|
|
541
|
-
// Alias: env deploy (register as separate command since Commander.js doesn't support multi-word aliases)
|
|
542
|
-
const env = program
|
|
543
|
-
.command('env')
|
|
544
|
-
.description('Environment management (alias for environment)');
|
|
545
|
-
|
|
546
|
-
env
|
|
547
|
-
.command('deploy <env>')
|
|
548
|
-
.description('Deploy/setup environment in Miso Controller')
|
|
549
|
-
.option('--config <file>', 'Environment configuration file')
|
|
550
|
-
.option('--skip-validation', 'Skip environment validation')
|
|
551
|
-
.option('--poll', 'Poll for deployment status', true)
|
|
552
|
-
.option('--no-poll', 'Do not poll for status')
|
|
553
|
-
.action(deployEnvHandler);
|
|
554
|
-
}
|
|
555
|
-
|
|
556
|
-
/**
|
|
557
|
-
* Handles split-json command logic
|
|
558
|
-
* @async
|
|
559
|
-
* @function handleSplitJsonCommand
|
|
560
|
-
* @param {string} appName - Application name
|
|
561
|
-
* @param {Object} options - Command options
|
|
562
|
-
* @returns {Promise<Object>} Paths to generated files
|
|
563
|
-
*/
|
|
564
|
-
async function handleSplitJsonCommand(appName, options) {
|
|
565
|
-
const fs = require('fs');
|
|
566
|
-
const { detectAppType, getDeployJsonPath } = require('./utils/paths');
|
|
567
|
-
const { appPath, appType } = await detectAppType(appName, options);
|
|
568
|
-
|
|
569
|
-
const outputDir = options.output || appPath;
|
|
570
|
-
if (appType === 'external') {
|
|
571
|
-
const schemaPath = path.join(appPath, 'application-schema.json');
|
|
572
|
-
if (!fs.existsSync(schemaPath)) {
|
|
573
|
-
throw new Error(`application-schema.json not found: ${schemaPath}`);
|
|
574
|
-
}
|
|
575
|
-
return generator.splitExternalApplicationSchema(schemaPath, outputDir);
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
const deployJsonPath = getDeployJsonPath(appName, appType, true);
|
|
579
|
-
if (!fs.existsSync(deployJsonPath)) {
|
|
580
|
-
throw new Error(`Deployment JSON file not found: ${deployJsonPath}`);
|
|
581
|
-
}
|
|
582
|
-
|
|
583
|
-
return generator.splitDeployJson(deployJsonPath, outputDir);
|
|
584
|
-
}
|
|
585
|
-
|
|
586
|
-
/**
|
|
587
|
-
* Logs split-json results
|
|
588
|
-
* @function logSplitJsonResult
|
|
589
|
-
* @param {Object} result - Generated file paths
|
|
590
|
-
* @returns {void}
|
|
591
|
-
*/
|
|
592
|
-
function logSplitJsonResult(result) {
|
|
593
|
-
logger.log(chalk.green('\n✓ Successfully split deployment JSON into component files:'));
|
|
594
|
-
logger.log(` • env.template: ${result.envTemplate}`);
|
|
595
|
-
logger.log(` • variables.yaml: ${result.variables}`);
|
|
596
|
-
if (result.rbac) {
|
|
597
|
-
logger.log(` • rbac.yml: ${result.rbac}`);
|
|
598
|
-
}
|
|
599
|
-
logger.log(` • README.md: ${result.readme}`);
|
|
600
|
-
}
|
|
601
|
-
|
|
602
|
-
/**
|
|
603
|
-
* Sets up utility commands
|
|
604
|
-
* @param {Command} program - Commander program instance
|
|
605
|
-
*/
|
|
606
|
-
function setupUtilityCommands(program) {
|
|
607
|
-
program.command('resolve <app>')
|
|
608
|
-
.description('Generate .env file from template and validate application files')
|
|
609
|
-
.option('-f, --force', 'Generate missing secret keys in secrets file')
|
|
610
|
-
.option('--skip-validation', 'Skip file validation after generating .env')
|
|
611
|
-
.action(async(appName, options) => {
|
|
612
|
-
try {
|
|
613
|
-
// builder/.env should use docker context (postgres:5432)
|
|
614
|
-
// apps/.env (if envOutputPath is set) will be generated with local context by processEnvVariables
|
|
615
|
-
const envPath = await secrets.generateEnvFile(appName, undefined, 'docker', options.force);
|
|
616
|
-
logger.log(`✓ Generated .env file: ${envPath}`);
|
|
617
|
-
|
|
618
|
-
// Validate application files after generating .env
|
|
619
|
-
if (!options.skipValidation) {
|
|
620
|
-
const validate = require('./validation/validate');
|
|
621
|
-
const result = await validate.validateAppOrFile(appName);
|
|
622
|
-
validate.displayValidationResults(result);
|
|
623
|
-
if (!result.valid) {
|
|
624
|
-
logger.log(chalk.yellow('\n⚠️ Validation found errors. Fix them before deploying.'));
|
|
625
|
-
process.exit(1);
|
|
626
|
-
}
|
|
627
|
-
}
|
|
628
|
-
} catch (error) {
|
|
629
|
-
handleCommandError(error, 'resolve');
|
|
630
|
-
process.exit(1);
|
|
631
|
-
}
|
|
632
|
-
});
|
|
633
|
-
|
|
634
|
-
program.command('json <app>')
|
|
635
|
-
.description('Generate deployment JSON (aifabrix-deploy.json for normal apps, application-schema.json for external systems)')
|
|
636
|
-
.option('--type <type>', 'Application type (external) - if set, only checks integration folder')
|
|
637
|
-
.action(async(appName, options) => {
|
|
638
|
-
try {
|
|
639
|
-
const result = await generator.generateDeployJsonWithValidation(appName, options);
|
|
640
|
-
if (result.success) {
|
|
641
|
-
const fileName = result.path.includes('application-schema.json') ? 'application-schema.json' : 'deployment JSON';
|
|
642
|
-
logger.log(`✓ Generated ${fileName}: ${result.path}`);
|
|
643
|
-
|
|
644
|
-
if (result.validation.warnings && result.validation.warnings.length > 0) {
|
|
645
|
-
logger.log('\n⚠️ Warnings:');
|
|
646
|
-
result.validation.warnings.forEach(warning => logger.log(` • ${warning}`));
|
|
647
|
-
}
|
|
648
|
-
} else {
|
|
649
|
-
logger.log('❌ Validation failed:');
|
|
650
|
-
if (result.validation.errors && result.validation.errors.length > 0) {
|
|
651
|
-
result.validation.errors.forEach(error => logger.log(` • ${error}`));
|
|
652
|
-
}
|
|
653
|
-
process.exit(1);
|
|
654
|
-
}
|
|
655
|
-
} catch (error) {
|
|
656
|
-
handleCommandError(error, 'json');
|
|
657
|
-
process.exit(1);
|
|
658
|
-
}
|
|
659
|
-
});
|
|
660
|
-
|
|
661
|
-
program.command('split-json <app>')
|
|
662
|
-
.description('Split deployment JSON into component files (env.template, variables.yaml, rbac.yml, README.md)')
|
|
663
|
-
.option('-o, --output <dir>', 'Output directory for component files (defaults to same directory as JSON)')
|
|
664
|
-
.option('--type <type>', 'Application type (external) - if set, only checks integration folder')
|
|
665
|
-
.action(async(appName, options) => {
|
|
666
|
-
try {
|
|
667
|
-
const result = await handleSplitJsonCommand(appName, options);
|
|
668
|
-
logSplitJsonResult(result);
|
|
669
|
-
} catch (error) {
|
|
670
|
-
handleCommandError(error, 'split-json');
|
|
671
|
-
process.exit(1);
|
|
672
|
-
}
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
program.command('genkey <app>')
|
|
676
|
-
.description('Generate deployment key')
|
|
677
|
-
.action(async(appName) => {
|
|
678
|
-
try {
|
|
679
|
-
// Generate JSON first, then extract key from it
|
|
680
|
-
const jsonPath = await generator.generateDeployJson(appName);
|
|
681
|
-
|
|
682
|
-
// Read the generated JSON file
|
|
683
|
-
const fs = require('fs');
|
|
684
|
-
const jsonContent = fs.readFileSync(jsonPath, 'utf8');
|
|
685
|
-
const deployment = JSON.parse(jsonContent);
|
|
686
|
-
|
|
687
|
-
// Extract deploymentKey from JSON
|
|
688
|
-
const key = deployment.deploymentKey;
|
|
689
|
-
|
|
690
|
-
if (!key) {
|
|
691
|
-
throw new Error('deploymentKey not found in generated JSON');
|
|
692
|
-
}
|
|
693
|
-
|
|
694
|
-
logger.log(`\nDeployment key for ${appName}:`);
|
|
695
|
-
logger.log(key);
|
|
696
|
-
logger.log(chalk.gray(`\nGenerated from: ${jsonPath}`));
|
|
697
|
-
} catch (error) {
|
|
698
|
-
handleCommandError(error, 'genkey');
|
|
699
|
-
process.exit(1);
|
|
700
|
-
}
|
|
701
|
-
});
|
|
702
|
-
|
|
703
|
-
program.command('show <appKey>')
|
|
704
|
-
.description('Show application info from local builder/ or integration/ (offline) or from controller (--online)')
|
|
705
|
-
.option('--online', 'Fetch application data from the controller')
|
|
706
|
-
.option('--json', 'Output as JSON')
|
|
707
|
-
.action(async(appKey, options) => {
|
|
708
|
-
try {
|
|
709
|
-
const { showApp } = require('./app/show');
|
|
710
|
-
await showApp(appKey, { online: options.online, json: options.json });
|
|
711
|
-
} catch (error) {
|
|
712
|
-
logger.error(chalk.red(`Error: ${error.message}`));
|
|
713
|
-
process.exit(1);
|
|
714
|
-
}
|
|
715
|
-
});
|
|
716
|
-
|
|
717
|
-
program.command('validate <appOrFile>')
|
|
718
|
-
.description('Validate application or external integration file')
|
|
719
|
-
.option('--type <type>', 'Application type (external) - if set, only checks integration folder')
|
|
720
|
-
.action(async(appOrFile, options) => {
|
|
721
|
-
try {
|
|
722
|
-
const validate = require('./validation/validate');
|
|
723
|
-
const result = await validate.validateAppOrFile(appOrFile, options);
|
|
724
|
-
validate.displayValidationResults(result);
|
|
725
|
-
if (!result.valid) {
|
|
726
|
-
process.exit(1);
|
|
727
|
-
}
|
|
728
|
-
} catch (error) {
|
|
729
|
-
handleCommandError(error, 'validate');
|
|
730
|
-
process.exit(1);
|
|
731
|
-
}
|
|
732
|
-
});
|
|
733
|
-
|
|
734
|
-
program.command('diff <file1> <file2>')
|
|
735
|
-
.description('Compare two configuration files (for deployment pipeline)')
|
|
736
|
-
.action(async(file1, file2) => {
|
|
737
|
-
try {
|
|
738
|
-
const diff = require('./core/diff');
|
|
739
|
-
const result = await diff.compareFiles(file1, file2);
|
|
740
|
-
diff.formatDiffOutput(result);
|
|
741
|
-
if (!result.identical) {
|
|
742
|
-
process.exit(1);
|
|
743
|
-
}
|
|
744
|
-
} catch (error) {
|
|
745
|
-
handleCommandError(error, 'diff');
|
|
746
|
-
process.exit(1);
|
|
747
|
-
}
|
|
748
|
-
});
|
|
749
|
-
}
|
|
750
|
-
|
|
751
|
-
/**
|
|
752
|
-
* Helper function to display developer configuration
|
|
753
|
-
* @param {string} devId - Developer ID
|
|
754
|
-
*/
|
|
755
|
-
async function displayDevConfig(devId) {
|
|
756
|
-
const devIdNum = parseInt(devId, 10);
|
|
757
|
-
const ports = devConfig.getDevPorts(devIdNum);
|
|
758
|
-
const configVars = [
|
|
759
|
-
{ key: 'aifabrix-home', value: await config.getAifabrixHomeOverride() },
|
|
760
|
-
{ key: 'aifabrix-secrets', value: await config.getAifabrixSecretsPath() },
|
|
761
|
-
{ key: 'aifabrix-env-config', value: await config.getAifabrixEnvConfigPath() }
|
|
762
|
-
].filter(v => v.value);
|
|
763
|
-
|
|
764
|
-
logger.log('\n🔧 Developer Configuration\n');
|
|
765
|
-
logger.log(`Developer ID: ${devId}`);
|
|
766
|
-
logger.log('\nPorts:');
|
|
767
|
-
logger.log(` App: ${ports.app}`);
|
|
768
|
-
logger.log(` Postgres: ${ports.postgres}`);
|
|
769
|
-
logger.log(` Redis: ${ports.redis}`);
|
|
770
|
-
logger.log(` pgAdmin: ${ports.pgadmin}`);
|
|
771
|
-
logger.log(` Redis Commander: ${ports.redisCommander}`);
|
|
772
|
-
|
|
773
|
-
if (configVars.length > 0) {
|
|
774
|
-
logger.log('\nConfiguration:');
|
|
775
|
-
configVars.forEach(v => logger.log(` ${v.key}: ${v.value}`));
|
|
776
|
-
}
|
|
777
|
-
logger.log('');
|
|
778
|
-
}
|
|
779
|
-
|
|
780
|
-
/**
|
|
781
|
-
* Sets up developer configuration commands
|
|
782
|
-
* @param {Command} program - Commander program instance
|
|
783
|
-
*/
|
|
784
|
-
function setupDevCommands(program) {
|
|
785
|
-
const dev = program
|
|
786
|
-
.command('dev')
|
|
787
|
-
.description('Developer configuration and isolation');
|
|
788
|
-
|
|
789
|
-
dev
|
|
790
|
-
.command('config')
|
|
791
|
-
.description('Show or set developer configuration')
|
|
792
|
-
.option('--set-id <id>', 'Set developer ID')
|
|
793
|
-
.action(async(options) => {
|
|
794
|
-
try {
|
|
795
|
-
// Commander.js converts --set-id to setId in options object
|
|
796
|
-
const setIdValue = options.setId || options['set-id'];
|
|
797
|
-
if (setIdValue) {
|
|
798
|
-
const digitsOnly = /^[0-9]+$/.test(setIdValue);
|
|
799
|
-
if (!digitsOnly) {
|
|
800
|
-
throw new Error('Developer ID must be a non-negative digit string (0 = default infra, > 0 = developer-specific)');
|
|
801
|
-
}
|
|
802
|
-
// Preserve the original string value to maintain leading zeros (e.g., "01")
|
|
803
|
-
await config.setDeveloperId(setIdValue);
|
|
804
|
-
process.env.AIFABRIX_DEVELOPERID = setIdValue;
|
|
805
|
-
logger.log(chalk.green(`✓ Developer ID set to ${setIdValue}`));
|
|
806
|
-
// Use the ID we just set instead of reading from file to avoid race conditions
|
|
807
|
-
await displayDevConfig(setIdValue);
|
|
808
|
-
return;
|
|
809
|
-
}
|
|
810
|
-
|
|
811
|
-
const devId = await config.getDeveloperId();
|
|
812
|
-
await displayDevConfig(devId);
|
|
813
|
-
} catch (error) {
|
|
814
|
-
handleCommandError(error, 'dev config');
|
|
815
|
-
process.exit(1);
|
|
816
|
-
}
|
|
817
|
-
});
|
|
818
|
-
|
|
819
|
-
dev
|
|
820
|
-
.command('set-id <id>')
|
|
821
|
-
.description('Set developer ID (convenience alias for "dev config --set-id")')
|
|
822
|
-
.action(async(id) => {
|
|
823
|
-
try {
|
|
824
|
-
const digitsOnly = /^[0-9]+$/.test(id);
|
|
825
|
-
if (!digitsOnly) {
|
|
826
|
-
throw new Error('Developer ID must be a non-negative digit string (0 = default infra, > 0 = developer-specific)');
|
|
827
|
-
}
|
|
828
|
-
// Preserve the original string value to maintain leading zeros (e.g., "01")
|
|
829
|
-
await config.setDeveloperId(id);
|
|
830
|
-
process.env.AIFABRIX_DEVELOPERID = id;
|
|
831
|
-
logger.log(chalk.green(`✓ Developer ID set to ${id}`));
|
|
832
|
-
// Use the ID we just set instead of reading from file to avoid race conditions
|
|
833
|
-
await displayDevConfig(id);
|
|
834
|
-
} catch (error) {
|
|
835
|
-
handleCommandError(error, 'dev set-id');
|
|
836
|
-
process.exit(1);
|
|
837
|
-
}
|
|
838
|
-
});
|
|
839
|
-
}
|
|
840
|
-
|
|
841
|
-
/**
|
|
842
|
-
* Sets up secrets and security commands
|
|
843
|
-
* @param {Command} program - Commander program instance
|
|
844
|
-
*/
|
|
845
|
-
function setupSecretsCommands(program) {
|
|
846
|
-
const secretsCmd = program
|
|
847
|
-
.command('secrets')
|
|
848
|
-
.description('Manage secrets in secrets files');
|
|
849
|
-
|
|
850
|
-
secretsCmd
|
|
851
|
-
.command('set <key> <value>')
|
|
852
|
-
.description('Set a secret value in secrets file')
|
|
853
|
-
.option('--shared', 'Save to general secrets file (from config.yaml aifabrix-secrets) instead of user secrets')
|
|
854
|
-
.action(async(key, value, options) => {
|
|
855
|
-
try {
|
|
856
|
-
await handleSecretsSet(key, value, options);
|
|
857
|
-
} catch (error) {
|
|
858
|
-
handleCommandError(error, 'secrets set');
|
|
859
|
-
process.exit(1);
|
|
860
|
-
}
|
|
861
|
-
});
|
|
862
|
-
|
|
863
|
-
program.command('secure')
|
|
864
|
-
.description('Encrypt secrets in secrets.local.yaml files for ISO 27001 compliance')
|
|
865
|
-
.option('--secrets-encryption <key>', 'Encryption key (32 bytes, hex or base64)')
|
|
866
|
-
.action(async(options) => {
|
|
867
|
-
try {
|
|
868
|
-
await handleSecure(options);
|
|
869
|
-
} catch (error) {
|
|
870
|
-
handleCommandError(error, 'secure');
|
|
871
|
-
process.exit(1);
|
|
872
|
-
}
|
|
873
|
-
});
|
|
874
|
-
}
|
|
875
|
-
|
|
876
|
-
/**
|
|
877
|
-
* Sets up external system commands
|
|
878
|
-
* @param {Command} program - Commander program instance
|
|
879
|
-
*/
|
|
880
|
-
function setupExternalSystemCommands(program) {
|
|
881
|
-
program.command('download <system-key>')
|
|
882
|
-
.description('Download external system from dataplane to local development structure')
|
|
883
|
-
.option('--dry-run', 'Show what would be downloaded without actually downloading')
|
|
884
|
-
.action(async(systemKey, options) => {
|
|
885
|
-
try {
|
|
886
|
-
const download = require('./external-system/download');
|
|
887
|
-
await download.downloadExternalSystem(systemKey, options);
|
|
888
|
-
} catch (error) {
|
|
889
|
-
handleCommandError(error, 'download');
|
|
890
|
-
process.exit(1);
|
|
891
|
-
}
|
|
892
|
-
});
|
|
893
|
-
|
|
894
|
-
program.command('delete <system-key>')
|
|
895
|
-
.description('Delete external system from dataplane (also deletes all associated datasources)')
|
|
896
|
-
.option('--type <type>', 'Application type (external) - required for external systems')
|
|
897
|
-
.option('--yes', 'Skip confirmation prompt')
|
|
898
|
-
.option('--force', 'Skip confirmation prompt (alias for --yes)')
|
|
899
|
-
.action(async(systemKey, options) => {
|
|
900
|
-
try {
|
|
901
|
-
if (options.type !== 'external') {
|
|
902
|
-
throw new Error('Delete command for external systems requires --type external');
|
|
903
|
-
}
|
|
904
|
-
const externalDelete = require('./external-system/delete');
|
|
905
|
-
await externalDelete.deleteExternalSystem(systemKey, options);
|
|
906
|
-
} catch (error) {
|
|
907
|
-
handleCommandError(error, 'delete');
|
|
908
|
-
process.exit(1);
|
|
909
|
-
}
|
|
910
|
-
});
|
|
911
|
-
|
|
912
|
-
program.command('test <app>')
|
|
913
|
-
.description('Run unit tests for external system (local validation, no API calls)')
|
|
914
|
-
.option('-d, --datasource <key>', 'Test specific datasource only')
|
|
915
|
-
.option('-v, --verbose', 'Show detailed validation output')
|
|
916
|
-
.action(async(appName, options) => {
|
|
917
|
-
try {
|
|
918
|
-
const test = require('./external-system/test');
|
|
919
|
-
const results = await test.testExternalSystem(appName, options);
|
|
920
|
-
test.displayTestResults(results, options.verbose);
|
|
921
|
-
if (!results.valid) {
|
|
922
|
-
process.exit(1);
|
|
923
|
-
}
|
|
924
|
-
} catch (error) {
|
|
925
|
-
handleCommandError(error, 'test');
|
|
926
|
-
process.exit(1);
|
|
927
|
-
}
|
|
928
|
-
});
|
|
929
|
-
|
|
930
|
-
program.command('test-integration <app>')
|
|
931
|
-
.description('Run integration tests via dataplane pipeline API')
|
|
932
|
-
.option('-d, --datasource <key>', 'Test specific datasource only')
|
|
933
|
-
.option('-p, --payload <file>', 'Path to custom test payload file')
|
|
934
|
-
.option('-v, --verbose', 'Show detailed test output')
|
|
935
|
-
.option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
|
|
936
|
-
.action(async(appName, options) => {
|
|
937
|
-
try {
|
|
938
|
-
const test = require('./external-system/test');
|
|
939
|
-
const results = await test.testExternalSystemIntegration(appName, options);
|
|
940
|
-
test.displayIntegrationTestResults(results, options.verbose);
|
|
941
|
-
if (!results.success) {
|
|
942
|
-
process.exit(1);
|
|
943
|
-
}
|
|
944
|
-
} catch (error) {
|
|
945
|
-
handleCommandError(error, 'test-integration');
|
|
946
|
-
process.exit(1);
|
|
947
|
-
}
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
|
|
951
|
-
/**
|
|
952
|
-
* Sets up all CLI commands on the Commander program instance
|
|
953
|
-
* @param {Command} program - Commander program instance
|
|
954
|
-
*/
|
|
955
|
-
function setupCommands(program) {
|
|
956
|
-
setupInfraCommands(program);
|
|
957
|
-
setupAuthCommands(program);
|
|
958
|
-
setupAppCommands(program);
|
|
959
|
-
setupEnvironmentCommands(program);
|
|
960
|
-
setupAppManagementCommands(program);
|
|
961
|
-
setupDatasourceCommands(program);
|
|
962
|
-
setupUtilityCommands(program);
|
|
963
|
-
setupExternalSystemCommands(program);
|
|
964
|
-
setupDevCommands(program);
|
|
965
|
-
setupSecretsCommands(program);
|
|
966
|
-
}
|
|
12
|
+
const cli = require('./cli/index');
|
|
967
13
|
|
|
968
14
|
module.exports = {
|
|
969
|
-
setupCommands,
|
|
970
|
-
validateCommand,
|
|
971
|
-
handleCommandError
|
|
15
|
+
setupCommands: cli.setupCommands,
|
|
16
|
+
validateCommand: cli.validateCommand,
|
|
17
|
+
handleCommandError: cli.handleCommandError
|
|
972
18
|
};
|