@aifabrix/builder 2.32.2 → 2.33.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/.cursor/rules/project-rules.mdc +8 -0
- package/README.md +36 -8
- package/bin/aifabrix.js +6 -8
- package/integration/hubspot/README.md +8 -7
- package/integration/hubspot/companies.json +2048 -0
- package/integration/hubspot/create-hubspot.js +665 -0
- package/integration/hubspot/{hubspot-deploy-company.json → hubspot-datasource-company.json} +1 -1
- package/integration/hubspot/{hubspot-deploy-contact.json → hubspot-datasource-contact.json} +1 -1
- package/integration/hubspot/{hubspot-deploy-deal.json → hubspot-datasource-deal.json} +1 -1
- package/integration/hubspot/hubspot-deploy.json +832 -81
- package/integration/hubspot/hubspot-system.json +99 -0
- package/integration/hubspot/test-artifacts/wizard-hubspot-credential-real.yaml +20 -0
- package/integration/hubspot/test-artifacts/wizard-hubspot-env-vars.yaml +9 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-add-datasource.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-app-name.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-credential-create.yaml +7 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-credential-select.yaml +7 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-known-platform.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-app.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-mode.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-openapi-file.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-openapi-url.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-invalid-source.yaml +4 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-array-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-test.yaml +5 -0
- package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +5 -0
- package/integration/hubspot/test-dataplane-down-helpers.js +246 -0
- package/integration/hubspot/test-dataplane-down-tests.js +419 -0
- package/integration/hubspot/test-dataplane-down.js +157 -0
- package/integration/hubspot/test.js +1517 -0
- package/integration/hubspot/variables.yaml +4 -4
- package/integration/hubspot/wizard-hubspot-e2e.yaml +16 -0
- package/integration/hubspot/wizard-hubspot-platform.yaml +8 -0
- package/lib/api/applications.api.js +1 -0
- package/lib/api/index.js +10 -5
- package/lib/api/types/wizard.types.js +176 -38
- package/lib/api/wizard.api.js +207 -38
- package/lib/app/deploy.js +116 -54
- package/lib/app/display.js +6 -5
- package/lib/app/dockerfile.js +2 -1
- package/lib/app/list.js +78 -37
- package/lib/app/prompts.js +9 -5
- package/lib/app/readme.js +41 -112
- package/lib/app/register.js +44 -9
- package/lib/app/rotate-secret.js +50 -32
- package/lib/cli.js +243 -65
- package/lib/commands/app.js +4 -9
- package/lib/commands/auth-config.js +125 -0
- package/lib/commands/auth-status.js +261 -0
- package/lib/commands/datasource.js +3 -6
- package/lib/commands/login-credentials.js +4 -4
- package/lib/commands/login-device.js +43 -29
- package/lib/commands/login.js +22 -13
- package/lib/commands/wizard-config-normalizer.js +92 -0
- package/lib/commands/wizard-core.js +515 -0
- package/lib/commands/wizard-dataplane.js +122 -0
- package/lib/commands/wizard-headless.js +115 -0
- package/lib/commands/wizard.js +129 -357
- package/lib/core/config.js +46 -0
- package/lib/core/secrets.js +3 -22
- package/lib/core/templates-env.js +1 -1
- package/lib/datasource/deploy.js +34 -23
- package/lib/datasource/list.js +8 -6
- package/lib/deployment/deployer.js +25 -0
- package/lib/deployment/environment.js +10 -13
- package/lib/external-system/delete.js +151 -0
- package/lib/external-system/deploy.js +54 -378
- package/lib/external-system/download-helpers.js +45 -65
- package/lib/external-system/download.js +34 -13
- package/lib/external-system/generator.js +11 -7
- package/lib/external-system/test-auth.js +5 -3
- package/lib/generator/builders.js +3 -1
- package/lib/generator/external-controller-manifest.js +157 -0
- package/lib/generator/external-schema-utils.js +236 -0
- package/lib/generator/external.js +55 -3
- package/lib/generator/index.js +22 -10
- package/lib/generator/wizard-prompts.js +33 -10
- package/lib/generator/wizard.js +69 -86
- package/lib/infrastructure/compose.js +100 -0
- package/lib/infrastructure/helpers.js +139 -0
- package/lib/infrastructure/index.js +52 -311
- package/lib/infrastructure/services.js +168 -0
- package/lib/schema/application-schema.json +24 -5
- package/lib/schema/external-datasource.schema.json +303 -17
- package/lib/schema/external-system.schema.json +1 -1
- package/lib/schema/wizard-config.schema.json +234 -0
- package/lib/utils/api.js +37 -42
- package/lib/utils/app-existence.js +42 -0
- package/lib/utils/app-register-config.js +7 -2
- package/lib/utils/app-register-display.js +2 -1
- package/lib/utils/auth-config-validator.js +92 -0
- package/lib/utils/cli-utils.js +3 -1
- package/lib/utils/command-header.js +43 -0
- package/lib/utils/compose-generator.js +113 -70
- package/lib/utils/controller-url.js +115 -0
- package/lib/utils/dataplane-health.js +115 -0
- package/lib/utils/dataplane-resolver.js +29 -0
- package/lib/utils/dev-config.js +6 -2
- package/lib/utils/env-copy.js +2 -1
- package/lib/utils/env-map.js +2 -1
- package/lib/utils/env-ports.js +2 -1
- package/lib/utils/env-template.js +1 -1
- package/lib/utils/error-formatter.js +149 -28
- package/lib/utils/external-readme.js +125 -0
- package/lib/utils/help-builder.js +190 -0
- package/lib/utils/infra-status.js +13 -3
- package/lib/utils/paths.js +17 -2
- package/lib/utils/port-resolver.js +111 -0
- package/lib/utils/secrets-helpers.js +3 -15
- package/lib/utils/secrets-utils.js +2 -2
- package/lib/utils/token-manager.js +69 -4
- package/lib/utils/variable-transformer.js +7 -2
- package/lib/validation/external-manifest-validator.js +202 -0
- package/lib/validation/validate-display.js +406 -0
- package/lib/validation/validate.js +159 -123
- package/lib/validation/validator.js +38 -4
- package/lib/validation/wizard-config-validator.js +267 -0
- package/package.json +4 -2
- package/templates/applications/README.md.hbs +19 -17
- package/templates/applications/miso-controller/env.template +1 -1
- package/templates/applications/miso-controller/rbac.yaml +7 -7
- package/templates/external-system/README.md.hbs +99 -0
- package/templates/external-system/external-system.json.hbs +1 -1
- package/templates/infra/compose.yaml.hbs +35 -0
- package/templates/python/docker-compose.hbs +26 -0
- package/templates/typescript/docker-compose.hbs +26 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Categorized help builder for AI Fabrix Builder CLI
|
|
3
|
+
*
|
|
4
|
+
* Groups commands into logical categories and outputs a user-friendly help.
|
|
5
|
+
*
|
|
6
|
+
* @fileoverview Categorized CLI help
|
|
7
|
+
* @author AI Fabrix Team
|
|
8
|
+
* @version 2.0.0
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const { Help } = require('commander');
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Command categories and order. Each command can have an optional `term` override
|
|
15
|
+
* for the help line (e.g. "down [app]"); otherwise the command name is used.
|
|
16
|
+
*
|
|
17
|
+
* @type {Array<{ name: string, commands: Array<{ name: string, term?: string }> }>}
|
|
18
|
+
*/
|
|
19
|
+
const CATEGORIES = [
|
|
20
|
+
{
|
|
21
|
+
name: 'Infrastructure (Local Development)',
|
|
22
|
+
commands: [
|
|
23
|
+
{ name: 'up' },
|
|
24
|
+
{ name: 'down', term: 'down [app]' },
|
|
25
|
+
{ name: 'doctor' },
|
|
26
|
+
{ name: 'status' },
|
|
27
|
+
{ name: 'restart', term: 'restart <service>' }
|
|
28
|
+
]
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
name: 'Authentication',
|
|
32
|
+
commands: [
|
|
33
|
+
{ name: 'login' },
|
|
34
|
+
{ name: 'logout' },
|
|
35
|
+
{ name: 'auth' }
|
|
36
|
+
]
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
name: 'Applications (Create & Develop)',
|
|
40
|
+
commands: [
|
|
41
|
+
{ name: 'create', term: 'create <app>' },
|
|
42
|
+
{ name: 'wizard' },
|
|
43
|
+
{ name: 'build', term: 'build <app>' },
|
|
44
|
+
{ name: 'run', term: 'run <app>' },
|
|
45
|
+
{ name: 'dockerfile', term: 'dockerfile <app>' }
|
|
46
|
+
]
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
name: 'Deployment',
|
|
50
|
+
commands: [
|
|
51
|
+
{ name: 'push', term: 'push <app>' },
|
|
52
|
+
{ name: 'deploy', term: 'deploy <app>' }
|
|
53
|
+
]
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
name: 'Environments',
|
|
57
|
+
commands: [
|
|
58
|
+
{ name: 'environment' },
|
|
59
|
+
{ name: 'env' }
|
|
60
|
+
]
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
name: 'Application & Datasource Management',
|
|
64
|
+
commands: [
|
|
65
|
+
{ name: 'app' },
|
|
66
|
+
{ name: 'datasource' }
|
|
67
|
+
]
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
name: 'Configuration & Validation',
|
|
71
|
+
commands: [
|
|
72
|
+
{ name: 'resolve', term: 'resolve <app>' },
|
|
73
|
+
{ name: 'json', term: 'json <app>' },
|
|
74
|
+
{ name: 'split-json', term: 'split-json <app>' },
|
|
75
|
+
{ name: 'genkey', term: 'genkey <app>' },
|
|
76
|
+
{ name: 'validate', term: 'validate <appOrFile>' },
|
|
77
|
+
{ name: 'diff', term: 'diff <file1> <file2>' }
|
|
78
|
+
]
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'External Systems',
|
|
82
|
+
commands: [
|
|
83
|
+
{ name: 'download', term: 'download <system-key>' },
|
|
84
|
+
{ name: 'delete', term: 'delete <system-key>' },
|
|
85
|
+
{ name: 'test', term: 'test <app>' },
|
|
86
|
+
{ name: 'test-integration', term: 'test-integration <app>' }
|
|
87
|
+
]
|
|
88
|
+
},
|
|
89
|
+
{
|
|
90
|
+
name: 'Developer & Secrets',
|
|
91
|
+
commands: [
|
|
92
|
+
{ name: 'dev' },
|
|
93
|
+
{ name: 'secrets' },
|
|
94
|
+
{ name: 'secure' }
|
|
95
|
+
]
|
|
96
|
+
}
|
|
97
|
+
];
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* @param {object} helper - Commander Help instance
|
|
101
|
+
* @param {import('commander').Command} program
|
|
102
|
+
* @returns {string[]}
|
|
103
|
+
*/
|
|
104
|
+
function formatHeader(helper, program) {
|
|
105
|
+
const out = [`Usage: ${helper.commandUsage(program)}`, ''];
|
|
106
|
+
const desc = helper.commandDescription(program);
|
|
107
|
+
if (desc && String(desc).length > 0) {
|
|
108
|
+
out.push(helper.wrap(String(desc), helper.helpWidth || 80, 0), '');
|
|
109
|
+
}
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* @param {object} helper - Commander Help instance
|
|
115
|
+
* @param {import('commander').Command} program
|
|
116
|
+
* @returns {string[]}
|
|
117
|
+
*/
|
|
118
|
+
function formatOptions(helper, program) {
|
|
119
|
+
const options = helper.visibleOptions(program);
|
|
120
|
+
if (options.length === 0) return [];
|
|
121
|
+
const optTermWidth = options.reduce((max, o) => Math.max(max, helper.optionTerm(o).length), 0);
|
|
122
|
+
const out = ['Options:'];
|
|
123
|
+
for (const opt of options) {
|
|
124
|
+
out.push(` ${helper.optionTerm(opt).padEnd(optTermWidth + 2)}${helper.optionDescription(opt)}`);
|
|
125
|
+
}
|
|
126
|
+
out.push('');
|
|
127
|
+
return out;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* @param {object} helper - Commander Help instance
|
|
132
|
+
* @param {import('commander').Command} program
|
|
133
|
+
* @returns {{ categorized: Array<{ name: string, lines: Array<{ term: string, desc: string }> }>, pad: number }}
|
|
134
|
+
*/
|
|
135
|
+
function buildCategorizedWithPad(helper, program) {
|
|
136
|
+
const nameToCmd = new Map(program.commands.map((c) => [c.name(), c]));
|
|
137
|
+
const categorized = [];
|
|
138
|
+
let maxTermLen = 'help [command]'.length;
|
|
139
|
+
|
|
140
|
+
for (const cat of CATEGORIES) {
|
|
141
|
+
const lines = [];
|
|
142
|
+
for (const spec of cat.commands) {
|
|
143
|
+
const cmd = nameToCmd.get(spec.name);
|
|
144
|
+
if (!cmd) continue;
|
|
145
|
+
const term = spec.term || cmd.name();
|
|
146
|
+
maxTermLen = Math.max(maxTermLen, term.length);
|
|
147
|
+
const descText = helper.subcommandDescription(cmd) || cmd.description() || '';
|
|
148
|
+
lines.push({ term, desc: descText });
|
|
149
|
+
}
|
|
150
|
+
if (lines.length > 0) categorized.push({ name: cat.name, lines });
|
|
151
|
+
}
|
|
152
|
+
return { categorized, pad: maxTermLen + 2 };
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {object} helper - Commander Help instance
|
|
157
|
+
* @param {import('commander').Command} program
|
|
158
|
+
* @returns {string[]}
|
|
159
|
+
*/
|
|
160
|
+
function formatCommandCategories(helper, program) {
|
|
161
|
+
const { categorized, pad } = buildCategorizedWithPad(helper, program);
|
|
162
|
+
const out = [];
|
|
163
|
+
for (const { name, lines } of categorized) {
|
|
164
|
+
out.push(name + ':');
|
|
165
|
+
for (const { term, desc: d } of lines) out.push(` ${term.padEnd(pad)}${d}`);
|
|
166
|
+
out.push('');
|
|
167
|
+
}
|
|
168
|
+
out.push('Help:');
|
|
169
|
+
out.push(` ${'help [command]'.padEnd(pad)}display help for command`);
|
|
170
|
+
out.push('');
|
|
171
|
+
return out;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Build the full categorized help string for the program.
|
|
176
|
+
*
|
|
177
|
+
* @param {import('commander').Command} program - Commander program
|
|
178
|
+
* @returns {string} Formatted help text
|
|
179
|
+
*/
|
|
180
|
+
function buildCategorizedHelp(program) {
|
|
181
|
+
const helper = new Help();
|
|
182
|
+
const output = [
|
|
183
|
+
...formatHeader(helper, program),
|
|
184
|
+
...formatOptions(helper, program),
|
|
185
|
+
...formatCommandCategories(helper, program)
|
|
186
|
+
];
|
|
187
|
+
return output.join('\n');
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
module.exports = { buildCategorizedHelp, CATEGORIES };
|
|
@@ -38,7 +38,11 @@ async function getInfraStatus() {
|
|
|
38
38
|
postgres: { port: ports.postgres, url: `localhost:${ports.postgres}` },
|
|
39
39
|
redis: { port: ports.redis, url: `localhost:${ports.redis}` },
|
|
40
40
|
pgadmin: { port: ports.pgadmin, url: `http://localhost:${ports.pgadmin}` },
|
|
41
|
-
'redis-commander': { port: ports.redisCommander, url: `http://localhost:${ports.redisCommander}` }
|
|
41
|
+
'redis-commander': { port: ports.redisCommander, url: `http://localhost:${ports.redisCommander}` },
|
|
42
|
+
traefik: {
|
|
43
|
+
port: `${ports.traefikHttp}/${ports.traefikHttps}`,
|
|
44
|
+
url: `http://localhost:${ports.traefikHttp}, https://localhost:${ports.traefikHttps}`
|
|
45
|
+
}
|
|
42
46
|
};
|
|
43
47
|
|
|
44
48
|
const status = {};
|
|
@@ -82,9 +86,15 @@ async function getInfraStatus() {
|
|
|
82
86
|
*/
|
|
83
87
|
function getInfraContainerNames(devIdNum, devId) {
|
|
84
88
|
if (devIdNum === 0) {
|
|
85
|
-
return ['aifabrix-postgres', 'aifabrix-redis', 'aifabrix-pgadmin', 'aifabrix-redis-commander'];
|
|
89
|
+
return ['aifabrix-postgres', 'aifabrix-redis', 'aifabrix-pgadmin', 'aifabrix-redis-commander', 'aifabrix-traefik'];
|
|
86
90
|
}
|
|
87
|
-
return [
|
|
91
|
+
return [
|
|
92
|
+
`aifabrix-dev${devId}-postgres`,
|
|
93
|
+
`aifabrix-dev${devId}-redis`,
|
|
94
|
+
`aifabrix-dev${devId}-pgadmin`,
|
|
95
|
+
`aifabrix-dev${devId}-redis-commander`,
|
|
96
|
+
`aifabrix-dev${devId}-traefik`
|
|
97
|
+
];
|
|
88
98
|
}
|
|
89
99
|
|
|
90
100
|
/**
|
package/lib/utils/paths.js
CHANGED
|
@@ -408,13 +408,28 @@ function checkBuilderFolder(appName) {
|
|
|
408
408
|
* Checks both integration/ and builder/ folders for backward compatibility
|
|
409
409
|
*
|
|
410
410
|
* @param {string} appName - Application name
|
|
411
|
-
* @
|
|
411
|
+
* @param {Object} [options] - Detection options
|
|
412
|
+
* @param {string} [options.type] - Forced application type (external)
|
|
413
|
+
* @returns {Promise<{isExternal: boolean, appPath: string, appType: string, baseDir?: string}>}
|
|
412
414
|
*/
|
|
413
|
-
async function detectAppType(appName) {
|
|
415
|
+
async function detectAppType(appName, options = {}) {
|
|
414
416
|
if (!appName || typeof appName !== 'string') {
|
|
415
417
|
throw new Error('App name is required and must be a string');
|
|
416
418
|
}
|
|
417
419
|
|
|
420
|
+
if (options.type === 'external') {
|
|
421
|
+
const integrationPath = getIntegrationPath(appName);
|
|
422
|
+
if (!fs.existsSync(integrationPath)) {
|
|
423
|
+
throw new Error(`External system not found in integration/${appName}`);
|
|
424
|
+
}
|
|
425
|
+
return {
|
|
426
|
+
isExternal: true,
|
|
427
|
+
appPath: integrationPath,
|
|
428
|
+
appType: 'external',
|
|
429
|
+
baseDir: 'integration'
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
|
|
418
433
|
// Check integration folder first (new structure)
|
|
419
434
|
const integrationResult = checkIntegrationFolder(appName);
|
|
420
435
|
if (integrationResult) {
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AI Fabrix Builder - Centralized Port Resolution
|
|
3
|
+
*
|
|
4
|
+
* Single source of truth for resolving application port from variables.yaml.
|
|
5
|
+
* Use getContainerPort for container/Docker/deployment/registration; use getLocalPort
|
|
6
|
+
* for local .env and dev-id–adjusted host port.
|
|
7
|
+
*
|
|
8
|
+
* @fileoverview Port resolution from variables (port, build.containerPort, build.localPort)
|
|
9
|
+
* @author AI Fabrix Team
|
|
10
|
+
* @version 2.0.0
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
'use strict';
|
|
14
|
+
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
const yaml = require('js-yaml');
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Resolve container port from variables object.
|
|
20
|
+
* Precedence: build.containerPort → port → defaultPort.
|
|
21
|
+
* Used for: Dockerfile, container .env PORT, compose, deployment, app register, variable-transformer, builders, secrets-utils.
|
|
22
|
+
*
|
|
23
|
+
* @param {Object} variables - Parsed variables.yaml (or subset with build, port)
|
|
24
|
+
* @param {number} [defaultPort=3000] - Default when neither build.containerPort nor port is set
|
|
25
|
+
* @returns {number} Resolved container port
|
|
26
|
+
*/
|
|
27
|
+
function getContainerPort(variables, defaultPort = 3000) {
|
|
28
|
+
const v = variables || {};
|
|
29
|
+
return v.build?.containerPort ?? v.port ?? defaultPort;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Resolve local (development) port from variables object.
|
|
34
|
+
* Precedence: build.localPort (if number and > 0) → port → defaultPort.
|
|
35
|
+
* Used for: env-copy, env-ports, and as base for getLocalPortFromPath (secrets-helpers).
|
|
36
|
+
*
|
|
37
|
+
* @param {Object} variables - Parsed variables.yaml
|
|
38
|
+
* @param {number} [defaultPort=3000] - Default when neither build.localPort nor port is set
|
|
39
|
+
* @returns {number} Resolved local port
|
|
40
|
+
*/
|
|
41
|
+
function getLocalPort(variables, defaultPort = 3000) {
|
|
42
|
+
const v = variables || {};
|
|
43
|
+
const local = v.build?.localPort;
|
|
44
|
+
if (typeof local === 'number' && local > 0) {
|
|
45
|
+
return local;
|
|
46
|
+
}
|
|
47
|
+
return v.port ?? defaultPort;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Load variables from path. Returns null if path missing, not found, or parse error.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} variablesPath - Path to variables.yaml
|
|
54
|
+
* @returns {Object|null} Parsed variables or null
|
|
55
|
+
*/
|
|
56
|
+
function loadVariablesFromPath(variablesPath) {
|
|
57
|
+
if (!variablesPath || !fs.existsSync(variablesPath)) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
const content = fs.readFileSync(variablesPath, 'utf8');
|
|
62
|
+
return yaml.load(content) || null;
|
|
63
|
+
} catch {
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Resolve container port from variables.yaml path.
|
|
70
|
+
* Returns null when file is missing or neither build.containerPort nor port is set (for chaining with other sources).
|
|
71
|
+
*
|
|
72
|
+
* @param {string} variablesPath - Path to variables.yaml
|
|
73
|
+
* @returns {number|null} Container port or null
|
|
74
|
+
*/
|
|
75
|
+
function getContainerPortFromPath(variablesPath) {
|
|
76
|
+
const v = loadVariablesFromPath(variablesPath);
|
|
77
|
+
if (!v) {
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
const p = v.build?.containerPort ?? v.port;
|
|
81
|
+
return (p !== undefined && p !== null) ? p : null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Resolve local port from variables.yaml path.
|
|
86
|
+
* Matches legacy getPortFromVariablesFile: build.localPort (if number and > 0) else variables.port or null.
|
|
87
|
+
* Returns null when file is missing or neither is set (for calculateAppPort chain).
|
|
88
|
+
*
|
|
89
|
+
* @param {string} variablesPath - Path to variables.yaml
|
|
90
|
+
* @returns {number|null} Local port or null
|
|
91
|
+
*/
|
|
92
|
+
function getLocalPortFromPath(variablesPath) {
|
|
93
|
+
const v = loadVariablesFromPath(variablesPath);
|
|
94
|
+
if (!v) {
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
const local = v.build?.localPort;
|
|
98
|
+
if (typeof local === 'number' && local > 0) {
|
|
99
|
+
return local;
|
|
100
|
+
}
|
|
101
|
+
const p = v.port;
|
|
102
|
+
return (p !== undefined && p !== null) ? p : null;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
module.exports = {
|
|
106
|
+
getContainerPort,
|
|
107
|
+
getLocalPort,
|
|
108
|
+
getContainerPortFromPath,
|
|
109
|
+
getLocalPortFromPath,
|
|
110
|
+
loadVariablesFromPath
|
|
111
|
+
};
|
|
@@ -17,6 +17,7 @@ const { rewriteInfraEndpoints, getEnvHosts, getServicePort, getServiceHost, getL
|
|
|
17
17
|
const { loadEnvConfig } = require('./env-config-loader');
|
|
18
18
|
const { updateContainerPortInEnvFile } = require('./env-ports');
|
|
19
19
|
const { buildEnvVarMap } = require('./env-map');
|
|
20
|
+
const { getLocalPortFromPath } = require('./port-resolver');
|
|
20
21
|
|
|
21
22
|
/**
|
|
22
23
|
* Interpolate ${VAR} occurrences with values from envVars map
|
|
@@ -145,26 +146,13 @@ function getPortFromLocalEnv(localEnv) {
|
|
|
145
146
|
}
|
|
146
147
|
|
|
147
148
|
/**
|
|
148
|
-
* Gets port from variables.yaml file
|
|
149
|
+
* Gets port from variables.yaml file (build.localPort if positive, else port). Uses port-resolver.
|
|
149
150
|
* @function getPortFromVariablesFile
|
|
150
151
|
* @param {string} variablesPath - Path to variables.yaml
|
|
151
152
|
* @returns {number|null} Port value or null
|
|
152
153
|
*/
|
|
153
154
|
function getPortFromVariablesFile(variablesPath) {
|
|
154
|
-
|
|
155
|
-
return null;
|
|
156
|
-
}
|
|
157
|
-
try {
|
|
158
|
-
const variablesContent = fs.readFileSync(variablesPath, 'utf8');
|
|
159
|
-
const variables = yaml.load(variablesContent);
|
|
160
|
-
const localPort = variables?.build?.localPort;
|
|
161
|
-
if (typeof localPort === 'number' && localPort > 0) {
|
|
162
|
-
return localPort;
|
|
163
|
-
}
|
|
164
|
-
return variables?.port || null;
|
|
165
|
-
} catch {
|
|
166
|
-
return null;
|
|
167
|
-
}
|
|
155
|
+
return getLocalPortFromPath(variablesPath);
|
|
168
156
|
}
|
|
169
157
|
|
|
170
158
|
/**
|
|
@@ -14,6 +14,7 @@ const path = require('path');
|
|
|
14
14
|
const yaml = require('js-yaml');
|
|
15
15
|
const logger = require('./logger');
|
|
16
16
|
const pathsUtil = require('./paths');
|
|
17
|
+
const { getContainerPort } = require('./port-resolver');
|
|
17
18
|
|
|
18
19
|
/**
|
|
19
20
|
* Loads secrets from file with cascading lookup support
|
|
@@ -145,8 +146,7 @@ function resolveUrlPort(protocol, hostname, port, urlPath, hostnameToService) {
|
|
|
145
146
|
const variablesContent = fs.readFileSync(serviceVariablesPath, 'utf8');
|
|
146
147
|
const variables = yaml.load(variablesContent);
|
|
147
148
|
|
|
148
|
-
|
|
149
|
-
const containerPort = variables?.build?.containerPort || variables?.port || port;
|
|
149
|
+
const containerPort = getContainerPort(variables, port);
|
|
150
150
|
|
|
151
151
|
// Replace port in URL
|
|
152
152
|
return `${protocol}${hostname}:${containerPort}${urlPath}`;
|
|
@@ -321,7 +321,7 @@ async function getDeploymentAuth(controllerUrl, environment, appName) {
|
|
|
321
321
|
async function extractClientCredentials(authConfig, appKey, envKey, _options = {}) {
|
|
322
322
|
if (authConfig.type === 'client-credentials') {
|
|
323
323
|
if (!authConfig.clientId || !authConfig.clientSecret) {
|
|
324
|
-
throw new Error('Client ID and Client Secret are required');
|
|
324
|
+
throw new Error('Client ID and Client Secret are required for client-credentials authentication');
|
|
325
325
|
}
|
|
326
326
|
return {
|
|
327
327
|
clientId: authConfig.clientId,
|
|
@@ -349,14 +349,77 @@ async function extractClientCredentials(authConfig, appKey, envKey, _options = {
|
|
|
349
349
|
};
|
|
350
350
|
}
|
|
351
351
|
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
352
|
+
// No credentials found - provide helpful error message
|
|
353
|
+
throw new Error(
|
|
354
|
+
'Client ID and Client Secret are required for deployment.\n' +
|
|
355
|
+
'Add credentials to ~/.aifabrix/secrets.local.yaml as:\n' +
|
|
356
|
+
` '${appKey}-client-idKeyVault': <client-id>\n` +
|
|
357
|
+
` '${appKey}-client-secretKeyVault': <client-secret>\n\n` +
|
|
358
|
+
'Or use credentials authentication with --client-id and --client-secret flags.'
|
|
359
|
+
);
|
|
355
360
|
}
|
|
356
361
|
|
|
357
362
|
throw new Error('Invalid authentication type');
|
|
358
363
|
}
|
|
359
364
|
|
|
365
|
+
/**
|
|
366
|
+
* Force refresh device token for controller (regardless of local expiry time)
|
|
367
|
+
* Used when server returns 401 even though local token hasn't expired
|
|
368
|
+
* @param {string} controllerUrl - Controller URL
|
|
369
|
+
* @returns {Promise<{token: string, controller: string}|null>} Token and controller URL, or null if not available
|
|
370
|
+
*/
|
|
371
|
+
async function forceRefreshDeviceToken(controllerUrl) {
|
|
372
|
+
// Try to get existing token to get refresh token
|
|
373
|
+
const tokenInfo = await getDeviceToken(controllerUrl);
|
|
374
|
+
|
|
375
|
+
if (!tokenInfo) {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Must have refresh token to force refresh
|
|
380
|
+
if (!tokenInfo.refreshToken) {
|
|
381
|
+
logger.warn('Cannot refresh: no refresh token available. Please login again using: aifabrix login');
|
|
382
|
+
return null;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
try {
|
|
386
|
+
const refreshed = await refreshDeviceToken(controllerUrl, tokenInfo.refreshToken);
|
|
387
|
+
return {
|
|
388
|
+
token: refreshed.token,
|
|
389
|
+
controller: controllerUrl
|
|
390
|
+
};
|
|
391
|
+
} catch (error) {
|
|
392
|
+
const errorMessage = error.message || String(error);
|
|
393
|
+
if (errorMessage.includes('Refresh token has expired')) {
|
|
394
|
+
logger.warn(`Refresh token expired: ${errorMessage}`);
|
|
395
|
+
} else {
|
|
396
|
+
logger.warn(`Failed to refresh device token: ${errorMessage}`);
|
|
397
|
+
}
|
|
398
|
+
return null;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Get device-only authentication for services that require user-level authentication.
|
|
404
|
+
* Used for interactive commands like wizard that don't support client credentials.
|
|
405
|
+
* @async
|
|
406
|
+
* @function getDeviceOnlyAuth
|
|
407
|
+
* @param {string} controllerUrl - Controller URL
|
|
408
|
+
* @returns {Promise<Object>} Auth config with device token
|
|
409
|
+
* @throws {Error} If device token is not available
|
|
410
|
+
*/
|
|
411
|
+
async function getDeviceOnlyAuth(controllerUrl) {
|
|
412
|
+
const deviceToken = await getOrRefreshDeviceToken(controllerUrl);
|
|
413
|
+
if (deviceToken && deviceToken.token) {
|
|
414
|
+
return {
|
|
415
|
+
type: 'bearer',
|
|
416
|
+
token: deviceToken.token,
|
|
417
|
+
controller: deviceToken.controller
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
throw new Error('Device token authentication required. Run "aifabrix login" to authenticate.');
|
|
421
|
+
}
|
|
422
|
+
|
|
360
423
|
module.exports = {
|
|
361
424
|
getDeviceToken,
|
|
362
425
|
getClientToken,
|
|
@@ -367,7 +430,9 @@ module.exports = {
|
|
|
367
430
|
loadClientCredentials,
|
|
368
431
|
getOrRefreshClientToken,
|
|
369
432
|
getOrRefreshDeviceToken,
|
|
433
|
+
forceRefreshDeviceToken,
|
|
370
434
|
getDeploymentAuth,
|
|
435
|
+
getDeviceOnlyAuth,
|
|
371
436
|
extractClientCredentials
|
|
372
437
|
};
|
|
373
438
|
|
|
@@ -9,6 +9,8 @@
|
|
|
9
9
|
* @version 2.0.0
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
+
const { getContainerPort } = require('./port-resolver');
|
|
13
|
+
|
|
12
14
|
/**
|
|
13
15
|
* Sanitizes authentication type - map keycloak to azure (schema allows: azure, local, none)
|
|
14
16
|
* @function sanitizeAuthType
|
|
@@ -40,7 +42,7 @@ function buildBaseResult(variables, appName) {
|
|
|
40
42
|
type: variables.type || 'webapp',
|
|
41
43
|
image: variables.image,
|
|
42
44
|
registryMode: variables.registryMode || 'external',
|
|
43
|
-
port: variables
|
|
45
|
+
port: getContainerPort(variables, 3000),
|
|
44
46
|
requiresDatabase: variables.requiresDatabase || false,
|
|
45
47
|
requiresRedis: variables.requiresRedis || false,
|
|
46
48
|
requiresStorage: variables.requiresStorage || false,
|
|
@@ -304,6 +306,9 @@ function transformSimpleOptionalFields(variables, transformed) {
|
|
|
304
306
|
if (variables.permissions) {
|
|
305
307
|
transformed.permissions = variables.permissions;
|
|
306
308
|
}
|
|
309
|
+
if (variables.externalIntegration) {
|
|
310
|
+
transformed.externalIntegration = variables.externalIntegration;
|
|
311
|
+
}
|
|
307
312
|
}
|
|
308
313
|
|
|
309
314
|
/**
|
|
@@ -356,7 +361,7 @@ function buildBaseTransformedStructure(variables, appName) {
|
|
|
356
361
|
type: variables.app?.type || 'webapp',
|
|
357
362
|
image: imageRef,
|
|
358
363
|
registryMode: variables.image?.registryMode || 'external',
|
|
359
|
-
port: variables
|
|
364
|
+
port: getContainerPort(variables, 3000),
|
|
360
365
|
requiresDatabase: requires.database || false,
|
|
361
366
|
requiresRedis: requires.redis || false,
|
|
362
367
|
requiresStorage: requires.storage || false,
|