@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
@@ -2,7 +2,7 @@
2
2
  * AI Fabrix Builder - Datasource Commands
3
3
  *
4
4
  * Handles datasource validation, listing, comparison, and deployment
5
- * Commands: datasource validate, datasource list, datasource diff, datasource deploy
5
+ * Commands: datasource validate, datasource list, datasource diff, datasource upload
6
6
  *
7
7
  * @fileoverview Datasource management commands for AI Fabrix Builder
8
8
  * @author AI Fabrix Team
@@ -15,6 +15,9 @@ const { validateDatasourceFile } = require('../datasource/validate');
15
15
  const { listDatasources } = require('../datasource/list');
16
16
  const { compareDatasources } = require('../datasource/diff');
17
17
  const { deployDatasource } = require('../datasource/deploy');
18
+ const { runDatasourceTestIntegration } = require('../datasource/test-integration');
19
+ const { runDatasourceTestE2E } = require('../datasource/test-e2e');
20
+ const { displayIntegrationTestResults, displayE2EResults } = require('../utils/external-system-display');
18
21
 
19
22
  function setupDatasourceValidateCommand(datasource) {
20
23
  datasource.command('validate <file>')
@@ -62,14 +65,80 @@ function setupDatasourceDiffCommand(datasource) {
62
65
  });
63
66
  }
64
67
 
