@aifabrix/builder 2.8.0 → 2.9.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 (36) hide show
  1. package/integration/hubspot/README.md +136 -0
  2. package/integration/hubspot/env.template +9 -0
  3. package/integration/hubspot/hubspot-deploy-company.json +200 -0
  4. package/integration/hubspot/hubspot-deploy-contact.json +228 -0
  5. package/integration/hubspot/hubspot-deploy-deal.json +248 -0
  6. package/integration/hubspot/hubspot-deploy.json +91 -0
  7. package/integration/hubspot/variables.yaml +17 -0
  8. package/lib/app-config.js +4 -3
  9. package/lib/app-deploy.js +8 -20
  10. package/lib/app-dockerfile.js +7 -9
  11. package/lib/app-prompts.js +6 -5
  12. package/lib/app-push.js +9 -9
  13. package/lib/app-register.js +23 -5
  14. package/lib/app-rotate-secret.js +10 -0
  15. package/lib/app-run.js +5 -11
  16. package/lib/app.js +42 -14
  17. package/lib/build.js +20 -16
  18. package/lib/cli.js +61 -2
  19. package/lib/datasource-deploy.js +14 -20
  20. package/lib/external-system-deploy.js +123 -40
  21. package/lib/external-system-download.js +431 -0
  22. package/lib/external-system-generator.js +13 -10
  23. package/lib/external-system-test.js +446 -0
  24. package/lib/generator-builders.js +323 -0
  25. package/lib/generator.js +200 -292
  26. package/lib/schema/application-schema.json +853 -852
  27. package/lib/schema/external-datasource.schema.json +823 -49
  28. package/lib/schema/external-system.schema.json +96 -78
  29. package/lib/templates.js +1 -1
  30. package/lib/utils/cli-utils.js +4 -4
  31. package/lib/utils/external-system-display.js +159 -0
  32. package/lib/utils/external-system-validators.js +245 -0
  33. package/lib/utils/paths.js +151 -1
  34. package/lib/utils/schema-resolver.js +7 -2
  35. package/lib/validator.js +5 -2
  36. package/package.json +1 -1
