@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.
- package/.cursor/rules/docs-rules.mdc +30 -0
- package/README.md +7 -5
- package/integration/hubspot/README.md +8 -4
- package/integration/hubspot/application.json +54 -0
- package/integration/hubspot/create-hubspot.js +9 -136
- package/integration/hubspot/env.template +3 -4
- package/integration/hubspot/hubspot-datasource-company.json +343 -5
- package/integration/hubspot/hubspot-datasource-contact.json +413 -5
- package/integration/hubspot/hubspot-datasource-deal.json +341 -4
- package/integration/hubspot/hubspot-datasource-users.json +116 -0
- package/integration/hubspot/hubspot-deploy.json +1250 -108
- package/integration/hubspot/hubspot-system.json +15 -32
- package/integration/hubspot/test-dataplane-down-tests.js +17 -16
- package/integration/hubspot/test-dataplane-down.js +2 -2
- package/integration/hubspot/test.js +1 -1
- package/jest.config.manual.js +2 -1
- package/lib/api/credential.api.js +40 -0
- package/lib/api/dev.api.js +423 -0
- package/lib/api/external-test.api.js +111 -0
- package/lib/api/index.js +42 -19
- package/lib/api/pipeline.api.js +66 -120
- package/lib/api/types/credential.types.js +23 -0
- package/lib/api/types/dev.types.js +140 -0
- package/lib/api/types/pipeline.types.js +37 -0
- package/lib/api/wizard-platform.api.js +61 -0
- package/lib/api/wizard.api.js +34 -1
- package/lib/app/config.js +44 -11
- package/lib/app/down.js +2 -1
- package/lib/app/index.js +12 -1
- package/lib/app/prompts.js +44 -29
- package/lib/app/push.js +36 -12
- package/lib/app/readme.js +9 -6
- package/lib/app/run-env-compose.js +264 -0
- package/lib/app/run-helpers.js +121 -118
- package/lib/app/run.js +148 -28
- package/lib/app/show-display.js +1 -1
- package/lib/app/show.js +5 -2
- package/lib/build/index.js +11 -3
- package/lib/cli/setup-app.js +172 -15
- package/lib/cli/setup-credential-deployment.js +31 -6
- package/lib/cli/setup-dev.js +206 -16
- package/lib/cli/setup-environment.js +16 -6
- package/lib/cli/setup-external-system.js +89 -24
- package/lib/cli/setup-infra.js +82 -15
- package/lib/cli/setup-secrets.js +52 -5
- package/lib/cli/setup-utility.js +129 -24
- package/lib/commands/app-install.js +172 -0
- package/lib/commands/app-shell.js +75 -0
- package/lib/commands/app-test.js +282 -0
- package/lib/commands/app.js +1 -1
- package/lib/commands/credential-env.js +162 -0
- package/lib/commands/credential-list.js +17 -22
- package/lib/commands/credential-push.js +96 -0
- package/lib/commands/datasource.js +77 -6
- package/lib/commands/dev-cli-handlers.js +141 -0
- package/lib/commands/dev-down.js +114 -0
- package/lib/commands/dev-init.js +347 -0
- package/lib/commands/repair-auth-config.js +99 -0
- package/lib/commands/repair-datasource-keys.js +208 -0
- package/lib/commands/repair-datasource.js +235 -0
- package/lib/commands/repair-env-template.js +348 -0
- package/lib/commands/repair-internal.js +85 -0
- package/lib/commands/repair-rbac.js +158 -0
- package/lib/commands/repair.js +507 -0
- package/lib/commands/secrets-list.js +118 -0
- package/lib/commands/secrets-remove.js +97 -0
- package/lib/commands/secrets-set.js +30 -17
- package/lib/commands/secrets-validate.js +50 -0
- package/lib/commands/test-e2e-external.js +165 -0
- package/lib/commands/up-dataplane.js +2 -2
- package/lib/commands/up-miso.js +0 -25
- package/lib/commands/upload.js +96 -40
- package/lib/commands/wizard-core-helpers.js +226 -4
- package/lib/commands/wizard-core.js +67 -29
- package/lib/commands/wizard-dataplane.js +1 -1
- package/lib/commands/wizard-entity-selection.js +43 -0
- package/lib/commands/wizard-headless.js +44 -5
- package/lib/commands/wizard-helpers.js +7 -3
- package/lib/commands/wizard.js +86 -64
- package/lib/core/admin-secrets.js +96 -0
- package/lib/core/config.js +7 -1
- package/lib/core/secrets-ensure.js +378 -0
- package/lib/core/secrets-env-write.js +157 -0
- package/lib/core/secrets.js +176 -89
- package/lib/datasource/deploy.js +12 -3
- package/lib/datasource/field-reference-validator.js +91 -0
- package/lib/datasource/test-e2e.js +219 -0
- package/lib/datasource/test-integration.js +154 -0
- package/lib/datasource/validate.js +21 -3
- package/lib/deployment/deployer.js +7 -5
- package/lib/deployment/environment-config.js +137 -0
- package/lib/deployment/environment.js +21 -98
- package/lib/deployment/push.js +32 -2
- package/lib/external-system/download.js +188 -203
- package/lib/external-system/generator.js +204 -56
- package/lib/external-system/test-auth.js +7 -3
- package/lib/external-system/test-execution.js +2 -1
- package/lib/external-system/test-system-level.js +73 -0
- package/lib/external-system/test.js +56 -19
- package/lib/generator/external-controller-manifest.js +29 -2
- package/lib/generator/external-schema-utils.js +1 -1
- package/lib/generator/external.js +10 -3
- package/lib/generator/index.js +177 -25
- package/lib/generator/split-readme.js +1 -0
- package/lib/generator/split-variables.js +7 -1
- package/lib/generator/split.js +194 -54
- package/lib/generator/wizard-prompts-secondary.js +294 -0
- package/lib/generator/wizard-prompts.js +105 -106
- package/lib/generator/wizard-readme.js +88 -0
- package/lib/generator/wizard.js +155 -158
- package/lib/infrastructure/compose.js +11 -1
- package/lib/infrastructure/helpers.js +103 -20
- package/lib/infrastructure/index.js +98 -12
- package/lib/infrastructure/services.js +88 -22
- package/lib/schema/application-schema.json +32 -8
- package/lib/schema/external-datasource.schema.json +49 -26
- package/lib/schema/external-system.schema.json +509 -411
- package/lib/schema/wizard-config.schema.json +16 -0
- package/lib/utils/api.js +41 -13
- package/lib/utils/app-register-auth.js +25 -3
- package/lib/utils/auth-headers.js +8 -7
- package/lib/utils/cli-utils.js +20 -0
- package/lib/utils/compose-generator.js +77 -76
- package/lib/utils/compose-handlebars-helpers.js +54 -0
- package/lib/utils/compose-vector-helper.js +18 -0
- package/lib/utils/config-format-preference.js +51 -0
- package/lib/utils/config-format.js +36 -0
- package/lib/utils/config-paths.js +127 -2
- package/lib/utils/configuration-env-resolver.js +179 -0
- package/lib/utils/credential-display.js +83 -0
- package/lib/utils/credential-secrets-env.js +357 -0
- package/lib/utils/dataplane-pipeline-warning.js +28 -0
- package/lib/utils/deployment-validation-helpers.js +4 -4
- package/lib/utils/dev-ca-install.js +139 -0
- package/lib/utils/dev-cert-helper.js +122 -0
- package/lib/utils/device-code-helpers.js +224 -0
- package/lib/utils/device-code.js +37 -336
- package/lib/utils/docker-build.js +40 -8
- package/lib/utils/env-copy.js +103 -13
- package/lib/utils/env-map.js +35 -5
- package/lib/utils/env-template.js +6 -5
- package/lib/utils/error-formatters/http-status-errors.js +20 -2
- package/lib/utils/error-formatters/permission-errors.js +0 -1
- package/lib/utils/error-formatters/validation-errors.js +0 -1
- package/lib/utils/external-readme.js +56 -29
- package/lib/utils/external-system-display.js +59 -1
- package/lib/utils/external-system-test-helpers.js +21 -8
- package/lib/utils/external-system-validators.js +3 -0
- package/lib/utils/file-upload.js +20 -50
- package/lib/utils/help-builder.js +16 -2
- package/lib/utils/infra-status.js +80 -45
- package/lib/utils/local-secrets.js +7 -52
- package/lib/utils/mutagen-install.js +195 -0
- package/lib/utils/mutagen.js +146 -0
- package/lib/utils/paths.js +128 -37
- package/lib/utils/port-resolver.js +28 -16
- package/lib/utils/remote-dev-auth.js +38 -0
- package/lib/utils/remote-docker-env.js +43 -0
- package/lib/utils/remote-secrets-loader.js +60 -0
- package/lib/utils/secrets-canonical.js +93 -0
- package/lib/utils/secrets-generator.js +114 -6
- package/lib/utils/secrets-helpers.js +108 -114
- package/lib/utils/secrets-path.js +2 -2
- package/lib/utils/secrets-utils.js +52 -1
- package/lib/utils/secrets-validation.js +84 -0
- package/lib/utils/ssh-key-helper.js +116 -0
- package/lib/utils/test-log-writer.js +56 -0
- package/lib/utils/token-manager-messages.js +90 -0
- package/lib/utils/token-manager.js +29 -36
- package/lib/utils/variable-transformer.js +3 -3
- package/lib/validation/env-template-auth.js +157 -0
- package/lib/validation/env-template-kv.js +41 -0
- package/lib/validation/external-manifest-validator.js +25 -0
- package/lib/validation/external-system-auth-rules.js +86 -0
- package/lib/validation/validate-batch.js +149 -0
- package/lib/validation/validate-datasource-keys-api.js +33 -0
- package/lib/validation/validate-display.js +94 -16
- package/lib/validation/validate.js +25 -12
- package/lib/validation/validator.js +72 -9
- package/lib/validation/wizard-datasource-validation.js +50 -0
- package/package.json +8 -3
- package/scripts/install-local.js +34 -15
- package/templates/README.md +0 -1
- package/templates/applications/README.md.hbs +4 -4
- package/templates/applications/dataplane/application.yaml +6 -5
- package/templates/applications/dataplane/env.template +15 -10
- package/templates/applications/dataplane/rbac.yaml +2 -2
- package/templates/applications/keycloak/env.template +2 -0
- package/templates/applications/miso-controller/application.yaml +1 -0
- package/templates/applications/miso-controller/env.template +12 -10
- package/templates/external-system/README.md.hbs +65 -25
- package/templates/external-system/deploy.js.hbs +4 -2
- package/templates/external-system/external-datasource.yaml.hbs +217 -0
- package/templates/external-system/external-system.json.hbs +1 -18
- package/templates/infra/compose.yaml.hbs +6 -0
- package/templates/python/docker-compose.hbs +49 -23
- package/templates/typescript/docker-compose.hbs +48 -22
- package/integration/hubspot/application.yaml +0 -37
package/lib/cli/setup-secrets.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* CLI
|
|
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
|
|
19
|
-
.command('
|
|
37
|
+
const secretCmd = program
|
|
38
|
+
.command('secret')
|
|
20
39
|
.description('Manage secrets in secrets files');
|
|
21
40
|
|
|
22
|
-
|
|
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, '
|
|
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)')
|
package/lib/cli/setup-utility.js
CHANGED
|
@@ -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.
|
|
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
|
|
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 (
|
|
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
|
|
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:
|
|
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
|
|
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
|
-
.
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
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 = {
|
|
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 };
|