genbox 1.0.26 → 1.0.28
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/dist/api.js +20 -4
- package/dist/commands/create.js +8 -0
- package/dist/commands/init.js +140 -132
- package/dist/commands/status.js +21 -9
- package/package.json +1 -1
package/dist/api.js
CHANGED
|
@@ -49,10 +49,26 @@ async function fetchApi(endpoint, options = {}) {
|
|
|
49
49
|
if (token) {
|
|
50
50
|
headers['Authorization'] = `Bearer ${token}`;
|
|
51
51
|
}
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
52
|
+
let response;
|
|
53
|
+
try {
|
|
54
|
+
response = await fetch(url, {
|
|
55
|
+
...options,
|
|
56
|
+
headers,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
catch (error) {
|
|
60
|
+
// Handle network-level errors (DNS, connection refused, timeout, etc.)
|
|
61
|
+
if (error.cause?.code === 'ECONNREFUSED') {
|
|
62
|
+
throw new Error(`Cannot connect to Genbox API at ${API_URL}. Is the server running?`);
|
|
63
|
+
}
|
|
64
|
+
if (error.cause?.code === 'ENOTFOUND') {
|
|
65
|
+
throw new Error(`Cannot resolve Genbox API host. Check your internet connection.`);
|
|
66
|
+
}
|
|
67
|
+
if (error.message?.includes('fetch failed')) {
|
|
68
|
+
throw new Error(`Cannot connect to Genbox API at ${API_URL}. Check your internet connection or try again later.`);
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`Network error: ${error.message}`);
|
|
71
|
+
}
|
|
56
72
|
if (!response.ok) {
|
|
57
73
|
// Handle authentication errors with a friendly message
|
|
58
74
|
if (response.status === 401) {
|
package/dist/commands/create.js
CHANGED
|
@@ -763,6 +763,14 @@ function buildPayload(resolved, config, publicKey, privateKey, configLoader) {
|
|
|
763
763
|
gitToken: envVars.GIT_TOKEN,
|
|
764
764
|
envVars: resolved.env,
|
|
765
765
|
apps: resolved.apps.map(a => a.name),
|
|
766
|
+
appConfigs: resolved.apps.map(a => ({
|
|
767
|
+
name: a.name,
|
|
768
|
+
path: a.path.startsWith('/') ? a.path : `${resolved.repos[0]?.path || '/home/dev'}/${a.path}`,
|
|
769
|
+
type: a.type,
|
|
770
|
+
port: a.port,
|
|
771
|
+
framework: a.framework,
|
|
772
|
+
commands: a.commands,
|
|
773
|
+
})),
|
|
766
774
|
infrastructure: resolved.infrastructure.map(i => ({
|
|
767
775
|
name: i.name,
|
|
768
776
|
type: i.type,
|
package/dist/commands/init.js
CHANGED
|
@@ -1012,35 +1012,59 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
1012
1012
|
* Create default profiles (sync version for non-interactive mode)
|
|
1013
1013
|
*/
|
|
1014
1014
|
function createDefaultProfilesSync(scan, config) {
|
|
1015
|
-
|
|
1015
|
+
const definedEnvs = Object.keys(config.environments || {});
|
|
1016
|
+
return createProfilesFromScan(scan, definedEnvs);
|
|
1016
1017
|
}
|
|
1017
1018
|
async function createDefaultProfiles(scan, config) {
|
|
1018
|
-
|
|
1019
|
+
// Get defined environments to use in profiles
|
|
1020
|
+
const definedEnvs = Object.keys(config.environments || {});
|
|
1021
|
+
return createProfilesFromScan(scan, definedEnvs);
|
|
1019
1022
|
}
|
|
1020
|
-
function createProfilesFromScan(scan) {
|
|
1023
|
+
function createProfilesFromScan(scan, definedEnvironments = []) {
|
|
1021
1024
|
const profiles = {};
|
|
1022
1025
|
const frontendApps = scan.apps.filter(a => a.type === 'frontend');
|
|
1023
1026
|
const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
|
|
1024
1027
|
const hasApi = scan.apps.some(a => a.name === 'api' || a.type === 'backend');
|
|
1028
|
+
// Determine which environment to use for remote connections
|
|
1029
|
+
// Priority: staging > production > first defined
|
|
1030
|
+
const remoteEnv = definedEnvironments.includes('staging')
|
|
1031
|
+
? 'staging'
|
|
1032
|
+
: definedEnvironments.includes('production')
|
|
1033
|
+
? 'production'
|
|
1034
|
+
: definedEnvironments[0];
|
|
1025
1035
|
// Quick UI profiles for each frontend (v4: use default_connection instead of connect_to)
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1036
|
+
// Only create if we have a remote environment defined
|
|
1037
|
+
if (remoteEnv) {
|
|
1038
|
+
for (const frontend of frontendApps.slice(0, 3)) {
|
|
1039
|
+
profiles[`${frontend.name}-quick`] = {
|
|
1040
|
+
description: `${frontend.name} only, connected to ${remoteEnv}`,
|
|
1041
|
+
size: 'small',
|
|
1042
|
+
apps: [frontend.name],
|
|
1043
|
+
default_connection: remoteEnv,
|
|
1044
|
+
};
|
|
1045
|
+
}
|
|
1033
1046
|
}
|
|
1034
|
-
|
|
1047
|
+
else {
|
|
1048
|
+
// No remote environment - create local-only profiles
|
|
1049
|
+
for (const frontend of frontendApps.slice(0, 3)) {
|
|
1050
|
+
profiles[`${frontend.name}-local`] = {
|
|
1051
|
+
description: `${frontend.name} with local API`,
|
|
1052
|
+
size: 'medium',
|
|
1053
|
+
apps: [frontend.name, ...(hasApi ? ['api'] : [])],
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
// Full local development (with DB copy from available environment)
|
|
1035
1058
|
if (hasApi && frontendApps.length > 0) {
|
|
1036
1059
|
const primaryFrontend = frontendApps[0];
|
|
1060
|
+
const dbSource = remoteEnv || 'staging'; // Use defined env or default
|
|
1037
1061
|
profiles[`${primaryFrontend.name}-full`] = {
|
|
1038
1062
|
description: `${primaryFrontend.name} + local API + DB copy`,
|
|
1039
1063
|
size: 'large',
|
|
1040
1064
|
apps: [primaryFrontend.name, 'api'],
|
|
1041
1065
|
database: {
|
|
1042
|
-
mode: 'copy',
|
|
1043
|
-
source:
|
|
1066
|
+
mode: remoteEnv ? 'copy' : 'local', // Only copy if we have a source
|
|
1067
|
+
...(remoteEnv && { source: dbSource }),
|
|
1044
1068
|
},
|
|
1045
1069
|
};
|
|
1046
1070
|
}
|
|
@@ -1055,24 +1079,25 @@ function createProfilesFromScan(scan) {
|
|
|
1055
1079
|
},
|
|
1056
1080
|
};
|
|
1057
1081
|
}
|
|
1058
|
-
// All frontends +
|
|
1059
|
-
if (frontendApps.length > 1) {
|
|
1060
|
-
profiles[
|
|
1061
|
-
description:
|
|
1082
|
+
// All frontends + remote backend (only if remote env defined)
|
|
1083
|
+
if (frontendApps.length > 1 && remoteEnv) {
|
|
1084
|
+
profiles[`frontends-${remoteEnv}`] = {
|
|
1085
|
+
description: `All frontends with ${remoteEnv} backend`,
|
|
1062
1086
|
size: 'medium',
|
|
1063
1087
|
apps: frontendApps.map(a => a.name),
|
|
1064
|
-
default_connection:
|
|
1088
|
+
default_connection: remoteEnv,
|
|
1065
1089
|
};
|
|
1066
1090
|
}
|
|
1067
1091
|
// Full stack
|
|
1068
1092
|
if (scan.apps.length > 1) {
|
|
1093
|
+
const dbSource = remoteEnv || 'staging';
|
|
1069
1094
|
profiles['full-stack'] = {
|
|
1070
|
-
description: 'Everything local with DB copy',
|
|
1095
|
+
description: 'Everything local' + (remoteEnv ? ' with DB copy' : ''),
|
|
1071
1096
|
size: 'xl',
|
|
1072
1097
|
apps: scan.apps.filter(a => a.type !== 'library').map(a => a.name),
|
|
1073
1098
|
database: {
|
|
1074
|
-
mode: 'copy',
|
|
1075
|
-
source:
|
|
1099
|
+
mode: remoteEnv ? 'copy' : 'local',
|
|
1100
|
+
...(remoteEnv && { source: dbSource }),
|
|
1076
1101
|
},
|
|
1077
1102
|
};
|
|
1078
1103
|
}
|
|
@@ -1150,127 +1175,96 @@ async function setupGitAuth(gitInfo, projectName) {
|
|
|
1150
1175
|
* Setup staging/production environments (v4 format)
|
|
1151
1176
|
*/
|
|
1152
1177
|
async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvValues = {}) {
|
|
1153
|
-
|
|
1154
|
-
|
|
1155
|
-
|
|
1178
|
+
// First ask which environments they want to configure
|
|
1179
|
+
const envChoice = await prompts.select({
|
|
1180
|
+
message: 'Which environments do you want to configure?',
|
|
1181
|
+
choices: [
|
|
1182
|
+
{ name: 'Staging only', value: 'staging', description: 'Connect to staging API' },
|
|
1183
|
+
{ name: 'Production only', value: 'production', description: 'Connect to production API' },
|
|
1184
|
+
{ name: 'Both staging and production', value: 'both', description: 'Configure both environments' },
|
|
1185
|
+
{ name: 'Skip for now', value: 'skip', description: 'No remote environments' },
|
|
1186
|
+
],
|
|
1187
|
+
default: 'staging',
|
|
1156
1188
|
});
|
|
1157
|
-
if (
|
|
1189
|
+
if (envChoice === 'skip') {
|
|
1158
1190
|
return undefined;
|
|
1159
1191
|
}
|
|
1160
1192
|
console.log('');
|
|
1161
1193
|
console.log(chalk_1.default.blue('=== Environment Setup ==='));
|
|
1162
1194
|
console.log(chalk_1.default.dim('These URLs will be used when connecting to external services.'));
|
|
1163
1195
|
console.log(chalk_1.default.dim('Actual secrets go in .env.genbox'));
|
|
1164
|
-
console.log('');
|
|
1165
1196
|
const environments = {};
|
|
1166
|
-
// Get existing
|
|
1197
|
+
// Get existing URLs if available
|
|
1167
1198
|
const existingStagingApiUrl = existingEnvValues['STAGING_API_URL'];
|
|
1168
1199
|
const existingProductionApiUrl = existingEnvValues['PRODUCTION_API_URL'] || existingEnvValues['PROD_API_URL'];
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
default:
|
|
1200
|
+
const configureStaging = envChoice === 'staging' || envChoice === 'both';
|
|
1201
|
+
const configureProduction = envChoice === 'production' || envChoice === 'both';
|
|
1202
|
+
// Configure staging if selected
|
|
1203
|
+
if (configureStaging) {
|
|
1204
|
+
console.log('');
|
|
1205
|
+
console.log(chalk_1.default.cyan('Staging Environment:'));
|
|
1206
|
+
if (isMultiRepo) {
|
|
1207
|
+
const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
|
|
1208
|
+
if (backendApps.length > 0) {
|
|
1209
|
+
const urls = {};
|
|
1210
|
+
for (const app of backendApps) {
|
|
1211
|
+
const existingUrl = existingEnvValues[`STAGING_${app.name.toUpperCase()}_URL`] ||
|
|
1212
|
+
(app.name === 'api' ? existingStagingApiUrl : '');
|
|
1213
|
+
if (existingUrl) {
|
|
1214
|
+
console.log(chalk_1.default.dim(` Found existing value for ${app.name}: ${existingUrl}`));
|
|
1215
|
+
const useExisting = await prompts.confirm({
|
|
1216
|
+
message: ` Use existing ${app.name} staging URL?`,
|
|
1217
|
+
default: true,
|
|
1218
|
+
});
|
|
1219
|
+
if (useExisting) {
|
|
1220
|
+
urls[app.name] = existingUrl;
|
|
1221
|
+
continue;
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
const url = await prompts.input({
|
|
1225
|
+
message: ` ${app.name} staging URL:`,
|
|
1226
|
+
default: '',
|
|
1184
1227
|
});
|
|
1185
|
-
if (
|
|
1186
|
-
urls[app.name] =
|
|
1187
|
-
continue;
|
|
1228
|
+
if (url) {
|
|
1229
|
+
urls[app.name] = url;
|
|
1188
1230
|
}
|
|
1189
1231
|
}
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
urls[app.name] = url;
|
|
1232
|
+
if (Object.keys(urls).length > 0) {
|
|
1233
|
+
environments.staging = {
|
|
1234
|
+
description: 'Staging environment',
|
|
1235
|
+
urls,
|
|
1236
|
+
};
|
|
1196
1237
|
}
|
|
1197
1238
|
}
|
|
1198
|
-
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1239
|
+
else {
|
|
1240
|
+
const stagingApiUrl = await promptForApiUrl('staging', existingStagingApiUrl);
|
|
1241
|
+
if (stagingApiUrl) {
|
|
1242
|
+
environments.staging = {
|
|
1243
|
+
description: 'Staging environment',
|
|
1244
|
+
urls: { api: stagingApiUrl },
|
|
1245
|
+
};
|
|
1246
|
+
}
|
|
1203
1247
|
}
|
|
1204
1248
|
}
|
|
1205
1249
|
else {
|
|
1206
|
-
|
|
1207
|
-
let stagingApiUrl = '';
|
|
1208
|
-
if (existingStagingApiUrl) {
|
|
1209
|
-
console.log(chalk_1.default.dim(` Found existing value: ${existingStagingApiUrl}`));
|
|
1210
|
-
const useExisting = await prompts.confirm({
|
|
1211
|
-
message: 'Use existing staging API URL?',
|
|
1212
|
-
default: true,
|
|
1213
|
-
});
|
|
1214
|
-
if (useExisting) {
|
|
1215
|
-
stagingApiUrl = existingStagingApiUrl;
|
|
1216
|
-
}
|
|
1217
|
-
}
|
|
1218
|
-
if (!stagingApiUrl) {
|
|
1219
|
-
stagingApiUrl = await prompts.input({
|
|
1220
|
-
message: 'Staging API URL (leave empty to skip):',
|
|
1221
|
-
default: '',
|
|
1222
|
-
});
|
|
1223
|
-
}
|
|
1250
|
+
const stagingApiUrl = await promptForApiUrl('staging', existingStagingApiUrl);
|
|
1224
1251
|
if (stagingApiUrl) {
|
|
1225
1252
|
environments.staging = {
|
|
1226
1253
|
description: 'Staging environment',
|
|
1227
|
-
urls: {
|
|
1228
|
-
api: stagingApiUrl,
|
|
1229
|
-
},
|
|
1254
|
+
urls: { api: stagingApiUrl },
|
|
1230
1255
|
};
|
|
1231
1256
|
}
|
|
1232
1257
|
}
|
|
1233
1258
|
}
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
console.log(chalk_1.default.dim(` Found existing value: ${existingStagingApiUrl}`));
|
|
1239
|
-
const useExisting = await prompts.confirm({
|
|
1240
|
-
message: 'Use existing staging API URL?',
|
|
1241
|
-
default: true,
|
|
1242
|
-
});
|
|
1243
|
-
if (useExisting) {
|
|
1244
|
-
stagingApiUrl = existingStagingApiUrl;
|
|
1245
|
-
}
|
|
1246
|
-
}
|
|
1247
|
-
if (!stagingApiUrl) {
|
|
1248
|
-
stagingApiUrl = await prompts.input({
|
|
1249
|
-
message: 'Staging API URL (leave empty to skip):',
|
|
1250
|
-
default: '',
|
|
1251
|
-
});
|
|
1252
|
-
}
|
|
1253
|
-
if (stagingApiUrl) {
|
|
1254
|
-
environments.staging = {
|
|
1255
|
-
description: 'Staging environment',
|
|
1256
|
-
urls: {
|
|
1257
|
-
api: stagingApiUrl,
|
|
1258
|
-
},
|
|
1259
|
-
};
|
|
1260
|
-
}
|
|
1261
|
-
}
|
|
1262
|
-
const setupProd = await prompts.confirm({
|
|
1263
|
-
message: 'Also configure production environment?',
|
|
1264
|
-
default: false,
|
|
1265
|
-
});
|
|
1266
|
-
if (setupProd) {
|
|
1259
|
+
// Configure production if selected
|
|
1260
|
+
if (configureProduction) {
|
|
1261
|
+
console.log('');
|
|
1262
|
+
console.log(chalk_1.default.cyan('Production Environment:'));
|
|
1267
1263
|
if (isMultiRepo) {
|
|
1268
1264
|
const backendApps = scan.apps.filter(a => a.type === 'backend' || a.type === 'api');
|
|
1269
1265
|
if (backendApps.length > 0) {
|
|
1270
|
-
console.log(chalk_1.default.dim('Configure production API URLs for each backend service:'));
|
|
1271
1266
|
const prodUrls = {};
|
|
1272
1267
|
for (const app of backendApps) {
|
|
1273
|
-
// Check for existing app-specific URL or use general production URL for 'api' app
|
|
1274
1268
|
const existingUrl = existingEnvValues[`PRODUCTION_${app.name.toUpperCase()}_URL`] ||
|
|
1275
1269
|
existingEnvValues[`PROD_${app.name.toUpperCase()}_URL`] ||
|
|
1276
1270
|
(app.name === 'api' ? existingProductionApiUrl : '');
|
|
@@ -1304,31 +1298,26 @@ async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvV
|
|
|
1304
1298
|
};
|
|
1305
1299
|
}
|
|
1306
1300
|
}
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1301
|
+
else {
|
|
1302
|
+
const prodApiUrl = await promptForApiUrl('production', existingProductionApiUrl);
|
|
1303
|
+
if (prodApiUrl) {
|
|
1304
|
+
environments.production = {
|
|
1305
|
+
description: 'Production (use with caution)',
|
|
1306
|
+
urls: { api: prodApiUrl },
|
|
1307
|
+
safety: {
|
|
1308
|
+
read_only: true,
|
|
1309
|
+
require_confirmation: true,
|
|
1310
|
+
},
|
|
1311
|
+
};
|
|
1318
1312
|
}
|
|
1319
1313
|
}
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
default: '',
|
|
1324
|
-
});
|
|
1325
|
-
}
|
|
1314
|
+
}
|
|
1315
|
+
else {
|
|
1316
|
+
const prodApiUrl = await promptForApiUrl('production', existingProductionApiUrl);
|
|
1326
1317
|
if (prodApiUrl) {
|
|
1327
1318
|
environments.production = {
|
|
1328
1319
|
description: 'Production (use with caution)',
|
|
1329
|
-
urls: {
|
|
1330
|
-
api: prodApiUrl,
|
|
1331
|
-
},
|
|
1320
|
+
urls: { api: prodApiUrl },
|
|
1332
1321
|
safety: {
|
|
1333
1322
|
read_only: true,
|
|
1334
1323
|
require_confirmation: true,
|
|
@@ -1339,6 +1328,25 @@ async function setupEnvironments(scan, config, isMultiRepo = false, existingEnvV
|
|
|
1339
1328
|
}
|
|
1340
1329
|
return Object.keys(environments).length > 0 ? environments : undefined;
|
|
1341
1330
|
}
|
|
1331
|
+
/**
|
|
1332
|
+
* Prompt for a single API URL with existing value support
|
|
1333
|
+
*/
|
|
1334
|
+
async function promptForApiUrl(envName, existingUrl) {
|
|
1335
|
+
if (existingUrl) {
|
|
1336
|
+
console.log(chalk_1.default.dim(` Found existing value: ${existingUrl}`));
|
|
1337
|
+
const useExisting = await prompts.confirm({
|
|
1338
|
+
message: ` Use existing ${envName} API URL?`,
|
|
1339
|
+
default: true,
|
|
1340
|
+
});
|
|
1341
|
+
if (useExisting) {
|
|
1342
|
+
return existingUrl;
|
|
1343
|
+
}
|
|
1344
|
+
}
|
|
1345
|
+
return await prompts.input({
|
|
1346
|
+
message: ` ${envName.charAt(0).toUpperCase() + envName.slice(1)} API URL:`,
|
|
1347
|
+
default: '',
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1342
1350
|
/**
|
|
1343
1351
|
* Setup .env.genbox file with segregated app sections
|
|
1344
1352
|
*/
|
package/dist/commands/status.js
CHANGED
|
@@ -165,16 +165,28 @@ exports.statusCommand = new commander_1.Command('status')
|
|
|
165
165
|
}
|
|
166
166
|
}
|
|
167
167
|
else if (status.includes('done')) {
|
|
168
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
168
|
+
// Try to get actual cloud-init completion time from logs first
|
|
169
|
+
let completionTimeFormatted = '';
|
|
170
|
+
// First, try to extract "Setup Complete in Xm Ys" from cloud-init logs
|
|
171
|
+
const setupTime = sshExec(target.ipAddress, keyPath, "sudo grep -oP 'Setup Complete in \\K[0-9]+m [0-9]+s' /var/log/cloud-init-output.log 2>/dev/null | tail -1");
|
|
172
|
+
if (setupTime && setupTime.trim()) {
|
|
173
|
+
completionTimeFormatted = setupTime.trim();
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
// Fallback: Calculate from cloud-init result.json timestamp vs boot time
|
|
177
|
+
// Get boot timestamp and cloud-init finish timestamp
|
|
178
|
+
const timestamps = sshExec(target.ipAddress, keyPath, "echo $(date -d \"$(uptime -s)\" +%s) $(stat -c %Y /run/cloud-init/result.json 2>/dev/null || echo 0)");
|
|
179
|
+
if (timestamps && timestamps.trim()) {
|
|
180
|
+
const [bootTime, finishTime] = timestamps.trim().split(' ').map(Number);
|
|
181
|
+
if (bootTime && finishTime && finishTime > bootTime) {
|
|
182
|
+
const elapsedSecs = finishTime - bootTime;
|
|
183
|
+
const mins = Math.floor(elapsedSecs / 60);
|
|
184
|
+
const secs = elapsedSecs % 60;
|
|
185
|
+
completionTimeFormatted = mins > 0 ? `${mins}m ${secs}s` : `${secs}s`;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
176
188
|
}
|
|
177
|
-
console.log(chalk_1.default.green(`[SUCCESS] Cloud-init completed!${
|
|
189
|
+
console.log(chalk_1.default.green(`[SUCCESS] Cloud-init completed!${completionTimeFormatted ? ` (${completionTimeFormatted})` : ''}`));
|
|
178
190
|
console.log('');
|
|
179
191
|
// Show Database Stats - only if configured
|
|
180
192
|
let dbContainer = '';
|