@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.
Files changed (130) hide show
  1. package/.cursor/rules/project-rules.mdc +8 -0
  2. package/README.md +36 -8
  3. package/bin/aifabrix.js +6 -8
  4. package/integration/hubspot/README.md +8 -7
  5. package/integration/hubspot/companies.json +2048 -0
  6. package/integration/hubspot/create-hubspot.js +665 -0
  7. package/integration/hubspot/{hubspot-deploy-company.json → hubspot-datasource-company.json} +1 -1
  8. package/integration/hubspot/{hubspot-deploy-contact.json → hubspot-datasource-contact.json} +1 -1
  9. package/integration/hubspot/{hubspot-deploy-deal.json → hubspot-datasource-deal.json} +1 -1
  10. package/integration/hubspot/hubspot-deploy.json +832 -81
  11. package/integration/hubspot/hubspot-system.json +99 -0
  12. package/integration/hubspot/test-artifacts/wizard-hubspot-credential-real.yaml +20 -0
  13. package/integration/hubspot/test-artifacts/wizard-hubspot-env-vars.yaml +9 -0
  14. package/integration/hubspot/test-artifacts/wizard-invalid-add-datasource.yaml +5 -0
  15. package/integration/hubspot/test-artifacts/wizard-invalid-app-name.yaml +5 -0
  16. package/integration/hubspot/test-artifacts/wizard-invalid-credential-create.yaml +7 -0
  17. package/integration/hubspot/test-artifacts/wizard-invalid-credential-select.yaml +7 -0
  18. package/integration/hubspot/test-artifacts/wizard-invalid-known-platform.yaml +4 -0
  19. package/integration/hubspot/test-artifacts/wizard-invalid-missing-app.yaml +4 -0
  20. package/integration/hubspot/test-artifacts/wizard-invalid-missing-source.yaml +2 -0
  21. package/integration/hubspot/test-artifacts/wizard-invalid-mode.yaml +5 -0
  22. package/integration/hubspot/test-artifacts/wizard-invalid-openapi-file.yaml +5 -0
  23. package/integration/hubspot/test-artifacts/wizard-invalid-openapi-url.yaml +4 -0
  24. package/integration/hubspot/test-artifacts/wizard-invalid-source.yaml +4 -0
  25. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-array-test.yaml +5 -0
  26. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-key-test.yaml +5 -0
  27. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-path-test.yaml +5 -0
  28. package/integration/hubspot/test-artifacts/wizard-valid-for-dimension-test.yaml +5 -0
  29. package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-test.yaml +5 -0
  30. package/integration/hubspot/test-artifacts/wizard-valid-for-rbac-yaml-test.yaml +5 -0
  31. package/integration/hubspot/test-dataplane-down-helpers.js +246 -0
  32. package/integration/hubspot/test-dataplane-down-tests.js +419 -0
  33. package/integration/hubspot/test-dataplane-down.js +157 -0
  34. package/integration/hubspot/test.js +1517 -0
  35. package/integration/hubspot/variables.yaml +4 -4
  36. package/integration/hubspot/wizard-hubspot-e2e.yaml +16 -0
  37. package/integration/hubspot/wizard-hubspot-platform.yaml +8 -0
  38. package/lib/api/applications.api.js +1 -0
  39. package/lib/api/index.js +10 -5
  40. package/lib/api/types/wizard.types.js +176 -38
  41. package/lib/api/wizard.api.js +207 -38
  42. package/lib/app/deploy.js +116 -54
  43. package/lib/app/display.js +6 -5
  44. package/lib/app/dockerfile.js +2 -1
  45. package/lib/app/list.js +78 -37
  46. package/lib/app/prompts.js +9 -5
  47. package/lib/app/readme.js +41 -112
  48. package/lib/app/register.js +44 -9
  49. package/lib/app/rotate-secret.js +50 -32
  50. package/lib/cli.js +243 -65
  51. package/lib/commands/app.js +4 -9
  52. package/lib/commands/auth-config.js +125 -0
  53. package/lib/commands/auth-status.js +261 -0
  54. package/lib/commands/datasource.js +3 -6
  55. package/lib/commands/login-credentials.js +4 -4
  56. package/lib/commands/login-device.js +43 -29
  57. package/lib/commands/login.js +22 -13
  58. package/lib/commands/wizard-config-normalizer.js +92 -0
  59. package/lib/commands/wizard-core.js +515 -0
  60. package/lib/commands/wizard-dataplane.js +122 -0
  61. package/lib/commands/wizard-headless.js +115 -0
  62. package/lib/commands/wizard.js +129 -357
  63. package/lib/core/config.js +46 -0
  64. package/lib/core/secrets.js +3 -22
  65. package/lib/core/templates-env.js +1 -1
  66. package/lib/datasource/deploy.js +34 -23
  67. package/lib/datasource/list.js +8 -6
  68. package/lib/deployment/deployer.js +25 -0
  69. package/lib/deployment/environment.js +10 -13
  70. package/lib/external-system/delete.js +151 -0
  71. package/lib/external-system/deploy.js +54 -378
  72. package/lib/external-system/download-helpers.js +45 -65
  73. package/lib/external-system/download.js +34 -13
  74. package/lib/external-system/generator.js +11 -7
  75. package/lib/external-system/test-auth.js +5 -3
  76. package/lib/generator/builders.js +3 -1
  77. package/lib/generator/external-controller-manifest.js +157 -0
  78. package/lib/generator/external-schema-utils.js +236 -0
  79. package/lib/generator/external.js +55 -3
  80. package/lib/generator/index.js +22 -10
  81. package/lib/generator/wizard-prompts.js +33 -10
  82. package/lib/generator/wizard.js +69 -86
  83. package/lib/infrastructure/compose.js +100 -0
  84. package/lib/infrastructure/helpers.js +139 -0
  85. package/lib/infrastructure/index.js +52 -311
  86. package/lib/infrastructure/services.js +168 -0
  87. package/lib/schema/application-schema.json +24 -5
  88. package/lib/schema/external-datasource.schema.json +303 -17
  89. package/lib/schema/external-system.schema.json +1 -1
  90. package/lib/schema/wizard-config.schema.json +234 -0
  91. package/lib/utils/api.js +37 -42
  92. package/lib/utils/app-existence.js +42 -0
  93. package/lib/utils/app-register-config.js +7 -2
  94. package/lib/utils/app-register-display.js +2 -1
  95. package/lib/utils/auth-config-validator.js +92 -0
  96. package/lib/utils/cli-utils.js +3 -1
  97. package/lib/utils/command-header.js +43 -0
  98. package/lib/utils/compose-generator.js +113 -70
  99. package/lib/utils/controller-url.js +115 -0
  100. package/lib/utils/dataplane-health.js +115 -0
  101. package/lib/utils/dataplane-resolver.js +29 -0
  102. package/lib/utils/dev-config.js +6 -2
  103. package/lib/utils/env-copy.js +2 -1
  104. package/lib/utils/env-map.js +2 -1
  105. package/lib/utils/env-ports.js +2 -1
  106. package/lib/utils/env-template.js +1 -1
  107. package/lib/utils/error-formatter.js +149 -28
  108. package/lib/utils/external-readme.js +125 -0
  109. package/lib/utils/help-builder.js +190 -0
  110. package/lib/utils/infra-status.js +13 -3
  111. package/lib/utils/paths.js +17 -2
  112. package/lib/utils/port-resolver.js +111 -0
  113. package/lib/utils/secrets-helpers.js +3 -15
  114. package/lib/utils/secrets-utils.js +2 -2
  115. package/lib/utils/token-manager.js +69 -4
  116. package/lib/utils/variable-transformer.js +7 -2
  117. package/lib/validation/external-manifest-validator.js +202 -0
  118. package/lib/validation/validate-display.js +406 -0
  119. package/lib/validation/validate.js +159 -123
  120. package/lib/validation/validator.js +38 -4
  121. package/lib/validation/wizard-config-validator.js +267 -0
  122. package/package.json +4 -2
  123. package/templates/applications/README.md.hbs +19 -17
  124. package/templates/applications/miso-controller/env.template +1 -1
  125. package/templates/applications/miso-controller/rbac.yaml +7 -7
  126. package/templates/external-system/README.md.hbs +99 -0
  127. package/templates/external-system/external-system.json.hbs +1 -1
  128. package/templates/infra/compose.yaml.hbs +35 -0
  129. package/templates/python/docker-compose.hbs +26 -0
  130. 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 [`aifabrix-dev${devId}-postgres`, `aifabrix-dev${devId}-redis`, `aifabrix-dev${devId}-pgadmin`, `aifabrix-dev${devId}-redis-commander`];
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
  /**
@@ -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
- * @returns {Promise<{isExternal: boolean, appPath: string, appType: string}>}
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
- if (!variablesPath || !fs.existsSync(variablesPath)) {
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
- // Get containerPort or fall back to port
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
- // Construct clientId from controller, environment, and application key
353
- // (not used, but shown in error message for reference)
354
- throw new Error(`Client ID and Client Secret are required. Add credentials to ~/.aifabrix/secrets.local.yaml as '${appKey}-client-idKeyVault' and '${appKey}-client-secretKeyVault', or use credentials authentication.`);
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.port || 3000,
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.port || 3000,
364
+ port: getContainerPort(variables, 3000),
360
365
  requiresDatabase: requires.database || false,
361
366
  requiresRedis: requires.redis || false,
362
367
  requiresStorage: requires.storage || false,