@aifabrix/builder 2.37.9 → 2.39.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.
- package/.cursor/rules/project-rules.mdc +3 -0
- package/README.md +19 -0
- package/integration/hubspot/hubspot-deploy.json +1 -5
- package/integration/hubspot/hubspot-system.json +0 -3
- package/lib/api/applications.api.js +29 -1
- package/lib/api/auth.api.js +14 -0
- package/lib/api/credentials.api.js +34 -0
- package/lib/api/datasources-core.api.js +16 -1
- package/lib/api/datasources-extended.api.js +18 -1
- package/lib/api/deployments.api.js +32 -0
- package/lib/api/environments.api.js +11 -0
- package/lib/api/external-systems.api.js +16 -1
- package/lib/api/pipeline.api.js +12 -4
- package/lib/api/service-users.api.js +41 -0
- package/lib/api/types/applications.types.js +1 -1
- package/lib/api/types/deployments.types.js +1 -1
- package/lib/api/types/pipeline.types.js +1 -1
- package/lib/api/types/service-users.types.js +24 -0
- package/lib/api/wizard.api.js +40 -1
- package/lib/app/deploy.js +86 -21
- package/lib/app/rotate-secret.js +3 -1
- package/lib/app/run-helpers.js +35 -2
- package/lib/app/show-display.js +30 -11
- package/lib/app/show.js +34 -8
- package/lib/cli/index.js +4 -0
- package/lib/cli/setup-app.js +40 -0
- package/lib/cli/setup-credential-deployment.js +72 -0
- package/lib/cli/setup-infra.js +3 -3
- package/lib/cli/setup-service-user.js +52 -0
- package/lib/cli/setup-utility.js +1 -25
- package/lib/commands/app-down.js +80 -0
- package/lib/commands/app-logs.js +146 -0
- package/lib/commands/app.js +24 -1
- package/lib/commands/credential-list.js +104 -0
- package/lib/commands/deployment-list.js +184 -0
- package/lib/commands/service-user.js +193 -0
- package/lib/commands/up-common.js +74 -5
- package/lib/commands/up-dataplane.js +13 -7
- package/lib/commands/up-miso.js +17 -24
- package/lib/core/templates.js +2 -2
- package/lib/external-system/deploy.js +79 -15
- package/lib/generator/builders.js +8 -27
- package/lib/generator/external-controller-manifest.js +5 -4
- package/lib/generator/index.js +16 -14
- package/lib/generator/split.js +1 -0
- package/lib/generator/wizard.js +4 -1
- package/lib/schema/application-schema.json +6 -14
- package/lib/schema/deployment-rules.yaml +121 -0
- package/lib/schema/external-system.schema.json +0 -16
- package/lib/utils/app-register-config.js +10 -12
- package/lib/utils/app-run-containers.js +2 -1
- package/lib/utils/compose-generator.js +2 -1
- package/lib/utils/deployment-errors.js +10 -0
- package/lib/utils/environment-checker.js +25 -6
- package/lib/utils/help-builder.js +0 -1
- package/lib/utils/image-version.js +209 -0
- package/lib/utils/schema-loader.js +1 -1
- package/lib/utils/variable-transformer.js +7 -33
- package/lib/validation/external-manifest-validator.js +1 -1
- package/package.json +1 -1
- package/templates/applications/README.md.hbs +1 -3
- package/templates/applications/dataplane/Dockerfile +2 -2
- package/templates/applications/dataplane/README.md +20 -6
- package/templates/applications/dataplane/env.template +31 -2
- package/templates/applications/dataplane/rbac.yaml +1 -1
- package/templates/applications/dataplane/variables.yaml +7 -4
- package/templates/applications/keycloak/Dockerfile +3 -3
- package/templates/applications/keycloak/README.md +14 -4
- package/templates/applications/keycloak/env.template +17 -2
- package/templates/applications/keycloak/variables.yaml +2 -1
- package/templates/applications/miso-controller/README.md +1 -3
- package/templates/applications/miso-controller/env.template +85 -25
- package/templates/applications/miso-controller/rbac.yaml +15 -0
- package/templates/applications/miso-controller/variables.yaml +24 -23
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* App logs command – show container env (masked) and docker logs for an app
|
|
3
|
+
*
|
|
4
|
+
* @fileoverview App logs command implementation
|
|
5
|
+
* @author AI Fabrix Team
|
|
6
|
+
* @version 2.0.0
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const chalk = require('chalk');
|
|
10
|
+
const { exec, spawn } = require('child_process');
|
|
11
|
+
const { promisify } = require('util');
|
|
12
|
+
const logger = require('../utils/logger');
|
|
13
|
+
const config = require('../core/config');
|
|
14
|
+
const containerHelpers = require('../utils/app-run-containers');
|
|
15
|
+
const { validateAppName } = require('../app/push');
|
|
16
|
+
|
|
17
|
+
const execAsync = promisify(exec);
|
|
18
|
+
|
|
19
|
+
/** Default number of log lines */
|
|
20
|
+
const DEFAULT_TAIL_LINES = 100;
|
|
21
|
+
|
|
22
|
+
/** Env key patterns that indicate a secret (mask value) */
|
|
23
|
+
const SECRET_KEY_PATTERN = /password|secret|token|credential|api[_-]?key/i;
|
|
24
|
+
|
|
25
|
+
/** Prefixes to strip before checking key (avoids masking KEYCLOAK_SERVER_URL etc.) */
|
|
26
|
+
const KEY_PREFIXES_TO_STRIP = /^KEYCLOAK_|^KEY_VAULT_/;
|
|
27
|
+
|
|
28
|
+
/** URL with embedded credentials: scheme://user:password@host → scheme://user:***@host */
|
|
29
|
+
const URL_CREDENTIAL_PATTERN = /(\w+:\/\/)([^:@]*):([^@]+)@/g;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Masks a single env line if the key looks like a secret or value contains URL credentials
|
|
33
|
+
* @param {string} line - Line in form KEY=value
|
|
34
|
+
* @returns {string} Same line or KEY=*** or value with masked URL credentials
|
|
35
|
+
*/
|
|
36
|
+
function maskEnvLine(line) {
|
|
37
|
+
const eq = line.indexOf('=');
|
|
38
|
+
if (eq <= 0) return line;
|
|
39
|
+
const key = line.slice(0, eq);
|
|
40
|
+
const value = line.slice(eq + 1);
|
|
41
|
+
|
|
42
|
+
const keyForCheck = key.replace(KEY_PREFIXES_TO_STRIP, '');
|
|
43
|
+
const isSecretKey = SECRET_KEY_PATTERN.test(keyForCheck);
|
|
44
|
+
|
|
45
|
+
const maskedValue = value.replace(URL_CREDENTIAL_PATTERN, '$1$2:***@');
|
|
46
|
+
const hasUrlCredentials = maskedValue !== value;
|
|
47
|
+
|
|
48
|
+
if (isSecretKey) return `${key}=***`;
|
|
49
|
+
if (hasUrlCredentials) return `${key}=${maskedValue}`;
|
|
50
|
+
return line;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Dump container env (masked) and print to logger
|
|
55
|
+
* @async
|
|
56
|
+
* @param {string} containerName - Docker container name
|
|
57
|
+
* @returns {Promise<void>}
|
|
58
|
+
*/
|
|
59
|
+
async function dumpMaskedEnv(containerName) {
|
|
60
|
+
try {
|
|
61
|
+
const { stdout } = await execAsync(`docker exec ${containerName} env`, { encoding: 'utf8', timeout: 5000 });
|
|
62
|
+
const lines = stdout.split('\n').filter((l) => l.trim());
|
|
63
|
+
if (lines.length === 0) return;
|
|
64
|
+
logger.log(chalk.bold('\n--- Environment (sensitive values masked) ---\n'));
|
|
65
|
+
lines.sort((a, b) => {
|
|
66
|
+
const keyA = a.indexOf('=') > 0 ? a.slice(0, a.indexOf('=')) : a;
|
|
67
|
+
const keyB = b.indexOf('=') > 0 ? b.slice(0, b.indexOf('=')) : b;
|
|
68
|
+
return keyA.localeCompare(keyB);
|
|
69
|
+
});
|
|
70
|
+
lines.forEach((line) => logger.log(maskEnvLine(line)));
|
|
71
|
+
logger.log(chalk.gray('\n--- Logs ---\n'));
|
|
72
|
+
} catch (err) {
|
|
73
|
+
logger.log(chalk.gray('(Could not read container env; container may be stopped)\n'));
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Run docker logs (non-follow): tail N lines or full (tail 0)
|
|
79
|
+
* @async
|
|
80
|
+
* @param {string} containerName - Docker container name
|
|
81
|
+
* @param {Object} options - { tail: number } (0 = full, no limit)
|
|
82
|
+
* @returns {Promise<void>}
|
|
83
|
+
*/
|
|
84
|
+
async function runDockerLogs(containerName, options) {
|
|
85
|
+
const args = options.tail === 0 ? ['logs', containerName] : ['logs', '--tail', String(options.tail), containerName];
|
|
86
|
+
return new Promise((resolve, reject) => {
|
|
87
|
+
const proc = spawn('docker', args, { stdio: 'inherit' });
|
|
88
|
+
proc.on('error', reject);
|
|
89
|
+
proc.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`docker logs exited with ${code}`))));
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Run docker logs --follow (stream), optionally with tail
|
|
95
|
+
* @param {string} containerName - Docker container name
|
|
96
|
+
* @param {number} [tail] - Lines to show (0 = full, omit --tail)
|
|
97
|
+
*/
|
|
98
|
+
function runDockerLogsFollow(containerName, tail) {
|
|
99
|
+
const args = tail === 0 ? ['logs', '-f', containerName] : ['logs', '-f', '--tail', String(tail), containerName];
|
|
100
|
+
const proc = spawn('docker', args, { stdio: 'inherit' });
|
|
101
|
+
proc.on('error', (err) => {
|
|
102
|
+
logger.log(chalk.red(`Error: ${err.message}`));
|
|
103
|
+
process.exit(1);
|
|
104
|
+
});
|
|
105
|
+
proc.on('close', (code) => {
|
|
106
|
+
if (code !== 0 && code !== null) process.exit(code);
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Run app logs command: optional env dump (masked), then docker logs
|
|
112
|
+
* @async
|
|
113
|
+
* @param {string} appKey - Application key (app name)
|
|
114
|
+
* @param {Object} options - CLI options
|
|
115
|
+
* @param {boolean} [options.follow] - Follow log stream (-f)
|
|
116
|
+
* @param {number} [options.tail] - Number of lines (default 100; 0 = full list)
|
|
117
|
+
* @returns {Promise<void>}
|
|
118
|
+
*/
|
|
119
|
+
async function runAppLogs(appKey, options = {}) {
|
|
120
|
+
validateAppName(appKey);
|
|
121
|
+
const developerId = await config.getDeveloperId();
|
|
122
|
+
const containerName = containerHelpers.getContainerName(appKey, developerId);
|
|
123
|
+
|
|
124
|
+
const follow = !!options.follow;
|
|
125
|
+
const tail = typeof options.tail === 'number' ? options.tail : DEFAULT_TAIL_LINES;
|
|
126
|
+
|
|
127
|
+
logger.log(chalk.blue(`Container: ${containerName}\n`));
|
|
128
|
+
|
|
129
|
+
if (!follow) {
|
|
130
|
+
await dumpMaskedEnv(containerName);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (follow) {
|
|
134
|
+
runDockerLogsFollow(containerName, tail);
|
|
135
|
+
return;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
try {
|
|
139
|
+
await runDockerLogs(containerName, { tail });
|
|
140
|
+
} catch (err) {
|
|
141
|
+
logger.log(chalk.red(`Error: ${err.message}`));
|
|
142
|
+
throw new Error(`Failed to show logs: ${err.message}`);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
module.exports = { runAppLogs, maskEnvLine };
|
package/lib/commands/app.js
CHANGED
|
@@ -15,6 +15,7 @@ const { listApplications } = require('../app/list');
|
|
|
15
15
|
const { registerApplication } = require('../app/register');
|
|
16
16
|
const { rotateSecret } = require('../app/rotate-secret');
|
|
17
17
|
const { showApp } = require('../app/show');
|
|
18
|
+
const { runAppDeploymentList } = require('./deployment-list');
|
|
18
19
|
|
|
19
20
|
/**
|
|
20
21
|
* Setup application management commands
|
|
@@ -73,9 +74,31 @@ function setupAppCommands(program) {
|
|
|
73
74
|
.command('show <appKey>')
|
|
74
75
|
.description('Show application from controller (online). Same as aifabrix show <appKey> --online')
|
|
75
76
|
.option('--json', 'Output as JSON')
|
|
77
|
+
.option('--permissions', 'Show only list of permissions')
|
|
76
78
|
.action(async(appKey, options) => {
|
|
77
79
|
try {
|
|
78
|
-
await showApp(appKey, { online: true, json: !!options.json });
|
|
80
|
+
await showApp(appKey, { online: true, json: !!options.json, permissions: !!options.permissions });
|
|
81
|
+
} catch (error) {
|
|
82
|
+
logger.error(chalk.red(`Error: ${error.message}`));
|
|
83
|
+
process.exit(1);
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Deployment list for an application
|
|
88
|
+
app
|
|
89
|
+
.command('deployment <appKey>')
|
|
90
|
+
.description('List last N deployments for an application in current environment (default pageSize=50)')
|
|
91
|
+
.option('--controller <url>', 'Controller URL (default: from config)')
|
|
92
|
+
.option('--environment <env>', 'Environment key (default: from config)')
|
|
93
|
+
.option('--page-size <n>', 'Items per page', '50')
|
|
94
|
+
.action(async(appKey, options) => {
|
|
95
|
+
try {
|
|
96
|
+
const opts = {
|
|
97
|
+
controller: options.controller,
|
|
98
|
+
environment: options.environment,
|
|
99
|
+
pageSize: parseInt(options.pageSize, 10) || 50
|
|
100
|
+
};
|
|
101
|
+
await runAppDeploymentList(appKey, opts);
|
|
79
102
|
} catch (error) {
|
|
80
103
|
logger.error(chalk.red(`Error: ${error.message}`));
|
|
81
104
|
process.exit(1);
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Credential list command – list credentials from controller/dataplane
|
|
3
|
+
* GET /api/v1/credential. Used by `aifabrix credential list`.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Credential list command implementation
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
const logger = require('../utils/logger');
|
|
12
|
+
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
13
|
+
const { getOrRefreshDeviceToken } = require('../utils/token-manager');
|
|
14
|
+
const { normalizeControllerUrl } = require('../core/config');
|
|
15
|
+
const { listCredentials } = require('../api/credentials.api');
|
|
16
|
+
|
|
17
|
+
const DEFAULT_PAGE_SIZE = 50;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get auth token for credential list (device token from config)
|
|
21
|
+
* @async
|
|
22
|
+
* @param {string} controllerUrl - Controller base URL
|
|
23
|
+
* @returns {Promise<{token: string, controllerUrl: string}|null>}
|
|
24
|
+
*/
|
|
25
|
+
async function getCredentialListAuth(controllerUrl) {
|
|
26
|
+
const normalizedUrl = normalizeControllerUrl(controllerUrl);
|
|
27
|
+
const deviceToken = await getOrRefreshDeviceToken(normalizedUrl);
|
|
28
|
+
if (deviceToken && deviceToken.token) {
|
|
29
|
+
return {
|
|
30
|
+
token: deviceToken.token,
|
|
31
|
+
controllerUrl: deviceToken.controller || normalizedUrl
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Extract credentials array from API response
|
|
39
|
+
* @param {Object} response - API response
|
|
40
|
+
* @returns {Array}
|
|
41
|
+
*/
|
|
42
|
+
function extractCredentials(response) {
|
|
43
|
+
const data = response?.data ?? response;
|
|
44
|
+
const items = data?.credentials ?? data?.items ?? (Array.isArray(data) ? data : []);
|
|
45
|
+
return Array.isArray(items) ? items : [];
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Display credential list to user
|
|
50
|
+
* @param {Array} list - Credentials array
|
|
51
|
+
* @param {string} controllerUrl - Controller URL for header
|
|
52
|
+
*/
|
|
53
|
+
function displayCredentialList(list, controllerUrl) {
|
|
54
|
+
logger.log(chalk.bold(`\n🔐 Credentials (${controllerUrl}):\n`));
|
|
55
|
+
if (list.length === 0) {
|
|
56
|
+
logger.log(chalk.gray(' No credentials found.\n'));
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
list.forEach((c) => {
|
|
60
|
+
const key = c.key ?? c.id ?? c.credentialKey ?? '-';
|
|
61
|
+
const name = c.displayName ?? c.name ?? key;
|
|
62
|
+
logger.log(` ${chalk.cyan(key)} - ${name}`);
|
|
63
|
+
});
|
|
64
|
+
logger.log('');
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Run credential list command: call GET /api/v1/credential and display results
|
|
69
|
+
* @async
|
|
70
|
+
* @param {Object} options - CLI options
|
|
71
|
+
* @param {string} [options.controller] - Controller URL override
|
|
72
|
+
* @param {boolean} [options.activeOnly] - List only active credentials
|
|
73
|
+
* @param {number} [options.pageSize] - Items per page
|
|
74
|
+
* @returns {Promise<void>}
|
|
75
|
+
*/
|
|
76
|
+
async function runCredentialList(options = {}) {
|
|
77
|
+
const controllerUrl = options.controller || (await resolveControllerUrl());
|
|
78
|
+
if (!controllerUrl) {
|
|
79
|
+
logger.error(chalk.red('❌ Controller URL is required. Run "aifabrix login" first.'));
|
|
80
|
+
process.exit(1);
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
const authResult = await getCredentialListAuth(controllerUrl);
|
|
84
|
+
if (!authResult || !authResult.token) {
|
|
85
|
+
logger.error(chalk.red(`❌ No authentication token for controller: ${controllerUrl}`));
|
|
86
|
+
logger.error(chalk.gray('Run: aifabrix login'));
|
|
87
|
+
process.exit(1);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
const authConfig = { type: 'bearer', token: authResult.token };
|
|
91
|
+
const listOptions = {
|
|
92
|
+
pageSize: options.pageSize || DEFAULT_PAGE_SIZE,
|
|
93
|
+
activeOnly: options.activeOnly
|
|
94
|
+
};
|
|
95
|
+
try {
|
|
96
|
+
const response = await listCredentials(authResult.controllerUrl, authConfig, listOptions);
|
|
97
|
+
displayCredentialList(extractCredentials(response), authResult.controllerUrl);
|
|
98
|
+
} catch (error) {
|
|
99
|
+
logger.error(chalk.red(`❌ Failed to list credentials: ${error.message}`));
|
|
100
|
+
process.exit(1);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
module.exports = { runCredentialList };
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deployment list commands – list deployments for environment or for an app
|
|
3
|
+
* Uses GET .../deployments and GET .../applications/{appKey}/deployments.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Deployment list command implementation
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
const logger = require('../utils/logger');
|
|
12
|
+
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
13
|
+
const { resolveEnvironment } = require('../core/config');
|
|
14
|
+
const { getOrRefreshDeviceToken } = require('../utils/token-manager');
|
|
15
|
+
const { listDeployments, listApplicationDeployments } = require('../api/deployments.api');
|
|
16
|
+
const { normalizeControllerUrl } = require('../core/config');
|
|
17
|
+
|
|
18
|
+
const DEFAULT_PAGE_SIZE = 50;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get auth token for deployment list (device token from config)
|
|
22
|
+
* @async
|
|
23
|
+
* @param {string} controllerUrl - Controller base URL
|
|
24
|
+
* @returns {Promise<{token: string, controllerUrl: string}|null>}
|
|
25
|
+
*/
|
|
26
|
+
async function getDeploymentListAuth(controllerUrl) {
|
|
27
|
+
const normalizedUrl = normalizeControllerUrl(controllerUrl);
|
|
28
|
+
const deviceToken = await getOrRefreshDeviceToken(normalizedUrl);
|
|
29
|
+
if (deviceToken && deviceToken.token) {
|
|
30
|
+
return {
|
|
31
|
+
token: deviceToken.token,
|
|
32
|
+
controllerUrl: deviceToken.controller || normalizedUrl
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extract deployments array from API response
|
|
40
|
+
* @param {Object} response - API response
|
|
41
|
+
* @returns {Array}
|
|
42
|
+
*/
|
|
43
|
+
function extractDeployments(response) {
|
|
44
|
+
const data = response?.data ?? response;
|
|
45
|
+
const items = data?.items ?? data?.deployments ?? (Array.isArray(data) ? data : []);
|
|
46
|
+
return Array.isArray(items) ? items : [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Display environment deployment list to user
|
|
51
|
+
* @param {Array} deployments - Deployments array
|
|
52
|
+
* @param {string} environment - Environment key
|
|
53
|
+
* @param {string} controllerUrl - Controller URL
|
|
54
|
+
*/
|
|
55
|
+
function displayDeploymentList(deployments, environment, controllerUrl) {
|
|
56
|
+
logger.log(chalk.bold(`\n📋 Deployments (${environment}) at ${controllerUrl}:\n`));
|
|
57
|
+
if (deployments.length === 0) {
|
|
58
|
+
logger.log(chalk.gray(' No deployments found.\n'));
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
deployments.forEach((d) => {
|
|
62
|
+
const id = d.id ?? d.deploymentId ?? '-';
|
|
63
|
+
const appKey = d.applicationKey ?? d.appKey ?? d.application?.key ?? '-';
|
|
64
|
+
const status = d.status ?? '-';
|
|
65
|
+
const createdAt = d.createdAt ?? d.created ?? '';
|
|
66
|
+
logger.log(` ${chalk.cyan(id)} ${appKey} ${status} ${chalk.gray(createdAt)}`);
|
|
67
|
+
});
|
|
68
|
+
logger.log('');
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Run deployment list (environment): list last N deployments for current environment
|
|
73
|
+
* @async
|
|
74
|
+
* @param {Object} options - CLI options
|
|
75
|
+
* @param {string} [options.controller] - Controller URL override
|
|
76
|
+
* @param {string} [options.environment] - Environment key override
|
|
77
|
+
* @param {number} [options.pageSize] - Items per page (default 50)
|
|
78
|
+
* @returns {Promise<void>}
|
|
79
|
+
*/
|
|
80
|
+
async function runDeploymentList(options = {}) {
|
|
81
|
+
const { environment, authResult } = await resolveDeploymentListContext(options);
|
|
82
|
+
const authConfig = { type: 'bearer', token: authResult.token };
|
|
83
|
+
const listOptions = { pageSize: options.pageSize || DEFAULT_PAGE_SIZE };
|
|
84
|
+
try {
|
|
85
|
+
const response = await listDeployments(
|
|
86
|
+
authResult.controllerUrl,
|
|
87
|
+
environment,
|
|
88
|
+
authConfig,
|
|
89
|
+
listOptions
|
|
90
|
+
);
|
|
91
|
+
displayDeploymentList(extractDeployments(response), environment, authResult.controllerUrl);
|
|
92
|
+
} catch (error) {
|
|
93
|
+
logger.error(chalk.red(`❌ Failed to list deployments: ${error.message}`));
|
|
94
|
+
process.exit(1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Display app deployment list to user
|
|
100
|
+
* @param {Array} deployments - Deployments array
|
|
101
|
+
* @param {string} appKey - Application key
|
|
102
|
+
* @param {string} environment - Environment key
|
|
103
|
+
* @param {string} controllerUrl - Controller URL
|
|
104
|
+
*/
|
|
105
|
+
function displayAppDeploymentList(deployments, appKey, environment, controllerUrl) {
|
|
106
|
+
logger.log(chalk.bold(`\n📋 Deployments for ${appKey} (${environment}) at ${controllerUrl}:\n`));
|
|
107
|
+
if (deployments.length === 0) {
|
|
108
|
+
logger.log(chalk.gray(' No deployments found for this application.\n'));
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
deployments.forEach((d) => {
|
|
112
|
+
const id = d.id ?? d.deploymentId ?? '-';
|
|
113
|
+
const status = d.status ?? '-';
|
|
114
|
+
const createdAt = d.createdAt ?? d.created ?? '';
|
|
115
|
+
logger.log(` ${chalk.cyan(id)} ${status} ${chalk.gray(createdAt)}`);
|
|
116
|
+
});
|
|
117
|
+
logger.log('');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Resolve controller URL, environment, and auth for deployment list commands
|
|
122
|
+
* @async
|
|
123
|
+
* @param {Object} options - Options with optional controller, environment
|
|
124
|
+
* @returns {Promise<{controllerUrl: string, environment: string, authResult: Object}>}
|
|
125
|
+
*/
|
|
126
|
+
async function resolveDeploymentListContext(options) {
|
|
127
|
+
const controllerUrl = options.controller || (await resolveControllerUrl());
|
|
128
|
+
if (!controllerUrl) {
|
|
129
|
+
logger.error(chalk.red('❌ Controller URL is required. Run "aifabrix login" first.'));
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
const environment = options.environment || (await resolveEnvironment());
|
|
133
|
+
const authResult = await getDeploymentListAuth(controllerUrl);
|
|
134
|
+
if (!authResult || !authResult.token) {
|
|
135
|
+
logger.error(chalk.red(`❌ No authentication token for controller: ${controllerUrl}`));
|
|
136
|
+
logger.error(chalk.gray('Run: aifabrix login'));
|
|
137
|
+
process.exit(1);
|
|
138
|
+
}
|
|
139
|
+
return { controllerUrl, environment, authResult };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Run app deployment list: list last N deployments for an application
|
|
144
|
+
* @async
|
|
145
|
+
* @param {string} appKey - Application key
|
|
146
|
+
* @param {Object} options - CLI options
|
|
147
|
+
* @param {string} [options.controller] - Controller URL override
|
|
148
|
+
* @param {string} [options.environment] - Environment key override
|
|
149
|
+
* @param {number} [options.pageSize] - Items per page (default 50)
|
|
150
|
+
* @returns {Promise<void>}
|
|
151
|
+
*/
|
|
152
|
+
async function runAppDeploymentList(appKey, options = {}) {
|
|
153
|
+
if (!appKey || typeof appKey !== 'string') {
|
|
154
|
+
logger.error(chalk.red('❌ Application key is required.'));
|
|
155
|
+
process.exit(1);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
const { environment, authResult } = await resolveDeploymentListContext(options);
|
|
159
|
+
const authConfig = { type: 'bearer', token: authResult.token };
|
|
160
|
+
const listOptions = { pageSize: options.pageSize || DEFAULT_PAGE_SIZE };
|
|
161
|
+
try {
|
|
162
|
+
const response = await listApplicationDeployments(
|
|
163
|
+
authResult.controllerUrl,
|
|
164
|
+
environment,
|
|
165
|
+
appKey,
|
|
166
|
+
authConfig,
|
|
167
|
+
listOptions
|
|
168
|
+
);
|
|
169
|
+
displayAppDeploymentList(
|
|
170
|
+
extractDeployments(response),
|
|
171
|
+
appKey,
|
|
172
|
+
environment,
|
|
173
|
+
authResult.controllerUrl
|
|
174
|
+
);
|
|
175
|
+
} catch (error) {
|
|
176
|
+
logger.error(chalk.red(`❌ Failed to list deployments for ${appKey}: ${error.message}`));
|
|
177
|
+
process.exit(1);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
runDeploymentList,
|
|
183
|
+
runAppDeploymentList
|
|
184
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service user create command – create service user and get one-time secret
|
|
3
|
+
* POST /api/v1/service-users. Used by `aifabrix service-user create`.
|
|
4
|
+
*
|
|
5
|
+
* @fileoverview Service user create command implementation
|
|
6
|
+
* @author AI Fabrix Team
|
|
7
|
+
* @version 2.0.0
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const chalk = require('chalk');
|
|
11
|
+
const logger = require('../utils/logger');
|
|
12
|
+
const { resolveControllerUrl } = require('../utils/controller-url');
|
|
13
|
+
const { getOrRefreshDeviceToken } = require('../utils/token-manager');
|
|
14
|
+
const { normalizeControllerUrl } = require('../core/config');
|
|
15
|
+
const { createServiceUser } = require('../api/service-users.api');
|
|
16
|
+
|
|
17
|
+
const ONE_TIME_WARNING =
|
|
18
|
+
'Save this secret now; it will not be shown again.';
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Get auth token for service-user (device token from config)
|
|
22
|
+
* @async
|
|
23
|
+
* @param {string} controllerUrl - Controller base URL
|
|
24
|
+
* @returns {Promise<{token: string, controllerUrl: string}|null>}
|
|
25
|
+
*/
|
|
26
|
+
async function getServiceUserAuth(controllerUrl) {
|
|
27
|
+
const normalizedUrl = normalizeControllerUrl(controllerUrl);
|
|
28
|
+
const deviceToken = await getOrRefreshDeviceToken(normalizedUrl);
|
|
29
|
+
if (deviceToken && deviceToken.token) {
|
|
30
|
+
return {
|
|
31
|
+
token: deviceToken.token,
|
|
32
|
+
controllerUrl: deviceToken.controller || normalizedUrl
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Extract clientId and clientSecret from API response (response may be wrapped in data)
|
|
40
|
+
* @param {Object} response - API response
|
|
41
|
+
* @returns {{ clientId: string, clientSecret: string }}
|
|
42
|
+
*/
|
|
43
|
+
function extractCreateResponse(response) {
|
|
44
|
+
const data = response?.data ?? response;
|
|
45
|
+
return {
|
|
46
|
+
clientId: data?.clientId ?? '',
|
|
47
|
+
clientSecret: data?.clientSecret ?? ''
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Log error for failed create response and exit
|
|
53
|
+
* @param {Object} response - API response with success: false
|
|
54
|
+
*/
|
|
55
|
+
function handleCreateError(response) {
|
|
56
|
+
const status = response.status;
|
|
57
|
+
const msg = response.formattedError || response.error || 'Request failed';
|
|
58
|
+
if (status === 400) {
|
|
59
|
+
logger.error(chalk.red(`❌ Validation error: ${msg}`));
|
|
60
|
+
} else if (status === 401) {
|
|
61
|
+
logger.error(chalk.red('❌ Unauthorized. Run "aifabrix login" and try again.'));
|
|
62
|
+
} else if (status === 403) {
|
|
63
|
+
logger.error(chalk.red('❌ Missing permission: service-user:create'));
|
|
64
|
+
logger.error(chalk.gray('Your account needs the service-user:create permission on the controller.'));
|
|
65
|
+
} else {
|
|
66
|
+
logger.error(chalk.red(`❌ Failed to create service user: ${msg}`));
|
|
67
|
+
}
|
|
68
|
+
process.exit(1);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Display success output with clientId, clientSecret and one-time warning
|
|
73
|
+
* @param {string} clientId - Service user client ID
|
|
74
|
+
* @param {string} clientSecret - One-time client secret
|
|
75
|
+
*/
|
|
76
|
+
function displayCreateSuccess(clientId, clientSecret) {
|
|
77
|
+
logger.log(chalk.bold('\n✓ Service user created\n'));
|
|
78
|
+
logger.log(chalk.cyan(' clientId: ') + clientId);
|
|
79
|
+
logger.log(chalk.cyan(' clientSecret: ') + clientSecret);
|
|
80
|
+
logger.log('');
|
|
81
|
+
logger.log(chalk.yellow('⚠ ' + ONE_TIME_WARNING));
|
|
82
|
+
logger.log('');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Parse comma-separated string into non-empty trimmed array
|
|
87
|
+
* @param {string} [val] - Comma-separated value
|
|
88
|
+
* @returns {string[]}
|
|
89
|
+
*/
|
|
90
|
+
function parseList(val) {
|
|
91
|
+
if (val === undefined || val === null || String(val).trim() === '') {
|
|
92
|
+
return [];
|
|
93
|
+
}
|
|
94
|
+
return String(val)
|
|
95
|
+
.split(',')
|
|
96
|
+
.map(s => s.trim())
|
|
97
|
+
.filter(Boolean);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Validate username, email, redirectUris, groupIds; exit on failure
|
|
102
|
+
* @param {Object} options - CLI options
|
|
103
|
+
* @returns {{ username: string, email: string, redirectUris: string[], groupNames: string[], description?: string }}
|
|
104
|
+
*/
|
|
105
|
+
function validateServiceUserOptions(options) {
|
|
106
|
+
const username = options.username?.trim();
|
|
107
|
+
const email = options.email?.trim();
|
|
108
|
+
const redirectUris = parseList(options.redirectUris);
|
|
109
|
+
const groupNames = parseList(options.groupNames);
|
|
110
|
+
if (!username) {
|
|
111
|
+
logger.error(chalk.red('❌ Username is required. Use --username <username>.'));
|
|
112
|
+
process.exit(1);
|
|
113
|
+
}
|
|
114
|
+
if (!email) {
|
|
115
|
+
logger.error(chalk.red('❌ Email is required. Use --email <email>.'));
|
|
116
|
+
process.exit(1);
|
|
117
|
+
}
|
|
118
|
+
if (redirectUris.length === 0) {
|
|
119
|
+
logger.error(chalk.red('❌ At least one redirect URI is required. Use --redirect-uris <uri1,uri2,...>.'));
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
if (groupNames.length === 0) {
|
|
123
|
+
logger.error(chalk.red('❌ At least one group name is required. Use --group-names <name1,name2,...>.'));
|
|
124
|
+
process.exit(1);
|
|
125
|
+
}
|
|
126
|
+
return {
|
|
127
|
+
username,
|
|
128
|
+
email,
|
|
129
|
+
redirectUris,
|
|
130
|
+
groupNames,
|
|
131
|
+
description: options.description?.trim() || undefined
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Resolve controller URL and auth; exit on failure
|
|
137
|
+
* @async
|
|
138
|
+
* @param {Object} options - CLI options
|
|
139
|
+
* @returns {Promise<{ username: string, email: string, redirectUris: string[], groupNames: string[], description?: string, controllerUrl: string, authConfig: Object }>}
|
|
140
|
+
*/
|
|
141
|
+
async function resolveOptionsAndAuth(options) {
|
|
142
|
+
const validated = validateServiceUserOptions(options);
|
|
143
|
+
const controllerUrl = options.controller || (await resolveControllerUrl());
|
|
144
|
+
if (!controllerUrl) {
|
|
145
|
+
logger.error(chalk.red('❌ Controller URL is required. Run "aifabrix login" first.'));
|
|
146
|
+
process.exit(1);
|
|
147
|
+
}
|
|
148
|
+
const authResult = await getServiceUserAuth(controllerUrl);
|
|
149
|
+
if (!authResult || !authResult.token) {
|
|
150
|
+
logger.error(chalk.red(`❌ No authentication token for controller: ${controllerUrl}`));
|
|
151
|
+
logger.error(chalk.gray('Run: aifabrix login'));
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
...validated,
|
|
156
|
+
controllerUrl: authResult.controllerUrl,
|
|
157
|
+
authConfig: { type: 'bearer', token: authResult.token }
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Run service-user create: call POST /api/v1/service-users and display one-time secret with warning
|
|
163
|
+
* @async
|
|
164
|
+
* @param {Object} options - CLI options
|
|
165
|
+
* @param {string} [options.controller] - Controller URL override
|
|
166
|
+
* @param {string} options.username - Username (required)
|
|
167
|
+
* @param {string} options.email - Email (required)
|
|
168
|
+
* @param {string} options.redirectUris - Comma-separated redirect URIs (required, min 1)
|
|
169
|
+
* @param {string} options.groupNames - Comma-separated group names (required, e.g. AI-Fabrix-Developers)
|
|
170
|
+
* @param {string} [options.description] - Optional description
|
|
171
|
+
* @returns {Promise<void>}
|
|
172
|
+
*/
|
|
173
|
+
async function runServiceUserCreate(options = {}) {
|
|
174
|
+
const ctx = await resolveOptionsAndAuth(options);
|
|
175
|
+
const body = {
|
|
176
|
+
username: ctx.username,
|
|
177
|
+
email: ctx.email,
|
|
178
|
+
redirectUris: ctx.redirectUris,
|
|
179
|
+
groupNames: ctx.groupNames,
|
|
180
|
+
description: ctx.description
|
|
181
|
+
};
|
|
182
|
+
const response = await createServiceUser(ctx.controllerUrl, ctx.authConfig, body);
|
|
183
|
+
if (!response.success) {
|
|
184
|
+
handleCreateError(response);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
const { clientId, clientSecret } = extractCreateResponse(response);
|
|
188
|
+
displayCreateSuccess(clientId, clientSecret);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
module.exports = {
|
|
192
|
+
runServiceUserCreate
|
|
193
|
+
};
|