@aifabrix/builder 2.0.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/LICENSE +21 -0
- package/README.md +75 -0
- package/bin/aifabrix.js +51 -0
- package/lib/app-deploy.js +209 -0
- package/lib/app-run.js +291 -0
- package/lib/app.js +472 -0
- package/lib/audit-logger.js +162 -0
- package/lib/build.js +313 -0
- package/lib/cli.js +307 -0
- package/lib/deployer.js +256 -0
- package/lib/env-reader.js +250 -0
- package/lib/generator.js +361 -0
- package/lib/github-generator.js +220 -0
- package/lib/infra.js +300 -0
- package/lib/key-generator.js +93 -0
- package/lib/push.js +141 -0
- package/lib/schema/application-schema.json +649 -0
- package/lib/schema/env-config.yaml +15 -0
- package/lib/secrets.js +282 -0
- package/lib/templates.js +301 -0
- package/lib/validator.js +377 -0
- package/package.json +59 -0
- package/templates/README.md +51 -0
- package/templates/github/ci.yaml.hbs +15 -0
- package/templates/github/pr-checks.yaml.hbs +35 -0
- package/templates/github/release.yaml.hbs +79 -0
- package/templates/github/test.hbs +11 -0
- package/templates/github/test.yaml.hbs +11 -0
- package/templates/infra/compose.yaml +93 -0
- package/templates/python/Dockerfile.hbs +49 -0
- package/templates/python/docker-compose.hbs +69 -0
- package/templates/typescript/Dockerfile.hbs +46 -0
- package/templates/typescript/docker-compose.hbs +69 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 eSystems Nordic Ltd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# š§± @aifabrix/builder
|
|
2
|
+
|
|
3
|
+
Local development infrastructure + Azure deployment tool.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install -g @aifabrix/builder
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Quick Start
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
aifabrix up # Start Postgres + Redis
|
|
15
|
+
aifabrix create myapp # Create your app
|
|
16
|
+
aifabrix build myapp # Build Docker image
|
|
17
|
+
aifabrix run myapp # Run locally
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
ā [Full Guide](docs/QUICK-START.md) | [CLI Commands](docs/CLI-REFERENCE.md)
|
|
21
|
+
|
|
22
|
+
## What You Get
|
|
23
|
+
|
|
24
|
+
- **Local Postgres + Redis infrastructure** - Runs in Docker
|
|
25
|
+
- **Auto-generated Dockerfiles** - TypeScript and Python templates
|
|
26
|
+
- **Environment variable management** - Secret resolution with kv:// references
|
|
27
|
+
- **Azure deployment pipeline** - Push to ACR and deploy via controller
|
|
28
|
+
|
|
29
|
+
## Optional Platform Apps
|
|
30
|
+
|
|
31
|
+
Want authentication or deployment controller?
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
# Keycloak for authentication
|
|
35
|
+
aifabrix create keycloak --port 8082 --database --template platform
|
|
36
|
+
aifabrix build keycloak
|
|
37
|
+
aifabrix run keycloak
|
|
38
|
+
|
|
39
|
+
# Miso Controller for Azure deployments
|
|
40
|
+
aifabrix create miso-controller --port 3000 --database --redis --template platform
|
|
41
|
+
aifabrix build miso-controller
|
|
42
|
+
aifabrix run miso-controller
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
ā [Infrastructure Guide](docs/INFRASTRUCTURE.md)
|
|
46
|
+
|
|
47
|
+
## Documentation
|
|
48
|
+
|
|
49
|
+
- [Quick Start](docs/QUICK-START.md) - Get running in 5 minutes
|
|
50
|
+
- [Infrastructure](docs/INFRASTRUCTURE.md) - What runs and why
|
|
51
|
+
- [Configuration](docs/CONFIGURATION.md) - Config file reference
|
|
52
|
+
- [Building](docs/BUILDING.md) - Build process explained
|
|
53
|
+
- [Running](docs/RUNNING.md) - Run apps locally
|
|
54
|
+
- [Deploying](docs/DEPLOYING.md) - Deploy to Azure
|
|
55
|
+
- [CLI Reference](docs/CLI-REFERENCE.md) - All commands
|
|
56
|
+
|
|
57
|
+
## How It Works
|
|
58
|
+
|
|
59
|
+
1. **Infrastructure** - Minimal baseline (Postgres + Redis)
|
|
60
|
+
2. **Create** - Generate config files for your app
|
|
61
|
+
3. **Build** - Auto-detect runtime and build Docker image
|
|
62
|
+
4. **Run** - Start locally, connected to infrastructure
|
|
63
|
+
5. **Deploy** - Push to ACR and deploy via controller
|
|
64
|
+
|
|
65
|
+
## Requirements
|
|
66
|
+
|
|
67
|
+
- **Docker Desktop** - For running containers
|
|
68
|
+
- **Node.js 18+** - For running the CLI
|
|
69
|
+
- **Azure CLI** - For deploying to Azure (optional)
|
|
70
|
+
|
|
71
|
+
## License
|
|
72
|
+
|
|
73
|
+
Ā© eSystems Nordic Ltd 2025 - All Rights Reserved
|
|
74
|
+
|
|
75
|
+
`@aifabrix/builder` is part of the AI Fabrix platform.
|
package/bin/aifabrix.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AI Fabrix Builder CLI Entry Point
|
|
5
|
+
*
|
|
6
|
+
* This is the main entry point for the @aifabrix/builder CLI tool.
|
|
7
|
+
* It initializes Commander.js and delegates command handling to lib/cli.js
|
|
8
|
+
*
|
|
9
|
+
* Usage: aifabrix <command> [options]
|
|
10
|
+
*
|
|
11
|
+
* @fileoverview CLI entry point for AI Fabrix Builder SDK
|
|
12
|
+
* @author AI Fabrix Team
|
|
13
|
+
* @version 2.0.0
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { Command } = require('commander');
|
|
17
|
+
const cli = require('../lib/cli');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Initialize and configure the CLI
|
|
21
|
+
* Sets up command parsing, help text, and version information
|
|
22
|
+
*/
|
|
23
|
+
function initializeCLI() {
|
|
24
|
+
const program = new Command();
|
|
25
|
+
|
|
26
|
+
program.name('aifabrix')
|
|
27
|
+
.version('2.0.0')
|
|
28
|
+
.description('AI Fabrix Local Fabric & Deployment SDK');
|
|
29
|
+
|
|
30
|
+
// Delegate command setup to lib/cli.js
|
|
31
|
+
cli.setupCommands(program);
|
|
32
|
+
|
|
33
|
+
// Parse command line arguments
|
|
34
|
+
program.parse();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// TODO: Add error handling for CLI initialization
|
|
38
|
+
// TODO: Add graceful shutdown handling
|
|
39
|
+
// TODO: Add telemetry/analytics hooks (opt-in)
|
|
40
|
+
|
|
41
|
+
// Initialize CLI when this file is executed directly
|
|
42
|
+
if (require.main === module) {
|
|
43
|
+
try {
|
|
44
|
+
initializeCLI();
|
|
45
|
+
} catch (error) {
|
|
46
|
+
console.error('ā Failed to initialize CLI:', error.message);
|
|
47
|
+
process.exit(1);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = { initializeCLI };
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder Application Deployment Module
|
|
3
|
+
*
|
|
4
|
+
* Handles deployment to Miso Controller with manifest generation
|
|
5
|
+
* and orchestration. Includes push to Azure Container Registry.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Application deployment for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs').promises;
|
|
13
|
+
const path = require('path');
|
|
14
|
+
const yaml = require('js-yaml');
|
|
15
|
+
const chalk = require('chalk');
|
|
16
|
+
const pushUtils = require('./push');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Validate application name format
|
|
20
|
+
* @param {string} appName - Application name to validate
|
|
21
|
+
* @throws {Error} If app name is invalid
|
|
22
|
+
*/
|
|
23
|
+
function validateAppName(appName) {
|
|
24
|
+
if (!appName || typeof appName !== 'string') {
|
|
25
|
+
throw new Error('Application name is required');
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// App name should be lowercase, alphanumeric with dashes, 3-40 characters
|
|
29
|
+
const nameRegex = /^[a-z0-9-]{3,40}$/;
|
|
30
|
+
if (!nameRegex.test(appName)) {
|
|
31
|
+
throw new Error('Application name must be 3-40 characters, lowercase letters, numbers, and dashes only');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Cannot start or end with dash
|
|
35
|
+
if (appName.startsWith('-') || appName.endsWith('-')) {
|
|
36
|
+
throw new Error('Application name cannot start or end with a dash');
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Cannot have consecutive dashes
|
|
40
|
+
if (appName.includes('--')) {
|
|
41
|
+
throw new Error('Application name cannot have consecutive dashes');
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Pushes application image to Azure Container Registry
|
|
47
|
+
* @async
|
|
48
|
+
* @function pushApp
|
|
49
|
+
* @param {string} appName - Name of the application
|
|
50
|
+
* @param {Object} options - Push options (registry, tag)
|
|
51
|
+
* @returns {Promise<void>} Resolves when push is complete
|
|
52
|
+
*/
|
|
53
|
+
async function pushApp(appName, options = {}) {
|
|
54
|
+
try {
|
|
55
|
+
// Validate app name
|
|
56
|
+
validateAppName(appName);
|
|
57
|
+
|
|
58
|
+
const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
|
|
59
|
+
let config;
|
|
60
|
+
try {
|
|
61
|
+
config = yaml.load(await fs.readFile(configPath, 'utf8'));
|
|
62
|
+
} catch (error) {
|
|
63
|
+
throw new Error(`Failed to load configuration: ${configPath}\nRun 'aifabrix create ${appName}' first`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const registry = options.registry || config.image?.registry;
|
|
67
|
+
if (!registry) {
|
|
68
|
+
throw new Error('Registry URL is required. Provide via --registry flag or configure in variables.yaml under image.registry');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!pushUtils.validateRegistryURL(registry)) {
|
|
72
|
+
throw new Error(`Invalid registry URL format: ${registry}. Expected format: *.azurecr.io`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const tags = options.tag ? options.tag.split(',').map(t => t.trim()) : ['latest'];
|
|
76
|
+
|
|
77
|
+
if (!await pushUtils.checkLocalImageExists(appName, 'latest')) {
|
|
78
|
+
throw new Error(`Docker image ${appName}:latest not found locally.\nRun 'aifabrix build ${appName}' first`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if (!await pushUtils.checkAzureCLIInstalled()) {
|
|
82
|
+
throw new Error('Azure CLI is not installed. Install from: https://docs.microsoft.com/cli/azure/install-azure-cli');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (await pushUtils.checkACRAuthentication(registry)) {
|
|
86
|
+
console.log(chalk.green(`ā Already authenticated with ${registry}`));
|
|
87
|
+
} else {
|
|
88
|
+
await pushUtils.authenticateACR(registry);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await Promise.all(tags.map(async(tag) => {
|
|
92
|
+
await pushUtils.tagImage(`${appName}:latest`, `${registry}/${appName}:${tag}`);
|
|
93
|
+
await pushUtils.pushImage(`${registry}/${appName}:${tag}`);
|
|
94
|
+
}));
|
|
95
|
+
|
|
96
|
+
console.log(chalk.green(`\nā Successfully pushed ${tags.length} tag(s) to ${registry}`));
|
|
97
|
+
console.log(chalk.gray(`Image: ${registry}/${appName}:*`));
|
|
98
|
+
console.log(chalk.gray(`Tags: ${tags.join(', ')}`));
|
|
99
|
+
|
|
100
|
+
} catch (error) {
|
|
101
|
+
throw new Error(`Failed to push application: ${error.message}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Deploys application to Miso Controller
|
|
107
|
+
* Orchestrates manifest generation, key creation, and deployment
|
|
108
|
+
*
|
|
109
|
+
* @async
|
|
110
|
+
* @function deployApp
|
|
111
|
+
* @param {string} appName - Name of the application to deploy
|
|
112
|
+
* @param {Object} options - Deployment options
|
|
113
|
+
* @param {string} options.controller - Controller URL (required)
|
|
114
|
+
* @param {string} [options.environment] - Target environment (dev/tst/pro)
|
|
115
|
+
* @param {boolean} [options.poll] - Poll for deployment status
|
|
116
|
+
* @param {number} [options.pollInterval] - Polling interval in milliseconds
|
|
117
|
+
* @returns {Promise<Object>} Deployment result
|
|
118
|
+
* @throws {Error} If deployment fails
|
|
119
|
+
*
|
|
120
|
+
* @example
|
|
121
|
+
* await deployApp('myapp', { controller: 'https://controller.aifabrix.ai', environment: 'dev' });
|
|
122
|
+
*/
|
|
123
|
+
async function deployApp(appName, options = {}) {
|
|
124
|
+
try {
|
|
125
|
+
// 1. Input validation
|
|
126
|
+
if (!appName || typeof appName !== 'string') {
|
|
127
|
+
throw new Error('App name is required');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
validateAppName(appName);
|
|
131
|
+
|
|
132
|
+
if (!options.controller) {
|
|
133
|
+
throw new Error('Controller URL is required (--controller flag required)');
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 2. Load application configuration
|
|
137
|
+
const builderPath = path.join(process.cwd(), 'builder', appName);
|
|
138
|
+
try {
|
|
139
|
+
await fs.access(builderPath);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
if (error.code === 'ENOENT') {
|
|
142
|
+
throw new Error(`Application '${appName}' not found in builder/. Run 'aifabrix create ${appName}' first`);
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// 3. Generate deployment manifest
|
|
148
|
+
console.log(chalk.blue(`\nš Generating deployment manifest for ${appName}...`));
|
|
149
|
+
const generator = require('./generator');
|
|
150
|
+
const manifestPath = await generator.generateDeployJson(appName);
|
|
151
|
+
const manifest = JSON.parse(await fs.readFile(manifestPath, 'utf8'));
|
|
152
|
+
|
|
153
|
+
// 4. Validate manifest
|
|
154
|
+
const validation = generator.validateDeploymentJson(manifest);
|
|
155
|
+
if (!validation.valid) {
|
|
156
|
+
console.log(chalk.red('\nā Validation failed:'));
|
|
157
|
+
validation.errors.forEach(error => console.log(chalk.red(` ⢠${error}`)));
|
|
158
|
+
throw new Error('Deployment manifest validation failed');
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (validation.warnings.length > 0) {
|
|
162
|
+
console.log(chalk.yellow('\nā ļø Warnings:'));
|
|
163
|
+
validation.warnings.forEach(warning => console.log(chalk.yellow(` ⢠${warning}`)));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// 5. Display deployment info
|
|
167
|
+
console.log(chalk.green(`ā Manifest generated: ${manifestPath}`));
|
|
168
|
+
console.log(chalk.blue(` Key: ${manifest.key}`));
|
|
169
|
+
console.log(chalk.blue(` Display Name: ${manifest.displayName}`));
|
|
170
|
+
console.log(chalk.blue(` Image: ${manifest.image}`));
|
|
171
|
+
console.log(chalk.blue(` Port: ${manifest.port}`));
|
|
172
|
+
|
|
173
|
+
// 6. Deploy to controller
|
|
174
|
+
console.log(chalk.blue(`\nš Deploying to ${options.controller}...`));
|
|
175
|
+
const deployer = require('./deployer');
|
|
176
|
+
const result = await deployer.deployToController(manifest, options.controller, {
|
|
177
|
+
environment: options.environment,
|
|
178
|
+
poll: options.poll !== false, // Poll by default
|
|
179
|
+
pollInterval: options.pollInterval || 5000,
|
|
180
|
+
pollMaxAttempts: options.pollMaxAttempts || 60
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
// 7. Display results
|
|
184
|
+
console.log(chalk.green('\nā
Deployment initiated successfully'));
|
|
185
|
+
if (result.deploymentUrl) {
|
|
186
|
+
console.log(chalk.blue(` URL: ${result.deploymentUrl}`));
|
|
187
|
+
}
|
|
188
|
+
if (result.deploymentId) {
|
|
189
|
+
console.log(chalk.blue(` Deployment ID: ${result.deploymentId}`));
|
|
190
|
+
}
|
|
191
|
+
if (result.status) {
|
|
192
|
+
const statusIcon = result.status.status === 'completed' ? 'ā
' :
|
|
193
|
+
result.status.status === 'failed' ? 'ā' : 'ā³';
|
|
194
|
+
console.log(chalk.blue(` Status: ${statusIcon} ${result.status.status}`));
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return result;
|
|
198
|
+
|
|
199
|
+
} catch (error) {
|
|
200
|
+
throw new Error(`Failed to deploy application: ${error.message}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
module.exports = {
|
|
205
|
+
pushApp,
|
|
206
|
+
deployApp,
|
|
207
|
+
validateAppName
|
|
208
|
+
};
|
|
209
|
+
|
package/lib/app-run.js
ADDED
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder Application Run Management
|
|
3
|
+
*
|
|
4
|
+
* This module handles application running with Docker containers.
|
|
5
|
+
* Includes Docker orchestration, health checking, and port management.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Application run management for AI Fabrix Builder
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const fs = require('fs').promises;
|
|
13
|
+
const fsSync = require('fs');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const net = require('net');
|
|
16
|
+
const chalk = require('chalk');
|
|
17
|
+
const yaml = require('js-yaml');
|
|
18
|
+
const handlebars = require('handlebars');
|
|
19
|
+
const { exec } = require('child_process');
|
|
20
|
+
const { promisify } = require('util');
|
|
21
|
+
const validator = require('./validator');
|
|
22
|
+
const infra = require('./infra');
|
|
23
|
+
const secrets = require('./secrets');
|
|
24
|
+
|
|
25
|
+
const execAsync = promisify(exec);
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Checks if Docker image exists for the application
|
|
29
|
+
* @param {string} appName - Application name
|
|
30
|
+
* @returns {Promise<boolean>} True if image exists
|
|
31
|
+
*/
|
|
32
|
+
async function checkImageExists(appName) {
|
|
33
|
+
try {
|
|
34
|
+
const { stdout } = await execAsync(`docker images --format "{{.Repository}}:{{.Tag}}" | grep "^${appName}:latest$"`);
|
|
35
|
+
return stdout.trim() === `${appName}:latest`;
|
|
36
|
+
} catch (error) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Checks if container is already running
|
|
43
|
+
* @param {string} appName - Application name
|
|
44
|
+
* @returns {Promise<boolean>} True if container is running
|
|
45
|
+
*/
|
|
46
|
+
async function checkContainerRunning(appName) {
|
|
47
|
+
try {
|
|
48
|
+
const { stdout } = await execAsync(`docker ps --filter "name=aifabrix-${appName}" --format "{{.Names}}"`);
|
|
49
|
+
return stdout.trim() === `aifabrix-${appName}`;
|
|
50
|
+
} catch (error) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Stops and removes existing container
|
|
57
|
+
* @param {string} appName - Application name
|
|
58
|
+
*/
|
|
59
|
+
async function stopAndRemoveContainer(appName) {
|
|
60
|
+
try {
|
|
61
|
+
console.log(chalk.yellow(`Stopping existing container aifabrix-${appName}...`));
|
|
62
|
+
await execAsync(`docker stop aifabrix-${appName}`);
|
|
63
|
+
await execAsync(`docker rm aifabrix-${appName}`);
|
|
64
|
+
console.log(chalk.green(`ā Container aifabrix-${appName} stopped and removed`));
|
|
65
|
+
} catch (error) {
|
|
66
|
+
// Container might not exist, which is fine
|
|
67
|
+
console.log(chalk.gray(`Container aifabrix-${appName} was not running`));
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Checks if port is available
|
|
73
|
+
* @param {number} port - Port number to check
|
|
74
|
+
* @returns {Promise<boolean>} True if port is available
|
|
75
|
+
*/
|
|
76
|
+
async function checkPortAvailable(port) {
|
|
77
|
+
return new Promise((resolve) => {
|
|
78
|
+
const server = net.createServer();
|
|
79
|
+
server.listen(port, () => {
|
|
80
|
+
server.close(() => resolve(true));
|
|
81
|
+
});
|
|
82
|
+
server.on('error', () => resolve(false));
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Generates Docker Compose configuration from template
|
|
88
|
+
* @param {string} appName - Application name
|
|
89
|
+
* @param {Object} config - Application configuration
|
|
90
|
+
* @param {Object} options - Run options
|
|
91
|
+
* @returns {Promise<string>} Generated compose content
|
|
92
|
+
*/
|
|
93
|
+
async function generateDockerCompose(appName, config, options) {
|
|
94
|
+
const language = config.build?.language || config.language || 'typescript';
|
|
95
|
+
const templatePath = path.join(__dirname, '..', 'templates', language, 'docker-compose.hbs');
|
|
96
|
+
if (!fsSync.existsSync(templatePath)) {
|
|
97
|
+
throw new Error(`Docker Compose template not found for language: ${language}`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const templateContent = fsSync.readFileSync(templatePath, 'utf8');
|
|
101
|
+
const template = handlebars.compile(templateContent);
|
|
102
|
+
|
|
103
|
+
const port = options.port || config.build?.localPort || config.port || 3000;
|
|
104
|
+
|
|
105
|
+
const templateData = {
|
|
106
|
+
app: {
|
|
107
|
+
key: appName,
|
|
108
|
+
name: config.displayName || appName
|
|
109
|
+
},
|
|
110
|
+
image: {
|
|
111
|
+
name: appName,
|
|
112
|
+
tag: 'latest'
|
|
113
|
+
},
|
|
114
|
+
port: config.port || 3000,
|
|
115
|
+
build: {
|
|
116
|
+
localPort: port
|
|
117
|
+
},
|
|
118
|
+
healthCheck: {
|
|
119
|
+
path: config.healthCheck?.path || '/health',
|
|
120
|
+
interval: config.healthCheck?.interval || 30
|
|
121
|
+
},
|
|
122
|
+
requiresDatabase: config.services?.database || false,
|
|
123
|
+
requiresStorage: config.services?.storage || false,
|
|
124
|
+
requiresRedis: config.services?.redis || false,
|
|
125
|
+
mountVolume: path.join(process.cwd(), 'data', appName),
|
|
126
|
+
databases: config.databases || []
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
return template(templateData);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Waits for container health check to pass
|
|
134
|
+
* @param {string} appName - Application name
|
|
135
|
+
* @param {number} timeout - Timeout in seconds
|
|
136
|
+
*/
|
|
137
|
+
async function waitForHealthCheck(appName, timeout = 60) {
|
|
138
|
+
const maxAttempts = timeout / 2; // Check every 2 seconds
|
|
139
|
+
let attempts = 0;
|
|
140
|
+
|
|
141
|
+
while (attempts < maxAttempts) {
|
|
142
|
+
try {
|
|
143
|
+
const { stdout } = await execAsync(`docker inspect --format='{{.State.Health.Status}}' aifabrix-${appName}`);
|
|
144
|
+
const status = stdout.trim();
|
|
145
|
+
|
|
146
|
+
if (status === 'healthy') {
|
|
147
|
+
return;
|
|
148
|
+
} else if (status === 'unhealthy') {
|
|
149
|
+
throw new Error(`Container aifabrix-${appName} is unhealthy`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
attempts++;
|
|
153
|
+
if (attempts < maxAttempts) {
|
|
154
|
+
console.log(chalk.yellow(`Waiting for health check... (${attempts}/${maxAttempts})`));
|
|
155
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
156
|
+
}
|
|
157
|
+
} catch (error) {
|
|
158
|
+
attempts++;
|
|
159
|
+
if (attempts < maxAttempts) {
|
|
160
|
+
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
throw new Error(`Health check timeout after ${timeout} seconds`);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Runs the application locally using Docker
|
|
170
|
+
* Starts container with proper port mapping and environment
|
|
171
|
+
*
|
|
172
|
+
* @async
|
|
173
|
+
* @function runApp
|
|
174
|
+
* @param {string} appName - Name of the application to run
|
|
175
|
+
* @param {Object} options - Run options
|
|
176
|
+
* @param {number} [options.port] - Override local port
|
|
177
|
+
* @returns {Promise<void>} Resolves when app is running
|
|
178
|
+
* @throws {Error} If run fails or app is not built
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* await runApp('myapp', { port: 3001 });
|
|
182
|
+
* // Application is now running on localhost:3001
|
|
183
|
+
*/
|
|
184
|
+
async function runApp(appName, options = {}) {
|
|
185
|
+
try {
|
|
186
|
+
// Validate app name
|
|
187
|
+
if (!appName || typeof appName !== 'string') {
|
|
188
|
+
throw new Error('Application name is required');
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Load and validate app configuration
|
|
192
|
+
const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
|
|
193
|
+
if (!fsSync.existsSync(configPath)) {
|
|
194
|
+
throw new Error(`Application configuration not found: ${configPath}\nRun 'aifabrix create ${appName}' first`);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const configContent = fsSync.readFileSync(configPath, 'utf8');
|
|
198
|
+
const config = yaml.load(configContent);
|
|
199
|
+
|
|
200
|
+
// Validate configuration
|
|
201
|
+
const validation = await validator.validateApplication(appName);
|
|
202
|
+
if (!validation.valid) {
|
|
203
|
+
throw new Error(`Configuration validation failed:\n${validation.variables.errors.join('\n')}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// Check if Docker image exists
|
|
207
|
+
console.log(chalk.blue(`Checking if image ${appName}:latest exists...`));
|
|
208
|
+
const imageExists = await checkImageExists(appName);
|
|
209
|
+
if (!imageExists) {
|
|
210
|
+
throw new Error(`Docker image ${appName}:latest not found\nRun 'aifabrix build ${appName}' first`);
|
|
211
|
+
}
|
|
212
|
+
console.log(chalk.green(`ā Image ${appName}:latest found`));
|
|
213
|
+
|
|
214
|
+
// Check infrastructure health
|
|
215
|
+
console.log(chalk.blue('Checking infrastructure health...'));
|
|
216
|
+
const infraHealth = await infra.checkInfraHealth();
|
|
217
|
+
const unhealthyServices = Object.entries(infraHealth)
|
|
218
|
+
.filter(([_, status]) => status !== 'healthy')
|
|
219
|
+
.map(([service, _]) => service);
|
|
220
|
+
|
|
221
|
+
if (unhealthyServices.length > 0) {
|
|
222
|
+
throw new Error(`Infrastructure services not healthy: ${unhealthyServices.join(', ')}\nRun 'aifabrix up' first`);
|
|
223
|
+
}
|
|
224
|
+
console.log(chalk.green('ā Infrastructure is running'));
|
|
225
|
+
|
|
226
|
+
// Check if container is already running
|
|
227
|
+
const containerRunning = await checkContainerRunning(appName);
|
|
228
|
+
if (containerRunning) {
|
|
229
|
+
console.log(chalk.yellow(`Container aifabrix-${appName} is already running`));
|
|
230
|
+
await stopAndRemoveContainer(appName);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
// Check port availability
|
|
234
|
+
const port = options.port || config.build?.localPort || config.port || 3000;
|
|
235
|
+
const portAvailable = await checkPortAvailable(port);
|
|
236
|
+
if (!portAvailable) {
|
|
237
|
+
throw new Error(`Port ${port} is already in use. Try --port <alternative>`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Ensure .env file exists
|
|
241
|
+
const envPath = path.join(process.cwd(), 'builder', appName, '.env');
|
|
242
|
+
if (!fsSync.existsSync(envPath)) {
|
|
243
|
+
console.log(chalk.yellow('Generating .env file from template...'));
|
|
244
|
+
await secrets.generateEnvFile(appName);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Generate Docker Compose configuration
|
|
248
|
+
console.log(chalk.blue('Generating Docker Compose configuration...'));
|
|
249
|
+
const composeContent = await generateDockerCompose(appName, config, options);
|
|
250
|
+
// Write compose file to temporary location
|
|
251
|
+
const tempComposePath = path.join(process.cwd(), 'builder', appName, 'docker-compose.yaml');
|
|
252
|
+
await fs.writeFile(tempComposePath, composeContent);
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
// Start container
|
|
256
|
+
console.log(chalk.blue(`Starting ${appName}...`));
|
|
257
|
+
await execAsync(`docker-compose -f "${tempComposePath}" up -d`);
|
|
258
|
+
console.log(chalk.green(`ā Container aifabrix-${appName} started`));
|
|
259
|
+
|
|
260
|
+
// Wait for health check
|
|
261
|
+
console.log(chalk.blue('Waiting for application to be healthy...'));
|
|
262
|
+
await waitForHealthCheck(appName);
|
|
263
|
+
|
|
264
|
+
// Display success message
|
|
265
|
+
console.log(chalk.green(`\nā App running at http://localhost:${port}`));
|
|
266
|
+
console.log(chalk.gray(`Container: aifabrix-${appName}`));
|
|
267
|
+
console.log(chalk.gray('Health check: /health'));
|
|
268
|
+
|
|
269
|
+
} finally {
|
|
270
|
+
// Clean up temporary compose file
|
|
271
|
+
try {
|
|
272
|
+
await fs.unlink(tempComposePath);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
// Ignore cleanup errors
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
} catch (error) {
|
|
279
|
+
throw new Error(`Failed to run application: ${error.message}`);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = {
|
|
284
|
+
runApp,
|
|
285
|
+
checkImageExists,
|
|
286
|
+
checkContainerRunning,
|
|
287
|
+
stopAndRemoveContainer,
|
|
288
|
+
checkPortAvailable,
|
|
289
|
+
generateDockerCompose,
|
|
290
|
+
waitForHealthCheck
|
|
291
|
+
};
|