@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
@@ -1,5 +1,5 @@
1
1
  /**
2
- * CLI secrets and security command setup (secrets set, secure).
2
+ * CLI secret and security command setup (secret set, secure).
3
3
  *
4
4
  * @fileoverview Secrets command definitions for AI Fabrix Builder CLI
5
5
  * @author AI Fabrix Team
@@ -8,18 +8,50 @@
8
8
 
9
9
  const { handleCommandError } = require('../utils/cli-utils');
10
10
  const { handleSecretsSet } = require('../commands/secrets-set');
11
+ const { handleSecretsList } = require('../commands/secrets-list');
12
+ const { handleSecretsRemove } = require('../commands/secrets-remove');
13
+ const { handleSecretsValidate } = require('../commands/secrets-validate');
11
14
  const { handleSecure } = require('../commands/secure');
12
15
 
16
+ function setupSecretValidateCommand(secretCmd) {
17
+ secretCmd
18
+ .command('validate [path]')
19
+ .description('Validate secrets file (YAML structure and optional naming convention)')
20
+ .option('--naming', 'Check key names against *KeyVault convention')
21
+ .action(async(pathArg, options) => {
22
+ try {
23
+ const result = await handleSecretsValidate(pathArg, options);
24
+ if (!result.valid) process.exit(1);
25
+ } catch (error) {
26
+ handleCommandError(error, 'secret validate');
27
+ process.exit(1);
28
+ }
29
+ });
30
+ }
31
+
13
32
  /**
14
33
  * Sets up secrets and security commands
15
34
  * @param {Command} program - Commander program instance
16
35
  */
17
36
  function setupSecretsCommands(program) {
18
- const secretsCmd = program
19
- .command('secrets')
37
+ const secretCmd = program
38
+ .command('secret')
20
39
  .description('Manage secrets in secrets files');
21
40
 
22
- secretsCmd
41
+ secretCmd
42
+ .command('list')
43
+ .description('List secret keys (user or shared; use --shared for shared)')
44
+ .option('--shared', 'List shared secrets (from config aifabrix-secrets or remote API)')
45
+ .action(async(options) => {
46
+ try {
47
+ await handleSecretsList(options);
48
+ } catch (error) {
49
+ handleCommandError(error, 'secret list');
50
+ process.exit(1);
51
+ }
52
+ });
53
+
54
+ secretCmd
23
55
  .command('set <key> <value>')
24
56
  .description('Set a secret value in secrets file')
25
57
  .option('--shared', 'Save to general secrets file (from config.yaml aifabrix-secrets) instead of user secrets')
@@ -27,11 +59,26 @@ function setupSecretsCommands(program) {
27
59
  try {
28
60
  await handleSecretsSet(key, value, options);
29
61
  } catch (error) {
30
- handleCommandError(error, 'secrets set');
62
+ handleCommandError(error, 'secret set');
63
+ process.exit(1);
64
+ }
65
+ });
66
+
67
+ secretCmd
68
+ .command('remove <key>')
69
+ .description('Remove a secret by key')
70
+ .option('--shared', 'Remove from shared secrets (file or remote API)')
71
+ .action(async(key, options) => {
72
+ try {
73
+ await handleSecretsRemove(key, options);
74
+ } catch (error) {
75
+ handleCommandError(error, 'secret remove');
31
76
  process.exit(1);
32
77
  }
33
78
  });
34
79
 
80
+ setupSecretValidateCommand(secretCmd);
81
+
35
82
  program.command('secure')
36
83
  .description('Encrypt secrets in secrets.local.yaml files for ISO 27001 compliance')
37
84
  .option('--secrets-encryption <key>', 'Encryption key (32 bytes, hex or base64)')
@@ -13,7 +13,7 @@ const secrets = require('../core/secrets');
13
13
  const generator = require('../generator');
14
14
  const logger = require('../utils/logger');
15
15
  const { handleCommandError, logOfflinePathWhenType } = require('../utils/cli-utils');
16
- const { detectAppType, getDeployJsonPath } = require('../utils/paths');
16
+ const { detectAppType, getDeployJsonPath, getResolveAppPath } = require('../utils/paths');
17
17
 