@@ -0,0 +1,248 @@
1
+ {
2
+ "key": "hubspot-deal",
3
+ "displayName": "HubSpot Deal",
4
+ "description": "HubSpot deals datasource with field mappings for CRM deal data",
5
+ "systemKey": "hubspot",
6
+ "entityKey": "deal",
7
+ "resourceType": "deal",
8
+ "enabled": true,
9
+ "version": "1.0.0",
10
+ "metadataSchema": {
11
+ "type": "object",
12
+ "properties": {
13
+ "id": {
14
+ "type": "string"
15
+ },
16
+ "properties": {
17
+ "type": "object",
18
+ "properties": {
19
+ "dealname": {
20
+ "type": "object",
21
+ "properties": {
22
+ "value": {
23
+ "type": "string"
24
+ }
25
+ }
26
+ },
27
+ "amount": {
28
+ "type": "object",
29
+ "properties": {
30
+ "value": {
31
+ "type": "string"
32
+ }
33
+ }
34
+ },
35
+ "deal_currency_code": {
36
+ "type": "object",
37
+ "properties": {
38
+ "value": {
39
+ "type": "string"
40
+ }
41
+ }
42
+ },
43
+ "dealstage": {
44
+ "type": "object",
45
+ "properties": {
46
+ "value": {
47
+ "type": "string"
48
+ }
49
+ }
50
+ },
51
+ "pipeline": {
52
+ "type": "object",
53
+ "properties": {
54
+ "value": {
55
+ "type": "string"
56
+ }
57
+ }
58
+ },
59
+ "closedate": {
60
+ "type": "object",
61
+ "properties": {
62
+ "value": {
63
+ "type": "string"
64
+ }
65
+ }
66
+ },
67
+ "dealtype": {
68
+ "type": "object",
69
+ "properties": {
70
+ "value": {
71
+ "type": "string"
72
+ }
73
+ }
74
+ },
75
+ "createdate": {
76
+ "type": "object",
77
+ "properties": {
78
+ "value": {
79
+ "type": "string"
80
+ }
81
+ }
82
+ },
83
+ "hs_lastmodifieddate": {
84
+ "type": "object",
85
+ "properties": {
86
+ "value": {
87
+ "type": "string"
88
+ }
89
+ }
90
+ }
91
+ }
92
+ },
93
+ "associations": {
94
+ "type": "object",
95
+ "properties": {
96
+ "companies": {
97
+ "type": "object",
98
+ "properties": {
99
+ "results": {
100
+ "type": "array",
101
+ "items": {
102
+ "type": "object",
103
+ "properties": {
104
+ "id": {
105
+ "type": "string"
106
+ }
107
+ }
108
+ }
109
+ }
110
+ }
111
+ },
112
+ "contacts": {
113
+ "type": "object",
114
+ "properties": {
115
+ "results": {
116
+ "type": "array",
117
+ "items": {
118
+ "type": "object",
119
+ "properties": {
120
+ "id": {
121
+ "type": "string"
122
+ }
123
+ }
124
+ }
125
+ }
126
+ }
127
+ }
128
+ }
129
+ }
130
+ },
131
+ "required": ["id", "properties"]
132
+ },
133
+ "fieldMappings": {
134
+ "accessFields": ["stage", "pipeline"],
135
+ "fields": {
136
+ "id": {
137
+ "expression": "{{id}}",
138
+ "type": "string",
139
+ "description": "Unique deal identifier",
140
+ "required": true
141
+ },
142
+ "dealName": {
143
+ "expression": "{{properties.dealname.value}} | trim",
144
+ "type": "string",
145
+ "description": "Deal name",
146
+ "required": false
147
+ },
148
+ "amount": {
149
+ "expression": "{{properties.amount.value}}",
150
+ "type": "string",
151
+ "description": "Deal amount",
152
+ "required": false
153
+ },
154
+ "currency": {
155
+ "expression": "{{properties.deal_currency_code.value}} | toUpper | trim",
156
+ "type": "string",
157
+ "description": "Currency code",
158
+ "required": false
159
+ },
160
+ "stage": {
161
+ "expression": "{{properties.dealstage.value}} | trim",
162
+ "type": "string",
163
+ "description": "Deal stage for ABAC filtering",
164
+ "required": false
165
+ },
166
+ "pipeline": {
167
+ "expression": "{{properties.pipeline.value}} | trim",
168
+ "type": "string",
169
+ "description": "Pipeline name for ABAC filtering",
170
+ "required": false
171
+ },
172
+ "closeDate": {
173
+ "expression": "{{properties.closedate.value}}",
174
+ "type": "string",
175
+ "description": "Expected close date",
176
+ "required": false
177
+ },
178
+ "dealType": {
179
+ "expression": "{{properties.dealtype.value}} | trim",
180
+ "type": "string",
181
+ "description": "Deal type",
182
+ "required": false
183
+ },
184
+ "associatedCompany": {
185
+ "expression": "{{associations.companies.results[0].id}}",
186
+ "type": "string",
187
+ "description": "Associated company ID",
188
+ "required": false
189
+ },
190
+ "associatedContacts": {
191
+ "expression": "{{associations.contacts.results}}",
192
+ "type": "array",
193
+ "description": "Array of associated contact IDs",
194
+ "required": false
195
+ },
196
+ "createdAt": {
197
+ "expression": "{{properties.createdate.value}}",
198
+ "type": "string",
199
+ "description": "Creation timestamp",
200
+ "required": false
201
+ },
202
+ "updatedAt": {
203
+ "expression": "{{properties.hs_lastmodifieddate.value}}",
204
+ "type": "string",
205
+ "description": "Last modification timestamp",
206
+ "required": false
207
+ }
208
+ }
209
+ },
210
+ "exposed": {
211
+ "fields": ["id", "dealName", "amount", "currency", "stage", "pipeline", "closeDate", "dealType", "associatedCompany", "associatedContacts", "createdAt", "updatedAt"],
212
+ "description": "Exposed fields for HubSpot deals"
213
+ },
214
+ "openapi": {
215
+ "enabled": true,
216
+ "documentKey": "hubspot-v3",
217
+ "baseUrl": "https://api.hubapi.com",
218
+ "operations": {
219
+ "list": {
220
+ "operationId": "getDeals",
221
+ "method": "GET",
222
+ "path": "/crm/v3/objects/deals"
223
+ },
224
+ "get": {
225
+ "operationId": "getDeal",
226
+ "method": "GET",
227
+ "path": "/crm/v3/objects/deals/{dealId}"
228
+ },
229
+ "create": {
230
+ "operationId": "createDeal",
231
+ "method": "POST",
232
+ "path": "/crm/v3/objects/deals"
233
+ },
234
+ "update": {
235
+ "operationId": "updateDeal",
236
+ "method": "PATCH",
237
+ "path": "/crm/v3/objects/deals/{dealId}"
238
+ },
239
+ "delete": {
240
+ "operationId": "deleteDeal",
241
+ "method": "DELETE",
242
+ "path": "/crm/v3/objects/deals/{dealId}"
243
+ }
244
+ },
245
+ "autoRbac": true
246
+ }
247
+ }
248
+
@@ -0,0 +1,91 @@
1
+ {
2
+ "key": "hubspot",
3
+ "displayName": "HubSpot CRM",
4
+ "description": "HubSpot CRM integration with OpenAPI support for companies, contacts, and deals",
5
+ "type": "openapi",
6
+ "enabled": true,
7
+ "environment": {
8
+ "baseUrl": "https://api.hubapi.com"
9
+ },
10
+ "authentication": {
11
+ "type": "oauth2",
12
+ "mode": "oauth2",
13
+ "oauth2": {
14
+ "tokenUrl": "{{TOKENURL}}",
15
+ "clientId": "{{CLIENTID}}",
16
+ "clientSecret": "{{CLIENTSECRET}}",
17
+ "scopes": [
18
+ "crm.objects.companies.read",
19
+ "crm.objects.companies.write",
20
+ "crm.objects.contacts.read",
21
+ "crm.objects.contacts.write",
22
+ "crm.objects.deals.read",
23
+ "crm.objects.deals.write"
24
+ ]
25
+ }
26
+ },
27
+ "configuration": [
28
+ {
29
+ "name": "CLIENTID",
30
+ "value": "hubspot-clientidKeyVault",
31
+ "location": "keyvault",
32
+ "required": true
33
+ },
34
+ {
35
+ "name": "CLIENTSECRET",
36
+ "value": "hubspot-clientsecretKeyVault",
37
+ "location": "keyvault",
38
+ "required": true
39
+ },
40
+ {
41
+ "name": "TOKENURL",
42
+ "value": "https://api.hubapi.com/oauth/v1/token",
43
+ "location": "variable",
44
+ "required": true
45
+ },
46
+ {
47
+ "name": "REDIRECT_URI",
48
+ "value": "hubspot-redirect-uriKeyVault",
49
+ "location": "keyvault",
50
+ "required": false
51
+ },
52
+ {
53
+ "name": "HUBSPOT_API_VERSION",
54
+ "value": "v3",
55
+ "location": "variable",
56
+ "required": false,
57
+ "portalInput": {
58
+ "field": "select",
59
+ "label": "HubSpot API Version",
60
+ "placeholder": "Select API version",
61
+ "options": ["v1", "v2", "v3"],
62
+ "validation": {
63
+ "required": false
64
+ }
65
+ }
66
+ },
67
+ {
68
+ "name": "MAX_PAGE_SIZE",
69
+ "value": "100",
70
+ "location": "variable",
71
+ "required": false,
72
+ "portalInput": {
73
+ "field": "text",
74
+ "label": "Maximum Page Size",
75
+ "placeholder": "100",
76
+ "validation": {
77
+ "required": false,
78
+ "pattern": "^[0-9]+$",
79
+ "min": 1,
80
+ "max": 1000
81
+ }
82
+ }
83
+ }
84
+ ],
85
+ "openapi": {
86
+ "documentKey": "hubspot-v3",
87
+ "autoDiscoverEntities": false
88
+ },
89
+ "tags": ["crm", "sales", "marketing", "hubspot"]
90
+ }
91
+
@@ -0,0 +1,17 @@
1
+ app:
2
+ key: hubspot
3
+ displayName: "HubSpot CRM Integration"
4
+ description: "HubSpot CRM external system integration with companies, contacts, and deals"
5
+ type: external
6
+
7
+ externalIntegration:
8
+ schemaBasePath: ./
9
+ systems:
10
+ - hubspot-deploy.json
11
+ dataSources:
12
+ - hubspot-deploy-company.json
13
+ - hubspot-deploy-contact.json
14
+ - hubspot-deploy-deal.json
15
+ autopublish: true
16
+ version: 1.0.0
17
+
package/lib/app-config.js CHANGED
@@ -99,14 +99,14 @@ async function generateRbacYamlFile(appPath, appName, config) {
99
99
  }
