@aifabrix/builder 2.44.5 → 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 +48 -2
- 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/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.js +20 -2
- package/lib/cli/setup-auth.js +26 -0
- package/lib/cli/setup-dev-path-commands.js +50 -3
- package/lib/cli/setup-infra.js +134 -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 +78 -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 +214 -31
- package/lib/commands/repair-system-alignment.js +246 -0
- package/lib/commands/repair-system-permissions.js +168 -0
- package/lib/commands/repair.js +120 -338
- 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/up-common.js +79 -19
- 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 +6 -5
- package/lib/commands/wizard-dataplane.js +2 -2
- package/lib/commands/wizard-entity-selection.js +4 -3
- package/lib/commands/wizard-headless.js +2 -1
- package/lib/commands/wizard.js +2 -1
- 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/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 +1 -1
- 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 +71 -2
- 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 +23 -12
- 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 +1 -1
- package/templates/applications/miso-controller/rbac.yaml +9 -9
- package/templates/external-system/README.md.hbs +83 -123
- 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,388 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Inquirer prompts for `aifabrix setup`.
|
|
3
|
+
*
|
|
4
|
+
* Three small prompt helpers:
|
|
5
|
+
* 1. {@link promptModeSelection} - Mode menu shown when infra is already up.
|
|
6
|
+
* 2. {@link promptAdminCredentials} - Admin email + password (fresh install only).
|
|
7
|
+
* 3. {@link promptAiTool} - AI tool provider + keys, only when the merged
|
|
8
|
+
* secret cascade (user-local + shared) does not already provide the keys.
|
|
9
|
+
*
|
|
10
|
+
* Reads merged secrets via `loadSecrets()` from `lib/core/secrets.js` so the
|
|
11
|
+
* resolution honors both user-local secrets and the shared `aifabrix-secrets`
|
|
12
|
+
* store (file or remote API) when configured.
|
|
13
|
+
*
|
|
14
|
+
* Writes use `setSecretInStore(...)` (primary user `secrets.local.yaml` only; shared is human via
|
|
15
|
+
* `aifabrix secret set --shared`) so the
|
|
16
|
+
* installer can write either to user-local secrets or to the shared store,
|
|
17
|
+
* depending on config.
|
|
18
|
+
*
|
|
19
|
+
* @fileoverview Setup prompts (mode menu, admin creds, AI tool)
|
|
20
|
+
* @author AI Fabrix Team
|
|
21
|
+
* @version 2.0.0
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
'use strict';
|
|
25
|
+
|
|
26
|
+
const inquirer = require('inquirer');
|
|
27
|
+
const chalk = require('chalk');
|
|
28
|
+
const logger = require('../utils/logger');
|
|
29
|
+
const secretsCore = require('../core/secrets');
|
|
30
|
+
const secretsEnsure = require('../core/secrets-ensure');
|
|
31
|
+
const { formatSuccessLine, infoLine } = require('../utils/cli-test-layout-chalk');
|
|
32
|
+
|
|
33
|
+
/** Mode identifiers. Public so callers can switch on them without string typos. */
|
|
34
|
+
const MODE = Object.freeze({
|
|
35
|
+
REINSTALL: 'reinstall',
|
|
36
|
+
WIPE_DATA: 'wipe-data',
|
|
37
|
+
CLEAN_FILES: 'clean-files',
|
|
38
|
+
UPDATE_IMAGES: 'update-images'
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
/** AI tool key names in `secrets.local.yaml` / shared secrets. */
|
|
42
|
+
const AI_KEYS = Object.freeze({
|
|
43
|
+
OPENAI_API_KEY: 'secrets-openaiApiKeyVault',
|
|
44
|
+
AZURE_OPENAI_URL: 'azure-openaiapi-urlKeyVault',
|
|
45
|
+
AZURE_OPENAI_API_KEY: 'secrets-azureOpenaiApiKeyVault'
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
/** Builder folder conflict actions (setup preflight). */
|
|
49
|
+
const BUILDER_DIR_ACTION = Object.freeze({
|
|
50
|
+
KEEP: 'keep',
|
|
51
|
+
KEEP_FILES: 'keep-files',
|
|
52
|
+
BACKUP: 'backup',
|
|
53
|
+
ABORT: 'abort'
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Prompt the user to choose what to do when infra is already running.
|
|
58
|
+
* @async
|
|
59
|
+
* @returns {Promise<string>} One of MODE.* values
|
|
60
|
+
*/
|
|
61
|
+
async function promptModeSelection() {
|
|
62
|
+
const { mode } = await inquirer.prompt([
|
|
63
|
+
{
|
|
64
|
+
type: 'list',
|
|
65
|
+
name: 'mode',
|
|
66
|
+
message: 'Infrastructure is already running. What do you want to do?',
|
|
67
|
+
choices: [
|
|
68
|
+
{
|
|
69
|
+
name: 'Re-install platform + infra (DESTROYS all local data)',
|
|
70
|
+
value: MODE.REINSTALL
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
name: 'Reset databases (keeps volumes, removes DBs/users)',
|
|
74
|
+
value: MODE.WIPE_DATA
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: 'Refresh platform config (keeps data, re-copy platform files)',
|
|
78
|
+
value: MODE.CLEAN_FILES
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
name: 'Update images + restart services (keeps data)',
|
|
82
|
+
value: MODE.UPDATE_IMAGES
|
|
83
|
+
}
|
|
84
|
+
]
|
|
85
|
+
}
|
|
86
|
+
]);
|
|
87
|
+
return mode;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Confirm a destructive mode unless `assumeYes` is true.
|
|
92
|
+
* @async
|
|
93
|
+
* @param {string} mode - One of MODE.* values
|
|
94
|
+
* @param {boolean} assumeYes - When true, skip the confirmation
|
|
95
|
+
* @returns {Promise<boolean>} True when the user confirms (or `assumeYes`)
|
|
96
|
+
*/
|
|
97
|
+
async function confirmDestructiveMode(mode, assumeYes) {
|
|
98
|
+
if (assumeYes) return true;
|
|
99
|
+
if (mode !== MODE.REINSTALL && mode !== MODE.WIPE_DATA) return true;
|
|
100
|
+
const message =
|
|
101
|
+
mode === MODE.REINSTALL
|
|
102
|
+
? 'This will stop all services and DELETE every infra volume. Continue?'
|
|
103
|
+
: 'This will DROP every database and DB user. Continue?';
|
|
104
|
+
const { ok } = await inquirer.prompt([
|
|
105
|
+
{ type: 'confirm', name: 'ok', message, default: false }
|
|
106
|
+
]);
|
|
107
|
+
return ok === true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Validate an email address (RFC 5322-lite for CLI use).
|
|
112
|
+
* @param {string} input
|
|
113
|
+
* @returns {true|string}
|
|
114
|
+
*/
|
|
115
|
+
function validateEmail(input) {
|
|
116
|
+
const value = (input || '').trim();
|
|
117
|
+
if (!value) return 'Admin email is required';
|
|
118
|
+
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return 'Enter a valid email address';
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Validate a password (non-empty, at least 8 chars).
|
|
124
|
+
* @param {string} input
|
|
125
|
+
* @returns {true|string}
|
|
126
|
+
*/
|
|
127
|
+
function validatePassword(input) {
|
|
128
|
+
const value = String(input ?? '');
|
|
129
|
+
if (value.length === 0) return 'Admin password is required';
|
|
130
|
+
if (value.length < 8) return 'Admin password must be at least 8 characters';
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Prompt for admin email + password (fresh install only).
|
|
136
|
+
* Asks once for the password and once for confirmation; returns trimmed email
|
|
137
|
+
* and the verbatim password (never logged).
|
|
138
|
+
*
|
|
139
|
+
* @async
|
|
140
|
+
* @returns {Promise<{ adminEmail: string, adminPassword: string }>}
|
|
141
|
+
*/
|
|
142
|
+
async function promptAdminCredentials() {
|
|
143
|
+
const answers = await inquirer.prompt([
|
|
144
|
+
{
|
|
145
|
+
type: 'input',
|
|
146
|
+
name: 'adminEmail',
|
|
147
|
+
message: 'Admin email (Keycloak / pgAdmin login):',
|
|
148
|
+
validate: validateEmail
|
|
149
|
+
},
|
|
150
|
+
{
|
|
151
|
+
type: 'password',
|
|
152
|
+
name: 'adminPassword',
|
|
153
|
+
message: 'Admin password (Postgres / Keycloak / pgAdmin):',
|
|
154
|
+
mask: '*',
|
|
155
|
+
validate: validatePassword
|
|
156
|
+
},
|
|
157
|
+
{
|
|
158
|
+
type: 'password',
|
|
159
|
+
name: 'adminPasswordConfirm',
|
|
160
|
+
message: 'Confirm admin password:',
|
|
161
|
+
mask: '*',
|
|
162
|
+
validate(input, all) {
|
|
163
|
+
if (input !== all.adminPassword) return 'Passwords do not match';
|
|
164
|
+
return true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
]);
|
|
168
|
+
return {
|
|
169
|
+
adminEmail: String(answers.adminEmail).trim(),
|
|
170
|
+
adminPassword: answers.adminPassword
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* @typedef {Object} AiToolStatus
|
|
176
|
+
* @property {boolean} openAiConfigured - True when OpenAI key resolves non-empty
|
|
177
|
+
* @property {boolean} azureOpenAiConfigured - True when Azure URL + key resolve non-empty
|
|
178
|
+
*/
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Detect whether AI tool keys are already present in the merged secret view.
|
|
182
|
+
* Treats values surrounded by Handlebars-style placeholders (e.g. `{{...}}`)
|
|
183
|
+
* as not configured.
|
|
184
|
+
*
|
|
185
|
+
* @async
|
|
186
|
+
* @returns {Promise<AiToolStatus>}
|
|
187
|
+
*/
|
|
188
|
+
async function detectAiToolStatus() {
|
|
189
|
+
let merged = {};
|
|
190
|
+
try {
|
|
191
|
+
merged = await secretsCore.loadSecrets(undefined);
|
|
192
|
+
} catch {
|
|
193
|
+
merged = {};
|
|
194
|
+
}
|
|
195
|
+
const isSet = (key) => {
|
|
196
|
+
const raw = merged ? merged[key] : undefined;
|
|
197
|
+
if (raw === undefined || raw === null) return false;
|
|
198
|
+
const value = String(raw).trim();
|
|
199
|
+
if (!value) return false;
|
|
200
|
+
if (value.startsWith('{{') && value.endsWith('}}')) return false;
|
|
201
|
+
return true;
|
|
202
|
+
};
|
|
203
|
+
return {
|
|
204
|
+
openAiConfigured: isSet(AI_KEYS.OPENAI_API_KEY),
|
|
205
|
+
azureOpenAiConfigured:
|
|
206
|
+
isSet(AI_KEYS.AZURE_OPENAI_URL) && isSet(AI_KEYS.AZURE_OPENAI_API_KEY)
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Ask which AI tool the user wants to configure when none is configured yet.
|
|
212
|
+
* @async
|
|
213
|
+
* @returns {Promise<'openai'|'azure'|'skip'>}
|
|
214
|
+
*/
|
|
215
|
+
async function askAiToolChoice() {
|
|
216
|
+
const { choice } = await inquirer.prompt([
|
|
217
|
+
{
|
|
218
|
+
type: 'list',
|
|
219
|
+
name: 'choice',
|
|
220
|
+
message: 'Which AI tool do you want to configure for the dataplane?',
|
|
221
|
+
choices: [
|
|
222
|
+
{ name: 'OpenAI (api.openai.com)', value: 'openai' },
|
|
223
|
+
{ name: 'Azure OpenAI (your Azure resource)', value: 'azure' },
|
|
224
|
+
{ name: 'Skip for now (set later via aifabrix secret set)', value: 'skip' }
|
|
225
|
+
]
|
|
226
|
+
}
|
|
227
|
+
]);
|
|
228
|
+
return choice;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Collect and persist OpenAI key.
|
|
233
|
+
* @async
|
|
234
|
+
* @returns {Promise<void>}
|
|
235
|
+
*/
|
|
236
|
+
async function collectAndSaveOpenAiKey() {
|
|
237
|
+
const { apiKey } = await inquirer.prompt([
|
|
238
|
+
{
|
|
239
|
+
type: 'password',
|
|
240
|
+
name: 'apiKey',
|
|
241
|
+
message: 'OpenAI API key:',
|
|
242
|
+
mask: '*',
|
|
243
|
+
validate(input) {
|
|
244
|
+
return String(input || '').trim() ? true : 'API key is required';
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
]);
|
|
248
|
+
await secretsEnsure.setSecretInStore(AI_KEYS.OPENAI_API_KEY, String(apiKey).trim());
|
|
249
|
+
logger.log(formatSuccessLine('OpenAI API key saved'));
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Collect and persist Azure OpenAI URL + key.
|
|
254
|
+
* @async
|
|
255
|
+
* @returns {Promise<void>}
|
|
256
|
+
*/
|
|
257
|
+
async function collectAndSaveAzureOpenAi() {
|
|
258
|
+
const answers = await inquirer.prompt([
|
|
259
|
+
{
|
|
260
|
+
type: 'input',
|
|
261
|
+
name: 'url',
|
|
262
|
+
message: 'Azure OpenAI endpoint URL (e.g. https://my-aoai.openai.azure.com):',
|
|
263
|
+
validate(input) {
|
|
264
|
+
const value = String(input || '').trim();
|
|
265
|
+
if (!value) return 'Endpoint URL is required';
|
|
266
|
+
if (!/^https?:\/\//.test(value)) return 'URL must start with http(s)://';
|
|
267
|
+
return true;
|
|
268
|
+
}
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
type: 'password',
|
|
272
|
+
name: 'apiKey',
|
|
273
|
+
message: 'Azure OpenAI API key:',
|
|
274
|
+
mask: '*',
|
|
275
|
+
validate(input) {
|
|
276
|
+
return String(input || '').trim() ? true : 'API key is required';
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
]);
|
|
280
|
+
await secretsEnsure.setSecretInStore(AI_KEYS.AZURE_OPENAI_URL, String(answers.url).trim());
|
|
281
|
+
await secretsEnsure.setSecretInStore(AI_KEYS.AZURE_OPENAI_API_KEY, String(answers.apiKey).trim());
|
|
282
|
+
logger.log(formatSuccessLine('Azure OpenAI endpoint and API key saved'));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Run the AI tool prompt sequence.
|
|
287
|
+
* Skips silently when either provider is already configured (in any source).
|
|
288
|
+
*
|
|
289
|
+
* @async
|
|
290
|
+
* @param {{ silentIfConfigured?: boolean }} [options]
|
|
291
|
+
* @returns {Promise<void>}
|
|
292
|
+
*/
|
|
293
|
+
async function promptAiTool(options = {}) {
|
|
294
|
+
const silentIfConfigured = options && options.silentIfConfigured === true;
|
|
295
|
+
const status = await detectAiToolStatus();
|
|
296
|
+
if (status.openAiConfigured) {
|
|
297
|
+
if (!silentIfConfigured) {
|
|
298
|
+
logger.log(infoLine('OpenAI key already configured - skipping AI tool prompt.'));
|
|
299
|
+
}
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
if (status.azureOpenAiConfigured) {
|
|
303
|
+
if (!silentIfConfigured) {
|
|
304
|
+
logger.log(infoLine('Azure OpenAI endpoint and key already configured - skipping AI tool prompt.'));
|
|
305
|
+
}
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
const choice = await askAiToolChoice();
|
|
309
|
+
if (choice === 'openai') {
|
|
310
|
+
await collectAndSaveOpenAiKey();
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
if (choice === 'azure') {
|
|
314
|
+
await collectAndSaveAzureOpenAi();
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
logger.log(
|
|
318
|
+
chalk.gray(
|
|
319
|
+
'Skipped. Set later: aifabrix secret set ' +
|
|
320
|
+
AI_KEYS.OPENAI_API_KEY +
|
|
321
|
+
' <key> (or the two azure-openaiapi-urlKeyVault / secrets-azureOpenaiApiKeyVault keys)'
|
|
322
|
+
)
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* When `aifabrix setup` is about to run `up-platform --force`, it will wipe
|
|
328
|
+
* platform app directories under `builder/`. If the builder root already
|
|
329
|
+
* contains files, prompt for how to proceed.
|
|
330
|
+
*
|
|
331
|
+
* @async
|
|
332
|
+
* @param {{
|
|
333
|
+
* builderRoot: string,
|
|
334
|
+
* totalEntries: number,
|
|
335
|
+
* platformApps: string[],
|
|
336
|
+
* systemBuilderRoot?: string,
|
|
337
|
+
* systemPlatformApps?: string[]
|
|
338
|
+
* }} info
|
|
339
|
+
* @returns {Promise<'keep'|'keep-files'|'backup'|'abort'>}
|
|
340
|
+
*/
|
|
341
|
+
async function promptBuilderDirConflict(info) {
|
|
342
|
+
const builderRoot = info && typeof info.builderRoot === 'string' ? info.builderRoot : 'builder/';
|
|
343
|
+
const totalEntries = info && Number.isFinite(info.totalEntries) ? info.totalEntries : 0;
|
|
344
|
+
const platformApps = info && Array.isArray(info.platformApps) ? info.platformApps : [];
|
|
345
|
+
const sysRoot = info && typeof info.systemBuilderRoot === 'string' ? info.systemBuilderRoot : '';
|
|
346
|
+
const sysApps = info && Array.isArray(info.systemPlatformApps) ? info.systemPlatformApps : [];
|
|
347
|
+
const systemNote =
|
|
348
|
+
sysRoot && sysApps.length > 0
|
|
349
|
+
? `\n\nNon-empty platform app folder(s) also exist under:\n ${sysRoot}/\n (${sysApps.join(', ')})`
|
|
350
|
+
: '';
|
|
351
|
+
|
|
352
|
+
const warning =
|
|
353
|
+
'Existing builder folder detected:\n' +
|
|
354
|
+
` ${builderRoot}\n\n` +
|
|
355
|
+
`It contains ${totalEntries} item(s).` +
|
|
356
|
+
systemNote +
|
|
357
|
+
'\n\n' +
|
|
358
|
+
'This setup path will REPLACE the platform app folders under:\n' +
|
|
359
|
+
` ${platformApps.map(a => `builder/${a}/`).join('\n ')}`;
|
|
360
|
+
|
|
361
|
+
const { action } = await inquirer.prompt([
|
|
362
|
+
{
|
|
363
|
+
type: 'list',
|
|
364
|
+
name: 'action',
|
|
365
|
+
message: `${warning}\n\nHow do you want to proceed?`,
|
|
366
|
+
choices: [
|
|
367
|
+
{ name: 'Continue (platform app folders will be replaced)', value: BUILDER_DIR_ACTION.KEEP },
|
|
368
|
+
{ name: 'Keep platform app folders and continue (no replacement)', value: BUILDER_DIR_ACTION.KEEP_FILES },
|
|
369
|
+
{ name: 'Backup platform app folders under builder/ and continue (recommended)', value: BUILDER_DIR_ACTION.BACKUP },
|
|
370
|
+
{ name: 'Abort setup (no changes)', value: BUILDER_DIR_ACTION.ABORT }
|
|
371
|
+
],
|
|
372
|
+
default: BUILDER_DIR_ACTION.BACKUP
|
|
373
|
+
}
|
|
374
|
+
]);
|
|
375
|
+
return action;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
module.exports = {
|
|
379
|
+
MODE,
|
|
380
|
+
AI_KEYS,
|
|
381
|
+
BUILDER_DIR_ACTION,
|
|
382
|
+
promptModeSelection,
|
|
383
|
+
confirmDestructiveMode,
|
|
384
|
+
promptAdminCredentials,
|
|
385
|
+
promptAiTool,
|
|
386
|
+
detectAiToolStatus,
|
|
387
|
+
promptBuilderDirConflict
|
|
388
|
+
};
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Top-level handler for `aifabrix setup`.
|
|
3
|
+
*
|
|
4
|
+
* Decides between:
|
|
5
|
+
* - fresh install (no infra running) — wizard for admin creds + AI tool, then up-infra + up-platform.
|
|
6
|
+
* - mode menu (infra already running) — dispatch one of four destructive/refresh paths.
|
|
7
|
+
*
|
|
8
|
+
* The mode handlers live in `setup-modes.js`; the prompts live in
|
|
9
|
+
* `setup-prompts.js`. This module is a thin orchestrator so it stays well
|
|
10
|
+
* under the 50-line/function limit.
|
|
11
|
+
*
|
|
12
|
+
* @fileoverview aifabrix setup orchestrator
|
|
13
|
+
* @author AI Fabrix Team
|
|
14
|
+
* @version 2.0.0
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
'use strict';
|
|
18
|
+
|
|
19
|
+
const chalk = require('chalk');
|
|
20
|
+
|
|
21
|
+
const config = require('../core/config');
|
|
22
|
+
const infra = require('../infrastructure');
|
|
23
|
+
const logger = require('../utils/logger');
|
|
24
|
+
const {
|
|
25
|
+
formatSuccessParagraph,
|
|
26
|
+
formatProgress,
|
|
27
|
+
infoLine
|
|
28
|
+
} = require('../utils/cli-test-layout-chalk');
|
|
29
|
+
|
|
30
|
+
const setupPrompts = require('./setup-prompts');
|
|
31
|
+
const setupModes = require('./setup-modes');
|
|
32
|
+
|
|
33
|
+
const MODE = setupPrompts.MODE;
|
|
34
|
+
|
|
35
|
+
const SEPARATOR = '────────────────────────────────────────';
|
|
36
|
+
|
|
37
|
+
function title(text) {
|
|
38
|
+
return chalk.bold(text);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function logSetupHeader() {
|
|
42
|
+
logger.log('');
|
|
43
|
+
logger.log(title('AI Fabrix Setup'));
|
|
44
|
+
logger.log(SEPARATOR);
|
|
45
|
+
logger.log('');
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async function maybePinDeveloperIdForFreshInstall(rawDeveloper) {
|
|
49
|
+
if (rawDeveloper === undefined || rawDeveloper === null) return;
|
|
50
|
+
const dev = String(rawDeveloper).trim();
|
|
51
|
+
if (!dev) return;
|
|
52
|
+
await config.setDeveloperId(dev);
|
|
53
|
+
logger.log(infoLine(`Developer ID pinned to ${dev}.`));
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function runFreshInstallFlow(options) {
|
|
57
|
+
await maybePinDeveloperIdForFreshInstall(options.developer);
|
|
58
|
+
logger.log(infoLine('No infrastructure detected; starting fresh install.'));
|
|
59
|
+
const adminCreds = await setupPrompts.promptAdminCredentials();
|
|
60
|
+
await setupModes.runFreshInstall(adminCreds);
|
|
61
|
+
logger.log(formatSuccessParagraph('aifabrix setup complete (fresh install).'));
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async function runExistingInfraFlow(assumeYes) {
|
|
65
|
+
const mode = await setupPrompts.promptModeSelection();
|
|
66
|
+
const proceed = await setupPrompts.confirmDestructiveMode(mode, assumeYes);
|
|
67
|
+
if (!proceed) {
|
|
68
|
+
logger.log(chalk.yellow('Aborted by user.'));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
await dispatchMode(mode);
|
|
72
|
+
logger.log(formatSuccessParagraph(`aifabrix setup complete (mode: ${mode}).`));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Detect whether any infra service is currently running.
|
|
77
|
+
* Treats "running" or "healthy" container state as "infra is up".
|
|
78
|
+
*
|
|
79
|
+
* @async
|
|
80
|
+
* @returns {Promise<boolean>}
|
|
81
|
+
*/
|
|
82
|
+
async function isInfraRunning() {
|
|
83
|
+
let status;
|
|
84
|
+
try {
|
|
85
|
+
status = await infra.getInfraStatus();
|
|
86
|
+
} catch {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (!status || typeof status !== 'object') return false;
|
|
90
|
+
return Object.values(status).some(svc => {
|
|
91
|
+
if (!svc || typeof svc !== 'object') return false;
|
|
92
|
+
const s = String(svc.status || '').toLowerCase();
|
|
93
|
+
return s === 'running' || s === 'healthy' || s === 'starting';
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Dispatch a chosen mode handler.
|
|
99
|
+
* @async
|
|
100
|
+
* @param {string} mode - One of MODE.* values
|
|
101
|
+
* @returns {Promise<void>}
|
|
102
|
+
* @throws {Error} If mode is unknown
|
|
103
|
+
*/
|
|
104
|
+
async function dispatchMode(mode) {
|
|
105
|
+
switch (mode) {
|
|
106
|
+
case MODE.REINSTALL:
|
|
107
|
+
return setupModes.runReinstall();
|
|
108
|
+
case MODE.WIPE_DATA:
|
|
109
|
+
return setupModes.runWipeData();
|
|
110
|
+
case MODE.CLEAN_FILES:
|
|
111
|
+
return setupModes.runCleanInstallFiles();
|
|
112
|
+
case MODE.UPDATE_IMAGES:
|
|
113
|
+
return setupModes.runUpdateImages();
|
|
114
|
+
default:
|
|
115
|
+
throw new Error(`Unknown setup mode: ${mode}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Run the setup wizard / dispatcher.
|
|
121
|
+
*
|
|
122
|
+
* @async
|
|
123
|
+
* @function handleSetup
|
|
124
|
+
* @param {Object} [options] - Commander options
|
|
125
|
+
* @param {boolean} [options.yes] - Skip destructive confirmation prompts
|
|
126
|
+
* @param {string} [options.developer] - Pin developer ID before fresh install (fresh path only)
|
|
127
|
+
* @returns {Promise<void>}
|
|
128
|
+
* @throws {Error} If a sub-step fails (caller surfaces via handleCommandError)
|
|
129
|
+
*/
|
|
130
|
+
async function handleSetup(options = {}) {
|
|
131
|
+
const assumeYes = options.yes === true || options.assumeYes === true;
|
|
132
|
+
|
|
133
|
+
logSetupHeader();
|
|
134
|
+
|
|
135
|
+
logger.log(formatProgress('Detecting current installation state...'));
|
|
136
|
+
const running = await isInfraRunning();
|
|
137
|
+
|
|
138
|
+
if (!running) {
|
|
139
|
+
await runFreshInstallFlow(options);
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
await runExistingInfraFlow(assumeYes);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
module.exports = {
|
|
146
|
+
handleSetup,
|
|
147
|
+
isInfraRunning,
|
|
148
|
+
dispatchMode
|
|
149
|
+
};
|