@aifabrix/builder 2.40.2 → 2.42.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 (198) hide show
  1. package/.cursor/rules/docs-rules.mdc +30 -0
  2. package/README.md +7 -5
  3. package/integration/hubspot/README.md +8 -4
  4. package/integration/hubspot/application.json +54 -0
  5. package/integration/hubspot/create-hubspot.js +9 -136
  6. package/integration/hubspot/env.template +3 -4
  7. package/integration/hubspot/hubspot-datasource-company.json +343 -5
  8. package/integration/hubspot/hubspot-datasource-contact.json +413 -5
  9. package/integration/hubspot/hubspot-datasource-deal.json +341 -4
  10. package/integration/hubspot/hubspot-datasource-users.json +116 -0
  11. package/integration/hubspot/hubspot-deploy.json +1250 -108
  12. package/integration/hubspot/hubspot-system.json +15 -32
  13. package/integration/hubspot/test-dataplane-down-tests.js +17 -16
  14. package/integration/hubspot/test-dataplane-down.js +2 -2
  15. package/integration/hubspot/test.js +1 -1
  16. package/jest.config.manual.js +2 -1
  17. package/lib/api/credential.api.js +40 -0
  18. package/lib/api/dev.api.js +423 -0
  19. package/lib/api/external-test.api.js +111 -0
  20. package/lib/api/index.js +42 -19
  21. package/lib/api/pipeline.api.js +66 -120
  22. package/lib/api/types/credential.types.js +23 -0
  23. package/lib/api/types/dev.types.js +140 -0
  24. package/lib/api/types/pipeline.types.js +37 -0
  25. package/lib/api/wizard-platform.api.js +61 -0
  26. package/lib/api/wizard.api.js +34 -1
  27. package/lib/app/config.js +44 -11
  28. package/lib/app/down.js +2 -1
  29. package/lib/app/index.js +12 -1
  30. package/lib/app/prompts.js +44 -29
  31. package/lib/app/push.js +36 -12
  32. package/lib/app/readme.js +9 -6
  33. package/lib/app/run-env-compose.js +264 -0
  34. package/lib/app/run-helpers.js +121 -118
  35. package/lib/app/run.js +148 -28
  36. package/lib/app/show-display.js +1 -1
  37. package/lib/app/show.js +5 -2
  38. package/lib/build/index.js +11 -3
  39. package/lib/cli/setup-app.js +172 -15
  40. package/lib/cli/setup-credential-deployment.js +31 -6
  41. package/lib/cli/setup-dev.js +206 -16
  42. package/lib/cli/setup-environment.js +16 -6
  43. package/lib/cli/setup-external-system.js +89 -24
  44. package/lib/cli/setup-infra.js +82 -15
  45. package/lib/cli/setup-secrets.js +52 -5
  46. package/lib/cli/setup-utility.js +129 -24
  47. package/lib/commands/app-install.js +172 -0
  48. package/lib/commands/app-shell.js +75 -0
  49. package/lib/commands/app-test.js +282 -0
  50. package/lib/commands/app.js +1 -1
  51. package/lib/commands/credential-env.js +162 -0
  52. package/lib/commands/credential-list.js +17 -22
  53. package/lib/commands/credential-push.js +96 -0
  54. package/lib/commands/datasource.js +77 -6
  55. package/lib/commands/dev-cli-handlers.js +141 -0
  56. package/lib/commands/dev-down.js +114 -0
  57. package/lib/commands/dev-init.js +347 -0
  58. package/lib/commands/repair-auth-config.js +99 -0
  59. package/lib/commands/repair-datasource-keys.js +208 -0
  60. package/lib/commands/repair-datasource.js +235 -0
  61. package/lib/commands/repair-env-template.js +348 -0
  62. package/lib/commands/repair-internal.js +85 -0
  63. package/lib/commands/repair-rbac.js +158 -0
  64. package/lib/commands/repair.js +507 -0
  65. package/lib/commands/secrets-list.js +118 -0
  66. package/lib/commands/secrets-remove.js +97 -0
  67. package/lib/commands/secrets-set.js +30 -17
  68. package/lib/commands/secrets-validate.js +50 -0
  69. package/lib/commands/test-e2e-external.js +165 -0
  70. package/lib/commands/up-dataplane.js +2 -2
  71. package/lib/commands/up-miso.js +0 -25
  72. package/lib/commands/upload.js +96 -40
  73. package/lib/commands/wizard-core-helpers.js +226 -4
  74. package/lib/commands/wizard-core.js +67 -29
  75. package/lib/commands/wizard-dataplane.js +1 -1
  76. package/lib/commands/wizard-entity-selection.js +43 -0
  77. package/lib/commands/wizard-headless.js +44 -5
  78. package/lib/commands/wizard-helpers.js +7 -3
  79. package/lib/commands/wizard.js +86 -64
  80. package/lib/core/admin-secrets.js +96 -0
  81. package/lib/core/config.js +7 -1
  82. package/lib/core/secrets-ensure.js +378 -0
  83. package/lib/core/secrets-env-write.js +157 -0
  84. package/lib/core/secrets.js +176 -89
  85. package/lib/datasource/deploy.js +12 -3
  86. package/lib/datasource/field-reference-validator.js +91 -0
  87. package/lib/datasource/test-e2e.js +219 -0
  88. package/lib/datasource/test-integration.js +154 -0
  89. package/lib/datasource/validate.js +21 -3
  90. package/lib/deployment/deployer.js +7 -5
  91. package/lib/deployment/environment-config.js +137 -0
  92. package/lib/deployment/environment.js +21 -98
  93. package/lib/deployment/push.js +32 -2
  94. package/lib/external-system/download.js +188 -203
  95. package/lib/external-system/generator.js +204 -56
  96. package/lib/external-system/test-auth.js +7 -3
  97. package/lib/external-system/test-execution.js +2 -1
  98. package/lib/external-system/test-system-level.js +73 -0
  99. package/lib/external-system/test.js +56 -19
  100. package/lib/generator/external-controller-manifest.js +29 -2
  101. package/lib/generator/external-schema-utils.js +1 -1
  102. package/lib/generator/external.js +10 -3
  103. package/lib/generator/index.js +177 -25
  104. package/lib/generator/split-readme.js +1 -0
  105. package/lib/generator/split-variables.js +7 -1
  106. package/lib/generator/split.js +194 -54
  107. package/lib/generator/wizard-prompts-secondary.js +294 -0
  108. package/lib/generator/wizard-prompts.js +105 -106
  109. package/lib/generator/wizard-readme.js +88 -0
  110. package/lib/generator/wizard.js +155 -158
  111. package/lib/infrastructure/compose.js +11 -1
  112. package/lib/infrastructure/helpers.js +103 -20
  113. package/lib/infrastructure/index.js +98 -12
  114. package/lib/infrastructure/services.js +88 -22
  115. package/lib/schema/application-schema.json +32 -8
  116. package/lib/schema/external-datasource.schema.json +49 -26
  117. package/lib/schema/external-system.schema.json +509 -411
  118. package/lib/schema/wizard-config.schema.json +16 -0
  119. package/lib/utils/api.js +41 -13
  120. package/lib/utils/app-register-auth.js +25 -3
  121. package/lib/utils/auth-headers.js +8 -7
  122. package/lib/utils/cli-utils.js +20 -0
  123. package/lib/utils/compose-generator.js +77 -76
  124. package/lib/utils/compose-handlebars-helpers.js +54 -0
  125. package/lib/utils/compose-vector-helper.js +18 -0
  126. package/lib/utils/config-format-preference.js +51 -0
  127. package/lib/utils/config-format.js +36 -0
  128. package/lib/utils/config-paths.js +127 -2
  129. package/lib/utils/configuration-env-resolver.js +179 -0
  130. package/lib/utils/credential-display.js +83 -0
  131. package/lib/utils/credential-secrets-env.js +357 -0
  132. package/lib/utils/dataplane-pipeline-warning.js +28 -0
  133. package/lib/utils/deployment-validation-helpers.js +4 -4
  134. package/lib/utils/dev-ca-install.js +139 -0
  135. package/lib/utils/dev-cert-helper.js +122 -0
  136. package/lib/utils/device-code-helpers.js +224 -0
  137. package/lib/utils/device-code.js +37 -336
  138. package/lib/utils/docker-build.js +40 -8
  139. package/lib/utils/env-copy.js +103 -13
  140. package/lib/utils/env-map.js +35 -5
  141. package/lib/utils/env-template.js +6 -5
  142. package/lib/utils/error-formatters/http-status-errors.js +20 -2
  143. package/lib/utils/error-formatters/permission-errors.js +0 -1
  144. package/lib/utils/error-formatters/validation-errors.js +0 -1
  145. package/lib/utils/external-readme.js +56 -29
  146. package/lib/utils/external-system-display.js +59 -1
  147. package/lib/utils/external-system-test-helpers.js +21 -8
  148. package/lib/utils/external-system-validators.js +3 -0
  149. package/lib/utils/file-upload.js +20 -50
  150. package/lib/utils/help-builder.js +16 -2
  151. package/lib/utils/infra-status.js +80 -45
  152. package/lib/utils/local-secrets.js +7 -52
  153. package/lib/utils/mutagen-install.js +195 -0
  154. package/lib/utils/mutagen.js +146 -0
  155. package/lib/utils/paths.js +128 -37
  156. package/lib/utils/port-resolver.js +28 -16
  157. package/lib/utils/remote-dev-auth.js +38 -0
  158. package/lib/utils/remote-docker-env.js +43 -0
  159. package/lib/utils/remote-secrets-loader.js +60 -0
  160. package/lib/utils/secrets-canonical.js +93 -0
  161. package/lib/utils/secrets-generator.js +114 -6
  162. package/lib/utils/secrets-helpers.js +108 -114
  163. package/lib/utils/secrets-path.js +2 -2
  164. package/lib/utils/secrets-utils.js +52 -1
  165. package/lib/utils/secrets-validation.js +84 -0
  166. package/lib/utils/ssh-key-helper.js +116 -0
  167. package/lib/utils/test-log-writer.js +56 -0
  168. package/lib/utils/token-manager-messages.js +90 -0
  169. package/lib/utils/token-manager.js +29 -36
  170. package/lib/utils/variable-transformer.js +3 -3
  171. package/lib/validation/env-template-auth.js +157 -0
  172. package/lib/validation/env-template-kv.js +41 -0
  173. package/lib/validation/external-manifest-validator.js +25 -0
  174. package/lib/validation/external-system-auth-rules.js +86 -0
  175. package/lib/validation/validate-batch.js +149 -0
  176. package/lib/validation/validate-datasource-keys-api.js +33 -0
  177. package/lib/validation/validate-display.js +94 -16
  178. package/lib/validation/validate.js +25 -12
  179. package/lib/validation/validator.js +72 -9
  180. package/lib/validation/wizard-datasource-validation.js +50 -0
  181. package/package.json +8 -3
  182. package/scripts/install-local.js +34 -15
  183. package/templates/README.md +0 -1
  184. package/templates/applications/README.md.hbs +4 -4
  185. package/templates/applications/dataplane/application.yaml +6 -5
  186. package/templates/applications/dataplane/env.template +15 -10
  187. package/templates/applications/dataplane/rbac.yaml +2 -2
  188. package/templates/applications/keycloak/env.template +2 -0
  189. package/templates/applications/miso-controller/application.yaml +1 -0
  190. package/templates/applications/miso-controller/env.template +12 -10
  191. package/templates/external-system/README.md.hbs +65 -25
  192. package/templates/external-system/deploy.js.hbs +4 -2
  193. package/templates/external-system/external-datasource.yaml.hbs +217 -0
  194. package/templates/external-system/external-system.json.hbs +1 -18
  195. package/templates/infra/compose.yaml.hbs +6 -0
  196. package/templates/python/docker-compose.hbs +49 -23
  197. package/templates/typescript/docker-compose.hbs +48 -22
  198. package/integration/hubspot/application.yaml +0 -37
