@aifabrix/builder 2.49.1 → 2.49.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.
@@ -0,0 +1,9 @@
1
+ # Process documentation
2
+
3
+ Canonical process descriptions for aifabrix-builder automation and release workflows.
4
+
5
+ | Process | Description |
6
+ | ------- | ----------- |
7
+ | [deployment.md](./deployment.md) | NPM release: preflight, release branch CI, PR to `main`, publish and GitHub release |
8
+
9
+ Command specs (agent-executable) live under [`.cursor/commands/`](../commands/).
@@ -391,6 +391,7 @@ async function deleteSecret(serverUrl, clientCertPem, key, serverCaPem, secretsE
391
391
  }
392
392
 
393
393
  module.exports = {
394
+ request,
394
395
  issueCert,
395
396
  getHealth,
396
397
  getSettings,
@@ -0,0 +1,69 @@
1
+ /**
2
+ * @fileoverview Builder Server host-ops API
3
+ * @author AI Fabrix Team
4
+ * @version 1.0.0
5
+ */
6
+
7
+ const devApi = require('./dev.api');
8
+
9
+ /**
10
+ * Create host-ops job. POST /api/dev/host-ops/jobs
11
+ * @param {string} serverUrl
12
+ * @param {string} clientCertPem
13
+ * @param {Object} body - { type, params }
14
+ * @param {string} [serverCaPem]
15
+ * @returns {Promise<{ jobId: string, status: string }>}
16
+ */
17
+ async function createHostOpsJob(serverUrl, clientCertPem, body, serverCaPem) {
18
+ const base = devApi.normalizeBaseUrl(serverUrl);
19
+ return devApi.request(devApi.buildUrl(base, '/api/dev/host-ops/jobs'), {
20
+ method: 'POST',
21
+ headers: { 'X-Client-Cert': devApi.encodeCertForHeader(clientCertPem) },
22
+ body,
23
+ serverCaPem
24
+ });
25
+ }
26
+
27
+ /**
28
+ * Get host-ops job. GET /api/dev/host-ops/jobs/:jobId
29
+ * @param {string} serverUrl
30
+ * @param {string} clientCertPem
31
+ * @param {string} jobId
32
+ * @param {string} [serverCaPem]
33
+ * @returns {Promise<Object>}
34
+ */
35
+ async function getHostOpsJob(serverUrl, clientCertPem, jobId, serverCaPem) {
36
+ const base = devApi.normalizeBaseUrl(serverUrl);
37
+ return devApi.request(
38
+ devApi.buildUrl(base, `/api/dev/host-ops/jobs/${encodeURIComponent(jobId)}`),
39
+ {
40
+ method: 'GET',
41
+ headers: { 'X-Client-Cert': devApi.encodeCertForHeader(clientCertPem) },
42
+ serverCaPem
43
+ }
44
+ );
45
+ }
46
+
47
+ /**
48
+ * Queue preflight (check-only job). POST /api/dev/host-ops/preflight
49
+ * @param {string} serverUrl
50
+ * @param {string} clientCertPem
51
+ * @param {Object} body - { type, params }
52
+ * @param {string} [serverCaPem]
53
+ * @returns {Promise<{ jobId: string, status: string }>}
54
+ */
55
+ async function preflightHostOps(serverUrl, clientCertPem, body, serverCaPem) {
56
+ const base = devApi.normalizeBaseUrl(serverUrl);
57
+ return devApi.request(devApi.buildUrl(base, '/api/dev/host-ops/preflight'), {
58
+ method: 'POST',
59
+ headers: { 'X-Client-Cert': devApi.encodeCertForHeader(clientCertPem) },
60
+ body,
61
+ serverCaPem
62
+ });
63
+ }
64
+
65
+ module.exports = {
66
+ createHostOpsJob,
67
+ getHostOpsJob,
68
+ preflightHostOps
69
+ };
@@ -44,6 +44,29 @@ async function runShellEnvRegistration() {
44
44
  }
45
45
  }
46
46
 
