@aifabrix/builder 2.31.0 → 2.32.1

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 (119) hide show
  1. package/README.md +9 -9
  2. package/integration/hubspot/README.md +2 -2
  3. package/integration/hubspot/hubspot-deploy-company.json +17 -14
  4. package/integration/hubspot/hubspot-deploy-contact.json +19 -16
  5. package/integration/hubspot/hubspot-deploy-deal.json +21 -18
  6. package/lib/api/types/datasources.types.js +31 -5
  7. package/lib/api/types/wizard.types.js +142 -0
  8. package/lib/api/wizard.api.js +177 -0
  9. package/lib/{app-config.js → app/config.js} +4 -4
  10. package/lib/{app-deploy.js → app/deploy.js} +8 -8
  11. package/lib/app/display.js +90 -0
  12. package/lib/{app-dockerfile.js → app/dockerfile.js} +4 -4
  13. package/lib/{app-down.js → app/down.js} +4 -4
  14. package/lib/app/helpers.js +218 -0
  15. package/lib/app/index.js +298 -0
  16. package/lib/{app-list.js → app/list.js} +6 -6
  17. package/lib/{app-push.js → app/push.js} +4 -4
  18. package/lib/{app-readme.js → app/readme.js} +34 -13
  19. package/lib/{app-register.js → app/register.js} +9 -9
  20. package/lib/{app-rotate-secret.js → app/rotate-secret.js} +123 -37
  21. package/lib/{app-run-helpers.js → app/run-helpers.js} +10 -10
  22. package/lib/{app-run.js → app/run.js} +6 -6
  23. package/lib/{build.js → build/index.js} +59 -32
  24. package/lib/build/package.json +7 -0
  25. package/lib/cli.js +245 -179
  26. package/lib/commands/app.js +3 -3
  27. package/lib/commands/datasource.js +4 -4
  28. package/lib/commands/login-credentials.js +209 -0
  29. package/lib/commands/login-device.js +254 -0
  30. package/lib/commands/login.js +67 -378
  31. package/lib/commands/logout.js +1 -1
  32. package/lib/commands/secrets-set.js +1 -1
  33. package/lib/commands/secure.js +2 -2
  34. package/lib/commands/wizard.js +498 -0
  35. package/lib/{audit-logger.js → core/audit-logger.js} +1 -1
  36. package/lib/{config.js → core/config.js} +28 -26
  37. package/lib/{diff.js → core/diff.js} +157 -72
  38. package/lib/{secrets.js → core/secrets.js} +86 -49
  39. package/lib/{templates.js → core/templates-env.js} +14 -222
  40. package/lib/core/templates.js +279 -0
  41. package/lib/{datasource-deploy.js → datasource/deploy.js} +6 -6
  42. package/lib/{datasource-diff.js → datasource/diff.js} +2 -2
  43. package/lib/datasource/list.js +223 -0
  44. package/lib/{datasource-validate.js → datasource/validate.js} +2 -2
  45. package/lib/{deployer.js → deployment/deployer.js} +48 -18
  46. package/lib/{environment-deploy.js → deployment/environment.js} +163 -84
  47. package/lib/{push.js → deployment/push.js} +1 -1
  48. package/lib/external-system/deploy-helpers.js +145 -0
  49. package/lib/{external-system-deploy.js → external-system/deploy.js} +156 -111
  50. package/lib/external-system/download-helpers.js +114 -0
  51. package/lib/{external-system-download.js → external-system/download.js} +92 -135
  52. package/lib/{external-system-generator.js → external-system/generator.js} +15 -11
  53. package/lib/external-system/test-auth.js +40 -0
  54. package/lib/external-system/test-execution.js +84 -0
  55. package/lib/external-system/test-helpers.js +109 -0
  56. package/lib/{external-system-test.js → external-system/test.js} +174 -192
  57. package/lib/{generator-builders.js → generator/builders.js} +87 -10
  58. package/lib/{generator-external.js → generator/external.js} +115 -52
  59. package/lib/{github-generator.js → generator/github.js} +116 -15
  60. package/lib/{generator-helpers.js → generator/helpers.js} +92 -42
  61. package/lib/{generator.js → generator/index.js} +49 -22
  62. package/lib/{generator-split.js → generator/split.js} +108 -55
  63. package/lib/generator/wizard-prompts.js +357 -0
  64. package/lib/generator/wizard.js +490 -0
  65. package/lib/{infra.js → infrastructure/index.js} +49 -22
  66. package/lib/schema/external-datasource.schema.json +145 -133
  67. package/lib/schema/external-system.schema.json +42 -0
  68. package/lib/utils/api.js +9 -5
  69. package/lib/utils/app-register-api.js +60 -32
  70. package/lib/utils/app-register-auth.js +172 -47
  71. package/lib/utils/app-register-config.js +130 -59
  72. package/lib/utils/app-run-containers.js +29 -8
  73. package/lib/utils/build-helpers.js +1 -1
  74. package/lib/utils/cli-utils.js +78 -30
  75. package/lib/utils/compose-generator.js +145 -65
  76. package/lib/utils/config-paths.js +2 -0
  77. package/lib/utils/deployment-errors.js +1 -1
  78. package/lib/utils/device-code.js +99 -41
  79. package/lib/utils/env-config-loader.js +1 -1
  80. package/lib/utils/env-copy.js +21 -18
  81. package/lib/utils/env-endpoints.js +115 -67
  82. package/lib/utils/env-map.js +13 -14
  83. package/lib/utils/env-ports.js +45 -25
  84. package/lib/utils/env-template.js +84 -42
  85. package/lib/utils/error-formatter.js +26 -9
  86. package/lib/utils/error-formatters/error-parser.js +90 -4
  87. package/lib/utils/error-formatters/http-status-errors.js +54 -17
  88. package/lib/utils/error-formatters/network-errors.js +103 -26
  89. package/lib/utils/external-system-display.js +184 -90
  90. package/lib/utils/external-system-validators.js +164 -42
  91. package/lib/utils/file-upload.js +109 -0
  92. package/lib/utils/health-check.js +199 -83
  93. package/lib/utils/infra-containers.js +1 -1
  94. package/lib/utils/infra-status.js +66 -15
  95. package/lib/utils/local-secrets.js +45 -25
  96. package/lib/utils/paths.js +45 -33
  97. package/lib/utils/schema-loader.js +42 -25
  98. package/lib/utils/schema-resolver.js +123 -74
  99. package/lib/utils/secrets-encryption.js +62 -25
  100. package/lib/utils/secrets-helpers.js +126 -63
  101. package/lib/utils/secrets-path.js +1 -1
  102. package/lib/utils/secrets-url.js +1 -1
  103. package/lib/utils/token-manager-refresh.js +181 -0
  104. package/lib/utils/token-manager.js +76 -123
  105. package/lib/utils/variable-transformer.js +154 -77
  106. package/lib/utils/yaml-preserve.js +41 -47
  107. package/lib/{template-validator.js → validation/template.js} +54 -23
  108. package/lib/{validate.js → validation/validate.js} +205 -125
  109. package/lib/{validator.js → validation/validator.js} +58 -39
  110. package/package.json +34 -3
  111. package/scripts/install-local.js +210 -0
  112. package/templates/external-system/deploy.ps1.hbs +34 -0
  113. package/templates/external-system/deploy.sh.hbs +34 -0
  114. package/templates/external-system/external-datasource.json.hbs +31 -12
  115. package/lib/app.js +0 -467
  116. package/lib/datasource-list.js +0 -141
  117. /package/lib/{app-prompts.js → app/prompts.js} +0 -0
  118. /package/lib/{env-reader.js → core/env-reader.js} +0 -0
  119. /package/lib/{key-generator.js → core/key-generator.js} +0 -0
