@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.
- package/.cursor/process/README.md +9 -0
- package/lib/api/dev.api.js +1 -0
- package/lib/api/host-ops.api.js +69 -0
- package/lib/cli/setup-dev-path-commands.js +24 -0
- package/lib/cli/setup-dev.js +108 -14
- package/lib/commands/dev-cli-handlers.js +64 -3
- package/lib/commands/dev-host-ops.js +239 -0
- package/lib/schema/external-system.schema.json +114 -2
- package/package.json +1 -1
|
@@ -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/).
|
package/lib/api/dev.api.js
CHANGED
|
@@ -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);
|
package/lib/cli/setup-dev.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
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
|
-
|
|
347
|
-
await handleDevDown(options);
|
|
413
|
+
await handlePlatformInstallCli(developerId, options);
|
|
348
414
|
} catch (error) {
|
|
349
|
-
handleCommandError(error, 'dev
|
|
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
|
-
|
|
236
|
-
|
|
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
|
|
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
|
},
|