@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
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Decide whether `aifabrix run --reload` can bind-mount workspace without Mutagen.
|
|
3
|
+
* Mutagen is only needed when the CLI filesystem and the Docker engine host differ.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Co-located Docker detection for reload mounts
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
'use strict';
|
|
11
|
+
|
|
12
|
+
const os = require('os');
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* @param {string} host
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
function normalizeHost(host) {
|
|
19
|
+
return String(host || '')
|
|
20
|
+
.trim()
|
|
21
|
+
.toLowerCase()
|
|
22
|
+
.replace(/^\[|\]$/g, '');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {string} host
|
|
27
|
+
* @returns {boolean}
|
|
28
|
+
*/
|
|
29
|
+
function isLocalLoopbackHost(host) {
|
|
30
|
+
const h = normalizeHost(host);
|
|
31
|
+
return h === 'localhost' || h === '127.0.0.1' || h === '::1' || h === '0:0:0:0:0:0:0:1';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @param {string} rest - after "tcp://"
|
|
36
|
+
* @returns {string|null}
|
|
37
|
+
*/
|
|
38
|
+
function tcpHostFromRest(rest) {
|
|
39
|
+
if (rest.startsWith('[')) {
|
|
40
|
+
const end = rest.indexOf(']');
|
|
41
|
+
if (end === -1) return null;
|
|
42
|
+
return normalizeHost(rest.slice(1, end));
|
|
43
|
+
}
|
|
44
|
+
const hostPort = rest.split('/')[0];
|
|
45
|
+
const colonIdx = hostPort.lastIndexOf(':');
|
|
46
|
+
if (colonIdx === -1) {
|
|
47
|
+
return normalizeHost(hostPort);
|
|
48
|
+
}
|
|
49
|
+
const maybePort = hostPort.slice(colonIdx + 1);
|
|
50
|
+
if (/^\d+$/.test(maybePort)) {
|
|
51
|
+
return normalizeHost(hostPort.slice(0, colonIdx));
|
|
52
|
+
}
|
|
53
|
+
return normalizeHost(hostPort);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Extract host from docker-endpoint (tcp, optional URL form).
|
|
58
|
+
* @param {string} endpoint
|
|
59
|
+
* @returns {string|null} normalized host, or null for unix socket paths / empty
|
|
60
|
+
*/
|
|
61
|
+
function extractHostFromDockerEndpoint(endpoint) {
|
|
62
|
+
const s = String(endpoint || '').trim();
|
|
63
|
+
if (!s) return null;
|
|
64
|
+
const lower = s.toLowerCase();
|
|
65
|
+
if (lower.startsWith('unix:')) {
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
if (lower.startsWith('tcp://')) {
|
|
69
|
+
return tcpHostFromRest(s.slice(6));
|
|
70
|
+
}
|
|
71
|
+
try {
|
|
72
|
+
const u = new URL(s.includes('://') ? s : `tcp://${s}`);
|
|
73
|
+
return normalizeHost(u.hostname);
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* sync-ssh-host localhost check (same rules as run.js isLocalhostHost for reload gate).
|
|
81
|
+
* @param {string} host
|
|
82
|
+
* @returns {boolean}
|
|
83
|
+
*/
|
|
84
|
+
function isLocalhostSyncSshHost(host) {
|
|
85
|
+
if (!host || typeof host !== 'string') return false;
|
|
86
|
+
const h = host.trim().toLowerCase();
|
|
87
|
+
return h === 'localhost' || h === '127.0.0.1';
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* True when docker API host is this machine (first label or FQDN match).
|
|
92
|
+
* @param {string} dockerHost - from extractHostFromDockerEndpoint
|
|
93
|
+
* @returns {boolean}
|
|
94
|
+
*/
|
|
95
|
+
function hostnameMatchesDockerHost(dockerHost) {
|
|
96
|
+
const h = normalizeHost(dockerHost);
|
|
97
|
+
if (!h) return true;
|
|
98
|
+
if (isLocalLoopbackHost(h)) return true;
|
|
99
|
+
const hn = normalizeHost(os.hostname());
|
|
100
|
+
if (h === hn) return true;
|
|
101
|
+
const hnShort = hn.split('.')[0];
|
|
102
|
+
const hShort = h.split('.')[0];
|
|
103
|
+
if (h === hnShort || hn === hShort) return true;
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* When true, use a direct bind mount for --reload; Mutagen is not required.
|
|
109
|
+
* @param {string|null|undefined} dockerEndpoint - config `docker-endpoint`
|
|
110
|
+
* @returns {boolean}
|
|
111
|
+
*/
|
|
112
|
+
function isReloadBindMountOnEngineHost(dockerEndpoint) {
|
|
113
|
+
const e = String(dockerEndpoint || '').trim();
|
|
114
|
+
if (!e) return true;
|
|
115
|
+
if (e.toLowerCase().startsWith('unix:')) return true;
|
|
116
|
+
const host = extractHostFromDockerEndpoint(e);
|
|
117
|
+
if (host === null) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
return hostnameMatchesDockerHost(host);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = {
|
|
124
|
+
extractHostFromDockerEndpoint,
|
|
125
|
+
isReloadBindMountOnEngineHost,
|
|
126
|
+
isLocalhostSyncSshHost
|
|
127
|
+
};
|
|
@@ -15,6 +15,72 @@ const path = require('path');
|
|
|
15
15
|
const handlebars = require('handlebars');
|
|
16
16
|
const { getProjectRoot } = require('./paths');
|
|
17
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Extracts secret keys from integration/<appName>/env.template when present.
|
|
20
|
+
* Looks for values like `kv://systemKey/apiKey` and returns that key for
|
|
21
|
+
* `aifabrix secret set <key> <value>`.
|
|
22
|
+
*
|
|
23
|
+
* @param {object} args
|
|
24
|
+
* @param {string} args.projectRoot
|
|
25
|
+
* @param {string} args.appName
|
|
26
|
+
* @returns {Array<{path: string, description: string}>}
|
|
27
|
+
*/
|
|
28
|
+
function extractSecretPathsFromEnvTemplate({ projectRoot, appName }) {
|
|
29
|
+
if (!projectRoot || !appName) return [];
|
|
30
|
+
|
|
31
|
+
const envTemplatePath = path.join(projectRoot, 'integration', appName, 'env.template');
|
|
32
|
+
if (!fs.existsSync(envTemplatePath)) return [];
|
|
33
|
+
|
|
34
|
+
const raw = _tryReadTextOrNull(envTemplatePath);
|
|
35
|
+
if (raw === null) return [];
|
|
36
|
+
if (typeof raw !== 'string') return [];
|
|
37
|
+
|
|
38
|
+
function parseSecretKey(value) {
|
|
39
|
+
if (!value || typeof value !== 'string') return null;
|
|
40
|
+
const kvIdx = value.indexOf('kv://');
|
|
41
|
+
if (kvIdx === -1) return null;
|
|
42
|
+
const after = value.slice(kvIdx + 'kv://'.length);
|
|
43
|
+
const key = after.split(/[ \t#]/)[0]?.trim();
|
|
44
|
+
return key || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseLine(line) {
|
|
48
|
+
const trimmed = line.trim();
|
|
49
|
+
if (!trimmed || trimmed.startsWith('#')) return null;
|
|
50
|
+
const eqIdx = trimmed.indexOf('=');
|
|
51
|
+
if (eqIdx <= 0) return null;
|
|
52
|
+
const varName = trimmed.slice(0, eqIdx).trim();
|
|
53
|
+
const value = trimmed.slice(eqIdx + 1).trim();
|
|
54
|
+
const key = parseSecretKey(value);
|
|
55
|
+
if (!key) return null;
|
|
56
|
+
return { varName: varName || 'Secret', key };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const out = [];
|
|
60
|
+
const seen = new Set();
|
|
61
|
+
|
|
62
|
+
for (const line of raw.split('\n')) {
|
|
63
|
+
const parsed = parseLine(line);
|
|
64
|
+
if (!parsed) continue;
|
|
65
|
+
const dedupeKey = `${parsed.varName}::${parsed.key}`;
|
|
66
|
+
if (seen.has(dedupeKey)) continue;
|
|
67
|
+
seen.add(dedupeKey);
|
|
68
|
+
out.push({ path: parsed.key, description: parsed.varName });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function _tryReadTextOrNull(p) {
|
|
75
|
+
try {
|
|
76
|
+
return fs.readFileSync(p, 'utf8');
|
|
77
|
+
} catch (e) {
|
|
78
|
+
// Some Jest suites partially mock fs.existsSync; treat missing files as absent templates.
|
|
79
|
+
if (e && e.code === 'ENOENT') return null;
|
|
80
|
+
throw e;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
18
84
|
/**
|
|
19
85
|
* Formats a display name from a key
|
|
20
86
|
* @param {string} key - System or app key
|
|
@@ -137,17 +203,51 @@ function rbacOptionalFilename(normalizedExt) {
|
|
|
137
203
|
return normalizedExt === '.yaml' || normalizedExt === '.yml' ? 'rbac.yaml' : 'rbac.json';
|
|
138
204
|
}
|
|
139
205
|
|
|
206
|
+
/**
|
|
207
|
+
* Word-wraps plain description text for Markdown (MD013 ~80 columns). Keeps blank
|
|
208
|
+
* lines between paragraphs.
|
|
209
|
+
* @param {string} text - Raw description
|
|
210
|
+
* @param {number} [maxLen=80] - Target max line length
|
|
211
|
+
* @returns {string}
|
|
212
|
+
*/
|
|
213
|
+
function wrapPlainTextForMarkdown(text, maxLen = 80) {
|
|
214
|
+
if (!text || typeof text !== 'string') return text;
|
|
215
|
+
const blocks = text.split(/\n\s*\n/);
|
|
216
|
+
const wrapped = blocks.map((block) => {
|
|
217
|
+
const flat = block.replace(/\s+/g, ' ').trim();
|
|
218
|
+
if (!flat) return '';
|
|
219
|
+
const words = flat.split(' ');
|
|
220
|
+
const lines = [];
|
|
221
|
+
let current = '';
|
|
222
|
+
for (const w of words) {
|
|
223
|
+
const candidate = current ? `${current} ${w}` : w;
|
|
224
|
+
if (candidate.length <= maxLen) {
|
|
225
|
+
current = candidate;
|
|
226
|
+
} else {
|
|
227
|
+
if (current) lines.push(current);
|
|
228
|
+
current = w;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (current) lines.push(current);
|
|
232
|
+
return lines.join('\n');
|
|
233
|
+
});
|
|
234
|
+
return wrapped.filter(Boolean).join('\n\n');
|
|
235
|
+
}
|
|
236
|
+
|
|
140
237
|
function buildExternalReadmeContext(params = {}) {
|
|
141
238
|
const appName = params.appName || params.systemKey || 'external-system';
|
|
142
239
|
const systemKey = params.systemKey || appName;
|
|
143
240
|
const displayName = params.displayName || formatDisplayName(systemKey);
|
|
144
|
-
const
|
|
241
|
+
const rawDescription = params.description || `External system integration for ${systemKey}`;
|
|
242
|
+
const description = wrapPlainTextForMarkdown(rawDescription);
|
|
145
243
|
const systemType = params.systemType || 'openapi';
|
|
146
244
|
const fileExt = params.fileExt !== undefined ? params.fileExt : '.json';
|
|
147
245
|
const normalizedExt = fileExt && fileExt.startsWith('.') ? fileExt : `.${fileExt || 'json'}`;
|
|
148
246
|
const datasources = normalizeDatasources(params.datasources, systemKey, fileExt);
|
|
149
247
|
const authType = params.authType || params.authentication?.type || params.authentication?.method || params.authentication?.authType;
|
|
150
|
-
const
|
|
248
|
+
const projectRoot = getProjectRoot();
|
|
249
|
+
const secretPathsFromEnv = extractSecretPathsFromEnvTemplate({ projectRoot, appName });
|
|
250
|
+
const secretPaths = secretPathsFromEnv.length > 0 ? secretPathsFromEnv : buildSecretPaths(systemKey, authType);
|
|
151
251
|
const rbacOptionalFile = rbacOptionalFilename(normalizedExt);
|
|
152
252
|
|
|
153
253
|
return {
|
|
@@ -186,13 +286,26 @@ function loadExternalReadmeTemplate() {
|
|
|
186
286
|
* @param {Object} params - Context parameters
|
|
187
287
|
* @returns {string} README content
|
|
188
288
|
*/
|
|
289
|
+
/**
|
|
290
|
+
* Collapses 3+ consecutive newlines to 2 (fixes MD012 from Handlebars spacing).
|
|
291
|
+
* @param {string} md - Markdown body
|
|
292
|
+
* @returns {string}
|
|
293
|
+
*/
|
|
294
|
+
function collapseConsecutiveBlankLines(md) {
|
|
295
|
+
if (!md || typeof md !== 'string') return md;
|
|
296
|
+
return md.replace(/\n{3,}/g, '\n\n');
|
|
297
|
+
}
|
|
298
|
+
|
|
189
299
|
function generateExternalReadmeContent(params = {}) {
|
|
190
300
|
const template = loadExternalReadmeTemplate();
|
|
191
301
|
const context = buildExternalReadmeContext(params);
|
|
192
|
-
return template(context);
|
|
302
|
+
return collapseConsecutiveBlankLines(template(context));
|
|
193
303
|
}
|
|
194
304
|
|
|
195
305
|
module.exports = {
|
|
196
306
|
buildExternalReadmeContext,
|
|
197
|
-
generateExternalReadmeContent
|
|
307
|
+
generateExternalReadmeContent,
|
|
308
|
+
wrapPlainTextForMarkdown,
|
|
309
|
+
collapseConsecutiveBlankLines,
|
|
310
|
+
extractSecretPathsFromEnvTemplate
|
|
198
311
|
};
|
|
@@ -19,6 +19,7 @@ const {
|
|
|
19
19
|
headerKeyValue,
|
|
20
20
|
formatStatusKeyValue,
|
|
21
21
|
colorRollupPrefixedLine,
|
|
22
|
+
formatSuccessLine,
|
|
22
23
|
metadata: metaGray
|
|
23
24
|
} = require('./cli-test-layout-chalk');
|
|
24
25
|
|
|
@@ -338,7 +339,7 @@ function displayLocalExternalTestPlanLayout(results, verbose, appName) {
|
|
|
338
339
|
function logVerboseSystemRows(systemResults) {
|
|
339
340
|
for (const s of systemResults || []) {
|
|
340
341
|
const ok = s.valid;
|
|
341
|
-
logger.log(ok ? chalk.green(
|
|
342
|
+
logger.log(ok ? (chalk.green(' ') + formatSuccessLine(s.file)) : chalk.red(` ✖ ${s.file}`));
|
|
342
343
|
(s.errors || []).forEach(e => logger.log(chalk.red(` - ${e}`)));
|
|
343
344
|
}
|
|
344
345
|
}
|
|
@@ -346,7 +347,7 @@ function logVerboseSystemRows(systemResults) {
|
|
|
346
347
|
function logVerboseDatasourceRows(datasourceResults) {
|
|
347
348
|
for (const d of datasourceResults || []) {
|
|
348
349
|
const ok = d.valid;
|
|
349
|
-
logger.log(ok ? chalk.green(
|
|
350
|
+
logger.log(ok ? (chalk.green(' ') + formatSuccessLine(`${d.key} (${d.file})`)) : chalk.red(` ✖ ${d.key} (${d.file})`));
|
|
350
351
|
(d.errors || []).forEach(e => logger.log(chalk.red(` - ${e}`)));
|
|
351
352
|
(d.warnings || []).forEach(w => logger.log(chalk.yellow(` ⚠ ${w}`)));
|
|
352
353
|
if (d.fieldMappingResults && d.fieldMappingResults.mappedFields) {
|
|
@@ -43,30 +43,57 @@ function unwrapPublicationResult(res) {
|
|
|
43
43
|
* Rules: inactive/archived → Failed; MCP expected but missing → Partial; draft → Partial; published/deployed + active → Ready.
|
|
44
44
|
* @param {Object} ds - ExternalDataSourceResponse-like
|
|
45
45
|
* @param {boolean} generateMcpContract - From application config
|
|
46
|
-
* @returns {'ready'|'partial'|'failed'}
|
|
46
|
+
* @returns {{ tier: 'ready'|'partial'|'failed', partialReason: string|null }}
|
|
47
47
|
*/
|
|
48
|
-
function
|
|
48
|
+
function classifyDatasourceTierADetail(ds, generateMcpContract) {
|
|
49
49
|
const active = ds.isActive !== false;
|
|
50
50
|
const status = String(ds.status || '').toLowerCase();
|
|
51
51
|
if (!active || status === 'archived') {
|
|
52
|
-
return 'failed';
|
|
52
|
+
return { tier: 'failed', partialReason: null };
|
|
53
53
|
}
|
|
54
54
|
if (generateMcpContract === true && !ds.mcpContract) {
|
|
55
|
-
return 'partial';
|
|
55
|
+
return { tier: 'partial', partialReason: 'mcp_missing' };
|
|
56
56
|
}
|
|
57
57
|
if (status === 'draft') {
|
|
58
|
-
return 'partial';
|
|
58
|
+
return { tier: 'partial', partialReason: 'draft' };
|
|
59
59
|
}
|
|
60
60
|
if (status === 'published' || status === 'deployed') {
|
|
61
|
-
return 'ready';
|
|
61
|
+
return { tier: 'ready', partialReason: null };
|
|
62
|
+
}
|
|
63
|
+
return { tier: 'partial', partialReason: 'unknown_status' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* @param {Object} ds - ExternalDataSourceResponse-like
|
|
68
|
+
* @param {boolean} generateMcpContract - From application config
|
|
69
|
+
* @returns {'ready'|'partial'|'failed'}
|
|
70
|
+
*/
|
|
71
|
+
function classifyDatasourceTierA(ds, generateMcpContract) {
|
|
72
|
+
return classifyDatasourceTierADetail(ds, generateMcpContract).tier;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Short hint for CLI when Tier A is partial (machine codes from classifyDatasourceTierADetail).
|
|
77
|
+
* @param {string|null} partialReason
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
function formatTierAPartialHint(partialReason) {
|
|
81
|
+
if (partialReason === 'mcp_missing') {
|
|
82
|
+
return 'no MCP contract stored (OpenAPI link or generation)';
|
|
83
|
+
}
|
|
84
|
+
if (partialReason === 'draft') {
|
|
85
|
+
return 'status draft (trigger paths / certification gate)';
|
|
62
86
|
}
|
|
63
|
-
|
|
87
|
+
if (partialReason === 'unknown_status') {
|
|
88
|
+
return 'unexpected lifecycle status';
|
|
89
|
+
}
|
|
90
|
+
return '';
|
|
64
91
|
}
|
|
65
92
|
|
|
66
93
|
/**
|
|
67
94
|
* @param {Array<Object>} datasources - Datasource list
|
|
68
95
|
* @param {boolean} generateMcpContract
|
|
69
|
-
* @returns {{ rows: Array<{ key: string, tier: string }>, ready: number, partial: number, failed: number }}
|
|
96
|
+
* @returns {{ rows: Array<{ key: string, tier: string, partialReason?: string|null }>, ready: number, partial: number, failed: number }}
|
|
70
97
|
*/
|
|
71
98
|
function summarizeDatasourceTiersA(datasources, generateMcpContract) {
|
|
72
99
|
const rows = [];
|
|
@@ -75,10 +102,14 @@ function summarizeDatasourceTiersA(datasources, generateMcpContract) {
|
|
|
75
102
|
let failed = 0;
|
|
76
103
|
for (const ds of datasources || []) {
|
|
77
104
|
const key = ds.key || ds.sourceKey || 'unknown';
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
if (tier === '
|
|
81
|
-
|
|
105
|
+
const detail = classifyDatasourceTierADetail(ds, generateMcpContract);
|
|
106
|
+
const row = { key, tier: detail.tier };
|
|
107
|
+
if (detail.tier === 'partial' && detail.partialReason) {
|
|
108
|
+
row.partialReason = detail.partialReason;
|
|
109
|
+
}
|
|
110
|
+
rows.push(row);
|
|
111
|
+
if (detail.tier === 'ready') ready += 1;
|
|
112
|
+
else if (detail.tier === 'partial') partial += 1;
|
|
82
113
|
else failed += 1;
|
|
83
114
|
}
|
|
84
115
|
return { rows, ready, partial, failed };
|
|
@@ -401,7 +432,9 @@ module.exports = {
|
|
|
401
432
|
unwrapApiData,
|
|
402
433
|
unwrapPublicationResult,
|
|
403
434
|
isPublicationResultShape,
|
|
435
|
+
classifyDatasourceTierADetail,
|
|
404
436
|
classifyDatasourceTierA,
|
|
437
|
+
formatTierAPartialHint,
|
|
405
438
|
summarizeDatasourceTiersA,
|
|
406
439
|
aggregateVerdictFromCounts,
|
|
407
440
|
classifyDatasourceTierB,
|
|
@@ -83,7 +83,7 @@ function logDeployProbeDatasourceSection(probeData) {
|
|
|
83
83
|
* @param {Object|null} systemFromDataplane
|
|
84
84
|
* @param {boolean} genMcp
|
|
85
85
|
*/
|
|
86
|
-
function logDeployContractsSection(systemFromDataplane, genMcp) {
|
|
86
|
+
function logDeployContractsSection(systemFromDataplane, genMcp, dataplaneUrl, systemKey) {
|
|
87
87
|
if (!systemFromDataplane) return;
|
|
88
88
|
logSeparator();
|
|
89
89
|
logSectionTitle('Contracts:');
|
|
@@ -94,7 +94,7 @@ function logDeployContractsSection(systemFromDataplane, genMcp) {
|
|
|
94
94
|
} else {
|
|
95
95
|
logger.log(chalk.gray('○ OpenAPI docs URL not available'));
|
|
96
96
|
}
|
|
97
|
-
logDocsBlock(systemFromDataplane);
|
|
97
|
+
logDocsBlock(systemFromDataplane, { dataplaneUrl, systemKey, genMcp: mcpOk });
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/**
|
|
@@ -260,7 +260,7 @@ function logDeployReadinessSummary(ctx) {
|
|
|
260
260
|
|
|
261
261
|
logDeployIdentityAndCredentialBlocks(systemCfg, !!probeData);
|
|
262
262
|
|
|
263
|
-
logDeployContractsSection(systemFromDataplane, genMcp);
|
|
263
|
+
logDeployContractsSection(systemFromDataplane, genMcp, dataplaneUrl, systemKey);
|
|
264
264
|
logDeployNextActionsSection(systemKey, probeData, summary, genMcp);
|
|
265
265
|
}
|
|
266
266
|
|
|
@@ -9,7 +9,11 @@
|
|
|
9
9
|
const chalk = require('chalk');
|
|
10
10
|
const { failureGlyph, successGlyph } = require('./cli-test-layout-chalk');
|
|
11
11
|
const logger = require('./logger');
|
|
12
|
-
const {
|
|
12
|
+
const {
|
|
13
|
+
extractIdentitySummary,
|
|
14
|
+
resolveCredentialTestEndpointDisplay,
|
|
15
|
+
formatTierAPartialHint
|
|
16
|
+
} = require('./external-system-readiness-core');
|
|
13
17
|
|
|
14
18
|
const SEP = chalk.gray('────────────────────────────────');
|
|
15
19
|
|
|
@@ -56,8 +60,12 @@ function logDatasourceTable(rows, counts, title) {
|
|
|
56
60
|
logSectionTitle(title && String(title).trim() ? String(title).trim() : 'Datasources:');
|
|
57
61
|
for (const r of rows) {
|
|
58
62
|
const statusLabel = r.tier === 'ready' ? 'Ready' : r.tier === 'failed' ? 'Failed' : 'Partial';
|
|
63
|
+
const hint =
|
|
64
|
+
r.tier === 'partial' && r.partialReason
|
|
65
|
+
? chalk.gray(` — ${formatTierAPartialHint(r.partialReason)}`)
|
|
66
|
+
: '';
|
|
59
67
|
logger.log(
|
|
60
|
-
`${tierGlyph(r.tier)} ${r.key.padEnd(14, ' ')} ${chalk.gray('(' + statusLabel + ')')}`
|
|
68
|
+
`${tierGlyph(r.tier)} ${r.key.padEnd(14, ' ')} ${chalk.gray('(' + statusLabel + ')')}${hint}`
|
|
61
69
|
);
|
|
62
70
|
}
|
|
63
71
|
logger.log('');
|
|
@@ -120,14 +128,35 @@ function logNextActions(actions, extraLine) {
|
|
|
120
128
|
}
|
|
121
129
|
}
|
|
122
130
|
|
|
131
|
+
/**
|
|
132
|
+
* Dataplane serves MCP OpenAPI docs at /api/v1/mcp/{systemKey}/docs.
|
|
133
|
+
*
|
|
134
|
+
* @param {Object} sys - ExternalSystemResponse
|
|
135
|
+
* @param {{ dataplaneUrl?: string, systemKey?: string, genMcp?: boolean }} [opts]
|
|
136
|
+
* @returns {string|null}
|
|
137
|
+
*/
|
|
138
|
+
function deriveMcpDocsPageUrl(sys, opts) {
|
|
139
|
+
if (!sys || !opts) return null;
|
|
140
|
+
if (sys.mcpDocsPageUrl) return String(sys.mcpDocsPageUrl);
|
|
141
|
+
const { dataplaneUrl, systemKey, genMcp } = opts;
|
|
142
|
+
if (!genMcp || !dataplaneUrl || !systemKey) return null;
|
|
143
|
+
if (!sys.mcpServerUrl) return null;
|
|
144
|
+
if (!sys.openApiDocsPageUrl && !sys.apiDocumentUrl) return null;
|
|
145
|
+
const base = String(dataplaneUrl).replace(/\/+$/, '');
|
|
146
|
+
return `${base}/api/v1/mcp/${encodeURIComponent(systemKey)}/docs`;
|
|
147
|
+
}
|
|
148
|
+
|
|
123
149
|
/**
|
|
124
150
|
* @param {Object} sys - ExternalSystemResponse
|
|
151
|
+
* @param {{ dataplaneUrl?: string, systemKey?: string, genMcp?: boolean }} [opts]
|
|
125
152
|
*/
|
|
126
|
-
function logDocsBlock(sys) {
|
|
153
|
+
function logDocsBlock(sys, opts) {
|
|
127
154
|
if (!sys) return;
|
|
128
155
|
const urls = [];
|
|
129
156
|
if (sys.openApiDocsPageUrl) urls.push({ label: 'OpenAPI Docs Page', url: sys.openApiDocsPageUrl });
|
|
130
157
|
if (sys.apiDocumentUrl) urls.push({ label: 'API Docs', url: sys.apiDocumentUrl });
|
|
158
|
+
const mcpDocs = deriveMcpDocsPageUrl(sys, opts);
|
|
159
|
+
if (mcpDocs) urls.push({ label: 'MCP Docs Page', url: mcpDocs });
|
|
131
160
|
if (sys.mcpServerUrl) urls.push({ label: 'MCP Server', url: sys.mcpServerUrl });
|
|
132
161
|
if (urls.length === 0) return;
|
|
133
162
|
logSeparator();
|
|
@@ -147,5 +176,6 @@ module.exports = {
|
|
|
147
176
|
logIdentityBlock,
|
|
148
177
|
logCredentialIntentBlock,
|
|
149
178
|
logNextActions,
|
|
179
|
+
deriveMcpDocsPageUrl,
|
|
150
180
|
logDocsBlock
|
|
151
181
|
};
|
|
@@ -44,7 +44,16 @@ function logPublishResultBlock(publication) {
|
|
|
44
44
|
}
|
|
45
45
|
logSeparator();
|
|
46
46
|
logSectionTitle('MCP Contract:');
|
|
47
|
-
|
|
47
|
+
if (genMcp) {
|
|
48
|
+
logger.log(formatSuccessLine('Generation requested (manifest generateMcpContract)'));
|
|
49
|
+
logger.log(
|
|
50
|
+
chalk.gray(
|
|
51
|
+
' Per-datasource MCP appears when dataplane stores mcpContract (OpenAPI resolves + generation succeeds).'
|
|
52
|
+
)
|
|
53
|
+
);
|
|
54
|
+
} else {
|
|
55
|
+
logger.log(chalk.gray('○ Not requested (generateMcpContract false)'));
|
|
56
|
+
}
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
/**
|
package/lib/utils/file-upload.js
CHANGED
|
@@ -33,10 +33,10 @@ async function validateFileExists(filePath) {
|
|
|
33
33
|
* @param {Object} additionalFields - Additional fields
|
|
34
34
|
* @returns {Promise<FormData>} FormData object
|
|
35
35
|
*/
|
|
36
|
-
async function buildFormData(filePath, fieldName, additionalFields) {
|
|
36
|
+
async function buildFormData(filePath, fieldName, additionalFields, opts = {}) {
|
|
37
37
|
const formData = new FormData();
|
|
38
38
|
const fileContent = await fs.readFile(filePath);
|
|
39
|
-
const fileName = path.basename(filePath);
|
|
39
|
+
const fileName = opts.filenameOverride ? String(opts.filenameOverride) : path.basename(filePath);
|
|
40
40
|
const fileBlob = new Blob([fileContent], { type: 'application/octet-stream' });
|
|
41
41
|
formData.append(fieldName, fileBlob, fileName);
|
|
42
42
|
|
|
@@ -74,6 +74,43 @@ async function uploadFile(url, filePath, fieldName = 'file', authConfig = {}, ad
|
|
|
74
74
|
return await client.postFormData(endpointPath, formData);
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* Upload a file using multipart/form-data but override the filename sent to the server.
|
|
79
|
+
* Useful when the server derives a key from the upload filename.
|
|
80
|
+
*
|
|
81
|
+
* @async
|
|
82
|
+
* @function uploadFileAs
|
|
83
|
+
* @param {string} url - Full API endpoint URL
|
|
84
|
+
* @param {string} filePath - Path to file to upload
|
|
85
|
+
* @param {string} filenameOverride - Filename to present to server (e.g. 'my-key.json')
|
|
86
|
+
* @param {string} fieldName - Form field name for the file (default: 'file')
|
|
87
|
+
* @param {Object} [authConfig] - Authentication configuration
|
|
88
|
+
* @param {Object} [additionalFields] - Additional form fields to include
|
|
89
|
+
* @returns {Promise<Object>} API response
|
|
90
|
+
*/
|
|
91
|
+
async function uploadFileAs(
|
|
92
|
+
url,
|
|
93
|
+
filePath,
|
|
94
|
+
filenameOverride,
|
|
95
|
+
fieldName = 'file',
|
|
96
|
+
authConfig = {},
|
|
97
|
+
additionalFields = {}
|
|
98
|
+
) {
|
|
99
|
+
await validateFileExists(filePath);
|
|
100
|
+
if (!filenameOverride || typeof filenameOverride !== 'string') {
|
|
101
|
+
throw new Error('filenameOverride is required and must be a string');
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const parsed = new URL(url);
|
|
105
|
+
const baseUrl = parsed.origin;
|
|
106
|
+
const endpointPath = parsed.pathname + parsed.search;
|
|
107
|
+
|
|
108
|
+
const formData = await buildFormData(filePath, fieldName, additionalFields, { filenameOverride });
|
|
109
|
+
const client = new ApiClient(baseUrl, authConfig);
|
|
110
|
+
return await client.postFormData(endpointPath, formData);
|
|
111
|
+
}
|
|
112
|
+
|
|
77
113
|
module.exports = {
|
|
78
|
-
uploadFile
|
|
114
|
+
uploadFile,
|
|
115
|
+
uploadFileAs
|
|
79
116
|
};
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const chalk = require('chalk');
|
|
2
|
+
const { formatSuccessLine } = require('./cli-test-layout-chalk');
|
|
3
|
+
const logger = require('./logger');
|
|
4
|
+
const { execWithDockerEnv } = require('./docker-exec');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Checks if db-init container exists.
|
|
8
|
+
* @async
|
|
9
|
+
* @param {string} dbInitContainer
|
|
10
|
+
* @returns {Promise<boolean>}
|
|
11
|
+
*/
|
|
12
|
+
async function checkDbInitContainerExists(dbInitContainer) {
|
|
13
|
+
try {
|
|
14
|
+
const { stdout } = await execWithDockerEnv(
|
|
15
|
+
`docker ps -a --filter "name=${dbInitContainer}" --format "{{.Names}}"`
|
|
16
|
+
);
|
|
17
|
+
return stdout.trim() === dbInitContainer;
|
|
18
|
+
} catch {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Gets container exit code.
|
|
25
|
+
* @async
|
|
26
|
+
* @param {string} dbInitContainer
|
|
27
|
+
* @returns {Promise<string>}
|
|
28
|
+
*/
|
|
29
|
+
async function getContainerExitCode(dbInitContainer) {
|
|
30
|
+
const { stdout: exitCode } = await execWithDockerEnv(
|
|
31
|
+
`docker inspect --format='{{.State.ExitCode}}' ${dbInitContainer}`
|
|
32
|
+
);
|
|
33
|
+
return exitCode.trim();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Handles exited container status.
|
|
38
|
+
* @async
|
|
39
|
+
* @param {string} dbInitContainer
|
|
40
|
+
* @returns {Promise<boolean>}
|
|
41
|
+
*/
|
|
42
|
+
async function handleExitedContainer(dbInitContainer) {
|
|
43
|
+
const { stdout: status } = await execWithDockerEnv(
|
|
44
|
+
`docker inspect --format='{{.State.Status}}' ${dbInitContainer}`
|
|
45
|
+
);
|
|
46
|
+
if (status.trim() === 'exited') {
|
|
47
|
+
const exitCode = await getContainerExitCode(dbInitContainer);
|
|
48
|
+
if (exitCode === '0') {
|
|
49
|
+
logger.log(formatSuccessLine('Database initialization already completed'));
|
|
50
|
+
} else {
|
|
51
|
+
logger.log(chalk.yellow(`⚠ Database initialization exited with code ${exitCode}`));
|
|
52
|
+
}
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Waits for container to exit (best-effort).
|
|
60
|
+
* @async
|
|
61
|
+
* @param {string} dbInitContainer
|
|
62
|
+
* @param {number} maxAttempts
|
|
63
|
+
* @returns {Promise<void>}
|
|
64
|
+
*/
|
|
65
|
+
async function waitForContainerExit(dbInitContainer, maxAttempts) {
|
|
66
|
+
for (let attempts = 0; attempts < maxAttempts; attempts++) {
|
|
67
|
+
const { stdout: currentStatus } = await execWithDockerEnv(
|
|
68
|
+
`docker inspect --format='{{.State.Status}}' ${dbInitContainer}`
|
|
69
|
+
);
|
|
70
|
+
if (currentStatus.trim() === 'exited') {
|
|
71
|
+
const exitCode = await getContainerExitCode(dbInitContainer);
|
|
72
|
+
if (exitCode === '0') {
|
|
73
|
+
logger.log(formatSuccessLine('Database initialization completed'));
|
|
74
|
+
} else {
|
|
75
|
+
logger.log(chalk.yellow(`⚠ Database initialization exited with code ${exitCode}`));
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Checks if db-init container exists and waits for it to complete.
|
|
85
|
+
* @async
|
|
86
|
+
* @param {string} appName
|
|
87
|
+
* @returns {Promise<void>}
|
|
88
|
+
*/
|
|
89
|
+
async function waitForDbInit(appName) {
|
|
90
|
+
const dbInitContainer = `aifabrix-${appName}-db-init`;
|
|
91
|
+
try {
|
|
92
|
+
if (!(await checkDbInitContainerExists(dbInitContainer))) {
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (await handleExitedContainer(dbInitContainer)) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
logger.log(chalk.blue('Waiting for database initialization to complete...'));
|
|
101
|
+
await waitForContainerExit(dbInitContainer, 30);
|
|
102
|
+
} catch {
|
|
103
|
+
// db-init container might not exist, which is fine
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = { waitForDbInit };
|