@aifabrix/builder 2.39.3 → 2.40.2
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/.cursor/rules/project-rules.mdc +6 -6
- package/README.md +3 -3
- package/babel.config.js +6 -0
- package/integration/hubspot/README.md +53 -141
- package/integration/hubspot/application.yaml +37 -0
- package/integration/hubspot/env.template +2 -11
- package/integration/hubspot/hubspot-deploy.json +1 -0
- package/integration/hubspot/test.js +5 -5
- package/jest.config.manual.js +29 -0
- package/lib/api/credentials.api.js +5 -5
- package/lib/api/deployments.api.js +2 -2
- package/lib/api/pipeline.api.js +17 -17
- package/lib/api/wizard.api.js +2 -2
- package/lib/app/config.js +11 -6
- package/lib/app/deploy-config.js +13 -16
- package/lib/app/deploy.js +29 -22
- package/lib/app/display.js +1 -1
- package/lib/app/dockerfile.js +11 -12
- package/lib/app/helpers.js +51 -13
- package/lib/app/index.js +14 -2
- package/lib/app/prompts.js +37 -45
- package/lib/app/push.js +8 -11
- package/lib/app/readme.js +16 -12
- package/lib/app/register.js +1 -1
- package/lib/app/run-helpers.js +31 -22
- package/lib/app/run.js +44 -5
- package/lib/app/show-display.js +104 -44
- package/lib/app/show.js +123 -43
- package/lib/build/index.js +11 -18
- package/lib/cli/setup-app.js +36 -29
- package/lib/cli/setup-auth.js +19 -15
- package/lib/cli/setup-credential-deployment.js +3 -1
- package/lib/cli/setup-external-system.js +35 -16
- package/lib/cli/setup-infra.js +45 -23
- package/lib/cli/setup-utility.js +85 -31
- package/lib/commands/app-logs.js +28 -20
- package/lib/commands/app.js +30 -26
- package/lib/commands/auth-status.js +36 -3
- package/lib/commands/convert.js +202 -0
- package/lib/commands/credential-list.js +78 -17
- package/lib/commands/datasource.js +24 -24
- package/lib/commands/deployment-list.js +13 -6
- package/lib/commands/up-common.js +80 -42
- package/lib/commands/up-dataplane.js +15 -14
- package/lib/commands/up-miso.js +15 -14
- package/lib/commands/upload.js +163 -0
- package/lib/commands/wizard-core.js +5 -4
- package/lib/core/diff.js +84 -9
- package/lib/core/key-generator.js +9 -12
- package/lib/core/secrets-docker-env.js +2 -2
- package/lib/core/secrets.js +3 -2
- package/lib/core/templates.js +2 -2
- package/lib/datasource/deploy.js +2 -1
- package/lib/deployment/deployer.js +76 -48
- package/lib/external-system/delete.js +0 -1
- package/lib/external-system/deploy-helpers.js +5 -6
- package/lib/external-system/deploy.js +7 -2
- package/lib/external-system/download-helpers.js +4 -4
- package/lib/external-system/download.js +11 -10
- package/lib/external-system/generator.js +19 -17
- package/lib/external-system/test.js +10 -15
- package/lib/generator/builders.js +1 -1
- package/lib/generator/external-controller-manifest.js +26 -29
- package/lib/generator/external-schema-utils.js +6 -18
- package/lib/generator/external.js +32 -27
- package/lib/generator/github.js +1 -1
- package/lib/generator/helpers.js +12 -19
- package/lib/generator/index.js +15 -15
- package/lib/generator/parse-image.js +35 -0
- package/lib/generator/split-readme.js +105 -0
- package/lib/generator/split-variables.js +149 -0
- package/lib/generator/split.js +86 -246
- package/lib/generator/wizard.js +51 -70
- package/lib/schema/application-schema.json +4 -4
- package/lib/schema/external-datasource.schema.json +5 -0
- package/lib/schema/external-system.schema.json +10 -0
- package/lib/utils/app-config-resolver.js +52 -0
- package/lib/utils/app-register-api.js +1 -1
- package/lib/utils/app-register-auth.js +1 -1
- package/lib/utils/app-register-config.js +16 -23
- package/lib/utils/app-register-validator.js +2 -2
- package/lib/utils/cli-utils.js +47 -3
- package/lib/utils/config-format.js +154 -0
- package/lib/utils/config-paths.js +19 -52
- package/lib/utils/config-tokens.js +1 -0
- package/lib/utils/docker-build.js +71 -94
- package/lib/utils/dockerfile-utils.js +1 -1
- package/lib/utils/env-copy.js +4 -4
- package/lib/utils/env-ports.js +2 -2
- package/lib/utils/error-formatter.js +1 -1
- package/lib/utils/error-formatters/validation-errors.js +1 -1
- package/lib/utils/external-readme.js +12 -5
- package/lib/utils/external-system-test-helpers.js +2 -0
- package/lib/utils/health-check.js +55 -66
- package/lib/utils/image-version.js +12 -21
- package/lib/utils/paths.js +45 -66
- package/lib/utils/port-resolver.js +8 -8
- package/lib/utils/schema-loader.js +22 -0
- package/lib/utils/schema-resolver.js +23 -33
- package/lib/utils/secrets-helpers.js +7 -7
- package/lib/utils/secrets-utils.js +10 -12
- package/lib/utils/template-helpers.js +13 -13
- package/lib/utils/token-manager.js +20 -2
- package/lib/utils/variable-transformer.js +2 -2
- package/lib/validation/validate-display.js +3 -4
- package/lib/validation/validate.js +34 -28
- package/lib/validation/validator.js +50 -30
- package/package.json +4 -1
- package/templates/README.md +1 -1
- package/templates/applications/README.md.hbs +3 -3
- package/templates/applications/miso-controller/env.template +3 -1
- package/templates/external-system/README.md.hbs +4 -4
- package/templates/external-system/external-system.json.hbs +1 -16
- package/integration/hubspot/variables.yaml +0 -17
- /package/templates/applications/dataplane/{variables.yaml → application.yaml} +0 -0
- /package/templates/applications/keycloak/{variables.yaml → application.yaml} +0 -0
- /package/templates/applications/miso-controller/{variables.yaml → application.yaml} +0 -0
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Config Format Converter Layer
|
|
3
|
+
*
|
|
4
|
+
* Single place for YAML/JSON config I/O. All config loaders and writers use this
|
|
5
|
+
* layer; internal code works with plain JS objects and JSON Schema only.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Config format conversion (YAML/JSON) at I/O boundary
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const fs = require('fs');
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const yaml = require('js-yaml');
|
|
17
|
+
|
|
18
|
+
const YAML_EXTENSIONS = ['.yaml', '.yml'];
|
|
19
|
+
const JSON_EXTENSIONS = ['.json'];
|
|
20
|
+
|
|
21
|
+
const DEFAULT_YAML_OPTIONS = { indent: 2, lineWidth: 120, noRefs: true };
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parses YAML string to plain JS object (same shape as JSON).
|
|
25
|
+
* Used when reading .yaml / .yml files.
|
|
26
|
+
*
|
|
27
|
+
* @param {string} content - YAML string content
|
|
28
|
+
* @returns {Object} Plain JS object
|
|
29
|
+
* @throws {Error} If YAML syntax is invalid
|
|
30
|
+
*/
|
|
31
|
+
function yamlToJson(content) {
|
|
32
|
+
if (typeof content !== 'string') {
|
|
33
|
+
throw new Error('yamlToJson expects a string');
|
|
34
|
+
}
|
|
35
|
+
try {
|
|
36
|
+
const parsed = yaml.load(content);
|
|
37
|
+
return parsed === undefined || parsed === null ? {} : parsed;
|
|
38
|
+
} catch (error) {
|
|
39
|
+
throw new Error(`Invalid YAML syntax: ${error.message}`);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Serializes JS object to YAML string.
|
|
45
|
+
* Used when writing human-editable config as YAML.
|
|
46
|
+
*
|
|
47
|
+
* @param {Object} object - Plain JS object (config)
|
|
48
|
+
* @param {Object} [options] - js-yaml dump options
|
|
49
|
+
* @returns {string} YAML string
|
|
50
|
+
*/
|
|
51
|
+
function jsonToYaml(object, options = {}) {
|
|
52
|
+
if (object === undefined || object === null) {
|
|
53
|
+
return '';
|
|
54
|
+
}
|
|
55
|
+
const opts = { ...DEFAULT_YAML_OPTIONS, ...options };
|
|
56
|
+
return yaml.dump(object, opts);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Returns whether the file path is treated as YAML by extension.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} filePath - File path
|
|
63
|
+
* @returns {boolean} True if .yaml or .yml
|
|
64
|
+
*/
|
|
65
|
+
function isYamlPath(filePath) {
|
|
66
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
67
|
+
return YAML_EXTENSIONS.includes(ext);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Returns whether the file path is treated as JSON by extension.
|
|
72
|
+
*
|
|
73
|
+
* @param {string} filePath - File path
|
|
74
|
+
* @returns {boolean} True if .json
|
|
75
|
+
*/
|
|
76
|
+
function isJsonPath(filePath) {
|
|
77
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
78
|
+
return JSON_EXTENSIONS.includes(ext);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Reads config file at path; by extension uses yamlToJson or JSON.parse.
|
|
83
|
+
* Single entry point for "read config file regardless of format".
|
|
84
|
+
*
|
|
85
|
+
* @param {string} filePath - Absolute path to config file
|
|
86
|
+
* @returns {Object} Parsed config object
|
|
87
|
+
* @throws {Error} If file not found, unreadable, or invalid format
|
|
88
|
+
*/
|
|
89
|
+
function loadConfigFile(filePath) {
|
|
90
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
91
|
+
throw new Error('loadConfigFile requires a non-empty file path');
|
|
92
|
+
}
|
|
93
|
+
if (!fs.existsSync(filePath)) {
|
|
94
|
+
throw new Error(`Config file not found: ${filePath}`);
|
|
95
|
+
}
|
|
96
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
97
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
98
|
+
if (YAML_EXTENSIONS.includes(ext)) {
|
|
99
|
+
return yamlToJson(content);
|
|
100
|
+
}
|
|
101
|
+
if (JSON_EXTENSIONS.includes(ext)) {
|
|
102
|
+
try {
|
|
103
|
+
const parsed = JSON.parse(content);
|
|
104
|
+
return parsed === undefined || parsed === null ? {} : parsed;
|
|
105
|
+
} catch (error) {
|
|
106
|
+
throw new Error(`Invalid JSON syntax in ${path.basename(filePath)}: ${error.message}`);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
throw new Error(`Unsupported config file extension: ${ext}. Use .yaml, .yml, or .json`);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Writes config object to path as YAML or JSON based on format or path extension.
|
|
114
|
+
*
|
|
115
|
+
* @param {string} filePath - Absolute path to write (extension determines format if format omitted)
|
|
116
|
+
* @param {Object} object - Config object to write
|
|
117
|
+
* @param {string} [format] - 'yaml' or 'json'; if omitted, inferred from filePath extension
|
|
118
|
+
* @throws {Error} If format is invalid or write fails
|
|
119
|
+
*/
|
|
120
|
+
function writeConfigFile(filePath, object, format) {
|
|
121
|
+
if (!filePath || typeof filePath !== 'string') {
|
|
122
|
+
throw new Error('writeConfigFile requires a non-empty file path');
|
|
123
|
+
}
|
|
124
|
+
let targetFormat = format;
|
|
125
|
+
if (!targetFormat) {
|
|
126
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
127
|
+
if (YAML_EXTENSIONS.includes(ext)) {
|
|
128
|
+
targetFormat = 'yaml';
|
|
129
|
+
} else if (JSON_EXTENSIONS.includes(ext)) {
|
|
130
|
+
targetFormat = 'json';
|
|
131
|
+
} else {
|
|
132
|
+
throw new Error(`Cannot infer format from path ${filePath}. Use .yaml, .yml, or .json, or pass format.`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
const normalized = targetFormat.toLowerCase();
|
|
136
|
+
let content;
|
|
137
|
+
if (normalized === 'yaml' || normalized === 'yml') {
|
|
138
|
+
content = jsonToYaml(object);
|
|
139
|
+
} else if (normalized === 'json') {
|
|
140
|
+
content = JSON.stringify(object, null, 2);
|
|
141
|
+
} else {
|
|
142
|
+
throw new Error(`Invalid format: ${format}. Use 'yaml' or 'json'.`);
|
|
143
|
+
}
|
|
144
|
+
fs.writeFileSync(filePath, content, 'utf8');
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
yamlToJson,
|
|
149
|
+
jsonToYaml,
|
|
150
|
+
loadConfigFile,
|
|
151
|
+
writeConfigFile,
|
|
152
|
+
isYamlPath,
|
|
153
|
+
isJsonPath
|
|
154
|
+
};
|
|
@@ -41,77 +41,31 @@ async function setPathConfig(getConfigFn, saveConfigFn, key, value, errorMsg) {
|
|
|
41
41
|
await saveConfigFn(config);
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
* Create path configuration functions with config access
|
|
46
|
-
* @param {Function} getConfigFn - Function to get config
|
|
47
|
-
* @param {Function} saveConfigFn - Function to save config
|
|
48
|
-
* @returns {Object} Path configuration functions
|
|
49
|
-
*/
|
|
50
|
-
function createPathConfigFunctions(getConfigFn, saveConfigFn) {
|
|
44
|
+
function createHomeAndSecretsPathFunctions(getConfigFn, saveConfigFn) {
|
|
51
45
|
return {
|
|
52
|
-
/**
|
|
53
|
-
* Get aifabrix-home override path
|
|
54
|
-
* @async
|
|
55
|
-
* @returns {Promise<string|null>} Home path or null
|
|
56
|
-
*/
|
|
57
46
|
async getAifabrixHomeOverride() {
|
|
58
47
|
return getPathConfig(getConfigFn, 'aifabrix-home');
|
|
59
48
|
},
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Set aifabrix-home override path
|
|
63
|
-
* @async
|
|
64
|
-
* @param {string} homePath - Home path
|
|
65
|
-
* @returns {Promise<void>}
|
|
66
|
-
*/
|
|
67
49
|
async setAifabrixHomeOverride(homePath) {
|
|
68
50
|
await setPathConfig(getConfigFn, saveConfigFn, 'aifabrix-home', homePath, 'Home path is required and must be a string');
|
|
69
51
|
},
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Get aifabrix-secrets path
|
|
73
|
-
* @async
|
|
74
|
-
* @returns {Promise<string|null>} Secrets path or null
|
|
75
|
-
*/
|
|
76
52
|
async getAifabrixSecretsPath() {
|
|
77
53
|
return getPathConfig(getConfigFn, 'aifabrix-secrets');
|
|
78
54
|
},
|
|
79
|
-
|
|
80
|
-
/**
|
|
81
|
-
* Set aifabrix-secrets path
|
|
82
|
-
* @async
|
|
83
|
-
* @param {string} secretsPath - Secrets path
|
|
84
|
-
* @returns {Promise<void>}
|
|
85
|
-
*/
|
|
86
55
|
async setAifabrixSecretsPath(secretsPath) {
|
|
87
56
|
await setPathConfig(getConfigFn, saveConfigFn, 'aifabrix-secrets', secretsPath, 'Secrets path is required and must be a string');
|
|
88
|
-
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}
|
|
89
60
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
* @async
|
|
93
|
-
* @returns {Promise<string|null>} Env config path or null
|
|
94
|
-
*/
|
|
61
|
+
function createEnvConfigPathFunctions(getConfigFn, saveConfigFn) {
|
|
62
|
+
return {
|
|
95
63
|
async getAifabrixEnvConfigPath() {
|
|
96
64
|
return getPathConfig(getConfigFn, 'aifabrix-env-config');
|
|
97
65
|
},
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Set aifabrix-env-config path
|
|
101
|
-
* @async
|
|
102
|
-
* @param {string} envConfigPath - Env config path
|
|
103
|
-
* @returns {Promise<void>}
|
|
104
|
-
*/
|
|
105
66
|
async setAifabrixEnvConfigPath(envConfigPath) {
|
|
106
67
|
await setPathConfig(getConfigFn, saveConfigFn, 'aifabrix-env-config', envConfigPath, 'Env config path is required and must be a string');
|
|
107
68
|
},
|
|
108
|
-
|
|
109
|
-
/**
|
|
110
|
-
* Get builder root directory (dirname of aifabrix-env-config when set).
|
|
111
|
-
* When set, app dirs and generated .env use this instead of cwd/builder.
|
|
112
|
-
* @async
|
|
113
|
-
* @returns {Promise<string|null>} Builder root path or null to use cwd/builder
|
|
114
|
-
*/
|
|
115
69
|
async getAifabrixBuilderDir() {
|
|
116
70
|
const envConfigPath = await getPathConfig(getConfigFn, 'aifabrix-env-config');
|
|
117
71
|
return envConfigPath && typeof envConfigPath === 'string' ? path.dirname(envConfigPath) : null;
|
|
@@ -119,6 +73,19 @@ function createPathConfigFunctions(getConfigFn, saveConfigFn) {
|
|
|
119
73
|
};
|
|
120
74
|
}
|
|
121
75
|
|
|
76
|
+
/**
|
|
77
|
+
* Create path configuration functions with config access
|
|
78
|
+
* @param {Function} getConfigFn - Function to get config
|
|
79
|
+
* @param {Function} saveConfigFn - Function to save config
|
|
80
|
+
* @returns {Object} Path configuration functions
|
|
81
|
+
*/
|
|
82
|
+
function createPathConfigFunctions(getConfigFn, saveConfigFn) {
|
|
83
|
+
return {
|
|
84
|
+
...createHomeAndSecretsPathFunctions(getConfigFn, saveConfigFn),
|
|
85
|
+
...createEnvConfigPathFunctions(getConfigFn, saveConfigFn)
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
122
89
|
module.exports = {
|
|
123
90
|
getPathConfig,
|
|
124
91
|
setPathConfig,
|
|
@@ -39,6 +39,7 @@ function normalizeControllerUrl(url) {
|
|
|
39
39
|
* @param {Function} params.isTokenEncryptedFn - Function to check if token is encrypted
|
|
40
40
|
* @returns {Object} Token management functions
|
|
41
41
|
*/
|
|
42
|
+
/* eslint-disable max-lines-per-function -- factory returns many bound helpers; splitting would break encapsulation */
|
|
42
43
|
function createTokenManagementFunctions({
|
|
43
44
|
getConfigFn,
|
|
44
45
|
saveConfigFn,
|
|
@@ -63,6 +63,75 @@ function parseDockerBuildProgress(line) {
|
|
|
63
63
|
return null;
|
|
64
64
|
}
|
|
65
65
|
|
|
66
|
+
function updateSpinnerFromOutput(output, spinner, lastProgressUpdateRef) {
|
|
67
|
+
const lines = output.split('\n');
|
|
68
|
+
for (const line of lines) {
|
|
69
|
+
const progress = parseDockerBuildProgress(line.trim());
|
|
70
|
+
if (progress) {
|
|
71
|
+
const now = Date.now();
|
|
72
|
+
if (now - lastProgressUpdateRef.current > 200) {
|
|
73
|
+
spinner.text = `Building image... ${progress}`;
|
|
74
|
+
lastProgressUpdateRef.current = now;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function handleDockerClose(code, ctx) {
|
|
81
|
+
const { imageName, tag, stderrBuffer, stdoutBuffer, spinner, resolve, reject } = ctx;
|
|
82
|
+
if (code === 0) {
|
|
83
|
+
spinner.succeed(`Image built: ${imageName}:${tag}`);
|
|
84
|
+
resolve();
|
|
85
|
+
} else {
|
|
86
|
+
spinner.fail('Build failed');
|
|
87
|
+
const errorMessage = stderrBuffer || stdoutBuffer || 'Docker build failed';
|
|
88
|
+
if (isDockerNotAvailableError(errorMessage)) {
|
|
89
|
+
reject(new Error('Docker is not running or not installed. Please start Docker Desktop and try again.'));
|
|
90
|
+
} else {
|
|
91
|
+
const errorLines = errorMessage.split('\n').filter(line => line.trim());
|
|
92
|
+
reject(new Error(`Docker build failed: ${errorLines.slice(-5).join('\n')}`));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function runDockerBuildProcess(buildOpts) {
|
|
98
|
+
const { imageName, tag, dockerfilePath, contextPath, spinner, resolve, reject } = buildOpts;
|
|
99
|
+
const dockerProcess = spawn('docker', ['build', '-t', `${imageName}:${tag}`, '-f', dockerfilePath, contextPath], {
|
|
100
|
+
shell: process.platform === 'win32'
|
|
101
|
+
});
|
|
102
|
+
let stdoutBuffer = '';
|
|
103
|
+
let stderrBuffer = '';
|
|
104
|
+
const lastProgressUpdateRef = { current: Date.now() };
|
|
105
|
+
|
|
106
|
+
dockerProcess.stdout.on('data', (data) => {
|
|
107
|
+
const output = data.toString();
|
|
108
|
+
stdoutBuffer += output;
|
|
109
|
+
updateSpinnerFromOutput(output, spinner, lastProgressUpdateRef);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
dockerProcess.stderr.on('data', (data) => {
|
|
113
|
+
const output = data.toString();
|
|
114
|
+
stderrBuffer += output;
|
|
115
|
+
if (!output.toLowerCase().includes('warning')) {
|
|
116
|
+
updateSpinnerFromOutput(output, spinner, lastProgressUpdateRef);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
dockerProcess.on('close', (code) => {
|
|
121
|
+
handleDockerClose(code, { imageName, tag, stderrBuffer, stdoutBuffer, spinner, resolve, reject });
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
dockerProcess.on('error', (error) => {
|
|
125
|
+
spinner.fail('Build failed');
|
|
126
|
+
const msg = error.message || String(error);
|
|
127
|
+
if (isDockerNotAvailableError(msg)) {
|
|
128
|
+
reject(new Error('Docker is not running or not installed. Please start Docker Desktop and try again.'));
|
|
129
|
+
} else {
|
|
130
|
+
reject(new Error(`Docker build failed: ${msg}`));
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
66
135
|
/**
|
|
67
136
|
* Executes Docker build command with progress indicator
|
|
68
137
|
* @param {string} imageName - Image name to build
|
|
@@ -73,29 +142,20 @@ function parseDockerBuildProgress(line) {
|
|
|
73
142
|
* @throws {Error} If build fails
|
|
74
143
|
*/
|
|
75
144
|
async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag) {
|
|
76
|
-
const spinner = ora({
|
|
77
|
-
text: 'Starting Docker build...',
|
|
78
|
-
spinner: 'dots'
|
|
79
|
-
}).start();
|
|
80
|
-
|
|
81
|
-
// Ensure paths are absolute and normalized
|
|
145
|
+
const spinner = ora({ text: 'Starting Docker build...', spinner: 'dots' }).start();
|
|
82
146
|
const fsSync = require('fs');
|
|
83
147
|
const path = require('path');
|
|
84
|
-
|
|
85
148
|
dockerfilePath = path.resolve(dockerfilePath);
|
|
86
149
|
contextPath = path.resolve(contextPath);
|
|
87
150
|
|
|
88
|
-
// Validate paths exist (skip in test environments)
|
|
89
151
|
const isTestEnv = process.env.NODE_ENV === 'test' ||
|
|
90
152
|
process.env.JEST_WORKER_ID !== undefined ||
|
|
91
153
|
typeof jest !== 'undefined';
|
|
92
|
-
|
|
93
154
|
if (!isTestEnv) {
|
|
94
155
|
if (!fsSync.existsSync(dockerfilePath)) {
|
|
95
156
|
spinner.fail('Build failed');
|
|
96
157
|
throw new Error(`Dockerfile not found: ${dockerfilePath}`);
|
|
97
158
|
}
|
|
98
|
-
|
|
99
159
|
if (!fsSync.existsSync(contextPath)) {
|
|
100
160
|
spinner.fail('Build failed');
|
|
101
161
|
throw new Error(`Build context path does not exist: ${contextPath}`);
|
|
@@ -103,90 +163,7 @@ async function executeDockerBuild(imageName, dockerfilePath, contextPath, tag) {
|
|
|
103
163
|
}
|
|
104
164
|
|
|
105
165
|
return new Promise((resolve, reject) => {
|
|
106
|
-
|
|
107
|
-
const dockerProcess = spawn('docker', [
|
|
108
|
-
'build',
|
|
109
|
-
'-t', `${imageName}:${tag}`,
|
|
110
|
-
'-f', dockerfilePath,
|
|
111
|
-
contextPath
|
|
112
|
-
], {
|
|
113
|
-
shell: process.platform === 'win32'
|
|
114
|
-
});
|
|
115
|
-
|
|
116
|
-
let stdoutBuffer = '';
|
|
117
|
-
let stderrBuffer = '';
|
|
118
|
-
let lastProgressUpdate = Date.now();
|
|
119
|
-
|
|
120
|
-
dockerProcess.stdout.on('data', (data) => {
|
|
121
|
-
const output = data.toString();
|
|
122
|
-
stdoutBuffer += output;
|
|
123
|
-
|
|
124
|
-
// Parse progress from output lines
|
|
125
|
-
const lines = output.split('\n');
|
|
126
|
-
for (const line of lines) {
|
|
127
|
-
const progress = parseDockerBuildProgress(line.trim());
|
|
128
|
-
if (progress) {
|
|
129
|
-
// Update spinner text with progress (throttle updates)
|
|
130
|
-
const now = Date.now();
|
|
131
|
-
if (now - lastProgressUpdate > 200) {
|
|
132
|
-
spinner.text = `Building image... ${progress}`;
|
|
133
|
-
lastProgressUpdate = now;
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
dockerProcess.stderr.on('data', (data) => {
|
|
140
|
-
const output = data.toString();
|
|
141
|
-
stderrBuffer += output;
|
|
142
|
-
|
|
143
|
-
// Check for warnings vs errors
|
|
144
|
-
if (!output.toLowerCase().includes('warning')) {
|
|
145
|
-
// Parse progress from stderr too (Docker outputs progress to stderr)
|
|
146
|
-
const lines = output.split('\n');
|
|
147
|
-
for (const line of lines) {
|
|
148
|
-
const progress = parseDockerBuildProgress(line.trim());
|
|
149
|
-
if (progress) {
|
|
150
|
-
const now = Date.now();
|
|
151
|
-
if (now - lastProgressUpdate > 200) {
|
|
152
|
-
spinner.text = `Building image... ${progress}`;
|
|
153
|
-
lastProgressUpdate = now;
|
|
154
|
-
}
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
}
|
|
158
|
-
});
|
|
159
|
-
|
|
160
|
-
dockerProcess.on('close', (code) => {
|
|
161
|
-
if (code === 0) {
|
|
162
|
-
spinner.succeed(`Image built: ${imageName}:${tag}`);
|
|
163
|
-
resolve();
|
|
164
|
-
} else {
|
|
165
|
-
spinner.fail('Build failed');
|
|
166
|
-
|
|
167
|
-
const errorMessage = stderrBuffer || stdoutBuffer || 'Docker build failed';
|
|
168
|
-
|
|
169
|
-
if (isDockerNotAvailableError(errorMessage)) {
|
|
170
|
-
reject(new Error('Docker is not running or not installed. Please start Docker Desktop and try again.'));
|
|
171
|
-
} else {
|
|
172
|
-
// Show last few lines of error output
|
|
173
|
-
const errorLines = errorMessage.split('\n').filter(line => line.trim());
|
|
174
|
-
const lastError = errorLines.slice(-5).join('\n');
|
|
175
|
-
reject(new Error(`Docker build failed: ${lastError}`));
|
|
176
|
-
}
|
|
177
|
-
}
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
dockerProcess.on('error', (error) => {
|
|
181
|
-
spinner.fail('Build failed');
|
|
182
|
-
const errorMessage = error.message || String(error);
|
|
183
|
-
|
|
184
|
-
if (isDockerNotAvailableError(errorMessage)) {
|
|
185
|
-
reject(new Error('Docker is not running or not installed. Please start Docker Desktop and try again.'));
|
|
186
|
-
} else {
|
|
187
|
-
reject(new Error(`Docker build failed: ${errorMessage}`));
|
|
188
|
-
}
|
|
189
|
-
});
|
|
166
|
+
runDockerBuildProcess({ imageName, tag, dockerfilePath, contextPath, spinner, resolve, reject });
|
|
190
167
|
});
|
|
191
168
|
}
|
|
192
169
|
|
|
@@ -103,7 +103,7 @@ function checkTemplateDockerfile(builderPath, appName, forceTemplate) {
|
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
/**
|
|
106
|
-
* Checks for custom Dockerfile from
|
|
106
|
+
* Checks for custom Dockerfile from application config
|
|
107
107
|
* @function checkProjectDockerfile
|
|
108
108
|
* @param {string} builderPath - Builder directory path
|
|
109
109
|
* @param {string} appName - Application name
|
package/lib/utils/env-copy.js
CHANGED
|
@@ -50,8 +50,8 @@ function readDeveloperIdFromConfig(config) {
|
|
|
50
50
|
|
|
51
51
|
/**
|
|
52
52
|
* Resolve output path for env file
|
|
53
|
-
* @param {string} rawOutputPath - Raw output path from
|
|
54
|
-
* @param {string} variablesPath - Path to
|
|
53
|
+
* @param {string} rawOutputPath - Raw output path from application config
|
|
54
|
+
* @param {string} variablesPath - Path to application config
|
|
55
55
|
* @returns {string} Resolved output path
|
|
56
56
|
*/
|
|
57
57
|
function resolveEnvOutputPath(rawOutputPath, variablesPath) {
|
|
@@ -154,7 +154,7 @@ function extractEnvVarsFromContent(envContent, envVars) {
|
|
|
154
154
|
* Patch env content for local development
|
|
155
155
|
* @async
|
|
156
156
|
* @param {string} envContent - Original env content
|
|
157
|
-
* @param {Object} variables - Variables from
|
|
157
|
+
* @param {Object} variables - Variables from application config
|
|
158
158
|
* @returns {Promise<string>} Patched env content
|
|
159
159
|
*/
|
|
160
160
|
async function patchEnvContentForLocal(envContent, variables) {
|
|
@@ -186,7 +186,7 @@ async function patchEnvContentForLocal(envContent, variables) {
|
|
|
186
186
|
* @async
|
|
187
187
|
* @function processEnvVariables
|
|
188
188
|
* @param {string} envPath - Path to generated .env file
|
|
189
|
-
* @param {string} variablesPath - Path to
|
|
189
|
+
* @param {string} variablesPath - Path to application config
|
|
190
190
|
* @param {string} appName - Application name (for regenerating with local env)
|
|
191
191
|
* @param {string} [secretsPath] - Path to secrets file (optional, for regenerating)
|
|
192
192
|
*/
|
package/lib/utils/env-ports.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Environment port utilities
|
|
3
3
|
*
|
|
4
|
-
* @fileoverview Update container PORT based on
|
|
4
|
+
* @fileoverview Update container PORT based on application config and developer-id offset
|
|
5
5
|
* @author AI Fabrix Team
|
|
6
6
|
* @version 2.0.0
|
|
7
7
|
*/
|
|
@@ -17,7 +17,7 @@ const { getLocalPort } = require('./port-resolver');
|
|
|
17
17
|
* Update PORT in the container's .env file to use variables.port (+offset)
|
|
18
18
|
* @function updateContainerPortInEnvFile
|
|
19
19
|
* @param {string} envPath - Path to .env
|
|
20
|
-
* @param {string} variablesPath - Path to
|
|
20
|
+
* @param {string} variablesPath - Path to application config
|
|
21
21
|
*/
|
|
22
22
|
/**
|
|
23
23
|
* Gets developer ID from environment variable or config file
|
|
@@ -187,7 +187,7 @@ function formatMissingDbPasswordError(appKey, opts = {}) {
|
|
|
187
187
|
'Add ' + passwordKey + '=your_secret to your .env file. For multiple databases you need DB_0_PASSWORD, DB_1_PASSWORD, etc.';
|
|
188
188
|
}
|
|
189
189
|
return 'Missing required password variable DB_0_PASSWORD or DB_PASSWORD in .env file for application \'' + appKey + '\'. ' +
|
|
190
|
-
'This app has requires.database or databases in
|
|
190
|
+
'This app has requires.database or databases in application.yaml. Add DB_0_PASSWORD=your_secret or DB_PASSWORD=your_secret to builder/' + appKey + '/.env (or run \'aifabrix resolve ' + appKey + '\'), or set requires.database: false in application.yaml if not needed.';
|
|
191
191
|
}
|
|
192
192
|
|
|
193
193
|
module.exports = {
|
|
@@ -97,7 +97,7 @@ function addValidationGuidance(lines, hasErrors) {
|
|
|
97
97
|
return;
|
|
98
98
|
}
|
|
99
99
|
lines.push(chalk.gray('Tips:'));
|
|
100
|
-
lines.push(chalk.gray(' • Check your
|
|
100
|
+
lines.push(chalk.gray(' • Check your application.yaml file for the correct field values'));
|
|
101
101
|
lines.push(chalk.gray(' • Verify field names match the expected schema'));
|
|
102
102
|
lines.push(chalk.gray(' • Ensure required fields are present and valid'));
|
|
103
103
|
lines.push('');
|
|
@@ -51,11 +51,18 @@ function normalizeDatasources(datasources, systemKey) {
|
|
|
51
51
|
let fileName = datasource.fileName || datasource.file;
|
|
52
52
|
if (!fileName) {
|
|
53
53
|
const key = datasource.key || '';
|
|
54
|
-
//
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
54
|
+
// Suffix matches split getExternalDatasourceFileName for consistent README and file names
|
|
55
|
+
let suffix;
|
|
56
|
+
if (key.startsWith(`${systemKey}-deploy-`)) {
|
|
57
|
+
suffix = key.slice(`${systemKey}-deploy-`.length);
|
|
58
|
+
} else if (systemKey && key.startsWith(`${systemKey}-`)) {
|
|
59
|
+
suffix = key.slice(systemKey.length + 1);
|
|
60
|
+
} else if (key) {
|
|
61
|
+
suffix = key;
|
|
62
|
+
} else {
|
|
63
|
+
suffix = entityType;
|
|
64
|
+
}
|
|
65
|
+
fileName = systemKey ? `${systemKey}-datasource-${suffix}.yaml` : `${suffix}.yaml`;
|
|
59
66
|
}
|
|
60
67
|
return { entityType, displayName, fileName };
|
|
61
68
|
});
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
const fs = require('fs').promises;
|
|
13
13
|
const path = require('path');
|
|
14
14
|
const { testDatasourceViaPipeline } = require('../api/pipeline.api');
|
|
15
|
+
const { requireBearerForDataplanePipeline } = require('./token-manager');
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Retry API call with exponential backoff
|
|
@@ -51,6 +52,7 @@ async function retryApiCall(fn, maxRetries = 3, backoffMs = 1000) {
|
|
|
51
52
|
* @returns {Promise<Object>} Test response
|
|
52
53
|
*/
|
|
53
54
|
async function callPipelineTestEndpoint({ systemKey, datasourceKey, payloadTemplate, dataplaneUrl, authConfig, timeout = 30000 }) {
|
|
55
|
+
requireBearerForDataplanePipeline(authConfig);
|
|
54
56
|
const response = await retryApiCall(async() => {
|
|
55
57
|
return await testDatasourceViaPipeline({
|
|
56
58
|
dataplaneUrl,
|