@aifabrix/builder 2.44.4 ā 2.44.6
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/cli-layout.mdc +1 -1
- package/.cursor/rules/project-rules.mdc +1 -1
- package/.npmrc.token +1 -1
- package/README.md +15 -23
- package/integration/hubspot-test/README.md +2 -0
- package/integration/hubspot-test/test.js +5 -3
- package/jest.projects.js +68 -17
- package/lib/api/controller-health.api.js +49 -0
- package/lib/api/dimension-values.api.js +82 -0
- package/lib/api/dimensions.api.js +114 -0
- package/lib/api/external-systems.api.js +1 -0
- package/lib/api/integration-clients.api.js +168 -0
- package/lib/api/types/dimension-values.types.js +28 -0
- package/lib/api/types/dimensions.types.js +31 -0
- package/lib/api/types/integration-clients.types.js +45 -0
- package/lib/api/types/wizard.types.js +2 -1
- package/lib/api/validation-runner.js +46 -25
- package/lib/app/deploy-config.js +11 -1
- package/lib/app/deploy-status-display.js +3 -3
- package/lib/app/deploy.js +36 -14
- package/lib/app/display.js +15 -11
- package/lib/app/push.js +46 -23
- package/lib/app/register.js +1 -1
- package/lib/app/restart-display.js +95 -0
- package/lib/app/rotate-secret.js +1 -1
- package/lib/app/run-container-start.js +12 -6
- package/lib/app/run-env-compose.js +30 -1
- package/lib/app/run-helpers.js +44 -12
- package/lib/app/run-reload-sync.js +148 -0
- package/lib/app/run-resolve-image.js +51 -1
- package/lib/app/run.js +99 -73
- package/lib/build/index.js +75 -45
- package/lib/cli/doctor-check.js +117 -0
- package/lib/cli/index.js +8 -2
- package/lib/cli/infra-guided.js +445 -0
- package/lib/cli/setup-app.help.js +1 -1
- package/lib/cli/setup-app.js +20 -2
- package/lib/cli/setup-app.test-commands.js +9 -5
- package/lib/cli/setup-auth.js +26 -0
- package/lib/cli/setup-dev-path-commands.js +50 -3
- package/lib/cli/setup-infra.js +138 -61
- package/lib/cli/setup-integration-client.js +182 -0
- package/lib/cli/setup-parameters.js +21 -2
- package/lib/cli/setup-platform.js +102 -0
- package/lib/cli/setup-secrets.js +18 -6
- package/lib/cli/setup-utility.js +97 -33
- package/lib/commands/datasource-capability-dimension-cli.js +128 -0
- package/lib/commands/datasource-capability-output.js +29 -0
- package/lib/commands/datasource-capability-relate-cli.js +140 -0
- package/lib/commands/datasource-capability.js +411 -0
- package/lib/commands/datasource-unified-test-cli.options.js +1 -1
- package/lib/commands/datasource.js +53 -13
- package/lib/commands/dev-down.js +3 -3
- package/lib/commands/dev-infra-gate.js +32 -0
- package/lib/commands/dev-init.js +13 -7
- package/lib/commands/dimension-value.js +179 -0
- package/lib/commands/dimension.js +330 -0
- package/lib/commands/integration-client.js +430 -0
- package/lib/commands/login-device.js +65 -30
- package/lib/commands/login.js +21 -10
- package/lib/commands/parameters-validate.js +78 -13
- package/lib/commands/repair-datasource-auto-rbac.js +166 -0
- package/lib/commands/repair-datasource-keys.js +10 -5
- package/lib/commands/repair-datasource.js +19 -7
- package/lib/commands/repair-env-template.js +4 -1
- package/lib/commands/repair-openapi-sync.js +172 -0
- package/lib/commands/repair-persist.js +102 -0
- package/lib/commands/repair-rbac-extract.js +27 -0
- package/lib/commands/repair-rbac-migrate.js +186 -0
- package/lib/commands/repair-rbac.js +225 -19
- package/lib/commands/repair-system-alignment.js +246 -0
- package/lib/commands/repair-system-permissions.js +168 -0
- package/lib/commands/repair.js +120 -354
- package/lib/commands/secure.js +1 -1
- package/lib/commands/setup-modes.js +455 -0
- package/lib/commands/setup-prompts.js +388 -0
- package/lib/commands/setup.js +149 -0
- package/lib/commands/teardown.js +228 -0
- package/lib/commands/test-e2e-external.js +4 -3
- package/lib/commands/up-common.js +97 -12
- package/lib/commands/up-dataplane.js +33 -11
- package/lib/commands/up-miso.js +7 -11
- package/lib/commands/upload.js +109 -23
- package/lib/commands/wizard-core-helpers.js +14 -11
- package/lib/commands/wizard-core.js +58 -15
- package/lib/commands/wizard-dataplane.js +2 -2
- package/lib/commands/wizard-entity-selection.js +72 -14
- package/lib/commands/wizard-headless.js +7 -3
- package/lib/commands/wizard-helpers.js +13 -1
- package/lib/commands/wizard.js +210 -61
- package/lib/constants/infra-compose-service-names.js +40 -0
- package/lib/core/env-reader.js +16 -3
- package/lib/core/secrets-admin-env.js +101 -0
- package/lib/core/secrets-ensure-infra.js +34 -1
- package/lib/core/secrets-ensure.js +88 -66
- package/lib/core/secrets-env-content.js +432 -0
- package/lib/core/secrets-env-write.js +27 -1
- package/lib/core/secrets-load.js +248 -0
- package/lib/core/secrets-names.js +32 -0
- package/lib/core/secrets.js +17 -757
- package/lib/datasource/capability/basic-exposure.js +76 -0
- package/lib/datasource/capability/capability-diff-slice.js +41 -0
- package/lib/datasource/capability/capability-key.js +34 -0
- package/lib/datasource/capability/capability-resolve.js +172 -0
- package/lib/datasource/capability/capability-storage-keys.js +22 -0
- package/lib/datasource/capability/copy-operations.js +348 -0
- package/lib/datasource/capability/copy-test-payload.js +139 -0
- package/lib/datasource/capability/create-operations.js +235 -0
- package/lib/datasource/capability/dimension-operations.js +151 -0
- package/lib/datasource/capability/dimension-validate.js +219 -0
- package/lib/datasource/capability/json-pointer.js +31 -0
- package/lib/datasource/capability/reference-rewrite.js +51 -0
- package/lib/datasource/capability/relate-operations.js +325 -0
- package/lib/datasource/capability/relate-validate.js +219 -0
- package/lib/datasource/capability/remove-operations.js +275 -0
- package/lib/datasource/capability/run-capability-copy.js +152 -0
- package/lib/datasource/capability/run-capability-diff.js +135 -0
- package/lib/datasource/capability/run-capability-dimension.js +291 -0
- package/lib/datasource/capability/run-capability-edit.js +377 -0
- package/lib/datasource/capability/run-capability-relate.js +193 -0
- package/lib/datasource/capability/run-capability-remove.js +105 -0
- package/lib/datasource/capability/templates/minimal-fetch.json +18 -0
- package/lib/datasource/capability/validate-capability-slice.js +35 -0
- package/lib/datasource/list.js +136 -23
- package/lib/datasource/log-viewer.js +2 -4
- package/lib/datasource/unified-validation-run.js +51 -16
- package/lib/datasource/validate.js +53 -1
- package/lib/deployment/deploy-poll-ui.js +60 -0
- package/lib/deployment/deployer-status.js +29 -3
- package/lib/deployment/deployer.js +48 -30
- package/lib/deployment/environment.js +7 -2
- package/lib/deployment/poll-interval.js +72 -0
- package/lib/deployment/push.js +11 -9
- package/lib/external-system/deploy.js +4 -1
- package/lib/external-system/download.js +61 -32
- package/lib/external-system/sync-deploy-manifest.js +33 -0
- package/lib/generator/wizard-prompts.js +7 -1
- package/lib/generator/wizard.js +34 -0
- package/lib/infrastructure/index.js +49 -19
- package/lib/infrastructure/orphan-infra-docker-teardown.js +177 -0
- package/lib/parameters/infra-kv-discovery.js +29 -4
- package/lib/parameters/infra-parameter-catalog.js +6 -3
- package/lib/parameters/infra-parameter-validate.js +67 -19
- package/lib/resolvers/datasource-resolver.js +53 -0
- package/lib/resolvers/dimension-file.js +52 -0
- package/lib/resolvers/manifest-resolver.js +133 -0
- package/lib/schema/external-datasource.schema.json +183 -53
- package/lib/schema/external-system.schema.json +23 -10
- package/lib/schema/infra.parameter.yaml +26 -11
- package/lib/schema/wizard-config.schema.json +2 -2
- package/lib/utils/aifabrix-config-dir-walk.js +40 -0
- package/lib/utils/aifabrix-runtime-config-dir.js +26 -3
- package/lib/utils/app-run-containers.js +2 -2
- package/lib/utils/bash-secret-env.js +59 -0
- package/lib/utils/cli-secrets-error-format.js +78 -0
- package/lib/utils/cli-test-layout-chalk.js +31 -9
- package/lib/utils/cli-utils.js +4 -36
- package/lib/utils/datasource-test-run-display.js +8 -0
- package/lib/utils/dev-hosts-helper.js +3 -2
- package/lib/utils/dev-init-ssh-merge.js +2 -1
- package/lib/utils/docker-build.js +17 -9
- package/lib/utils/docker-reload-mount.js +127 -0
- package/lib/utils/external-readme.js +117 -4
- package/lib/utils/external-system-local-test-tty.js +3 -2
- package/lib/utils/external-system-readiness-core.js +45 -12
- package/lib/utils/external-system-readiness-deploy-display.js +3 -3
- package/lib/utils/external-system-readiness-display-internals.js +33 -3
- package/lib/utils/external-system-readiness-display.js +10 -1
- package/lib/utils/file-upload.js +40 -3
- package/lib/utils/health-check-db-init.js +107 -0
- package/lib/utils/health-check-public-warn.js +69 -0
- package/lib/utils/health-check-url.js +19 -4
- package/lib/utils/health-check.js +135 -105
- package/lib/utils/help-builder.js +5 -1
- package/lib/utils/image-name.js +34 -7
- package/lib/utils/integration-file-backup.js +74 -0
- package/lib/utils/mutagen-install.js +30 -3
- package/lib/utils/paths.js +108 -25
- package/lib/utils/postgres-wipe.js +212 -0
- package/lib/utils/register-aifabrix-shell-env.js +15 -0
- package/lib/utils/remote-dev-auth.js +21 -5
- package/lib/utils/remote-docker-env.js +9 -1
- package/lib/utils/remote-secrets-loader.js +42 -3
- package/lib/utils/resolve-docker-image-ref.js +9 -3
- package/lib/utils/secrets-ancestor-paths.js +47 -0
- package/lib/utils/secrets-helpers.js +17 -10
- package/lib/utils/secrets-kv-refs.js +42 -0
- package/lib/utils/secrets-kv-scope.js +19 -2
- package/lib/utils/secrets-materialize-local.js +134 -0
- package/lib/utils/secrets-path.js +24 -10
- package/lib/utils/secrets-utils.js +2 -2
- package/lib/utils/system-builder-root.js +34 -0
- package/lib/utils/url-declarative-resolve-build.js +6 -1
- package/lib/utils/url-declarative-runtime-base-path.js +32 -0
- package/lib/utils/url-declarative-vdir-inactive-env.js +2 -1
- package/lib/utils/urls-local-registry.js +73 -20
- package/lib/utils/validation-poll-ui.js +81 -0
- package/lib/utils/validation-run-poll.js +29 -5
- package/lib/utils/with-muted-logger.js +53 -0
- package/package.json +1 -1
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/rbac.yaml +10 -10
- package/templates/applications/keycloak/env.template +8 -6
- package/templates/applications/miso-controller/application.yaml +7 -0
- package/templates/applications/miso-controller/env.template +7 -7
- package/templates/applications/miso-controller/rbac.yaml +9 -9
- package/templates/external-system/README.md.hbs +89 -102
- package/.nyc_output/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
- package/.nyc_output/processinfo/55e9d034-ddab-4579-a706-e02a91d75c91.json +0 -1
- package/.nyc_output/processinfo/index.json +0 -1
- package/lib/api/service-users.api.js +0 -150
- package/lib/api/types/service-users.types.js +0 -65
- package/lib/cli/setup-service-user.js +0 -187
- package/lib/commands/service-user.js +0 -429
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { formatSuccessLine } = require('../utils/cli-test-layout-chalk');
|
|
1
|
+
const { formatSuccessLine, formatProgress, metadata } = require('../utils/cli-test-layout-chalk');
|
|
2
2
|
/**
|
|
3
3
|
* AI Fabrix Builder Deployment Module
|
|
4
4
|
*
|
|
@@ -22,6 +22,8 @@ const {
|
|
|
22
22
|
convertToPipelineAuthConfig,
|
|
23
23
|
processDeploymentStatusResponse
|
|
24
24
|
} = require('./deployer-status');
|
|
25
|
+
const { resolvePollIntervalFromController } = require('./poll-interval');
|
|
26
|
+
const { createDeployPollHandlers } = require('./deploy-poll-ui');
|
|
25
27
|
|
|
26
28
|
/**
|
|
27
29
|
* For external systems, send full manifest (application + inline system + full dataSources).
|
|
@@ -47,7 +49,10 @@ function transformExternalManifestForPipeline(manifest) {
|
|
|
47
49
|
* @returns {Promise<Object>} Object with validationData, pipelineAuthConfig, and useBearerOnly
|
|
48
50
|
*/
|
|
49
51
|
async function buildValidationData(manifest, validatedEnvKey, authConfig, options) {
|
|
50
|
-
const repositoryUrl =
|
|
52
|
+
const repositoryUrl =
|
|
53
|
+
options.repositoryUrl ||
|
|
54
|
+
manifest?.repository?.repositoryUrl ||
|
|
55
|
+
`https://github.com/aifabrix/${manifest.key}`;
|
|
51
56
|
|
|
52
57
|
if (authConfig.type === 'bearer' && authConfig.token && !authConfig.clientId) {
|
|
53
58
|
const pipelineAuthConfig = { type: 'bearer', token: authConfig.token };
|
|
@@ -295,10 +300,19 @@ async function pollDeploymentStatus(deploymentId, controllerUrl, envKey, authCon
|
|
|
295
300
|
const validatedEnvKey = validateEnvironmentKey(envKey);
|
|
296
301
|
const pipelineAuthConfig = convertToPipelineAuthConfig(authConfig);
|
|
297
302
|
|
|
303
|
+
const onPollProgress = typeof options.onPollProgress === 'function' ? options.onPollProgress : null;
|
|
304
|
+
|
|
298
305
|
for (let attempt = 0; attempt < maxAttempts; attempt++) {
|
|
299
306
|
try {
|
|
300
307
|
const response = await getPipelineDeployment(controllerUrl, validatedEnvKey, deploymentId, pipelineAuthConfig);
|
|
301
|
-
const deploymentData = await processDeploymentStatusResponse(
|
|
308
|
+
const deploymentData = await processDeploymentStatusResponse(
|
|
309
|
+
response,
|
|
310
|
+
attempt,
|
|
311
|
+
maxAttempts,
|
|
312
|
+
interval,
|
|
313
|
+
deploymentId,
|
|
314
|
+
onPollProgress
|
|
315
|
+
);
|
|
302
316
|
if (deploymentData) {
|
|
303
317
|
return deploymentData;
|
|
304
318
|
}
|
|
@@ -354,7 +368,7 @@ async function sendDeployment(url, validatedEnvKey, manifest, authConfig, option
|
|
|
354
368
|
await ensureBearerTokenValid(url, authConfig);
|
|
355
369
|
|
|
356
370
|
// Step 1: Validate deployment
|
|
357
|
-
logger.log(
|
|
371
|
+
logger.log(formatProgress('Validating deployment configuration...'));
|
|
358
372
|
const validateResult = await validateDeployment(url, validatedEnvKey, manifest, authConfig, {
|
|
359
373
|
repositoryUrl: options.repositoryUrl,
|
|
360
374
|
controllerId: options.controllerId,
|
|
@@ -368,11 +382,13 @@ async function sendDeployment(url, validatedEnvKey, manifest, authConfig, option
|
|
|
368
382
|
|
|
369
383
|
logger.log(formatSuccessLine('Validation successful'));
|
|
370
384
|
if (validateResult.draftDeploymentId) {
|
|
371
|
-
logger.log(
|
|
385
|
+
logger.log(metadata(`Draft Deployment ID: ${validateResult.draftDeploymentId}`));
|
|
372
386
|
}
|
|
373
387
|
|
|
374
|
-
// Step 2: Deploy using validateToken
|
|
375
|
-
|
|
388
|
+
// Step 2: Deploy using validateToken (when not polling, show a clear line; otherwise poll phase uses ora)
|
|
389
|
+
if (options.poll === false) {
|
|
390
|
+
logger.log(formatProgress('Deploying application...'));
|
|
391
|
+
}
|
|
376
392
|
const result = await sendDeploymentRequest(url, validatedEnvKey, validateResult.validateToken, authConfig, {
|
|
377
393
|
imageTag: options.imageTag || 'latest',
|
|
378
394
|
timeout: options.timeout || 30000,
|
|
@@ -386,35 +402,37 @@ async function sendDeployment(url, validatedEnvKey, manifest, authConfig, option
|
|
|
386
402
|
return result;
|
|
387
403
|
}
|
|
388
404
|
|
|
389
|
-
/**
|
|
390
|
-
* Polls deployment status if enabled
|
|
391
|
-
* @async
|
|
392
|
-
* @param {Object} result - Deployment result
|
|
393
|
-
* @param {string} url - Controller URL
|
|
394
|
-
* @param {string} validatedEnvKey - Validated environment key
|
|
395
|
-
* @param {Object} authConfig - Authentication configuration
|
|
396
|
-
* @param {Object} options - Deployment options
|
|
397
|
-
* @returns {Promise<Object>} Deployment result with status
|
|
398
|
-
*/
|
|
405
|
+
/** Poll pipeline deployment to terminal status (TTY: ora spinner). */
|
|
399
406
|
async function pollDeployment(result, url, validatedEnvKey, authConfig, options) {
|
|
400
407
|
if (!options.poll || !result.deploymentId) {
|
|
401
408
|
return result;
|
|
402
409
|
}
|
|
403
410
|
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
result.deploymentId,
|
|
407
|
-
url,
|
|
408
|
-
validatedEnvKey,
|
|
409
|
-
authConfig,
|
|
410
|
-
{
|
|
411
|
-
interval: options.pollInterval || 5000,
|
|
412
|
-
maxAttempts: options.pollMaxAttempts || 60
|
|
413
|
-
}
|
|
414
|
-
);
|
|
411
|
+
// Separate validation/draft block from deploy polling (matches guided CLI spacing)
|
|
412
|
+
logger.log('');
|
|
415
413
|
|
|
416
|
-
|
|
417
|
-
|
|
414
|
+
const resolvedInterval = await resolvePollIntervalFromController(url, options.pollInterval);
|
|
415
|
+
const maxAttempts = options.pollMaxAttempts || 60;
|
|
416
|
+
const pollUi = createDeployPollHandlers(maxAttempts, { silent: options.silentPoll === true });
|
|
417
|
+
|
|
418
|
+
try {
|
|
419
|
+
const status = await pollDeploymentStatus(
|
|
420
|
+
result.deploymentId,
|
|
421
|
+
url,
|
|
422
|
+
validatedEnvKey,
|
|
423
|
+
authConfig,
|
|
424
|
+
{
|
|
425
|
+
interval: resolvedInterval,
|
|
426
|
+
maxAttempts,
|
|
427
|
+
onPollProgress: pollUi.onPollProgress
|
|
428
|
+
}
|
|
429
|
+
);
|
|
430
|
+
|
|
431
|
+
result.status = status;
|
|
432
|
+
return result;
|
|
433
|
+
} finally {
|
|
434
|
+
pollUi.finish();
|
|
435
|
+
}
|
|
418
436
|
}
|
|
419
437
|
|
|
420
438
|
/**
|
|
@@ -378,14 +378,19 @@ async function executeEnvironmentDeployment(validatedControllerUrl, envKey, auth
|
|
|
378
378
|
async function pollDeploymentStatusIfEnabled(result, validatedControllerUrl, envKey, authConfig, options) {
|
|
379
379
|
const shouldPoll = options.poll !== false && !options.noPoll;
|
|
380
380
|
if (shouldPoll && result.deploymentId) {
|
|
381
|
+
const { resolvePollIntervalFromController } = require('./poll-interval');
|
|
382
|
+
const pollInterval = await resolvePollIntervalFromController(
|
|
383
|
+
validatedControllerUrl,
|
|
384
|
+
options.pollInterval
|
|
385
|
+
);
|
|
381
386
|
const pollResult = await pollEnvironmentStatus(
|
|
382
387
|
result.deploymentId,
|
|
383
388
|
validatedControllerUrl,
|
|
384
389
|
envKey,
|
|
385
390
|
authConfig,
|
|
386
391
|
{
|
|
387
|
-
pollInterval
|
|
388
|
-
maxAttempts: 60
|
|
392
|
+
pollInterval,
|
|
393
|
+
maxAttempts: options.pollMaxAttempts || 60
|
|
389
394
|
}
|
|
390
395
|
);
|
|
391
396
|
result.status = pollResult.status;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pipeline deployment polling intervals based on controller DEPLOYMENT mode.
|
|
3
|
+
*
|
|
4
|
+
* Miso-controller exposes `deploymentType` on GET /api/v1/health (azure | azure-mock | local | database).
|
|
5
|
+
* Local/database installs finish quickly; use a shorter poll interval unless the user overrides.
|
|
6
|
+
*
|
|
7
|
+
* @fileoverview Resolve poll interval from controller deployment mode
|
|
8
|
+
* @author AI Fabrix Team
|
|
9
|
+
* @version 2.0.0
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const FAST_POLL_MS = 1000;
|
|
13
|
+
const STANDARD_POLL_MS = 5000;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @param {string} [deploymentType] - From controller health: deploymentType
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
19
|
+
function isFastPollingDeploymentType(deploymentType) {
|
|
20
|
+
if (!deploymentType || typeof deploymentType !== 'string') {
|
|
21
|
+
return false;
|
|
22
|
+
}
|
|
23
|
+
const t = deploymentType.trim().toLowerCase();
|
|
24
|
+
return t === 'local' || t === 'database';
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} [deploymentType] - Controller deploymentType
|
|
29
|
+
* @param {number|string|undefined|null} explicitPollInterval - User/config override (ms)
|
|
30
|
+
* @returns {number}
|
|
31
|
+
*/
|
|
32
|
+
function resolvePollIntervalMs(deploymentType, explicitPollInterval) {
|
|
33
|
+
const n =
|
|
34
|
+
explicitPollInterval !== undefined && explicitPollInterval !== null
|
|
35
|
+
? Number(explicitPollInterval)
|
|
36
|
+
: NaN;
|
|
37
|
+
if (Number.isFinite(n) && n > 0) {
|
|
38
|
+
return n;
|
|
39
|
+
}
|
|
40
|
+
return isFastPollingDeploymentType(deploymentType) ? FAST_POLL_MS : STANDARD_POLL_MS;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Resolve poll interval: honor explicit ms when valid; else probe controller health for deploymentType.
|
|
45
|
+
* @param {string} controllerUrl - Miso controller base URL
|
|
46
|
+
* @param {number|string|undefined|null} explicitPollInterval - Optional CLI/config override
|
|
47
|
+
* @returns {Promise<number>}
|
|
48
|
+
*/
|
|
49
|
+
async function resolvePollIntervalFromController(controllerUrl, explicitPollInterval) {
|
|
50
|
+
const n =
|
|
51
|
+
explicitPollInterval !== undefined && explicitPollInterval !== null
|
|
52
|
+
? Number(explicitPollInterval)
|
|
53
|
+
: NaN;
|
|
54
|
+
if (Number.isFinite(n) && n > 0) {
|
|
55
|
+
return n;
|
|
56
|
+
}
|
|
57
|
+
try {
|
|
58
|
+
const { getControllerDeploymentType } = require('../api/controller-health.api');
|
|
59
|
+
const deploymentType = await getControllerDeploymentType(controllerUrl);
|
|
60
|
+
return resolvePollIntervalMs(deploymentType, undefined);
|
|
61
|
+
} catch {
|
|
62
|
+
return STANDARD_POLL_MS;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = {
|
|
67
|
+
FAST_POLL_MS,
|
|
68
|
+
STANDARD_POLL_MS,
|
|
69
|
+
isFastPollingDeploymentType,
|
|
70
|
+
resolvePollIntervalMs,
|
|
71
|
+
resolvePollIntervalFromController
|
|
72
|
+
};
|
package/lib/deployment/push.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
const { formatSuccessLine } = require('../utils/cli-test-layout-chalk');
|
|
1
|
+
const { formatSuccessLine, formatProgress } = require('../utils/cli-test-layout-chalk');
|
|
2
2
|
/**
|
|
3
3
|
* AI Fabrix Builder Push Utilities
|
|
4
4
|
*
|
|
@@ -150,8 +150,9 @@ function validateRegistryURL(registryUrl) {
|
|
|
150
150
|
async function checkACRAuthentication(registry) {
|
|
151
151
|
try {
|
|
152
152
|
const registryName = extractRegistryName(registry);
|
|
153
|
-
|
|
154
|
-
const
|
|
153
|
+
const { getDockerExecEnv } = require('../utils/remote-docker-env');
|
|
154
|
+
const env = await getDockerExecEnv();
|
|
155
|
+
const options = process.platform === 'win32' ? { shell: true, env } : { env };
|
|
155
156
|
await execAsync(`az acr show --name ${registryName}`, { ...options, timeout: AZ_ACR_SHOW_TIMEOUT_MS });
|
|
156
157
|
return true;
|
|
157
158
|
} catch (error) {
|
|
@@ -167,9 +168,10 @@ async function checkACRAuthentication(registry) {
|
|
|
167
168
|
async function authenticateACR(registry) {
|
|
168
169
|
try {
|
|
169
170
|
const registryName = extractRegistryName(registry);
|
|
170
|
-
logger.log(
|
|
171
|
-
|
|
172
|
-
const
|
|
171
|
+
logger.log(formatProgress(`Authenticating with ${registry}ā¦`));
|
|
172
|
+
const { getDockerExecEnv } = require('../utils/remote-docker-env');
|
|
173
|
+
const env = await getDockerExecEnv();
|
|
174
|
+
const options = process.platform === 'win32' ? { shell: true, env } : { env };
|
|
173
175
|
await execAsync(`az acr login --name ${registryName}`, { ...options, timeout: AZ_ACR_LOGIN_TIMEOUT_MS });
|
|
174
176
|
logger.log(formatSuccessLine(`Authenticated with ${registry}`));
|
|
175
177
|
} catch (error) {
|
|
@@ -192,7 +194,7 @@ async function authenticateACR(registry) {
|
|
|
192
194
|
*/
|
|
193
195
|
async function authenticateExternalRegistry(registry, username, password) {
|
|
194
196
|
try {
|
|
195
|
-
logger.log(
|
|
197
|
+
logger.log(formatProgress(`Authenticating with ${registry}ā¦`));
|
|
196
198
|
|
|
197
199
|
// Use cross-platform approach: write password to stdin directly
|
|
198
200
|
// This works on Windows, Linux, and macOS
|
|
@@ -262,7 +264,7 @@ async function tagImage(sourceImage, targetImage) {
|
|
|
262
264
|
try {
|
|
263
265
|
const { getDockerExecEnv } = require('../utils/remote-docker-env');
|
|
264
266
|
const env = await getDockerExecEnv();
|
|
265
|
-
logger.log(
|
|
267
|
+
logger.log(formatProgress(`Tagging ${sourceImage} ā ${targetImage}ā¦`));
|
|
266
268
|
await execAsync(`docker tag ${sourceImage} ${targetImage}`, { env });
|
|
267
269
|
logger.log(formatSuccessLine(`Tagged: ${targetImage}`));
|
|
268
270
|
} catch (error) {
|
|
@@ -280,7 +282,7 @@ async function pushImage(imageWithTag, registry = null) {
|
|
|
280
282
|
try {
|
|
281
283
|
const { getDockerExecEnv } = require('../utils/remote-docker-env');
|
|
282
284
|
const env = await getDockerExecEnv();
|
|
283
|
-
logger.log(
|
|
285
|
+
logger.log(formatProgress(`Pushing ${imageWithTag}ā¦`));
|
|
284
286
|
await execAsync(`docker push ${imageWithTag}`, { env });
|
|
285
287
|
logger.log(formatSuccessLine(`Pushed: ${imageWithTag}`));
|
|
286
288
|
} catch (error) {
|
|
@@ -29,6 +29,7 @@ const { validateExternalSystemComplete } = require('../validation/validate');
|
|
|
29
29
|
const { displayValidationResults } = require('../validation/validate-display');
|
|
30
30
|
const { maybeSyncSystemCertificationFromDataplane } = require('../certification/sync-system-certification');
|
|
31
31
|
const { cliOptsSkipCertSync } = require('../certification/cli-cert-sync-skip');
|
|
32
|
+
const { syncDeployJsonFromSources } = require('./sync-deploy-manifest');
|
|
32
33
|
|
|
33
34
|
/**
|
|
34
35
|
* Lists datasources for a system and loads system record for docs URLs.
|
|
@@ -152,7 +153,7 @@ function logImmediateControllerDeploymentOutcome(deploymentOutcome) {
|
|
|
152
153
|
logger.log(formatSuccessParagraph('Controller deployment OK'));
|
|
153
154
|
return;
|
|
154
155
|
}
|
|
155
|
-
logger.log(chalk.red('
|
|
156
|
+
logger.log(chalk.red('ā Controller deployment did not complete successfully'));
|
|
156
157
|
const parts = [deploymentOutcome.error, deploymentOutcome.message].filter(Boolean);
|
|
157
158
|
if (parts.length > 0) {
|
|
158
159
|
for (const line of parts) {
|
|
@@ -265,6 +266,8 @@ async function deployExternalSystem(appName, options = {}) {
|
|
|
265
266
|
|
|
266
267
|
logger.log(formatSuccessLine('Local validation passed, proceeding with deployment...'));
|
|
267
268
|
|
|
269
|
+
await syncDeployJsonFromSources(appName);
|
|
270
|
+
|
|
268
271
|
const manifest = await generateControllerManifest(appName, options);
|
|
269
272
|
|
|
270
273
|
const { environment, controllerUrl, authConfig } = await prepareDeploymentConfig(appName, options);
|
|
@@ -1,4 +1,12 @@
|
|
|
1
|
-
const {
|
|
1
|
+
const {
|
|
2
|
+
formatSuccessLine,
|
|
3
|
+
formatSuccessParagraph,
|
|
4
|
+
sectionTitle,
|
|
5
|
+
headerKeyValue,
|
|
6
|
+
metadata,
|
|
7
|
+
formatProgress,
|
|
8
|
+
formatWarningLine
|
|
9
|
+
} = require('../utils/cli-test-layout-chalk');
|
|
2
10
|
/**
|
|
3
11
|
* External System Download Module
|
|
4
12
|
*
|
|
@@ -24,7 +32,6 @@ const fsSync = require('fs');
|
|
|
24
32
|
const path = require('path');
|
|
25
33
|
const readline = require('readline');
|
|
26
34
|
const yaml = require('js-yaml');
|
|
27
|
-
const chalk = require('chalk');
|
|
28
35
|
const { getExternalSystemConfig } = require('../api/external-systems.api');
|
|
29
36
|
const { getDeploymentAuth, requireBearerForDataplanePipeline } = require('../utils/token-manager');
|
|
30
37
|
const { getConfig } = require('../core/config');
|
|
@@ -128,7 +135,7 @@ async function setupAuthenticationAndDataplane(systemKey, _options, _config) {
|
|
|
128
135
|
}
|
|
129
136
|
|
|
130
137
|
const { resolveDataplaneUrl } = require('../utils/dataplane-resolver');
|
|
131
|
-
logger.log(
|
|
138
|
+
logger.log(formatProgress('Resolving dataplane URLā¦'));
|
|
132
139
|
const dataplaneUrl = await resolveDataplaneUrl(controllerUrl, environment, authConfig);
|
|
133
140
|
logger.log(formatSuccessLine(`Dataplane URL: ${dataplaneUrl}`));
|
|
134
141
|
|
|
@@ -148,7 +155,7 @@ async function setupAuthenticationAndDataplane(systemKey, _options, _config) {
|
|
|
148
155
|
* @throws {Error} If download fails
|
|
149
156
|
*/
|
|
150
157
|
async function downloadFullManifest(dataplaneUrl, systemKey, authConfig) {
|
|
151
|
-
logger.log(
|
|
158
|
+
logger.log(formatProgress(`Downloading manifest for ${systemKey}ā¦`));
|
|
152
159
|
const response = await getExternalSystemConfig(dataplaneUrl, systemKey, authConfig);
|
|
153
160
|
|
|
154
161
|
if (!response.success || !response.data) {
|
|
@@ -268,14 +275,18 @@ function validateSystemKeyFormat(systemKey) {
|
|
|
268
275
|
* @param {string} dataplaneUrl - Dataplane URL
|
|
269
276
|
*/
|
|
270
277
|
function handleDryRun(systemKey, dataplaneUrl) {
|
|
271
|
-
logger.log(
|
|
272
|
-
logger.log(
|
|
273
|
-
logger.log(
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
278
|
+
logger.log(formatWarningLine('Dry run mode: no files written.'));
|
|
279
|
+
logger.log(metadata(`Would fetch: ${dataplaneUrl}/api/v1/external/systems/${systemKey}/config`));
|
|
280
|
+
logger.log(metadata('Would create:'));
|
|
281
|
+
[
|
|
282
|
+
`integration/${systemKey}/${systemKey}-deploy.json`,
|
|
283
|
+
`integration/${systemKey}/application.yaml`,
|
|
284
|
+
`integration/${systemKey}/${systemKey}-system.yaml`,
|
|
285
|
+
`integration/${systemKey}/env.template`,
|
|
286
|
+
`integration/${systemKey}/README.md`
|
|
287
|
+
].forEach(rel => {
|
|
288
|
+
logger.log(metadata(` - ${rel}`));
|
|
289
|
+
});
|
|
279
290
|
}
|
|
280
291
|
|
|
281
292
|
/**
|
|
@@ -285,7 +296,7 @@ function handleDryRun(systemKey, dataplaneUrl) {
|
|
|
285
296
|
* @returns {string} System type
|
|
286
297
|
*/
|
|
287
298
|
function validateAndLogDownloadedData(application, dataSources) {
|
|
288
|
-
logger.log(
|
|
299
|
+
logger.log(formatProgress('Validating downloaded dataā¦'));
|
|
289
300
|
validateDownloadedData(application, dataSources);
|
|
290
301
|
const systemType = validateSystemType(application);
|
|
291
302
|
logger.log(formatSuccessLine(`System type: ${systemType}`));
|
|
@@ -300,7 +311,7 @@ function validateAndLogDownloadedData(application, dataSources) {
|
|
|
300
311
|
function promptReplaceReadme() {
|
|
301
312
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
302
313
|
return new Promise(resolve => {
|
|
303
|
-
rl.question(
|
|
314
|
+
rl.question(`${metadata('README.md already exists. Replace it?')} (yes/no) `, answer => {
|
|
304
315
|
rl.close();
|
|
305
316
|
const normalized = (answer || '').trim().toLowerCase();
|
|
306
317
|
resolve(normalized === 'yes' || normalized === 'y');
|
|
@@ -326,7 +337,7 @@ async function resolveDownloadSplitOptions(finalPath, options = {}) {
|
|
|
326
337
|
opts.overwriteReadme = true;
|
|
327
338
|
} else {
|
|
328
339
|
opts.overwriteReadme = await promptReplaceReadme();
|
|
329
|
-
if (!opts.overwriteReadme) logger.log(
|
|
340
|
+
if (!opts.overwriteReadme) logger.log(metadata('Keeping existing README.md'));
|
|
330
341
|
}
|
|
331
342
|
}
|
|
332
343
|
return opts;
|
|
@@ -362,7 +373,8 @@ async function processDownloadedSystem(systemKey, manifest, splitOptions = {}) {
|
|
|
362
373
|
const { application, dataSources, version } = manifest;
|
|
363
374
|
const finalPath = getIntegrationPath(systemKey);
|
|
364
375
|
|
|
365
|
-
logger.log(
|
|
376
|
+
logger.log(formatProgress('Creating integration directoryā¦'));
|
|
377
|
+
logger.log(metadata(finalPath));
|
|
366
378
|
await fs.mkdir(finalPath, { recursive: true });
|
|
367
379
|
|
|
368
380
|
const deployJson = buildDeployJsonFromManifest(application, dataSources, version);
|
|
@@ -370,7 +382,7 @@ async function processDownloadedSystem(systemKey, manifest, splitOptions = {}) {
|
|
|
370
382
|
await fs.writeFile(deployJsonPath, JSON.stringify(deployJson, null, 2), 'utf8');
|
|
371
383
|
logger.log(formatSuccessLine(`Created: ${path.relative(process.cwd(), deployJsonPath)}`));
|
|
372
384
|
|
|
373
|
-
logger.log(
|
|
385
|
+
logger.log(formatProgress('Splitting deploy JSON into component filesā¦'));
|
|
374
386
|
const splitResult = await generator.splitDeployJson(deployJsonPath, finalPath, splitOptions);
|
|
375
387
|
await applyRetemplateToSystemFile(systemKey, splitResult.systemFile);
|
|
376
388
|
|
|
@@ -391,10 +403,37 @@ async function processDownloadedSystem(systemKey, manifest, splitOptions = {}) {
|
|
|
391
403
|
* @param {number} datasourceCount - Number of datasources
|
|
392
404
|
*/
|
|
393
405
|
function displayDownloadSuccess(systemKey, finalPath, datasourceCount) {
|
|
394
|
-
logger.log(
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
406
|
+
logger.log(
|
|
407
|
+
formatSuccessParagraph(
|
|
408
|
+
`Downloaded ${systemKey} (${datasourceCount} datasource${datasourceCount === 1 ? '' : 's'})`
|
|
409
|
+
)
|
|
410
|
+
);
|
|
411
|
+
logger.log(headerKeyValue('Location:', finalPath));
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
/**
|
|
415
|
+
* After YAML split, convert integration files to JSON when --format json.
|
|
416
|
+
* @async
|
|
417
|
+
* @param {string} systemKey
|
|
418
|
+
* @returns {Promise<void>}
|
|
419
|
+
*/
|
|
420
|
+
async function runConvertToJsonIfRequested(systemKey) {
|
|
421
|
+
const { runConvert } = require('../commands/convert');
|
|
422
|
+
try {
|
|
423
|
+
await runConvert(systemKey, { format: 'json', force: true });
|
|
424
|
+
logger.log(formatSuccessLine('Converted component files to JSON'));
|
|
425
|
+
} catch (convertErr) {
|
|
426
|
+
throw new Error(`Download succeeded but convert to JSON failed: ${convertErr.message}`);
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* @param {string} systemKey
|
|
432
|
+
*/
|
|
433
|
+
function logDownloadCommandHeader(systemKey) {
|
|
434
|
+
logger.log('');
|
|
435
|
+
logger.log(sectionTitle('Download'));
|
|
436
|
+
logger.log(headerKeyValue('System:', systemKey));
|
|
398
437
|
}
|
|
399
438
|
|
|
400
439
|
/**
|
|
@@ -411,23 +450,13 @@ function displayDownloadSuccess(systemKey, finalPath, datasourceCount) {
|
|
|
411
450
|
* @returns {Promise<void>} Resolves when download completes
|
|
412
451
|
* @throws {Error} If download fails
|
|
413
452
|
*/
|
|
414
|
-
async function runConvertToJsonIfRequested(systemKey) {
|
|
415
|
-
const { runConvert } = require('../commands/convert');
|
|
416
|
-
try {
|
|
417
|
-
await runConvert(systemKey, { format: 'json', force: true });
|
|
418
|
-
logger.log(formatSuccessLine('Converted component files to JSON'));
|
|
419
|
-
} catch (convertErr) {
|
|
420
|
-
throw new Error(`Download succeeded but convert to JSON failed: ${convertErr.message}`);
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
|
|
424
453
|
async function downloadExternalSystem(systemKey, options = {}) {
|
|
425
454
|
validateSystemKeyFormat(systemKey);
|
|
426
455
|
|
|
427
456
|
const format = (options.format || 'yaml').toLowerCase();
|
|
428
457
|
|
|
429
458
|
try {
|
|
430
|
-
|
|
459
|
+
logDownloadCommandHeader(systemKey);
|
|
431
460
|
|
|
432
461
|
const config = await getConfig();
|
|
433
462
|
const { authConfig, dataplaneUrl } = await setupAuthenticationAndDataplane(systemKey, options, config);
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Regenerate *-deploy.json on disk for an integration (same as `aifabrix json <systemKey>`).
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview Sync deployment manifest file before upload/deploy
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 1.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
const { formatSuccessLine } = require('../utils/cli-test-layout-chalk');
|
|
12
|
+
const generator = require('../generator');
|
|
13
|
+
const logger = require('../utils/logger');
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Writes integration/<systemKey>/<systemKey>-deploy.json from current application sources.
|
|
17
|
+
* Equivalent to running {@link generator.generateDeployJson} / `aifabrix json <systemKey>` for externals.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} systemKey - External system key (integration folder name)
|
|
20
|
+
* @param {{ quiet?: boolean }} [opts] - quiet: omit success log line
|
|
21
|
+
* @returns {Promise<string>} Absolute path to the written deploy JSON file
|
|
22
|
+
*/
|
|
23
|
+
async function syncDeployJsonFromSources(systemKey, opts = {}) {
|
|
24
|
+
const deployPath = await generator.generateDeployJson(systemKey, {});
|
|
25
|
+
if (!opts.quiet) {
|
|
26
|
+
logger.log(formatSuccessLine(`Updated deployment manifest: ${deployPath}`));
|
|
27
|
+
}
|
|
28
|
+
return deployPath;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
module.exports = {
|
|
32
|
+
syncDeployJsonFromSources
|
|
33
|
+
};
|
|
@@ -324,17 +324,22 @@ async function promptForExistingCredentialInput() {
|
|
|
324
324
|
* @function promptForUserIntent
|
|
325
325
|
* @returns {Promise<string>} User intent
|
|
326
326
|
*/
|
|
327
|
+
const WIZARD_INTENT_MAX_LENGTH = 1000;
|
|
328
|
+
|
|
327
329
|
async function promptForUserIntent() {
|
|
328
330
|
const { intent } = await inquirer.prompt([
|
|
329
331
|
{
|
|
330
332
|
type: 'input',
|
|
331
333
|
name: 'intent',
|
|
332
|
-
message:
|
|
334
|
+
message: `Describe your primary use case (max ${WIZARD_INTENT_MAX_LENGTH} characters):`,
|
|
333
335
|
default: 'general integration',
|
|
334
336
|
validate: (input) => {
|
|
335
337
|
if (!input || typeof input !== 'string' || input.trim().length === 0) {
|
|
336
338
|
return 'Intent is required';
|
|
337
339
|
}
|
|
340
|
+
if (input.length > WIZARD_INTENT_MAX_LENGTH) {
|
|
341
|
+
return `Intent must be ${WIZARD_INTENT_MAX_LENGTH} characters or fewer`;
|
|
342
|
+
}
|
|
338
343
|
return true;
|
|
339
344
|
}
|
|
340
345
|
}
|
|
@@ -437,6 +442,7 @@ async function promptForRunWithSavedConfig() {
|
|
|
437
442
|
const secondary = require('./wizard-prompts-secondary');
|
|
438
443
|
|
|
439
444
|
module.exports = {
|
|
445
|
+
WIZARD_INTENT_MAX_LENGTH,
|
|
440
446
|
promptForMode,
|
|
441
447
|
promptForSystemIdOrKey,
|
|
442
448
|
promptForExistingSystem,
|
package/lib/generator/wizard.js
CHANGED
|
@@ -32,6 +32,35 @@ function toKeySegment(str) {
|
|
|
32
32
|
return sanitized || 'default';
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
/**
|
|
36
|
+
* Align authentication.security kv:// namespaces and credentialKey with the final system key.
|
|
37
|
+
* Dataplane normalizes this before respond; when appName overrides a spec-derived key (e.g.
|
|
38
|
+
* OpenAPI title "Companies"), the builder still must rewrite nested auth so it matches env.template.
|
|
39
|
+
*
|
|
40
|
+
* @param {Object|null|undefined} authentication - authentication block from dataplane
|
|
41
|
+
* @param {string} systemKey - Final external system key (integration app name)
|
|
42
|
+
* @param {string} [authDisplayName] - Credential display name (typically title-cased app name)
|
|
43
|
+
*/
|
|
44
|
+
function normalizeAuthenticationToSystemKey(authentication, systemKey, authDisplayName) {
|
|
45
|
+
if (!authentication || typeof authentication !== 'object') return;
|
|
46
|
+
authentication.credentialKey = `${systemKey}-cred`;
|
|
47
|
+
const security = authentication.security;
|
|
48
|
+
if (security && typeof security === 'object') {
|
|
49
|
+
for (const k of Object.keys(security)) {
|
|
50
|
+
const v = security[k];
|
|
51
|
+
if (typeof v === 'string' && v.startsWith('kv://')) {
|
|
52
|
+
const rest = v.slice(5);
|
|
53
|
+
const idx = rest.indexOf('/');
|
|
54
|
+
const suffix = idx >= 0 ? rest.slice(idx + 1) : '';
|
|
55
|
+
security[k] = suffix ? `kv://${systemKey}/${suffix}` : `kv://${systemKey}`;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (authDisplayName) {
|
|
60
|
+
authentication.displayName = authDisplayName;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
35
64
|
/**
|
|
36
65
|
* Generate files from dataplane-generated wizard configurations
|
|
37
66
|
* @async
|
|
@@ -182,6 +211,11 @@ async function prepareWizardContext(appName, systemConfig, datasourceConfigs) {
|
|
|
182
211
|
const originalSystemKey = systemConfig.key || finalSystemKey;
|
|
183
212
|
const appDisplayName = appName.replace(/-/g, ' ').replace(/\b\w/g, l => l.toUpperCase());
|
|
184
213
|
const updatedSystemConfig = { ...systemConfig, key: finalSystemKey, displayName: appDisplayName };
|
|
214
|
+
normalizeAuthenticationToSystemKey(
|
|
215
|
+
updatedSystemConfig.authentication,
|
|
216
|
+
finalSystemKey,
|
|
217
|
+
appDisplayName
|
|
218
|
+
);
|
|
185
219
|
const originalPrefix = `${originalSystemKey}-`;
|
|
186
220
|
const updatedDatasourceConfigs = datasourceConfigs.map(ds => {
|
|
187
221
|
let newKey;
|