@aifabrix/builder 2.22.1 ā 2.31.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/jest.config.coverage.js +37 -0
- package/lib/api/pipeline.api.js +10 -9
- package/lib/app-deploy.js +36 -14
- package/lib/app-list.js +191 -71
- package/lib/app-prompts.js +77 -26
- package/lib/app-readme.js +123 -5
- package/lib/app-rotate-secret.js +101 -57
- package/lib/app-run-helpers.js +200 -172
- package/lib/app-run.js +137 -68
- package/lib/audit-logger.js +8 -7
- package/lib/build.js +161 -250
- package/lib/cli.js +73 -65
- package/lib/commands/login.js +45 -31
- package/lib/commands/logout.js +181 -0
- package/lib/commands/secrets-set.js +2 -2
- package/lib/commands/secure.js +61 -26
- package/lib/config.js +79 -45
- package/lib/datasource-deploy.js +89 -29
- package/lib/deployer.js +164 -129
- package/lib/diff.js +63 -21
- package/lib/environment-deploy.js +36 -19
- package/lib/external-system-deploy.js +134 -66
- package/lib/external-system-download.js +244 -171
- package/lib/external-system-test.js +199 -164
- package/lib/generator-external.js +145 -72
- package/lib/generator-helpers.js +49 -17
- package/lib/generator-split.js +105 -58
- package/lib/infra.js +101 -131
- package/lib/schema/application-schema.json +895 -896
- package/lib/schema/env-config.yaml +11 -4
- package/lib/template-validator.js +13 -4
- package/lib/utils/api.js +8 -8
- package/lib/utils/app-register-auth.js +36 -18
- package/lib/utils/app-run-containers.js +140 -0
- package/lib/utils/auth-headers.js +6 -6
- package/lib/utils/build-copy.js +60 -2
- package/lib/utils/build-helpers.js +94 -0
- package/lib/utils/cli-utils.js +177 -76
- package/lib/utils/compose-generator.js +12 -2
- package/lib/utils/config-tokens.js +151 -9
- package/lib/utils/deployment-errors.js +137 -69
- package/lib/utils/deployment-validation-helpers.js +103 -0
- package/lib/utils/docker-build.js +57 -0
- package/lib/utils/dockerfile-utils.js +13 -3
- package/lib/utils/env-copy.js +163 -94
- package/lib/utils/env-map.js +226 -86
- package/lib/utils/environment-checker.js +2 -2
- package/lib/utils/error-formatters/network-errors.js +0 -1
- package/lib/utils/external-system-display.js +14 -19
- package/lib/utils/external-system-env-helpers.js +107 -0
- package/lib/utils/external-system-test-helpers.js +144 -0
- package/lib/utils/health-check.js +10 -8
- package/lib/utils/infra-status.js +123 -0
- package/lib/utils/local-secrets.js +3 -2
- package/lib/utils/paths.js +228 -49
- package/lib/utils/schema-loader.js +125 -57
- package/lib/utils/token-manager.js +10 -7
- package/lib/utils/yaml-preserve.js +55 -16
- package/lib/validate.js +87 -89
- package/package.json +4 -4
- package/scripts/ci-fix.sh +19 -0
- package/scripts/ci-simulate.sh +19 -0
- package/templates/applications/miso-controller/test.yaml +1 -0
- package/templates/python/Dockerfile.hbs +8 -45
- package/templates/typescript/Dockerfile.hbs +8 -42
package/lib/build.js
CHANGED
|
@@ -14,75 +14,17 @@ const fs = require('fs').promises;
|
|
|
14
14
|
const fsSync = require('fs');
|
|
15
15
|
const path = require('path');
|
|
16
16
|
const paths = require('./utils/paths');
|
|
17
|
-
const { detectAppType } = require('./utils/paths');
|
|
18
|
-
const { exec } = require('child_process');
|
|
19
|
-
const { promisify } = require('util');
|
|
17
|
+
const { detectAppType, getProjectRoot } = require('./utils/paths');
|
|
20
18
|
const chalk = require('chalk');
|
|
21
19
|
const yaml = require('js-yaml');
|
|
22
20
|
const secrets = require('./secrets');
|
|
23
21
|
const config = require('./config');
|
|
24
22
|
const logger = require('./utils/logger');
|
|
25
|
-
const validator = require('./validator');
|
|
26
23
|
const dockerfileUtils = require('./utils/dockerfile-utils');
|
|
27
24
|
const dockerBuild = require('./utils/docker-build');
|
|
28
25
|
const buildCopy = require('./utils/build-copy');
|
|
29
26
|
const { buildDevImageName } = require('./utils/image-name');
|
|
30
|
-
|
|
31
|
-
const execAsync = promisify(exec);
|
|
32
|
-
|
|
33
|
-
/**
|
|
34
|
-
* Copies application template files to dev directory
|
|
35
|
-
* Used when apps directory doesn't exist to ensure build can proceed
|
|
36
|
-
* @async
|
|
37
|
-
* @param {string} templatePath - Path to template directory
|
|
38
|
-
* @param {string} devDir - Target dev directory
|
|
39
|
-
* @param {string} _language - Language (typescript/python) - currently unused but kept for future use
|
|
40
|
-
* @throws {Error} If copying fails
|
|
41
|
-
*/
|
|
42
|
-
async function copyTemplateFilesToDevDir(templatePath, devDir, _language) {
|
|
43
|
-
if (!fsSync.existsSync(templatePath)) {
|
|
44
|
-
throw new Error(`Template path not found: ${templatePath}`);
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const entries = await fs.readdir(templatePath);
|
|
48
|
-
|
|
49
|
-
// Copy only application files, skip Dockerfile and docker-compose templates
|
|
50
|
-
const appFiles = entries.filter(entry => {
|
|
51
|
-
const lowerEntry = entry.toLowerCase();
|
|
52
|
-
// Include .gitignore, exclude .hbs files and docker-related files
|
|
53
|
-
if (entry === '.gitignore') {
|
|
54
|
-
return true;
|
|
55
|
-
}
|
|
56
|
-
if (lowerEntry.endsWith('.hbs')) {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
if (lowerEntry.startsWith('dockerfile') || lowerEntry.includes('docker-compose')) {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
if (entry.startsWith('.') && entry !== '.gitignore') {
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
return true;
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
for (const entry of appFiles) {
|
|
69
|
-
const sourcePath = path.join(templatePath, entry);
|
|
70
|
-
const targetPath = path.join(devDir, entry);
|
|
71
|
-
|
|
72
|
-
// Skip if source file doesn't exist (e.g., .gitignore might not be in template)
|
|
73
|
-
try {
|
|
74
|
-
const entryStats = await fs.stat(sourcePath);
|
|
75
|
-
if (entryStats.isFile()) {
|
|
76
|
-
await fs.copyFile(sourcePath, targetPath);
|
|
77
|
-
}
|
|
78
|
-
} catch (error) {
|
|
79
|
-
// Skip files that don't exist (e.g., .gitignore might not be in template)
|
|
80
|
-
if (error.code !== 'ENOENT') {
|
|
81
|
-
throw error;
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
27
|
+
const buildHelpers = require('./utils/build-helpers');
|
|
86
28
|
|
|
87
29
|
/**
|
|
88
30
|
* Loads variables.yaml configuration for an application
|
|
@@ -235,96 +177,174 @@ async function generateDockerfile(appNameOrPath, language, config, buildConfig =
|
|
|
235
177
|
* @param {boolean} options.forceTemplate - Force template flag
|
|
236
178
|
* @returns {Promise<string>} Path to Dockerfile
|
|
237
179
|
*/
|
|
238
|
-
async function determineDockerfile(appName, options) {
|
|
239
|
-
// Use dev directory if provided, otherwise fall back to builder directory
|
|
240
|
-
const searchPath = options.devDir || path.join(process.cwd(), 'builder', appName);
|
|
241
|
-
|
|
242
|
-
const templateDockerfile = dockerfileUtils.checkTemplateDockerfile(searchPath, appName, options.forceTemplate);
|
|
243
|
-
if (templateDockerfile) {
|
|
244
|
-
const relativePath = path.relative(process.cwd(), templateDockerfile);
|
|
245
|
-
logger.log(chalk.green(`ā Using existing Dockerfile: ${relativePath}`));
|
|
246
|
-
return templateDockerfile;
|
|
247
|
-
}
|
|
248
180
|
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
181
|
+
/**
|
|
182
|
+
* Executes Docker build and handles tagging
|
|
183
|
+
* @async
|
|
184
|
+
* @param {string} imageName - Image name
|
|
185
|
+
* @param {string} dockerfilePath - Path to Dockerfile
|
|
186
|
+
* @param {string} contextPath - Build context path
|
|
187
|
+
* @param {string} tag - Image tag
|
|
188
|
+
* @param {Object} options - Build options
|
|
189
|
+
*/
|
|
254
190
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
191
|
+
async function postBuildTasks(appName, buildConfig) {
|
|
192
|
+
try {
|
|
193
|
+
const envPath = await secrets.generateEnvFile(appName, buildConfig.secrets, 'docker');
|
|
194
|
+
logger.log(chalk.green(`ā Generated .env file: ${envPath}`));
|
|
195
|
+
// Note: processEnvVariables is already called by generateEnvFile to generate local .env
|
|
196
|
+
// at the envOutputPath, so we don't need to manually copy the docker .env file
|
|
197
|
+
} catch (error) {
|
|
198
|
+
logger.log(chalk.yellow(`ā ļø Warning: Could not generate .env file: ${error.message}`));
|
|
199
|
+
}
|
|
260
200
|
}
|
|
261
201
|
|
|
262
202
|
/**
|
|
263
|
-
*
|
|
203
|
+
* Check if app is external type and handle accordingly
|
|
264
204
|
* @async
|
|
265
205
|
* @param {string} appName - Application name
|
|
266
|
-
* @returns {Promise<
|
|
267
|
-
* @throws {Error} If configuration cannot be loaded or validated
|
|
206
|
+
* @returns {Promise<boolean>} True if external (handled), false if should continue
|
|
268
207
|
*/
|
|
269
|
-
async function
|
|
208
|
+
async function checkExternalAppType(appName) {
|
|
270
209
|
const variables = await loadVariablesYaml(appName);
|
|
210
|
+
if (variables.app && variables.app.type === 'external') {
|
|
211
|
+
const generator = require('./generator');
|
|
212
|
+
const jsonPath = await generator.generateDeployJson(appName);
|
|
213
|
+
logger.log(chalk.green(`ā Generated deployment JSON: ${jsonPath}`));
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
return false;
|
|
217
|
+
}
|
|
271
218
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Prepare dev directory and copy application files
|
|
221
|
+
* @async
|
|
222
|
+
* @param {string} appName - Application name
|
|
223
|
+
* @param {Object} buildConfig - Build configuration
|
|
224
|
+
* @param {Object} options - Build options
|
|
225
|
+
* @returns {Promise<Object>} Object with devDir, effectiveImageName, imageName, appConfig, and developerId
|
|
226
|
+
*/
|
|
227
|
+
async function prepareDevDirectory(appName, buildConfig, options) {
|
|
228
|
+
const developerId = await config.getDeveloperId();
|
|
229
|
+
const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
|
|
230
|
+
const directoryName = idNum === 0 ? 'applications' : `dev-${developerId}`;
|
|
231
|
+
logger.log(chalk.blue(`Copying files to developer-specific directory (${directoryName})...`));
|
|
232
|
+
const devDir = await buildCopy.copyBuilderToDevDirectory(appName, developerId);
|
|
233
|
+
logger.log(chalk.green(`ā Files copied to: ${devDir}`));
|
|
234
|
+
|
|
235
|
+
const { config: appConfig, imageName } = await buildHelpers.loadAndValidateConfig(appName);
|
|
236
|
+
const effectiveImageName = buildDevImageName(imageName, developerId);
|
|
237
|
+
|
|
238
|
+
// Check if application source files exist, if not copy from templates
|
|
239
|
+
const appsPath = path.join(process.cwd(), 'apps', appName);
|
|
240
|
+
if (fsSync.existsSync(appsPath)) {
|
|
241
|
+
await buildCopy.copyAppSourceFiles(appsPath, devDir);
|
|
242
|
+
logger.log(chalk.green(`ā Copied application source files from apps/${appName}`));
|
|
243
|
+
} else {
|
|
244
|
+
// No apps directory - check if we need to copy template files
|
|
245
|
+
const language = options.language || buildConfig.language || detectLanguage(devDir);
|
|
246
|
+
const packageJsonPath = path.join(devDir, 'package.json');
|
|
247
|
+
const requirementsPath = path.join(devDir, 'requirements.txt');
|
|
248
|
+
|
|
249
|
+
if (language === 'typescript' && !fsSync.existsSync(packageJsonPath)) {
|
|
250
|
+
const projectRoot = getProjectRoot();
|
|
251
|
+
const templatePath = path.join(projectRoot, 'templates', 'typescript');
|
|
252
|
+
await buildCopy.copyTemplateFilesToDevDir(templatePath, devDir, language);
|
|
253
|
+
logger.log(chalk.green(`ā Generated application files from ${language} template`));
|
|
254
|
+
} else if (language === 'python' && !fsSync.existsSync(requirementsPath)) {
|
|
255
|
+
const projectRoot = getProjectRoot();
|
|
256
|
+
const templatePath = path.join(projectRoot, 'templates', 'python');
|
|
257
|
+
await buildCopy.copyTemplateFilesToDevDir(templatePath, devDir, language);
|
|
258
|
+
logger.log(chalk.green(`ā Generated application files from ${language} template`));
|
|
259
|
+
}
|
|
276
260
|
}
|
|
277
261
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
262
|
+
return { devDir, effectiveImageName, imageName, appConfig };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Prepare build context path
|
|
267
|
+
* @param {Object} buildConfig - Build configuration
|
|
268
|
+
* @param {string} devDir - Developer directory path
|
|
269
|
+
* @returns {string} Resolved context path
|
|
270
|
+
*/
|
|
271
|
+
function prepareBuildContext(buildConfig, devDir) {
|
|
272
|
+
let contextPath;
|
|
273
|
+
|
|
274
|
+
// Check if context is using old format (../appName) - these are incompatible with dev directory structure
|
|
275
|
+
if (buildConfig.context && buildConfig.context.startsWith('../') && buildConfig.context !== '../..') {
|
|
276
|
+
// Old format detected - always use devDir instead
|
|
277
|
+
logger.log(chalk.yellow(`ā ļø Warning: Build context uses old format: ${buildConfig.context}`));
|
|
278
|
+
logger.log(chalk.yellow(` Using dev directory instead: ${devDir}`));
|
|
279
|
+
contextPath = devDir;
|
|
280
|
+
} else if (buildConfig.context && buildConfig.context !== '../..') {
|
|
281
|
+
// Resolve relative context path from dev directory
|
|
282
|
+
contextPath = path.resolve(devDir, buildConfig.context);
|
|
283
|
+
} else if (buildConfig.context === '../..') {
|
|
284
|
+
// For apps flag, context is relative to devDir
|
|
285
|
+
contextPath = process.cwd();
|
|
286
286
|
} else {
|
|
287
|
-
|
|
287
|
+
// No context specified, use dev directory
|
|
288
|
+
contextPath = devDir;
|
|
288
289
|
}
|
|
289
290
|
|
|
290
|
-
//
|
|
291
|
-
|
|
291
|
+
// Ensure context path is absolute and normalized
|
|
292
|
+
contextPath = path.resolve(contextPath);
|
|
292
293
|
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
294
|
+
// Validate that context path exists (skip in test environments)
|
|
295
|
+
const isTestEnv = process.env.NODE_ENV === 'test' ||
|
|
296
|
+
process.env.JEST_WORKER_ID !== undefined ||
|
|
297
|
+
typeof jest !== 'undefined';
|
|
298
|
+
|
|
299
|
+
if (!isTestEnv && !fsSync.existsSync(contextPath)) {
|
|
300
|
+
throw new Error(
|
|
301
|
+
`Build context path does not exist: ${contextPath}\n` +
|
|
302
|
+
`Expected dev directory: ${devDir}\n` +
|
|
303
|
+
'Please ensure files were copied correctly or update the context in variables.yaml.'
|
|
304
|
+
);
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return contextPath;
|
|
298
308
|
}
|
|
299
309
|
|
|
300
310
|
/**
|
|
301
|
-
*
|
|
311
|
+
* Handle Dockerfile generation
|
|
302
312
|
* @async
|
|
303
|
-
* @param {string}
|
|
304
|
-
* @param {
|
|
305
|
-
* @param {string}
|
|
306
|
-
* @param {
|
|
313
|
+
* @param {string} appName - Application name
|
|
314
|
+
* @param {Object} params - Parameters
|
|
315
|
+
* @param {string} params.devDir - Developer directory
|
|
316
|
+
* @param {Object} params.buildConfig - Build configuration
|
|
317
|
+
* @param {string} params.contextPath - Build context path
|
|
318
|
+
* @param {Object} params.appConfig - Application configuration
|
|
307
319
|
* @param {Object} options - Build options
|
|
320
|
+
* @returns {Promise<string>} Dockerfile path
|
|
308
321
|
*/
|
|
309
|
-
async function
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
322
|
+
async function handleDockerfileGeneration(appName, params, options, buildHelpers) {
|
|
323
|
+
const { devDir, buildConfig, contextPath, appConfig } = params;
|
|
324
|
+
const appDockerfilePath = path.join(devDir, 'Dockerfile');
|
|
325
|
+
const hasExistingDockerfile = fsSync.existsSync(appDockerfilePath) && !options.forceTemplate;
|
|
326
|
+
|
|
327
|
+
// Determine language (skip if existing Dockerfile found)
|
|
328
|
+
let language = options.language || buildConfig.language;
|
|
329
|
+
if (!language && !hasExistingDockerfile) {
|
|
330
|
+
language = detectLanguage(devDir);
|
|
331
|
+
} else if (!language) {
|
|
332
|
+
// Default language if existing Dockerfile is found (won't be used, but needed for API)
|
|
333
|
+
language = 'typescript';
|
|
316
334
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
async function postBuildTasks(appName, buildConfig) {
|
|
320
|
-
try {
|
|
321
|
-
const envPath = await secrets.generateEnvFile(appName, buildConfig.secrets, 'docker');
|
|
322
|
-
logger.log(chalk.green(`ā Generated .env file: ${envPath}`));
|
|
323
|
-
// Note: processEnvVariables is already called by generateEnvFile to generate local .env
|
|
324
|
-
// at the envOutputPath, so we don't need to manually copy the docker .env file
|
|
325
|
-
} catch (error) {
|
|
326
|
-
logger.log(chalk.yellow(`ā ļø Warning: Could not generate .env file: ${error.message}`));
|
|
335
|
+
if (!hasExistingDockerfile) {
|
|
336
|
+
logger.log(chalk.green(`ā Detected language: ${language}`));
|
|
327
337
|
}
|
|
338
|
+
|
|
339
|
+
// Determine Dockerfile (needs context path to generate in correct location)
|
|
340
|
+
return await buildHelpers.determineDockerfile(appName, {
|
|
341
|
+
language,
|
|
342
|
+
config: appConfig,
|
|
343
|
+
buildConfig,
|
|
344
|
+
contextPath,
|
|
345
|
+
forceTemplate: options.forceTemplate,
|
|
346
|
+
devDir: devDir
|
|
347
|
+
}, generateDockerfile);
|
|
328
348
|
}
|
|
329
349
|
|
|
330
350
|
/**
|
|
@@ -347,11 +367,7 @@ async function postBuildTasks(appName, buildConfig) {
|
|
|
347
367
|
*/
|
|
348
368
|
async function buildApp(appName, options = {}) {
|
|
349
369
|
// Check if app type is external - generate JSON files only (not deploy)
|
|
350
|
-
|
|
351
|
-
if (variables.app && variables.app.type === 'external') {
|
|
352
|
-
const generator = require('./generator');
|
|
353
|
-
const jsonPath = await generator.generateDeployJson(appName);
|
|
354
|
-
logger.log(chalk.green(`ā Generated deployment JSON: ${jsonPath}`));
|
|
370
|
+
if (await checkExternalAppType(appName)) {
|
|
355
371
|
return null;
|
|
356
372
|
}
|
|
357
373
|
|
|
@@ -359,131 +375,27 @@ async function buildApp(appName, options = {}) {
|
|
|
359
375
|
logger.log(chalk.blue(`\nšØ Building application: ${appName}`));
|
|
360
376
|
|
|
361
377
|
// 1. Load and validate configuration
|
|
362
|
-
const {
|
|
363
|
-
|
|
364
|
-
// 2. Get developer ID and copy files to dev-specific directory
|
|
365
|
-
const developerId = await config.getDeveloperId();
|
|
366
|
-
const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
|
|
367
|
-
const directoryName = idNum === 0 ? 'applications' : `dev-${developerId}`;
|
|
368
|
-
logger.log(chalk.blue(`Copying files to developer-specific directory (${directoryName})...`));
|
|
369
|
-
const devDir = await buildCopy.copyBuilderToDevDirectory(appName, developerId);
|
|
370
|
-
logger.log(chalk.green(`ā Files copied to: ${devDir}`));
|
|
371
|
-
const effectiveImageName = buildDevImageName(imageName, developerId);
|
|
372
|
-
|
|
373
|
-
// 2a. Check if application source files exist, if not copy from templates
|
|
374
|
-
const appsPath = path.join(process.cwd(), 'apps', appName);
|
|
375
|
-
if (fsSync.existsSync(appsPath)) {
|
|
376
|
-
// Copy app source files from apps directory
|
|
377
|
-
await buildCopy.copyAppSourceFiles(appsPath, devDir);
|
|
378
|
-
logger.log(chalk.green(`ā Copied application source files from apps/${appName}`));
|
|
379
|
-
} else {
|
|
380
|
-
// No apps directory - check if we need to copy template files
|
|
381
|
-
const language = options.language || buildConfig.language || detectLanguage(devDir);
|
|
382
|
-
const packageJsonPath = path.join(devDir, 'package.json');
|
|
383
|
-
const requirementsPath = path.join(devDir, 'requirements.txt');
|
|
384
|
-
|
|
385
|
-
if (language === 'typescript' && !fsSync.existsSync(packageJsonPath)) {
|
|
386
|
-
// Copy TypeScript template files
|
|
387
|
-
const templatePath = path.join(__dirname, '..', 'templates', 'typescript');
|
|
388
|
-
await copyTemplateFilesToDevDir(templatePath, devDir, language);
|
|
389
|
-
logger.log(chalk.green(`ā Generated application files from ${language} template`));
|
|
390
|
-
} else if (language === 'python' && !fsSync.existsSync(requirementsPath)) {
|
|
391
|
-
// Copy Python template files
|
|
392
|
-
const templatePath = path.join(__dirname, '..', 'templates', 'python');
|
|
393
|
-
await copyTemplateFilesToDevDir(templatePath, devDir, language);
|
|
394
|
-
logger.log(chalk.green(`ā Generated application files from ${language} template`));
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
// 3. Prepare build context (use dev-specific directory)
|
|
399
|
-
// If buildConfig.context is relative, resolve it relative to devDir
|
|
400
|
-
// If buildConfig.context is '../..' (apps flag), keep it as is (it's relative to devDir)
|
|
401
|
-
let contextPath;
|
|
402
|
-
|
|
403
|
-
// Check if context is using old format (../appName) - these are incompatible with dev directory structure
|
|
404
|
-
if (buildConfig.context && buildConfig.context.startsWith('../') && buildConfig.context !== '../..') {
|
|
405
|
-
// Old format detected - always use devDir instead
|
|
406
|
-
logger.log(chalk.yellow(`ā ļø Warning: Build context uses old format: ${buildConfig.context}`));
|
|
407
|
-
logger.log(chalk.yellow(` Using dev directory instead: ${devDir}`));
|
|
408
|
-
contextPath = devDir;
|
|
409
|
-
} else if (buildConfig.context && buildConfig.context !== '../..') {
|
|
410
|
-
// Resolve relative context path from dev directory
|
|
411
|
-
contextPath = path.resolve(devDir, buildConfig.context);
|
|
412
|
-
} else if (buildConfig.context === '../..') {
|
|
413
|
-
// For apps flag, context is relative to devDir (which is ~/.aifabrix/<app>-dev-<id>)
|
|
414
|
-
// So '../..' from devDir goes to ~/.aifabrix, then we need to go to project root
|
|
415
|
-
// Actually, we need to keep the relative path and let Docker resolve it
|
|
416
|
-
// But the build context should be the project root, not devDir
|
|
417
|
-
contextPath = process.cwd();
|
|
418
|
-
} else {
|
|
419
|
-
// No context specified, use dev directory
|
|
420
|
-
contextPath = devDir;
|
|
421
|
-
}
|
|
378
|
+
const { buildConfig } = await buildHelpers.loadAndValidateConfig(appName);
|
|
422
379
|
|
|
423
|
-
//
|
|
424
|
-
|
|
380
|
+
// 2. Prepare dev directory and copy files
|
|
381
|
+
const { devDir, effectiveImageName, imageName, appConfig } = await prepareDevDirectory(appName, buildConfig, options);
|
|
425
382
|
|
|
426
|
-
//
|
|
427
|
-
const
|
|
428
|
-
process.env.JEST_WORKER_ID !== undefined ||
|
|
429
|
-
typeof jest !== 'undefined';
|
|
383
|
+
// 3. Prepare build context
|
|
384
|
+
const contextPath = prepareBuildContext(buildConfig, devDir);
|
|
430
385
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
`Expected dev directory: ${devDir}\n` +
|
|
435
|
-
'Please ensure files were copied correctly or update the context in variables.yaml.'
|
|
436
|
-
);
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
// 4. Check if Dockerfile exists in dev directory
|
|
440
|
-
const appDockerfilePath = path.join(devDir, 'Dockerfile');
|
|
441
|
-
const hasExistingDockerfile = fsSync.existsSync(appDockerfilePath) && !options.forceTemplate;
|
|
442
|
-
|
|
443
|
-
// 5. Determine language (skip if existing Dockerfile found)
|
|
444
|
-
let language = options.language || buildConfig.language;
|
|
445
|
-
if (!language && !hasExistingDockerfile) {
|
|
446
|
-
language = detectLanguage(devDir);
|
|
447
|
-
} else if (!language) {
|
|
448
|
-
// Default language if existing Dockerfile is found (won't be used, but needed for API)
|
|
449
|
-
language = 'typescript';
|
|
450
|
-
}
|
|
451
|
-
if (!hasExistingDockerfile) {
|
|
452
|
-
logger.log(chalk.green(`ā Detected language: ${language}`));
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
// 6. Determine Dockerfile (needs context path to generate in correct location)
|
|
456
|
-
// Use dev directory for Dockerfile generation
|
|
457
|
-
const dockerfilePath = await determineDockerfile(appName, {
|
|
458
|
-
language,
|
|
459
|
-
config: appConfig,
|
|
386
|
+
// 4. Handle Dockerfile generation
|
|
387
|
+
const dockerfilePath = await handleDockerfileGeneration(appName, {
|
|
388
|
+
devDir,
|
|
460
389
|
buildConfig,
|
|
461
390
|
contextPath,
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
});
|
|
391
|
+
appConfig
|
|
392
|
+
}, options, buildHelpers);
|
|
465
393
|
|
|
466
|
-
//
|
|
394
|
+
// 5. Execute Docker build
|
|
467
395
|
const tag = options.tag || 'latest';
|
|
396
|
+
await dockerBuild.executeDockerBuildWithTag(effectiveImageName, imageName, dockerfilePath, contextPath, tag, options);
|
|
468
397
|
|
|
469
|
-
//
|
|
470
|
-
logger.log(chalk.blue(`Using Dockerfile: ${dockerfilePath}`));
|
|
471
|
-
logger.log(chalk.blue(`Using build context: ${contextPath}`));
|
|
472
|
-
|
|
473
|
-
await executeBuild(effectiveImageName, dockerfilePath, contextPath, tag, options);
|
|
474
|
-
// Back-compat: also tag the built dev image as the base image name
|
|
475
|
-
try {
|
|
476
|
-
// Use runtime promisify so tests can capture this call reliably
|
|
477
|
-
const { promisify } = require('util');
|
|
478
|
-
const { exec } = require('child_process');
|
|
479
|
-
const run = promisify(exec);
|
|
480
|
-
await run(`docker tag ${effectiveImageName}:${tag} ${imageName}:${tag}`);
|
|
481
|
-
logger.log(chalk.green(`ā Tagged image: ${imageName}:${tag}`));
|
|
482
|
-
} catch (err) {
|
|
483
|
-
logger.log(chalk.yellow(`ā ļø Warning: Could not create compatibility tag ${imageName}:${tag} - ${err.message}`));
|
|
484
|
-
}
|
|
485
|
-
|
|
486
|
-
// 7. Post-build tasks
|
|
398
|
+
// 6. Post-build tasks
|
|
487
399
|
await postBuildTasks(appName, buildConfig);
|
|
488
400
|
|
|
489
401
|
logger.log(chalk.green('\nā
Build completed successfully!'));
|
|
@@ -493,5 +405,4 @@ async function buildApp(appName, options = {}) {
|
|
|
493
405
|
throw new Error(`Build failed: ${error.message}`);
|
|
494
406
|
}
|
|
495
407
|
}
|
|
496
|
-
|
|
497
|
-
module.exports = { loadVariablesYaml, resolveContextPath, executeDockerBuild: dockerBuild.executeDockerBuild, detectLanguage, generateDockerfile, buildApp, postBuildTasks };
|
|
408
|
+
module.exports = { loadVariablesYaml, resolveContextPath, detectLanguage, generateDockerfile, buildApp, postBuildTasks };
|
package/lib/cli.js
CHANGED
|
@@ -21,6 +21,7 @@ const chalk = require('chalk');
|
|
|
21
21
|
const logger = require('./utils/logger');
|
|
22
22
|
const { validateCommand, handleCommandError } = require('./utils/cli-utils');
|
|
23
23
|
const { handleLogin } = require('./commands/login');
|
|
24
|
+
const { handleLogout } = require('./commands/logout');
|
|
24
25
|
const { handleSecure } = require('./commands/secure');
|
|
25
26
|
const { handleSecretsSet } = require('./commands/secrets-set');
|
|
26
27
|
|
|
@@ -49,6 +50,20 @@ function setupCommands(program) {
|
|
|
49
50
|
}
|
|
50
51
|
});
|
|
51
52
|
|
|
53
|
+
program.command('logout')
|
|
54
|
+
.description('Clear authentication tokens')
|
|
55
|
+
.option('-c, --controller <url>', 'Clear device tokens for specific controller')
|
|
56
|
+
.option('-e, --environment <env>', 'Clear client tokens for specific environment')
|
|
57
|
+
.option('-a, --app <app>', 'Clear client tokens for specific app (requires --environment)')
|
|
58
|
+
.action(async(options) => {
|
|
59
|
+
try {
|
|
60
|
+
await handleLogout(options);
|
|
61
|
+
} catch (error) {
|
|
62
|
+
handleCommandError(error, 'logout');
|
|
63
|
+
process.exit(1);
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
|
|
52
67
|
// Infrastructure commands
|
|
53
68
|
program.command('up')
|
|
54
69
|
.description('Start local infrastructure services (Postgres, Redis, pgAdmin, Redis Commander)')
|
|
@@ -474,14 +489,43 @@ function setupCommands(program) {
|
|
|
474
489
|
});
|
|
475
490
|
|
|
476
491
|
// Developer configuration commands
|
|
477
|
-
|
|
492
|
+
const dev = program
|
|
493
|
+
.command('dev')
|
|
494
|
+
.description('Developer configuration and isolation');
|
|
495
|
+
|
|
496
|
+
// Helper function to display developer configuration
|
|
497
|
+
async function displayDevConfig(devId) {
|
|
498
|
+
const devIdNum = parseInt(devId, 10);
|
|
499
|
+
const ports = devConfig.getDevPorts(devIdNum);
|
|
500
|
+
const configVars = [
|
|
501
|
+
{ key: 'aifabrix-home', value: await config.getAifabrixHomeOverride() },
|
|
502
|
+
{ key: 'aifabrix-secrets', value: await config.getAifabrixSecretsPath() },
|
|
503
|
+
{ key: 'aifabrix-env-config', value: await config.getAifabrixEnvConfigPath() }
|
|
504
|
+
].filter(v => v.value);
|
|
505
|
+
|
|
506
|
+
logger.log('\nš§ Developer Configuration\n');
|
|
507
|
+
logger.log(`Developer ID: ${devId}`);
|
|
508
|
+
logger.log('\nPorts:');
|
|
509
|
+
logger.log(` App: ${ports.app}`);
|
|
510
|
+
logger.log(` Postgres: ${ports.postgres}`);
|
|
511
|
+
logger.log(` Redis: ${ports.redis}`);
|
|
512
|
+
logger.log(` pgAdmin: ${ports.pgadmin}`);
|
|
513
|
+
logger.log(` Redis Commander: ${ports.redisCommander}`);
|
|
514
|
+
|
|
515
|
+
if (configVars.length > 0) {
|
|
516
|
+
logger.log('\nConfiguration:');
|
|
517
|
+
configVars.forEach(v => logger.log(` ${v.key}: ${v.value}`));
|
|
518
|
+
}
|
|
519
|
+
logger.log('');
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
// Config subcommand
|
|
523
|
+
dev
|
|
524
|
+
.command('config')
|
|
478
525
|
.description('Show or set developer configuration')
|
|
479
526
|
.option('--set-id <id>', 'Set developer ID')
|
|
480
|
-
.action(async(
|
|
527
|
+
.action(async(options) => {
|
|
481
528
|
try {
|
|
482
|
-
// For commands with spaces like 'dev config', Commander.js passes command name as first arg
|
|
483
|
-
// Options are passed as second arg, or if only one arg, it might be the command name
|
|
484
|
-
const options = typeof cmdName === 'object' && cmdName !== null ? cmdName : (opts || {});
|
|
485
529
|
// Commander.js converts --set-id to setId in options object
|
|
486
530
|
const setIdValue = options.setId || options['set-id'];
|
|
487
531
|
if (setIdValue) {
|
|
@@ -493,77 +537,41 @@ function setupCommands(program) {
|
|
|
493
537
|
await config.setDeveloperId(setIdValue);
|
|
494
538
|
process.env.AIFABRIX_DEVELOPERID = setIdValue;
|
|
495
539
|
logger.log(chalk.green(`ā Developer ID set to ${setIdValue}`));
|
|
496
|
-
// Convert to number only for getDevPorts (which requires a number)
|
|
497
|
-
const devIdNum = parseInt(setIdValue, 10);
|
|
498
540
|
// Use the ID we just set instead of reading from file to avoid race conditions
|
|
499
|
-
|
|
500
|
-
logger.log('\nš§ Developer Configuration\n');
|
|
501
|
-
logger.log(`Developer ID: ${setIdValue}`);
|
|
502
|
-
logger.log('\nPorts:');
|
|
503
|
-
logger.log(` App: ${ports.app}`);
|
|
504
|
-
logger.log(` Postgres: ${ports.postgres}`);
|
|
505
|
-
logger.log(` Redis: ${ports.redis}`);
|
|
506
|
-
logger.log(` pgAdmin: ${ports.pgadmin}`);
|
|
507
|
-
logger.log(` Redis Commander: ${ports.redisCommander}`);
|
|
508
|
-
|
|
509
|
-
// Display configuration variables if set
|
|
510
|
-
const aifabrixHome = await config.getAifabrixHomeOverride();
|
|
511
|
-
const aifabrixSecrets = await config.getAifabrixSecretsPath();
|
|
512
|
-
const aifabrixEnvConfig = await config.getAifabrixEnvConfigPath();
|
|
513
|
-
|
|
514
|
-
if (aifabrixHome || aifabrixSecrets || aifabrixEnvConfig) {
|
|
515
|
-
logger.log('\nConfiguration:');
|
|
516
|
-
if (aifabrixHome) {
|
|
517
|
-
logger.log(` aifabrix-home: ${aifabrixHome}`);
|
|
518
|
-
}
|
|
519
|
-
if (aifabrixSecrets) {
|
|
520
|
-
logger.log(` aifabrix-secrets: ${aifabrixSecrets}`);
|
|
521
|
-
}
|
|
522
|
-
if (aifabrixEnvConfig) {
|
|
523
|
-
logger.log(` aifabrix-env-config: ${aifabrixEnvConfig}`);
|
|
524
|
-
}
|
|
525
|
-
}
|
|
526
|
-
logger.log('');
|
|
541
|
+
await displayDevConfig(setIdValue);
|
|
527
542
|
return;
|
|
528
543
|
}
|
|
529
544
|
|
|
530
545
|
const devId = await config.getDeveloperId();
|
|
531
|
-
|
|
532
|
-
const devIdNum = parseInt(devId, 10);
|
|
533
|
-
const ports = devConfig.getDevPorts(devIdNum);
|
|
534
|
-
logger.log('\nš§ Developer Configuration\n');
|
|
535
|
-
logger.log(`Developer ID: ${devId}`);
|
|
536
|
-
logger.log('\nPorts:');
|
|
537
|
-
logger.log(` App: ${ports.app}`);
|
|
538
|
-
logger.log(` Postgres: ${ports.postgres}`);
|
|
539
|
-
logger.log(` Redis: ${ports.redis}`);
|
|
540
|
-
logger.log(` pgAdmin: ${ports.pgadmin}`);
|
|
541
|
-
logger.log(` Redis Commander: ${ports.redisCommander}`);
|
|
542
|
-
|
|
543
|
-
// Display configuration variables if set
|
|
544
|
-
const aifabrixHome = await config.getAifabrixHomeOverride();
|
|
545
|
-
const aifabrixSecrets = await config.getAifabrixSecretsPath();
|
|
546
|
-
const aifabrixEnvConfig = await config.getAifabrixEnvConfigPath();
|
|
547
|
-
|
|
548
|
-
if (aifabrixHome || aifabrixSecrets || aifabrixEnvConfig) {
|
|
549
|
-
logger.log('\nConfiguration:');
|
|
550
|
-
if (aifabrixHome) {
|
|
551
|
-
logger.log(` aifabrix-home: ${aifabrixHome}`);
|
|
552
|
-
}
|
|
553
|
-
if (aifabrixSecrets) {
|
|
554
|
-
logger.log(` aifabrix-secrets: ${aifabrixSecrets}`);
|
|
555
|
-
}
|
|
556
|
-
if (aifabrixEnvConfig) {
|
|
557
|
-
logger.log(` aifabrix-env-config: ${aifabrixEnvConfig}`);
|
|
558
|
-
}
|
|
559
|
-
}
|
|
560
|
-
logger.log('');
|
|
546
|
+
await displayDevConfig(devId);
|
|
561
547
|
} catch (error) {
|
|
562
548
|
handleCommandError(error, 'dev config');
|
|
563
549
|
process.exit(1);
|
|
564
550
|
}
|
|
565
551
|
});
|
|
566
552
|
|
|
553
|
+
// Set-id subcommand
|
|
554
|
+
dev
|
|
555
|
+
.command('set-id <id>')
|
|
556
|
+
.description('Set developer ID (convenience alias for "dev config --set-id")')
|
|
557
|
+
.action(async(id) => {
|
|
558
|
+
try {
|
|
559
|
+
const digitsOnly = /^[0-9]+$/.test(id);
|
|
560
|
+
if (!digitsOnly) {
|
|
561
|
+
throw new Error('Developer ID must be a non-negative digit string (0 = default infra, > 0 = developer-specific)');
|
|
562
|
+
}
|
|
563
|
+
// Preserve the original string value to maintain leading zeros (e.g., "01")
|
|
564
|
+
await config.setDeveloperId(id);
|
|
565
|
+
process.env.AIFABRIX_DEVELOPERID = id;
|
|
566
|
+
logger.log(chalk.green(`ā Developer ID set to ${id}`));
|
|
567
|
+
// Use the ID we just set instead of reading from file to avoid race conditions
|
|
568
|
+
await displayDevConfig(id);
|
|
569
|
+
} catch (error) {
|
|
570
|
+
handleCommandError(error, 'dev set-id');
|
|
571
|
+
process.exit(1);
|
|
572
|
+
}
|
|
573
|
+
});
|
|
574
|
+
|
|
567
575
|
// Secrets management commands
|
|
568
576
|
const secretsCmd = program
|
|
569
577
|
.command('secrets')
|