@aifabrix/builder 2.57.0 → 2.59.0

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 (106) hide show
  1. package/lib/agent-kit/codex-app-server.js +44 -0
  2. package/lib/agent-kit/init.js +4 -17
  3. package/lib/agent-kit/session-report.js +15 -10
  4. package/lib/agent-kit/setup-auth.js +131 -0
  5. package/lib/agent-kit/setup-context.js +118 -0
  6. package/lib/agent-kit/setup-github.js +70 -0
  7. package/lib/agent-kit/setup-managed.js +79 -0
  8. package/lib/agent-kit/setup-process.js +42 -0
  9. package/lib/agent-kit/setup-session.js +53 -0
  10. package/lib/agent-kit/setup-state.js +52 -0
  11. package/lib/agent-kit/setup-workspace.js +113 -0
  12. package/lib/agent-kit/setup.js +84 -0
  13. package/lib/agent-kit/start.js +4 -3
  14. package/lib/agent-kit/tmux-sessions.js +20 -3
  15. package/lib/api/dev.api.js +1 -1
  16. package/lib/api/miso-health.api.js +26 -0
  17. package/lib/api/types/dev.types.js +3 -3
  18. package/lib/app/deploy.js +0 -102
  19. package/lib/app/push.js +98 -41
  20. package/lib/build/standard-docker-build.js +13 -11
  21. package/lib/cli/index.js +4 -0
  22. package/lib/cli/setup-app.js +16 -5
  23. package/lib/cli/setup-dev-path-commands.js +53 -2
  24. package/lib/cli/setup-dev.js +7 -4
  25. package/lib/cli/setup-environment.js +17 -4
  26. package/lib/cli/setup-help.js +86 -0
  27. package/lib/cli/setup-onboarding.js +37 -0
  28. package/lib/cli/setup-utility-repair.js +7 -5
  29. package/lib/commands/agent-kit-help.js +21 -7
  30. package/lib/commands/agent-kit-sessions.js +126 -0
  31. package/lib/commands/agent-kit-setup.js +44 -0
  32. package/lib/commands/agent-kit.js +5 -79
  33. package/lib/commands/dev-init.js +21 -23
  34. package/lib/commands/dev-refresh.js +2 -2
  35. package/lib/commands/dev-registry.js +155 -0
  36. package/lib/commands/dev-show-display.js +10 -2
  37. package/lib/commands/governance-verify-external.js +12 -5
  38. package/lib/commands/repair-datasource.js +9 -9
  39. package/lib/commands/repair.js +45 -8
  40. package/lib/commands/setup-image-refresh.js +26 -0
  41. package/lib/commands/upload-qualification-client.js +56 -11
  42. package/lib/commands/upload-qualification-poll.js +51 -22
  43. package/lib/commands/wizard-dataplane.js +16 -52
  44. package/lib/core/config-attach-extensions.js +4 -1
  45. package/lib/datasource/datasource-validate-summary.js +8 -5
  46. package/lib/datasource/sync-defaults.js +53 -0
  47. package/lib/deployment/installation/azure-infra-stage.js +50 -9
  48. package/lib/deployment/installation/azure-preflight.js +63 -7
  49. package/lib/deployment/installation/azure-readiness.js +59 -7
  50. package/lib/deployment/installation/environment-auth.js +60 -20
  51. package/lib/deployment/installation/environment-stage.js +9 -5
  52. package/lib/deployment/installation/infra-catalog.js +57 -5
  53. package/lib/deployment/installation/infra-manifest.js +22 -2
  54. package/lib/deployment/installation/infra-projection.js +14 -3
  55. package/lib/deployment/installation/registry-auth-mode.js +121 -0
  56. package/lib/deployment/installation/stage-input.js +21 -6
  57. package/lib/deployment/marketplace-environment.js +26 -1
  58. package/lib/generator/builders.js +8 -9
  59. package/lib/generator/index.js +23 -12
  60. package/lib/integration-definition/apply.js +2 -2
  61. package/lib/internal/fs-real-sync.js +4 -1
  62. package/lib/programmatic/bearer-auth.js +3 -62
  63. package/lib/programmatic/builder-api-startup.js +51 -0
  64. package/lib/programmatic/builder-miso-runtime.js +167 -0
  65. package/lib/programmatic/openapi-descriptions.js +3 -3
  66. package/lib/programmatic/openapi-schema-descriptions-core.js +3 -3
  67. package/lib/programmatic/openapi-spec.js +2 -1
  68. package/lib/programmatic/route-handler-map.js +3 -2
  69. package/lib/schema/environment-deploy-request.schema.json +1 -1
  70. package/lib/schema/external-datasource.schema.json +20 -5
  71. package/lib/schema/infra-parameter.schema.json +1 -1
  72. package/lib/schema/infra.parameter.yaml +2 -1
  73. package/lib/schema/infrastructure-schema.json +200 -3
  74. package/lib/schema/installation-environment.schema.json +1 -1
  75. package/lib/schema/wizard-config.schema.json +1 -1
  76. package/lib/utils/app-register-config.js +2 -4
  77. package/lib/utils/build-resolve-image.js +2 -21
  78. package/lib/utils/cli-utils.js +8 -1
  79. package/lib/utils/config-paths.js +24 -29
  80. package/lib/utils/config-registry-preference.js +113 -0
  81. package/lib/utils/dev-init-ssh-merge.js +5 -4
  82. package/lib/utils/dev-user-groups.js +2 -1
  83. package/lib/utils/docker-build.js +53 -19
  84. package/lib/utils/docker-daemon-tls-ca.js +2 -2
  85. package/lib/utils/env-map.js +23 -6
  86. package/lib/utils/error-formatters/validation-errors.js +25 -5
  87. package/lib/utils/help-builder.js +4 -3
  88. package/lib/utils/image-tags.js +88 -0
  89. package/lib/utils/promote-dev-docker-image.js +6 -0
  90. package/lib/utils/registry-credentials.js +236 -0
  91. package/lib/utils/resolve-docker-image-ref.js +78 -9
  92. package/lib/utils/secrets-helpers.js +9 -1
  93. package/lib/utils/token-manager.js +29 -36
  94. package/lib/utils/variable-transformer.js +2 -4
  95. package/package.json +4 -2
  96. package/templates/agent-kit/instructions/AGENTKIT.md +39 -2
  97. package/templates/agent-kit/instructions/root.AGENTS.md +40 -1
  98. package/templates/agent-kit/skills/shared/hosts.md +23 -5
  99. package/templates/agent-kit/workspace/README.md +14 -1
  100. package/templates/applications/builder-api/application.yaml +1 -1
  101. package/templates/applications/dataplane/application.yaml +1 -1
  102. package/templates/applications/dataplane/env.template +1 -1
  103. package/templates/applications/miso-controller/application.yaml +1 -1
  104. package/templates/applications/miso-controller/env.template +1 -1
  105. package/templates/external-system/external-datasource.yaml.hbs +7 -6
  106. package/templates/marketplace/main.json +440 -82