@@ -0,0 +1,298 @@
1
+ /**
2
+ * AI Fabrix Builder Application Management
3
+ *
4
+ * This module handles application building, running, and deployment.
5
+ * Includes runtime detection, Dockerfile generation, and container management.
6
+ *
7
+ * @fileoverview Application build and run management for AI Fabrix Builder
8
+ * @author AI Fabrix Team
9
+ * @version 2.0.0
10
+ */
11
+
12
+ const fs = require('fs').promises;
13
+ const { readExistingEnv } = require('../core/env-reader');
14
+ const build = require('../build');
15
+ const appRun = require('./run');
16
+ const { promptForOptions } = require('./prompts');
17
+ const { generateConfigFiles } = require('./config');
18
+ const { pushApp } = require('./push');
19
+ const { generateDockerfileForApp } = require('./dockerfile');
20
+ const { loadTemplateVariables, updateTemplateVariables, mergeTemplateVariables } = require('../utils/template-helpers');
21
+ const { validateTemplate } = require('../validation/template');
22
+ const auditLogger = require('../core/audit-logger');
23
+ const { downApp } = require('./down');
24
+ const { getAppPath } = require('../utils/paths');
25
+ const { displaySuccessMessage } = require('./display');
26
+ const {
27
+ validateAppDirectoryNotExists,
28
+ getBaseDirForAppType,
29
+ handleGitHubWorkflows,
30
+ validateAppCreation,
31
+ processTemplateFiles,
32
+ setupAppFiles
33
+ } = require('./helpers');
34
+
35
+ /**
36
+ * Creates new application with scaffolded configuration files
37
+ * Prompts for configuration options and generates builder/ folder structure
38
+ *
39
+ * @async
40
+ * @function createApp
41
+ * @param {string} appName - Name of the application to create
42
+ * @param {Object} options - Creation options
43
+ * @param {number} [options.port] - Application port
44
+ * @param {boolean} [options.database] - Requires database
45
+ * @param {boolean} [options.redis] - Requires Redis
46
+ * @param {boolean} [options.storage] - Requires file storage
47
+ * @param {boolean} [options.authentication] - Requires authentication/RBAC
48
+ * @param {string} [options.language] - Runtime language (typescript/python)
49
+ * @param {string} [options.template] - Template to use (e.g., controller, keycloak)
50
+ * @returns {Promise<void>} Resolves when app is created
51
+ * @throws {Error} If creation fails
52
+ *
53
+ * @example
54
+ * await createApp('myapp', { port: 3000, database: true, language: 'typescript' });
55
+ * // Creates builder/ with variables.yaml, env.template, rbac.yaml
56
+ */
57
+ /**
58
+ * Validates app name and initial setup
59
+ * @function validateAppNameAndSetup
60
+ * @param {string} appName - Application name
61
+ * @param {Object} options - Options
62
+ * @returns {Object} Initial paths and type
63
+ */
64
+ function validateAppNameAndSetup(appName, options) {
65
+ if (!appName || typeof appName !== 'string') {
66
+ throw new Error('Application name is required');
67
+ }
68
+
69
+ const initialType = options.type || 'webapp';
70
+ const baseDir = getBaseDirForAppType(initialType);
71
+ const appPath = getAppPath(appName, initialType);
72
+
73
+ return { initialType, baseDir, appPath };
74
+ }
75
+
76
+ /**
77
+ * Handles template validation and loading
78
+ * @async
79
+ * @function handleTemplateSetup
80
+ * @param {Object} options - Options
81
+ * @returns {Promise<Object>} Merged options with template variables
82
+ */
83
+ async function handleTemplateSetup(options) {
84
+ if (options.template) {
85
+ await validateTemplate(options.template);
86
+ }
87
+ const templateVariables = await loadTemplateVariables(options.template);
88
+ return mergeTemplateVariables(options, templateVariables);
89
+ }
90
+
91
+ /**
92
+ * Validates and prepares final app path
93
+ * @async
94
+ * @function prepareFinalAppPath
95
+ * @param {string} appName - Application name
96
+ * @param {Object} config - Configuration
97
+ * @param {string} initialAppPath - Initial app path
98
+ * @returns {Promise<string>} Final app path
99
+ */
100
+ async function prepareFinalAppPath(appName, config, initialAppPath) {
101
+ const finalBaseDir = getBaseDirForAppType(config.type);
102
+ const finalAppPath = getAppPath(appName, config.type);
103
+
104
+ // If path changed, validate the new path
105
+ if (finalAppPath !== initialAppPath) {
106
+ await validateAppDirectoryNotExists(finalAppPath, appName, finalBaseDir);
107
+ }
108
+
109
+ return finalAppPath;
110
+ }
111
+
112
+ /**
113
+ * Generates all application files
114
+ * @async
115
+ * @function generateApplicationFiles
116
+ * @param {string} finalAppPath - Final app path
117
+ * @param {string} appName - Application name
118
+ * @param {Object} config - Configuration
119
+ * @param {Object} options - Options
120
+ * @returns {Promise<string>} Environment conversion message
121
+ */
122
+ async function generateApplicationFiles(finalAppPath, appName, config, options) {
123
+ await fs.mkdir(finalAppPath, { recursive: true });
124
+ await processTemplateFiles(options.template, finalAppPath, appName, options, config);
125
+
126
+ const existingEnv = await readExistingEnv(process.cwd());
127
+ const envConversionMessage = existingEnv
128
+ ? '\n✓ Found existing .env file - sensitive values will be converted to kv:// references'
129
+ : '';
130
+
131
+ await generateConfigFiles(finalAppPath, appName, config, existingEnv);
132
+
133
+ // Generate external system files if type is external
134
+ if (config.type === 'external') {
135
+ const externalGenerator = require('../external-system/generator');
136
+ await externalGenerator.generateExternalSystemFiles(finalAppPath, appName, config);
137
+ }
138
+
139
+ if (options.app) {
140
+ await setupAppFiles(appName, finalAppPath, config, options);
141
+ }
142
+
143
+ await handleGitHubWorkflows(options, config);
144
+ return envConversionMessage;
145
+ }
146
+
147
+ /**
148
+ * Logs application creation for audit
149
+ * @async
150
+ * @function logApplicationCreation
151
+ * @param {string} appName - Application name
152
+ * @param {Object} config - Configuration
153
+ * @param {Object} options - Options
154
+ */
155
+ async function logApplicationCreation(appName, config, options) {
156
+ await auditLogger.logApplicationCreation(appName, {
157
+ language: config.language,
158
+ port: config.port,
159
+ database: config.database,
160
+ redis: config.redis,
161
+ storage: config.storage,
162
+ authentication: config.authentication,
163
+ template: options.template,
164
+ api: null // Local operation, no API involved
165
+ });
166
+ }
167
+
168
+ async function createApp(appName, options = {}) {
169
+ try {
170
+ const { appPath } = validateAppNameAndSetup(appName, options);
171
+ await validateAppCreation(appName, options, appPath, getBaseDirForAppType(options.type || 'webapp'));
172
+
173
+ const mergedOptions = await handleTemplateSetup(options);
174
+ const config = await promptForOptions(appName, mergedOptions);
175
+
176
+ const finalAppPath = await prepareFinalAppPath(appName, config, appPath);
177
+ const envConversionMessage = await generateApplicationFiles(finalAppPath, appName, config, options);
178
+
179
+ displaySuccessMessage(appName, config, envConversionMessage, options.app, finalAppPath);
180
+ await logApplicationCreation(appName, config, options);
181
+ } catch (error) {
182
+ throw new Error(`Failed to create application: ${error.message}`);
183
+ }
184
+ }
185
+
186
+ /**
187
+ * Builds a container image for the specified application
188
+ * Auto-detects runtime and generates Dockerfile if needed
189
+ *
190
+ * @async
191
+ * @function buildApp
192
+ * @param {string} appName - Name of the application to build
193
+ * @param {Object} options - Build options
194
+ * @param {string} [options.language] - Override language detection
195
+ * @param {boolean} [options.forceTemplate] - Force rebuild from template
196
+ * @param {string} [options.tag] - Image tag (default: latest)
197
+ * @returns {Promise<string>} Image tag that was built
198
+ * @throws {Error} If build fails or app configuration is invalid
199
+ *
200
+ * @example
201
+ * const imageTag = await buildApp('myapp', { language: 'typescript' });
202
+ * // Returns: 'myapp:latest'
203
+ */
204
+ async function buildApp(appName, options = {}) {
205
+ return build.buildApp(appName, options);
206
+ }
207
+
208
+ /**
209
+ * Detects the runtime language of an application
210
+ * Analyzes project files to determine TypeScript, Python, etc.
211
+ *
212
+ * @function detectLanguage
213
+ * @param {string} appPath - Path to application directory
214
+ * @returns {string} Detected language ('typescript', 'python', etc.)
215
+ * @throws {Error} If language cannot be detected
216
+ *
217
+ * @example
218
+ * const language = detectLanguage('./myapp');
219
+ * // Returns: 'typescript'
220
+ */
221
+ function detectLanguage(appPath) {
222
+ return build.detectLanguage(appPath);
223
+ }
224
+
225
+ /**
226
+ * Generates a Dockerfile from template based on detected language
227
+ * Uses Handlebars templates to create optimized Dockerfiles
228
+ *
229
+ * @async
230
+ * @function generateDockerfile
231
+ * @param {string} appPath - Path to application directory
232
+ * @param {string} language - Target language ('typescript', 'python')
233
+ * @param {Object} config - Application configuration from variables.yaml
234
+ * @returns {Promise<string>} Path to generated Dockerfile
235
+ * @throws {Error} If template generation fails
236
+ *
237
+ * @example
238
+ * const dockerfilePath = await generateDockerfile('./myapp', 'typescript', config);
239
+ * // Returns: './myapp/.aifabrix/Dockerfile.typescript'
240
+ */
241
+ async function generateDockerfile(appPath, language, config) {
242
+ return build.generateDockerfile(appPath, language, config);
243
+ }
244
+
245
+ /**
246
+ * Runs the application locally using Docker
247
+ * Starts container with proper port mapping and environment
248
+ *
249
+ * @async
250
+ * @function runApp
251
+ * @param {string} appName - Name of the application to run
252
+ * @param {Object} options - Run options
253
+ * @param {number} [options.port] - Override local port
254
+ * @param {boolean} [options.debug] - Enable debug output
255
+ * @returns {Promise<void>} Resolves when app is running
256
+ * @throws {Error} If run fails or app is not built
257
+ *
258
+ * @example
259
+ * await runApp('myapp', { port: 3001 });
260
+ * // Application is now running on localhost:3001
261
+ */
262
+ async function runApp(appName, options = {}) {
263
+ return appRun.runApp(appName, options);
264
+ }
265
+
266
+ /**
267
+ * Deploys application to controller
268
+ * @async
269
+ * @function deployApp
270
+ * @param {string} appName - Name of the application
271
+ * @param {Object} options - Deployment options
272
+ * @returns {Promise<void>} Resolves when deployment is complete
273
+ */
274
+ async function deployApp(appName, options = {}) {
275
+ const appDeploy = require('./deploy');
276
+ return appDeploy.deployApp(appName, options);
277
+ }
278
+
279
+ module.exports = {
280
+ createApp,
281
+ buildApp,
282
+ runApp,
283
+ downApp,
284
+ detectLanguage,
285
+ generateDockerfile,
286
+ generateDockerfileForApp,
287
+ pushApp,
288
+ deployApp,
289
+ loadTemplateVariables,
290
+ updateTemplateVariables,
291
+ mergeTemplateVariables,
292
+ checkImageExists: appRun.checkImageExists,
293
+ checkContainerRunning: appRun.checkContainerRunning,
294
+ stopAndRemoveContainer: appRun.stopAndRemoveContainer,
295
+ checkPortAvailable: appRun.checkPortAvailable,
296
+ generateDockerCompose: appRun.generateDockerCompose,
297
+ waitForHealthCheck: appRun.waitForHealthCheck
298
+ };
@@ -9,12 +9,12 @@
9
9
  */
