@aifabrix/builder 2.47.1 → 2.47.3

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.
@@ -120,6 +120,7 @@ const ISOLATED_SUITE_ENTRIES = [
120
120
  ['paths-jest-sandbox-integration', ['**/tests/lib/utils/paths-jest-sandbox-integration.test.js']],
121
121
  ['secrets-remove-all-guard', ['**/tests/lib/commands/secrets-remove-all-guard.test.js']],
122
122
  ['app-logs', ['**/tests/lib/commands/app-logs.test.js']],
123
+ ['app-down', ['**/tests/lib/app/app-down.test.js']],
123
124
  [
124
125
  'channel-real-fs',
125
126
  [
package/jest.projects.js CHANGED
@@ -351,6 +351,10 @@ const defaultProject = {
351
351
  '\\\\tests\\\\lib\\\\commands\\\\app-logs.test.js',
352
352
  'lib/commands/app-logs.test.js',
353
353
  'app-logs\\.test\\.js',
354
+ '/tests/lib/app/app-down.test.js',
355
+ '\\\\tests\\\\lib\\\\app\\\\app-down.test.js',
356
+ 'lib/app/app-down.test.js',
357
+ 'app-down\\.test\\.js',
354
358
  '/tests/lib/channels/channel-integration-generator.test.js',
355
359
  '\\\\tests\\\\lib\\\\channels\\\\channel-integration-generator.test.js',
356
360
  'lib/channels/channel-integration-generator.test.js',
@@ -166,7 +166,10 @@ function validateResponse(response) {
166
166
  * @param {string} apiUrl - API URL
167
167
  * @param {string} [message] - Optional message from API
168
168
  */
169
- function displayRotationResults(appKey, environment, credentials, apiUrl, message) {
169
+ function displayRotationResults(appKey, environment, credentials, apiUrl, message, options = {}) {
170
+ if (options.quiet) {
171
+ return;
172
+ }
170
173
  logger.log(formatSuccessLine('Secret rotated successfully!\n'));
171
174
  logger.log(chalk.bold('📋 Application Details:'));
172
175
  logger.log(` Key: ${appKey}`);
@@ -273,7 +276,7 @@ async function getRotationAuthToken(controllerUrl, config) {
273
276
  * @param {string} actualControllerUrl - Controller URL
274
277
  * @throws {Error} If saving fails
275
278
  */
276
- async function saveCredentialsLocally(appKey, credentials, actualControllerUrl) {
279
+ async function saveCredentialsLocally(appKey, credentials, actualControllerUrl, options = {}) {
277
280
  const clientIdKey = `${appKey}-client-idKeyVault`;
278
281
  const clientSecretKey = `${appKey}-client-secretKeyVault`;
279
282
 
@@ -293,10 +296,12 @@ async function saveCredentialsLocally(appKey, credentials, actualControllerUrl)
293
296
  logger.warn(chalk.yellow(`⚠ Could not validate env resolution: ${error.message}`));
294
297
  }
295
298
 
296
- logger.log(formatSuccessParagraph('Credentials saved to ~/.aifabrix/secrets.local.yaml'));
297
- logger.log(formatSuccessLine('env.template updated with MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL'));
298
- logger.log(formatSuccessLine('Run "aifabrix resolve ' + appKey + '" to materialize an on-disk .env\n'));
299
- } else {
299
+ if (!options.quiet) {
300
+ logger.log(formatSuccessParagraph('Credentials saved to ~/.aifabrix/secrets.local.yaml'));
301
+ logger.log(formatSuccessLine('env.template updated with MISO_CLIENTID, MISO_CLIENTSECRET, and MISO_CONTROLLER_URL'));
302
+ logger.log(formatSuccessLine('Run "aifabrix resolve ' + appKey + '" to materialize an on-disk .env\n'));
303
+ }
304
+ } else if (!options.quiet) {
300
305
  logger.log(formatSuccessParagraph('Credentials saved to ~/.aifabrix/secrets.local.yaml\n'));
301
306
  }
302
307
  } catch (error) {
@@ -312,7 +317,7 @@ async function saveCredentialsLocally(appKey, credentials, actualControllerUrl)
312
317
  * @param {string} environment - Environment ID or key
313
318
  * @param {string} token - Bearer token
314
319
  */
315
- async function executeRotation(appKey, actualControllerUrl, environment, token) {
320
+ async function executeRotation(appKey, actualControllerUrl, environment, token, options = {}) {
316
321
  const authConfig = { type: 'bearer', token };
317
322
  const response = await rotateApplicationSecret(actualControllerUrl, environment, appKey, authConfig);
318
323
 
@@ -324,8 +329,8 @@ async function executeRotation(appKey, actualControllerUrl, environment, token)
324
329
  }
325
330
 
326
331
  const { credentials, message } = validateResponse(response);
327
- await saveCredentialsLocally(appKey, credentials, actualControllerUrl);
328
- displayRotationResults(appKey, environment, credentials, actualControllerUrl, message);
332
+ await saveCredentialsLocally(appKey, credentials, actualControllerUrl, options);
333
+ displayRotationResults(appKey, environment, credentials, actualControllerUrl, message, options);
329
334
  // Clear cached client token so next deploy uses new credentials (avoids "Invalid or expired credentials" when running deploy/up-dataplane immediately after rotate)
330
335
  await clearClientToken(environment, appKey);
331
336
  }
@@ -338,8 +343,10 @@ async function executeRotation(appKey, actualControllerUrl, environment, token)
338
343
  * @param {Object} [_options] - Command options (reserved)
339
344
  * @throws {Error} If rotation fails
340
345
  */
341
- async function rotateSecret(appKey, _options = {}) {
342
- logger.log(chalk.yellow('⚠ This will invalidate the old ClientSecret!\n'));
346
+ async function rotateSecret(appKey, options = {}) {
347
+ if (!options.quiet) {
348
+ logger.log(chalk.yellow('⚠ This will invalidate the old ClientSecret!\n'));
349
+ }
343
350
 
344
351
  const { ensureSecretsEncryptionKey } = require('../core/config');
345
352
  await ensureSecretsEncryptionKey();
@@ -349,7 +356,7 @@ async function rotateSecret(appKey, _options = {}) {
349
356
  const { token, actualControllerUrl } = await getRotationAuthToken(controllerUrl, config);
350
357
 
351
358
  try {
352
- await executeRotation(appKey, actualControllerUrl, environment, token);
359
+ await executeRotation(appKey, actualControllerUrl, environment, token, options);
353
360
  } catch (error) {
354
361
  logger.error(formatBlockingError(`Failed to rotate secret via controller: ${actualControllerUrl}`));
355
362
  logger.error(chalk.gray(`Error: ${error.message}`));
@@ -10,7 +10,6 @@ const config = require('../core/config');
10
10
  const { resolveDockerImageRef } = require('../utils/resolve-docker-image-ref');
11
11
  const { checkImageExists } = require('../utils/app-run-containers');
12
12
  const { buildDevImageRepositoryPath } = require('../utils/image-name');
13
- const { wantsLocalDockerImageDiscovery } = require('../utils/build-resolve-image');
14
13
 
15
14
  /**
16
15
  * @param {string} appName
@@ -42,7 +41,7 @@ async function resolveRunImageWithLocalFallback(appName, appConfig, runOptions =
42
41
 
43
42
  const baseRef = resolveDockerImageRef(appName, appConfig, opts);
44
43
 
45
- if (opts.base === true || !wantsLocalDockerImageDiscovery(appConfig)) {
44
+ if (opts.base === true) {
46
45
  return baseRef;
47
46
  }
48
47
 
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Docker build-arg resolution from resolved env and shell overlays.
3
+ *
4
+ * @fileoverview Build-time args for `aifabrix build` / docker build
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const path = require('path');
12
+ const secretsEnvWrite = require('../core/secrets-env-write');
13
+ const { assertMisoControllerNpmSdkDeps } = require('./miso-controller-npm-sdk-guard');
14
+
15
+ const MISO_CONTROLLER_UI_BUILD_ARG_KEYS = [
16
+ 'MISO_RELATIVE_PATH',
17
+ 'MISO_WEB_SERVER_URL',
18
+ 'VITE_BASE_PATH',
19
+ 'KEYCLOAK_SERVER_URL',
20
+ 'KEYCLOAK_REALM',
21
+ 'KEYCLOAK_CLIENT_ID'
22
+ ];
23
+
24
+ /**
25
+ * @param {Object.<string, string>} buildArgs
26
+ * @param {Object.<string, string>} envMap
27
+ */
28
+ function applyMisoControllerUiBuildArgs(buildArgs, envMap) {
29
+ for (const key of MISO_CONTROLLER_UI_BUILD_ARG_KEYS) {
30
+ const value = envMap[key];
31
+ if (typeof value === 'string' && value.trim() && !value.includes('://')) {
32
+ buildArgs[key] = value.trim();
33
+ }
34
+ }
35
+ if (!buildArgs.VITE_BASE_PATH && buildArgs.MISO_RELATIVE_PATH) {
36
+ buildArgs.VITE_BASE_PATH = buildArgs.MISO_RELATIVE_PATH;
37
+ }
38
+ if (!buildArgs.VITE_BASE_PATH && buildArgs.MISO_WEB_SERVER_URL) {
39
+ try {
40
+ const pathname = new URL(buildArgs.MISO_WEB_SERVER_URL).pathname.replace(/\/+$/, '');
41
+ if (pathname && pathname !== '/') {
42
+ buildArgs.VITE_BASE_PATH = pathname;
43
+ }
44
+ } catch {
45
+ // ignore invalid public URL
46
+ }
47
+ }
48
+ }
49
+
50
+ /**
51
+ * Resolve Docker build-args from resolved env map and merged BASH_* secrets.
52
+ *
53
+ * @param {string} appName
54
+ * @param {string} [contextPath] Docker build context (repo root for miso-controller)
55
+ * @returns {Promise<Object.<string, string>>}
56
+ */
57
+ async function resolveDockerBuildArgsForApp(appName, contextPath) {
58
+ const envMap = await secretsEnvWrite.resolveAndGetEnvMap(appName, { environment: 'docker' });
59
+ const { getBashPrefixedProcessEnvOverlay } = require('../utils/bash-secret-env');
60
+ const bashOverlay = await getBashPrefixedProcessEnvOverlay(null, appName);
61
+ const buildArgs = {};
62
+ if (envMap.NPM_TOKEN) buildArgs.NPM_TOKEN = envMap.NPM_TOKEN;
63
+ if (envMap.PYPI_TOKEN) buildArgs.PYPI_TOKEN = envMap.PYPI_TOKEN;
64
+ for (const [k, v] of Object.entries(bashOverlay)) {
65
+ if (v && buildArgs[k] === undefined) buildArgs[k] = v;
66
+ }
67
+ const shellNpm = typeof process.env.NPM_TOKEN === 'string' ? process.env.NPM_TOKEN.trim() : '';
68
+ const shellPypi = typeof process.env.PYPI_TOKEN === 'string' ? process.env.PYPI_TOKEN.trim() : '';
69
+ if (shellNpm) buildArgs.NPM_TOKEN = shellNpm;
70
+ if (shellPypi) buildArgs.PYPI_TOKEN = shellPypi;
71
+ if (appName === 'miso-controller') {
72
+ applyMisoControllerUiBuildArgs(buildArgs, envMap);
73
+ }
74
+ if (appName === 'miso-controller' && contextPath) {
75
+ assertMisoControllerNpmSdkDeps(path.resolve(contextPath));
76
+ }
77
+ return buildArgs;
78
+ }
79
+
80
+ module.exports = {
81
+ resolveDockerBuildArgsForApp,
82
+ applyMisoControllerUiBuildArgs
83
+ };
@@ -35,9 +35,8 @@ const dockerBuild = require('../utils/docker-build');
35
35
  const buildCopy = require('../utils/build-copy');
36
36
  const { resolveBuildDockerTagNames } = require('../utils/build-resolve-image');
37
37
  const buildHelpers = require('../utils/build-helpers');
38
- const secretsEnvWrite = require('../core/secrets-env-write');
39
38
  const { ensureRunSecretsForApp } = require('../app/run-env-compose');
40
- const { assertMisoControllerNpmSdkDeps } = require('./miso-controller-npm-sdk-guard');
39
+ const { resolveDockerBuildArgsForApp } = require('./docker-build-args');
41
40
 
42
41
  /**
43
42
  * Loads application config for an application
@@ -394,33 +393,6 @@ async function handleDockerfileGeneration(appName, params, options, buildHelpers
394
393
  }, generateDockerfile);
395
394
  }
396
395
 
397
- /**
398
- * Resolve Docker build-args from resolved env map and merged BASH_* secrets.
399
- * @param {string} appName
400
- * @param {string} [contextPath] Docker build context (repo root for miso-controller)
401
- * @returns {Promise<Object.<string, string>>}
402
- */
403
- async function resolveDockerBuildArgsForApp(appName, contextPath) {
404
- const envMap = await secretsEnvWrite.resolveAndGetEnvMap(appName, { environment: 'docker' });
405
- const { getBashPrefixedProcessEnvOverlay } = require('../utils/bash-secret-env');
406
- const bashOverlay = await getBashPrefixedProcessEnvOverlay(null, appName);
407
- const buildArgs = {};
408
- if (envMap.NPM_TOKEN) buildArgs.NPM_TOKEN = envMap.NPM_TOKEN;
409
- if (envMap.PYPI_TOKEN) buildArgs.PYPI_TOKEN = envMap.PYPI_TOKEN;
410
- for (const [k, v] of Object.entries(bashOverlay)) {
411
- if (v && buildArgs[k] === undefined) buildArgs[k] = v;
412
- }
413
- // Exported shell tokens win (local dev: export NPM_TOKEN=... before af build / docker build).
414
- const shellNpm = typeof process.env.NPM_TOKEN === 'string' ? process.env.NPM_TOKEN.trim() : '';
415
- const shellPypi = typeof process.env.PYPI_TOKEN === 'string' ? process.env.PYPI_TOKEN.trim() : '';
416
- if (shellNpm) buildArgs.NPM_TOKEN = shellNpm;
417
- if (shellPypi) buildArgs.PYPI_TOKEN = shellPypi;
418
- if (appName === 'miso-controller' && contextPath) {
419
- assertMisoControllerNpmSdkDeps(path.resolve(contextPath));
420
- }
421
- return buildArgs;
422
- }
423
-
424
396
  /**
425
397
  * Standard Docker build path after CLI header is printed (non-external apps).
426
398
  * @param {string} appName
@@ -324,7 +324,7 @@ function setupBuildRunLogsDownCommands(program) {
324
324
  .option('-t, --tag <tag>', 'Image tag (default: latest). Set image.tag in application.yaml to match for deploy.')
325
325
  .option(
326
326
  '--base',
327
- 'When manifest omits image: also tag the base repository after a developer-scoped build (ignored when application.yaml defines image)'
327
+ 'Build/run the manifest base image (no -devN suffix). Default local build uses a developer-scoped repository when developer-id > 0.'
328
328
  )
329
329
  .action(async(appName, options) => {
330
330
  try {
@@ -35,6 +35,7 @@ const {
35
35
  formatProgress,
36
36
  successGlyph
37
37
  } = require('../utils/cli-test-layout-chalk');
38
+ const { applySecretsUrlFromRemote } = require('../utils/config-paths');
38
39
 
39
40
  /** File name kept by teardown (lowercase exact match). */
40
41
  const PRESERVE_FILE = 'config.yaml';
@@ -149,6 +150,26 @@ function cleanMaterializedBuilderTrees() {
149
150
  return { removed, failed };
150
151
  }
151
152
 
153
+ /**
154
+ * Fix preserved `config.yaml` after teardown: remote dev shared secrets API URL and cleared device tokens.
155
+ *
156
+ * @async
157
+ * @returns {Promise<boolean>} True when config was saved
158
+ */
159
+ async function sanitizePreservedConfigAfterTeardown() {
160
+ const cfg = await config.getConfig();
161
+ const beforeSecrets = cfg['aifabrix-secrets'];
162
+ applySecretsUrlFromRemote(cfg);
163
+ let changed = cfg['aifabrix-secrets'] !== beforeSecrets;
164
+ if (cfg.device && typeof cfg.device === 'object' && Object.keys(cfg.device).length > 0) {
165
+ cfg.device = {};
166
+ changed = true;
167
+ }
168
+ if (!changed) return false;
169
+ await config.saveConfig(cfg);
170
+ return true;
171
+ }
172
+
152
173
  /**
153
174
  * Stop infra with `down-infra -v` semantics. Tolerates already-down state.
154
175
  * @async
@@ -291,14 +312,13 @@ async function handleTeardown(options = {}) {
291
312
  await stopInfraQuietly();
292
313
  const { removed, failed } = cleanFilesWithSpinner();
293
314
  const builderClean = cleanMaterializedBuilderTrees();
294
- for (const p of builderClean.removed) {
295
- removed.push(p);
296
- }
297
- for (const p of builderClean.failed) {
298
- failed.push(p);
299
- }
315
+ mergeBuilderCleanResults(removed, failed, builderClean);
300
316
  const devStr = await buildDeveloperLabel();
317
+ const configFixed = await sanitizePreservedConfigAfterTeardown();
301
318
  logTeardownFooter({ devStr, removedCount: removed.length, failedCount: failed.length });
319
+ if (configFixed) {
320
+ logger.log(formatSuccessLine('Restored remote shared secrets URL in config.yaml'));
321
+ }
302
322
 
303
323
  logger.log(formatSuccessParagraph('aifabrix teardown complete.'));
304
324
  } finally {
@@ -306,10 +326,20 @@ async function handleTeardown(options = {}) {
306
326
  }
307
327
  }
308
328
 
329
+ function mergeBuilderCleanResults(removed, failed, builderClean) {
330
+ for (const p of builderClean.removed) {
331
+ removed.push(p);
332
+ }
333
+ for (const p of builderClean.failed) {
334
+ failed.push(p);
335
+ }
336
+ }
337
+
309
338
  module.exports = {
310
339
  handleTeardown,
311
340
  cleanAifabrixSystemDir,
312
341
  cleanMaterializedBuilderTrees,
342
+ sanitizePreservedConfigAfterTeardown,
313
343
  stopInfraQuietly,
314
344
  PRESERVE_FILE,
315
345
  PRESERVE_CERTS_DIR
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Post-deploy dataplane pipeline credential alignment.
3
+ *
4
+ * Controller deploy/onboarding can reset the stored ClientSecret without updating
5
+ * ~/.aifabrix/secrets.local.yaml. Probe the token endpoint after deploy and rotate
6
+ * when local credentials no longer match the controller.
7
+ *
8
+ * @fileoverview up-dataplane credential probe and sync
9
+ * @author AI Fabrix Team
10
+ * @version 1.0.0
11
+ */
12
+
13
+ const chalk = require('chalk');
14
+ const { makeApiCall } = require('../utils/api');
15
+ const { loadClientCredentials } = require('../utils/token-manager');
16
+ const { rotateSecret } = require('../app/rotate-secret');
17
+ const logger = require('../utils/logger');
18
+ const { formatSuccessLine } = require('../utils/cli-test-layout-chalk');
19
+
20
+ const DATAPLANE_APP_KEY = 'dataplane';
21
+ const MISALIGN_ERROR =
22
+ 'Dataplane pipeline credentials are not aligned with the controller. ' +
23
+ 'Run: aifabrix app rotate-secret dataplane && aifabrix run dataplane';
24
+
25
+ /**
26
+ * @param {string} controllerUrl
27
+ * @returns {string}
28
+ */
29
+ function normalizeControllerUrl(controllerUrl) {
30
+ return String(controllerUrl || '').replace(/\/+$/, '');
31
+ }
32
+
33
+ /**
34
+ * Exchange dataplane client id/secret for a controller token (probe only).
35
+ * @async
36
+ * @param {string} controllerUrl
37
+ * @param {{ clientId: string, clientSecret: string }} credentials
38
+ * @returns {Promise<boolean>}
39
+ */
40
+ async function probeDataplaneClientToken(controllerUrl, credentials) {
41
+ if (!credentials?.clientId || !credentials?.clientSecret) {
42
+ return false;
43
+ }
44
+ const response = await makeApiCall(`${normalizeControllerUrl(controllerUrl)}/api/v1/auth/token`, {
45
+ method: 'POST',
46
+ headers: {
47
+ 'Content-Type': 'application/json',
48
+ 'x-client-id': credentials.clientId,
49
+ 'x-client-secret': credentials.clientSecret
50
+ }
51
+ });
52
+ return response.success === true;
53
+ }
54
+
55
+ /**
56
+ * @async
57
+ * @param {string} controllerUrl
58
+ * @returns {Promise<boolean>}
59
+ */
60
+ async function credentialsAcceptToken(controllerUrl) {
61
+ const credentials = await loadClientCredentials(DATAPLANE_APP_KEY);
62
+ if (!credentials) {
63
+ return false;
64
+ }
65
+ return probeDataplaneClientToken(controllerUrl, credentials);
66
+ }
67
+
68
+ /**
69
+ * After deploy, verify dataplane pipeline credentials; rotate when misaligned.
70
+ * @async
71
+ * @param {Object} options - up-dataplane / platform install options
72
+ * @param {string} controllerUrl
73
+ * @returns {Promise<void>}
74
+ * @throws {Error} When credentials remain misaligned after rotate
75
+ */
76
+ async function alignDataplaneCredentialsAfterDeploy(options, controllerUrl) {
77
+ const quiet = options.platformInstall === true || options.quiet === true;
78
+ const aligned = await credentialsAcceptToken(controllerUrl);
79
+ if (aligned) {
80
+ if (!quiet) {
81
+ logger.log(formatSuccessLine('Dataplane pipeline credentials verified.'));
82
+ }
83
+ return;
84
+ }
85
+
86
+ if (!quiet) {
87
+ logger.log(chalk.blue('Aligning dataplane pipeline credentials with controller...'));
88
+ }
89
+ await rotateSecret(DATAPLANE_APP_KEY, { ...options, quiet });
90
+
91
+ const stillMisaligned = !(await credentialsAcceptToken(controllerUrl));
92
+ if (stillMisaligned) {
93
+ throw new Error(MISALIGN_ERROR);
94
+ }
95
+ if (!quiet) {
96
+ logger.log(formatSuccessLine('Dataplane pipeline credentials aligned.'));
97
+ }
98
+ }
99
+
100
+ module.exports = {
101
+ alignDataplaneCredentialsAfterDeploy,
102
+ probeDataplaneClientToken,
103
+ credentialsAcceptToken
104
+ };
@@ -3,9 +3,8 @@ const { formatSuccessLine, formatSuccessParagraph } = require('../utils/cli-test
3
3
  * AI Fabrix Builder - Up Dataplane Command
4
4
  *
5
5
  * Always local deployment: requires dev infra (same gate as up-miso), then registers or
6
- * rotates dataplane in dev, sends deployment manifest to Miso Controller, then runs dataplane
7
- * locally (same as aifabrix deploy dataplane --local). If app is already registered, uses
8
- * rotate-secret; otherwise registers.
6
+ * rotates dataplane in dev, sends deployment manifest to Miso Controller, aligns pipeline
7
+ * credentials when deploy changed controller secrets, then runs dataplane locally.
9
8
  *
10
9
  * @fileoverview up-dataplane command implementation
11
10
  * @author AI Fabrix Team
@@ -35,6 +34,7 @@ const {
35
34
  validateEnvOutputPathFolderOrNull,
36
35
  refreshUrlsLocalRegistryForCurrentProject
37
36
  } = require('./up-common');
37
+ const { alignDataplaneCredentialsAfterDeploy } = require('./up-dataplane-credentials');
38
38
 
39
39
  const CONTROLLER_HEALTH_PATH = '/health';
40
40
 
@@ -262,6 +262,8 @@ async function handleUpDataplane(options = {}) {
262
262
  await deployDataplaneToController({ ...options, envKey: environmentKey });
263
263
  logger.log('');
264
264
 
265
+ await alignDataplaneCredentialsAfterDeploy(options, controllerUrl);
266
+
265
267
  const { runEnvKey, portOverride } = await computeDataplaneRunOverrides(options);
266
268
 
267
269
  await app.runApp('dataplane', {
@@ -11,7 +11,7 @@
11
11
  const chalk = require('chalk');
12
12
  const logger = require('./logger');
13
13
  const { execWithDockerEnv } = require('./docker-exec');
14
- const { buildDevImageName } = require('./image-name');
14
+ const { buildDevImageName, buildDevImageRepositoryPath } = require('./image-name');
15
15
  const {
16
16
  getRepositoryPathFromConfig,
17
17
  resolveDockerImageRef
@@ -166,8 +166,8 @@ async function discoverLocalBaseImageRepository(appName, developerId) {
166
166
 
167
167
  /**
168
168
  * Resolve local `docker build -t` repository path(s) from the manifest or discovery.
169
- * When `application.yaml` declares `image`, the manifest ref is used exactly (registry + name).
170
- * Developer-scoped `-devN` suffixes apply only when the manifest omits `image` and Docker is scanned.
169
+ * When `application.yaml` declares `image`, local builds still use a `-dev<id>` repository
170
+ * suffix for developer id > 0 unless `--base` is set (manifest ref is used exactly for base builds).
171
171
  *
172
172
  * @param {string} appName
173
173
  * @param {Object} variables - application manifest
@@ -176,9 +176,19 @@ async function discoverLocalBaseImageRepository(appName, developerId) {
176
176
  * @returns {Promise<{ buildImageName: string, compatBaseName: string|null }>}
177
177
  */
178
178
  async function resolveBuildDockerTagNames(appName, variables, developerId, options = {}) {
179
+ const idNum = typeof developerId === 'string' ? parseInt(developerId, 10) : Number(developerId);
180
+ const scopeForDeveloper =
181
+ Number.isFinite(idNum) && idNum > 0 && options.base !== true;
182
+
179
183
  if (!wantsLocalDockerImageDiscovery(variables)) {
180
184
  const { imageName } = resolveDockerImageRef(appName, variables, options);
181
- return { buildImageName: imageName, compatBaseName: null };
185
+ if (!scopeForDeveloper) {
186
+ return { buildImageName: imageName, compatBaseName: null };
187
+ }
188
+ return {
189
+ buildImageName: buildDevImageRepositoryPath(imageName, developerId),
190
+ compatBaseName: null
191
+ };
182
192
  }
183
193
  const baseRepo = await resolveBuildImageRepositoryName(appName, variables, developerId);
184
194
  const buildImageName = buildDevImageName(baseRepo, developerId);
@@ -356,10 +356,11 @@ function hostnameFromUrl(url) {
356
356
 
357
357
  function applySecretsUrlFromRemote(config) {
358
358
  const remoteServer = config['remote-server'];
359
- const secretsPath = config['aifabrix-secrets'];
360
- if (!remoteServer || !secretsPath || isHttpUrl(secretsPath)) return;
359
+ if (!remoteServer) return;
361
360
  const base = typeof remoteServer === 'string' ? remoteServer.trim().replace(/\/+$/, '') : '';
362
361
  if (!base) return;
362
+ const secretsPath = config['aifabrix-secrets'];
363
+ if (secretsPath && isHttpUrl(secretsPath)) return;
363
364
  config['aifabrix-secrets'] = `${base}/api/dev/secrets`;
364
365
  }
365
366
 
@@ -421,6 +422,8 @@ module.exports = {
421
422
  setPathConfig,
422
423
  createPathConfigFunctions,
423
424
  resolveEnvConfigPathToAbsolute,
424
- SETTINGS_RESPONSE_KEYS
425
+ SETTINGS_RESPONSE_KEYS,
426
+ applySecretsUrlFromRemote,
427
+ applySyncAndDockerFromHost
425
428
  };
426
429
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.47.1",
3
+ "version": "2.47.3",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {
package/.npmrc.token DELETED
@@ -1 +0,0 @@
1
- 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