100
100
 
101
101
  /**
102
- * Generates aifabrix-deploy.json file
102
+ * Generates <app-name>-deploy.json file (consistent naming for all apps)
103
103
  * @async
104
104
  * @param {string} appPath - Path to application directory
105
105
  * @param {string} appName - Application name
106
106
  * @param {Object} config - Application configuration
107
107
  */
108
108
  async function generateDeployJsonFile(appPath, appName, config) {
109
- // Skip aifabrix-deploy.json for external type (uses pipeline API instead)
109
+ // Skip for external type (external system JSON is generated separately)
110
110
  if (config.type === 'external') {
111
111
  return;
112
112
  }
@@ -138,8 +138,9 @@ async function generateDeployJsonFile(appPath, appName, config) {
138
138
  }
139
139
  };
140
140
 
141
+ // Use consistent naming: <app-name>-deploy.json
141
142
  await fs.writeFile(
142
- path.join(appPath, 'aifabrix-deploy.json'),
143
+ path.join(appPath, `${appName}-deploy.json`),
143
144
  JSON.stringify(deployJson, null, 2)
144
145
  );
145
146
  }
package/lib/app-deploy.js CHANGED
@@ -17,6 +17,7 @@ const pushUtils = require('./push');
17
17
  const logger = require('./utils/logger');