10
10
 
11
11
  const chalk = require('chalk');
12
- const { getConfig, normalizeControllerUrl } = require('./config');
13
- const { getOrRefreshDeviceToken } = require('./utils/token-manager');
14
- const { listEnvironmentApplications } = require('./api/environments.api');
15
- const { formatApiError } = require('./utils/api-error-handler');
16
- const { formatAuthenticationError } = require('./utils/error-formatters/http-status-errors');
17
- const logger = require('./utils/logger');
12
+ const { getConfig, normalizeControllerUrl } = require('../core/config');
13
+ const { getOrRefreshDeviceToken } = require('../utils/token-manager');
14
+ const { listEnvironmentApplications } = require('../api/environments.api');
15
+ const { formatApiError } = require('../utils/api-error-handler');
16
+ const { formatAuthenticationError } = require('../utils/error-formatters/http-status-errors');
17
+ const logger = require('../utils/logger');
18
18
 
19
19
  /**
20
20
  * Extract wrapped array format: { success: true, data: { success: true, data: [...] } }
@@ -12,8 +12,8 @@ const fs = require('fs').promises;
12
12
  const path = require('path');
13
13
  const chalk = require('chalk');
14
14
  const yaml = require('js-yaml');
15
- const pushUtils = require('./push');
16
- const logger = require('./utils/logger');
15
+ const pushUtils = require('../deployment/push');
16
+ const logger = require('../utils/logger');
17
17
 
18
18
  /**
19
19
  * Validate application name format
@@ -70,7 +70,7 @@ function extractImageName(config, appName) {
70
70
  */
