@aifabrix/builder 2.55.0 → 2.55.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/lib/agent-kit/host-approve.js +48 -6
  2. package/lib/agent-kit/init.js +2 -2
  3. package/lib/agent-kit/remote-prompts.js +46 -13
  4. package/lib/agent-kit/run.js +1 -1
  5. package/lib/agent-kit/start.js +310 -0
  6. package/lib/app/deploy.js +7 -4
  7. package/lib/app/offer-create-from-template.js +184 -0
  8. package/lib/cli/setup-app.js +8 -1
  9. package/lib/cli/setup-auth.js +11 -2
  10. package/lib/commands/agent-kit-help.js +74 -4
  11. package/lib/commands/agent-kit.js +83 -12
  12. package/lib/commands/auth-status-user.js +99 -0
  13. package/lib/commands/auth-status.js +14 -4
  14. package/lib/commands/datasource-load-export-cli.js +7 -2
  15. package/lib/commands/datasource-test-trust-cli.js +5 -1
  16. package/lib/commands/governance-command-action.js +5 -4
  17. package/lib/commands/identity-membership-role-cache.js +6 -1
  18. package/lib/commands/lifecycle-command-action.js +16 -15
  19. package/lib/commands/login-credentials.js +8 -3
  20. package/lib/commands/login-device.js +14 -5
  21. package/lib/commands/login.js +14 -1
  22. package/lib/commands/logout.js +50 -15
  23. package/lib/commands/role-assistant.js +24 -12
  24. package/lib/commands/verify-governance-command-action.js +9 -7
  25. package/lib/commands/verify-operations-command-action.js +9 -8
  26. package/lib/commands/verify-trust-command-action.js +6 -5
  27. package/lib/external-system/download.js +1 -2
  28. package/lib/generator/helpers.js +12 -1
  29. package/lib/integration-definition/apply-static-enterprise.js +5 -2
  30. package/lib/role-assistant/knowledge-sync-settings-push.js +172 -0
  31. package/lib/role-assistant/knowledge-sync.js +9 -126
  32. package/lib/role-assistant/test-cases-evaluate.js +12 -1
  33. package/lib/role-assistant/test-cases-evidence.js +44 -5
  34. package/lib/role-assistant/test-cases-lifecycle.js +9 -3
  35. package/lib/role-assistant/test-runner.js +3 -0
  36. package/lib/schema/infra-parameter.schema.json +51 -3
  37. package/lib/schema/infra.parameter.yaml +476 -40
  38. package/lib/schema/type/document-storage.json +1 -1
  39. package/lib/utils/api-required-permissions.js +271 -0
  40. package/lib/utils/api.js +16 -13
  41. package/lib/utils/cli-json-exit.js +41 -0
  42. package/lib/utils/cli-utils.js +23 -5
  43. package/lib/utils/controller-url.js +78 -4
  44. package/lib/utils/deployment-errors.js +15 -0
  45. package/lib/utils/deployment-validation-helpers.js +4 -1
  46. package/lib/utils/device-code.js +31 -10
  47. package/lib/utils/error-formatters/error-parser.js +16 -1
  48. package/lib/utils/error-formatters/permission-errors.js +19 -5
  49. package/lib/utils/keycloak-realm-ready.js +5 -2
  50. package/lib/utils/logger.js +1 -0
  51. package/lib/utils/paths.js +26 -5
  52. package/lib/utils/secrets-helpers.js +2 -2
  53. package/lib/utils/write-stream-and-wait.js +63 -0
  54. package/package.json +20 -6
  55. package/templates/agent-kit/agent-kit.yaml +1 -1
  56. package/templates/agent-kit/instructions/AGENTKIT.md +25 -4
  57. package/templates/agent-kit/instructions/root.AGENTS.md +1 -1
  58. package/templates/agent-kit/skills/aifabrix-connected-system/SKILL.md +2 -1
  59. package/templates/agent-kit/skills/aifabrix-connected-system/references/delivery-gates.md +1 -1
  60. package/templates/agent-kit/skills/aifabrix-plan/SKILL.md +2 -1
  61. package/templates/agent-kit/skills/aifabrix-prove/SKILL.md +1 -0
  62. package/templates/agent-kit/skills/aifabrix-role-assistant/SKILL.md +1 -0
  63. package/templates/agent-kit/skills/shared/hosts.md +13 -26
  64. package/templates/agent-kit/skills/shared/login.md +16 -0
  65. package/templates/agent-kit/workspace/README.md +1 -1
  66. package/templates/applications/dataplane/application.yaml +1 -2
  67. package/templates/applications/dataplane/env.template +5 -5
  68. package/templates/applications/miso-controller/application.yaml +21 -2
  69. package/templates/applications/miso-controller/env.template +89 -17
  70. package/templates/applications/miso-controller/rbac.yaml +5 -0