@@ -80,10 +80,8 @@ async function createMinimalAppIfNeeded(appKey, options) {
80
80
  * @returns {string} Image reference string
81
81
  */
82
82
  function buildImageReference(variables, appKey) {
83
- const imageName = variables.image?.name || variables.app?.key || appKey;
84
- const registry = variables.image?.registry;
85
- const tag = variables.image?.tag || 'latest';
86
- return registry ? `${registry}/${imageName}:${tag}` : `${imageName}:${tag}`;
83
+ const { buildManifestImageReference } = require('./resolve-docker-image-ref');
84
+ return buildManifestImageReference(variables, appKey);
87
85
  }
88
86
 
89
87
  /**
@@ -14,7 +14,8 @@ const { execWithDockerEnv } = require('./docker-exec');
14
14
  const { buildDevImageName, buildDevImageRepositoryPath } = require('./image-name');
15
15
  const {
16
16
  getRepositoryPathFromConfig,
17
- resolveDockerImageRef
17
+ resolveDockerImageRef,
18
+ stripRegistryHostFromRepository
18
19
  } = require('./resolve-docker-image-ref');
19
20
 
20
21
  /**
@@ -48,26 +49,6 @@ function repositoryTail(repoPath) {
48
49
  return parts[parts.length - 1] || repoPath;
49
50
  }
50
51
 
51
- /**
52
- * Remove registry host prefix from a Docker repository path (local build only).
53
- * @param {string} repository
54
- * @returns {string}
55
- */
56
- function stripRegistryHostFromRepository(repository) {
57
- if (!repository || typeof repository !== 'string') {
58
- return repository;
59
- }
60
- const parts = repository.split('/');
61
- if (parts.length <= 1) {
62
- return repository;
63
- }
64
- const host = parts[0];
65
- if (host.includes('.') || host.includes(':') || host === 'localhost') {
66
- return parts.slice(1).join('/');
67
- }
68
- return repository;
69
- }
70
-
71
52
  /**
72
53
  * Strip dev-scoped or -extra suffix from a Docker repository path.
73
54
  * @param {string} repo
@@ -198,6 +198,9 @@ function isAzureCliToolingError(errorMsg) {
198
198
  return false;
199
199
  }
200
200
 
201
+ /** Registry-credential and per-tag push errors that already name the host and the next step. */
202
+ const REGISTRY_ACTIONABLE_ERROR = /dev set-registry|Push failed for |No registry credentials are available/;
203
+
201
204
  /**
202
205
  * Format Azure-related errors
203
206
  * @param {string} errorMsg - Error message
@@ -221,6 +224,10 @@ function formatAzureError(errorMsg) {
221
224
  if (errorMsg.includes('Docker build failed') || errorMsg.includes('Build failed: Docker')) {
222
225
  return null;
223
226
  }
227
+ // Registry-credential and per-tag push errors already carry host-specific next steps.
228
+ if (REGISTRY_ACTIONABLE_ERROR.test(errorMsg)) {
229
+ return null;
230
+ }
224
231
  const lowerMsg = String(errorMsg || '').toLowerCase();
225
232
  const hasAcrDomain = /\b[a-z0-9.-]+\.azurecr\.io\b/i.test(errorMsg);
226
233
  const hasAcrKeyword = /\bacr\b/.test(lowerMsg);
@@ -238,7 +245,7 @@ function formatAzureError(errorMsg) {
238
245
  if (errorMsg.includes('Registry URL is required')) {
239
246
  return [
240
247
  ' Registry URL is required.',
241
- ' Provide via --registry flag or configure in application.yaml under image.registry'
248
+ ' Provide via --registry, set a default with aifabrix dev set-registry <host>, or configure image.registry in application.yaml'
242
249
  ];
243
250
  }
244
251
  return null;
@@ -101,13 +101,8 @@ function createHomeAndSecretsPathFunctions(getConfigFn, saveConfigFn) {
101
101
  return;
102
102
  }
103
103
  const resolved = path.resolve(trimmed);
104
- await setPathConfig(
105
- getConfigFn,
106
- saveConfigFn,
107
- 'aifabrix-work',
108
- resolved,
109
- 'Work path must be a non-empty string'
110
- );
104
+ await setPathConfig(getConfigFn, saveConfigFn, 'aifabrix-work', resolved,
105
+ 'Work path must be a non-empty string');
111
106
  },
112
107
  async getAifabrixSecretsPath() {
113
108
  return getPathConfig(getConfigFn, 'aifabrix-secrets');
@@ -168,6 +163,17 @@ async function resolveEnvConfigPathToAbsolute(raw, getConfigFn) {
168
163
  return path.normalize(path.resolve(base, trimmed));
169
164
  }
170
165
 
166
+ function findBuilderDirectory(base) {
167
+ if (!base) return null;
168
+ const candidate = path.join(base, 'builder');
169
+ try {
170
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isDirectory()) return candidate;
171
+ } catch {
172
+ /* Missing or inaccessible builder directory. */
173
+ }
174
+ return null;
175
+ }
176
+
171
177
  function createEnvConfigPathFunctions(getConfigFn, saveConfigFn) {
172
178
  return {
173
179
  /**
@@ -199,30 +205,16 @@ function createEnvConfigPathFunctions(getConfigFn, saveConfigFn) {
199
205
  return path.dirname(absolute);
200
206
  }
201
207
  const pathsMod = require('./paths');
202
- const tryDir = (base) => {
203
- if (!base) {
204
- return null;
205
- }
206
- const b = path.join(base, 'builder');
207
- try {
208
- if (fs.existsSync(b) && fs.statSync(b).isDirectory()) {
209
- return b;
210
- }
211
- } catch {
212
- /* ignore */
213
- }
214
- return null;
215
- };
216
- const fromProject = tryDir(pathsMod.getProjectRoot());
208
+ const fromProject = findBuilderDirectory(pathsMod.getProjectRoot());
217
209
  if (fromProject) {
218
210
  return fromProject;
219
211
  }