71
71
  async function loadPushConfig(appName, options) {
72
72
  // Detect app type and get correct path (integration or builder)
73
- const { detectAppType } = require('./utils/paths');
73
+ const { detectAppType } = require('../utils/paths');
74
74
  const { appPath } = await detectAppType(appName);
75
75
  const configPath = path.join(appPath, 'variables.yaml');
76
76
  try {
@@ -183,7 +183,7 @@ function displayPushResults(registry, imageName, tags) {
183
183
  */
184
184
  async function pushApp(appName, options = {}) {
185
185
  // Check if app type is external - skip push
186
- const { detectAppType } = require('./utils/paths');
186
+ const { detectAppType } = require('../utils/paths');
187
187
  try {
188
188
  const { isExternal } = await detectAppType(appName);
189
189
  if (isExternal) {
@@ -48,7 +48,7 @@ function formatAppDisplayName(appName) {
48
48
  */
49
49
  function _loadReadmeTemplate() {
50
50
  // Use getProjectRoot to reliably find templates in all environments
51
- const { getProjectRoot } = require('./utils/paths');
51
+ const { getProjectRoot } = require('../utils/paths');
52
52
  const projectRoot = getProjectRoot();
53
53
  const templatePath = path.join(projectRoot, 'templates', 'applications', 'README.md.hbs');
54
54
 
@@ -71,32 +71,51 @@ function _loadReadmeTemplate() {
71
71
  * @param {Object} config - Application configuration
72
72
  * @returns {string} README.md content
73
73
  */
74
- function generateReadmeMd(appName, config) {
74
+ /**
75
+ * Extracts service flags from config
76
+ * @function extractServiceFlags
77
+ * @param {Object} config - Application configuration
78
+ * @returns {Object} Service flags object
79
+ */
80
+ function extractServiceFlags(config) {
81
+ return {
82
+ hasDatabase: config.database || config.requires?.database || false,
83
+ hasRedis: config.redis || config.requires?.redis || false,
84
+ hasStorage: config.storage || config.requires?.storage || false,
85
+ hasAuthentication: config.authentication || false
86
+ };
87
+ }
88
+
89
+ /**
90
+ * Builds template context for README generation
91
+ * @function buildReadmeContext
92
+ * @param {string} appName - Application name
93
+ * @param {Object} config - Application configuration
94
+ * @returns {Object} Template context
95
+ */
96
+ function buildReadmeContext(appName, config) {
75
97
  const displayName = formatAppDisplayName(appName);
76
98
  const imageName = `aifabrix/${appName}`;
77
99
  const port = config.port || 3000;
78
100
  // Extract registry from nested structure (config.image.registry) or flattened (config.registry)
79
101
  const registry = config.image?.registry || config.registry || 'myacr.azurecr.io';
80
102
 
81
- const hasDatabase = config.database || config.requires?.database || false;
82
- const hasRedis = config.redis || config.requires?.redis || false;
83
- const hasStorage = config.storage || config.requires?.storage || false;
84
- const hasAuthentication = config.authentication || false;
85
- const hasAnyService = hasDatabase || hasRedis || hasStorage || hasAuthentication;
103
+ const serviceFlags = extractServiceFlags(config);
104
+ const hasAnyService = serviceFlags.hasDatabase || serviceFlags.hasRedis || serviceFlags.hasStorage || serviceFlags.hasAuthentication;
86
105
 
87
- const context = {
106
+ return {
88
107
  appName,
89
108
  displayName,
90
109
  imageName,
91
110
  port,
92
111
  registry,
93
- hasDatabase,
94
- hasRedis,
95
- hasStorage,
96
- hasAuthentication,
112
+ ...serviceFlags,
97
113
  hasAnyService
98
114
  };
115
+ }
99
116
 
117
+ function generateReadmeMd(appName, config) {
118
+ const context = buildReadmeContext(appName, config);
100
119
  // Always generate comprehensive README programmatically to ensure consistency
101
120
  // regardless of template file content
102
121
  return generateComprehensiveReadme(context);
@@ -220,10 +239,12 @@ For more information, see the [AI Fabrix Builder documentation](https://docs.aif
220
239
  * @throws {Error} If file generation fails
221
240
  */
222
241
  async function generateReadmeMdFile(appPath, appName, config) {
242
+ // Ensure directory exists
243
+ await fs.mkdir(appPath, { recursive: true });
223
244
  const readmePath = path.join(appPath, 'README.md');
224
245
  if (!(await fileExists(readmePath))) {
225
246
  const readmeContent = generateReadmeMd(appName, config);
226
- await fs.writeFile(readmePath, readmeContent);
247
+ await fs.writeFile(readmePath, readmeContent, 'utf8');
227
248
  }
228
249
  }
229
250
 
@@ -9,19 +9,19 @@
9
9
  */
10
10
 
11
11
  const chalk = require('chalk');
12
- const logger = require('./utils/logger');
13
- const { saveLocalSecret, isLocalhost } = require('./utils/local-secrets');
14
- const { updateEnvTemplate } = require('./utils/env-template');
15
- const { generateEnvFile } = require('./secrets');
16
- const { registerApplicationSchema, validateAppRegistrationData } = require('./utils/app-register-validator');
12
+ const logger = require('../utils/logger');
13
+ const { saveLocalSecret, isLocalhost } = require('../utils/local-secrets');
14
+ const { updateEnvTemplate } = require('../utils/env-template');
15
+ const { generateEnvFile } = require('../core/secrets');
16
+ const { registerApplicationSchema, validateAppRegistrationData } = require('../utils/app-register-validator');
17
17
  const {
18
18
  loadVariablesYaml,
19
19
  createMinimalAppIfNeeded,
20
20
  extractAppConfiguration
21
- } = require('./utils/app-register-config');
22
- const { checkAuthentication } = require('./utils/app-register-auth');
23
- const { callRegisterApi } = require('./utils/app-register-api');
24
- const { displayRegistrationResults, getEnvironmentPrefix } = require('./utils/app-register-display');
21
+ } = require('../utils/app-register-config');
22
+ const { checkAuthentication } = require('../utils/app-register-auth');
23
+ const { callRegisterApi } = require('../utils/app-register-api');
24
+ const { displayRegistrationResults, getEnvironmentPrefix } = require('../utils/app-register-display');
25
25
 
26
26
  /**
27
27
  * Build registration data payload from app configuration
@@ -9,16 +9,16 @@
9
9
  */
10
10
 
11
11
  const chalk = require('chalk');
12
- const { getConfig, normalizeControllerUrl } = require('./config');
13
- const { getOrRefreshDeviceToken } = require('./utils/token-manager');
14
- const { rotateApplicationSecret } = require('./api/applications.api');
15
- const { formatApiError } = require('./utils/api-error-handler');
16
- const { formatAuthenticationError } = require('./utils/error-formatters/http-status-errors');
17
- const logger = require('./utils/logger');
18
- const { saveLocalSecret, isLocalhost } = require('./utils/local-secrets');
19
- const { updateEnvTemplate } = require('./utils/env-template');
20
- const { getEnvironmentPrefix } = require('./app-register');
21
- const { generateEnvFile } = require('./secrets');
12
+ const { getConfig, normalizeControllerUrl } = require('../core/config');
13
+ const { getOrRefreshDeviceToken } = require('../utils/token-manager');
14
+ const { rotateApplicationSecret } = require('../api/applications.api');
15
+ const { formatApiError } = require('../utils/api-error-handler');
16
+ const { formatAuthenticationError } = require('../utils/error-formatters/http-status-errors');
17
+ const logger = require('../utils/logger');
18
+ const { saveLocalSecret, isLocalhost } = require('../utils/local-secrets');
19
+ const { updateEnvTemplate } = require('../utils/env-template');
20
+ const { getEnvironmentPrefix } = require('./register');
21
+ const { generateEnvFile } = require('../core/secrets');
22
22
 
23
23
  /**
24
24
  * Find device token from config by trying each stored URL
@@ -61,20 +61,83 @@ function validateEnvironment(environment) {
61
61
  }
62
62
  }
63
63
 
64
+ /**
65
+ * Validate credentials object structure
66
+ * @param {Object} credentials - Credentials object to validate
67
+ * @returns {boolean} True if credentials are valid
68
+ */
69
+ function isValidCredentials(credentials) {
70
+ return credentials &&
71
+ typeof credentials === 'object' &&
72
+ typeof credentials.clientId === 'string' &&
73
+ typeof credentials.clientSecret === 'string';
74
+ }
75
+
76
+ /**
77
+ * Extract credentials from API response
78
+ * Handles multiple response formats:
79
+ * 1. Direct format: { success: true, data: { credentials: {...} } }
80
+ * 2. Wrapped format: { success: true, data: { success: true, data: { credentials: {...} } } }
81
+ * @param {Object} response - API response from centralized API client
82
+ * @returns {Object|null} Object with credentials and message, or null if not found
83
+ */
84
+ function extractCredentials(response) {
85
+ // Note: response.data is already validated in validateResponse
86
+ const apiResponse = response.data;
87
+
88
+ // Try wrapped format first: response.data.data.credentials
89
+ if (apiResponse.data && apiResponse.data.credentials) {
90
+ const credentials = apiResponse.data.credentials;
91
+ if (isValidCredentials(credentials)) {
92
+ return {
93
+ credentials: credentials,
94
+ message: apiResponse.data.message || apiResponse.message
95
+ };
96
+ }
97
+ }
98
+
99
+ // Try direct format: response.data.credentials
100
+ if (apiResponse.credentials) {
101
+ const credentials = apiResponse.credentials;
102
+ if (isValidCredentials(credentials)) {
103
+ return {
104
+ credentials: credentials,
105
+ message: apiResponse.message
106
+ };
107
+ }
108
+ }
109
+
110
+ return null;
111
+ }
112
+
64
113
  /**
65
114
  * Validate API response structure
66
115
  * @param {Object} response - API response
116
+ * @returns {Object} Object with credentials and message
67
117
  * @throws {Error} If response structure is invalid
68
118
  */
69
119
  function validateResponse(response) {
70
120
  if (!response.data || typeof response.data !== 'object') {
121
+ logger.error(chalk.red('❌ Invalid response: missing data'));
122
+ logger.error(chalk.gray('\nAPI response type:'), typeof response.data);
123
+ logger.error(chalk.gray('API response:'), JSON.stringify(response.data, null, 2));
124
+ logger.error(chalk.gray('\nFull response for debugging:'));
125
+ logger.error(chalk.gray(JSON.stringify(response, null, 2)));
71
126
  throw new Error('Invalid response: missing data');
72
127
  }
73
128
 
74
- const credentials = response.data.credentials;
75
- if (!credentials || typeof credentials !== 'object' || typeof credentials.clientId !== 'string' || typeof credentials.clientSecret !== 'string') {
129
+ const result = extractCredentials(response);
130
+
131
+ if (!result) {
132
+ logger.error(chalk.red('❌ Invalid response: missing or invalid credentials'));
133
+ logger.error(chalk.gray('\nAPI response type:'), typeof response.data);
134
+ logger.error(chalk.gray('API response:'), JSON.stringify(response.data, null, 2));
135
+ logger.error(chalk.gray('\nFull response for debugging:'));
136
+ logger.error(chalk.gray(JSON.stringify(response, null, 2)));
76
137
  throw new Error('Invalid response: missing or invalid credentials');
77
138
  }
139
+
140
+ return result;
78
141
  }
79
142
 
80
143
  /**
@@ -108,6 +171,47 @@ function displayRotationResults(appKey, environment, credentials, apiUrl, messag
108
171
  }
109
172
  }
110
173
 
174
+ /**
175
+ * Get device token from provided controller URL
176
+ * @async
177
+ * @param {string} controllerUrl - Controller URL
178
+ * @returns {Promise<Object|null>} Object with token and controllerUrl, or null if failed
179
+ */
180
+ async function getTokenFromUrl(controllerUrl) {
181
+ try {
182
+ const normalizedUrl = normalizeControllerUrl(controllerUrl);
183
+ const deviceToken = await getOrRefreshDeviceToken(normalizedUrl);
184
+ if (deviceToken && deviceToken.token) {
185
+ return {
186
+ token: deviceToken.token,
187
+ controllerUrl: deviceToken.controller || normalizedUrl
188
+ };
189
+ }
190
+ } catch (error) {
191
+ logger.error(chalk.red(`❌ Failed to authenticate with controller: ${controllerUrl}`));
192
+ logger.error(chalk.gray(`Error: ${error.message}`));
193
+ process.exit(1);
194
+ }
195
+ return null;
196
+ }
197
+
198
+ /**
199
+ * Validate and handle missing authentication token
200
+ * @param {string|null} token - Authentication token
201
+ * @param {string|null} controllerUrl - Controller URL
202
+ * @param {string} [providedUrl] - Original provided URL for error context
203
+ */
204
+ function validateAuthToken(token, controllerUrl, providedUrl) {
205
+ if (!token || !controllerUrl) {
206
+ const formattedError = formatAuthenticationError({
207
+ controllerUrl: providedUrl || undefined,
208
+ message: 'No valid authentication found'
209
+ });
210
+ logger.error(formattedError);
211
+ process.exit(1);
212
+ }
213
+ }
214
+
111
215
  /**
112
216
  * Get authentication token for rotation
113
217
  * @async
@@ -122,17 +226,10 @@ async function getRotationAuthToken(controllerUrl, config) {
122
226
 
123
227
  // If controller URL provided, try to get device token
124
228
  if (controllerUrl) {
125
- try {
126
- const normalizedUrl = normalizeControllerUrl(controllerUrl);
127
- const deviceToken = await getOrRefreshDeviceToken(normalizedUrl);
128
- if (deviceToken && deviceToken.token) {
129
- token = deviceToken.token;
130
- actualControllerUrl = deviceToken.controller || normalizedUrl;
131
- }
132
- } catch (error) {
133
- logger.error(chalk.red(`❌ Failed to authenticate with controller: ${controllerUrl}`));
134
- logger.error(chalk.gray(`Error: ${error.message}`));
135
- process.exit(1);
229
+ const tokenResult = await getTokenFromUrl(controllerUrl);
230
+ if (tokenResult) {
231
+ token = tokenResult.token;
232
+ actualControllerUrl = tokenResult.controllerUrl;
136
233
  }
137
234
  }
138
235
 
@@ -145,15 +242,7 @@ async function getRotationAuthToken(controllerUrl, config) {
145
242
  }
146
243
  }
147
244
 
148
- if (!token || !actualControllerUrl) {
149
- const formattedError = formatAuthenticationError({
150
- controllerUrl: controllerUrl || undefined,
151
- message: 'No valid authentication found'
152
- });
153
- logger.error(formattedError);
154
- process.exit(1);
155
- }
156
-
245
+ validateAuthToken(token, actualControllerUrl, controllerUrl);
157
246
  return { token, actualControllerUrl };
158
247
  }
159
248
 
@@ -228,11 +317,8 @@ async function rotateSecret(appKey, options) {
228
317
  process.exit(1);
229
318
  }
230
319
 
231
- // Validate response structure
232
- validateResponse(response);
233
-
234
- const credentials = response.data.credentials;
235
- const message = response.data.message;
320
+ // Validate response structure and extract credentials
321
+ const { credentials, message } = validateResponse(response);
236
322
 
237
323
  // Save credentials locally
238
324
  await saveCredentialsLocally(appKey, credentials, actualControllerUrl);