@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.
Files changed (27) hide show
  1. package/.cursor/skills/README.md +14 -0
  2. package/lib/cli/infra-guided-auth.js +8 -6
  3. package/lib/cli/infra-guided.js +11 -2
  4. package/lib/commands/teardown.js +3 -5
  5. package/lib/commands/up-common.js +8 -11
  6. package/lib/commands/up-dataplane.js +81 -13
  7. package/lib/commands/up-miso.js +6 -2
  8. package/lib/utils/health-check-poll-ui.js +63 -28
  9. package/lib/utils/health-check.js +50 -21
  10. package/lib/utils/paths.js +26 -36
  11. package/package.json +1 -1
  12. package/templates/applications/dataplane/application.yaml +1 -1
  13. package/templates/applications/dataplane/env.template +2 -1
  14. package/templates/applications/miso-controller/env.template +2 -0
  15. package/.temp/jest-protection-convert/run-IFN2nv/.protection/hubspot-companies.json +0 -1
  16. package/.temp/jest-protection-convert/run-IvmKLO/.protection/hubspot-companies.yaml +0 -17
  17. package/.temp/jest-protection-convert/run-SdvblF/.protection/hubspot-companies.json +0 -1
  18. package/.temp/jest-protection-convert/run-b51TsW/.protection/hubspot-companies.json +0 -1
  19. package/.temp/jest-protection-convert/run-fiLmb1/.protection/hubspot-companies.json +0 -1
  20. package/.temp/jest-protection-convert/run-qYGHMU/.protection/hubspot-companies.yaml +0 -17
  21. package/.temp/jest-protection-fixtures/hubspot-Mq5re8/hubspot-companies.yaml +0 -17
  22. package/.temp/jest-protection-fixtures/hubspot-f3u4YL/hubspot-companies.yaml +0 -17
  23. package/.temp/jest-protection-fixtures/hubspot-iQtd1O/hubspot-companies.yaml +0 -17
  24. package/.temp/jest-protection-fixtures/hubspot-mdmz5M/hubspot-companies.yaml +0 -17
  25. package/.temp/jest-protection-fixtures/hubspot-qbKYxQ/hubspot-companies.yaml +0 -17
  26. package/.temp/jest-protection-fixtures/hubspot-yzlJy6/hubspot-companies.yaml +0 -17
  27. 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
 
@@ -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', { timeoutSeconds: 120 });
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', { timeoutSeconds: 120 });
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
  }
@@ -120,17 +120,15 @@ function cleanAifabrixSystemDir() {
120
120
  }
121
121
 
122
122
  /**
123
- * Remove materialized `builder/` trees (aifabrix-work, aifabrix-home / config dir, cwd).
124
- * Complements {@link cleanAifabrixSystemDir} when `builder/` lives under `aifabrix-work` outside
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 builder app directories for the given app names. Only removes paths under an allowed
246
- * builder root (project `builder/` or `~/.aifabrix/builder/`) to prevent path traversal.
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 allowed builder roots (path traversal attempt)
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.getBuilderPath(appName));
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.getBuilderPath(appName);
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
- * Resolve controller URL and ensure it is healthy; if not, prompt once for new URL.
80
- * @returns {Promise<string>} Controller URL to use
81
- * @throws {Error} If controller unavailable and user aborts or new URL still unhealthy
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 resolveControllerUrlWithHealthCheck() {
84
- let controllerUrl = await resolveControllerUrl();
85
- controllerUrl = controllerUrl.replace(/\/+$/, '');
86
-
87
- let healthy = await isControllerHealthy(controllerUrl);
88
- if (healthy) {
89
- return controllerUrl;
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
- await app.runApp('dataplane', {
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.'));
@@ -57,11 +57,15 @@ async function runMisoApps(options) {
57
57
  base: useBaseImage,
58
58
  skipManifestMetadataLine: true
59
59
  };
60
- const keycloakRunOpts = { ...common };
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
- * @param {number} maxAttempts
31
- * @returns {{ usesSpinner: boolean, onWaiting: (attemptIndex: number) => void, finish: () => void }}
32
- */
33
- function createHealthCheckPollHandlers(maxAttempts) {
34
- if (shouldUseHealthCheckPollSpinner()) {
35
- const ora = require('ora');
36
- const spinner = ora({
37
- text: buildHealthCheckPollText(0, maxAttempts),
38
- spinner: 'dots'
39
- });
40
- let started = false;
41
- return {
42
- usesSpinner: true,
43
- onWaiting: (attemptIndex) => {
44
- if (!started) {
45
- spinner.start();
46
- started = true;
47
- }
48
- spinner.text = buildHealthCheckPollText(attemptIndex, maxAttempts);
49
- },
50
- finish: () => {
51
- if (started) {
52
- spinner.stop();
53
- process.stdout.write('\n');
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
- async function waitForHealthCheck(appName, timeout = 90, port = null, config = null, debug = false, runOptions = {}) {
335
- await waitForDbInit(appName);
336
-
337
- const healthCheckPort = await determineHealthCheckPort(port, appName, debug);
338
- const { maxAttempts } = buildHealthCheckConfig(healthCheckPort, config, timeout, debug);
339
- const { urlsToTry, skippedPublicHealthUrl } = await computePreferredHealthCheckUrls(
340
- appName,
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
- const poll = createHealthCheckPollHandlers(maxAttempts);
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
@@ -459,13 +459,23 @@ function isSystemBuilderAppName(appName) {
459
459
  }
460
460
 
461
461
  /**
462
- * `builder/<appName>` under {@link getAppsMaterializationParent} (same tree as {@link getSystemBuilderRoot}).
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 path.join(getIntegrationBuilderBaseDir(), 'builder', appName);
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 present as a directory and allowed (platform empty stubs skipped).
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 tryCwdBuilderPathOrNull(appName) {
639
- const cwdApp = path.join(path.resolve(process.cwd()), 'builder', appName);
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. Plan 141 Tier‑1: `cwd/builder/<app>` or `cwd/integration/<app>` when a resolvable application manifest exists.
660
- * 2. Else **`cwd/builder/<app>`** when that directory exists. For **platform** apps (`keycloak`,
661
- * `miso-controller`, `dataplane`), an **empty** cwd stub is ignored so materialization under
662
- * `(work|home)/builder` still wins.
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 { resolveApplicationManifestPathSync } = require('./manifest-location');
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 path.join(getAppsMaterializationParent(), 'builder', appName);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.49.0",
3
+ "version": "2.49.1",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
@@ -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: 1.9.5
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=1.9.5
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
- {"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,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,5 +0,0 @@
1
- lint:fix exit=0
2
- lint exit=0
3
- test exit=0
4
- miso typecheck exit=0
5
- miso build exit=0