@aifabrix/builder 2.37.0 → 2.37.9
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 +19 -0
- package/integration/hubspot/test.js +1 -1
- package/lib/api/wizard.api.js +24 -1
- package/lib/app/deploy.js +43 -7
- package/lib/app/list.js +3 -1
- package/lib/build/index.js +3 -4
- package/lib/cli/setup-app.js +1 -0
- package/lib/cli/setup-external-system.js +1 -0
- package/lib/cli/setup-utility.js +1 -1
- package/lib/commands/up-common.js +31 -1
- package/lib/commands/up-miso.js +7 -3
- package/lib/commands/wizard-core.js +44 -7
- package/lib/core/config.js +16 -1
- package/lib/core/secrets.js +42 -50
- package/lib/deployment/deployer-status.js +101 -0
- package/lib/deployment/deployer.js +62 -110
- package/lib/deployment/environment.js +146 -36
- package/lib/external-system/deploy.js +5 -1
- package/lib/external-system/test-auth.js +14 -7
- package/lib/generator/wizard.js +27 -16
- package/lib/schema/environment-deploy-request.schema.json +64 -0
- package/lib/utils/paths.js +28 -7
- package/lib/utils/secrets-generator.js +23 -8
- package/lib/utils/secrets-helpers.js +46 -21
- package/package.json +1 -1
- package/scripts/install-local.js +11 -2
- package/templates/external-system/deploy.js.hbs +11 -0
- package/templates/infra/environment-dev.json +10 -0
|
@@ -70,6 +70,23 @@ lib/
|
|
|
70
70
|
└── schema/ # JSON schemas
|
|
71
71
|
```
|
|
72
72
|
|
|
73
|
+
### Generated Output (integration/ and builder/)
|
|
74
|
+
|
|
75
|
+
Files under **integration/** and **builder/** are **auto-generated** by the CLI. When fixing bugs or changing behavior, validate **where** each file is produced so fixes go into the generator, not only into the generated artifact.
|
|
76
|
+
|
|
77
|
+
- **integration/** – External system / wizard output:
|
|
78
|
+
- Path: `integration/<appName>/` (see `lib/utils/paths.js` → `getIntegrationPath`).
|
|
79
|
+
- Generated by: `lib/generator/wizard.js` (`generateWizardFiles`, `generateConfigFilesForWizard`), `lib/external-system/download.js`, wizard commands in `lib/commands/wizard-core.js` and `lib/commands/wizard.js`.
|
|
80
|
+
- Typical outputs: `variables.yaml`, `env.template`, `README.md`, `*-system.json`, `*-datasource*.json`, `*-deploy.json`, deploy script (`deploy.js`), and optionally `wizard.yaml`, `error.log`.
|
|
81
|
+
- **builder/** – Application (non-external) output:
|
|
82
|
+
- Path: `builder/<appName>/` or custom root via `AIFABRIX_BUILDER_DIR` (see `lib/utils/paths.js` → `getBuilderPath`).
|
|
83
|
+
- Generated by: app create/register flow, `lib/generator/index.js`, `lib/commands/up-common.js`, `lib/core/secrets.js`, and related app/deploy logic.
|
|
84
|
+
- Typical outputs: `variables.yaml`, `env.template`, deploy JSON, `.env` (from secrets), and app-specific config.
|
|
85
|
+
|
|
86
|
+
**Editable vs generated:**
|
|
87
|
+
- Some generated files are **intended to be edited** (e.g. `variables.yaml`, `env.template`, `README.md`, `wizard.yaml`). Improvements to defaults or structure still belong in the generator/templates.
|
|
88
|
+
- **When debugging:** First identify the **source of generation** (which module and function write the file). Fix bugs in that generator or template; avoid treating a one-off edit in integration/ or builder/ as the permanent fix unless it’s a deliberate local override.
|
|
89
|
+
|
|
73
90
|
### CLI Command Pattern
|
|
74
91
|
Commands are defined in `lib/cli.js` using Commander.js:
|
|
75
92
|
```javascript
|
|
@@ -850,6 +867,7 @@ Define request/response types using JSDoc `@typedef`:
|
|
|
850
867
|
|
|
851
868
|
### Must Do (✅)
|
|
852
869
|
- ✅ Validate all inputs (app names, file paths, URLs)
|
|
870
|
+
- ✅ When fixing bugs in integration/ or builder/ output: identify the generator that produces the file and fix the source (lib/generator, lib/commands, templates), not only the generated artifact
|
|
853
871
|
- ✅ Use try-catch for all async operations
|
|
854
872
|
- ✅ Provide meaningful error messages with context
|
|
855
873
|
- ✅ Use JSDoc for all public functions
|
|
@@ -874,6 +892,7 @@ Define request/response types using JSDoc `@typedef`:
|
|
|
874
892
|
- ❌ Never use `eval()` or `Function()` constructor
|
|
875
893
|
- ❌ Never use raw paths (always use path.join)
|
|
876
894
|
- ❌ Never make direct API calls using `makeApiCall` in new code (use `lib/api/` modules)
|
|
895
|
+
- ❌ Never treat one-off edits in integration/ or builder/ as the permanent fix for a bug—update the generator or template that produces the file
|
|
877
896
|
- ❌ Never skip type definitions for API request/response types
|
|
878
897
|
- ❌ Never log authentication tokens or secrets in API calls
|
|
879
898
|
|
|
@@ -511,7 +511,7 @@ async function checkAppDirectory(appPath) {
|
|
|
511
511
|
* @throws {Error} If required files are missing
|
|
512
512
|
*/
|
|
513
513
|
async function validateRequiredFiles(appPath, entries) {
|
|
514
|
-
const requiredFiles = ['variables.yaml', 'env.template', 'README.md', 'deploy.
|
|
514
|
+
const requiredFiles = ['variables.yaml', 'env.template', 'README.md', 'deploy.js'];
|
|
515
515
|
const missingFiles = [];
|
|
516
516
|
for (const fileName of requiredFiles) {
|
|
517
517
|
const filePath = path.join(appPath, fileName);
|
package/lib/api/wizard.api.js
CHANGED
|
@@ -311,7 +311,7 @@ async function testMcpConnection(dataplaneUrl, authConfig, serverUrl, token) {
|
|
|
311
311
|
}
|
|
312
312
|
|
|
313
313
|
/**
|
|
314
|
-
* Get deployment documentation for a system
|
|
314
|
+
* Get deployment documentation for a system (from dataplane DB only)
|
|
315
315
|
* GET /api/v1/wizard/deployment-docs/{systemKey}
|
|
316
316
|
* @async
|
|
317
317
|
* @function getDeploymentDocs
|
|
@@ -326,6 +326,28 @@ async function getDeploymentDocs(dataplaneUrl, authConfig, systemKey) {
|
|
|
326
326
|
return await client.get(`/api/v1/wizard/deployment-docs/${systemKey}`);
|
|
327
327
|
}
|
|
328
328
|
|
|
329
|
+
/**
|
|
330
|
+
* Generate deployment documentation with variables.yaml and deploy JSON for better quality
|
|
331
|
+
* POST /api/v1/wizard/deployment-docs/{systemKey}
|
|
332
|
+
* Sends deployJson and variablesYaml in the request body so the dataplane can align README with the integration folder.
|
|
333
|
+
* @async
|
|
334
|
+
* @function postDeploymentDocs
|
|
335
|
+
* @param {string} dataplaneUrl - Dataplane base URL
|
|
336
|
+
* @param {Object} authConfig - Authentication configuration
|
|
337
|
+
* @param {string} systemKey - System key identifier
|
|
338
|
+
* @param {Object} [body] - Optional request body (WizardDeploymentDocsRequest)
|
|
339
|
+
* @param {Object} [body.deployJson] - Deploy JSON object (e.g. *-deploy.json content)
|
|
340
|
+
* @param {string} [body.variablesYaml] - variables.yaml file content as string
|
|
341
|
+
* @returns {Promise<Object>} Deployment documentation response (content, contentType, systemKey)
|
|
342
|
+
* @throws {Error} If request fails
|
|
343
|
+
*/
|
|
344
|
+
async function postDeploymentDocs(dataplaneUrl, authConfig, systemKey, body = null) {
|
|
345
|
+
const client = new ApiClient(dataplaneUrl, authConfig);
|
|
346
|
+
return await client.post(`/api/v1/wizard/deployment-docs/${systemKey}`, {
|
|
347
|
+
body: body || {}
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
|
|
329
351
|
/**
|
|
330
352
|
* Get known wizard platforms from dataplane.
|
|
331
353
|
* GET /api/v1/wizard/platforms
|
|
@@ -365,5 +387,6 @@ module.exports = {
|
|
|
365
387
|
getPreview,
|
|
366
388
|
testMcpConnection,
|
|
367
389
|
getDeploymentDocs,
|
|
390
|
+
postDeploymentDocs,
|
|
368
391
|
getWizardPlatforms
|
|
369
392
|
};
|
package/lib/app/deploy.js
CHANGED
|
@@ -15,7 +15,7 @@ const yaml = require('js-yaml');
|
|
|
15
15
|
const chalk = require('chalk');
|
|
16
16
|
const pushUtils = require('../deployment/push');
|
|
17
17
|
const logger = require('../utils/logger');
|
|
18
|
-
const { detectAppType } = require('../utils/paths');
|
|
18
|
+
const { detectAppType, getBuilderPath, getIntegrationPath } = require('../utils/paths');
|
|
19
19
|
const { checkApplicationExists } = require('../utils/app-existence');
|
|
20
20
|
const { loadDeploymentConfig } = require('./deploy-config');
|
|
21
21
|
|
|
@@ -219,15 +219,16 @@ function displayDeploymentResults(result) {
|
|
|
219
219
|
}
|
|
220
220
|
|
|
221
221
|
/**
|
|
222
|
-
* Check if app is external and handle external deployment
|
|
222
|
+
* Check if app is external and handle external deployment.
|
|
223
|
+
* When options.type === 'external', forces deployment from integration/<app> (no app register needed).
|
|
223
224
|
* @async
|
|
224
225
|
* @function handleExternalDeployment
|
|
225
226
|
* @param {string} appName - Application name
|
|
226
|
-
* @param {Object} options - Deployment options
|
|
227
|
+
* @param {Object} options - Deployment options (type: 'external' to force integration/<app>)
|
|
227
228
|
* @returns {Promise<Object|null>} Deployment result if external, null otherwise
|
|
228
229
|
*/
|
|
229
230
|
async function handleExternalDeployment(appName, options) {
|
|
230
|
-
const { isExternal } = await detectAppType(appName);
|
|
231
|
+
const { isExternal } = await detectAppType(appName, options);
|
|
231
232
|
if (isExternal) {
|
|
232
233
|
const externalDeploy = require('../external-system/deploy');
|
|
233
234
|
await externalDeploy.deployExternalSystem(appName, options);
|
|
@@ -317,6 +318,37 @@ async function executeStandardDeployment(appName, options) {
|
|
|
317
318
|
}
|
|
318
319
|
}
|
|
319
320
|
|
|
321
|
+
/**
|
|
322
|
+
* Tries external deploy when builder/<app> does not exist but integration/<app> does.
|
|
323
|
+
* @async
|
|
324
|
+
* @param {string} appName - Application name
|
|
325
|
+
* @param {Object} options - Deployment options
|
|
326
|
+
* @returns {Promise<{usedExternalDeploy: boolean, result: Object|null}>}
|
|
327
|
+
*/
|
|
328
|
+
async function tryExternalDeployFallback(appName, options) {
|
|
329
|
+
const builderPath = getBuilderPath(appName);
|
|
330
|
+
const integrationPath = getIntegrationPath(appName);
|
|
331
|
+
let builderExists = false;
|
|
332
|
+
let integrationExists = false;
|
|
333
|
+
try {
|
|
334
|
+
await fs.access(builderPath);
|
|
335
|
+
builderExists = true;
|
|
336
|
+
} catch (e) {
|
|
337
|
+
if (e.code !== 'ENOENT') throw e;
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
await fs.access(integrationPath);
|
|
341
|
+
integrationExists = true;
|
|
342
|
+
} catch (e) {
|
|
343
|
+
if (e.code !== 'ENOENT') throw e;
|
|
344
|
+
}
|
|
345
|
+
if (!builderExists && integrationExists) {
|
|
346
|
+
const fallbackResult = await handleExternalDeployment(appName, { ...options, type: 'external' });
|
|
347
|
+
if (fallbackResult) return { usedExternalDeploy: true, result: fallbackResult };
|
|
348
|
+
}
|
|
349
|
+
return { usedExternalDeploy: false, result: null };
|
|
350
|
+
}
|
|
351
|
+
|
|
320
352
|
/**
|
|
321
353
|
* Deploys application to Miso Controller
|
|
322
354
|
* Orchestrates manifest generation, key creation, and deployment
|
|
@@ -338,7 +370,7 @@ async function executeStandardDeployment(appName, options) {
|
|
|
338
370
|
*/
|
|
339
371
|
async function deployApp(appName, options = {}) {
|
|
340
372
|
let controllerUrl = null;
|
|
341
|
-
let usedExternalDeploy =
|
|
373
|
+
let usedExternalDeploy = options.type === 'external';
|
|
342
374
|
|
|
343
375
|
try {
|
|
344
376
|
if (!appName || typeof appName !== 'string' || appName.trim().length === 0) {
|
|
@@ -347,8 +379,12 @@ async function deployApp(appName, options = {}) {
|
|
|
347
379
|
validateAppName(appName);
|
|
348
380
|
|
|
349
381
|
const externalResult = await handleExternalDeployment(appName, options);
|
|
350
|
-
if (externalResult)
|
|
351
|
-
|
|
382
|
+
if (externalResult) return externalResult;
|
|
383
|
+
|
|
384
|
+
const fallback = await tryExternalDeployFallback(appName, options);
|
|
385
|
+
if (fallback.result) {
|
|
386
|
+
usedExternalDeploy = fallback.usedExternalDeploy;
|
|
387
|
+
return fallback.result;
|
|
352
388
|
}
|
|
353
389
|
usedExternalDeploy = false;
|
|
354
390
|
|
package/lib/app/list.js
CHANGED
|
@@ -164,9 +164,11 @@ function displayApplications(applications, environment, controllerUrl) {
|
|
|
164
164
|
|
|
165
165
|
logger.log(chalk.bold(`\n📱 ${header}:\n`));
|
|
166
166
|
applications.forEach((app) => {
|
|
167
|
+
const isExternal = app.configuration?.type === 'external';
|
|
168
|
+
const externalIcon = isExternal ? '🔗 ' : '';
|
|
167
169
|
const hasPipeline = app.configuration?.pipeline?.isActive ? '✓' : '✗';
|
|
168
170
|
const urlAndPort = formatUrlAndPort(app);
|
|
169
|
-
logger.log(`${hasPipeline} ${chalk.cyan(app.key)} - ${app.displayName} (${app.status || 'unknown'})${urlAndPort}`);
|
|
171
|
+
logger.log(`${externalIcon}${hasPipeline} ${chalk.cyan(app.key)} - ${app.displayName} (${app.status || 'unknown'})${urlAndPort}`);
|
|
170
172
|
});
|
|
171
173
|
logger.log(chalk.gray(' To show details for an app: aifabrix app show <appKey>\n'));
|
|
172
174
|
}
|
package/lib/build/index.js
CHANGED
|
@@ -200,7 +200,7 @@ async function postBuildTasks(appName, buildConfig) {
|
|
|
200
200
|
}
|
|
201
201
|
|
|
202
202
|
/**
|
|
203
|
-
*
|
|
203
|
+
* External apps have no Docker image; deploy JSON is generated by aifabrix json.
|
|
204
204
|
* @async
|
|
205
205
|
* @param {string} appName - Application name
|
|
206
206
|
* @returns {Promise<boolean>} True if external (handled), false if should continue
|
|
@@ -208,9 +208,8 @@ async function postBuildTasks(appName, buildConfig) {
|
|
|
208
208
|
async function checkExternalAppType(appName) {
|
|
209
209
|
const variables = await loadVariablesYaml(appName);
|
|
210
210
|
if (variables.app && variables.app.type === 'external') {
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
logger.log(chalk.green(`✓ Generated deployment JSON: ${jsonPath}`));
|
|
211
|
+
logger.log(chalk.blue(`External system: ${appName}`));
|
|
212
|
+
logger.log(chalk.gray('To regenerate deployment JSON, run: aifabrix json ' + appName));
|
|
214
213
|
return true;
|
|
215
214
|
}
|
|
216
215
|
return false;
|
package/lib/cli/setup-app.js
CHANGED
|
@@ -197,6 +197,7 @@ See integration/hubspot/wizard-hubspot-e2e.yaml for an example.`)
|
|
|
197
197
|
|
|
198
198
|
program.command('deploy <app>')
|
|
199
199
|
.description('Deploy to Azure via Miso Controller')
|
|
200
|
+
.option('--type <type>', 'Application type: external to deploy from integration/<app> (no app register needed)')
|
|
200
201
|
.option('--client-id <id>', 'Client ID (overrides config)')
|
|
201
202
|
.option('--client-secret <secret>', 'Client Secret (overrides config)')
|
|
202
203
|
.option('--poll', 'Poll for deployment status', true)
|
|
@@ -66,6 +66,7 @@ function setupExternalSystemCommands(program) {
|
|
|
66
66
|
.description('Run integration tests via dataplane pipeline API')
|
|
67
67
|
.option('-d, --datasource <key>', 'Test specific datasource only')
|
|
68
68
|
.option('-p, --payload <file>', 'Path to custom test payload file')
|
|
69
|
+
.option('--dataplane <url>', 'Dataplane URL (default: discovered from controller)')
|
|
69
70
|
.option('-v, --verbose', 'Show detailed test output')
|
|
70
71
|
.option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
|
|
71
72
|
.action(async(appName, options) => {
|
package/lib/cli/setup-utility.js
CHANGED
|
@@ -87,7 +87,7 @@ function setupUtilityCommands(program) {
|
|
|
87
87
|
});
|
|
88
88
|
|
|
89
89
|
program.command('json <app>')
|
|
90
|
-
.description('Generate deployment JSON (
|
|
90
|
+
.description('Generate deployment JSON to disk (<app>-deploy.json). Use before commit so version control has the correct file.')
|
|
91
91
|
.option('--type <type>', 'Application type (external) - if set, only checks integration folder')
|
|
92
92
|
.action(async(appName, options) => {
|
|
93
93
|
try {
|
|
@@ -30,6 +30,36 @@ async function ensureTemplateAtPath(appName, targetAppPath) {
|
|
|
30
30
|
return true;
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Patches variables.yaml to set build.envOutputPath to null for deploy-only (no local code).
|
|
35
|
+
* Use when running up-miso/up-platform so we do not copy .env to repo paths or show that message.
|
|
36
|
+
* Patches both primary builder path and cwd/builder if different.
|
|
37
|
+
*
|
|
38
|
+
* @param {string} appName - Application name (e.g. miso-controller, dataplane)
|
|
39
|
+
*/
|
|
40
|
+
function patchEnvOutputPathForDeployOnly(appName) {
|
|
41
|
+
if (!appName || typeof appName !== 'string') return;
|
|
42
|
+
const pathsToPatch = [pathsUtil.getBuilderPath(appName)];
|
|
43
|
+
const cwdBuilderPath = path.join(process.cwd(), 'builder', appName);
|
|
44
|
+
if (path.resolve(cwdBuilderPath) !== path.resolve(pathsToPatch[0])) {
|
|
45
|
+
pathsToPatch.push(cwdBuilderPath);
|
|
46
|
+
}
|
|
47
|
+
const envOutputPathLine = /^(\s*envOutputPath:)\s*.*$/m;
|
|
48
|
+
const replacement = '$1 null # deploy only, no copy';
|
|
49
|
+
for (const appPath of pathsToPatch) {
|
|
50
|
+
const variablesPath = path.join(appPath, 'variables.yaml');
|
|
51
|
+
if (!fs.existsSync(variablesPath)) continue;
|
|
52
|
+
try {
|
|
53
|
+
let content = fs.readFileSync(variablesPath, 'utf8');
|
|
54
|
+
if (!envOutputPathLine.test(content)) continue;
|
|
55
|
+
content = content.replace(envOutputPathLine, replacement);
|
|
56
|
+
fs.writeFileSync(variablesPath, content, 'utf8');
|
|
57
|
+
} catch (err) {
|
|
58
|
+
logger.warn(chalk.yellow(`Could not patch envOutputPath in ${variablesPath}: ${err.message}`));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
33
63
|
/**
|
|
34
64
|
* Ensures builder app directory exists from template if variables.yaml is missing.
|
|
35
65
|
* If builder/<appName>/variables.yaml does not exist, copies from templates/applications/<appName>.
|
|
@@ -69,4 +99,4 @@ async function ensureAppFromTemplate(appName) {
|
|
|
69
99
|
return primaryCopied;
|
|
70
100
|
}
|
|
71
101
|
|
|
72
|
-
module.exports = { ensureAppFromTemplate };
|
|
102
|
+
module.exports = { ensureAppFromTemplate, patchEnvOutputPathForDeployOnly };
|
package/lib/commands/up-miso.js
CHANGED
|
@@ -19,7 +19,7 @@ const secrets = require('../core/secrets');
|
|
|
19
19
|
const infra = require('../infrastructure');
|
|
20
20
|
const app = require('../app');
|
|
21
21
|
const { saveLocalSecret } = require('../utils/local-secrets');
|
|
22
|
-
const { ensureAppFromTemplate } = require('./up-common');
|
|
22
|
+
const { ensureAppFromTemplate, patchEnvOutputPathForDeployOnly } = require('./up-common');
|
|
23
23
|
|
|
24
24
|
/** Keycloak base port (from templates/applications/keycloak/variables.yaml) */
|
|
25
25
|
const KEYCLOAK_BASE_PORT = 8082;
|
|
@@ -132,12 +132,16 @@ async function handleUpMiso(options = {}) {
|
|
|
132
132
|
await ensureAppFromTemplate('keycloak');
|
|
133
133
|
await ensureAppFromTemplate('miso-controller');
|
|
134
134
|
await ensureAppFromTemplate('dataplane');
|
|
135
|
+
// Deploy-only: do not copy .env to repo paths; patch variables so envOutputPath is null
|
|
136
|
+
patchEnvOutputPathForDeployOnly('keycloak');
|
|
137
|
+
patchEnvOutputPathForDeployOnly('miso-controller');
|
|
138
|
+
patchEnvOutputPathForDeployOnly('dataplane');
|
|
135
139
|
const developerId = await config.getDeveloperId();
|
|
136
140
|
const devIdNum = typeof developerId === 'string' ? parseInt(developerId, 10) : developerId;
|
|
137
141
|
await setMisoSecretsAndResolve(devIdNum);
|
|
138
142
|
await runMisoApps(options);
|
|
139
|
-
logger.log(chalk.green('\n✓ up-miso complete. Keycloak, miso-controller, and dataplane are running.')
|
|
140
|
-
|
|
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.'));
|
|
141
145
|
}
|
|
142
146
|
|
|
143
147
|
module.exports = { handleUpMiso, parseImageOptions };
|
|
@@ -18,7 +18,8 @@ const {
|
|
|
18
18
|
detectType,
|
|
19
19
|
generateConfig,
|
|
20
20
|
validateWizardConfig,
|
|
21
|
-
getDeploymentDocs
|
|
21
|
+
getDeploymentDocs,
|
|
22
|
+
postDeploymentDocs
|
|
22
23
|
} = require('../api/wizard.api');
|
|
23
24
|
const { generateWizardFiles } = require('../generator/wizard');
|
|
24
25
|
const {
|
|
@@ -337,6 +338,44 @@ async function validateWizardConfiguration(dataplaneUrl, authConfig, systemConfi
|
|
|
337
338
|
}
|
|
338
339
|
}
|
|
339
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
|
+
|
|
340
379
|
/**
|
|
341
380
|
* Handle file saving step
|
|
342
381
|
* @async
|
|
@@ -353,23 +392,21 @@ async function handleFileSaving(appName, systemConfig, datasourceConfigs, system
|
|
|
353
392
|
logger.log(chalk.blue('\n\uD83D\uDCCB Step 7: Save Files'));
|
|
354
393
|
const spinner = ora('Saving files...').start();
|
|
355
394
|
try {
|
|
356
|
-
|
|
357
|
-
if (systemKey && dataplaneUrl && authConfig) {
|
|
395
|
+
const generatedFiles = await generateWizardFiles(appName, systemConfig, datasourceConfigs, systemKey, { aiGeneratedReadme: null });
|
|
396
|
+
if (systemKey && dataplaneUrl && authConfig && generatedFiles.appPath) {
|
|
358
397
|
try {
|
|
359
|
-
|
|
360
|
-
if (docsResponse.success && docsResponse.data?.content) aiGeneratedReadme = docsResponse.data.content;
|
|
398
|
+
await tryUpdateReadmeFromDeploymentDocs(generatedFiles.appPath, appName, dataplaneUrl, authConfig, systemKey);
|
|
361
399
|
} catch (e) {
|
|
362
400
|
logger.log(chalk.gray(` Could not fetch AI-generated README: ${e.message}`));
|
|
363
401
|
}
|
|
364
402
|
}
|
|
365
|
-
const generatedFiles = await generateWizardFiles(appName, systemConfig, datasourceConfigs, systemKey, { aiGeneratedReadme });
|
|
366
403
|
spinner.stop();
|
|
367
404
|
logger.log(chalk.green('\n\u2713 Wizard completed successfully!'));
|
|
368
405
|
logger.log(chalk.green(`\nFiles created in: ${generatedFiles.appPath}`));
|
|
369
406
|
logger.log(chalk.blue('\nNext steps:'));
|
|
370
407
|
logger.log(chalk.gray(` 1. Review the generated files in integration/${appName}/`));
|
|
371
408
|
logger.log(chalk.gray(' 2. Update env.template with your authentication details'));
|
|
372
|
-
logger.log(chalk.gray(` 3. Deploy using:
|
|
409
|
+
logger.log(chalk.gray(` 3. Deploy using: node deploy.js or aifabrix deploy ${appName}`));
|
|
373
410
|
return generatedFiles;
|
|
374
411
|
} catch (error) {
|
|
375
412
|
spinner.stop();
|
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,
|