47
+ function addSetEnvConfigCommand(dev) {
48
+ dev
49
+ .command('set-env-config <path>')
50
+ .description('Set or clear aifabrix-env-config path in ~/.aifabrix/config.yaml')
51
+ .action(async(envConfigPath) => {
52
+ try {
53
+ const trimmed = (envConfigPath || '').trim();
54
+ await config.setAifabrixEnvConfigPath(trimmed);
55
+ logger.log(
56
+ trimmed === ''
57
+ ? formatSuccessLine('Env config path cleared')
58
+ : formatSuccessLine(`Env config path set to ${trimmed}`)
59
+ );
60
+ const devId = await config.getDeveloperId();
61
+ const { displayDevConfig } = require('../commands/dev-show-display');
62
+ await displayDevConfig(devId);
63
+ } catch (error) {
64
+ handleCommandError(error, 'dev set-env-config');
65
+ process.exit(1);
66
+ }
67
+ });
68
+ }
69
+
47
70
  function addSetHomeCommand(dev) {
48
71
  dev
49
72
  .command('set-home <path>')
@@ -162,6 +185,7 @@ function addSetFormatCommand(dev, handleSetFormat) {
162
185
  * @returns {void}
163
186
  */
164
187
  function setupDevPathAndFormatCommands(dev, handleSetFormat) {
188
+ addSetEnvConfigCommand(dev);
165
189
  addSetHomeCommand(dev);
166
190
  addSetWorkCommand(dev);
167
191
  addPrintHomeWorkCommands(dev);
@@ -21,6 +21,11 @@ const {
21
21
  handleDevPin,
22
22
  handleDevDelete
23
23
  } = require('../commands/dev-cli-handlers');
24
+ const {
25
+ handlePlatformInstallCli,
26
+ handlePlatformUpdate,
27
+ handlePlatformInstall
28
+ } = require('../commands/dev-host-ops');
24
29
 
25
30
  /** Appended to `dev update --help` (examples + partial-update behaviour). */
26
31
  const DEV_UPDATE_HELP_AFTER = `
@@ -49,7 +54,7 @@ const DEV_GROUP_HELP_AFTER = `
49
54
  Categories:
50
55
  Local config show, set-id, set-scoped-resources, set-env-config, set-home, set-work, set-format, print-home, print-work
51
56
  Onboarding init (PIN onboarding), refresh (sync settings / renew cert)
52
- Remote admin list, add, update, pin, delete (need remote-server + client cert in ~/.aifabrix)
57
+ Remote admin list, add, update, pin, delete, platform-install, platform-update, platform-install-cli (need remote-server + client cert in ~/.aifabrix)
53
58
  Sync / stop down (Mutagen; optional --apps for containers)
54
59
 
55
60
  Use: aifabrix dev <command> --help
@@ -85,10 +90,31 @@ when their config already has remote-server + developer-id.
85
90
 
86
91
  /** Appended to `dev delete --help`. */
87
92
  const DEV_DELETE_HELP_AFTER = `
88
- Example:
93
+ Examples:
89
94
  $ aifabrix dev delete 02
95
+ $ aifabrix dev delete 02 -y
96
+ $ aifabrix dev delete 02 --db-only
97
+
98
+ Default removes the developer from the host (OS user, containers) and server database. Use --db-only for database-only removal.
90
99
  `;
91
100
 
101
+ /** Shared host-ops flags for platform commands and dev add provision. */
102
+ function addHostOpsSharedOptions(cmd) {
103
+ return cmd
104
+ .option('--check', 'Preflight only (no changes)')
105
+ .option('-y, --yes', 'Non-interactive (skip prompts)')
106
+ .option('--wait', 'Poll until job completes', true)
107
+ .option('--no-wait', 'Return after job is queued')
108
+ .option('--json', 'JSON output (includes job result when --wait)')
109
+ .option('--include <ids>', 'Comma-separated developer ids (batch)')
110
+ .option('--exclude <ids>', 'Comma-separated developer ids to skip')
111
+ .option('--builder-version <ver>', 'Builder CLI / platform bundle version')
112
+ .option('--admin-email <email>', 'Platform admin email (install / provision)')
113
+ .option('--admin-password <password>', 'Platform admin password (never logged)')
114
+ .option('--platform <mode>', 'Platform mode (e.g. single-traefik)')
115
+ .option('--installation <profile>', 'Installation profile (e.g. dev)');
116
+ }
117
+
92
118
  /** Appended to `dev down --help`. */
93
119
  const DEV_DOWN_HELP_AFTER = `
94
120
  Examples:
@@ -276,6 +302,15 @@ function setupDevListAddCommands(dev) {
276
302
  .requiredOption('--name <name>', 'Display name')
277
303
  .requiredOption('--email <email>', 'Email address')
278
304
  .option('--groups <items>', 'Comma-separated roles: admin, secret-manager, secret-reader, developer, docker', 'developer')
305
+ .option('--no-provision', 'Create user in database only (skip host provision job)')
306
+ .option('--builder-version <ver>', 'Builder CLI version for provision job')
307
+ .option('--admin-email <email>', 'Platform admin email for provision job')
308
+ .option('--admin-password <password>', 'Platform admin password for provision job')
309
+ .option('--platform <mode>', 'Platform mode for provision job')
310
+ .option('--installation <profile>', 'Installation profile for provision job')
311
+ .option('--skip-cert', 'Skip certificate step in provision job')
312
+ .option('--skip-platform', 'Skip platform install in provision job')
313
+ .option('--skip-cli', 'Skip Builder CLI install in provision job')
279
314
  .addHelpText('after', DEV_ADD_HELP_AFTER)
280
315
  .action(async(options) => {
281
316
  try {
@@ -287,6 +322,27 @@ function setupDevListAddCommands(dev) {
287
322
  });
288
323
  }
289
324
 
325
+ /**
326
+ * Register dev down command.
327
+ * @param {Command} dev - dev subcommand group
328
+ */
329
+ function setupDevDownCommand(dev) {
330
+ dev
331
+ .command('down')
332
+ .description('Stop Mutagen sync; --apps also stops app containers')
333
+ .addHelpText('after', DEV_DOWN_HELP_AFTER)
334
+ .option('--apps', 'Also stop running app containers for this developer')
335
+ .action(async(options) => {
336
+ try {
337
+ const { handleDevDown } = require('../commands/dev-down');
338
+ await handleDevDown(options);
339
+ } catch (error) {
340
+ handleCommandError(error, 'dev down');
341
+ process.exit(1);
342
+ }
343
+ });
344
+ }
345
+
290
346
  /**
291
347
  * Register dev update/pin/delete commands (remote only).
292
348
  * @param {Command} dev - dev subcommand group
@@ -325,31 +381,68 @@ function setupDevUpdatePinDeleteCommands(dev) {
325
381
 
326
382
  dev
327
383
  .command('delete <developerId>')
328
- .description('Remove developer from server (admin)')
384
+ .description('Remove developer from host and server (admin)')
385
+ .option('--db-only', 'Remove from server database only (legacy behaviour)')
386
+ .option('-y, --yes', 'Skip confirmation prompt')
329
387
  .addHelpText('after', DEV_DELETE_HELP_AFTER)
330
- .action(async(developerId) => {
388
+ .action(async(developerId, options) => {
331
389
  try {
332
- await handleDevDelete(developerId);
390
+ await handleDevDelete(developerId, options);
333
391
  } catch (error) {
334
392
  handleCommandError(error, 'dev delete');
335
393
  process.exit(1);
336
394
  }
337
395
  });
338
396
 
339
- dev
340
- .command('down')
341
- .description('Stop Mutagen sync; --apps also stops app containers')
342
- .addHelpText('after', DEV_DOWN_HELP_AFTER)
343
- .option('--apps', 'Also stop running app containers for this developer')
344
- .action(async(options) => {
397
+ setupDevDownCommand(dev);
398
+ }
399
+
400
+ /**
401
+ * Register platform lifecycle commands (host-ops).
402
+ * @param {Command} dev - dev subcommand group
403
+ */
404
+ function setupDevPlatformCommands(dev) {
405
+ addHostOpsSharedOptions(
406
+ dev
407
+ .command('platform-install-cli [developerId]')
408
+ .description('Install or upgrade Builder CLI on dev host(s) (admin)')
409
+ )
410
+ .option('--force', 'Reinstall even when CLI is present')
411
+ .action(async(developerId, options) => {
345
412
  try {
346
- const { handleDevDown } = require('../commands/dev-down');
347
- await handleDevDown(options);
413
+ await handlePlatformInstallCli(developerId, options);
348
414
  } catch (error) {
349
- handleCommandError(error, 'dev down');
415
+ handleCommandError(error, 'dev platform-install-cli');
350
416
  process.exit(1);
351
417
  }
352
418
  });
419
+
420
+ addHostOpsSharedOptions(
421
+ dev
422
+ .command('platform-update [developerId]')
423
+ .description('Refresh dev platform images (admin)')
424
+ ).action(async(developerId, options) => {
425
+ try {
426
+ await handlePlatformUpdate(developerId, options);
427
+ } catch (error) {
428
+ handleCommandError(error, 'dev platform-update');
429
+ process.exit(1);
430
+ }
431
+ });
432
+
433
+ addHostOpsSharedOptions(
434
+ dev
435
+ .command('platform-install <developerId>')
436
+ .description('Install dev platform for one developer (admin)')
437
+ .option('--force', 'Remove existing platform then reinstall')
438
+ ).action(async(developerId, options) => {
439
+ try {
440
+ await handlePlatformInstall(developerId, options);
441
+ } catch (error) {
442
+ handleCommandError(error, 'dev platform-install');
443
+ process.exit(1);
444
+ }
445
+ });
353
446
  }
354
447
 
355
448
  /**
@@ -359,6 +452,7 @@ function setupDevUpdatePinDeleteCommands(dev) {
359
452
  function setupDevUserCommands(dev) {
360
453
  setupDevListAddCommands(dev);
361
454
  setupDevUpdatePinDeleteCommands(dev);
455
+ setupDevPlatformCommands(dev);
362
456
  }
363
457
 
364
458
  /**
@@ -16,6 +16,7 @@ const {
16
16
  validateDevGroups,
17
17
  augmentDevUserGroupsServerError
18
18
  } = require('../utils/dev-user-groups');
19
+ const { handleProvisionDevUser, handleRemoveDevUser } = require('./dev-host-ops');
19
20
 
20
21
  const REMOTE_NOT_CONFIGURED_MSG = 'Remote server is not configured. Set remote-server and run "aifabrix dev init" first.';
21
22
 
@@ -26,6 +27,19 @@ const CERT_WIDTH = 10;
26
27
  const UNTIL_WIDTH = 22; // 2026-03-20T17:31:14
27
28
  const TABLE_SEPARATOR_LENGTH = 130;
28
29
 
30
+ /**
31
+ * Normalize numeric developer id for comparison (e.g. 3 → 03).
32
+ * @param {string} id - Developer ID
33
+ * @returns {string}
34
+ */
35
+ function normalizeDeveloperIdForCompare(id) {
36
+ const trimmed = String(id ?? '').trim();
37
+ if (!/^\d+$/.test(trimmed)) {
38
+ return trimmed;
39
+ }
40
+ return String(parseInt(trimmed, 10)).padStart(2, '0');
41
+ }
42
+
29
43
  /**
30
44
  * Format certificate validity end for display (e.g. 2026-03-20T17:31:14.000Z -> 2026-03-20T17:31:14).
31
45
  * @param {string} iso - ISO 8601 date string
@@ -91,6 +105,10 @@ async function handleDevAdd(options) {
91
105
  groups
92
106
  }, auth.serverCaPem || undefined);
93
107
  logger.log(formatSuccessLine(`Developer ${user.id} created. Use "aifabrix dev pin ${user.id}" to create a PIN for onboarding.`));
108
+ const noProvision = options.noProvision || options['no-provision'];
109
+ if (!noProvision) {
110
+ await handleProvisionDevUser(user.id, options);
111
+ }
94
112
  } catch (err) {
95
113
  augmentDevUserGroupsServerError(err, groups);
96
114
  throw err;
@@ -222,18 +240,61 @@ async function handleDevPin(developerId, options = {}) {
222
240
  });
223
241
  }
224
242
 
243
+ /**
244
+ * @param {string} question
245
+ * @returns {Promise<boolean>}
246
+ */
247
+ async function promptYesNo(question) {
248
+ if (!process.stdin.isTTY) {
249
+ return false;
250
+ }
251
+ const readline = require('readline');
252
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
253
+ return new Promise((resolve) => {
254
+ rl.question(`${question} [y/N] `, (answer) => {
255
+ rl.close();
256
+ resolve(/^y(es)?$/i.test(String(answer || '').trim()));
257
+ });
258
+ });
259
+ }
260
+
225
261
  /**
226
262
  * Handle dev delete – remove developer (remote only).
263
+ * Default: full host removal via host-ops. Use --db-only for API-only delete.
227
264
  * @param {string} developerId - Developer ID
265
+ * @param {Object} [options] - Commander options
228
266
  * @returns {Promise<void>}
229
267
  */
230
- async function handleDevDelete(developerId) {
268
+ async function handleDevDelete(developerId, options = {}) {
231
269
  const auth = await getRemoteDevAuth();
232
270
  if (!auth) throw new Error(REMOTE_NOT_CONFIGURED_MSG);
233
271
  const id = developerId;
234
272
  if (!id) throw new Error('Developer ID is required (positional argument or --developer-id).');
235
- await devApi.deleteUser(auth.serverUrl, auth.clientCertPem, id, auth.serverCaPem || undefined);
236
- logger.log(formatSuccessLine(`Developer ${id} removed.`));
273
+ const actorId = await config.getDeveloperId();
274
+ if (
275
+ actorId &&
276
+ normalizeDeveloperIdForCompare(actorId) === normalizeDeveloperIdForCompare(id)
277
+ ) {
278
+ throw new Error('You cannot delete your own developer account.');
279
+ }
280
+ const dbOnly = options.dbOnly || options['db-only'];
281
+ if (dbOnly) {
282
+ await devApi.deleteUser(auth.serverUrl, auth.clientCertPem, id, auth.serverCaPem || undefined);
283
+ logger.log(formatSuccessLine(`Developer ${id} removed from server database.`));
284
+ return;
285
+ }
286
+ const assumeYes = options.yes || options.y;
287
+ if (!assumeYes) {
288
+ const ok = await promptYesNo(
289
+ `Remove developer ${id} from host and server (containers, OS user, database)?`
290
+ );
291
+ if (!ok) {
292
+ logger.log(chalk.gray('Cancelled.'));
293
+ return;
294
+ }
295
+ }
296
+ await handleRemoveDevUser(id, { ...options, developerId: id });
297
+ logger.log(formatSuccessLine(`Developer ${id} removal queued and completed.`));
237
298
  }
238
299
 
239
300
  module.exports = {
@@ -0,0 +1,239 @@
1
+ /**
2
+ * @fileoverview Dev host-ops CLI handlers (platform-install, platform-update, etc.)
3
+ * @author AI Fabrix Team
4
+ * @version 1.0.0
5
+ */
6
+
7
+ const readline = require('readline');
8
+ const chalk = require('chalk');
9
+ const logger = require('../utils/logger');
10
+ const { formatSuccessLine } = require('../utils/cli-test-layout-chalk');
11
+ const { getRemoteDevAuth } = require('../utils/remote-dev-auth');
12
+ const hostOpsApi = require('../api/host-ops.api');
13
+
14
+ const REMOTE_NOT_CONFIGURED_MSG =
15
+ 'Remote server is not configured. Set remote-server and run "aifabrix dev init" first.';
16
+
17
+ const POLL_INTERVAL_MS = 3000;
18
+ const POLL_TIMEOUT_MS = 3 * 60 * 60 * 1000;
19
+
20
+ function sleep(ms) {
21
+ return new Promise((resolve) => setTimeout(resolve, ms));
22
+ }
23
+
24
+ /**
25
+ * Build job params from Commander options.
26
+ * @param {Object} options
27
+ * @param {string} [developerId]
28
+ * @returns {Object}
29
+ */
30
+ function buildJobParams(options, developerId) {
31
+ const params = {};
32
+ const id = developerId || options.developerId || options['developer-id'];
33
+ if (id) params.developerId = String(id).trim();
34
+ if (options.include) params.include = options.include;
35
+ if (options.exclude) params.exclude = options.exclude;
36
+ if (options.builderVersion || options['builder-version']) {
37
+ params.builderVersion = options.builderVersion || options['builder-version'];
38
+ }
39
+ if (options.adminEmail || options['admin-email']) {
40
+ params.adminEmail = options.adminEmail || options['admin-email'];
41
+ }
42
+ if (options.adminPassword || options['admin-password']) {
43
+ params.adminPassword = options.adminPassword || options['admin-password'];
44
+ }
45
+ if (options.platform) params.platform = options.platform;
46
+ if (options.installation) params.installation = options.installation;
47
+ if (options.skipCert || options['skip-cert']) params.skipCert = true;
48
+ if (options.skipPlatform || options['skip-platform']) params.skipPlatform = true;
49
+ if (options.skipCli || options['skip-cli']) params.skipCli = true;
50
+ if (options.check) params.check = true;
51
+ if (options.failOnBlocked) params.failOnBlocked = true;
52
+ if (options.timeoutMinutes || options['timeout-minutes']) {
53
+ params.timeoutMinutes = parseInt(options.timeoutMinutes || options['timeout-minutes'], 10);
54
+ }
55
+ return params;
56
+ }
57
+
58
+ /**
59
+ * Poll host-ops job until terminal state.
60
+ * @param {Object} auth
61
+ * @param {string} jobId
62
+ * @param {Object} [opts]
63
+ * @param {boolean} [opts.wait=true]
64
+ * @param {boolean} [opts.json=false]
65
+ * @returns {Promise<Object>}
66
+ */
67
+ async function waitForHostOpsJob(auth, jobId, opts = {}) {
68
+ const wait = opts.wait !== false;
69
+ const json = Boolean(opts.json);
70
+ if (!wait) {
71
+ return { jobId, status: 'pending' };
72
+ }
73
+ const started = Date.now();
74
+ while (Date.now() - started < POLL_TIMEOUT_MS) {
75
+ const job = await hostOpsApi.getHostOpsJob(
76
+ auth.serverUrl,
77
+ auth.clientCertPem,
78
+ jobId,
79
+ auth.serverCaPem || undefined
80
+ );
81
+ if (!json) {
82
+ logger.log(chalk.gray(` host-ops job ${jobId}: ${job.status}`));
83
+ }
84
+ if (job.status === 'completed' || job.status === 'failed') {
85
+ if (job.status === 'failed') {
86
+ throw new Error(job.error || `Host-ops job ${jobId} failed`);
87
+ }
88
+ return job;
89
+ }
90
+ await sleep(POLL_INTERVAL_MS);
91
+ }
92
+ throw new Error(`Timed out waiting for host-ops job ${jobId}`);
93
+ }
94
+
95
+ /**
96
+ * @param {string} question
97
+ * @returns {Promise<boolean>}
98
+ */
99
+ function promptYesNo(question) {
100
+ if (!process.stdin.isTTY) {
101
+ return Promise.resolve(false);
102
+ }
103
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
104
+ return new Promise((resolve) => {
105
+ rl.question(`${question} [y/N] `, (answer) => {
106
+ rl.close();
107
+ resolve(/^y(es)?$/i.test(String(answer || '').trim()));
108
+ });
109
+ });
110
+ }
111
+
112
+ function platformAlreadyInstalled(outcomes) {
113
+ if (!Array.isArray(outcomes)) return false;
114
+ return outcomes.some(
115
+ (row) =>
116
+ row &&
117
+ (row.overall === 'SKIP' || row.status === 'SKIP') &&
118
+ String(row.detail || '').toLowerCase().includes('installed')
119
+ );
120
+ }
121
+
122
+ /**
123
+ * Run a host-ops job and optionally wait.
124
+ * @param {string} type
125
+ * @param {Object} params
126
+ * @param {Object} options - Commander options (yes, wait, json)
127
+ * @returns {Promise<Object>}
128
+ */
129
+ async function runHostOpsJob(type, params, options = {}) {
130
+ const auth = await getRemoteDevAuth();
131
+ if (!auth) throw new Error(REMOTE_NOT_CONFIGURED_MSG);
132
+ const { jobId } = await hostOpsApi.createHostOpsJob(
133
+ auth.serverUrl,
134
+ auth.clientCertPem,
135
+ { type, params },
136
+ auth.serverCaPem || undefined
137
+ );
138
+ if (options.json) {
139
+ const job = await waitForHostOpsJob(auth, jobId, { wait: options.wait !== false, json: true });
140
+ logger.log(JSON.stringify(job, null, 2));
141
+ return job;
142
+ }
143
+ const job = await waitForHostOpsJob(auth, jobId, { wait: options.wait !== false, json: false });
144
+ logger.log(formatSuccessLine(`Host-ops job ${jobId} completed (${type}).`));
145
+ return job;
146
+ }
147
+
148
+ async function handlePlatformInstallCli(developerId, options) {
149
+ const params = buildJobParams(options, developerId);
150
+ if (options.check) {
151
+ params.check = true;
152
+ } else if (!params.builderVersion) {
153
+ throw new Error('--builder-version is required unless --check is set');
154
+ }
155
+ await runHostOpsJob('install-builder-cli', params, options);
156
+ }
157
+
158
+ async function handlePlatformUpdate(developerId, options) {
159
+ const params = buildJobParams(options, developerId);
160
+ if (options.check) params.check = true;
161
+ await runHostOpsJob('update-dev-platform', params, options);
162
+ }
163
+
164
+ async function handlePlatformInstall(developerId, options) {
165
+ if (!developerId) {
166
+ throw new Error('Developer ID is required (positional argument).');
167
+ }
168
+ const params = buildJobParams(options, developerId);
169
+
170
+ if (options.check) {
171
+ params.check = true;
172
+ const auth = await getRemoteDevAuth();
173
+ if (!auth) throw new Error(REMOTE_NOT_CONFIGURED_MSG);
174
+ const { jobId } = await hostOpsApi.preflightHostOps(
175
+ auth.serverUrl,
176
+ auth.clientCertPem,
177
+ { type: 'install-dev-platform', params },
178
+ auth.serverCaPem || undefined
179
+ );
180
+ const job = await waitForHostOpsJob(auth, jobId, options);
181
+ if (options.json) {
182
+ logger.log(JSON.stringify(job, null, 2));
183
+ } else if (Array.isArray(job.outcomes)) {
184
+ job.outcomes.forEach((row) => {
185
+ logger.log(` ${row.linuxUser || developerId}: ${row.overall || row.status}${row.detail ? ` — ${row.detail}` : ''}`);
186
+ });
187
+ }
188
+ return job;
189
+ }
190
+
191
+ if (!options.force && !options.yes) {
192
+ const auth = await getRemoteDevAuth();
193
+ if (!auth) throw new Error(REMOTE_NOT_CONFIGURED_MSG);
194
+ const { jobId } = await hostOpsApi.preflightHostOps(
195
+ auth.serverUrl,
196
+ auth.clientCertPem,
197
+ { type: 'install-dev-platform', params: { ...params, developerId } },
198
+ auth.serverCaPem || undefined
199
+ );
200
+ const preflight = await waitForHostOpsJob(auth, jobId, { ...options, json: false });
201
+ if (platformAlreadyInstalled(preflight.outcomes)) {
202
+ const ok = await promptYesNo(
203
+ `Platform already installed for dev${String(developerId).padStart(2, '0')}. Re-install?`
204
+ );
205
+ if (!ok) {
206
+ logger.log(chalk.gray('Cancelled.'));
207
+ return preflight;
208
+ }
209
+ await runHostOpsJob('reinstall-dev-platform', params, options);
210
+ return;
211
+ }
212
+ }
213
+
214
+ const jobType = options.force ? 'reinstall-dev-platform' : 'install-dev-platform';
215
+ await runHostOpsJob(jobType, params, options);
216
+ }
217
+
218
+ async function handleProvisionDevUser(developerId, options) {
219
+ const params = buildJobParams(options, developerId);
220
+ await runHostOpsJob('provision-dev-user', params, options);
221
+ }
222
+
223
+ async function handleRemoveDevUser(developerId, options) {
224
+ const params = buildJobParams(options, developerId);
225
+ if (!params.developerId) {
226
+ throw new Error('Developer ID is required.');
227
+ }
228
+ await runHostOpsJob('remove-dev-user', params, options);
229
+ }
230
+
231
+ module.exports = {
232
+ buildJobParams,
233
+ waitForHostOpsJob,
234
+ handlePlatformInstallCli,
235
+ handlePlatformUpdate,
236
+ handlePlatformInstall,
237
+ handleProvisionDevUser,
238
+ handleRemoveDevUser
239
+ };
@@ -212,7 +212,8 @@
212
212
  "compileCom",
213
213
  "generateEnterpriseDatasources",
214
214
  "generateRestContracts",
215
- "generateMcpContracts"
215
+ "generateMcpContracts",
216
+ "runDigitalWorkerTask"
216
217
  ],
217
218
  "description": "Platform task type executed when the subscription fires."
218
219
  },
@@ -221,6 +222,104 @@
221
222
  "minimum": 1,
222
223
  "default": 1,
223
224
  "description": "Task definition version snapshot for audit."
225
+ },
226
+ "digitalWorkerKey": {
227
+ "type": "string",
228
+ "minLength": 1,
229
+ "description": "Target digital worker system key (runDigitalWorkerTask)."
230
+ },
231
+ "taskType": {
232
+ "type": "string",
233
+ "minLength": 1,
234
+ "description": "Worker task type label (runDigitalWorkerTask)."
235
+ },
236
+ "objectiveTemplate": {
237
+ "type": "string",
238
+ "description": "Objective template with {{recordId}} placeholders."
239
+ },
240
+ "requiresApproval": {
241
+ "type": "boolean",
242
+ "default": false
243
+ },
244
+ "owner": {
245
+ "$ref": "#/$defs/subscriptionTaskOwner"
246
+ }
247
+ },
248
+ "additionalProperties": false
249
+ },
250
+ "subscriptionCondition": {
251
+ "type": "object",
252
+ "required": [
253
+ "field",
254
+ "operator"
255
+ ],
256
+ "properties": {
257
+ "field": {
258
+ "type": "string",
259
+ "minLength": 1,
260
+ "description": "Path against afterSummary (default) or before.* prefix."
261
+ },
262
+ "operator": {
263
+ "type": "string",
264
+ "enum": [
265
+ "equals",
266
+ "notEquals",
267
+ "in",
268
+ "notIn",
269
+ "exists",
270
+ "missing",
271
+ "changed",
272
+ "greaterThan",
273
+ "lessThan"
274
+ ]
275
+ },
276
+ "value": {}
277
+ },
278
+ "additionalProperties": false
279
+ },
280
+ "subscriptionTriggerPolicy": {
281
+ "type": "object",
282
+ "required": [
283
+ "mode"
284
+ ],
285
+ "properties": {
286
+ "mode": {
287
+ "type": "string",
288
+ "enum": [
289
+ "immediate",
290
+ "debounce",
291
+ "scheduled"
292
+ ]
293
+ },
294
+ "windowSeconds": {
295
+ "type": "integer",
296
+ "minimum": 1,
297
+ "description": "Required when mode is debounce."
298
+ }
299
+ },
300
+ "additionalProperties": false
301
+ },
302
+ "subscriptionTaskOwner": {
303
+ "type": "object",
304
+ "required": [
305
+ "mode"
306
+ ],
307
+ "properties": {
308
+ "mode": {
309
+ "type": "string",
310
+ "enum": [
311
+ "explicit",
312
+ "recordField"
313
+ ]
314
+ },
315
+ "userId": {
316
+ "type": "string",
317
+ "minLength": 1
318
+ },
319
+ "field": {
320
+ "type": "string",
321
+ "minLength": 1,
322
+ "description": "Record summary field for recordField mode."
224
323
  }
225
324
  },
226
325
  "additionalProperties": false
@@ -251,11 +350,24 @@
251
350
  "event": {
252
351
  "type": "string",
253
352
  "minLength": 1,
254
- "description": "V1 event namespace (platform.* or schedule.*)."
353
+ "description": "V1 event namespace (platform.*, schedule.*, or record.*)."
255
354
  },
256
355
  "filter": {
257
356
  "$ref": "#/$defs/subscriptionFilter"
258
357
  },
358
+ "conditions": {
359
+ "type": "array",
360
+ "items": {
361
+ "$ref": "#/$defs/subscriptionCondition"
362
+ }
363
+ },
364
+ "triggerPolicy": {
365
+ "$ref": "#/$defs/subscriptionTriggerPolicy"
366
+ },
367
+ "deduplicationKeyTemplate": {
368
+ "type": "string",
369
+ "description": "Default: {{subscriptionId}}:{{resourceType}}:{{recordId}}:{{task.taskType}}"
370
+ },
259
371
  "task": {
260
372
  "$ref": "#/$defs/subscriptionTask"
261
373
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aifabrix/builder",
3
- "version": "2.49.1",
3
+ "version": "2.49.2",
4
4
  "description": "AI Fabrix Local Fabric & Deployment SDK",
5
5
  "main": "lib/index.js",
6
6
  "bin": {