@@ -10,9 +10,50 @@ const fs = require('../internal/fs-real-sync');
10
10
 
11
11
  const CLAUDE_REL = '.claude/settings.local.json';
12
12
  const CODEX_REL = '.codex/config.toml';
13
+ const APPROVE_HOSTS = ['claude', 'codex'];
13
14
  const IGNORE_COMMENT = '# Host auto-approve (machine-local; aifabrix agent-kit)';
14
15
  const IGNORE_LINES = [CLAUDE_REL, CODEX_REL];
15
16
 
17
+ /**
18
+ * --approve-all [host]: true/'all' = every selected host; claude|codex = one tool.
19
+ * @param {unknown} value
20
+ * @returns {{ requested: boolean, target: string|null }}
21
+ */
22
+ function parseApproveAll(value) {
23
+ if (value === undefined || value === false || value === '') {
24
+ return { requested: false, target: null };
25
+ }
26
+ if (value === true || value === 'all') {
27
+ return { requested: true, target: 'all' };
28
+ }
29
+ const target = String(value);
30
+ if (APPROVE_HOSTS.includes(target)) {
31
+ return { requested: true, target };
32
+ }
33
+ throw new Error('Invalid --approve-all. Use all, claude, or codex.');
34
+ }
35
+
36
+ /**
37
+ * Subset of managed hosts that receive auto-approve files.
38
+ * @param {string[]} [managedHosts]
39
+ * @param {unknown} approveAll
40
+ * @returns {string[]}
41
+ */
42
+ function resolveApproveHosts(managedHosts, approveAll) {
43
+ const parsed = parseApproveAll(approveAll);
44
+ if (!parsed.requested) {
45
+ return [];
46
+ }
47
+ const eligible = (managedHosts || []).filter((h) => APPROVE_HOSTS.includes(h));
48
+ if (parsed.target === 'all') {
49
+ return eligible;
50
+ }
51
+ if (!eligible.includes(parsed.target)) {
52
+ throw new Error(`--approve-all ${parsed.target} requires --host ${parsed.target} or --host all.`);
53
+ }
54
+ return [parsed.target];
55
+ }
56
+
16
57
  /**
17
58
  * This workspace for writes. Read(~/**) is home-anchored. Bash(aifabrix *)
18
59
  * allows the delivery CLI without granting every shell command.
@@ -101,15 +142,13 @@ function listApproveWrites(options) {
101
142
  * @returns {{ requested: boolean, files: { path: string, action: string }[] }}
102
143
  */
