@aifabrix/builder 2.49.0 → 2.49.1
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/skills/README.md +14 -0
- package/lib/cli/infra-guided-auth.js +8 -6
- package/lib/cli/infra-guided.js +11 -2
- 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/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,14 @@
|
|
|
1
|
+
# aifabrix-builder Cursor skills
|
|
2
|
+
|
|
3
|
+
| Skill | Command | Purpose |
|
|
4
|
+
| ----- | ------- | ------- |
|
|
5
|
+
| [builder-push-github](builder-push-github/SKILL.md) | `/push-github` | Release branch CI + optional NPM deploy PR |
|
|
6
|
+
| [builder-repair-release](builder-repair-release/SKILL.md) | `/repair-release` | Version bump + CHANGELOG |
|
|
7
|
+
| [builder-validate-tests](builder-validate-tests/SKILL.md) | `/validate-tests` | Lint + Jest fix loop |
|
|
8
|
+
| [builder-validate-tests-ci](builder-validate-tests-ci/SKILL.md) | `/validate-tests.ci` | `build:ci` / GitHub parity |
|
|
9
|
+
| [builder-validate-code](builder-validate-code/SKILL.md) | `/validate-code` | Code analysis → improvement plans |
|
|
10
|
+
| [builder-validate-plan](builder-validate-plan/SKILL.md) | `/validate-plan` | Pre-implementation plan validation |
|
|
11
|
+
| [builder-validate-implementation](builder-validate-implementation/SKILL.md) | `/validate-implementation` | Post-implementation plan validation |
|
|
12
|
+
| [builder-validate-knowledgebase](builder-validate-knowledgebase/SKILL.md) | `/validate-knowledgebase` | `docs/` + schema validation |
|
|
13
|
+
|
|
14
|
+
Commands live in `.cursor/commands/`.
|
|
@@ -123,10 +123,14 @@ async function runGuidedDataplaneWithAuthRecovery(
|
|
|
123
123
|
ui = {}
|
|
124
124
|
) {
|
|
125
125
|
const onSpinnerText = typeof ui.onSpinnerText === 'function' ? ui.onSpinnerText : null;
|
|
126
|
+
const dataplaneOpts = {
|
|
127
|
+
...options,
|
|
128
|
+
platformInstall: true,
|
|
129
|
+
skipInfraCheck: true,
|
|
130
|
+
...(onSpinnerText ? { onPollText: onSpinnerText } : {})
|
|
131
|
+
};
|
|
126
132
|
try {
|
|
127
|
-
await withMutedLogger(() =>
|
|
128
|
-
handleUpDataplane({ ...options, platformInstall: true, skipInfraCheck: true })
|
|
129
|
-
);
|
|
133
|
+
await withMutedLogger(() => handleUpDataplane(dataplaneOpts));
|
|
130
134
|
} catch (error) {
|
|
131
135
|
if (!isAuthenticationError(error)) {
|
|
132
136
|
throw error;
|
|
@@ -154,9 +158,7 @@ async function runGuidedDataplaneWithAuthRecovery(
|
|
|
154
158
|
if (onSpinnerText) {
|
|
155
159
|
onSpinnerText('Validating Dataplane...');
|
|
156
160
|
}
|
|
157
|
-
await withMutedLogger(() =>
|
|
158
|
-
handleUpDataplane({ ...options, platformInstall: true, skipInfraCheck: true })
|
|
159
|
-
);
|
|
161
|
+
await withMutedLogger(() => handleUpDataplane(dataplaneOpts));
|
|
160
162
|
}
|
|
161
163
|
}
|
|
162
164
|
|
package/lib/cli/infra-guided.js
CHANGED
|
@@ -104,6 +104,9 @@ async function waitForAppReady(appName, opts = {}) {
|
|
|
104
104
|
if (userCfg && userCfg.traefik === true) {
|
|
105
105
|
healthRunOpts.traefikEnabled = true;
|
|
106
106
|
}
|
|
107
|
+
if (typeof opts.onPollText === 'function') {
|
|
108
|
+
healthRunOpts.onPollText = opts.onPollText;
|
|
109
|
+
}
|
|
107
110
|
await healthCheck.waitForHealthCheck(appName, timeoutSeconds, hostPort, appConfig, false, healthRunOpts);
|
|
108
111
|
}
|
|
109
112
|
|
|
@@ -180,7 +183,10 @@ async function runGuidedUpMiso(options, handleUpMiso) {
|
|
|
180
183
|
stopSpinnerSuccess(kcSpin, 'Keycloak ready');
|
|
181
184
|
logger.log('');
|
|
182
185
|
const mcSpin = startSpinner('Starting Miso Controller...');
|
|
183
|
-
await waitForAppReady('miso-controller', {
|
|
186
|
+
await waitForAppReady('miso-controller', {
|
|
187
|
+
timeoutSeconds: 120,
|
|
188
|
+
onPollText: createSpinnerTextUpdater(mcSpin)
|
|
189
|
+
});
|
|
184
190
|
stopSpinnerSuccess(mcSpin, 'Miso Controller ready');
|
|
185
191
|
await logMisoReadyFooter();
|
|
186
192
|
await validateAppErrorLogs(['miso-controller']);
|
|
@@ -242,7 +248,10 @@ async function runGuidedKeycloakAndMisoControllerPhase(options, handleUpMiso) {
|
|
|
242
248
|
logger.log('');
|
|
243
249
|
emitSystemBuilderAppManifestLineIfTTY(logger, 'miso-controller');
|
|
244
250
|
const mcSpin = startSpinner('Starting Miso Controller...');
|
|
245
|
-
await waitForAppReady('miso-controller', {
|
|
251
|
+
await waitForAppReady('miso-controller', {
|
|
252
|
+
timeoutSeconds: 120,
|
|
253
|
+
onPollText: createSpinnerTextUpdater(mcSpin)
|
|
254
|
+
});
|
|
246
255
|
stopSpinnerSuccess(mcSpin, 'Miso Controller ready');
|
|
247
256
|
logger.log('');
|
|
248
257
|
}
|
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
|
}
|
|
@@ -26,36 +26,50 @@ function buildHealthCheckPollText(attemptIndex, maxAttempts) {
|
|
|
26
26
|
);
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
29
|
+
function createSilentHealthCheckPollHandlers() {
|
|
30
|
+
return {
|
|
31
|
+
usesSpinner: false,
|
|
32
|
+
onWaiting: () => {},
|
|
33
|
+
finish: () => {}
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function createDelegatedHealthCheckPollHandlers(maxAttempts, onPollText) {
|
|
38
|
+
return {
|
|
39
|
+
usesSpinner: false,
|
|
40
|
+
onWaiting: (attemptIndex) => {
|
|
41
|
+
onPollText(buildHealthCheckPollText(attemptIndex, maxAttempts));
|
|
42
|
+
},
|
|
43
|
+
finish: () => {}
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function createOraHealthCheckPollHandlers(maxAttempts) {
|
|
48
|
+
const ora = require('ora');
|
|
49
|
+
const spinner = ora({
|
|
50
|
+
text: buildHealthCheckPollText(0, maxAttempts),
|
|
51
|
+
spinner: 'dots'
|
|
52
|
+
});
|
|
53
|
+
let started = false;
|
|
54
|
+
return {
|
|
55
|
+
usesSpinner: true,
|
|
56
|
+
onWaiting: (attemptIndex) => {
|
|
57
|
+
if (!started) {
|
|
58
|
+
spinner.start();
|
|
59
|
+
started = true;
|
|
55
60
|
}
|
|
56
|
-
|
|
57
|
-
|
|
61
|
+
spinner.text = buildHealthCheckPollText(attemptIndex, maxAttempts);
|
|
62
|
+
},
|
|
63
|
+
finish: () => {
|
|
64
|
+
if (started) {
|
|
65
|
+
spinner.stop();
|
|
66
|
+
process.stdout.write('\n');
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
};
|
|
70
|
+
}
|
|
58
71
|
|
|
72
|
+
function createCarriageReturnHealthCheckPollHandlers(maxAttempts) {
|
|
59
73
|
let lastLine = '';
|
|
60
74
|
return {
|
|
61
75
|
usesSpinner: false,
|
|
@@ -77,6 +91,27 @@ function createHealthCheckPollHandlers(maxAttempts) {
|
|
|
77
91
|
};
|
|
78
92
|
}
|
|
79
93
|
|
|
94
|
+
/**
|
|
95
|
+
* @param {number} maxAttempts
|
|
96
|
+
* @param {{ silent?: boolean, onPollText?: (text: string) => void }} [options]
|
|
97
|
+
* @returns {{ usesSpinner: boolean, onWaiting: (attemptIndex: number) => void, finish: () => void }}
|
|
98
|
+
*/
|
|
99
|
+
function createHealthCheckPollHandlers(maxAttempts, options = {}) {
|
|
100
|
+
const silent = options && options.silent === true;
|
|
101
|
+
const onPollText = options && typeof options.onPollText === 'function' ? options.onPollText : null;
|
|
102
|
+
|
|
103
|
+
if (silent) {
|
|
104
|
+
return createSilentHealthCheckPollHandlers();
|
|
105
|
+
}
|
|
106
|
+
if (onPollText) {
|
|
107
|
+
return createDelegatedHealthCheckPollHandlers(maxAttempts, onPollText);
|
|
108
|
+
}
|
|
109
|
+
if (shouldUseHealthCheckPollSpinner()) {
|
|
110
|
+
return createOraHealthCheckPollHandlers(maxAttempts);
|
|
111
|
+
}
|
|
112
|
+
return createCarriageReturnHealthCheckPollHandlers(maxAttempts);
|
|
113
|
+
}
|
|
114
|
+
|
|
80
115
|
module.exports = {
|
|
81
116
|
buildHealthCheckPollText,
|
|
82
117
|
createHealthCheckPollHandlers,
|
|
@@ -331,29 +331,26 @@ async function performHealthCheckAttemptForUrls(
|
|
|
331
331
|
return { ok: false, resolvedIndex: -1 };
|
|
332
332
|
}
|
|
333
333
|
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
healthCheckPort,
|
|
342
|
-
config,
|
|
343
|
-
runOptions,
|
|
344
|
-
debug
|
|
345
|
-
);
|
|
346
|
-
|
|
347
|
-
if (skippedPublicHealthUrl && urlsToTry.length === 1 && debug) {
|
|
348
|
-
logger.log(
|
|
349
|
-
chalk.gray(
|
|
350
|
-
`ℹ Health check: public URL not used (DNS): ${skippedPublicHealthUrl}. ` +
|
|
351
|
-
`Probing ${urlsToTry[0]} only until the app responds.`
|
|
352
|
-
)
|
|
353
|
-
);
|
|
334
|
+
function resolveHealthCheckPollOptions(runOptions = {}) {
|
|
335
|
+
const pollOptions = {};
|
|
336
|
+
if (runOptions.silentPoll === true) {
|
|
337
|
+
pollOptions.silent = true;
|
|
338
|
+
}
|
|
339
|
+
if (typeof runOptions.onPollText === 'function') {
|
|
340
|
+
pollOptions.onPollText = runOptions.onPollText;
|
|
354
341
|
}
|
|
342
|
+
return pollOptions;
|
|
343
|
+
}
|
|
355
344
|
|
|
356
|
-
|
|
345
|
+
async function pollUntilHealthCheckPasses({
|
|
346
|
+
urlsToTry,
|
|
347
|
+
maxAttempts,
|
|
348
|
+
timeout,
|
|
349
|
+
debug,
|
|
350
|
+
skippedPublicHealthUrl,
|
|
351
|
+
pollOptions = {}
|
|
352
|
+
}) {
|
|
353
|
+
const poll = createHealthCheckPollHandlers(maxAttempts, pollOptions);
|
|
357
354
|
const pollAttemptOpts = { suppressSuccessLog: true };
|
|
358
355
|
|
|
359
356
|
for (let attempts = 0; attempts < maxAttempts; attempts++) {
|
|
@@ -389,6 +386,38 @@ async function waitForHealthCheck(appName, timeout = 90, port = null, config = n
|
|
|
389
386
|
throw new Error(`Health check timeout after ${timeout} seconds`);
|
|
390
387
|
}
|
|
391
388
|
|
|
389
|
+
async function waitForHealthCheck(appName, timeout = 90, port = null, config = null, debug = false, runOptions = {}) {
|
|
390
|
+
await waitForDbInit(appName);
|
|
391
|
+
|
|
392
|
+
const healthCheckPort = await determineHealthCheckPort(port, appName, debug);
|
|
393
|
+
const { maxAttempts } = buildHealthCheckConfig(healthCheckPort, config, timeout, debug);
|
|
394
|
+
const { urlsToTry, skippedPublicHealthUrl } = await computePreferredHealthCheckUrls(
|
|
395
|
+
appName,
|
|
396
|
+
healthCheckPort,
|
|
397
|
+
config,
|
|
398
|
+
runOptions,
|
|
399
|
+
debug
|
|
400
|
+
);
|
|
401
|
+
|
|
402
|
+
if (skippedPublicHealthUrl && urlsToTry.length === 1 && debug) {
|
|
403
|
+
logger.log(
|
|
404
|
+
chalk.gray(
|
|
405
|
+
`ℹ Health check: public URL not used (DNS): ${skippedPublicHealthUrl}. ` +
|
|
406
|
+
`Probing ${urlsToTry[0]} only until the app responds.`
|
|
407
|
+
)
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
await pollUntilHealthCheckPasses({
|
|
412
|
+
urlsToTry,
|
|
413
|
+
maxAttempts,
|
|
414
|
+
timeout,
|
|
415
|
+
debug,
|
|
416
|
+
skippedPublicHealthUrl,
|
|
417
|
+
pollOptions: resolveHealthCheckPollOptions(runOptions)
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
|
|
392
421
|
/**
|
|
393
422
|
* Checks if port is available
|
|
394
423
|
* @param {number} port - Port number to check
|
package/lib/utils/paths.js
CHANGED
|
@@ -459,13 +459,23 @@ function isSystemBuilderAppName(appName) {
|
|
|
459
459
|
}
|
|
460
460
|
|
|
461
461
|
/**
|
|
462
|
-
*
|
|
462
|
+
* Home / work materialization path: `(aifabrix-work | aifabrix-home)/builder/<appName>`.
|
|
463
|
+
* Used by setup, teardown, and template copy — never the product-repo `cwd/builder` tree.
|
|
463
464
|
*
|
|
464
465
|
* @param {string} appName
|
|
465
466
|
* @returns {string}
|
|
466
467
|
*/
|
|
468
|
+
function getHomeBuilderAppPath(appName) {
|
|
469
|
+
return path.join(getAppsMaterializationParent(), 'builder', appName);
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* @deprecated Prefer {@link getHomeBuilderAppPath}; same path.
|
|
474
|
+
* @param {string} appName
|
|
475
|
+
* @returns {string}
|
|
476
|
+
*/
|
|
467
477
|
function getProjectBuilderAppPath(appName) {
|
|
468
|
-
return
|
|
478
|
+
return getHomeBuilderAppPath(appName);
|
|
469
479
|
}
|
|
470
480
|
|
|
471
481
|
/**
|
|
@@ -630,38 +640,25 @@ function resolveBuildContext(configDir, buildContext) {
|
|
|
630
640
|
}
|
|
631
641
|
|
|
632
642
|
/**
|
|
633
|
-
* `cwd/builder/<appName>` when
|
|
643
|
+
* `cwd/builder/<appName>` when the running folder has a resolvable application manifest.
|
|
634
644
|
*
|
|
635
645
|
* @param {string} appName
|
|
636
646
|
* @returns {string|null}
|
|
637
647
|
*/
|
|
638
|
-
function
|
|
639
|
-
|
|
640
|
-
try {
|
|
641
|
-
if (!nodeFs().existsSync(cwdApp)) {
|
|
642
|
-
return null;
|
|
643
|
-
}
|
|
644
|
-
const st = nodeFs().statSync(cwdApp);
|
|
645
|
-
if (!st || typeof st.isDirectory !== 'function' || !st.isDirectory()) {
|
|
646
|
-
return null;
|
|
647
|
-
}
|
|
648
|
-
if (isSystemBuilderAppName(appName) && !isProjectBuilderAppDirectory(cwdApp)) {
|
|
649
|
-
return null;
|
|
650
|
-
}
|
|
651
|
-
return cwdApp;
|
|
652
|
-
} catch {
|
|
648
|
+
function getCwdBuilderAppPath(appName) {
|
|
649
|
+
if (!appName || typeof appName !== 'string') {
|
|
653
650
|
return null;
|
|
654
651
|
}
|
|
652
|
+
const cwdApp = path.join(path.resolve(process.cwd()), 'builder', appName);
|
|
653
|
+
return isProjectBuilderAppDirectory(cwdApp) ? cwdApp : null;
|
|
655
654
|
}
|
|
656
655
|
|
|
657
656
|
/**
|
|
658
|
-
* Gets the builder folder path:
|
|
659
|
-
* 1.
|
|
660
|
-
* 2. Else **`
|
|
661
|
-
*
|
|
662
|
-
*
|
|
663
|
-
* 3. Else **`(aifabrix-work or aifabrix-home)/builder/<appName>`** — used by setup / `up-platform` /
|
|
664
|
-
* `up-miso` / `up-dataplane` for template materialization.
|
|
657
|
+
* Gets the builder folder path for run/deploy/register:
|
|
658
|
+
* 1. **`cwd/builder/<app>`** when a resolvable application manifest exists in the running folder.
|
|
659
|
+
* 2. Else **`(aifabrix-work | aifabrix-home)/builder/<app>`** (setup materializes templates there).
|
|
660
|
+
*
|
|
661
|
+
* Teardown and setup `--force` clean use {@link getHomeBuilderAppPath} only (never product-repo trees).
|
|
665
662
|
*
|
|
666
663
|
* @param {string} appName - Application name
|
|
667
664
|
* @returns {string} Absolute path to builder directory
|
|
@@ -670,20 +667,11 @@ function getBuilderPath(appName) {
|
|
|
670
667
|
if (!appName || typeof appName !== 'string') {
|
|
671
668
|
throw new Error('App name is required and must be a string');
|
|
672
669
|
}
|
|
673
|
-
const
|
|
674
|
-
const manifestHit = resolveApplicationManifestPathSync({
|
|
675
|
-
targetKey: appName,
|
|
676
|
-
mode: 'auto',
|
|
677
|
-
cwd: process.cwd()
|
|
678
|
-
});
|
|
679
|
-
if (manifestHit && (manifestHit.tier === 'cwd-builder' || manifestHit.tier === 'cwd-integration')) {
|
|
680
|
-
return manifestHit.absolutePath;
|
|
681
|
-
}
|
|
682
|
-
const cwdHit = tryCwdBuilderPathOrNull(appName);
|
|
670
|
+
const cwdHit = getCwdBuilderAppPath(appName);
|
|
683
671
|
if (cwdHit) {
|
|
684
672
|
return cwdHit;
|
|
685
673
|
}
|
|
686
|
-
return
|
|
674
|
+
return getHomeBuilderAppPath(appName);
|
|
687
675
|
}
|
|
688
676
|
|
|
689
677
|
/**
|
|
@@ -918,6 +906,8 @@ module.exports = {
|
|
|
918
906
|
getIntegrationBuilderBaseDir,
|
|
919
907
|
SYSTEM_BUILDER_APP_KEYS,
|
|
920
908
|
isSystemBuilderAppName,
|
|
909
|
+
getHomeBuilderAppPath,
|
|
910
|
+
getCwdBuilderAppPath,
|
|
921
911
|
getProjectBuilderAppPath,
|
|
922
912
|
getSystemPlatformMaterializationParent,
|
|
923
913
|
getSystemBuilderRoot,
|
package/package.json
CHANGED
|
@@ -5,7 +5,7 @@ app:
|
|
|
5
5
|
description: "AI Fabrix Dataplane is a secure, in-tenant integration and automation layer that supplies governed, normalized, and explainable enterprise data to AI agents. Using CIP as a declarative standard, it enforces RBAC and ABAC, executes integrations, and exposes trusted data via MCP and OpenAPI."
|
|
6
6
|
type: webapp
|
|
7
7
|
language: python # Explicitly specify Python language
|
|
8
|
-
version:
|
|
8
|
+
version: 2.0.0
|
|
9
9
|
|
|
10
10
|
# Image Configuration
|
|
11
11
|
# Set tag to match your build (e.g. aifabrix build dataplane -t 1.0.0 then tag: 1.0.0)
|
|
@@ -26,6 +26,7 @@ LOCAL_MODE=false
|
|
|
26
26
|
# Celery worker process tuning (dataplane-worker container; optional overrides)
|
|
27
27
|
# CELERY_WORKER_QUEUES=external_data,document_sync,document_processing,audit
|
|
28
28
|
# CELERY_WORKER_CONCURRENCY=2
|
|
29
|
+
# CELERY_WORKER_RUNTIME is set automatically by python -m app.core.celery_worker_main (NullPool per task).
|
|
29
30
|
# CELERY_LOG_LEVEL=info
|
|
30
31
|
|
|
31
32
|
# When API_KEY is set, a matching Bearer token bypasses OAuth2 validation
|
|
@@ -42,7 +43,7 @@ API_KEY_BYPASS_ROLES=full-access
|
|
|
42
43
|
|
|
43
44
|
# API Configuration
|
|
44
45
|
API_V1_STR=/api/v1
|
|
45
|
-
VERSION=
|
|
46
|
+
VERSION=2.0.0
|
|
46
47
|
# Minimum supported aifabrix-builder CLI semver (advertised on /health and /api/v1/health; empty = no gate)
|
|
47
48
|
# MIN_BUILDER_CLI_VERSION=2.45.0
|
|
48
49
|
MIN_BUILDER_CLI_VERSION=2.45.5
|
|
@@ -157,6 +157,8 @@ WAIT_FOR_KEYCLOAK=true
|
|
|
157
157
|
# For production: use public domain (e.g., https://miso.example.com)
|
|
158
158
|
# url://public includes front-door path from application.yaml (e.g. /controller).
|
|
159
159
|
MISO_WEB_SERVER_URL=url://public
|
|
160
|
+
# Provider-grant OAuth redirect_uri base (Azure/start-dev also set this explicitly; code falls back to MISO_WEB_SERVER_URL)
|
|
161
|
+
CONTROLLER_BASE_URL=url://public
|
|
160
162
|
MISO_CONTROLLER_URL=url://internal
|
|
161
163
|
MISO_RELATIVE_PATH=url://vdir-public
|
|
162
164
|
ALLOWED_ORIGINS=http://localhost:*,url://host-public,url://host-private,url://dataplane-host-public,url://dataplane-host-private
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"apiVersion":"dataplane.aifabrix.ai/v1","kind":"Protection","metadata":{"key":"hubspot-country-sales","displayName":"HubSpot Country Sales Access"},"spec":{"enabled":true,"datasourceKey":"hubspot-companies","mode":"replaceForSource","rules":[{"key":"sales-country-users","principal":{"type":"group","expression":"Sales {{fk.country.metadata.iso3}} Users"},"grants":[{"dimensionKey":"country","valueExpression":"{{fk.country.metadata.iso2}}"}]}]}}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
apiVersion: dataplane.aifabrix.ai/v1
|
|
2
|
-
kind: Protection
|
|
3
|
-
metadata:
|
|
4
|
-
key: hubspot-country-sales
|
|
5
|
-
displayName: HubSpot Country Sales Access
|
|
6
|
-
spec:
|
|
7
|
-
enabled: true
|
|
8
|
-
datasourceKey: hubspot-companies
|
|
9
|
-
mode: replaceForSource
|
|
10
|
-
rules:
|
|
11
|
-
- key: sales-country-users
|
|
12
|
-
principal:
|
|
13
|
-
type: group
|
|
14
|
-
expression: "Sales {{fk.country.metadata.iso3}} Users"
|
|
15
|
-
grants:
|
|
16
|
-
- dimensionKey: country
|
|
17
|
-
valueExpression: "{{fk.country.metadata.iso2}}"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"kind":"Protection"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"apiVersion":"dataplane.aifabrix.ai/v1","kind":"Protection","metadata":{"key":"hubspot-country-sales","displayName":"HubSpot Country Sales Access"},"spec":{"enabled":true,"datasourceKey":"hubspot-companies","mode":"replaceForSource","rules":[{"key":"sales-country-users","principal":{"type":"group","expression":"Sales {{fk.country.metadata.iso3}} Users"},"grants":[{"dimensionKey":"country","valueExpression":"{{fk.country.metadata.iso2}}"}]}]}}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"kind":"Protection"}
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
apiVersion: dataplane.aifabrix.ai/v1
|
|
2
|
-
kind: Protection
|
|
3
|
-
metadata:
|
|
4
|
-
key: hubspot-country-sales
|
|
5
|
-
displayName: HubSpot Country Sales Access
|
|
6
|
-
spec:
|
|
7
|
-
enabled: true
|
|
8
|
-
datasourceKey: hubspot-companies
|
|
9
|
-
mode: replaceForSource
|
|
10
|
-
rules:
|
|
11
|
-
- key: sales-country-users
|
|
12
|
-
principal:
|
|
13
|
-
type: group
|
|
14
|
-
expression: "Sales {{fk.country.metadata.iso3}} Users"
|
|
15
|
-
grants:
|
|
16
|
-
- dimensionKey: country
|
|
17
|
-
valueExpression: "{{fk.country.metadata.iso2}}"
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
apiVersion: dataplane.aifabrix.ai/v1
|
|
2
|
-
kind: Protection
|
|
3
|
-
metadata:
|
|
4
|
-
key: hubspot-country-sales
|
|
5
|
-
displayName: HubSpot Country Sales Access
|
|
6
|
-
spec:
|
|
7
|
-
enabled: true
|
|
8
|
-
datasourceKey: hubspot-companies
|
|
9
|
-
mode: replaceForSource
|
|
10
|
-
rules:
|
|
11
|
-
- key: sales-country-users
|
|
12
|
-
principal:
|
|
13
|
-
type: group
|
|
14
|
-
expression: "Sales {{fk.country.metadata.iso3}} Users"
|
|
15
|
-
grants:
|
|
16
|
-
- dimensionKey: country
|
|
17
|
-
valueExpression: "{{fk.country.metadata.iso2}}"
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
apiVersion: dataplane.aifabrix.ai/v1
|
|
2
|
-
kind: Protection
|
|
3
|
-
metadata:
|
|
4
|
-
key: hubspot-country-sales
|
|
5
|
-
displayName: HubSpot Country Sales Access
|
|
6
|
-
spec:
|
|
7
|
-
enabled: true
|
|
8
|
-
datasourceKey: hubspot-companies
|
|
9
|
-
mode: replaceForSource
|
|
10
|
-
rules:
|
|
11
|
-
- key: sales-country-users
|
|
12
|
-
principal:
|
|
13
|
-
type: group
|
|
14
|
-
expression: "Sales {{fk.country.metadata.iso3}} Users"
|
|
15
|
-
grants:
|
|
16
|
-
- dimensionKey: country
|
|
17
|
-
valueExpression: "{{fk.country.metadata.iso2}}"
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
apiVersion: dataplane.aifabrix.ai/v1
|
|
2
|
-
kind: Protection
|
|
3
|
-
metadata:
|
|
4
|
-
key: hubspot-country-sales
|
|
5
|
-
displayName: HubSpot Country Sales Access
|
|
6
|
-
spec:
|
|
7
|
-
enabled: true
|
|
8
|
-
datasourceKey: hubspot-companies
|
|
9
|
-
mode: replaceForSource
|
|
10
|
-
rules:
|
|
11
|
-
- key: sales-country-users
|
|
12
|
-
principal:
|
|
13
|
-
type: group
|
|
14
|
-
expression: "Sales {{fk.country.metadata.iso3}} Users"
|
|
15
|
-
grants:
|
|
16
|
-
- dimensionKey: country
|
|
17
|
-
valueExpression: "{{fk.country.metadata.iso2}}"
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
apiVersion: dataplane.aifabrix.ai/v1
|
|
2
|
-
kind: Protection
|
|
3
|
-
metadata:
|
|
4
|
-
key: hubspot-country-sales
|
|
5
|
-
displayName: HubSpot Country Sales Access
|
|
6
|
-
spec:
|
|
7
|
-
enabled: true
|
|
8
|
-
datasourceKey: hubspot-companies
|
|
9
|
-
mode: replaceForSource
|
|
10
|
-
rules:
|
|
11
|
-
- key: sales-country-users
|
|
12
|
-
principal:
|
|
13
|
-
type: group
|
|
14
|
-
expression: "Sales {{fk.country.metadata.iso3}} Users"
|
|
15
|
-
grants:
|
|
16
|
-
- dimensionKey: country
|
|
17
|
-
valueExpression: "{{fk.country.metadata.iso2}}"
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
apiVersion: dataplane.aifabrix.ai/v1
|
|
2
|
-
kind: Protection
|
|
3
|
-
metadata:
|
|
4
|
-
key: hubspot-country-sales
|
|
5
|
-
displayName: HubSpot Country Sales Access
|
|
6
|
-
spec:
|
|
7
|
-
enabled: true
|
|
8
|
-
datasourceKey: hubspot-companies
|
|
9
|
-
mode: replaceForSource
|
|
10
|
-
rules:
|
|
11
|
-
- key: sales-country-users
|
|
12
|
-
principal:
|
|
13
|
-
type: group
|
|
14
|
-
expression: "Sales {{fk.country.metadata.iso3}} Users"
|
|
15
|
-
grants:
|
|
16
|
-
- dimensionKey: country
|
|
17
|
-
valueExpression: "{{fk.country.metadata.iso2}}"
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
apiVersion: dataplane.aifabrix.ai/v1
|
|
2
|
-
kind: Protection
|
|
3
|
-
metadata:
|
|
4
|
-
key: hubspot-country-sales
|
|
5
|
-
displayName: HubSpot Country Sales Access
|
|
6
|
-
spec:
|
|
7
|
-
enabled: true
|
|
8
|
-
datasourceKey: hubspot-companies
|
|
9
|
-
mode: replaceForSource
|
|
10
|
-
rules:
|
|
11
|
-
- key: sales-country-users
|
|
12
|
-
principal:
|
|
13
|
-
type: group
|
|
14
|
-
expression: "Sales {{fk.country.metadata.iso3}} Users"
|
|
15
|
-
grants:
|
|
16
|
-
- dimensionKey: country
|
|
17
|
-
valueExpression: "{{fk.country.metadata.iso2}}"
|