65
- function setupDatasourceDeployCommand(datasource) {
66
- datasource.command('deploy <myapp> <file>')
67
- .description('Deploy datasource to dataplane')
68
+ function setupDatasourceUploadCommand(datasource) {
69
+ datasource.command('upload <myapp> <file>')
70
+ .description('Upload datasource to dataplane')
68
71
  .action(async(myapp, file, options) => {
69
72
  try {
70
73
  await deployDatasource(myapp, file, options);
71
74
  } catch (error) {
72
- logger.error(chalk.red('❌ Deployment failed:'), error.message);
75
+ logger.error(chalk.red('❌ Upload failed:'), error.message);
76
+ process.exit(1);
77
+ }
78
+ });
79
+ }
80
+
81
+ function setupDatasourceTestIntegrationCommand(datasource) {
82
+ datasource.command('test-integration <datasourceKey>')
83
+ .description('Run integration (config) test for one datasource via dataplane pipeline')
84
+ .option('-a, --app <appKey>', 'App key (default: resolve from cwd if inside integration/<appKey>/)')
85
+ .option('-p, --payload <file>', 'Path to custom test payload file')
86
+ .option('-e, --env <env>', 'Environment: dev, tst, or pro')
87
+ .option('--debug', 'Include debug output and write log to integration/<appKey>/logs/')
88
+ .option('--timeout <ms>', 'Request timeout in milliseconds', '30000')
89
+ .action(async(datasourceKey, options) => {
90
+ try {
91
+ const result = await runDatasourceTestIntegration(datasourceKey, {
92
+ app: options.app,
93
+ payload: options.payload,
94
+ environment: options.env,
95
+ debug: options.debug,
96
+ timeout: options.timeout
97
+ });
98
+ displayIntegrationTestResults({
99
+ systemKey: result.systemKey || 'unknown',
100
+ datasourceResults: [result],
101
+ success: result.success
102
+ }, options.verbose);
103
+ if (!result.success) process.exit(1);
104
+ } catch (error) {
105
+ logger.error(chalk.red('❌ Integration test failed:'), error.message);
106
+ process.exit(1);
107
+ }
108
+ });
109
+ }
110
+
111
+ function setupDatasourceTestE2ECommand(datasource) {
112
+ datasource.command('test-e2e <datasourceKey>')
113
+ .description('Run E2E test for one datasource (config, credential, sync, data, CIP) via dataplane')
114
+ .option('-a, --app <appKey>', 'App key (default: resolve from cwd if inside integration/<appKey>/)')
115
+ .option('-e, --env <env>', 'Environment: dev, tst, or pro')
116
+ .option('-v, --verbose', 'Show detailed step output and poll progress')
117
+ .option('--debug', 'Include debug output and write log to integration/<appKey>/logs/')
118
+ .option('--test-crud', 'Enable CRUD lifecycle test (body testCrud: true)')
119
+ .option('--record-id <id>', 'Record ID for test (body recordId)')
120
+ .option('--no-cleanup', 'Disable cleanup after test (body cleanup: false)')
121
+ .option('--primary-key-value <value|@path>', 'Primary key value or path to JSON file (e.g. @pk.json) for body primaryKeyValue')
122
+ .option('--no-async', 'Use sync mode (no polling); single POST, no asyncRun')
123
+ .action(async(datasourceKey, options) => {
124
+ try {
125
+ const data = await runDatasourceTestE2E(datasourceKey, {
126
+ app: options.app,
127
+ environment: options.env,
128
+ debug: options.debug,
129
+ verbose: options.verbose,
130
+ async: options.async !== false,
131
+ testCrud: options.testCrud,
132
+ recordId: options.recordId,
133
+ cleanup: options.cleanup,
134
+ primaryKeyValue: options.primaryKeyValue
135
+ });
136
+ displayE2EResults(data, options.verbose);
137
+ const steps = data.steps || data.completedActions || [];
138
+ const failed = data.success === false || steps.some(s => s.success === false || s.error);
139
+ if (failed) process.exit(1);
140
+ } catch (error) {
141
+ logger.error(chalk.red('❌ E2E test failed:'), error.message);
73
142
  process.exit(1);
74
143
  }
75
144
  });
@@ -84,7 +153,9 @@ function setupDatasourceCommands(program) {
84
153
  setupDatasourceValidateCommand(datasource);
85
154
  setupDatasourceListCommand(datasource);
86
155
  setupDatasourceDiffCommand(datasource);
87
- setupDatasourceDeployCommand(datasource);
156
+ setupDatasourceUploadCommand(datasource);
157
+ setupDatasourceTestIntegrationCommand(datasource);
158
+ setupDatasourceTestE2ECommand(datasource);
88
159
  }
89
160
 
90
161
  module.exports = { setupDatasourceCommands };
@@ -0,0 +1,141 @@
1
+ /**
2
+ * @fileoverview CLI action handlers for dev list/add/update/pin/delete (remote Builder Server)
3
+ * @author AI Fabrix Team
4
+ * @version 2.0.0
5
+ */
6
+
7
+ const chalk = require('chalk');
8
+ const config = require('../core/config');
9
+ const logger = require('../utils/logger');
10
+ const devApi = require('../api/dev.api');
11
+ const { getRemoteDevAuth } = require('../utils/remote-dev-auth');
12
+
13
+ const REMOTE_NOT_CONFIGURED_MSG = 'Remote server is not configured. Set remote-server and run "aifabrix dev init" first.';
14
+
15
+ const ID_WIDTH = 8;
16
+ const NAME_WIDTH = 25;
17
+ const EMAIL_WIDTH = 30;
18
+ const CERT_WIDTH = 10;
19
+ const UNTIL_WIDTH = 22; // 2026-03-20T17:31:14
20
+ const TABLE_SEPARATOR_LENGTH = 130;
21
+
22
+ /**
23
+ * Format certificate validity end for display (e.g. 2026-03-20T17:31:14.000Z -> 2026-03-20T17:31:14).
24
+ * @param {string} iso - ISO 8601 date string
25
+ * @returns {string} Truncated datetime without milliseconds and Z
26
+ */
27
+ function formatCertUntil(iso) {
28
+ if (!iso || typeof iso !== 'string') return '?';
29
+ return iso.replace(/\.\d{3}Z?$/i, '').trim();
30
+ }
31
+
32
+ /**
33
+ * Handle dev list – list developer users (remote only). Table format, sorted by name.
34
+ * @returns {Promise<void>}
35
+ */
36
+ async function handleDevList() {
37
+ const auth = await getRemoteDevAuth();
38
+ if (!auth) {
39
+ logger.log(chalk.yellow(REMOTE_NOT_CONFIGURED_MSG));
40
+ return;
41
+ }
42
+ const users = await devApi.listUsers(auth.serverUrl, auth.clientCertPem);
43
+ if (users.length === 0) {
44
+ logger.log(chalk.gray('No developers registered.'));
45
+ return;
46
+ }
47
+ logger.log(chalk.bold('\n📋 Developers:\n'));
48
+ logger.log(chalk.gray('ID'.padEnd(ID_WIDTH) + 'Name'.padEnd(NAME_WIDTH) + 'Email'.padEnd(EMAIL_WIDTH) + 'Cert'.padEnd(CERT_WIDTH) + 'Until'.padEnd(UNTIL_WIDTH) + 'Groups'));
49
+ logger.log(chalk.gray('-'.repeat(TABLE_SEPARATOR_LENGTH)));
50
+ const sorted = [...users].sort((a, b) => (a.name || '').localeCompare(b.name || '', undefined, { sensitivity: 'base' }));
51
+ sorted.forEach(u => {
52
+ const certLabel = u.certificateIssued ? 'yes' : 'no cert';
53
+ const untilRaw = u.certificateIssued && u.certificateValidNotAfter ? u.certificateValidNotAfter : null;
54
+ const untilStr = untilRaw ? formatCertUntil(untilRaw) : '-';
55
+ const id = String(u.id ?? '').padEnd(ID_WIDTH);
56
+ const name = (u.name || 'N/A').padEnd(NAME_WIDTH);
57
+ const email = (u.email || 'N/A').padEnd(EMAIL_WIDTH);
58
+ const cert = certLabel.padEnd(CERT_WIDTH);
59
+ const until = untilStr.padEnd(UNTIL_WIDTH);
60
+ const groups = (u.groups || []).join(', ');
61
+ logger.log(`${id}${name}${email}${cert}${until}${groups}`);
62
+ });
63
+ logger.log('');
64
+ }
65
+
66
+ /**
67
+ * Handle dev add – create developer (remote only).
68
+ * @param {Object} options - Commander options (developerId, name, email, groups)
69
+ * @returns {Promise<void>}
70
+ */
71
+ async function handleDevAdd(options) {
72
+ const auth = await getRemoteDevAuth();
73
+ if (!auth) throw new Error(REMOTE_NOT_CONFIGURED_MSG);
74
+ const groups = (options.groups || 'developer').split(',').map(s => s.trim()).filter(Boolean);
75
+ const user = await devApi.createUser(auth.serverUrl, auth.clientCertPem, {
76
+ developerId: options.developerId,
77
+ name: options.name,
78
+ email: options.email,
79
+ groups: groups.length ? groups : ['developer']
80
+ });
81
+ logger.log(chalk.green(`✓ Developer ${user.id} created. Use "aifabrix dev pin ${user.id}" to create a PIN for onboarding.`));
82
+ }
83
+
84
+ /**
85
+ * Handle dev update – update developer (remote only).
86
+ * @param {string} developerId - Developer ID
87
+ * @param {Object} options - Commander options (name, email, groups)
88
+ * @returns {Promise<void>}
89
+ */
90
+ async function handleDevUpdate(developerId, options) {
91
+ const auth = await getRemoteDevAuth();
92
+ if (!auth) throw new Error(REMOTE_NOT_CONFIGURED_MSG);
93
+ const id = options.developerId || options['developer-id'] || developerId;
94
+ if (!id) throw new Error('Developer ID is required (--developer-id or positional argument).');
95
+ const body = {};
96
+ if (options.name) body.name = options.name;
97
+ if (options.email) body.email = options.email;
98
+ if (options.groups) body.groups = options.groups.split(',').map(s => s.trim()).filter(Boolean);
99
+ if (Object.keys(body).length === 0) {
100
+ throw new Error('Provide at least one of --name, --email, --groups');
101
+ }
102
+ await devApi.updateUser(auth.serverUrl, auth.clientCertPem, id, body);
103
+ logger.log(chalk.green(`✓ Developer ${id} updated.`));
104
+ }
105
+
106
+ /**
107
+ * Handle dev pin – create/regenerate PIN (remote only).
108
+ * @param {string} [developerId] - Developer ID (optional; uses config if omitted)
109
+ * @returns {Promise<void>}
110
+ */
111
+ async function handleDevPin(developerId) {
112
+ const auth = await getRemoteDevAuth();
113
+ if (!auth) throw new Error(REMOTE_NOT_CONFIGURED_MSG);
114
+ const id = developerId || await config.getDeveloperId();
115
+ if (!id) throw new Error('developerId is required (argument or set developer-id in config)');
116
+ const res = await devApi.createPin(auth.serverUrl, auth.clientCertPem, id);
117
+ logger.log(chalk.green(`✓ PIN created for ${id}, expires ${res.expiresAt}.`));
118
+ logger.log(chalk.yellow(` Give this PIN once to the developer for: aifabrix dev init --developer-id ${id} --server ${auth.serverUrl} --pin ${res.pin}`));
119
+ }
120
+
121
+ /**
122
+ * Handle dev delete – remove developer (remote only).
123
+ * @param {string} developerId - Developer ID
124
+ * @returns {Promise<void>}
125
+ */
126
+ async function handleDevDelete(developerId) {
127
+ const auth = await getRemoteDevAuth();
128
+ if (!auth) throw new Error(REMOTE_NOT_CONFIGURED_MSG);
129
+ const id = developerId;
130
+ if (!id) throw new Error('Developer ID is required (positional argument or --developer-id).');
131
+ await devApi.deleteUser(auth.serverUrl, auth.clientCertPem, id);
132
+ logger.log(chalk.green(`✓ Developer ${id} removed.`));
133
+ }
134
+
135
+ module.exports = {
136
+ handleDevList,
137
+ handleDevAdd,
138
+ handleDevUpdate,
139
+ handleDevPin,
140
+ handleDevDelete
141
+ };
@@ -0,0 +1,114 @@
1
+ /**
2
+ * dev down – stop Mutagen sync sessions and optionally app containers for this developer.
3
+ *
4
+ * @fileoverview Dev down command (plan 65: stop sync sessions; optional stop apps)
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const { exec } = require('child_process');
10
+ const { promisify } = require('util');
11
+ const chalk = require('chalk');
12
+ const logger = require('../utils/logger');
13
+ const config = require('../core/config');
14
+ const appLib = require('../app');
15
+
16
+ const execAsync = promisify(exec);
17
+
18
+ /**
19
+ * Stop Mutagen sync sessions for this developer (session names aifabrix-<dev-id>-*).
20
+ * When Mutagen is not available or no sessions exist, no-op.
21
+ * @param {string} developerId - Developer ID
22
+ * @returns {Promise<void>}
23
+ */
24
+ async function stopMutagenSessions(developerId) {
25
+ const { getMutagenPath } = require('../utils/mutagen');
26
+ const mutagenPath = await getMutagenPath();
27
+ if (!mutagenPath) {
28
+ logger.log(chalk.gray('Mutagen not installed; no sync sessions to stop.'));
29
+ return;
30
+ }
31
+ try {
32
+ const { stdout } = await execAsync(`"${mutagenPath}" sync list --template '{{.Name}}'`, {
33
+ encoding: 'utf8',
34
+ timeout: 5000
35
+ });
36
+ const sessions = (stdout || '').trim().split('\n').filter(Boolean);
37
+ const prefix = `aifabrix-${developerId}-`;
38
+ const toTerminate = sessions.filter(name => name.startsWith(prefix));
39
+ for (const name of toTerminate) {
40
+ await execAsync(`"${mutagenPath}" sync terminate "${name}"`, { timeout: 5000 });
41
+ logger.log(chalk.green(` ✓ Stopped sync session: ${name}`));
42
+ }
43
+ if (toTerminate.length === 0 && sessions.length > 0) {
44
+ logger.log(chalk.gray('No sync sessions for this developer.'));
45
+ } else if (toTerminate.length === 0) {
46
+ logger.log(chalk.gray('No Mutagen sync sessions to stop.'));
47
+ }
48
+ } catch (err) {
49
+ logger.log(chalk.gray('No Mutagen sync sessions to stop.'));
50
+ }
51
+ }
52
+
53
+ const INFRA_SUFFIXES = ['-postgres', '-redis', '-pgadmin', '-redis-commander', '-traefik', '-db-init'];
54
+
55
+ /**
56
+ * List running app container names for this developer (excludes infra containers).
57
+ * @param {string} developerId - Developer ID
58
+ * @returns {Promise<string[]>} Container names (e.g. aifabrix-dev1-myapp)
59
+ */
60
+ async function listAppContainersForDeveloper(developerId) {
61
+ const idNum = parseInt(developerId, 10);
62
+ const filter = idNum === 0 ? 'aifabrix-' : `aifabrix-dev${developerId}-`;
63
+ const { stdout } = await execAsync(
64
+ `docker ps --filter "name=${filter}" --format "{{.Names}}"`,
65
+ { encoding: 'utf8' }
66
+ );
67
+ const names = (stdout || '').trim().split('\n').filter(Boolean);
68
+ return names.filter(n => !INFRA_SUFFIXES.some(s => n.endsWith(s)));
69
+ }
70
+
71
+ /**
72
+ * Extract app name from container name (aifabrix-dev1-myapp -> myapp, aifabrix-myapp -> myapp).
73
+ * @param {string} containerName - Container name
74
+ * @param {string} developerId - Developer ID
75
+ * @returns {string} App name
76
+ */
77
+ function appNameFromContainer(containerName, developerId) {
78
+ const idNum = parseInt(developerId, 10);
79
+ const prefix = idNum === 0 ? 'aifabrix-' : `aifabrix-dev${developerId}-`;
80
+ return containerName.startsWith(prefix) ? containerName.slice(prefix.length) : containerName;
81
+ }
82
+
83
+ /**
84
+ * Handle dev down: stop Mutagen sessions; optionally stop app containers.
85
+ * @param {Object} options - { apps: boolean }
86
+ * @returns {Promise<void>}
87
+ */
88
+ async function handleDevDown(options = {}) {
89
+ const developerId = await config.getDeveloperId();
90
+ logger.log(chalk.blue('\nStopping dev resources for developer ' + developerId + '...\n'));
91
+
92
+ await stopMutagenSessions(developerId);
93
+
94
+ if (options.apps) {
95
+ const containers = await listAppContainersForDeveloper(developerId);
96
+ for (const containerName of containers) {
97
+ const appName = appNameFromContainer(containerName, developerId);
98
+ if (!appName) continue;
99
+ try {
100
+ await appLib.downApp(appName, {});
101
+ logger.log(chalk.green(` ✓ Stopped app: ${appName}`));
102
+ } catch (err) {
103
+ logger.log(chalk.yellow(` ⚠ Could not stop ${appName}: ${err.message}`));
104
+ }
105
+ }
106
+ if (containers.length === 0) {
107
+ logger.log(chalk.gray('No running app containers for this developer.'));
108
+ }
109
+ }
110
+
111
+ logger.log(chalk.green('\n✓ dev down complete.\n'));
112
+ }
113
+
114
+ module.exports = { handleDevDown };