@aifabrix/builder 2.37.5 → 2.38.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +19 -0
- package/integration/hubspot/hubspot-deploy.json +1 -2
- package/lib/api/applications.api.js +23 -1
- package/lib/api/credentials.api.js +34 -0
- package/lib/api/deployments.api.js +27 -0
- package/lib/api/types/applications.types.js +1 -1
- package/lib/api/types/deployments.types.js +1 -1
- package/lib/api/types/pipeline.types.js +1 -1
- package/lib/api/wizard.api.js +21 -1
- package/lib/app/run-helpers.js +30 -2
- package/lib/cli/index.js +2 -0
- package/lib/cli/setup-app.js +32 -0
- package/lib/cli/setup-credential-deployment.js +72 -0
- package/lib/cli/setup-utility.js +1 -25
- package/lib/commands/app-down.js +80 -0
- package/lib/commands/app-logs.js +146 -0
- package/lib/commands/app.js +22 -0
- package/lib/commands/credential-list.js +104 -0
- package/lib/commands/deployment-list.js +184 -0
- package/lib/commands/up-miso.js +2 -2
- package/lib/commands/wizard-core.js +39 -27
- package/lib/core/config.js +16 -1
- package/lib/core/secrets.js +42 -50
- package/lib/core/templates.js +2 -1
- package/lib/deployment/environment.js +32 -21
- package/lib/generator/builders.js +8 -3
- package/lib/generator/external-controller-manifest.js +5 -4
- package/lib/generator/index.js +16 -14
- package/lib/generator/split.js +1 -0
- package/lib/generator/wizard.js +4 -1
- package/lib/schema/application-schema.json +6 -2
- package/lib/schema/deployment-rules.yaml +121 -0
- package/lib/utils/app-run-containers.js +2 -1
- package/lib/utils/compose-generator.js +2 -1
- package/lib/utils/help-builder.js +0 -1
- package/lib/utils/image-version.js +209 -0
- package/lib/utils/paths.js +6 -3
- package/lib/utils/schema-loader.js +1 -1
- package/lib/utils/variable-transformer.js +1 -19
- package/lib/validation/external-manifest-validator.js +1 -1
- package/package.json +1 -1
- package/templates/applications/README.md.hbs +1 -3
- package/templates/applications/dataplane/Dockerfile +2 -2
- package/templates/applications/dataplane/README.md +1 -3
- package/templates/applications/dataplane/variables.yaml +5 -3
- package/templates/applications/keycloak/Dockerfile +3 -3
- package/templates/applications/keycloak/README.md +14 -4
- package/templates/applications/keycloak/env.template +14 -2
- package/templates/applications/keycloak/variables.yaml +1 -1
- package/templates/applications/miso-controller/README.md +1 -3
- package/templates/applications/miso-controller/env.template +64 -11
package/lib/commands/app.js
CHANGED
|
@@ -15,6 +15,7 @@ const { listApplications } = require('../app/list');
|
|
|
15
15
|
const { registerApplication } = require('../app/register');
|
|
16
16
|
const { rotateSecret } = require('../app/rotate-secret');
|
|
17
17
|
const { showApp } = require('../app/show');
|
|
18
|
+
const { runAppDeploymentList } = require('./deployment-list');
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Setup application management commands
|
|
@@ -81,6 +82,27 @@ function setupAppCommands(program) {
|
|
|
81
82
|
process.exit(1);
|
|
82
83
|
}
|
|
83
84
|
});
|
|
85
|
+
|
|
86
|
+
// Deployment list for an application
|
|
87
|
+
app
|
|
88
|
+
.command('deployment <appKey>')
|
|
89
|
+
.description('List last N deployments for an application in current environment (default pageSize=50)')
|
|
90
|
+
.option('--controller <url>', 'Controller URL (default: from config)')
|
|
91
|
+
.option('--environment <env>', 'Environment key (default: from config)')
|
|
92
|
+
.option('--page-size <n>', 'Items per page', '50')
|
|
93
|
+
.action(async(appKey, options) => {
|
|
94
|
+
try {
|
|
95
|
+
const opts = {
|
|
96
|
+
controller: options.controller,
|
|
97
|
+
environment: options.environment,
|
|
98
|
+
pageSize: parseInt(options.pageSize, 10) || 50
|
|
99
|
+
};
|
|
100
|
+
await runAppDeploymentList(appKey, opts);
|
|
101
|
+
} catch (error) {
|
|
102
|
+
logger.error(chalk.red(`Error: ${error.message}`));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
84
106
|
}
|
|
85
107
|
|
|
86
108
|
module.exports = { setupAppCommands };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential list command – list credentials from controller/dataplane
|
|
3
|
+
* GET /api/v1/credential. Used by `aifabrix credential list`.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Credential list command implementation
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
const logger = require('../utils/logger');
|
|
12
|
+
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
13
|
+
const { getOrRefreshDeviceToken } = require('../utils/token-manager');
|
|
14
|
+
const { normalizeControllerUrl } = require('../core/config');
|
|
15
|
+
const { listCredentials } = require('../api/credentials.api');
|
|
16
|
+
|
|
17
|
+
const DEFAULT_PAGE_SIZE = 50;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get auth token for credential list (device token from config)
|
|
21
|
+
* @async
|
|
22
|
+
* @param {string} controllerUrl - Controller base URL
|
|
23
|
+
* @returns {Promise<{token: string, controllerUrl: string}|null>}
|
|
24
|
+
*/
|
|
25
|
+
async function getCredentialListAuth(controllerUrl) {
|
|
26
|
+
const normalizedUrl = normalizeControllerUrl(controllerUrl);
|
|
27
|
+
const deviceToken = await getOrRefreshDeviceToken(normalizedUrl);
|
|
28
|
+
if (deviceToken && deviceToken.token) {
|
|
29
|
+
return {
|
|
30
|
+
token: deviceToken.token,
|
|
31
|
+
controllerUrl: deviceToken.controller || normalizedUrl
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extract credentials array from API response
|
|
39
|
+
* @param {Object} response - API response
|
|
40
|
+
* @returns {Array}
|
|
41
|
+
*/
|
|
42
|
+
function extractCredentials(response) {
|
|
43
|
+
const data = response?.data ?? response;
|
|
44
|
+
const items = data?.credentials ?? data?.items ?? (Array.isArray(data) ? data : []);
|
|
45
|
+
return Array.isArray(items) ? items : [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Display credential list to user
|
|
50
|
+
* @param {Array} list - Credentials array
|
|
51
|
+
* @param {string} controllerUrl - Controller URL for header
|
|
52
|
+
*/
|
|
53
|
+
function displayCredentialList(list, controllerUrl) {
|
|
54
|
+
logger.log(chalk.bold(`\n🔐 Credentials (${controllerUrl}):\n`));
|
|
55
|
+
if (list.length === 0) {
|
|
56
|
+
logger.log(chalk.gray(' No credentials found.\n'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
list.forEach((c) => {
|
|
60
|
+
const key = c.key ?? c.id ?? c.credentialKey ?? '-';
|
|
61
|
+
const name = c.displayName ?? c.name ?? key;
|
|
62
|
+
logger.log(` ${chalk.cyan(key)} - ${name}`);
|
|
63
|
+
});
|
|
64
|
+
logger.log('');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Run credential list command: call GET /api/v1/credential and display results
|
|
69
|
+
* @async
|
|
70
|
+
* @param {Object} options - CLI options
|
|
71
|
+
* @param {string} [options.controller] - Controller URL override
|
|
72
|
+
* @param {boolean} [options.activeOnly] - List only active credentials
|
|
73
|
+
* @param {number} [options.pageSize] - Items per page
|
|
74
|
+
* @returns {Promise<void>}
|
|
75
|
+
*/
|
|
76
|
+
async function runCredentialList(options = {}) {
|
|
77
|
+
const controllerUrl = options.controller || (await resolveControllerUrl());
|
|
78
|
+
if (!controllerUrl) {
|
|
79
|
+
logger.error(chalk.red('❌ Controller URL is required. Run "aifabrix login" first.'));
|
|
80
|
+
process.exit(1);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const authResult = await getCredentialListAuth(controllerUrl);
|
|
84
|
+
if (!authResult || !authResult.token) {
|
|
85
|
+
logger.error(chalk.red(`❌ No authentication token for controller: ${controllerUrl}`));
|
|
86
|
+
logger.error(chalk.gray('Run: aifabrix login'));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const authConfig = { type: 'bearer', token: authResult.token };
|
|
91
|
+
const listOptions = {
|
|
92
|
+
pageSize: options.pageSize || DEFAULT_PAGE_SIZE,
|
|
93
|
+
activeOnly: options.activeOnly
|
|
94
|
+
};
|
|
95
|
+
try {
|
|
96
|
+
const response = await listCredentials(authResult.controllerUrl, authConfig, listOptions);
|
|
97
|
+
displayCredentialList(extractCredentials(response), authResult.controllerUrl);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
logger.error(chalk.red(`❌ Failed to list credentials: ${error.message}`));
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { runCredentialList };
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment list commands – list deployments for environment or for an app
|
|
3
|
+
* Uses GET .../deployments and GET .../applications/{appKey}/deployments.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Deployment list command implementation
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
const logger = require('../utils/logger');
|
|
12
|
+
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
13
|
+
const { resolveEnvironment } = require('../core/config');
|
|
14
|
+
const { getOrRefreshDeviceToken } = require('../utils/token-manager');
|
|
15
|
+
const { listDeployments, listApplicationDeployments } = require('../api/deployments.api');
|
|
16
|
+
const { normalizeControllerUrl } = require('../core/config');
|
|
17
|
+
|
|
18
|
+
const DEFAULT_PAGE_SIZE = 50;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get auth token for deployment list (device token from config)
|
|
22
|
+
* @async
|
|
23
|
+
* @param {string} controllerUrl - Controller base URL
|
|
24
|
+
* @returns {Promise<{token: string, controllerUrl: string}|null>}
|
|
25
|
+
*/
|
|
26
|
+
async function getDeploymentListAuth(controllerUrl) {
|
|
27
|
+
const normalizedUrl = normalizeControllerUrl(controllerUrl);
|
|
28
|
+
const deviceToken = await getOrRefreshDeviceToken(normalizedUrl);
|
|
29
|
+
if (deviceToken && deviceToken.token) {
|
|
30
|
+
return {
|
|
31
|
+
token: deviceToken.token,
|
|
32
|
+
controllerUrl: deviceToken.controller || normalizedUrl
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extract deployments array from API response
|
|
40
|
+
* @param {Object} response - API response
|
|
41
|
+
* @returns {Array}
|
|
42
|
+
*/
|
|
43
|
+
function extractDeployments(response) {
|
|
44
|
+
const data = response?.data ?? response;
|
|
45
|
+
const items = data?.items ?? data?.deployments ?? (Array.isArray(data) ? data : []);
|
|
46
|
+
return Array.isArray(items) ? items : [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Display environment deployment list to user
|
|
51
|
+
* @param {Array} deployments - Deployments array
|
|
52
|
+
* @param {string} environment - Environment key
|
|
53
|
+
* @param {string} controllerUrl - Controller URL
|
|
54
|
+
*/
|
|
55
|
+
function displayDeploymentList(deployments, environment, controllerUrl) {
|
|
56
|
+
logger.log(chalk.bold(`\n📋 Deployments (${environment}) at ${controllerUrl}:\n`));
|
|
57
|
+
if (deployments.length === 0) {
|
|
58
|
+
logger.log(chalk.gray(' No deployments found.\n'));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
deployments.forEach((d) => {
|
|
62
|
+
const id = d.id ?? d.deploymentId ?? '-';
|
|
63
|
+
const appKey = d.applicationKey ?? d.appKey ?? d.application?.key ?? '-';
|
|
64
|
+
const status = d.status ?? '-';
|
|
65
|
+
const createdAt = d.createdAt ?? d.created ?? '';
|
|
66
|
+
logger.log(` ${chalk.cyan(id)} ${appKey} ${status} ${chalk.gray(createdAt)}`);
|
|
67
|
+
});
|
|
68
|
+
logger.log('');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Run deployment list (environment): list last N deployments for current environment
|
|
73
|
+
* @async
|
|
74
|
+
* @param {Object} options - CLI options
|
|
75
|
+
* @param {string} [options.controller] - Controller URL override
|
|
76
|
+
* @param {string} [options.environment] - Environment key override
|
|
77
|
+
* @param {number} [options.pageSize] - Items per page (default 50)
|
|
78
|
+
* @returns {Promise<void>}
|
|
79
|
+
*/
|
|
80
|
+
async function runDeploymentList(options = {}) {
|
|
81
|
+
const { environment, authResult } = await resolveDeploymentListContext(options);
|
|
82
|
+
const authConfig = { type: 'bearer', token: authResult.token };
|
|
83
|
+
const listOptions = { pageSize: options.pageSize || DEFAULT_PAGE_SIZE };
|
|
84
|
+
try {
|
|
85
|
+
const response = await listDeployments(
|
|
86
|
+
authResult.controllerUrl,
|
|
87
|
+
environment,
|
|
88
|
+
authConfig,
|
|
89
|
+
listOptions
|
|
90
|
+
);
|
|
91
|
+
displayDeploymentList(extractDeployments(response), environment, authResult.controllerUrl);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
logger.error(chalk.red(`❌ Failed to list deployments: ${error.message}`));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Display app deployment list to user
|
|
100
|
+
* @param {Array} deployments - Deployments array
|
|
101
|
+
* @param {string} appKey - Application key
|
|
102
|
+
* @param {string} environment - Environment key
|
|
103
|
+
* @param {string} controllerUrl - Controller URL
|
|
104
|
+
*/
|
|
105
|
+
function displayAppDeploymentList(deployments, appKey, environment, controllerUrl) {
|
|
106
|
+
logger.log(chalk.bold(`\n📋 Deployments for ${appKey} (${environment}) at ${controllerUrl}:\n`));
|
|
107
|
+
if (deployments.length === 0) {
|
|
108
|
+
logger.log(chalk.gray(' No deployments found for this application.\n'));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
deployments.forEach((d) => {
|
|
112
|
+
const id = d.id ?? d.deploymentId ?? '-';
|
|
113
|
+
const status = d.status ?? '-';
|
|
114
|
+
const createdAt = d.createdAt ?? d.created ?? '';
|
|
115
|
+
logger.log(` ${chalk.cyan(id)} ${status} ${chalk.gray(createdAt)}`);
|
|
116
|
+
});
|
|
117
|
+
logger.log('');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve controller URL, environment, and auth for deployment list commands
|
|
122
|
+
* @async
|
|
123
|
+
* @param {Object} options - Options with optional controller, environment
|
|
124
|
+
* @returns {Promise<{controllerUrl: string, environment: string, authResult: Object}>}
|
|
125
|
+
*/
|
|
126
|
+
async function resolveDeploymentListContext(options) {
|
|
127
|
+
const controllerUrl = options.controller || (await resolveControllerUrl());
|
|
128
|
+
if (!controllerUrl) {
|
|
129
|
+
logger.error(chalk.red('❌ Controller URL is required. Run "aifabrix login" first.'));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
const environment = options.environment || (await resolveEnvironment());
|
|
133
|
+
const authResult = await getDeploymentListAuth(controllerUrl);
|
|
134
|
+
if (!authResult || !authResult.token) {
|
|
135
|
+
logger.error(chalk.red(`❌ No authentication token for controller: ${controllerUrl}`));
|
|
136
|
+
logger.error(chalk.gray('Run: aifabrix login'));
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
return { controllerUrl, environment, authResult };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Run app deployment list: list last N deployments for an application
|
|
144
|
+
* @async
|
|
145
|
+
* @param {string} appKey - Application key
|
|
146
|
+
* @param {Object} options - CLI options
|
|
147
|
+
* @param {string} [options.controller] - Controller URL override
|
|
148
|
+
* @param {string} [options.environment] - Environment key override
|
|
149
|
+
* @param {number} [options.pageSize] - Items per page (default 50)
|
|
150
|
+
* @returns {Promise<void>}
|
|
151
|
+
*/
|
|
152
|
+
async function runAppDeploymentList(appKey, options = {}) {
|
|
153
|
+
if (!appKey || typeof appKey !== 'string') {
|
|
154
|
+
logger.error(chalk.red('❌ Application key is required.'));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const { environment, authResult } = await resolveDeploymentListContext(options);
|
|
159
|
+
const authConfig = { type: 'bearer', token: authResult.token };
|
|
160
|
+
const listOptions = { pageSize: options.pageSize || DEFAULT_PAGE_SIZE };
|
|
161
|
+
try {
|
|
162
|
+
const response = await listApplicationDeployments(
|
|
163
|
+
authResult.controllerUrl,
|
|
164
|
+
environment,
|
|
165
|
+
appKey,
|
|
166
|
+
authConfig,
|
|
167
|
+
listOptions
|
|
168
|
+
);
|
|
169
|
+
displayAppDeploymentList(
|
|
170
|
+
extractDeployments(response),
|
|
171
|
+
appKey,
|
|
172
|
+
environment,
|
|
173
|
+
authResult.controllerUrl
|
|
174
|
+
);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
logger.error(chalk.red(`❌ Failed to list deployments for ${appKey}: ${error.message}`));
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
runDeploymentList,
|
|
183
|
+
runAppDeploymentList
|
|
184
|
+
};
|
package/lib/commands/up-miso.js
CHANGED
|
@@ -140,8 +140,8 @@ async function handleUpMiso(options = {}) {
|
|
|
140
140
|
const devIdNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
|
|
141
141
|
await setMisoSecretsAndResolve(devIdNum);
|
|
142
142
|
await runMisoApps(options);
|
|
143
|
-
logger.log(chalk.green('\n✓ up-miso complete. Keycloak, miso-controller, and dataplane are running.')
|
|
144
|
-
|
|
143
|
+
logger.log(chalk.green('\n✓ up-miso complete. Keycloak, miso-controller, and dataplane are running.') +
|
|
144
|
+
chalk.gray('\n Run onboarding and register Keycloak from the miso-controller repo if needed.'));
|
|
145
145
|
}
|
|
146
146
|
|
|
147
147
|
module.exports = { handleUpMiso, parseImageOptions };
|
|
@@ -338,6 +338,44 @@ async function validateWizardConfiguration(dataplaneUrl, authConfig, systemConfi
|
|
|
338
338
|
}
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
/**
|
|
342
|
+
* Fetches deployment docs and writes README.md when variables.yaml and deploy JSON are available.
|
|
343
|
+
* @async
|
|
344
|
+
* @param {string} appPath - Application path
|
|
345
|
+
* @param {string} appName - Application name
|
|
346
|
+
* @param {string} dataplaneUrl - Dataplane URL
|
|
347
|
+
* @param {Object} authConfig - Authentication configuration
|
|
348
|
+
* @param {string} systemKey - System key
|
|
349
|
+
*/
|
|
350
|
+
async function tryUpdateReadmeFromDeploymentDocs(appPath, appName, dataplaneUrl, authConfig, systemKey) {
|
|
351
|
+
const variablesPath = path.join(appPath, 'variables.yaml');
|
|
352
|
+
const deployPath = path.join(appPath, `${appName}-deploy.json`);
|
|
353
|
+
let variablesYaml = null;
|
|
354
|
+
let deployJson = null;
|
|
355
|
+
try {
|
|
356
|
+
variablesYaml = await fs.readFile(variablesPath, 'utf8');
|
|
357
|
+
} catch {
|
|
358
|
+
// optional
|
|
359
|
+
}
|
|
360
|
+
try {
|
|
361
|
+
const deployContent = await fs.readFile(deployPath, 'utf8');
|
|
362
|
+
deployJson = JSON.parse(deployContent);
|
|
363
|
+
} catch {
|
|
364
|
+
// optional
|
|
365
|
+
}
|
|
366
|
+
const hasBody = variablesYaml !== null || deployJson !== null;
|
|
367
|
+
const body = hasBody ? { variablesYaml: variablesYaml || null, deployJson: deployJson || null } : null;
|
|
368
|
+
const docsResponse = body
|
|
369
|
+
? await postDeploymentDocs(dataplaneUrl, authConfig, systemKey, body)
|
|
370
|
+
: await getDeploymentDocs(dataplaneUrl, authConfig, systemKey);
|
|
371
|
+
const content = docsResponse?.data?.content ?? docsResponse?.content;
|
|
372
|
+
if (content && typeof content === 'string') {
|
|
373
|
+
const readmePath = path.join(appPath, 'README.md');
|
|
374
|
+
await fs.writeFile(readmePath, content, 'utf8');
|
|
375
|
+
logger.log(chalk.gray(' Updated README.md from deployment-docs API (variables.yaml + deploy JSON).'));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
341
379
|
/**
|
|
342
380
|
* Handle file saving step
|
|
343
381
|
* @async
|
|
@@ -357,33 +395,7 @@ async function handleFileSaving(appName, systemConfig, datasourceConfigs, system
|
|
|
357
395
|
const generatedFiles = await generateWizardFiles(appName, systemConfig, datasourceConfigs, systemKey, { aiGeneratedReadme: null });
|
|
358
396
|
if (systemKey && dataplaneUrl && authConfig && generatedFiles.appPath) {
|
|
359
397
|
try {
|
|
360
|
-
|
|
361
|
-
const deployKey = appName;
|
|
362
|
-
const variablesPath = path.join(appPath, 'variables.yaml');
|
|
363
|
-
const deployPath = path.join(appPath, `${deployKey}-deploy.json`);
|
|
364
|
-
let variablesYaml = null;
|
|
365
|
-
let deployJson = null;
|
|
366
|
-
try {
|
|
367
|
-
variablesYaml = await fs.readFile(variablesPath, 'utf8');
|
|
368
|
-
} catch {
|
|
369
|
-
// optional
|
|
370
|
-
}
|
|
371
|
-
try {
|
|
372
|
-
const deployContent = await fs.readFile(deployPath, 'utf8');
|
|
373
|
-
deployJson = JSON.parse(deployContent);
|
|
374
|
-
} catch {
|
|
375
|
-
// optional
|
|
376
|
-
}
|
|
377
|
-
const body = (variablesYaml !== null && variablesYaml !== undefined) || (deployJson !== null && deployJson !== undefined) ? { variablesYaml: variablesYaml || null, deployJson: deployJson || null } : null;
|
|
378
|
-
const docsResponse = body
|
|
379
|
-
? await postDeploymentDocs(dataplaneUrl, authConfig, systemKey, body)
|
|
380
|
-
: await getDeploymentDocs(dataplaneUrl, authConfig, systemKey);
|
|
381
|
-
const content = docsResponse?.data?.content ?? docsResponse?.content;
|
|
382
|
-
if (content && typeof content === 'string') {
|
|
383
|
-
const readmePath = path.join(appPath, 'README.md');
|
|
384
|
-
await fs.writeFile(readmePath, content, 'utf8');
|
|
385
|
-
logger.log(chalk.gray(' Updated README.md from deployment-docs API (variables.yaml + deploy JSON).'));
|
|
386
|
-
}
|
|
398
|
+
await tryUpdateReadmeFromDeploymentDocs(generatedFiles.appPath, appName, dataplaneUrl, authConfig, systemKey);
|
|
387
399
|
} catch (e) {
|
|
388
400
|
logger.log(chalk.gray(` Could not fetch AI-generated README: ${e.message}`));
|
|
389
401
|
}
|
package/lib/core/config.js
CHANGED
|
@@ -405,9 +405,24 @@ async function ensureSecretsEncryptionKey() {
|
|
|
405
405
|
await run({ getSecretsEncryptionKey, setSecretsEncryptionKey, getSecretsPath });
|
|
406
406
|
}
|
|
407
407
|
|
|
408
|
+
/**
|
|
409
|
+
* Expand leading ~ to home directory so config paths like ~/.aifabrix/secrets.local.yaml resolve correctly.
|
|
410
|
+
* @param {string} filePath - Path that may start with ~ or ~/
|
|
411
|
+
* @returns {string} Path with ~ expanded, or unchanged if no leading ~
|
|
412
|
+
*/
|
|
413
|
+
function expandTilde(filePath) {
|
|
414
|
+
if (!filePath || typeof filePath !== 'string') return filePath;
|
|
415
|
+
if (filePath === '~') return os.homedir();
|
|
416
|
+
if (filePath.startsWith('~/') || filePath.startsWith('~' + path.sep)) {
|
|
417
|
+
return path.join(os.homedir(), filePath.slice(2));
|
|
418
|
+
}
|
|
419
|
+
return filePath;
|
|
420
|
+
}
|
|
421
|
+
|
|
408
422
|
async function getSecretsPath() {
|
|
409
423
|
const config = await getConfig();
|
|
410
|
-
|
|
424
|
+
const raw = config['aifabrix-secrets'] || config['secrets-path'] || null;
|
|
425
|
+
return raw ? expandTilde(raw) : null;
|
|
411
426
|
}
|
|
412
427
|
|
|
413
428
|
async function setSecretsPath(secretsPath) {
|
package/lib/core/secrets.js
CHANGED
|
@@ -132,28 +132,57 @@ async function decryptSecretsObject(secrets) {
|
|
|
132
132
|
* const secrets = await loadSecrets(undefined, 'myapp');
|
|
133
133
|
*/
|
|
134
134
|
|
|
135
|
+
/**
|
|
136
|
+
* Merges config file secrets into user secrets (user wins). Returns null if path missing or config empty.
|
|
137
|
+
* @param {Object} userSecrets - User secrets object
|
|
138
|
+
* @param {string} resolvedConfigPath - Absolute path to config secrets file
|
|
139
|
+
* @returns {Object|null} Merged secrets or null
|
|
140
|
+
*/
|
|
141
|
+
function mergeUserWithConfigFile(userSecrets, resolvedConfigPath) {
|
|
142
|
+
if (!fs.existsSync(resolvedConfigPath)) {
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
let configSecrets;
|
|
146
|
+
try {
|
|
147
|
+
configSecrets = readYamlAtPath(resolvedConfigPath);
|
|
148
|
+
} catch (loadError) {
|
|
149
|
+
throw new Error(`Failed to load secrets file ${resolvedConfigPath}: ${loadError.message}`);
|
|
150
|
+
}
|
|
151
|
+
if (!configSecrets || typeof configSecrets !== 'object') {
|
|
152
|
+
return null;
|
|
153
|
+
}
|
|
154
|
+
const merged = { ...userSecrets };
|
|
155
|
+
for (const key of Object.keys(configSecrets)) {
|
|
156
|
+
if (!(key in merged) || merged[key] === undefined || merged[key] === null || merged[key] === '') {
|
|
157
|
+
merged[key] = configSecrets[key];
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
return merged;
|
|
161
|
+
}
|
|
162
|
+
|
|
135
163
|
/**
|
|
136
164
|
* Loads config secrets path, merges with user secrets (user overrides). Used by loadSecrets cascade.
|
|
137
165
|
* @async
|
|
138
166
|
* @returns {Promise<Object|null>} Merged secrets object or null
|
|
139
167
|
*/
|
|
140
168
|
async function loadMergedConfigAndUserSecrets() {
|
|
169
|
+
const userSecrets = loadUserSecrets();
|
|
170
|
+
const hasKeys = (obj) => obj && Object.keys(obj).length > 0;
|
|
171
|
+
const userOrNull = () => (hasKeys(userSecrets) ? userSecrets : null);
|
|
141
172
|
try {
|
|
142
173
|
const configSecretsPath = await config.getSecretsPath();
|
|
143
|
-
if (!configSecretsPath)
|
|
174
|
+
if (!configSecretsPath) {
|
|
175
|
+
return userOrNull();
|
|
176
|
+
}
|
|
144
177
|
const resolvedConfigPath = path.isAbsolute(configSecretsPath)
|
|
145
178
|
? configSecretsPath
|
|
146
179
|
: path.resolve(process.cwd(), configSecretsPath);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
for (const key of Object.keys(userSecrets)) {
|
|
153
|
-
merged[key] = userSecrets[key];
|
|
180
|
+
const merged = mergeUserWithConfigFile(userSecrets, resolvedConfigPath);
|
|
181
|
+
return merged !== null ? merged : userOrNull();
|
|
182
|
+
} catch (error) {
|
|
183
|
+
if (error.message && error.message.startsWith('Failed to load secrets file')) {
|
|
184
|
+
throw error;
|
|
154
185
|
}
|
|
155
|
-
return merged;
|
|
156
|
-
} catch {
|
|
157
186
|
return null;
|
|
158
187
|
}
|
|
159
188
|
}
|
|
@@ -229,22 +258,11 @@ async function resolveKvReferences(envTemplate, secrets, environment = 'local',
|
|
|
229
258
|
return replaceKvInContent(resolved, secrets, envVars);
|
|
230
259
|
}
|
|
231
260
|
|
|
232
|
-
/**
|
|
233
|
-
* Applies environment-specific transformations to resolved content
|
|
234
|
-
* @async
|
|
235
|
-
* @function applyEnvironmentTransformations
|
|
236
|
-
* @param {string} resolved - Resolved environment content
|
|
237
|
-
* @param {string} environment - Environment context
|
|
238
|
-
* @param {string} variablesPath - Path to variables.yaml file
|
|
239
|
-
* @returns {Promise<string>} Transformed content
|
|
240
|
-
*/
|
|
261
|
+
/** Applies environment-specific transformations to resolved content. */
|
|
241
262
|
async function applyEnvironmentTransformations(resolved, environment, variablesPath) {
|
|
242
263
|
if (environment === 'docker') {
|
|
243
264
|
resolved = await resolveServicePortsInEnvContent(resolved, environment);
|
|
244
265
|
resolved = await rewriteInfraEndpoints(resolved, 'docker');
|
|
245
|
-
// Interpolate ${VAR} references created by rewriteInfraEndpoints
|
|
246
|
-
// Get the actual host and port values from env-endpoints.js directly
|
|
247
|
-
// to ensure they are correctly populated in envVars for interpolation
|
|
248
266
|
const { getEnvHosts, getServiceHost, getServicePort, getLocalhostOverride } = require('../utils/env-endpoints');
|
|
249
267
|
const hosts = await getEnvHosts('docker');
|
|
250
268
|
const localhostOverride = getLocalhostOverride('docker');
|
|
@@ -252,10 +270,7 @@ async function applyEnvironmentTransformations(resolved, environment, variablesP
|
|
|
252
270
|
const redisPort = await getServicePort('REDIS_PORT', 'redis', hosts, 'docker', null);
|
|
253
271
|
const dbHost = getServiceHost(hosts.DB_HOST, 'docker', 'postgres', localhostOverride);
|
|
254
272
|
const dbPort = await getServicePort('DB_PORT', 'postgres', hosts, 'docker', null);
|
|
255
|
-
|
|
256
|
-
// Build envVars map and ensure it has the correct values
|
|
257
273
|
const envVars = await buildEnvVarMap('docker');
|
|
258
|
-
// Override with the actual values that were just set by rewriteInfraEndpoints
|
|
259
274
|
envVars.REDIS_HOST = redisHost;
|
|
260
275
|
envVars.REDIS_PORT = String(redisPort);
|
|
261
276
|
envVars.DB_HOST = dbHost;
|
|
@@ -269,35 +284,15 @@ async function applyEnvironmentTransformations(resolved, environment, variablesP
|
|
|
269
284
|
return resolved;
|
|
270
285
|
}
|
|
271
286
|
|
|
272
|
-
/**
|
|
273
|
-
* Generates .env file content from template and secrets (without writing to disk)
|
|
274
|
-
* @async
|
|
275
|
-
* @function generateEnvContent
|
|
276
|
-
* @param {string} appName - Name of the application
|
|
277
|
-
* @param {string} [secretsPath] - Path to secrets file (optional)
|
|
278
|
-
* @param {string} [environment='local'] - Environment context
|
|
279
|
-
* @param {boolean} [force=false] - Generate missing secret keys in secrets file
|
|
280
|
-
* @returns {Promise<string>} Generated .env file content
|
|
281
|
-
* @throws {Error} If generation fails
|
|
282
|
-
*/
|
|
287
|
+
/** Generates .env file content from template and secrets (without writing to disk). */
|
|
283
288
|
async function generateEnvContent(appName, secretsPath, environment = 'local', force = false) {
|
|
284
289
|
const builderPath = pathsUtil.getBuilderPath(appName);
|
|
285
290
|
const templatePath = path.join(builderPath, 'env.template');
|
|
286
291
|
const variablesPath = path.join(builderPath, 'variables.yaml');
|
|
287
|
-
|
|
288
292
|
const template = loadEnvTemplate(templatePath);
|
|
289
293
|
const secretsPaths = await getActualSecretsPath(secretsPath, appName);
|
|
290
|
-
|
|
291
294
|
if (force) {
|
|
292
|
-
|
|
293
|
-
// If explicit path provided, use it; otherwise use the path that loadUserSecrets() would use
|
|
294
|
-
let secretsFileForGeneration;
|
|
295
|
-
if (secretsPath) {
|
|
296
|
-
secretsFileForGeneration = resolveSecretsPath(secretsPath);
|
|
297
|
-
} else {
|
|
298
|
-
// Use the same path that loadUserSecrets() would use (now uses paths.getAifabrixHome())
|
|
299
|
-
secretsFileForGeneration = secretsPaths.userPath;
|
|
300
|
-
}
|
|
295
|
+
const secretsFileForGeneration = secretsPath ? resolveSecretsPath(secretsPath) : secretsPaths.userPath;
|
|
301
296
|
await generateMissingSecrets(template, secretsFileForGeneration);
|
|
302
297
|
}
|
|
303
298
|
|
|
@@ -472,9 +467,6 @@ REDIS_COMMANDER_PASSWORD=${postgresPassword}
|
|
|
472
467
|
fs.writeFileSync(adminEnvPath, adminSecrets, { mode: 0o600 });
|
|
473
468
|
return adminEnvPath;
|
|
474
469
|
}
|
|
475
|
-
|
|
476
|
-
// validateSecrets is imported from ./utils/secrets-helpers
|
|
477
|
-
|
|
478
470
|
module.exports = {
|
|
479
471
|
loadSecrets,
|
|
480
472
|
resolveKvReferences,
|
package/lib/core/templates.js
CHANGED
|
@@ -83,7 +83,8 @@ function buildWebappVariables(appName, displayName, config, imageName, imageTag)
|
|
|
83
83
|
key: appName,
|
|
84
84
|
displayName: displayName,
|
|
85
85
|
description: `${appName.replace(/-/g, ' ')} application`,
|
|
86
|
-
type: appType
|
|
86
|
+
type: appType,
|
|
87
|
+
version: config.version || '1.0.0'
|
|
87
88
|
},
|
|
88
89
|
image: {
|
|
89
90
|
name: imageName,
|