@@ -12,7 +12,8 @@
12
12
  const fs = require('fs').promises;
13
13
  const path = require('path');
14
14
  const { testDatasourceViaPipeline } = require('../api/pipeline.api');
15
- const { requireBearerForDataplanePipeline } = require('./token-manager');
15
+
16
+ /** Pipeline test endpoints accept client credentials; do not enforce Bearer-only */
16
17
 
17
18
  /**
18
19
  * Retry API call with exponential backoff
@@ -40,26 +41,31 @@ async function retryApiCall(fn, maxRetries = 3, backoffMs = 1000) {
40
41
  }
41
42
 
42
43
  /**
43
- * Calls pipeline test endpoint using centralized API client
44
+ * Calls pipeline test endpoint using centralized API client.
45
+ * Pipeline test accepts Bearer, API_KEY, or client credentials (x-client-id/x-client-secret) for CI/CD.
44
46
  * @async
45
47
  * @param {Object} params - Function parameters
46
48
  * @param {string} params.systemKey - System key
47
49
  * @param {string} params.datasourceKey - Datasource key
48
50
  * @param {Object} params.payloadTemplate - Test payload template
49
51
  * @param {string} params.dataplaneUrl - Dataplane URL
50
- * @param {Object} params.authConfig - Authentication configuration
52
+ * @param {Object} params.authConfig - Authentication configuration (token or client credentials)
51
53
  * @param {number} [params.timeout] - Request timeout in milliseconds (default: 30000)
54
+ * @param {boolean} [params.includeDebug] - Include debug output in response
52
55
  * @returns {Promise<Object>} Test response
53
56
  */