18
18
  /**
19
19
  * Resolve app path and type for split-json (integration first, then builder).
@@ -77,9 +77,13 @@ function logSplitJsonResult(result) {
77
77
  result.datasourceFiles.forEach(filePath => logger.log(` • datasource: ${filePath}`));
78
78
  }
79
79
  if (result.rbac) {
80
- logger.log(` • rbac.yml: ${result.rbac}`);
80
+ logger.log(` • rbac.yaml: ${result.rbac}`);
81
+ }
82
+ if (result.readmeSkipped) {
83
+ logger.log(` • README.md: (kept existing) ${result.readmeSkipped}`);
84
+ } else if (result.readme) {
85
+ logger.log(` • README.md: ${result.readme}`);
81
86
  }
82
- logger.log(` • README.md: ${result.readme}`);
83
87
  }
84
88
 
85
89
  function setupResolveCommand(program) {
@@ -89,9 +93,18 @@ function setupResolveCommand(program) {
89
93
  .option('--skip-validation', 'Skip file validation after generating .env')
90
94
  .action(async(appName, options) => {
91
95
  try {
92
- const envPath = await secrets.generateEnvFile(appName, undefined, 'docker', options.force);
96
+ const { appPath, envOnly } = await getResolveAppPath(appName);
97
+ const envPath = await secrets.generateEnvFile(
98
+ appName,
99
+ undefined,
100
+ 'docker',
101
+ options.force,
102
+ { appPath, envOnly, skipOutputPath: false, preserveFromPath: null }
103
+ );
93
104
  logger.log(`✓ Generated .env file: ${envPath}`);
94
- if (!options.skipValidation) {
105
+ if (envOnly) {
106
+ logger.log(chalk.gray(' (env-only mode: validation skipped; no application.yaml)'));
107
+ } else if (!options.skipValidation) {
95
108
  const validate = require('../validation/validate');
96
109
  const result = await validate.validateAppOrFile(appName);
97
110
  validate.displayValidationResults(result);
@@ -132,7 +145,7 @@ function setupJsonCommand(program) {
132
145
  });
133
146
  }
134
147
 
135
- function setupSplitJsonConvertShowCommands(program) {
148
+ function setupSplitJsonCommand(program) {
136
149
  program.command('split-json <app>')
137
150
  .description('Split deployment JSON into component files (env.template, application.yaml, rbac.yml, README.md)')
138
151
  .option('-o, --output <dir>', 'Output directory for component files (defaults to same directory as JSON)')
@@ -144,15 +157,66 @@ function setupSplitJsonConvertShowCommands(program) {
144
157
  process.exit(1);
145
158
  }
146
159
  });
160
+ }
147
161
 
162
+ function setupRepairCommand(program) {
163
+ program.command('repair <app>')
164
+ .description('Repair external integration config: fix drift (file lists, app key, datasource alignment, rbac, manifest)')
165
+ .option('--auth <method>', 'Set authentication method (oauth2, aad, apikey, basic, queryParam, oidc, hmac, none); updates system file and env.template')
166
+ .option('--dry-run', 'Report changes only; do not write')
167
+ .option('--rbac', 'Ensure RBAC permissions per datasource and add default Admin/Reader roles if none exist')
168
+ .option('--expose', 'Set exposed.attributes on each datasource to all fieldMappings.attributes keys')
169
+ .option('--sync', 'Add default sync section to datasources that lack it')
170
+ .option('--test', 'Generate testPayload.payloadTemplate and testPayload.expectedResult from attributes')
171
+ .action(async(appName, options) => {
172
+ try {
173
+ const { repairExternalIntegration } = require('../commands/repair');
174
+ const { detectAppType } = require('../utils/paths');
175
+ const { appPath } = await detectAppType(appName);
176
+ logOfflinePathWhenType(appPath);
177
+ const result = await repairExternalIntegration(appName, {
178
+ auth: options.auth,
179
+ dryRun: options.dryRun,
180
+ rbac: options.rbac,
181
+ expose: options.expose,
182
+ sync: options.sync,
183
+ test: options.test
184
+ });
185
+ if (options.dryRun && result.updated && result.changes.length > 0) {
186
+ logger.log(chalk.yellow('\nWould apply:'));
187
+ result.changes.forEach(c => logger.log(chalk.gray(` ${c}`)));
188
+ } else if (result.updated) {
189
+ logger.log(chalk.green('\n✓ Repaired external integration config.'));
190
+ } else {
191
+ logger.log(chalk.gray('No changes needed; config already matches files on disk.'));
192
+ }
193
+ } catch (error) {
194
+ handleCommandError(error, 'repair');
195
+ process.exit(1);
196
+ }
197
+ });
198
+ }
199
+
200
+ function setupConvertCommand(program) {
148
201
  program.command('convert <app>')
149
202
  .description('Convert integration/external system and datasource config files between JSON and YAML')
150
- .option('--format <format>', 'Target format: json | yaml (required)')
203
+ .option('--format <format>', 'Target format: json | yaml (required unless config format is set)')
151
204
  .option('-f, --force', 'Skip confirmation prompt')
152
205
  .action(async(appName, options) => {
153
206
  try {
207
+ const config = require('../core/config');
208
+ const effectiveFormat = options.format || (await config.getFormat());
209
+ if (!effectiveFormat) {
210
+ throw new Error(
211
+ 'Option --format is required and must be \'json\' or \'yaml\' (or set default with aifabrix dev set-format)'
212
+ );
213
+ }
214
+ const normalized = effectiveFormat.trim().toLowerCase();
215
+ if (normalized !== 'json' && normalized !== 'yaml') {
216
+ throw new Error('Option --format must be \'json\' or \'yaml\'');
217
+ }
154
218
  const { runConvert } = require('../commands/convert');
155
- const { converted, deleted } = await runConvert(appName, { format: options.format, force: options.force });
219
+ const { converted, deleted } = await runConvert(appName, { format: normalized, force: options.force });
156
220
  logger.log(chalk.green('\n✓ Convert complete.'));
157
221
  converted.forEach(p => logger.log(` • ${p}`));
158
222
  if (deleted.length > 0) {
@@ -164,7 +228,9 @@ function setupSplitJsonConvertShowCommands(program) {
164
228
  process.exit(1);
165
229
  }
166
230
  });
231
+ }
167
232
 
233
+ function setupShowCommand(program) {
168
234
  program.command('show <appKey>')
169
235
  .description('Show application info from local builder/ or integration/ (offline) or from controller (--online)')
170
236
  .option('--online', 'Fetch application data from the controller')
@@ -180,25 +246,60 @@ function setupSplitJsonConvertShowCommands(program) {
180
246
  });
181
247
  }
182
248
 
249
+ function setupSplitJsonConvertShowCommands(program) {
250
+ setupSplitJsonCommand(program);
251
+ setupRepairCommand(program);
252
+ setupConvertCommand(program);
253
+ setupShowCommand(program);
254
+ }
255
+
256
+ async function runValidateCommand(appOrFile, options) {
257
+ const validate = require('../validation/validate');
258
+ const opts = options.opts ? options.opts() : options;
259
+ const integration = opts.integration === true;
260
+ const builder = opts.builder === true;
261
+ const outFormat = (opts.format || 'default').toLowerCase();
262
+
263
+ if (integration || builder) {
264
+ const batchResult = integration && builder
265
+ ? await validate.validateAll(opts)
266
+ : integration
267
+ ? await validate.validateAllIntegrations(opts)
268
+ : await validate.validateAllBuilderApps(opts);
269
+ if (outFormat === 'json') {
270
+ logger.log(JSON.stringify(batchResult, null, 2));
271
+ } else {
272
+ validate.displayBatchValidationResults(batchResult);
273
+ }
274
+ if (!batchResult.valid) process.exit(1);
275
+ return;
276
+ }
277
+
278
+ if (!appOrFile || typeof appOrFile !== 'string') {
279
+ logger.log(chalk.red('App name or file path is required, or use --integration / --builder'));
280
+ process.exit(1);
281
+ }
282
+
283
+ const result = await validate.validateAppOrFile(appOrFile, opts);
284
+ if (outFormat === 'json') {
285
+ logger.log(JSON.stringify(result, null, 2));
286
+ } else {
287
+ validate.displayValidationResults(result);
288
+ }
289
+ if (!result.valid) process.exit(1);
290
+ }
291
+
183
292
  function setupValidateDiffCommands(program) {
184
- program.command('validate <appOrFile>')
185
- .description('Validate application or external integration file')
293
+ program.command('validate [appOrFile]')
294
+ .description('Validate application or external integration file; use --integration or --builder to validate all apps')
186
295
  .option('--format <format>', 'Output format: json | default (human-readable)')
187
- .action(async(appOrFile, options) => {
188
- try {
189
- const validate = require('../validation/validate');
190
- const result = await validate.validateAppOrFile(appOrFile, options);
191
- const outFormat = (options.format || 'default').toLowerCase();
192
- if (outFormat === 'json') {
193
- logger.log(JSON.stringify(result, null, 2));
194
- } else {
195
- validate.displayValidationResults(result);
196
- }
197
- if (!result.valid) process.exit(1);
198
- } catch (error) {
296
+ .option('--integration', 'Validate all applications under integration/')
297
+ .option('--builder', 'Validate all applications under builder/')
298
+ .action((appOrFile, options) => {
299
+ runValidateCommand(appOrFile, options).catch((error) => {
199
300
  handleCommandError(error, 'validate');
200
301
  process.exit(1);
201
- }
302
+ });
202
303
  });
203
304
 
204
305
  program.command('diff <file1> <file2>')
@@ -229,4 +330,8 @@ function setupUtilityCommands(program) {
229
330
  setupValidateDiffCommands(program);
230
331
  }
231
332
 
232
- module.exports = { setupUtilityCommands };
333
+ module.exports = {
334
+ setupUtilityCommands,
335
+ resolveSplitJsonApp,
336
+ handleSplitJsonCommand
337
+ };
@@ -0,0 +1,172 @@
1
+ /**
2
+ * Install command – run install (dependencies) inside app container (dev: running; tst: ephemeral with .env).
3
+ *
4
+ * @fileoverview App install command for builder apps (plan 73)
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const { spawn } = require('child_process');
10
+ const chalk = require('chalk');
11
+ const logger = require('../utils/logger');
12
+ const config = require('../core/config');
13
+ const containerHelpers = require('../utils/app-run-containers');
14
+ const composeGenerator = require('../utils/compose-generator');
15
+ const pathsUtil = require('../utils/paths');
16
+ const { loadConfigFile } = require('../utils/config-format');
17
+ const secretsEnvWrite = require('../core/secrets-env-write');
18
+
19
+ /**
20
+ * Load application config for builder app.
21
+ * @param {string} appName - Application name
22
+ * @returns {Object} Application config
23
+ */
24
+ function loadAppConfig(appName) {
25
+ const builderPath = pathsUtil.getBuilderPath(appName);
26
+ const configPath = pathsUtil.resolveApplicationConfigPath(builderPath);
27
+ return loadConfigFile(configPath);
28
+ }
29
+
30
+ /** Env set so install/test/lint use a writable temp dir in the container (e.g. when /app is read-only). */
31
+ const TMPDIR_VALUE = '/tmp';
32
+
33
+ /** pnpm store in /tmp when /app is read-only (avoids EACCES on _tmp_ files in project root). */
34
+ const PNPM_STORE_DIR = '/tmp/.pnpm-store';
35
+
36
+ /**
37
+ * Ensure install command can run when /app is read-only: use TMPDIR and pnpm --store-dir.
38
+ * @param {string} cmd - Raw install command (e.g. "pnpm install")
39
+ * @returns {string} Command (possibly with --store-dir for pnpm)
40
+ */
41
+ function installCommandForReadOnlyApp(cmd) {
42
+ const t = typeof cmd === 'string' ? cmd.trim() : '';
43
+ if (t === 'pnpm install' || t.startsWith('pnpm install ') ||
44
+ t === 'pnpm i' || t.startsWith('pnpm i ')) {
45
+ return t.includes('--store-dir') ? t : `${t} --store-dir ${PNPM_STORE_DIR}`;
46
+ }
47
+ return t;
48
+ }
49
+
50
+ /**
51
+ * Resolve install command from application config (language or build.scripts).
52
+ * @param {Object} appConfig - Application config
53
+ * @returns {string} Shell command to run install (e.g. "pnpm install" or "make install")
54
+ */
55
+ function getInstallCommand(appConfig) {
56
+ const build = appConfig.build;
57
+ const scripts = (build && build.scripts) || appConfig.scripts;
58
+ if (scripts && typeof scripts.install === 'string' && scripts.install.trim()) {
59
+ return scripts.install.trim();
60
+ }
61
+ const lang = (build && build.language || appConfig.language || 'typescript').toLowerCase();
62
+ return lang === 'python' ? 'make install' : 'pnpm install';
63
+ }
64
+
65
+ /**
66
+ * Run install in dev (exec in running container).
67
+ * Resolves app .env (including NPM_TOKEN/PYPI_TOKEN from kv://) and passes it so install has registry tokens.
68
+ * @param {string} appName - Application name
69
+ * @param {string|number} developerId - Developer ID
70
+ * @param {string} installCmd - Install command
71
+ * @returns {Promise<number>} Exit code
72
+ */
73
+ async function runInstallInDev(appName, developerId, installCmd) {
74
+ const containerName = containerHelpers.getContainerName(appName, developerId);
75
+ const isRunning = await containerHelpers.checkContainerRunning(appName, developerId);
76
+ if (!isRunning) {
77
+ throw new Error(
78
+ `Container ${containerName} is not running.\nRun 'aifabrix run ${appName}' first.`
79
+ );
80
+ }
81
+ logger.log(chalk.blue(`Running install in container ${containerName}: ${installCmd}\n`));
82
+ const cmd = installCommandForReadOnlyApp(installCmd);
83
+ const envFilePath = await secretsEnvWrite.resolveAndWriteEnvFile(appName, {});
84
+ return runDockerExec(containerName, cmd, envFilePath);
85
+ }
86
+
87
+ /**
88
+ * Run command in container via docker exec; stream output. Returns exit code.
89
+ * Uses -e TMPDIR, pnpm store, CI; when envFilePath is set passes --env-file so NPM_TOKEN/PYPI_TOKEN (from kv://) are available.
90
+ * @param {string} containerName - Container name
91
+ * @param {string} cmd - Shell command
92
+ * @param {string|null} [envFilePath] - Path to resolved .env for --env-file (optional; when set, install has registry tokens)
93
+ * @returns {Promise<number>} Exit code
94
+ */
95
+ function runDockerExec(containerName, cmd, envFilePath = null) {
96
+ return new Promise((resolve) => {
97
+ const args = [
98
+ 'exec',
99
+ '-e', `TMPDIR=${TMPDIR_VALUE}`,
100
+ '-e', `npm_config_store_dir=${PNPM_STORE_DIR}`,
101
+ '-e', 'CI=true'
102
+ ];
103
+ if (envFilePath) {
104
+ args.push('--env-file', envFilePath);
105
+ }
106
+ args.push(containerName, 'sh', '-c', cmd);
107
+ const proc = spawn('docker', args, {
108
+ stdio: 'inherit',
109
+ shell: false
110
+ });
111
+ proc.on('close', code => resolve(code !== null ? code : 1));
112
+ proc.on('error', () => resolve(1));
113
+ });
114
+ }
115
+
116
+ /**
117
+ * Run command in ephemeral container with optional env file; stream output. Returns exit code.
118
+ * @param {string} fullImage - Image:tag
119
+ * @param {string} cmd - Shell command
120
+ * @param {string|null} [envFilePath] - Path to .env file for --env-file (optional)
121
+ * @returns {Promise<number>} Exit code
122
+ */
123
+ function runDockerRunEphemeral(fullImage, cmd, envFilePath = null) {
124
+ return new Promise((resolve) => {
125
+ const args = ['run', '--rm', '-e', `TMPDIR=${TMPDIR_VALUE}`, '-e', `npm_config_store_dir=${PNPM_STORE_DIR}`, '-e', 'CI=true'];
126
+ if (envFilePath) {
127
+ args.push('--env-file', envFilePath);
128
+ }
129
+ args.push(fullImage, 'sh', '-c', cmd);
130
+ const proc = spawn('docker', args, {
131
+ stdio: 'inherit',
132
+ shell: false
133
+ });
134
+ proc.on('close', code => resolve(code !== null ? code : 1));
135
+ proc.on('error', () => resolve(1));
136
+ });
137
+ }
138
+
139
+ /**
140
+ * Run install for a builder app. DEV: exec in running container; TST: ephemeral with resolved .env.
141
+ * @param {string} appName - Application name
142
+ * @param {Object} [options] - { env: 'dev'|'tst' }
143
+ * @returns {Promise<void>}
144
+ */
145
+ async function runAppInstall(appName, options = {}) {
146
+ const env = (options.env || 'dev').toLowerCase();
147
+ if (env !== 'dev' && env !== 'tst') {
148
+ throw new Error('--env must be dev or tst');
149
+ }
150
+ const appConfig = loadAppConfig(appName);
151
+ const installCmd = getInstallCommand(appConfig);
152
+ const developerId = await config.getDeveloperId();
153
+ const imageName = composeGenerator.getImageName(appConfig, appName);
154
+ const imageTag = (appConfig.image && appConfig.image.tag) ? appConfig.image.tag : 'latest';
155
+ const fullImage = `${imageName}:${imageTag}`;
156
+
157
+ if (env === 'dev') {
158
+ const code = await runInstallInDev(appName, developerId, installCmd);
159
+ if (code !== 0) process.exit(code);
160
+ logger.log(chalk.green('✓ Install completed'));
161
+ return;
162
+ }
163
+
164
+ logger.log(chalk.blue(`Running install in ephemeral container (${fullImage}): ${installCmd}\n`));
165
+ const envFilePath = await secretsEnvWrite.resolveAndWriteEnvFile(appName, {});
166
+ const cmd = installCommandForReadOnlyApp(installCmd);
167
+ const code = await runDockerRunEphemeral(fullImage, cmd, envFilePath);
168
+ if (code !== 0) process.exit(code);
169
+ logger.log(chalk.green('✓ Install completed'));
170
+ }
171
+
172
+ module.exports = { runAppInstall, getInstallCommand };
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Shell command – open an interactive shell in the app container (docker exec -it).
3
+ *
4
+ * @fileoverview App shell command implementation
5
+ * @author AI Fabrix Team
6
+ * @version 2.0.0
7
+ */
8
+
9
+ const { spawn } = require('child_process');
10
+ const chalk = require('chalk');
11
+ const logger = require('../utils/logger');
12
+ const config = require('../core/config');
13
+ const containerHelpers = require('../utils/app-run-containers');
14
+ const pathsUtil = require('../utils/paths');
15
+ const secretsEnvWrite = require('../core/secrets-env-write');
16
+
17
+ /**
18
+ * Load application config for builder app (used to ensure app exists).
19
+ * @param {string} appName - Application name
20
+ * @returns {Object} Application config
21
+ */
22
+ function loadAppConfig(appName) {
23
+ const { loadConfigFile } = require('../utils/config-format');
24
+ const builderPath = pathsUtil.getBuilderPath(appName);
25
+ const configPath = pathsUtil.resolveApplicationConfigPath(builderPath);
26
+ return loadConfigFile(configPath);
27
+ }
28
+
29
+ /**
30
+ * Run interactive shell in the application container. Uses sh.
31
+ * Resolves app .env (including NPM_TOKEN/PYPI_TOKEN from kv://) and passes it via --env-file so the shell has registry tokens.
32
+ * @param {string} appName - Application name
33
+ * @param {Object} [options] - Options (e.g. --env for future remote)
34
+ * @returns {Promise<void>} Resolves when shell exits
35
+ */
36
+ async function runAppShell(appName, _options = {}) {
37
+ loadAppConfig(appName);
38
+
39
+ const developerId = await config.getDeveloperId();
40
+ const containerName = containerHelpers.getContainerName(appName, developerId);
41
+ const isRunning = await containerHelpers.checkContainerRunning(appName, developerId);
42
+
43
+ if (!isRunning) {
44
+ throw new Error(
45
+ `Container ${containerName} is not running.\nRun 'aifabrix run ${appName}' first.`
46
+ );
47
+ }
48
+
49
+ logger.log(chalk.blue(`Opening shell in ${containerName} (exit with 'exit' or Ctrl+D)...\n`));
50
+
51
+ const envFilePath = await secretsEnvWrite.resolveAndWriteEnvFile(appName, {});
52
+ const proc = spawn('docker', [
53
+ 'exec', '-it',
54
+ '--env-file', envFilePath,
55
+ '-e', 'TMPDIR=/tmp',
56
+ containerName,
57
+ 'sh'
58
+ ], {
59
+ stdio: 'inherit',
60
+ shell: false
61
+ });
62
+
63
+ return new Promise((resolve, reject) => {
64
+ proc.on('close', code => {
65
+ if (code !== 0 && code !== null) {
66
+ reject(new Error(`Shell exited with code ${code}`));
67
+ } else {
68
+ resolve();
69
+ }
70
+ });
71
+ proc.on('error', err => reject(err));
72
+ });
73
+ }
74
+
75
+ module.exports = { runAppShell };