18
18
  const config = require('./config');
19
19
  const { getDeploymentAuth } = require('./utils/token-manager');
20
+ const { detectAppType } = require('./utils/paths');
20
21
 
21
22
  /**
22
23
  * Validate application name format
@@ -215,10 +216,11 @@ function validateDeploymentConfig(deploymentConfig) {
215
216
  * @throws {Error} If configuration is invalid
216
217
  */
217
218
  async function loadDeploymentConfig(appName, options) {
218
- const builderPath = path.join(process.cwd(), 'builder', appName);
219
- await validateAppDirectory(builderPath, appName);
219
+ // Detect app type and get correct path (integration or builder)
220
+ const { appPath } = await detectAppType(appName);
221
+ await validateAppDirectory(appPath, appName);
220
222
 
221
- const variablesPath = path.join(builderPath, 'variables.yaml');
223
+ const variablesPath = path.join(appPath, 'variables.yaml');
222
224
  const variables = await loadVariablesFile(variablesPath);
223
225
 
224
226
  const deploymentConfig = extractDeploymentConfig(options, variables);
@@ -366,23 +368,9 @@ async function deployApp(appName, options = {}) {
366
368
 
367
369
  validateAppName(appName);
368
370
 
369
- // 2. Check if app type is external - route to external deployment
370
- const yaml = require('js-yaml');
371
- const fs = require('fs').promises;
372
- const path = require('path');
373
- const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
374
- try {
375
- const variablesContent = await fs.readFile(variablesPath, 'utf8');
376
- const variables = yaml.load(variablesContent);
377
- if (variables.app && variables.app.type === 'external') {
378
- const externalDeploy = require('./external-system-deploy');
379
- await externalDeploy.deployExternalSystem(appName, options);
380
- return { success: true, type: 'external' };
381
- }
382
- } catch (error) {
383
- // If variables.yaml doesn't exist or can't be read, continue with normal deployment
384
- // The error will be properly handled in loadDeploymentConfig
385
- }
371
+ // 2. Check if app type is external - use normal deployment flow with application-schema.json
372
+ // External systems now deploy via miso controller as normal application (full application file)
373
+ // The json command generates application-schema.json which is used for deployment
386
374
 
387
375
  // 2. Load deployment configuration
388
376
  config = await loadDeploymentConfig(appName, options);
@@ -85,25 +85,23 @@ async function generateAndCopyDockerfile(appPath, dockerfilePath, config) {
85
85
  */
86
86
  async function generateDockerfileForApp(appName, options = {}) {
87
87
  // Check if app type is external - skip Dockerfile generation
88
- const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
88
+ const { detectAppType } = require('./utils/paths');
89
89
  try {
90
- const yamlContent = await fs.readFile(configPath, 'utf8');
91
- const variables = yaml.load(yamlContent);
92
- if (variables.app && variables.app.type === 'external') {
90
+ const { isExternal } = await detectAppType(appName);
91
+ if (isExternal) {
93
92
  logger.log(chalk.yellow('⚠️ External systems don\'t require Dockerfiles. Skipping...'));
94
93
  return null;
95
94
  }
96
95
  } catch (error) {
97
- // If variables.yaml doesn't exist, continue with normal generation
98
- if (error.code !== 'ENOENT') {
99
- throw error;
100
- }
96
+ // If detection fails, continue with normal generation
97
+ // (detectAppType throws if app doesn't exist, which is fine for dockerfile command)
101
98
  }
102
99
  try {
103
100
  // Validate app name
104
101
  validateAppName(appName);
105
102
 
106
- const appPath = path.join(process.cwd(), 'builder', appName);
103
+ // Detect app type and get correct path (integration or builder)
104
+ const { appPath } = await detectAppType(appName);
107
105
  const dockerfilePath = path.join(appPath, 'Dockerfile');
108
106
 
109
107
  // Check if Dockerfile already exists
@@ -364,11 +364,6 @@ function mergePromptAnswers(appName, options, answers) {
364
364
  * @returns {Promise<Object>} Complete configuration
365
365
  */
366
366
  async function promptForOptions(appName, options) {
367
- // Default github to false if not provided (make it truly optional)
368
- if (!Object.prototype.hasOwnProperty.call(options, 'github')) {
369
- options.github = false;
370
- }
371
-
372
367
  // Get app type from options (default to webapp)
373
368
  const appType = options.type || 'webapp';
374
369
 
@@ -398,6 +393,12 @@ async function promptForOptions(appName, options) {
398
393
  // Add type to merged config
399
394
  merged.type = appType;
400
395
 
396
+ // For external type, remove port and language as they're not applicable
397
+ if (appType === 'external') {
398
+ delete merged.port;
399
+ delete merged.language;
400
+ }
401
+
401
402
  return merged;
402
403
  }
403
404
 
package/lib/app-push.js CHANGED
@@ -69,7 +69,10 @@ function extractImageName(config, appName) {
69
69
  * @throws {Error} If configuration cannot be loaded
70
70
  */
71
71
  async function loadPushConfig(appName, options) {
72
- const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
72
+ // Detect app type and get correct path (integration or builder)
73
+ const { detectAppType } = require('./utils/paths');
74
+ const { appPath } = await detectAppType(appName);
75
+ const configPath = path.join(appPath, 'variables.yaml');
73
76
  try {
74
77
  const config = yaml.load(await fs.readFile(configPath, 'utf8'));
75
78
  const registry = options.registry || config.image?.registry;
@@ -180,19 +183,16 @@ function displayPushResults(registry, imageName, tags) {
180
183
  */
181
184
  async function pushApp(appName, options = {}) {
182
185
  // Check if app type is external - skip push
183
- const configPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
186
+ const { detectAppType } = require('./utils/paths');
184
187
  try {
185
- const yamlContent = await fs.readFile(configPath, 'utf8');
186
- const config = yaml.load(yamlContent);
187
- if (config.app && config.app.type === 'external') {
188
+ const { isExternal } = await detectAppType(appName);
189
+ if (isExternal) {
188
190
  logger.log(chalk.yellow('⚠️ External systems don\'t require Docker images. Skipping push...'));
189
191
  return;
190
192
  }
191
193
  } catch (error) {
192
- // If variables.yaml doesn't exist, continue with normal push
193
- if (error.code !== 'ENOENT') {
194
- throw error;
195
- }
194
+ // If detection fails, continue with normal push
195
+ // (detectAppType throws if app doesn't exist, which is fine for push command)
196
196
  }
197
197
  try {
198
198
  // Validate app name
@@ -19,6 +19,8 @@ const logger = require('./utils/logger');
19
19
  const { saveLocalSecret, isLocalhost } = require('./utils/local-secrets');
20
20
  const { updateEnvTemplate } = require('./utils/env-template');
21
21
  const { getOrRefreshDeviceToken } = require('./utils/token-manager');
22
+ const { detectAppType } = require('./utils/paths');
23
+ const { generateEnvFile } = require('./secrets');
22
24
 
23
25
  // Import createApp to auto-generate config if missing
24
26
  let createApp;
@@ -86,7 +88,9 @@ const registerApplicationSchema = {
86
88
  * @returns {Promise<{variables: Object, created: boolean}>} Variables and creation flag
87
89
  */
88
90
  async function loadVariablesYaml(appKey) {
89
- const variablesPath = path.join(process.cwd(), 'builder', appKey, 'variables.yaml');
91
+ // Detect app type and get correct path (integration or builder)
92
+ const { appPath } = await detectAppType(appKey);
93
+ const variablesPath = path.join(appPath, 'variables.yaml');
90
94
 
91
95
  try {
92
96
  const variablesContent = await fs.readFile(variablesPath, 'utf-8');
@@ -122,7 +126,9 @@ async function createMinimalAppIfNeeded(appKey, options) {
122
126
  authentication: false
123
127
  });
124
128
 
125
- const variablesPath = path.join(process.cwd(), 'builder', appKey, 'variables.yaml');
129
+ // Detect app type and get correct path (integration or builder)
130
+ const { appPath } = await detectAppType(appKey);
131
+ const variablesPath = path.join(appPath, 'variables.yaml');
126
132
  const variablesContent = await fs.readFile(variablesPath, 'utf-8');
127
133
  return yaml.load(variablesContent);
128
134
  }
@@ -170,11 +176,12 @@ function extractAppConfiguration(variables, appKey, options) {
170
176
 
171
177
  /**
172
178
  * Validate application registration data
179
+ * @async
173
180
  * @param {Object} config - Application configuration
174
181
  * @param {string} originalAppKey - Original app key for error messages
175
182
  * @throws {Error} If validation fails
176
183
  */
177
- function validateAppRegistrationData(config, originalAppKey) {
184
+ async function validateAppRegistrationData(config, originalAppKey) {
178
185
  const missingFields = [];
179
186
  if (!config.appKey) missingFields.push('app.key');
180
187
  if (!config.displayName) missingFields.push('app.name');
@@ -182,7 +189,10 @@ function validateAppRegistrationData(config, originalAppKey) {
182
189
  if (missingFields.length > 0) {
183
190
  logger.error(chalk.red('❌ Missing required fields in variables.yaml:'));
184
191
  missingFields.forEach(field => logger.error(chalk.red(` - ${field}`)));
185
- logger.error(chalk.red(`\n Please update builder/${originalAppKey}/variables.yaml and try again.`));
192
+ // Detect app type to show correct path
193
+ const { appPath } = await detectAppType(originalAppKey);
194
+ const relativePath = path.relative(process.cwd(), appPath);
195
+ logger.error(chalk.red(`\n Please update ${relativePath}/variables.yaml and try again.`));
186
196
  process.exit(1);
187
197
  }
188
198
 
@@ -382,7 +392,7 @@ async function registerApplication(appKey, options) {
382
392
  const appConfig = extractAppConfiguration(finalVariables, appKey, options);
383
393
 
384
394
  // Validate configuration (pass original appKey for error messages)
385
- validateAppRegistrationData(appConfig, appKey);
395
+ await validateAppRegistrationData(appConfig, appKey);
386
396
 
387
397
  // Get controller URL from variables.yaml if available
388
398
  const controllerUrl = finalVariables?.deployment?.controllerUrl;
@@ -434,6 +444,14 @@ async function registerApplication(appKey, options) {
434
444
  // Update env.template
435
445
  await updateEnvTemplate(registeredAppKey, clientIdKey, clientSecretKey, authConfig.apiUrl);
436
446
 
447
+ // Regenerate .env file with updated credentials
448
+ try {
449
+ await generateEnvFile(registeredAppKey, null, 'local');
450
+ logger.log(chalk.green('✓ .env file updated with new credentials'));
451
+ } catch (error) {
452
+ logger.warn(chalk.yellow(`⚠️ Could not regenerate .env file: ${error.message}`));
453
+ }
454
+
437
455
  logger.log(chalk.green('\n✓ Credentials saved to ~/.aifabrix/secrets.local.yaml'));
438
456
  logger.log(chalk.green('✓ env.template updated with MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL\n'));
439
457
  } catch (error) {
@@ -17,6 +17,7 @@ const logger = require('./utils/logger');
17
17
  const { saveLocalSecret, isLocalhost } = require('./utils/local-secrets');
18
18
  const { updateEnvTemplate } = require('./utils/env-template');
19
19
  const { getEnvironmentPrefix } = require('./app-register');
20
+ const { generateEnvFile } = require('./secrets');
20
21
 
21
22
  /**
22
23
  * Validate environment parameter
@@ -147,6 +148,15 @@ async function rotateSecret(appKey, options) {
147
148
  // Update env.template if localhost
148
149
  if (isLocalhost(controllerUrl)) {
149
150
  await updateEnvTemplate(appKey, clientIdKey, clientSecretKey, controllerUrl);
151
+
152
+ // Regenerate .env file with updated credentials
153
+ try {
154
+ await generateEnvFile(appKey, null, 'local');
155
+ logger.log(chalk.green('✓ .env file updated with new credentials'));
156
+ } catch (error) {
157
+ logger.warn(chalk.yellow(`⚠️ Could not regenerate .env file: ${error.message}`));
158
+ }
159
+
150
160
  logger.log(chalk.green('\n✓ Credentials saved to ~/.aifabrix/secrets.local.yaml'));
151
161
  logger.log(chalk.green('✓ env.template updated with MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL\n'));
152
162
  } else {
package/lib/app-run.js CHANGED
@@ -49,23 +49,17 @@ async function runApp(appName, options = {}) {
49
49
  }
50
50
 
51
51
  // Check if app type is external - skip Docker run
52
- const yaml = require('js-yaml');
53
- const fs = require('fs').promises;
54
- const path = require('path');
55
- const variablesPath = path.join(process.cwd(), 'builder', appName, 'variables.yaml');
52
+ const { detectAppType } = require('./utils/paths');
56
53
  try {
57
- const variablesContent = await fs.readFile(variablesPath, 'utf8');
58
- const variables = yaml.load(variablesContent);
59
- if (variables.app && variables.app.type === 'external') {
54
+ const { isExternal } = await detectAppType(appName);
55
+ if (isExternal) {
60
56
  logger.log(chalk.yellow('⚠️ External systems don\'t run as Docker containers.'));
61
57
  logger.log(chalk.blue('Use "aifabrix build" to deploy to dataplane, then test via OpenAPI endpoints.'));
62
58
  return;
63
59
  }
64
60
  } catch (error) {
65
- // If variables.yaml doesn't exist, continue with normal run
66
- if (error.code !== 'ENOENT') {
67
- throw error;
68
- }
61
+ // If detection fails, continue with normal run
62
+ // (detectAppType throws if app doesn't exist, which is fine for run command)
69
63
  }
70
64
 
71
65
  // Validate app name and load configuration