54
- async function callPipelineTestEndpoint({ systemKey, datasourceKey, payloadTemplate, dataplaneUrl, authConfig, timeout = 30000 }) {
55
- requireBearerForDataplanePipeline(authConfig);
57
+ async function callPipelineTestEndpoint({ systemKey, datasourceKey, payloadTemplate, dataplaneUrl, authConfig, timeout = 30000, includeDebug = false }) {
58
+ const testData = { payloadTemplate };
59
+ if (includeDebug) {
60
+ testData.includeDebug = true;
61
+ }
56
62
  const response = await retryApiCall(async() => {
57
63
  return await testDatasourceViaPipeline({
58
64
  dataplaneUrl,
59
65
  systemKey,
60
66
  datasourceKey,
61
67
  authConfig,
62
- testData: { payloadTemplate },
68
+ testData,
63
69
  options: { timeout }
64
70
  });
65
71
  });
@@ -67,6 +73,11 @@ async function callPipelineTestEndpoint({ systemKey, datasourceKey, payloadTempl
67
73
  if (!response.success || !response.data) {
68
74
  throw new Error(`Test endpoint failed: ${response.error || response.formattedError || 'Unknown error'}`);
69
75
  }
76
+ // When 200 with success: false in body, pass through; caller interprets via data.success
77
+ if (response.data?.success === false) {
78
+ const errMsg = response.data?.error || response.data?.formattedError || 'Test failed';
79
+ throw new Error(`Test endpoint failed: ${errMsg}`);
80
+ }
70
81
 
71
82
  return response.data.data || response.data;
72
83
  }
@@ -114,16 +125,18 @@ function determinePayloadTemplate(datasource, datasourceKey, customPayload) {
114
125
  * @param {string} params.dataplaneUrl - Dataplane URL
115
126
  * @param {Object} params.authConfig - Authentication configuration
116
127
  * @param {number} params.timeout - Request timeout
128
+ * @param {boolean} [params.includeDebug] - Include debug in response
117
129
  * @returns {Promise<Object>} Test result
118
130
  */
119
- async function testSingleDatasource({ systemKey, datasourceKey, payloadTemplate, dataplaneUrl, authConfig, timeout }) {
131
+ async function testSingleDatasource({ systemKey, datasourceKey, payloadTemplate, dataplaneUrl, authConfig, timeout, includeDebug = false }) {
120
132
  const testResponse = await callPipelineTestEndpoint({
121
133
  systemKey,
122
134
  datasourceKey,
123
135
  payloadTemplate,
124
136
  dataplaneUrl,
125
137
  authConfig,
126
- timeout
138
+ timeout,
139
+ includeDebug
127
140
  });
128
141
 
129
142
  return {
@@ -9,6 +9,7 @@
9
9
  */
10
10
 
11
11
  const Ajv = require('ajv');
12
+ const addFormats = require('ajv-formats');
12
13
 
13
14
  /**
14
15
  * Validates field mapping expression syntax (pipe-based DSL)
@@ -263,6 +264,7 @@ function validateMetadataSchema(datasource, testPayload) {
263
264
 
264
265
  try {
265
266
  const ajv = new Ajv({ allErrors: true, strict: false });
267
+ addFormats(ajv);
266
268
  const validate = ajv.compile(datasource.metadataSchema);
267
269
  const valid = validate(payloadTemplate);
268
270
 
@@ -319,6 +321,7 @@ function validateAgainstSchema(data, schema) {
319
321
  allowUnionTypes: true,
320
322
  validateSchema: false
321
323
  });
324
+ addFormats(ajv);
322
325
  // Remove $schema for draft-2020-12 to avoid AJV issues
323
326
  const schemaCopy = { ...schema };
324
327
  if (schemaCopy.$schema && schemaCopy.$schema.includes('2020-12')) {
@@ -1,29 +1,14 @@
1
1
  /**
2
2
  * @fileoverview File upload utilities for multipart/form-data requests
3
+ * All API calls go via ApiClient (lib/api/index.js); no duplicate auth logic.
3
4
  * @author AI Fabrix Team
4
5
  * @version 2.0.0
5
6
  */
6
7
 
7
8
  const fs = require('fs').promises;
8
9
  const path = require('path');
9
- const { makeApiCall, authenticatedApiCall } = require('./api');
10
+ const { ApiClient } = require('../api');
10
11
 
11
- /**
12
- * Upload a file using multipart/form-data
13
- * @async
14
- * @function uploadFile
15
- * @param {string} url - API endpoint URL
16
- * @param {string} filePath - Path to file to upload
17
- * @param {string} fieldName - Form field name for the file (default: 'file')
18
- * @param {Object} [authConfig] - Authentication configuration
19
- * @param {string} [authConfig.type] - Auth type ('bearer' | 'client-credentials')
20
- * @param {string} [authConfig.token] - Bearer token
21
- * @param {string} [authConfig.clientId] - Client ID
22
- * @param {string} [authConfig.clientSecret] - Client secret
23
- * @param {Object} [additionalFields] - Additional form fields to include
24
- * @returns {Promise<Object>} API response
25
- * @throws {Error} If file upload fails
26
- */
27
12
  /**
28
13
  * Validates file exists
29
14
  * @async
@@ -63,47 +48,32 @@ async function buildFormData(filePath, fieldName, additionalFields) {
63
48
  }
64
49
 
65
50
  /**
66
- * Builds authentication headers
67
- * @function buildAuthHeaders
68
- * @param {Object} authConfig - Authentication configuration
69
- * @returns {Object} Headers object
51
+ * Upload a file using multipart/form-data via ApiClient (single place for auth and API calls).
52
+ * @async
53
+ * @function uploadFile
54
+ * @param {string} url - Full API endpoint URL (e.g. https://dataplane.example.com/api/v1/wizard/parse-openapi)
55
+ * @param {string} filePath - Path to file to upload
56
+ * @param {string} fieldName - Form field name for the file (default: 'file')
57
+ * @param {Object} [authConfig] - Authentication configuration (token-only for app endpoints)
58
+ * @param {string} [authConfig.type] - Auth type ('bearer' | 'client-token')
59
+ * @param {string} [authConfig.token] - Token (Bearer user token or x-client-token application token)
60
+ * @param {Object} [additionalFields] - Additional form fields to include
61
+ * @returns {Promise<Object>} API response
62
+ * @throws {Error} If file upload fails
70
63
  */
71
- function buildAuthHeaders(authConfig) {
72
- const headers = {};
73
- if (authConfig.type === 'bearer' && authConfig.token) {
74
- headers['Authorization'] = `Bearer ${authConfig.token}`;
75
- } else if (authConfig.type === 'client-credentials') {
76
- if (authConfig.clientId) {
77
- headers['x-client-id'] = authConfig.clientId;
78
- }
79
- if (authConfig.clientSecret) {
80
- headers['x-client-secret'] = authConfig.clientSecret;
81
- }
82
- }
83
- return headers;
84
- }
85
-
86
64
  async function uploadFile(url, filePath, fieldName = 'file', authConfig = {}, additionalFields = {}) {
87
65
  await validateFileExists(filePath);
88
66
 
89
- const formData = await buildFormData(filePath, fieldName, additionalFields);
90
- const headers = buildAuthHeaders(authConfig);
91
-
92
- const options = {
93
- method: 'POST',
94
- headers,
95
- body: formData
96
- };
67
+ const parsed = new URL(url);
68
+ const baseUrl = parsed.origin;
69
+ const endpointPath = parsed.pathname + parsed.search;
97
70
 
98
- // Use authenticatedApiCall if bearer token, otherwise makeApiCall
99
- if (authConfig.type === 'bearer' && authConfig.token) {
100
- return await authenticatedApiCall(url, options, authConfig.token);
101
- }
71
+ const formData = await buildFormData(filePath, fieldName, additionalFields);
72
+ const client = new ApiClient(baseUrl, authConfig);
102
73
 
103
- return await makeApiCall(url, options);
74
+ return await client.postFormData(endpointPath, formData);
104
75
  }
105
76
 
106
77
  module.exports = {
107
78
  uploadFile
108
79
  };
109
-
@@ -45,6 +45,13 @@ const CATEGORIES = [
45
45
  { name: 'wizard' },
46
46
  { name: 'build', term: 'build <app>' },
47
47
  { name: 'run', term: 'run <app>' },
48
+ { name: 'shell', term: 'shell <app>' },
49
+ { name: 'test', term: 'test <app>' },
50
+ { name: 'install', term: 'install <app>' },
51
+ { name: 'test-e2e', term: 'test-e2e <app>' },
52
+ { name: 'lint', term: 'lint <app>' },
53
+ { name: 'logs', term: 'logs <app>' },
54
+ { name: 'stop', term: 'stop <app>' },
48
55
  { name: 'dockerfile', term: 'dockerfile <app>' }
49
56
  ]
50
57
  },
@@ -66,7 +73,10 @@ const CATEGORIES = [
66
73
  name: 'Application & Datasource Management',
67
74
  commands: [
68
75
  { name: 'app' },
69
- { name: 'datasource' }
76
+ { name: 'datasource' },
77
+ { name: 'credential' },
78
+ { name: 'deployment' },
79
+ { name: 'service-user' }
70
80
  ]
71
81
  },
72
82
  {
@@ -75,6 +85,8 @@ const CATEGORIES = [
75
85
  { name: 'resolve', term: 'resolve <app>' },
76
86
  { name: 'json', term: 'json <app>' },
77
87
  { name: 'split-json', term: 'split-json <app>' },
88
+ { name: 'convert', term: 'convert <app>' },
89
+ { name: 'show', term: 'show <appKey>' },
78
90
  { name: 'validate', term: 'validate <appOrFile>' },
79
91
  { name: 'diff', term: 'diff <file1> <file2>' }
80
92
  ]
@@ -83,7 +95,9 @@ const CATEGORIES = [
83
95
  name: 'External Systems',
84
96
  commands: [
85
97
  { name: 'download', term: 'download <system-key>' },
98
+ { name: 'upload', term: 'upload <system-key>' },
86
99
  { name: 'delete', term: 'delete <system-key>' },
100
+ { name: 'repair', term: 'repair <app>' },
87
101
  { name: 'test', term: 'test <app>' },
88
102
  { name: 'test-integration', term: 'test-integration <app>' }
89
103
  ]
@@ -92,7 +106,7 @@ const CATEGORIES = [
92
106
  name: 'Developer & Secrets',
93
107
  commands: [
94
108
  { name: 'dev' },
95
- { name: 'secrets' },
109
+ { name: 'secret' },
96
110
  { name: 'secure' }
97
111
  ]
98
112
  }
@@ -17,9 +17,53 @@ const containerUtils = require('./infra-containers');
17
17
 
18
18
  const execAsync = promisify(exec);
19
19
 
20
+ /**
21
+ * Builds services config map from ports and config flags.
22
+ * @param {Object} ports - Port configuration
23
+ * @param {Object} cfg - Config (pgadmin, redisCommander, traefik)
24
+ * @returns {Object} Map of serviceName -> { port, url }
25
+ */
26
+ function buildServicesConfig(ports, cfg) {
27
+ const services = {
28
+ postgres: { port: ports.postgres, url: `localhost:${ports.postgres}` },
29
+ redis: { port: ports.redis, url: `localhost:${ports.redis}` }
30
+ };
31
+ if (cfg.pgadmin !== false) services.pgadmin = { port: ports.pgadmin, url: `http://localhost:${ports.pgadmin}` };
32
+ if (cfg.redisCommander !== false) {
33
+ services['redis-commander'] = { port: ports.redisCommander, url: `http://localhost:${ports.redisCommander}` };
34
+ }
35
+ if (cfg.traefik) {
36
+ services.traefik = {
37
+ port: `${ports.traefikHttp}, ${ports.traefikHttps}`,
38
+ url: `http://localhost:${ports.traefikHttp}, https://localhost:${ports.traefikHttps}`
39
+ };
40
+ }
41
+ return services;
42
+ }
43
+
44
+ /**
45
+ * Gets status for a single service.
46
+ * @param {string} serviceName - Service name
47
+ * @param {Object} serviceConfig - { port, url }
48
+ * @param {string} devId - Developer ID
49
+ * @returns {Promise<Object>} Status entry
50
+ */
51
+ async function getServiceStatus(serviceName, serviceConfig, devId) {
52
+ try {
53
+ const containerName = await containerUtils.findContainer(serviceName, devId, { strict: true });
54
+ const rawStatus = containerName
55
+ ? (await execAsync(`docker inspect --format='{{.State.Status}}' ${containerName}`)).stdout.trim().replace(/['"]/g, '')
56
+ : 'not running';
57
+ return { status: rawStatus, port: serviceConfig.port, url: serviceConfig.url };
58
+ } catch {
59
+ return { status: 'not running', port: serviceConfig.port, url: serviceConfig.url };
60
+ }
61
+ }
62
+
20
63
  /**
21
64
  * Gets the status of infrastructure services
22
- * Returns detailed information about running containers
65
+ * Returns detailed information about running containers.
66
+ * Only includes pgAdmin, Redis Commander, and Traefik when enabled in config.
23
67
  *
24
68
  * @async
25
69
  * @function getInfraStatus
@@ -31,51 +75,13 @@ const execAsync = promisify(exec);
31
75
  */
32
76
  async function getInfraStatus() {
33
77
  const devId = await config.getDeveloperId();
34
- // Convert string developer ID to number for getDevPorts
35
- const devIdNum = parseInt(devId, 10);
36
- const ports = devConfig.getDevPorts(devIdNum);
37
- const services = {
38
- postgres: { port: ports.postgres, url: `localhost:${ports.postgres}` },
39
- redis: { port: ports.redis, url: `localhost:${ports.redis}` },
40
- pgadmin: { port: ports.pgadmin, url: `http://localhost:${ports.pgadmin}` },
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
- }
46
- };
47
-
78
+ const cfg = await config.getConfig();
79
+ const ports = devConfig.getDevPorts(parseInt(devId, 10));
80
+ const services = buildServicesConfig(ports, cfg);
48
81
  const status = {};
49
-
50
- for (const [serviceName, serviceConfig] of Object.entries(services)) {
51
- try {
52
- // Strict: only this developer's infra (no fallback to dev 0), so status reflects reality
53
- const containerName = await containerUtils.findContainer(serviceName, devId, { strict: true });
54
- if (containerName) {
55
- const { stdout } = await execAsync(`docker inspect --format='{{.State.Status}}' ${containerName}`);
56
- // Normalize status value (trim whitespace and remove quotes)
57
- const normalizedStatus = stdout.trim().replace(/['"]/g, '');
58
- status[serviceName] = {
59
- status: normalizedStatus,
60
- port: serviceConfig.port,
61
- url: serviceConfig.url
62
- };
63
- } else {
64
- status[serviceName] = {
65
- status: 'not running',
66
- port: serviceConfig.port,
67
- url: serviceConfig.url
68
- };
69
- }
70
- } catch (error) {
71
- status[serviceName] = {
72
- status: 'not running',
73
- port: serviceConfig.port,
74
- url: serviceConfig.url
75
- };
76
- }
82
+ for (const [name, svc] of Object.entries(services)) {
83
+ status[name] = await getServiceStatus(name, svc, devId);
77
84
  }
78
-
79
85
  return status;
80
86
  }
81
87
 
@@ -186,8 +192,37 @@ async function getAppStatus() {
186
192
  return apps;
187
193
  }
188
194
 
195
+ /**
196
+ * Lists app container names for a developer (excludes infra containers).
197
+ * Used by down-infra to stop/remove all app-related containers on the same network.
198
+ * When includeExited is true, includes stopped/exited containers (e.g. db-init one-offs).
199
+ *
200
+ * @async
201
+ * @function listAppContainerNamesForDeveloper
202
+ * @param {string} devId - Developer ID
203
+ * @param {Object} [options] - Options
204
+ * @param {boolean} [options.includeExited=false] - If true, use docker ps -a to include exited containers
205
+ * @returns {Promise<string[]>} Container names (e.g. aifabrix-myapp, aifabrix-keycloak-db-init)
206
+ */
207
+ async function listAppContainerNamesForDeveloper(devId, options = {}) {
208
+ const devIdNum = parseInt(devId, 10);
209
+ const filterPattern = devIdNum === 0 ? 'aifabrix-' : `aifabrix-dev${devId}-`;
210
+ const infraContainers = getInfraContainerNames(devIdNum, devId);
211
+ const includeExited = !!options.includeExited;
212
+ try {
213
+ const allFlag = includeExited ? ' -a' : '';
214
+ const { stdout } = await execAsync(`docker ps${allFlag} --filter "name=${filterPattern}" --format "{{.Names}}"`);
215
+ const names = (stdout || '').trim().split('\n').filter(Boolean);
216
+ return names.filter(n => !infraContainers.includes(n));
217
+ } catch {
218
+ return [];
219
+ }
220
+ }
221
+
189
222
  module.exports = {
190
223
  getInfraStatus,
191
- getAppStatus
224
+ getAppStatus,
225
+ extractAppName,
226
+ listAppContainerNamesForDeveloper
192
227
  };
193
228
 
@@ -13,11 +13,12 @@ const path = require('path');
13
13
  const yaml = require('js-yaml');
14
14
  const logger = require('../utils/logger');
15
15
  const pathsUtil = require('./paths');
16
+ const { mergeSecretsIntoFile } = require('./secrets-generator');
16
17
 
17
18
  /**
18
19
  * Saves a secret to ~/.aifabrix/secrets.local.yaml
19
20
  * Uses paths.getAifabrixHome() to respect config.yaml aifabrix-home override
20
- * Merges with existing secrets without overwriting other keys
21
+ * Merges the key into the file (updates in place if key already exists, e.g. after rotate-secret)
21
22
  *
22
23
  * @async
23
24
  * @function saveLocalSecret
@@ -39,48 +40,12 @@ async function saveLocalSecret(key, value) {
39
40
  }
40
41
 
41
42
  const secretsPath = path.join(pathsUtil.getAifabrixHome(), 'secrets.local.yaml');
42
- const secretsDir = path.dirname(secretsPath);
43
-
44
- // Create directory if needed
45
- if (!fs.existsSync(secretsDir)) {
46
- fs.mkdirSync(secretsDir, { recursive: true, mode: 0o700 });
47
- }
48
-
49
- // Load existing secrets
50
- let existingSecrets = {};
51
- if (fs.existsSync(secretsPath)) {
52
- try {
53
- const content = fs.readFileSync(secretsPath, 'utf8');
54
- existingSecrets = yaml.load(content) || {};
55
- if (typeof existingSecrets !== 'object') {
56
- existingSecrets = {};
57
- }
58
- } catch (error) {
59
- logger.warn(`Warning: Could not read existing secrets file: ${error.message}`);
60
- existingSecrets = {};
61
- }
62
- }
63
-
64
- // Merge with new secret
65
- const updatedSecrets = {
66
- ...existingSecrets,
67
- [key]: value
68
- };
69
-
70
- // Save to file
71
- const yamlContent = yaml.dump(updatedSecrets, {
72
- indent: 2,
73
- lineWidth: 120,
74
- noRefs: true,
75
- sortKeys: false
76
- });
77
-
78
- fs.writeFileSync(secretsPath, yamlContent, { mode: 0o600 });
43
+ mergeSecretsIntoFile(secretsPath, { [key]: value });
79
44
  }
80
45
 
81
46
  /**
82
47
  * Saves a secret to a specified secrets file path
83
- * Merges with existing secrets without overwriting other keys
48
+ * Merges the key into the file (updates in place if key already exists)
84
49
  *
85
50
  * @async
86
51
  * @function saveSecret
@@ -134,11 +99,11 @@ function resolveAndPrepareSecretsPath(secretsPath) {
134
99
 
135
100
  /**
136
101
  * Loads existing secrets from file
137
- * @function loadExistingSecrets
102
+ * @function _loadExistingSecrets
138
103
  * @param {string} resolvedPath - Resolved secrets path
139
104
  * @returns {Object} Existing secrets object
140
105
  */
141
- function loadExistingSecrets(resolvedPath) {
106
+ function _loadExistingSecrets(resolvedPath) {
142
107
  if (!fs.existsSync(resolvedPath)) {
143
108
  return {};
144
109
  }
@@ -157,17 +122,7 @@ async function saveSecret(key, value, secretsPath) {
157
122
  validateSaveSecretParams(key, value, secretsPath);
158
123
 
159
124
  const resolvedPath = resolveAndPrepareSecretsPath(secretsPath);
160
- const existingSecrets = loadExistingSecrets(resolvedPath);
161
-
162
- const updatedSecrets = { ...existingSecrets, [key]: value };
163
- const yamlContent = yaml.dump(updatedSecrets, {
164
- indent: 2,
165
- lineWidth: 120,
166
- noRefs: true,
167
- sortKeys: false
168
- });
169
-
170
- fs.writeFileSync(resolvedPath, yamlContent, { mode: 0o600 });
125
+ mergeSecretsIntoFile(resolvedPath, { [key]: value });
171
126
  }
172
127
 
173
128
  /**