@aifabrix/builder 2.49.0 → 2.49.2
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/process/README.md +9 -0
- package/.cursor/skills/README.md +14 -0
- package/lib/api/dev.api.js +1 -0
- package/lib/api/host-ops.api.js +69 -0
- package/lib/cli/infra-guided-auth.js +8 -6
- package/lib/cli/infra-guided.js +11 -2
- package/lib/cli/setup-dev-path-commands.js +24 -0
- package/lib/cli/setup-dev.js +108 -14
- package/lib/commands/dev-cli-handlers.js +64 -3
- package/lib/commands/dev-host-ops.js +239 -0
- package/lib/commands/teardown.js +3 -5
- package/lib/commands/up-common.js +8 -11
- package/lib/commands/up-dataplane.js +81 -13
- package/lib/commands/up-miso.js +6 -2
- package/lib/schema/external-system.schema.json +114 -2
- package/lib/utils/health-check-poll-ui.js +63 -28
- package/lib/utils/health-check.js +50 -21
- package/lib/utils/paths.js +26 -36
- package/package.json +1 -1
- package/templates/applications/dataplane/application.yaml +1 -1
- package/templates/applications/dataplane/env.template +2 -1
- package/templates/applications/miso-controller/env.template +2 -0
- package/.temp/jest-protection-convert/run-IFN2nv/.protection/hubspot-companies.json +0 -1
- package/.temp/jest-protection-convert/run-IvmKLO/.protection/hubspot-companies.yaml +0 -17
- package/.temp/jest-protection-convert/run-SdvblF/.protection/hubspot-companies.json +0 -1
- package/.temp/jest-protection-convert/run-b51TsW/.protection/hubspot-companies.json +0 -1
- package/.temp/jest-protection-convert/run-fiLmb1/.protection/hubspot-companies.json +0 -1
- package/.temp/jest-protection-convert/run-qYGHMU/.protection/hubspot-companies.yaml +0 -17
- package/.temp/jest-protection-fixtures/hubspot-Mq5re8/hubspot-companies.yaml +0 -17
- package/.temp/jest-protection-fixtures/hubspot-f3u4YL/hubspot-companies.yaml +0 -17
- package/.temp/jest-protection-fixtures/hubspot-iQtd1O/hubspot-companies.yaml +0 -17
- package/.temp/jest-protection-fixtures/hubspot-mdmz5M/hubspot-companies.yaml +0 -17
- package/.temp/jest-protection-fixtures/hubspot-qbKYxQ/hubspot-companies.yaml +0 -17
- package/.temp/jest-protection-fixtures/hubspot-yzlJy6/hubspot-companies.yaml +0 -17
- package/.temp/validation/summary.txt +0 -5
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Dev host-ops CLI handlers (platform-install, platform-update, etc.)
|
|
3
|
+
* @author AI Fabrix Team
|
|
4
|
+
* @version 1.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const readline = require('readline');
|
|
8
|
+
const chalk = require('chalk');
|
|
9
|
+
const logger = require('../utils/logger');
|
|
10
|
+
const { formatSuccessLine } = require('../utils/cli-test-layout-chalk');
|
|
11
|
+
const { getRemoteDevAuth } = require('../utils/remote-dev-auth');
|
|
12
|
+
const hostOpsApi = require('../api/host-ops.api');
|
|
13
|
+
|
|
14
|
+
const REMOTE_NOT_CONFIGURED_MSG =
|
|
15
|
+
'Remote server is not configured. Set remote-server and run "aifabrix dev init" first.';
|
|
16
|
+
|
|
17
|
+
const POLL_INTERVAL_MS = 3000;
|
|
18
|
+
const POLL_TIMEOUT_MS = 3 * 60 * 60 * 1000;
|
|
19
|
+
|
|
20
|
+
function sleep(ms) {
|
|
21
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Build job params from Commander options.
|
|
26
|
+
* @param {Object} options
|
|
27
|
+
* @param {string} [developerId]
|
|
28
|
+
* @returns {Object}
|
|
29
|
+
*/
|
|
30
|
+
function buildJobParams(options, developerId) {
|
|
31
|
+
const params = {};
|
|
32
|
+
const id = developerId || options.developerId || options['developer-id'];
|
|
33
|
+
if (id) params.developerId = String(id).trim();
|
|
34
|
+
if (options.include) params.include = options.include;
|
|
35
|
+
if (options.exclude) params.exclude = options.exclude;
|
|
36
|
+
if (options.builderVersion || options['builder-version']) {
|
|
37
|
+
params.builderVersion = options.builderVersion || options['builder-version'];
|
|
38
|
+
}
|
|
39
|
+
if (options.adminEmail || options['admin-email']) {
|
|
40
|
+
params.adminEmail = options.adminEmail || options['admin-email'];
|
|
41
|
+
}
|
|
42
|
+
if (options.adminPassword || options['admin-password']) {
|
|
43
|
+
params.adminPassword = options.adminPassword || options['admin-password'];
|
|
44
|
+
}
|
|
45
|
+
if (options.platform) params.platform = options.platform;
|
|
46
|
+
if (options.installation) params.installation = options.installation;
|
|
47
|
+
if (options.skipCert || options['skip-cert']) params.skipCert = true;
|
|
48
|
+
if (options.skipPlatform || options['skip-platform']) params.skipPlatform = true;
|
|
49
|
+
if (options.skipCli || options['skip-cli']) params.skipCli = true;
|
|
50
|
+
if (options.check) params.check = true;
|
|
51
|
+
if (options.failOnBlocked) params.failOnBlocked = true;
|
|
52
|
+
if (options.timeoutMinutes || options['timeout-minutes']) {
|
|
53
|
+
params.timeoutMinutes = parseInt(options.timeoutMinutes || options['timeout-minutes'], 10);
|
|
54
|
+
}
|
|
55
|
+
return params;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Poll host-ops job until terminal state.
|
|
60
|
+
* @param {Object} auth
|
|
61
|
+
* @param {string} jobId
|
|
62
|
+
* @param {Object} [opts]
|
|
63
|
+
* @param {boolean} [opts.wait=true]
|
|
64
|
+
* @param {boolean} [opts.json=false]
|
|
65
|
+
* @returns {Promise<Object>}
|
|
66
|
+
*/
|
|
67
|
+
async function waitForHostOpsJob(auth, jobId, opts = {}) {
|
|
68
|
+
const wait = opts.wait !== false;
|
|
69
|
+
const json = Boolean(opts.json);
|
|
70
|
+
if (!wait) {
|
|
71
|
+
return { jobId, status: 'pending' };
|
|
72
|
+
}
|
|
73
|
+
const started = Date.now();
|
|
74
|
+
while (Date.now() - started < POLL_TIMEOUT_MS) {
|
|
75
|
+
const job = await hostOpsApi.getHostOpsJob(
|
|
76
|
+
auth.serverUrl,
|
|
77
|
+
auth.clientCertPem,
|
|
78
|
+
jobId,
|
|
79
|
+
auth.serverCaPem || undefined
|
|
80
|
+
);
|
|
81
|
+
if (!json) {
|
|
82
|
+
logger.log(chalk.gray(` host-ops job ${jobId}: ${job.status}`));
|
|
83
|
+
}
|
|
84
|
+
if (job.status === 'completed' || job.status === 'failed') {
|
|
85
|
+
if (job.status === 'failed') {
|
|
86
|
+
throw new Error(job.error || `Host-ops job ${jobId} failed`);
|
|
87
|
+
}
|
|
88
|
+
return job;
|
|
89
|
+
}
|
|
90
|
+
await sleep(POLL_INTERVAL_MS);
|
|
91
|
+
}
|
|
92
|
+
throw new Error(`Timed out waiting for host-ops job ${jobId}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* @param {string} question
|
|
97
|
+
* @returns {Promise<boolean>}
|
|
98
|
+
*/
|
|
99
|
+
function promptYesNo(question) {
|
|
100
|
+
if (!process.stdin.isTTY) {
|
|
101
|
+
return Promise.resolve(false);
|
|
102
|
+
}
|
|
103
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
104
|
+
return new Promise((resolve) => {
|
|
105
|
+
rl.question(`${question} [y/N] `, (answer) => {
|
|
106
|
+
rl.close();
|
|
107
|
+
resolve(/^y(es)?$/i.test(String(answer || '').trim()));
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function platformAlreadyInstalled(outcomes) {
|
|
113
|
+
if (!Array.isArray(outcomes)) return false;
|
|
114
|
+
return outcomes.some(
|
|
115
|
+
(row) =>
|
|
116
|
+
row &&
|
|
117
|
+
(row.overall === 'SKIP' || row.status === 'SKIP') &&
|
|
118
|
+
String(row.detail || '').toLowerCase().includes('installed')
|
|
119
|
+
);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Run a host-ops job and optionally wait.
|
|
124
|
+
* @param {string} type
|
|
125
|
+
* @param {Object} params
|
|
126
|
+
* @param {Object} options - Commander options (yes, wait, json)
|
|
127
|
+
* @returns {Promise<Object>}
|
|
128
|
+
*/
|
|
129
|
+
async function runHostOpsJob(type, params, options = {}) {
|
|
130
|
+
const auth = await getRemoteDevAuth();
|
|
131
|
+
if (!auth) throw new Error(REMOTE_NOT_CONFIGURED_MSG);
|
|
132
|
+
const { jobId } = await hostOpsApi.createHostOpsJob(
|
|
133
|
+
auth.serverUrl,
|
|
134
|
+
auth.clientCertPem,
|
|
135
|
+
{ type, params },
|
|
136
|
+
auth.serverCaPem || undefined
|
|
137
|
+
);
|
|
138
|
+
if (options.json) {
|
|
139
|
+
const job = await waitForHostOpsJob(auth, jobId, { wait: options.wait !== false, json: true });
|
|
140
|
+
logger.log(JSON.stringify(job, null, 2));
|
|
141
|
+
return job;
|
|
142
|
+
}
|
|
143
|
+
const job = await waitForHostOpsJob(auth, jobId, { wait: options.wait !== false, json: false });
|
|
144
|
+
logger.log(formatSuccessLine(`Host-ops job ${jobId} completed (${type}).`));
|
|
145
|
+
return job;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function handlePlatformInstallCli(developerId, options) {
|
|
149
|
+
const params = buildJobParams(options, developerId);
|
|
150
|
+
if (options.check) {
|
|
151
|
+
params.check = true;
|
|
152
|
+
} else if (!params.builderVersion) {
|
|
153
|
+
throw new Error('--builder-version is required unless --check is set');
|
|
154
|
+
}
|
|
155
|
+
await runHostOpsJob('install-builder-cli', params, options);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function handlePlatformUpdate(developerId, options) {
|
|
159
|
+
const params = buildJobParams(options, developerId);
|
|
160
|
+
if (options.check) params.check = true;
|
|
161
|
+
await runHostOpsJob('update-dev-platform', params, options);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async function handlePlatformInstall(developerId, options) {
|
|
165
|
+
if (!developerId) {
|
|
166
|
+
throw new Error('Developer ID is required (positional argument).');
|
|
167
|
+
}
|
|
168
|
+
const params = buildJobParams(options, developerId);
|
|
169
|
+
|
|
170
|
+
if (options.check) {
|
|
171
|
+
params.check = true;
|
|
172
|
+
const auth = await getRemoteDevAuth();
|
|
173
|
+
if (!auth) throw new Error(REMOTE_NOT_CONFIGURED_MSG);
|
|
174
|
+
const { jobId } = await hostOpsApi.preflightHostOps(
|
|
175
|
+
auth.serverUrl,
|
|
176
|
+
auth.clientCertPem,
|
|
177
|
+
{ type: 'install-dev-platform', params },
|
|
178
|
+
auth.serverCaPem || undefined
|
|
179
|
+
);
|
|
180
|
+
const job = await waitForHostOpsJob(auth, jobId, options);
|
|
181
|
+
if (options.json) {
|
|
182
|
+
logger.log(JSON.stringify(job, null, 2));
|
|
183
|
+
} else if (Array.isArray(job.outcomes)) {
|
|
184
|
+
job.outcomes.forEach((row) => {
|
|
185
|
+
logger.log(` ${row.linuxUser || developerId}: ${row.overall || row.status}${row.detail ? ` — ${row.detail}` : ''}`);
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
return job;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
if (!options.force && !options.yes) {
|
|
192
|
+
const auth = await getRemoteDevAuth();
|
|
193
|
+
if (!auth) throw new Error(REMOTE_NOT_CONFIGURED_MSG);
|
|
194
|
+
const { jobId } = await hostOpsApi.preflightHostOps(
|
|
195
|
+
auth.serverUrl,
|
|
196
|
+
auth.clientCertPem,
|
|
197
|
+
{ type: 'install-dev-platform', params: { ...params, developerId } },
|
|
198
|
+
auth.serverCaPem || undefined
|
|
199
|
+
);
|
|
200
|
+
const preflight = await waitForHostOpsJob(auth, jobId, { ...options, json: false });
|
|
201
|
+
if (platformAlreadyInstalled(preflight.outcomes)) {
|
|
202
|
+
const ok = await promptYesNo(
|
|
203
|
+
`Platform already installed for dev${String(developerId).padStart(2, '0')}. Re-install?`
|
|
204
|
+
);
|
|
205
|
+
if (!ok) {
|
|
206
|
+
logger.log(chalk.gray('Cancelled.'));
|
|
207
|
+
return preflight;
|
|
208
|
+
}
|
|
209
|
+
await runHostOpsJob('reinstall-dev-platform', params, options);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const jobType = options.force ? 'reinstall-dev-platform' : 'install-dev-platform';
|
|
215
|
+
await runHostOpsJob(jobType, params, options);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async function handleProvisionDevUser(developerId, options) {
|
|
219
|
+
const params = buildJobParams(options, developerId);
|
|
220
|
+
await runHostOpsJob('provision-dev-user', params, options);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
async function handleRemoveDevUser(developerId, options) {
|
|
224
|
+
const params = buildJobParams(options, developerId);
|
|
225
|
+
if (!params.developerId) {
|
|
226
|
+
throw new Error('Developer ID is required.');
|
|
227
|
+
}
|
|
228
|
+
await runHostOpsJob('remove-dev-user', params, options);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
module.exports = {
|
|
232
|
+
buildJobParams,
|
|
233
|
+
waitForHostOpsJob,
|
|
234
|
+
handlePlatformInstallCli,
|
|
235
|
+
handlePlatformUpdate,
|
|
236
|
+
handlePlatformInstall,
|
|
237
|
+
handleProvisionDevUser,
|
|
238
|
+
handleRemoveDevUser
|
|
239
|
+
};
|
package/lib/commands/teardown.js
CHANGED
|
@@ -120,17 +120,15 @@ function cleanAifabrixSystemDir() {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
/**
|
|
123
|
-
* Remove materialized `builder/` trees
|
|
124
|
-
*
|
|
125
|
-
* the config directory.
|
|
123
|
+
* Remove materialized `builder/` trees under home/work and config dir only.
|
|
124
|
+
* Does not remove `cwd/builder` in product repos (e.g. aifabrix-dataplane/builder/dataplane).
|
|
126
125
|
*
|
|
127
126
|
* @returns {{ removed: string[], failed: string[] }}
|
|
128
127
|
*/
|
|
129
128
|
function cleanMaterializedBuilderTrees() {
|
|
130
129
|
const candidates = [
|
|
131
130
|
pathsUtil.getBuilderRoot(),
|
|
132
|
-
path.join(pathsUtil.getAifabrixSystemDir(), 'builder')
|
|
133
|
-
path.join(process.cwd(), 'builder')
|
|
131
|
+
path.join(pathsUtil.getAifabrixSystemDir(), 'builder')
|
|
134
132
|
];
|
|
135
133
|
const seen = new Set();
|
|
136
134
|
const removed = [];
|
|
@@ -242,26 +242,21 @@ function isUnderAllowedBuilderRoot(resolvedAppPath, allowedRoots) {
|
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
/**
|
|
245
|
-
* Removes
|
|
246
|
-
*
|
|
247
|
-
* Uses {@link pathsUtil.getBuilderPath} for each app (system apps may resolve under the config dir).
|
|
245
|
+
* Removes platform app directories under the home/work materialized `builder/` root only.
|
|
246
|
+
* Never deletes `cwd/builder` trees in product repos (e.g. aifabrix-miso/builder/miso-controller).
|
|
248
247
|
*
|
|
249
248
|
* @param {string[]} appNames - Application names (e.g. ['keycloak', 'miso-controller', 'dataplane'])
|
|
250
249
|
* @returns {Promise<void>}
|
|
251
|
-
* @throws {Error} If any path is outside
|
|
250
|
+
* @throws {Error} If any path is outside the home builder root (path traversal attempt)
|
|
252
251
|
*/
|
|
253
252
|
async function cleanBuilderAppDirs(appNames, opts = {}) {
|
|
254
253
|
const silent = Boolean(opts.silent);
|
|
255
254
|
if (!Array.isArray(appNames) || appNames.length === 0) return;
|
|
256
|
-
const allowedRoots = uniqueResolvedRoots([
|
|
257
|
-
path.join(process.cwd(), 'builder'),
|
|
258
|
-
pathsUtil.getBuilderRoot(),
|
|
259
|
-
pathsUtil.getSystemBuilderRoot()
|
|
260
|
-
]);
|
|
255
|
+
const allowedRoots = uniqueResolvedRoots([pathsUtil.getSystemBuilderRoot()]);
|
|
261
256
|
const cleaned = [];
|
|
262
257
|
for (const appName of appNames) {
|
|
263
258
|
if (!appName || typeof appName !== 'string') continue;
|
|
264
|
-
const appPath = path.resolve(pathsUtil.
|
|
259
|
+
const appPath = path.resolve(pathsUtil.getHomeBuilderAppPath(appName));
|
|
265
260
|
if (!isUnderAllowedBuilderRoot(appPath, allowedRoots)) {
|
|
266
261
|
throw new Error(
|
|
267
262
|
`Path ${appPath} is outside allowed builder roots (${allowedRoots.join(', ')}); refusing to clean`
|
|
@@ -303,7 +298,9 @@ async function ensureAppFromTemplate(appName, opts = {}) {
|
|
|
303
298
|
}
|
|
304
299
|
const silent = Boolean(opts.silent);
|
|
305
300
|
|
|
306
|
-
const appPath = pathsUtil.
|
|
301
|
+
const appPath = pathsUtil.isSystemBuilderAppName(appName)
|
|
302
|
+
? pathsUtil.getHomeBuilderAppPath(appName)
|
|
303
|
+
: pathsUtil.getBuilderPath(appName);
|
|
307
304
|
const primaryCopied = await ensureTemplateAtPath(appName, appPath);
|
|
308
305
|
if (primaryCopied && !silent) {
|
|
309
306
|
logger.log(chalk.blue(`Creating builder/${appName} from template...`));
|
|
@@ -41,6 +41,17 @@ const {
|
|
|
41
41
|
} = require('./up-dataplane-bootstrap');
|
|
42
42
|
|
|
43
43
|
const CONTROLLER_HEALTH_PATH = '/health';
|
|
44
|
+
/** Automation / af-server: wait for edge routing before dataplane (nginx + Traefik). */
|
|
45
|
+
const CONTROLLER_HEALTH_RETRY_ATTEMPTS = 30;
|
|
46
|
+
const CONTROLLER_HEALTH_RETRY_DELAY_MS = 2000;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* @param {number} ms
|
|
50
|
+
* @returns {Promise<void>}
|
|
51
|
+
*/
|
|
52
|
+
function sleep(ms) {
|
|
53
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
54
|
+
}
|
|
44
55
|
|
|
45
56
|
/**
|
|
46
57
|
* Check if controller is reachable (health endpoint).
|
|
@@ -76,19 +87,46 @@ function promptForControllerUrl(currentUrl) {
|
|
|
76
87
|
}
|
|
77
88
|
|
|
78
89
|
/**
|
|
79
|
-
*
|
|
80
|
-
* @
|
|
81
|
-
* @
|
|
90
|
+
* Poll controller /health until success or attempts exhausted.
|
|
91
|
+
* @param {string} baseUrl - Controller base URL (no trailing slash)
|
|
92
|
+
* @param {{ attempts?: number, delayMs?: number }} [opts]
|
|
93
|
+
* @returns {Promise<boolean>}
|
|
82
94
|
*/
|
|
83
|
-
async function
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
let
|
|
88
|
-
|
|
89
|
-
|
|
95
|
+
async function waitForControllerHealthy(baseUrl, opts = {}) {
|
|
96
|
+
const attempts = opts.attempts ?? CONTROLLER_HEALTH_RETRY_ATTEMPTS;
|
|
97
|
+
const delayMs = opts.delayMs ?? CONTROLLER_HEALTH_RETRY_DELAY_MS;
|
|
98
|
+
const normalized = baseUrl.replace(/\/+$/, '');
|
|
99
|
+
for (let i = 0; i < attempts; i += 1) {
|
|
100
|
+
if (await isControllerHealthy(normalized)) {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
if (i < attempts - 1) {
|
|
104
|
+
await sleep(delayMs);
|
|
105
|
+
}
|
|
90
106
|
}
|
|
107
|
+
return false;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* @param {string} controllerUrl
|
|
112
|
+
* @returns {string}
|
|
113
|
+
*/
|
|
114
|
+
function buildControllerUnavailableMessage(controllerUrl) {
|
|
115
|
+
return (
|
|
116
|
+
`Controller at ${controllerUrl} is not responding after ${CONTROLLER_HEALTH_RETRY_ATTEMPTS} health checks ` +
|
|
117
|
+
`(~${Math.round((CONTROLLER_HEALTH_RETRY_ATTEMPTS * CONTROLLER_HEALTH_RETRY_DELAY_MS) / 1000)}s). ` +
|
|
118
|
+
'Miso may be up on localhost before nginx/Traefik routes are ready. ' +
|
|
119
|
+
'On the builder host run: sudo systemctl start aifabrix-sync-dev-traefik-nginx.service ' +
|
|
120
|
+
'then retry: aifabrix up-dataplane --force'
|
|
121
|
+
);
|
|
122
|
+
}
|
|
91
123
|
|
|
124
|
+
/**
|
|
125
|
+
* Prompt for a new controller URL and verify it responds to health checks.
|
|
126
|
+
* @param {string} controllerUrl - URL that failed the initial health check
|
|
127
|
+
* @returns {Promise<string>} Verified controller URL
|
|
128
|
+
*/
|
|
129
|
+
async function promptForHealthyControllerUrl(controllerUrl) {
|
|
92
130
|
logger.log(chalk.yellow(`\nController at ${controllerUrl} is not responding (health check failed).\n`));
|
|
93
131
|
const newUrl = await promptForControllerUrl(controllerUrl);
|
|
94
132
|
if (!newUrl) {
|
|
@@ -103,7 +141,7 @@ async function resolveControllerUrlWithHealthCheck() {
|
|
|
103
141
|
|
|
104
142
|
await setControllerUrl(newUrl);
|
|
105
143
|
const normalizedNew = newUrl.trim().replace(/\/+$/, '');
|
|
106
|
-
healthy = await isControllerHealthy(normalizedNew);
|
|
144
|
+
const healthy = await isControllerHealthy(normalizedNew);
|
|
107
145
|
if (!healthy) {
|
|
108
146
|
throw new Error(`Controller at ${normalizedNew} is not responding. Ensure the controller is running and reachable, then run "aifabrix up-dataplane" again.`);
|
|
109
147
|
}
|
|
@@ -112,6 +150,28 @@ async function resolveControllerUrlWithHealthCheck() {
|
|
|
112
150
|
return normalizedNew;
|
|
113
151
|
}
|
|
114
152
|
|
|
153
|
+
/**
|
|
154
|
+
* Resolve controller URL and ensure it is healthy; if not, prompt once for new URL (TTY only).
|
|
155
|
+
* @returns {Promise<string>} Controller URL to use
|
|
156
|
+
* @throws {Error} If controller unavailable and user aborts or new URL still unhealthy
|
|
157
|
+
*/
|
|
158
|
+
async function resolveControllerUrlWithHealthCheck() {
|
|
159
|
+
let controllerUrl = await resolveControllerUrl();
|
|
160
|
+
controllerUrl = controllerUrl.replace(/\/+$/, '');
|
|
161
|
+
|
|
162
|
+
const healthy = await waitForControllerHealthy(controllerUrl);
|
|
163
|
+
if (healthy) {
|
|
164
|
+
return controllerUrl;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const isInteractive = Boolean(process.stdin && process.stdin.isTTY);
|
|
168
|
+
if (!isInteractive) {
|
|
169
|
+
throw new Error(buildControllerUnavailableMessage(controllerUrl));
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return promptForHealthyControllerUrl(controllerUrl);
|
|
173
|
+
}
|
|
174
|
+
|
|
115
175
|
async function resolveUpDataplaneAuthContext(options, environmentKey) {
|
|
116
176
|
if (options.skipInfraCheck !== true) {
|
|
117
177
|
await assertDevInfraUp();
|
|
@@ -327,14 +387,22 @@ async function handleUpDataplane(options = {}) {
|
|
|
327
387
|
|
|
328
388
|
const { runEnvKey, portOverride } = await computeDataplaneRunOverrides(options);
|
|
329
389
|
|
|
330
|
-
|
|
390
|
+
const runAppOpts = {
|
|
331
391
|
skipEnvOutputPath: true,
|
|
332
392
|
env: runEnvKey,
|
|
333
393
|
port: portOverride,
|
|
334
394
|
registry: options.registry || undefined,
|
|
335
395
|
base: options.base !== false,
|
|
336
396
|
skipManifestMetadataLine: true
|
|
337
|
-
}
|
|
397
|
+
};
|
|
398
|
+
if (options.platformInstall === true) {
|
|
399
|
+
if (typeof options.onPollText === 'function') {
|
|
400
|
+
runAppOpts.onPollText = options.onPollText;
|
|
401
|
+
} else {
|
|
402
|
+
runAppOpts.silentPoll = true;
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
await app.runApp('dataplane', runAppOpts);
|
|
338
406
|
|
|
339
407
|
if (options.platformInstall !== true) {
|
|
340
408
|
logger.log(formatSuccessParagraph('up-dataplane complete. Dataplane is registered, deployed in dev, and running locally.'));
|
package/lib/commands/up-miso.js
CHANGED
|
@@ -57,11 +57,15 @@ async function runMisoApps(options) {
|
|
|
57
57
|
base: useBaseImage,
|
|
58
58
|
skipManifestMetadataLine: true
|
|
59
59
|
};
|
|
60
|
-
const
|
|
60
|
+
const platformPollOpts =
|
|
61
|
+
options.platformInstall === true
|
|
62
|
+
? { silentPoll: true }
|
|
63
|
+
: {};
|
|
64
|
+
const keycloakRunOpts = { ...common, ...platformPollOpts };
|
|
61
65
|
if (imageMap.keycloak) {
|
|
62
66
|
keycloakRunOpts.image = imageMap.keycloak;
|
|
63
67
|
}
|
|
64
|
-
const misoRunOpts = { ...common };
|
|
68
|
+
const misoRunOpts = { ...common, ...platformPollOpts };
|
|
65
69
|
if (imageMap['miso-controller']) {
|
|
66
70
|
misoRunOpts.image = imageMap['miso-controller'];
|
|
67
71
|
}
|
|
@@ -212,7 +212,8 @@
|
|
|
212
212
|
"compileCom",
|
|
213
213
|
"generateEnterpriseDatasources",
|
|
214
214
|
"generateRestContracts",
|
|
215
|
-
"generateMcpContracts"
|
|
215
|
+
"generateMcpContracts",
|
|
216
|
+
"runDigitalWorkerTask"
|
|
216
217
|
],
|
|
217
218
|
"description": "Platform task type executed when the subscription fires."
|
|
218
219
|
},
|
|
@@ -221,6 +222,104 @@
|
|
|
221
222
|
"minimum": 1,
|
|
222
223
|
"default": 1,
|
|
223
224
|
"description": "Task definition version snapshot for audit."
|
|
225
|
+
},
|
|
226
|
+
"digitalWorkerKey": {
|
|
227
|
+
"type": "string",
|
|
228
|
+
"minLength": 1,
|
|
229
|
+
"description": "Target digital worker system key (runDigitalWorkerTask)."
|
|
230
|
+
},
|
|
231
|
+
"taskType": {
|
|
232
|
+
"type": "string",
|
|
233
|
+
"minLength": 1,
|
|
234
|
+
"description": "Worker task type label (runDigitalWorkerTask)."
|
|
235
|
+
},
|
|
236
|
+
"objectiveTemplate": {
|
|
237
|
+
"type": "string",
|
|
238
|
+
"description": "Objective template with {{recordId}} placeholders."
|
|
239
|
+
},
|
|
240
|
+
"requiresApproval": {
|
|
241
|
+
"type": "boolean",
|
|
242
|
+
"default": false
|
|
243
|
+
},
|
|
244
|
+
"owner": {
|
|
245
|
+
"$ref": "#/$defs/subscriptionTaskOwner"
|
|
246
|
+
}
|
|
247
|
+
},
|
|
248
|
+
"additionalProperties": false
|
|
249
|
+
},
|
|
250
|
+
"subscriptionCondition": {
|
|
251
|
+
"type": "object",
|
|
252
|
+
"required": [
|
|
253
|
+
"field",
|
|
254
|
+
"operator"
|
|
255
|
+
],
|
|
256
|
+
"properties": {
|
|
257
|
+
"field": {
|
|
258
|
+
"type": "string",
|
|
259
|
+
"minLength": 1,
|
|
260
|
+
"description": "Path against afterSummary (default) or before.* prefix."
|
|
261
|
+
},
|
|
262
|
+
"operator": {
|
|
263
|
+
"type": "string",
|
|
264
|
+
"enum": [
|
|
265
|
+
"equals",
|
|
266
|
+
"notEquals",
|
|
267
|
+
"in",
|
|
268
|
+
"notIn",
|
|
269
|
+
"exists",
|
|
270
|
+
"missing",
|
|
271
|
+
"changed",
|
|
272
|
+
"greaterThan",
|
|
273
|
+
"lessThan"
|
|
274
|
+
]
|
|
275
|
+
},
|
|
276
|
+
"value": {}
|
|
277
|
+
},
|
|
278
|
+
"additionalProperties": false
|
|
279
|
+
},
|
|
280
|
+
"subscriptionTriggerPolicy": {
|
|
281
|
+
"type": "object",
|
|
282
|
+
"required": [
|
|
283
|
+
"mode"
|
|
284
|
+
],
|
|
285
|
+
"properties": {
|
|
286
|
+
"mode": {
|
|
287
|
+
"type": "string",
|
|
288
|
+
"enum": [
|
|
289
|
+
"immediate",
|
|
290
|
+
"debounce",
|
|
291
|
+
"scheduled"
|
|
292
|
+
]
|
|
293
|
+
},
|
|
294
|
+
"windowSeconds": {
|
|
295
|
+
"type": "integer",
|
|
296
|
+
"minimum": 1,
|
|
297
|
+
"description": "Required when mode is debounce."
|
|
298
|
+
}
|
|
299
|
+
},
|
|
300
|
+
"additionalProperties": false
|
|
301
|
+
},
|
|
302
|
+
"subscriptionTaskOwner": {
|
|
303
|
+
"type": "object",
|
|
304
|
+
"required": [
|
|
305
|
+
"mode"
|
|
306
|
+
],
|
|
307
|
+
"properties": {
|
|
308
|
+
"mode": {
|
|
309
|
+
"type": "string",
|
|
310
|
+
"enum": [
|
|
311
|
+
"explicit",
|
|
312
|
+
"recordField"
|
|
313
|
+
]
|
|
314
|
+
},
|
|
315
|
+
"userId": {
|
|
316
|
+
"type": "string",
|
|
317
|
+
"minLength": 1
|
|
318
|
+
},
|
|
319
|
+
"field": {
|
|
320
|
+
"type": "string",
|
|
321
|
+
"minLength": 1,
|
|
322
|
+
"description": "Record summary field for recordField mode."
|
|
224
323
|
}
|
|
225
324
|
},
|
|
226
325
|
"additionalProperties": false
|
|
@@ -251,11 +350,24 @@
|
|
|
251
350
|
"event": {
|
|
252
351
|
"type": "string",
|
|
253
352
|
"minLength": 1,
|
|
254
|
-
"description": "V1 event namespace (platform
|
|
353
|
+
"description": "V1 event namespace (platform.*, schedule.*, or record.*)."
|
|
255
354
|
},
|
|
256
355
|
"filter": {
|
|
257
356
|
"$ref": "#/$defs/subscriptionFilter"
|
|
258
357
|
},
|
|
358
|
+
"conditions": {
|
|
359
|
+
"type": "array",
|
|
360
|
+
"items": {
|
|
361
|
+
"$ref": "#/$defs/subscriptionCondition"
|
|
362
|
+
}
|
|
363
|
+
},
|
|
364
|
+
"triggerPolicy": {
|
|
365
|
+
"$ref": "#/$defs/subscriptionTriggerPolicy"
|
|
366
|
+
},
|
|
367
|
+
"deduplicationKeyTemplate": {
|
|
368
|
+
"type": "string",
|
|
369
|
+
"description": "Default: {{subscriptionId}}:{{resourceType}}:{{recordId}}:{{task.taskType}}"
|
|
370
|
+
},
|
|
259
371
|
"task": {
|
|
260
372
|
"$ref": "#/$defs/subscriptionTask"
|
|
261
373
|
},
|