@aifabrix/builder 2.1.7 → 2.2.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/lib/app-deploy.js +73 -29
- package/lib/app-list.js +132 -0
- package/lib/app-readme.js +11 -4
- package/lib/app-register.js +435 -0
- package/lib/app-rotate-secret.js +164 -0
- package/lib/app-run.js +98 -84
- package/lib/app.js +13 -0
- package/lib/audit-logger.js +195 -15
- package/lib/build.js +57 -37
- package/lib/cli.js +90 -8
- package/lib/commands/app.js +8 -391
- package/lib/commands/login.js +130 -36
- package/lib/config.js +257 -4
- package/lib/deployer.js +221 -183
- package/lib/infra.js +177 -112
- package/lib/secrets.js +17 -0
- package/lib/utils/api-error-handler.js +465 -0
- package/lib/utils/api.js +165 -16
- package/lib/utils/auth-headers.js +84 -0
- package/lib/utils/build-copy.js +144 -0
- package/lib/utils/cli-utils.js +21 -0
- package/lib/utils/compose-generator.js +43 -14
- package/lib/utils/deployment-errors.js +90 -0
- package/lib/utils/deployment-validation.js +60 -0
- package/lib/utils/dev-config.js +83 -0
- package/lib/utils/env-template.js +30 -10
- package/lib/utils/health-check.js +18 -1
- package/lib/utils/infra-containers.js +101 -0
- package/lib/utils/local-secrets.js +0 -2
- package/lib/utils/token-manager.js +381 -0
- package/package.json +1 -1
- package/templates/applications/README.md.hbs +155 -23
- package/templates/applications/miso-controller/Dockerfile +7 -119
- package/templates/infra/compose.yaml.hbs +93 -0
- package/templates/python/docker-compose.hbs +25 -17
- package/templates/typescript/docker-compose.hbs +25 -17
package/lib/build.js
CHANGED
|
@@ -19,10 +19,12 @@ const { promisify } = require('util');
|
|
|
19
19
|
const chalk = require('chalk');
|
|
20
20
|
const yaml = require('js-yaml');
|
|
21
21
|
const secrets = require('./secrets');
|
|
22
|
+
const config = require('./config');
|
|
22
23
|
const logger = require('./utils/logger');
|
|
23
24
|
const validator = require('./validator');
|
|
24
25
|
const dockerfileUtils = require('./utils/dockerfile-utils');
|
|
25
26
|
const dockerBuild = require('./utils/docker-build');
|
|
27
|
+
const buildCopy = require('./utils/build-copy');
|
|
26
28
|
|
|
27
29
|
const execAsync = promisify(exec);
|
|
28
30
|
|
|
@@ -127,7 +129,7 @@ function detectLanguage(appPath) {
|
|
|
127
129
|
* const dockerfilePath = await generateDockerfile('myapp', 'typescript', config);
|
|
128
130
|
* // Returns: '~/.aifabrix/myapp/Dockerfile.typescript'
|
|
129
131
|
*/
|
|
130
|
-
async function generateDockerfile(appNameOrPath, language, config, buildConfig = {}) {
|
|
132
|
+
async function generateDockerfile(appNameOrPath, language, config, buildConfig = {}, devDir = null) {
|
|
131
133
|
let appName;
|
|
132
134
|
if (appNameOrPath.includes(path.sep) || appNameOrPath.includes('/') || appNameOrPath.includes('\\')) {
|
|
133
135
|
appName = path.basename(appNameOrPath);
|
|
@@ -151,12 +153,19 @@ async function generateDockerfile(appNameOrPath, language, config, buildConfig =
|
|
|
151
153
|
|
|
152
154
|
const dockerfileContent = dockerfileUtils.renderDockerfile(template, templateVars, language, isAppFlag, appSourcePath);
|
|
153
155
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
156
|
+
// Use dev directory if provided, otherwise use default aifabrix directory
|
|
157
|
+
let targetDir;
|
|
158
|
+
if (devDir) {
|
|
159
|
+
targetDir = devDir;
|
|
160
|
+
} else {
|
|
161
|
+
targetDir = path.join(os.homedir(), '.aifabrix', appName);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!fsSync.existsSync(targetDir)) {
|
|
165
|
+
await fs.mkdir(targetDir, { recursive: true });
|
|
157
166
|
}
|
|
158
167
|
|
|
159
|
-
const dockerfilePath = path.join(
|
|
168
|
+
const dockerfilePath = path.join(targetDir, 'Dockerfile');
|
|
160
169
|
await fs.writeFile(dockerfilePath, dockerfileContent, 'utf8');
|
|
161
170
|
|
|
162
171
|
return dockerfilePath;
|
|
@@ -175,41 +184,29 @@ async function generateDockerfile(appNameOrPath, language, config, buildConfig =
|
|
|
175
184
|
* @returns {Promise<string>} Path to Dockerfile
|
|
176
185
|
*/
|
|
177
186
|
async function determineDockerfile(appName, options) {
|
|
178
|
-
|
|
187
|
+
// Use dev directory if provided, otherwise fall back to builder directory
|
|
188
|
+
const searchPath = options.devDir || path.join(process.cwd(), 'builder', appName);
|
|
179
189
|
|
|
180
|
-
const templateDockerfile = dockerfileUtils.checkTemplateDockerfile(
|
|
190
|
+
const templateDockerfile = dockerfileUtils.checkTemplateDockerfile(searchPath, appName, options.forceTemplate);
|
|
181
191
|
if (templateDockerfile) {
|
|
182
|
-
|
|
192
|
+
const relativePath = path.relative(process.cwd(), templateDockerfile);
|
|
193
|
+
logger.log(chalk.green(`✓ Using existing Dockerfile: ${relativePath}`));
|
|
183
194
|
return templateDockerfile;
|
|
184
195
|
}
|
|
185
196
|
|
|
186
|
-
const customDockerfile = dockerfileUtils.checkProjectDockerfile(
|
|
197
|
+
const customDockerfile = dockerfileUtils.checkProjectDockerfile(searchPath, appName, options.buildConfig, options.contextPath, options.forceTemplate);
|
|
187
198
|
if (customDockerfile) {
|
|
188
199
|
logger.log(chalk.green(`✓ Using custom Dockerfile: ${options.buildConfig.dockerfile}`));
|
|
189
200
|
return customDockerfile;
|
|
190
201
|
}
|
|
191
202
|
|
|
192
|
-
|
|
203
|
+
// Generate Dockerfile in dev directory if provided
|
|
204
|
+
const dockerfilePath = await generateDockerfile(appName, options.language, options.config, options.buildConfig, options.devDir);
|
|
193
205
|
const relativePath = path.relative(process.cwd(), dockerfilePath);
|
|
194
206
|
logger.log(chalk.green(`✓ Generated Dockerfile from template: ${relativePath}`));
|
|
195
207
|
return dockerfilePath;
|
|
196
208
|
}
|
|
197
209
|
|
|
198
|
-
/**
|
|
199
|
-
* Prepares build context path
|
|
200
|
-
* @param {string} appName - Application name
|
|
201
|
-
* @param {string} contextPath - Relative context path
|
|
202
|
-
* @returns {string} Absolute context path
|
|
203
|
-
*/
|
|
204
|
-
function prepareBuildContext(appName, contextPath) {
|
|
205
|
-
// Ensure contextPath is a string
|
|
206
|
-
const context = typeof contextPath === 'string' ? contextPath : (contextPath || '');
|
|
207
|
-
return resolveContextPath(
|
|
208
|
-
path.join(process.cwd(), 'builder', appName),
|
|
209
|
-
context
|
|
210
|
-
);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
210
|
/**
|
|
214
211
|
* Loads and validates configuration for build
|
|
215
212
|
* @async
|
|
@@ -309,20 +306,41 @@ async function buildApp(appName, options = {}) {
|
|
|
309
306
|
logger.log(chalk.blue(`\n🔨 Building application: ${appName}`));
|
|
310
307
|
|
|
311
308
|
// 1. Load and validate configuration
|
|
312
|
-
const { config, imageName, buildConfig } = await loadAndValidateConfig(appName);
|
|
313
|
-
|
|
314
|
-
// 2.
|
|
315
|
-
const
|
|
309
|
+
const { config: appConfig, imageName, buildConfig } = await loadAndValidateConfig(appName);
|
|
310
|
+
|
|
311
|
+
// 2. Get developer ID and copy files to dev-specific directory
|
|
312
|
+
const developerId = await config.getDeveloperId();
|
|
313
|
+
const directoryName = developerId === 0 ? 'applications' : `dev-${developerId}`;
|
|
314
|
+
logger.log(chalk.blue(`Copying files to developer-specific directory (${directoryName})...`));
|
|
315
|
+
const devDir = await buildCopy.copyBuilderToDevDirectory(appName, developerId);
|
|
316
|
+
logger.log(chalk.green(`✓ Files copied to: ${devDir}`));
|
|
317
|
+
|
|
318
|
+
// 3. Prepare build context (use dev-specific directory)
|
|
319
|
+
// If buildConfig.context is relative, resolve it relative to devDir
|
|
320
|
+
// If buildConfig.context is '../..' (apps flag), keep it as is (it's relative to devDir)
|
|
321
|
+
let contextPath;
|
|
322
|
+
if (buildConfig.context && buildConfig.context !== '../..') {
|
|
323
|
+
// Resolve relative context path from dev directory
|
|
324
|
+
contextPath = path.resolve(devDir, buildConfig.context);
|
|
325
|
+
} else if (buildConfig.context === '../..') {
|
|
326
|
+
// For apps flag, context is relative to devDir (which is ~/.aifabrix/<app>-dev-<id>)
|
|
327
|
+
// So '../..' from devDir goes to ~/.aifabrix, then we need to go to project root
|
|
328
|
+
// Actually, we need to keep the relative path and let Docker resolve it
|
|
329
|
+
// But the build context should be the project root, not devDir
|
|
330
|
+
contextPath = process.cwd();
|
|
331
|
+
} else {
|
|
332
|
+
// No context specified, use dev directory
|
|
333
|
+
contextPath = devDir;
|
|
334
|
+
}
|
|
316
335
|
|
|
317
|
-
//
|
|
318
|
-
const
|
|
319
|
-
const appDockerfilePath = path.join(builderPath, 'Dockerfile');
|
|
336
|
+
// 4. Check if Dockerfile exists in dev directory
|
|
337
|
+
const appDockerfilePath = path.join(devDir, 'Dockerfile');
|
|
320
338
|
const hasExistingDockerfile = fsSync.existsSync(appDockerfilePath) && !options.forceTemplate;
|
|
321
339
|
|
|
322
|
-
//
|
|
340
|
+
// 5. Determine language (skip if existing Dockerfile found)
|
|
323
341
|
let language = options.language || buildConfig.language;
|
|
324
342
|
if (!language && !hasExistingDockerfile) {
|
|
325
|
-
language = detectLanguage(
|
|
343
|
+
language = detectLanguage(devDir);
|
|
326
344
|
} else if (!language) {
|
|
327
345
|
// Default language if existing Dockerfile is found (won't be used, but needed for API)
|
|
328
346
|
language = 'typescript';
|
|
@@ -331,13 +349,15 @@ async function buildApp(appName, options = {}) {
|
|
|
331
349
|
logger.log(chalk.green(`✓ Detected language: ${language}`));
|
|
332
350
|
}
|
|
333
351
|
|
|
334
|
-
//
|
|
352
|
+
// 6. Determine Dockerfile (needs context path to generate in correct location)
|
|
353
|
+
// Use dev directory for Dockerfile generation
|
|
335
354
|
const dockerfilePath = await determineDockerfile(appName, {
|
|
336
355
|
language,
|
|
337
|
-
config,
|
|
356
|
+
config: appConfig,
|
|
338
357
|
buildConfig,
|
|
339
358
|
contextPath,
|
|
340
|
-
forceTemplate: options.forceTemplate
|
|
359
|
+
forceTemplate: options.forceTemplate,
|
|
360
|
+
devDir: devDir
|
|
341
361
|
});
|
|
342
362
|
|
|
343
363
|
// 6. Build Docker image
|
package/lib/cli.js
CHANGED
|
@@ -15,6 +15,8 @@ const secrets = require('./secrets');
|
|
|
15
15
|
const generator = require('./generator');
|
|
16
16
|
const validator = require('./validator');
|
|
17
17
|
const keyGenerator = require('./key-generator');
|
|
18
|
+
const config = require('./config');
|
|
19
|
+
const devConfig = require('./utils/dev-config');
|
|
18
20
|
const chalk = require('chalk');
|
|
19
21
|
const logger = require('./utils/logger');
|
|
20
22
|
const { validateCommand, handleCommandError } = require('./utils/cli-utils');
|
|
@@ -28,11 +30,12 @@ function setupCommands(program) {
|
|
|
28
30
|
// Authentication command
|
|
29
31
|
program.command('login')
|
|
30
32
|
.description('Authenticate with Miso Controller')
|
|
31
|
-
.option('-
|
|
33
|
+
.option('-c, --controller <url>', 'Controller URL', 'http://localhost:3000')
|
|
32
34
|
.option('-m, --method <method>', 'Authentication method (device|credentials)')
|
|
33
|
-
.option('
|
|
34
|
-
.option('--client-
|
|
35
|
-
.option('-
|
|
35
|
+
.option('-a, --app <app>', 'Application name (required for credentials method, reads from secrets.local.yaml)')
|
|
36
|
+
.option('--client-id <id>', 'Client ID (for credentials method, overrides secrets.local.yaml)')
|
|
37
|
+
.option('--client-secret <secret>', 'Client Secret (for credentials method, overrides secrets.local.yaml)')
|
|
38
|
+
.option('-e, --environment <env>', 'Environment key (updates root-level environment in config.yaml, e.g., miso, dev, tst, pro)')
|
|
36
39
|
.action(async(options) => {
|
|
37
40
|
try {
|
|
38
41
|
await handleLogin(options);
|
|
@@ -45,9 +48,21 @@ function setupCommands(program) {
|
|
|
45
48
|
// Infrastructure commands
|
|
46
49
|
program.command('up')
|
|
47
50
|
.description('Start local infrastructure services (Postgres, Redis, pgAdmin, Redis Commander)')
|
|
48
|
-
.
|
|
51
|
+
.option('-d, --developer <id>', 'Set developer ID and start infrastructure')
|
|
52
|
+
.action(async(options) => {
|
|
49
53
|
try {
|
|
50
|
-
|
|
54
|
+
let developerId = null;
|
|
55
|
+
if (options.developer) {
|
|
56
|
+
const id = parseInt(options.developer, 10);
|
|
57
|
+
if (isNaN(id) || id < 0) {
|
|
58
|
+
throw new Error('Developer ID must be a non-negative number (0 = default infra, > 0 = developer-specific)');
|
|
59
|
+
}
|
|
60
|
+
await config.setDeveloperId(id);
|
|
61
|
+
process.env.AIFABRIX_DEVELOPERID = id.toString();
|
|
62
|
+
developerId = id;
|
|
63
|
+
logger.log(chalk.green(`✓ Developer ID set to ${id}`));
|
|
64
|
+
}
|
|
65
|
+
await infra.startInfra(developerId);
|
|
51
66
|
} catch (error) {
|
|
52
67
|
handleCommandError(error, 'up');
|
|
53
68
|
process.exit(1);
|
|
@@ -138,7 +153,7 @@ function setupCommands(program) {
|
|
|
138
153
|
program.command('deploy <app>')
|
|
139
154
|
.description('Deploy to Azure via Miso Controller')
|
|
140
155
|
.option('-c, --controller <url>', 'Controller URL')
|
|
141
|
-
.option('-e, --environment <env>', 'Environment (dev, tst, pro)', 'dev')
|
|
156
|
+
.option('-e, --environment <env>', 'Environment (miso, dev, tst, pro)', 'dev')
|
|
142
157
|
.option('--client-id <id>', 'Client ID (overrides config)')
|
|
143
158
|
.option('--client-secret <secret>', 'Client Secret (overrides config)')
|
|
144
159
|
.option('--poll', 'Poll for deployment status', true)
|
|
@@ -191,7 +206,7 @@ function setupCommands(program) {
|
|
|
191
206
|
});
|
|
192
207
|
|
|
193
208
|
program.command('status')
|
|
194
|
-
.description('Show detailed infrastructure service status')
|
|
209
|
+
.description('Show detailed infrastructure service status and running applications')
|
|
195
210
|
.action(async() => {
|
|
196
211
|
try {
|
|
197
212
|
const status = await infra.getInfraStatus();
|
|
@@ -207,6 +222,22 @@ function setupCommands(program) {
|
|
|
207
222
|
logger.log(` URL: ${info.url}`);
|
|
208
223
|
logger.log('');
|
|
209
224
|
});
|
|
225
|
+
|
|
226
|
+
// Show running applications
|
|
227
|
+
const apps = await infra.getAppStatus();
|
|
228
|
+
if (apps.length > 0) {
|
|
229
|
+
logger.log('📱 Running Applications\n');
|
|
230
|
+
apps.forEach((app) => {
|
|
231
|
+
const normalizedStatus = String(app.status).trim().toLowerCase();
|
|
232
|
+
const icon = normalizedStatus.includes('running') || normalizedStatus.includes('up') ? '✅' : '❌';
|
|
233
|
+
logger.log(`${icon} ${app.name}:`);
|
|
234
|
+
logger.log(` Container: ${app.container}`);
|
|
235
|
+
logger.log(` Port: ${app.port}`);
|
|
236
|
+
logger.log(` Status: ${app.status}`);
|
|
237
|
+
logger.log(` URL: ${app.url}`);
|
|
238
|
+
logger.log('');
|
|
239
|
+
});
|
|
240
|
+
}
|
|
210
241
|
} catch (error) {
|
|
211
242
|
handleCommandError(error, 'status');
|
|
212
243
|
process.exit(1);
|
|
@@ -291,6 +322,57 @@ function setupCommands(program) {
|
|
|
291
322
|
process.exit(1);
|
|
292
323
|
}
|
|
293
324
|
});
|
|
325
|
+
|
|
326
|
+
// Developer configuration commands
|
|
327
|
+
program.command('dev config')
|
|
328
|
+
.description('Show or set developer configuration')
|
|
329
|
+
.option('--set-id <id>', 'Set developer ID')
|
|
330
|
+
.action(async(cmdName, opts) => {
|
|
331
|
+
try {
|
|
332
|
+
// For commands with spaces like 'dev config', Commander.js passes command name as first arg
|
|
333
|
+
// Options are passed as second arg, or if only one arg, it might be the command name
|
|
334
|
+
const options = typeof cmdName === 'object' && cmdName !== null ? cmdName : (opts || {});
|
|
335
|
+
// Commander.js converts --set-id to setId in options object
|
|
336
|
+
const setIdValue = options.setId || options['set-id'];
|
|
337
|
+
if (setIdValue) {
|
|
338
|
+
const id = parseInt(setIdValue, 10);
|
|
339
|
+
if (isNaN(id) || id < 0) {
|
|
340
|
+
throw new Error('Developer ID must be a non-negative number (0 = default infra, > 0 = developer-specific)');
|
|
341
|
+
}
|
|
342
|
+
await config.setDeveloperId(id);
|
|
343
|
+
process.env.AIFABRIX_DEVELOPERID = id.toString();
|
|
344
|
+
logger.log(chalk.green(`✓ Developer ID set to ${id}`));
|
|
345
|
+
// Use the ID we just set instead of reading from file to avoid race conditions
|
|
346
|
+
const devId = id;
|
|
347
|
+
const ports = devConfig.getDevPorts(devId);
|
|
348
|
+
logger.log('\n🔧 Developer Configuration\n');
|
|
349
|
+
logger.log(`Developer ID: ${devId}`);
|
|
350
|
+
logger.log('\nPorts:');
|
|
351
|
+
logger.log(` App: ${ports.app}`);
|
|
352
|
+
logger.log(` Postgres: ${ports.postgres}`);
|
|
353
|
+
logger.log(` Redis: ${ports.redis}`);
|
|
354
|
+
logger.log(` pgAdmin: ${ports.pgadmin}`);
|
|
355
|
+
logger.log(` Redis Commander: ${ports.redisCommander}`);
|
|
356
|
+
logger.log('');
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
const devId = await config.getDeveloperId();
|
|
361
|
+
const ports = devConfig.getDevPorts(devId);
|
|
362
|
+
logger.log('\n🔧 Developer Configuration\n');
|
|
363
|
+
logger.log(`Developer ID: ${devId}`);
|
|
364
|
+
logger.log('\nPorts:');
|
|
365
|
+
logger.log(` App: ${ports.app}`);
|
|
366
|
+
logger.log(` Postgres: ${ports.postgres}`);
|
|
367
|
+
logger.log(` Redis: ${ports.redis}`);
|
|
368
|
+
logger.log(` pgAdmin: ${ports.pgadmin}`);
|
|
369
|
+
logger.log(` Redis Commander: ${ports.redisCommander}`);
|
|
370
|
+
logger.log('');
|
|
371
|
+
} catch (error) {
|
|
372
|
+
handleCommandError(error, 'dev config');
|
|
373
|
+
process.exit(1);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
294
376
|
}
|
|
295
377
|
|
|
296
378
|
module.exports = {
|