220
- const fromEffective = tryDir(pathsMod.getBuilderRoot());
212
+ const fromEffective = findBuilderDirectory(pathsMod.getBuilderRoot());
221
213
  if (fromEffective) {
222
214
  return fromEffective;
223
215
  }
224
216
  const work = pathsMod.getAifabrixWork();
225
- return tryDir(work);
217
+ return findBuilderDirectory(work);
226
218
  }
227
219
  };
228
220
  }
@@ -368,11 +360,10 @@ function applySecretsUrlFromRemote(config) {
368
360
  config['aifabrix-secrets'] = `${base}/api/dev/secrets`;
369
361
  }
370
362
 
371
- function applySyncAndDockerFromHost(config) {
363
+ function applySyncHost(config) {
372
364
  const host = hostnameFromUrl(config['remote-server']);
373
365
  if (!host) return;
374
366
  if (!config['sync-ssh-host']) config['sync-ssh-host'] = host;
375
- if (!config['docker-endpoint']) config['docker-endpoint'] = `tcp://${host}:2376`;
376
367
  }
377
368
 
378
369
  async function mergeRemoteSettingsImpl(getConfigFn, saveConfigFn, settings) {
@@ -386,8 +377,13 @@ async function mergeRemoteSettingsImpl(getConfigFn, saveConfigFn, settings) {
386
377
  if (value === '') continue;
387
378
  config[key] = value;
388
379
  }
380
+ // Current settings are authoritative: never inherit Docker access from another identity.
381
+ if (typeof settings['docker-endpoint'] !== 'string' || !settings['docker-endpoint'].trim()) {
382
+ delete config['docker-endpoint'];
383
+ delete config['docker-tls-skip-verify'];
384
+ }
389
385
  applySecretsUrlFromRemote(config);
390
- applySyncAndDockerFromHost(config);
386
+ applySyncHost(config);
391
387
  await saveConfigFn(config);
392
388
  }
393
389
 
@@ -428,7 +424,6 @@ module.exports = {
428
424
  resolveEnvConfigPathToAbsolute,
429
425
  SETTINGS_RESPONSE_KEYS,
430
426
  applySecretsUrlFromRemote,
431
- applySyncAndDockerFromHost,
427
+ applySyncAndDockerFromHost: applySyncHost,
432
428
  hostnameFromUrl
433
429
  };
434
-
@@ -0,0 +1,113 @@
1
+ /**
2
+ * Installation-level default container registry (`registry` in the Builder config.yaml).
3
+ *
4
+ * @fileoverview Registry host validation plus get/set/clear preference helpers
5
+ * @author AI Fabrix Team
6
+ * @version 1.0.0
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ const yaml = require('js-yaml');
12
+ const fs = require('../internal/fs-real-sync');
13
+ const { getRuntimeConfigFile } = require('../core/config-runtime-paths');
14
+
15
+ const REGISTRY_CONFIG_KEY = 'registry';
16
+ const HOST_LABEL = '[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?';
17
+ const REGISTRY_HOST_RE = new RegExp(`^${HOST_LABEL}(?:\\.${HOST_LABEL})*$`);
18
+
19
+ /**
20
+ * Normalizes and validates a registry host.
21
+ * Rejects schemes, paths, embedded credentials, query/fragment text and whitespace. A port is
22
+ * rejected because the deployment `image` contract (application-schema.json, shared with the
23
+ * Controller) accepts only one colon, which separates the tag.
24
+ * @param {string} value - Raw registry value
25
+ * @returns {string} Lower-case host
26
+ * @throws {Error} When the value is empty or not a plain host
27
+ */
28
+ function normalizeRegistryHost(value) {
29
+ const raw = typeof value === 'string' ? value.trim() : '';
30
+ const withoutSlash = raw.replace(/\/+$/, '');
31
+ if (!withoutSlash) {
32
+ throw new Error('Registry is required (for example myregistry.azurecr.io)');
33
+ }
34
+ if (/[\s@?#/\\]|:\/\//.test(withoutSlash)) {
35
+ throw new Error(`Invalid registry "${raw}": use a host name such as myregistry.azurecr.io (no scheme, path or credentials)`);
36
+ }
37
+ const host = withoutSlash.toLowerCase();
38
+ if (host.includes(':')) {
39
+ throw new Error(
40
+ `Invalid registry "${raw}": a port is not supported because the deployed image reference keeps one colon for the tag. ` +
41
+ 'Use a host name such as myregistry.azurecr.io.'
42
+ );
43
+ }
44
+ if (!REGISTRY_HOST_RE.test(host)) {
45
+ throw new Error(`Invalid registry "${raw}": use a host name such as myregistry.azurecr.io`);
46
+ }
47
+ return host;
48
+ }
49
+
50
+ /**
51
+ * Reads the installation registry synchronously (used by the shared image resolver).
52
+ * An unreadable or invalid value is treated as unset so image resolution keeps working.
53
+ * @returns {string} Normalized host or '' when unset
54
+ */
55
+ function readInstallationRegistrySync() {
56
+ try {
57
+ const file = getRuntimeConfigFile();
58
+ if (!fs.existsSync(file)) return '';
59
+ const cfg = yaml.load(fs.readFileSync(file, 'utf8')) || {};
60
+ const value = cfg[REGISTRY_CONFIG_KEY];
61
+ return typeof value === 'string' && value.trim() ? normalizeRegistryHost(value) : '';
62
+ } catch {
63
+ return '';
64
+ }
65
+ }
66
+
67
+ /**
68
+ * @param {Function} getConfigFn - async () => config object
69
+ * @param {Function} saveConfigFn - async (config) => void
70
+ * @returns {{ getRegistry: Function, setRegistry: Function, clearRegistry: Function }}
71
+ */
72
+ function createRegistryPreferenceFunctions(getConfigFn, saveConfigFn) {
73
+ return {
74
+ /**
75
+ * @returns {Promise<string|null>} Saved registry host or null
76
+ */
77
+ async getRegistry() {
78
+ const cfg = await getConfigFn();
79
+ const value = cfg[REGISTRY_CONFIG_KEY];
80
+ return typeof value === 'string' && value.trim() ? value.trim() : null;
81
+ },
82
+
83
+ /**
84
+ * @param {string} registry - Registry host
85
+ * @returns {Promise<string>} Normalized saved value
86
+ */
87
+ async setRegistry(registry) {
88
+ const host = normalizeRegistryHost(registry);
89
+ const cfg = await getConfigFn();
90
+ cfg[REGISTRY_CONFIG_KEY] = host;
91
+ await saveConfigFn(cfg);
92
+ return host;
93
+ },
94
+
95
+ /**
96
+ * Removes the saved registry (idempotent).
97
+ * @returns {Promise<void>}
98
+ */
99
+ async clearRegistry() {
100
+ const cfg = await getConfigFn();
101
+ if (!(REGISTRY_CONFIG_KEY in cfg)) return;
102
+ delete cfg[REGISTRY_CONFIG_KEY];
103
+ await saveConfigFn(cfg);
104
+ }
105
+ };
106
+ }
107
+
108
+ module.exports = {
109
+ REGISTRY_CONFIG_KEY,
110
+ normalizeRegistryHost,
111
+ readInstallationRegistrySync,
112
+ createRegistryPreferenceFunctions
113
+ };
@@ -163,16 +163,17 @@ async function mergeDevSshConfigBlock(p) {
163
163
  * Update ~/.ssh/config with Host <user>.<host> for interactive SSH; log result.
164
164
  * @param {string} baseUrl - Builder Server base URL
165
165
  * @param {string} devId - Developer ID (digits)
166
+ * @param {Object} [freshSettings] - Current authenticated server settings; never mix with saved identity
166
167
  * @returns {Promise<{ hostAlias: string|null, syncUser: string, syncHost: string|null }>}
167
168
  */
168
- async function mergeDevSshConfigAfterInit(baseUrl, devId) {
169
- const syncHost = (await config.getSyncSshHost()) || hostnameFromBaseUrl(baseUrl);
170
- const syncUser = (await config.getSyncSshUser()) || `dev${devId}`;
169
+ async function mergeDevSshConfigAfterInit(baseUrl, devId, freshSettings) {
170
+ const syncHost = (freshSettings ? freshSettings['sync-ssh-host'] : await config.getSyncSshHost()) || hostnameFromBaseUrl(baseUrl);
171
+ const syncUser = (freshSettings ? freshSettings['sync-ssh-user'] : await config.getSyncSshUser()) || `dev${devId}`;
171
172
  if (!syncHost) {
172
173
  return { hostAlias: null, syncUser, syncHost: null };
173
174
  }
174
175
  try {
175
- const rootTrimmed = trimDeveloperRootDomain(await config.getDeveloperRootDomain());
176
+ const rootTrimmed = trimDeveloperRootDomain(freshSettings ? freshSettings['developer-root-domain'] : await config.getDeveloperRootDomain());
176
177
  return await mergeDevSshConfigBlock({
177
178
  baseUrl,
178
179
  devId,
@@ -10,7 +10,8 @@ const ALLOWED_DEV_GROUPS = Object.freeze([
10
10
  'secret-manager',
11
11
  'secret-reader',
12
12
  'developer',
13
- 'docker'
13
+ 'docker',
14
+ 'integrator'
14
15
  ]);
15
16
 
16
17
  /**
@@ -252,13 +252,44 @@ async function executeBuild(imageName, dockerfilePath, contextPath, tag, options
252
252
  (options.cache === false || options.noCache === true || options['no-cache'] === true)
253
253
  );
254
254
  await executeDockerBuild(imageName, dockerfilePath, contextPath, tag, buildArgs, noCache);
255
+ await applyTagAliases(imageName, tag, resolveTagAliases(tag, options));
256
+ }
255
257
 
256
- // Also apply floating `latest` when building a version tag (matches dev-promote dual tags).
258
+ /**
259
+ * Aliases for the built image: explicit `options.tagAliases` from the shared tag plan, else the
260
+ * legacy rule (a single non-floating `options.tag` also gets `latest`).
261
+ * @param {string} tag - Built tag
262
+ * @param {Object} [options] - Build options
263
+ * @returns {string[]}
264
+ */
265
+ function resolveTagAliases(tag, options) {
266
+ if (options && Array.isArray(options.tagAliases)) {
267
+ return options.tagAliases.filter((alias) => alias !== tag);
268
+ }
257
269
  if (options && options.tag && options.tag !== 'latest' && options.tag !== 'main') {
258
- const { execFileWithDockerEnv } = require('./docker-exec');
259
- const { getDockerExecEnv } = require('./remote-docker-env');
260
- const env = await getDockerExecEnv();
261
- await execFileWithDockerEnv('docker', ['tag', `${imageName}:${tag}`, `${imageName}:latest`], { env });
270
+ return ['latest'];
271
+ }
272
+ return [];
273
+ }
274
+
275
+ /**
276
+ * Tag the built image with every alias (argument arrays; no shell interpolation).
277
+ * @param {string} imageName - Repository
278
+ * @param {string} tag - Built tag
279
+ * @param {string[]} aliases - Additional tags
280
+ * @returns {Promise<void>}
281
+ */
282
+ async function applyTagAliases(imageName, tag, aliases) {
283
+ if (!aliases.length) return;
284
+ const { execFileWithDockerEnv } = require('./docker-exec');
285
+ const { getDockerExecEnv } = require('./remote-docker-env');
286
+ const env = await getDockerExecEnv();
287
+ for (const alias of aliases) {
288
+ try {
289
+ await execFileWithDockerEnv('docker', ['tag', `${imageName}:${tag}`, `${imageName}:${alias}`], { env });
290
+ } catch (error) {
291
+ throw new Error(`Failed to tag ${imageName}:${alias}: ${error.message}`);
292
+ }
262
293
  }
263
294
  }
264
295
 
@@ -286,26 +317,29 @@ async function executeDockerBuildWithTag(effectiveImageName, imageName, dockerfi
286
317
  return;
287
318
  }
288
319
 
289
- try {
290
- const { execFileWithDockerEnv } = require('./docker-exec');
291
- const { getDockerExecEnv } = require('./remote-docker-env');
292
- const env = await getDockerExecEnv();
293
- await execFileWithDockerEnv(
294
- 'docker',
295
- ['tag', `${effectiveImageName}:${tag}`, `${imageName}:${tag}`],
296
- { env }
297
- );
298
- logger.log(formatSuccessLine(`Tagged image: ${imageName}:${tag}`));
299
- } catch (err) {
300
- logger.log(
301
- formatWarningLine(`Could not create compatibility tag ${imageName}:${tag} - ${err.message}`)
302
- );
320
+ const { execFileWithDockerEnv } = require('./docker-exec');
321
+ const { getDockerExecEnv } = require('./remote-docker-env');
322
+ const env = await getDockerExecEnv();
323
+ for (const compatTag of [tag, ...resolveTagAliases(tag, options)]) {
324
+ try {
325
+ await execFileWithDockerEnv(
326
+ 'docker',
327
+ ['tag', `${effectiveImageName}:${compatTag}`, `${imageName}:${compatTag}`],
328
+ { env }
329
+ );
330
+ logger.log(formatSuccessLine(`Tagged image: ${imageName}:${compatTag}`));
331
+ } catch (err) {
332
+ logger.log(
333
+ formatWarningLine(`Could not create compatibility tag ${imageName}:${compatTag} - ${err.message}`)
334
+ );
335
+ }
303
336
  }
304
337
  }
305
338
 
306
339
  module.exports = {
307
340
  executeDockerBuild,
308
341
  executeBuild,
342
+ resolveTagAliases,
309
343
  executeDockerBuildWithTag,
310
344
  isDockerNotAvailableError
311
345
  };
@@ -146,7 +146,7 @@ function dockerEndpointFromRemoteServer(remoteServerUrl) {
146
146
  }
147
147
 
148
148
  /**
149
- * Resolve docker-endpoint for CA augmentation (settings override, else derive from remote-server).
149
+ * Resolve only an explicitly advertised docker-endpoint; a server hostname is not Docker access.
150
150
  * @param {{ settings?: Object|null, remoteServerUrl?: string|null }} ctx
151
151
  * @returns {string|null}
152
152
  */
@@ -156,7 +156,7 @@ function resolveDockerEndpointForCaAugment(ctx) {
156
156
  if (typeof fromSettings === 'string' && fromSettings.trim()) {
157
157
  return fromSettings.trim();
158
158
  }
159
- return dockerEndpointFromRemoteServer(ctx && ctx.remoteServerUrl);
159
+ return null;
160
160
  }
161
161
 
162
162
  /**
@@ -12,6 +12,8 @@ const fs = require('fs');
12
12
  const yaml = require('js-yaml');
13
13
  const { loadEnvConfig, loadSchemaEnvConfig } = require('./env-config-loader');
14
14
  const config = require('../core/config');
15
+ const { buildDevUsernameForFrontDoorHost } = require('./compose-traefik-ingress-base');
16
+ const { resolveRemoteHostForFrontDoor } = require('./remote-host-for-front-door');
15
17
 
16
18
  /**
17
19
  * Load base environment variables from env-config.yaml
@@ -309,19 +311,34 @@ function applyPublicPortAdjustmentsForContext(result, context, devIdNum) {
309
311
  }
310
312
 
311
313
  /**
312
- * Set TLS_ENABLED / HTTP_ENABLED from ~/.aifabrix/config.yaml (tlsEnabled).
314
+ * Add config-derived interpolation values used by env.template.
313
315
  * @param {Object} result - Env map (mutated)
316
+ * @param {number} devIdNum - Normalized developer ID
314
317
  */
315
- async function applyTlsHttpManifestFlags(result) {
318
+ async function applyRuntimeConfigVars(result, devIdNum) {
319
+ let userCfg = {};
316
320
  let tlsOn = false;
317
321
  try {
318
- const userCfg = await config.getConfig();
322
+ userCfg = await config.getConfig();
319
323
  tlsOn = Boolean(userCfg && userCfg.tlsEnabled === true);
320
324
  } catch {
321
- tlsOn = false;
325
+ userCfg = {};
322
326
  }
323
327
  result.TLS_ENABLED = tlsOn ? 'true' : 'false';
324
328
  result.HTTP_ENABLED = tlsOn ? 'false' : 'true';
329
+ const devUsername = buildDevUsernameForFrontDoorHost(devIdNum);
330
+ const remoteServerHost = resolveRemoteHostForFrontDoor({
331
+ remoteServer: userCfg && userCfg['remote-server']
332
+ });
333
+ const remoteHost = remoteServerHost ? remoteServerHost.split('.')[0] : '';
334
+ const devRootDomain = resolveRemoteHostForFrontDoor({
335
+ developerRootDomain: userCfg && userCfg['developer-root-domain']
336
+ });
337
+ if (devUsername || remoteHost) {
338
+ result.DEV_USERNAME = devUsername;
339
+ result.REMOTE_HOST = remoteHost;
340
+ }
341
+ if (devRootDomain) result.DEV_ROOT_DOMAIN = devRootDomain;
325
342
  }
326
343
 
327
344
  /**
@@ -333,6 +350,7 @@ async function applyTlsHttpManifestFlags(result) {
333
350
  * - Applies developer-id adjustment to port variables for local context
334
351
  * - Calculates *_PUBLIC_PORT for both local and docker context (basePort + developer-id * 100)
335
352
  * - Adds TLS_ENABLED and HTTP_ENABLED ("true" / "false"); HTTP_ENABLED = !TLS_ENABLED for manifest ${...} substitution
353
+ * - Adds DEV_USERNAME, REMOTE_HOST, and DEV_ROOT_DOMAIN from Builder configuration
336
354
  * @async
337
355
  * @function buildEnvVarMap
338
356
  * @param {'docker'|'local'} context - Environment context
@@ -358,7 +376,7 @@ async function buildEnvVarMap(context, osModule = null, developerId = null, opti
358
376
  applyLocalPortAdjustment(result, devIdNum);
359
377
  }
360
378
  applyPublicPortAdjustmentsForContext(result, context, devIdNum);
361
- await applyTlsHttpManifestFlags(result);
379
+ await applyRuntimeConfigVars(result, devIdNum);
362
380
  return result;
363
381
  }
364
382
 
@@ -367,4 +385,3 @@ module.exports = {
367
385
  getDeveloperIdNumber,
368
386
  resolvePortInEnvValues
369
387
  };
370
-
@@ -11,6 +11,26 @@ const { formatBlockingError } = require('../cli-test-layout-chalk');
11
11
 
12
12
  const chalk = require('chalk');
13
13
 
14
+ /**
15
+ * Resolves legacy application lookup codes without replacing API explanations.
16
+ * @param {Object|string} error - Validation error entry
17
+ * @returns {{message: string, applicationNotFound: boolean}} Display information
18
+ */
19
+ function getValidationMessage(error) {
20
+ const message = typeof error === 'string' ? error : error.msg || error.message;
21
+ const code = typeof error === 'object' ? error.code : undefined;
22
+ const applicationNotFound = message === 'APPLICATION_NOT_FOUND' || code === 'APPLICATION_NOT_FOUND';
23
+ if (applicationNotFound && (!message || message === 'APPLICATION_NOT_FOUND')) {
24
+ return {
25
+ message: 'The controller could not find the application. ' +
26
+ 'Check that it is registered on the selected controller and that its application key matches. ' +
27
+ '(Error code: APPLICATION_NOT_FOUND)',
28
+ applicationNotFound
29
+ };
30
+ }
31
+ return { message: message || 'Invalid value', applicationNotFound };
32
+ }
33
+
14
34
  /**
15
35
  * Adds main error message to lines array
16
36
  * @param {Array<string>} lines - Lines array to append to
@@ -52,7 +72,7 @@ function addValidationErrorsList(lines, errors) {
52
72
  lines.push(chalk.yellow('Validation errors:'));
53
73
  errors.forEach(err => {
54
74
  const field = err.field || err.path || err.loc?.join('.') || 'validation';
55
- const message = err.msg || err.message || 'Invalid value';
75
+ const { message } = getValidationMessage(err);
56
76
  const value = err.value !== undefined ? ` (value: ${JSON.stringify(err.value)})` : '';
57
77
  if (field === 'validation' || field === 'unknown') {
58
78
  lines.push(chalk.gray(` • ${message}${value}`));
@@ -91,10 +111,10 @@ function addConfigurationErrors(lines, configuration) {
91
111
  /**
92
112
  * Adds actionable guidance tips to lines
93
113
  * @param {Array<string>} lines - Lines array to append to
94
- * @param {boolean} hasErrors - Whether there are validation errors
114
+ * @param {Array<Object|string>} errors - Validation errors
95
115
  */
96
- function addValidationGuidance(lines, hasErrors) {
97
- if (!hasErrors) {
116
+ function addValidationGuidance(lines, errors) {
117
+ if (!Array.isArray(errors) || !errors.some(err => !getValidationMessage(err).applicationNotFound)) {
98
118
  return;
99
119
  }
100
120
  lines.push(chalk.gray('Tips:'));
@@ -117,7 +137,7 @@ function formatValidationError(errorData) {
117
137
  addValidationErrorMessage(lines, errorData);
118
138
  addValidationErrorsList(lines, errorData.errors);
119
139
  addConfigurationErrors(lines, errorData.configuration);
120
- addValidationGuidance(lines, errorData.errors && errorData.errors.length > 0);
140
+ addValidationGuidance(lines, errorData.errors);
121
141
 
122
142
  if (errorData.instance) {
123
143
  lines.push(chalk.gray(`Endpoint: ${errorData.instance}`));
@@ -17,6 +17,7 @@ const { Help } = require('commander');
17
17
  * @type {Array<{ name: string, commands: Array<{ name: string, term?: string }> }>}
18
18
  */
19
19
  const CATEGORIES = [
20
+ { name: 'Getting Started', commands: [{ name: 'help', term: 'help [command]' }, { name: 'onboarding' }] },
20
21
  {
21
22
  name: 'Infrastructure (Local Development)',
22
23
  commands: [
@@ -209,9 +210,9 @@ function formatCommandCategories(helper, program) {
209
210
  for (const { term, desc: d } of lines) out.push(` ${term.padEnd(pad)}${d}`);
210
211
  out.push('');
211
212
  }
212
- out.push('Help:');
213
- out.push(` ${'help [command]'.padEnd(pad)}display help for command`);
214
- out.push('');
213
+ if (!program.commands.some(command => command.name() === 'help')) {
214
+ out.push('Help:', ` ${'help [command]'.padEnd(pad)}display help for command`, '');
215
+ }
215
216
  out.push(` ${''.padEnd(pad)}Tip: aifabrix <command> --help for flags and examples`);
216
217
  out.push('');
217
218
  return out;