103
144
  function writeApproveAllFiles(options) {
104
- if (!options.approveAll) {
145
+ const parsed = parseApproveAll(options.approveAll);
146
+ if (!parsed.requested) {
105
147
  return { requested: false, files: [] };
106
148
  }
149
+ const hosts = resolveApproveHosts(options.hosts, options.approveAll);
107
150
  const fsApi = options.fs || fs;
108
- const ctx = {
109
- repoRoot: options.repoRoot,
110
- hosts: options.hosts || [],
111
- fsApi
112
- };
151
+ const ctx = { repoRoot: options.repoRoot, hosts, fsApi };
113
152
  const dryRun = Boolean(options.dryRun);
114
153
  const force = Boolean(options.force);
115
154
  const files = listApproveWrites(ctx).map((row) =>
@@ -124,6 +163,9 @@ function writeApproveAllFiles(options) {
124
163
  module.exports = {
125
164
  CLAUDE_REL,
126
165
  CODEX_REL,
166
+ APPROVE_HOSTS,
167
+ parseApproveAll,
168
+ resolveApproveHosts,
127
169
  buildClaudeSettings,
128
170
  buildCodexConfig,
129
171
  writeApproveAllFiles
@@ -152,7 +152,7 @@ function runInit(options) {
152
152
  dryRun,
153
153
  targetAbs,
154
154
  host: options.host || 'all',
155
- approveAll: Boolean(options.approveAll),
155
+ approveAll: options.approveAll,
156
156
  force: Boolean(options.force),
157
157
  gitResult,
158
158
  phases
@@ -195,7 +195,7 @@ function installKitPhase(p) {
195
195
  const kit = runInstall({
196
196
  cwd: p.targetAbs,
197
197
  host: p.host,
198
- approveAll: Boolean(p.approveAll),
198
+ approveAll: p.approveAll,
199
199
  force: Boolean(p.force)
200
200
  });
201
201
  p.phases.push({ phase: 'agent-kit', state: kit.state });
@@ -7,6 +7,7 @@
7
7
 
8
8
  const { execFileSync } = require('child_process');
9
9
  const { ALL_HOSTS, HOST_LABELS, normalizeHosts } = require('./hosts');
10
+ const { APPROVE_HOSTS, parseApproveAll } = require('./host-approve');
10
11
 
11
12
  /**
12
13
  * @param {{ execFileSync?: Function }} [options]
@@ -198,18 +199,57 @@ async function enrichInitRemoteOptions(options, deps = {}) {
198
199
  }
199
200
 
200
201
  /**
201
- * TTY asks; --approve-all / --yes / non-TTY auto-approve; --skip-approve-all skips.
202
+ * @param {object} inquirer
203
+ * @param {string[]} eligible
204
+ * @returns {Promise<true|false|string>}
205
+ */
206
+ async function askApproveAllChoice(inquirer, eligible) {
207
+ if (eligible.length === 1) {
208
+ const host = eligible[0];
209
+ const { approveAll } = await inquirer.prompt([
210
+ {
211
+ type: 'confirm',
212
+ name: 'approveAll',
213
+ message: `Auto-approve ${HOST_LABELS[host] || host} tool permissions in this workspace?`,
214
+ default: true
215
+ }
216
+ ]);
217
+ return Boolean(approveAll);
218
+ }
219
+ const { approveAll } = await inquirer.prompt([
220
+ {
221
+ type: 'list',
222
+ name: 'approveAll',
223
+ message: 'Which coding host should auto-approve tools in this workspace?',
224
+ default: 'all',
225
+ choices: [
226
+ { name: 'Both Claude Code and Codex', value: 'all' },
227
+ { name: 'Claude Code only', value: 'claude' },
228
+ { name: 'Codex only', value: 'codex' },
229
+ { name: 'Skip (the coding app will ask each time)', value: 'skip' }
230
+ ]
231
+ }
232
+ ]);
233
+ return approveAll === 'skip' ? false : approveAll;
234
+ }
235
+
236
+ /**
237
+ * TTY asks; --approve-all [host] / --yes / non-TTY auto-approve; --skip-approve-all skips.
202
238
  * @param {object} options
203
239
  * @param {{ interactive?: boolean, inquirer?: object, ask?: boolean }} [deps]
204
240
  * @returns {Promise<object>}
205
241
  */
206
242
  async function enrichApproveAllOptions(options, deps = {}) {
207
243
  const hosts = normalizeHosts(options.host);
208
- const needsHost = hosts.includes('claude') || hosts.includes('codex');
209
- if (!needsHost || options.skipApproveAll) {
244
+ const eligible = hosts.filter((h) => APPROVE_HOSTS.includes(h));
245
+ if (!eligible.length || options.skipApproveAll) {
210
246
  return { ...options, approveAll: false };
211
247
  }
212
- if (options.approveAll || options.yes) {
248
+ if (options.approveAll) {
249
+ parseApproveAll(options.approveAll);
250
+ return options;
251
+ }
252
+ if (options.yes) {
213
253
  return { ...options, approveAll: true };
214
254
  }
215
255
  if (deps.ask === false) {
@@ -219,15 +259,8 @@ async function enrichApproveAllOptions(options, deps = {}) {
219
259
  return { ...options, approveAll: true };
220
260
  }
221
261
  const inquirer = deps.inquirer || require('inquirer');
222
- const { approveAll } = await inquirer.prompt([
223
- {
224
- type: 'confirm',
225
- name: 'approveAll',
226
- message: 'Auto-approve all Claude Code and Codex tool permissions in this workspace?',
227
- default: true
228
- }
229
- ]);
230
- return { ...options, approveAll: Boolean(approveAll) };
262
+ const approveAll = await askApproveAllChoice(inquirer, eligible);
263
+ return { ...options, approveAll };
231
264
  }
232
265
 
233
266
  module.exports = {
@@ -46,7 +46,7 @@ function runManagedCommand(options, mode) {
46
46
  const approvals = writeApproveAllFiles({
47
47
  repoRoot,
48
48
  hosts,
49
- approveAll: Boolean(options.approveAll),
49
+ approveAll: options.approveAll,
50
50
  dryRun: Boolean(options.dryRun),
51
51
  force: Boolean(options.force)
52
52
  });
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Start Claude Code or Codex in a named tmux session.
3
+ * @fileoverview Does not install Agent Kit skills
4
+ */
5
+
6
+ 'use strict';
7
+
8
+ const os = require('os');
9
+ const path = require('path');
10
+ const { execFileSync, spawnSync } = require('child_process');
11
+ const logger = require('../utils/logger');
12
+ const { resolveGitRoot } = require('./repo-guard');
13
+ const { writeApproveAllFiles } = require('./host-approve');
14
+
15
+ const START_APPS = {
16
+ claude: { bin: 'claude' },
17
+ codex: { bin: 'codex' }
18
+ };
19
+
20
+ /**
21
+ * @param {unknown} value
22
+ * @returns {string}
23
+ */
24
+ function normalizeStartApp(value) {
25
+ const app = String(value || '').trim();
26
+ if (!START_APPS[app]) {
27
+ throw new Error('Invalid app. Use claude or codex.');
28
+ }
29
+ return app;
30
+ }
31
+
32
+ /**
33
+ * Session names may not contain colons or periods.
34
+ * @param {unknown} value
35
+ * @returns {string}
36
+ */
37
+ function sanitizeSessionName(value) {
38
+ const name = String(value || '')
39
+ .trim()
40
+ .replace(/[.:\s]+/g, '-')
41
+ .replace(/-+/g, '-')
42
+ .replace(/^-|-$/g, '');
43
+ if (!name) {
44
+ throw new Error(
45
+ 'Invalid --name. Use the last folder of this git workspace, for example aifabrix-docs.'
46
+ );
47
+ }
48
+ return name;
49
+ }
50
+
51
+ /**
52
+ * Default display name is the last folder of the git workspace.
53
+ * @param {unknown} explicit
54
+ * @param {string} cwd
55
+ * @returns {string}
56
+ */
57
+ function resolveSessionName(explicit, cwd) {
58
+ const raw = explicit === undefined || explicit === null || explicit === ''
59
+ ? path.basename(cwd)
60
+ : explicit;
61
+ return sanitizeSessionName(raw);
62
+ }
63
+
64
+ /**
65
+ * @param {object} options
66
+ * @returns {string}
67
+ */
68
+ function currentUsername(options) {
69
+ if (options && options.username) {
70
+ return String(options.username);
71
+ }
72
+ try {
73
+ return os.userInfo().username;
74
+ } catch {
75
+ return process.env.USER || process.env.LOGNAME || 'user';
76
+ }
77
+ }
78
+
79
+ /**
80
+ * Hostname + login, e.g. builder01-dev03.
81
+ * @param {object} [options]
82
+ * @returns {string}
83
+ */
84
+ function hostUserPrefix(options) {
85
+ const hostRaw = options && options.hostname ? options.hostname : os.hostname();
86
+ const host = String(hostRaw).split('.')[0];
87
+ return sanitizeSessionName(`${host}-${currentUsername(options || {})}`);
88
+ }
89
+
90
+ /**
91
+ * Remote Control session: builder01-dev03-aifabrix-docs.
92
+ * @param {string} prefix
93
+ * @param {string} session
94
+ * @returns {string}
95
+ */
96
+ function resolveRemoteControlName(prefix, session) {
97
+ return sanitizeSessionName(`${prefix}-${session}`);
98
+ }
99
+
100
+ /**
101
+ * @param {string} bin
102
+ * @param {Function} exec
103
+ * @returns {boolean}
104
+ */
105
+ function hasOnPath(bin, exec) {
106
+ try {
107
+ exec('which', [bin], {
108
+ encoding: 'utf8',
109
+ stdio: ['ignore', 'pipe', 'pipe']
110
+ });
111
+ return true;
112
+ } catch {
113
+ return false;
114
+ }
115
+ }
116
+
117
+ /**
118
+ * @param {object} options
119
+ * @returns {void}
120
+ */
121
+ function validateStartOptions(options) {
122
+ const app = String(options.app || '').trim();
123
+ if (options.remoteControl && app !== 'claude') {
124
+ throw new Error('--remote-control is only valid with claude.');
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Claude argv after the binary (Remote Control names, not Claude --tmux).
130
+ * @param {object} p
131
+ * @returns {string[]}
132
+ */
133
+ function claudeInnerArgs(p) {
134
+ const args = ['--name', p.session];
135
+ if (!p.remoteControl) {
136
+ return args;
137
+ }
138
+ return args.concat([
139
+ '--remote-control',
140
+ p.remoteControlName,
141
+ '--remote-control-session-name-prefix',
142
+ p.remoteControlPrefix
143
+ ]);
144
+ }
145
+
146
+ /**
147
+ * @param {object} p
148
+ * @returns {object}
149
+ */
150
+ function buildTmuxSpec(p) {
151
+ const spec = START_APPS[p.app];
152
+ const extra = p.app === 'claude' ? claudeInnerArgs(p) : [];
153
+ const mode = p.detach
154
+ ? ['new-session', '-d', '-s', p.session, '-c', p.cwd]
155
+ : ['new-session', '-A', '-s', p.session, '-c', p.cwd];
156
+ const remote = p.app === 'claude' && p.remoteControl;
157
+ return {
158
+ bin: 'tmux',
159
+ args: [...mode, spec.bin, ...extra],
160
+ session: p.session,
161
+ appBin: spec.bin,
162
+ needTmux: true,
163
+ remoteControlName: remote ? p.remoteControlName : undefined,
164
+ remoteControlPrefix: remote ? p.remoteControlPrefix : undefined
165
+ };
166
+ }
167
+
168
+ /**
169
+ * @param {object} p
170
+ * @returns {object}
171
+ */
172
+ function buildLaunchSpec(p) {
173
+ return buildTmuxSpec(p);
174
+ }
175
+
176
+ /**
177
+ * @param {{ appBin: string, needTmux?: boolean }} spec
178
+ * @param {Function} exec
179
+ * @returns {void}
180
+ */
181
+ function requireBinaries(spec, exec) {
182
+ if (spec.needTmux && !hasOnPath('tmux', exec)) {
183
+ throw new Error('tmux is not on PATH. Install tmux, then retry.');
184
+ }
185
+ if (hasOnPath(spec.appBin, exec)) {
186
+ return;
187
+ }
188
+ if (spec.appBin === 'claude') {
189
+ throw new Error(
190
+ 'claude is not on PATH. Install with: curl -fsSL https://claude.ai/install.sh | bash'
191
+ );
192
+ }
193
+ throw new Error('codex is not on PATH. Install with: npm install -g @openai/codex');
194
+ }
195
+
196
+ /**
197
+ * @param {object} options
198
+ * @returns {boolean}
199
+ */
200
+ function shouldExecute(options) {
201
+ if (options.dryRun || options.json) {
202
+ return false;
203
+ }
204
+ if (options.detach) {
205
+ return true;
206
+ }
207
+ if (!process.stdout.isTTY) {
208
+ throw new Error('agent-kit start needs a TTY to attach. Pass --detach or --dry-run.');
209
+ }
210
+ return true;
211
+ }
212
+
213
+ /**
214
+ * @param {object} options
215
+ * @param {{ bin: string, args: string[], cwd: string }} spec
216
+ * @returns {unknown}
217
+ */
218
+ function spawnLaunch(options, spec) {
219
+ const spawn = options.spawnSync || spawnSync;
220
+ const result = spawn(spec.bin, spec.args, { stdio: 'inherit', cwd: spec.cwd });
221
+ const status = result && typeof result.status === 'number' ? result.status : 0;
222
+ if (status !== 0) {
223
+ throw new Error(`${spec.bin} exited with status ${status}`);
224
+ }
225
+ return status;
226
+ }
227
+
228
+ /**
229
+ * @param {object} spec
230
+ * @param {object} extra
231
+ * @returns {object}
232
+ */
233
+ function toStartResult(spec, extra) {
234
+ const launch = [spec.bin, ...spec.args];
235
+ return {
236
+ command: 'start',
237
+ app: extra.app,
238
+ session: spec.session,
239
+ remoteControlName: spec.remoteControlName,
240
+ remoteControlPrefix: spec.remoteControlPrefix,
241
+ path: extra.cwd,
242
+ launch,
243
+ tmux: spec.needTmux ? launch : undefined,
244
+ approvals: extra.approvals,
245
+ dryRun: extra.dryRun,
246
+ detach: extra.detach,
247
+ executed: extra.executed
248
+ };
249
+ }
250
+
251
+ /**
252
+ * @param {object} options
253
+ * @returns {object}
254
+ */
255
+ function runStart(options) {
256
+ validateStartOptions(options);
257
+ const app = normalizeStartApp(options.app);
258
+ const cwd = resolveGitRoot(options.cwd || process.cwd());
259
+ const session = resolveSessionName(options.name, cwd);
260
+ const remoteControlPrefix = hostUserPrefix(options);
261
+ const remoteControlName = resolveRemoteControlName(remoteControlPrefix, session);
262
+ const exec = options.execFileSync || execFileSync;
263
+ const spec = buildLaunchSpec({
264
+ app,
265
+ cwd,
266
+ session,
267
+ remoteControlPrefix,
268
+ remoteControlName,
269
+ detach: Boolean(options.detach),
270
+ remoteControl: Boolean(options.remoteControl)
271
+ });
272
+ spec.cwd = cwd;
273
+ const approveAll = !options.skipApproveAll && Boolean(options.approveAll || options.yes);
274
+ const approvals = writeApproveAllFiles({
275
+ repoRoot: cwd,
276
+ hosts: [app],
277
+ approveAll,
278
+ dryRun: Boolean(options.dryRun)
279
+ });
280
+ if (!options.dryRun) {
281
+ requireBinaries(spec, exec);
282
+ }
283
+ const execute = shouldExecute(options);
284
+ if (execute && !options.json) {
285
+ logger.log(`Starting ${app} session ${spec.session}`);
286
+ }
287
+ if (execute) {
288
+ spawnLaunch(options, spec);
289
+ }
290
+ return toStartResult(spec, {
291
+ app,
292
+ cwd,
293
+ approvals,
294
+ dryRun: Boolean(options.dryRun),
295
+ detach: Boolean(options.detach),
296
+ executed: execute
297
+ });
298
+ }
299
+
300
+ module.exports = {
301
+ START_APPS,
302
+ normalizeStartApp,
303
+ sanitizeSessionName,
304
+ resolveSessionName,
305
+ hostUserPrefix,
306
+ resolveRemoteControlName,
307
+ claudeInnerArgs,
308
+ buildTmuxSpec,
309
+ runStart
310
+ };
package/lib/app/deploy.js CHANGED
@@ -23,6 +23,7 @@ const pushUtils = require('../deployment/push');
23
23
  const SEP = chalk.gray('────────────────────────────────────────');
24
24
  const logger = require('../utils/logger');
25
25
  const { detectAppType, getBuilderPath, getIntegrationPath } = require('../utils/paths');
26
+ const { offerCreateFromTemplateIfMissing } = require('./offer-create-from-template');
26
27
  const { checkApplicationExists } = require('../utils/app-existence');
27
28
  const { loadDeploymentConfig } = require('./deploy-config');
28
29
  const { logOfflinePathWhenType } = require('../utils/cli-utils');
@@ -462,7 +463,7 @@ async function executeDeployPipeline(appName, options) {
462
463
  * @param {boolean} [options.poll] - Poll for deployment status
463
464
  * @param {number} [options.pollInterval] - Polling interval in milliseconds
464
465
  * @param {number} [options.pollMaxAttempts] - Max polling attempts
465
- * @returns {Promise<{ result: Object, usedExternalDeploy: boolean }>} Deployment result and whether external deploy was used
466
+ * @returns {Promise<{ result: Object, usedExternalDeploy: boolean, appKey: string }>} Deployment result and whether external deploy was used
466
467
  * @throws {Error} If deployment fails
467
468
  *
468
469
  * Controller and environment come from config.yaml (set via aifabrix login or aifabrix auth config).
@@ -473,16 +474,18 @@ async function executeDeployPipeline(appName, options) {
473
474
  async function deployApp(appName, options = {}) {
474
475
  let controllerUrl = null;
475
476
  let usedExternalDeploy = false;
477
+ let resolvedName = appName;
476
478
 
477
479
  try {
478
480
  assertDeployNotProtectionScope(appName);
479
481
  validateAppName(appName);
480
- const pipeline = await executeDeployPipeline(appName, options);
482
+ resolvedName = await offerCreateFromTemplateIfMissing(appName, options);
483
+ const pipeline = await executeDeployPipeline(resolvedName, options);
481
484
  controllerUrl = pipeline.controllerUrl ?? null;
482
485
  usedExternalDeploy = pipeline.usedExternalDeploy;
483
- return { result: pipeline.result, usedExternalDeploy };
486
+ return { result: pipeline.result, usedExternalDeploy, appKey: resolvedName };
484
487
  } catch (error) {
485
- await handleDeploymentError(error, appName, controllerUrl, usedExternalDeploy);
488
+ await handleDeploymentError(error, resolvedName, controllerUrl, usedExternalDeploy);
486
489
  }
487
490
  }
488
491