genbox 1.0.102 → 1.0.104
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/commands/extend.js +11 -2
- package/dist/commands/init.js +158 -22
- package/dist/commands/list.js +54 -4
- package/dist/commands/status.js +74 -8
- package/package.json +1 -1
package/dist/commands/extend.js
CHANGED
|
@@ -53,9 +53,15 @@ exports.extendCommand = new commander_1.Command('extend')
|
|
|
53
53
|
{ name: '4 hours', value: 4 },
|
|
54
54
|
{ name: '8 hours', value: 8 },
|
|
55
55
|
{ name: 'Custom...', value: 0 },
|
|
56
|
+
{ name: 'Remove auto expiry', value: -1 },
|
|
56
57
|
],
|
|
57
58
|
});
|
|
58
|
-
if (choice ===
|
|
59
|
+
if (choice === -1) {
|
|
60
|
+
// Remove auto expiry - set minimal hours and disable auto-destroy
|
|
61
|
+
hours = 1;
|
|
62
|
+
options.autoDestroy = false; // This will set disableAutoDestroy = true
|
|
63
|
+
}
|
|
64
|
+
else if (choice === 0) {
|
|
59
65
|
const customHours = await (0, input_1.default)({
|
|
60
66
|
message: 'Enter number of hours:',
|
|
61
67
|
validate: (val) => {
|
|
@@ -81,7 +87,10 @@ exports.extendCommand = new commander_1.Command('extend')
|
|
|
81
87
|
else if (options.enableAutoDestroy) {
|
|
82
88
|
disableAutoDestroy = false; // --enable-auto-destroy flag
|
|
83
89
|
}
|
|
84
|
-
const
|
|
90
|
+
const spinnerText = disableAutoDestroy
|
|
91
|
+
? `Removing auto expiry for ${target.name}...`
|
|
92
|
+
: `Extending ${target.name} by ${hours} hour${hours > 1 ? 's' : ''}...`;
|
|
93
|
+
const spinner = (0, ora_1.default)(spinnerText).start();
|
|
85
94
|
const result = await (0, api_1.fetchApi)(`/genboxes/${target._id}/extend`, {
|
|
86
95
|
method: 'POST',
|
|
87
96
|
body: JSON.stringify({
|
package/dist/commands/init.js
CHANGED
|
@@ -333,6 +333,21 @@ function loadDetectedConfig(rootDir) {
|
|
|
333
333
|
return null;
|
|
334
334
|
}
|
|
335
335
|
}
|
|
336
|
+
/**
|
|
337
|
+
* Load existing genbox.yaml config if present
|
|
338
|
+
*/
|
|
339
|
+
function loadExistingConfig(rootDir) {
|
|
340
|
+
const configPath = path_1.default.join(rootDir, CONFIG_FILENAME);
|
|
341
|
+
if (!fs_1.default.existsSync(configPath))
|
|
342
|
+
return null;
|
|
343
|
+
try {
|
|
344
|
+
const content = fs_1.default.readFileSync(configPath, 'utf8');
|
|
345
|
+
return yaml.load(content);
|
|
346
|
+
}
|
|
347
|
+
catch {
|
|
348
|
+
return null;
|
|
349
|
+
}
|
|
350
|
+
}
|
|
336
351
|
/**
|
|
337
352
|
* Save detected config to .genbox/detected.yaml
|
|
338
353
|
*/
|
|
@@ -922,7 +937,8 @@ async function selectScripts(detected) {
|
|
|
922
937
|
/**
|
|
923
938
|
* Combined environment and service URL configuration
|
|
924
939
|
*/
|
|
925
|
-
async function setupEnvironmentsAndServiceUrls(detected,
|
|
940
|
+
async function setupEnvironmentsAndServiceUrls(detected, existingEnvData) {
|
|
941
|
+
const existingEnvValues = existingEnvData.values;
|
|
926
942
|
const envVars = {};
|
|
927
943
|
let environments = {};
|
|
928
944
|
console.log('');
|
|
@@ -998,8 +1014,8 @@ async function setupEnvironmentsAndServiceUrls(detected, existingEnvValues) {
|
|
|
998
1014
|
}
|
|
999
1015
|
}
|
|
1000
1016
|
}
|
|
1001
|
-
// Service URL detection and mapping
|
|
1002
|
-
const serviceUrlMappings = await setupServiceUrls(detected, environments,
|
|
1017
|
+
// Service URL detection and mapping (pass existing mappings for retention option)
|
|
1018
|
+
const serviceUrlMappings = await setupServiceUrls(detected, environments, existingEnvData);
|
|
1003
1019
|
// Always add LOCAL_API_URL - get port from detected api app
|
|
1004
1020
|
const apiApp = detected.apps?.['api'];
|
|
1005
1021
|
const apiPort = apiApp?.port || 3050;
|
|
@@ -1013,29 +1029,64 @@ async function setupEnvironmentsAndServiceUrls(detected, existingEnvValues) {
|
|
|
1013
1029
|
/**
|
|
1014
1030
|
* Setup service URL mappings for frontend apps
|
|
1015
1031
|
*/
|
|
1016
|
-
async function setupServiceUrls(detected, environments,
|
|
1032
|
+
async function setupServiceUrls(detected, environments, existingEnvData) {
|
|
1033
|
+
const existingMappings = existingEnvData.serviceUrlMappings;
|
|
1034
|
+
const mappings = [];
|
|
1035
|
+
// Check for existing service URL mappings first
|
|
1036
|
+
if (existingMappings.length > 0) {
|
|
1037
|
+
console.log('');
|
|
1038
|
+
console.log(chalk_1.default.blue('=== Existing Service URL Mappings ==='));
|
|
1039
|
+
console.log(chalk_1.default.dim('Found service URL mappings in existing .env.genbox:'));
|
|
1040
|
+
console.log('');
|
|
1041
|
+
for (const mapping of existingMappings) {
|
|
1042
|
+
console.log(` ${chalk_1.default.cyan(mapping.localUrl)}`);
|
|
1043
|
+
if (mapping.remoteUrl && mapping.remoteEnv) {
|
|
1044
|
+
console.log(chalk_1.default.dim(` → ${mapping.remoteEnv}: ${mapping.remoteUrl}`));
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
console.log('');
|
|
1048
|
+
// Let user select which existing mappings to retain
|
|
1049
|
+
const retainChoices = existingMappings.map(m => ({
|
|
1050
|
+
name: `${m.localUrl}${m.remoteUrl ? ` → ${m.remoteUrl}` : ''}`,
|
|
1051
|
+
value: m.localUrl,
|
|
1052
|
+
checked: true,
|
|
1053
|
+
}));
|
|
1054
|
+
const retainedUrls = await prompts.checkbox({
|
|
1055
|
+
message: 'Select existing service URL mappings to retain:',
|
|
1056
|
+
choices: retainChoices,
|
|
1057
|
+
});
|
|
1058
|
+
// Add retained mappings
|
|
1059
|
+
for (const mapping of existingMappings) {
|
|
1060
|
+
if (retainedUrls.includes(mapping.localUrl)) {
|
|
1061
|
+
mappings.push(mapping);
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1017
1065
|
const frontendApps = Object.entries(detected.apps)
|
|
1018
1066
|
.filter(([, app]) => app.type === 'frontend')
|
|
1019
1067
|
.map(([name]) => name);
|
|
1020
1068
|
if (frontendApps.length === 0) {
|
|
1021
|
-
return
|
|
1069
|
+
return mappings;
|
|
1022
1070
|
}
|
|
1023
1071
|
// Scan env files for service URLs
|
|
1024
1072
|
const serviceUrls = scanEnvFilesForUrls(detected.apps, detected._meta.scanned_root);
|
|
1025
|
-
|
|
1026
|
-
|
|
1073
|
+
// Filter out URLs that are already in retained mappings
|
|
1074
|
+
const retainedLocalUrls = new Set(mappings.map(m => m.localUrl));
|
|
1075
|
+
const newServiceUrls = serviceUrls.filter(svc => !retainedLocalUrls.has(svc.base_url));
|
|
1076
|
+
if (newServiceUrls.length === 0) {
|
|
1077
|
+
return mappings;
|
|
1027
1078
|
}
|
|
1028
1079
|
console.log('');
|
|
1029
1080
|
console.log(chalk_1.default.blue('=== Service URL Configuration ==='));
|
|
1030
1081
|
console.log(chalk_1.default.dim('Detected local service URLs in frontend env files:'));
|
|
1031
1082
|
console.log('');
|
|
1032
|
-
for (const svc of
|
|
1083
|
+
for (const svc of newServiceUrls) {
|
|
1033
1084
|
console.log(` ${chalk_1.default.cyan(svc.base_url)}`);
|
|
1034
1085
|
console.log(chalk_1.default.dim(` Used by: ${svc.used_by.slice(0, 3).join(', ')}${svc.used_by.length > 3 ? ` +${svc.used_by.length - 3} more` : ''}`));
|
|
1035
1086
|
}
|
|
1036
1087
|
console.log('');
|
|
1037
1088
|
// Let user select which to configure
|
|
1038
|
-
const urlChoices =
|
|
1089
|
+
const urlChoices = newServiceUrls.map(svc => ({
|
|
1039
1090
|
name: `${svc.base_url} (${svc.used_by.length} var${svc.used_by.length > 1 ? 's' : ''})`,
|
|
1040
1091
|
value: svc.base_url,
|
|
1041
1092
|
checked: true,
|
|
@@ -1044,8 +1095,7 @@ async function setupServiceUrls(detected, environments, existingEnvValues) {
|
|
|
1044
1095
|
message: 'Select service URLs to configure for remote environments:',
|
|
1045
1096
|
choices: urlChoices,
|
|
1046
1097
|
});
|
|
1047
|
-
const selectedServices =
|
|
1048
|
-
const mappings = [];
|
|
1098
|
+
const selectedServices = newServiceUrls.filter(svc => selectedUrls.includes(svc.base_url));
|
|
1049
1099
|
// Determine primary remote environment
|
|
1050
1100
|
const envNames = Object.keys(environments || {});
|
|
1051
1101
|
const primaryEnv = envNames.includes('staging') ? 'staging' :
|
|
@@ -1100,16 +1150,63 @@ async function setupProfiles(detected, environments) {
|
|
|
1100
1150
|
console.log(chalk_1.default.blue('=== Profile Configuration ==='));
|
|
1101
1151
|
console.log('');
|
|
1102
1152
|
// Generate default profiles
|
|
1103
|
-
const
|
|
1153
|
+
const defaultProfiles = generateDefaultProfiles(detected, environments);
|
|
1154
|
+
const defaultProfileNames = new Set(Object.keys(defaultProfiles));
|
|
1155
|
+
// Load existing profiles from genbox.yaml
|
|
1156
|
+
const existingConfig = loadExistingConfig(detected._meta.scanned_root);
|
|
1157
|
+
const existingProfiles = existingConfig?.profiles || {};
|
|
1158
|
+
// Identify user-created profiles (profiles that don't match auto-generated names)
|
|
1159
|
+
const userCreatedProfiles = {};
|
|
1160
|
+
for (const [name, profile] of Object.entries(existingProfiles)) {
|
|
1161
|
+
if (!defaultProfileNames.has(name)) {
|
|
1162
|
+
userCreatedProfiles[name] = profile;
|
|
1163
|
+
}
|
|
1164
|
+
}
|
|
1165
|
+
// Start with default profiles
|
|
1166
|
+
let profiles = { ...defaultProfiles };
|
|
1167
|
+
// If there are existing profiles (including user-created ones), offer to retain them
|
|
1168
|
+
if (Object.keys(existingProfiles).length > 0) {
|
|
1169
|
+
console.log(chalk_1.default.dim('Found existing profiles in genbox.yaml:'));
|
|
1170
|
+
console.log('');
|
|
1171
|
+
// Display existing profiles with indication if they're user-created
|
|
1172
|
+
for (const [name, profile] of Object.entries(existingProfiles)) {
|
|
1173
|
+
const isUserCreated = !defaultProfileNames.has(name);
|
|
1174
|
+
const label = isUserCreated ? chalk_1.default.yellow(' (user-created)') : chalk_1.default.dim(' (auto-generated)');
|
|
1175
|
+
console.log(` ${chalk_1.default.cyan(name)}${label}`);
|
|
1176
|
+
console.log(chalk_1.default.dim(` ${profile.description || 'No description'}`));
|
|
1177
|
+
console.log(chalk_1.default.dim(` Apps: ${profile.apps?.join(', ') || 'all'}`));
|
|
1178
|
+
console.log('');
|
|
1179
|
+
}
|
|
1180
|
+
// Let user select which existing profiles to retain
|
|
1181
|
+
const retainChoices = Object.entries(existingProfiles).map(([name, profile]) => {
|
|
1182
|
+
const isUserCreated = !defaultProfileNames.has(name);
|
|
1183
|
+
return {
|
|
1184
|
+
name: `${name}${isUserCreated ? ' (user-created)' : ''} - ${profile.description || 'No description'}`,
|
|
1185
|
+
value: name,
|
|
1186
|
+
checked: isUserCreated, // User-created profiles are checked by default
|
|
1187
|
+
};
|
|
1188
|
+
});
|
|
1189
|
+
const retainedNames = await prompts.checkbox({
|
|
1190
|
+
message: 'Select existing profiles to retain:',
|
|
1191
|
+
choices: retainChoices,
|
|
1192
|
+
});
|
|
1193
|
+
// Add retained profiles (they override defaults with same name)
|
|
1194
|
+
for (const name of retainedNames) {
|
|
1195
|
+
profiles[name] = existingProfiles[name];
|
|
1196
|
+
}
|
|
1197
|
+
}
|
|
1104
1198
|
if (Object.keys(profiles).length === 0) {
|
|
1105
1199
|
console.log(chalk_1.default.dim(' No profiles generated (no runnable apps detected).'));
|
|
1106
1200
|
return {};
|
|
1107
1201
|
}
|
|
1108
|
-
// Display profiles
|
|
1109
|
-
console.log(
|
|
1202
|
+
// Display final profiles
|
|
1203
|
+
console.log('');
|
|
1204
|
+
console.log(chalk_1.default.dim('Final profiles:'));
|
|
1110
1205
|
console.log('');
|
|
1111
1206
|
for (const [name, profile] of Object.entries(profiles)) {
|
|
1112
|
-
|
|
1207
|
+
const isRetained = existingProfiles[name] && !defaultProfileNames.has(name);
|
|
1208
|
+
const label = isRetained ? chalk_1.default.yellow(' (retained)') : '';
|
|
1209
|
+
console.log(` ${chalk_1.default.cyan(name)}${label}`);
|
|
1113
1210
|
console.log(chalk_1.default.dim(` ${profile.description || 'No description'}`));
|
|
1114
1211
|
console.log(chalk_1.default.dim(` Apps: ${profile.apps?.join(', ') || 'all'}`));
|
|
1115
1212
|
console.log(chalk_1.default.dim(` Size: ${profile.size || 'default'}`));
|
|
@@ -1968,11 +2065,17 @@ function httpsToSsh(url) {
|
|
|
1968
2065
|
}
|
|
1969
2066
|
function readExistingEnvGenbox() {
|
|
1970
2067
|
const envPath = path_1.default.join(process.cwd(), ENV_FILENAME);
|
|
1971
|
-
const
|
|
2068
|
+
const result = {
|
|
2069
|
+
values: {},
|
|
2070
|
+
serviceUrlMappings: [],
|
|
2071
|
+
rawContent: '',
|
|
2072
|
+
};
|
|
1972
2073
|
if (!fs_1.default.existsSync(envPath))
|
|
1973
|
-
return
|
|
2074
|
+
return result;
|
|
1974
2075
|
try {
|
|
1975
2076
|
const content = fs_1.default.readFileSync(envPath, 'utf8');
|
|
2077
|
+
result.rawContent = content;
|
|
2078
|
+
// Parse all key-value pairs
|
|
1976
2079
|
for (const line of content.split('\n')) {
|
|
1977
2080
|
const trimmed = line.trim();
|
|
1978
2081
|
if (!trimmed || trimmed.startsWith('#'))
|
|
@@ -1983,12 +2086,44 @@ function readExistingEnvGenbox() {
|
|
|
1983
2086
|
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
1984
2087
|
value = value.slice(1, -1);
|
|
1985
2088
|
}
|
|
1986
|
-
values[match[1]] = value;
|
|
2089
|
+
result.values[match[1]] = value;
|
|
2090
|
+
}
|
|
2091
|
+
}
|
|
2092
|
+
// Parse service URL mappings from the "# Service URL Configuration" section
|
|
2093
|
+
// Pattern: LOCAL_<name>_URL=<url> and <ENV>_<name>_URL=<url>
|
|
2094
|
+
const localUrlEntries = {};
|
|
2095
|
+
const remoteUrlEntries = {};
|
|
2096
|
+
for (const [key, value] of Object.entries(result.values)) {
|
|
2097
|
+
// Match LOCAL_*_URL patterns
|
|
2098
|
+
const localMatch = key.match(/^LOCAL_(.+)_URL$/);
|
|
2099
|
+
if (localMatch && value.startsWith('http')) {
|
|
2100
|
+
localUrlEntries[localMatch[1]] = value;
|
|
2101
|
+
}
|
|
2102
|
+
// Match STAGING_*_URL or PRODUCTION_*_URL patterns (for service URLs)
|
|
2103
|
+
const remoteMatch = key.match(/^(STAGING|PRODUCTION)_(.+)_URL$/);
|
|
2104
|
+
if (remoteMatch && value.startsWith('http')) {
|
|
2105
|
+
const envName = remoteMatch[1].toLowerCase();
|
|
2106
|
+
const varName = remoteMatch[2];
|
|
2107
|
+
// Skip environment-level API URLs like STAGING_API_URL (no PORT_ prefix)
|
|
2108
|
+
if (varName !== 'API' && varName !== 'MONGODB') {
|
|
2109
|
+
remoteUrlEntries[varName] = { url: value, env: envName };
|
|
2110
|
+
}
|
|
1987
2111
|
}
|
|
1988
2112
|
}
|
|
2113
|
+
// Build service URL mappings from local entries
|
|
2114
|
+
for (const [varName, localUrl] of Object.entries(localUrlEntries)) {
|
|
2115
|
+
const remote = remoteUrlEntries[varName];
|
|
2116
|
+
result.serviceUrlMappings.push({
|
|
2117
|
+
varName: `${varName}_URL`,
|
|
2118
|
+
localUrl,
|
|
2119
|
+
remoteUrl: remote?.url,
|
|
2120
|
+
remoteEnv: remote?.env,
|
|
2121
|
+
description: `Service at ${localUrl}`,
|
|
2122
|
+
});
|
|
2123
|
+
}
|
|
1989
2124
|
}
|
|
1990
2125
|
catch { }
|
|
1991
|
-
return
|
|
2126
|
+
return result;
|
|
1992
2127
|
}
|
|
1993
2128
|
// =============================================================================
|
|
1994
2129
|
// Main Command
|
|
@@ -2017,8 +2152,9 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
2017
2152
|
return;
|
|
2018
2153
|
}
|
|
2019
2154
|
console.log(chalk_1.default.blue('\nInitializing Genbox...\n'));
|
|
2020
|
-
// Read existing .env.genbox values
|
|
2021
|
-
const
|
|
2155
|
+
// Read existing .env.genbox values and service URL mappings
|
|
2156
|
+
const existingEnvData = readExistingEnvGenbox();
|
|
2157
|
+
const existingEnvValues = existingEnvData.values;
|
|
2022
2158
|
// =========================================
|
|
2023
2159
|
// PHASE 1: Scan or Load
|
|
2024
2160
|
// =========================================
|
|
@@ -2126,7 +2262,7 @@ exports.initCommand = new commander_1.Command('init')
|
|
|
2126
2262
|
// =========================================
|
|
2127
2263
|
// PHASE 5: Environments & Service URLs
|
|
2128
2264
|
// =========================================
|
|
2129
|
-
const { environments, serviceUrlMappings, envVars: envEnvVars } = await setupEnvironmentsAndServiceUrls(detected,
|
|
2265
|
+
const { environments, serviceUrlMappings, envVars: envEnvVars } = await setupEnvironmentsAndServiceUrls(detected, existingEnvData);
|
|
2130
2266
|
// =========================================
|
|
2131
2267
|
// PHASE 6: Profiles
|
|
2132
2268
|
// =========================================
|
package/dist/commands/list.js
CHANGED
|
@@ -49,14 +49,64 @@ exports.listCommand = new commander_1.Command('list')
|
|
|
49
49
|
}
|
|
50
50
|
return;
|
|
51
51
|
}
|
|
52
|
+
// Calculate column widths
|
|
53
|
+
const nameWidth = Math.max(12, ...genboxes.map(g => g.name.length + (options.all && g.project ? g.project.length + 3 : 0)));
|
|
54
|
+
const statusWidth = 12;
|
|
55
|
+
const ipWidth = 16;
|
|
56
|
+
const sizeWidth = 8;
|
|
52
57
|
genboxes.forEach((genbox) => {
|
|
53
58
|
const statusColor = genbox.status === 'running' ? chalk_1.default.green :
|
|
54
59
|
genbox.status === 'terminated' ? chalk_1.default.red : chalk_1.default.yellow;
|
|
55
60
|
// Show project info when listing all
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
const projectSuffix = options.all && genbox.project ? ` [${genbox.project}]` : '';
|
|
62
|
+
const nameWithProject = genbox.name + projectSuffix;
|
|
63
|
+
// Format end hour as relative time
|
|
64
|
+
let endHourInfo = '';
|
|
65
|
+
if (genbox.currentHourEnd && genbox.status === 'running') {
|
|
66
|
+
const endTime = new Date(genbox.currentHourEnd);
|
|
67
|
+
const now = new Date();
|
|
68
|
+
const diffMs = endTime.getTime() - now.getTime();
|
|
69
|
+
if (diffMs > 0) {
|
|
70
|
+
const totalSeconds = Math.floor(diffMs / 1000);
|
|
71
|
+
const mins = Math.floor(totalSeconds / 60);
|
|
72
|
+
const secs = totalSeconds % 60;
|
|
73
|
+
if (mins > 0) {
|
|
74
|
+
endHourInfo = ` hour ends in ${mins}m:${secs.toString().padStart(2, '0')}s`;
|
|
75
|
+
}
|
|
76
|
+
else {
|
|
77
|
+
endHourInfo = ` hour ends in ${secs}s`;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
endHourInfo = ' hour renewing...';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
// Show auto-destroy status
|
|
85
|
+
let protectedInfo = '';
|
|
86
|
+
if (genbox.autoDestroyOnInactivity === false) {
|
|
87
|
+
protectedInfo = chalk_1.default.yellow(' [protected]');
|
|
88
|
+
}
|
|
89
|
+
else if (genbox.protectedUntil) {
|
|
90
|
+
const protectedUntil = new Date(genbox.protectedUntil);
|
|
91
|
+
const now = new Date();
|
|
92
|
+
const diffMs = protectedUntil.getTime() - now.getTime();
|
|
93
|
+
const hoursRemaining = Math.ceil(diffMs / (1000 * 60 * 60));
|
|
94
|
+
if (hoursRemaining > 0) {
|
|
95
|
+
protectedInfo = chalk_1.default.cyan(` [extended ${hoursRemaining}h]`);
|
|
96
|
+
}
|
|
97
|
+
else {
|
|
98
|
+
protectedInfo = chalk_1.default.dim(' [auto-destroy]');
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
protectedInfo = chalk_1.default.dim(' [auto-destroy]');
|
|
103
|
+
}
|
|
104
|
+
const namePart = chalk_1.default.cyan(nameWithProject.padEnd(nameWidth));
|
|
105
|
+
const statusPart = statusColor(genbox.status.padEnd(statusWidth));
|
|
106
|
+
const ipPart = chalk_1.default.dim((genbox.ipAddress || 'Pending IP').padEnd(ipWidth));
|
|
107
|
+
const sizePart = `(${genbox.size})`.padEnd(sizeWidth);
|
|
108
|
+
const extraInfo = chalk_1.default.dim(endHourInfo) + protectedInfo;
|
|
109
|
+
console.log(`${namePart} ${statusPart} ${ipPart} ${sizePart}${extraInfo}`);
|
|
60
110
|
});
|
|
61
111
|
console.log(chalk_1.default.dim('────────────────────────────────────────────────────'));
|
|
62
112
|
// Show hint if in project context and not showing all
|
package/dist/commands/status.js
CHANGED
|
@@ -400,19 +400,81 @@ exports.statusCommand = new commander_1.Command('status')
|
|
|
400
400
|
}
|
|
401
401
|
// Show Docker containers status
|
|
402
402
|
const dockerStatus = sshExec(target.ipAddress, keyPath, 'docker ps --format "{{.Names}}\\t{{.Status}}" 2>/dev/null', 10);
|
|
403
|
-
|
|
403
|
+
const hasDockerServices = dockerStatus && dockerStatus.trim();
|
|
404
|
+
if (hasDockerServices) {
|
|
404
405
|
console.log(chalk_1.default.blue('[INFO] === Docker Services ==='));
|
|
405
406
|
console.log('NAMES\tSTATUS');
|
|
406
407
|
console.log(dockerStatus);
|
|
407
408
|
console.log('');
|
|
408
409
|
}
|
|
409
|
-
// Show PM2 processes
|
|
410
|
+
// Show PM2 processes (only if there are actual apps)
|
|
410
411
|
const pm2Status = sshExec(target.ipAddress, keyPath, 'source ~/.nvm/nvm.sh 2>/dev/null; pm2 list 2>/dev/null || echo ""', 10);
|
|
411
|
-
|
|
412
|
+
// Check for actual PM2 apps - empty tables don't contain status keywords
|
|
413
|
+
const hasPm2Apps = pm2Status && pm2Status.trim() &&
|
|
414
|
+
!pm2Status.includes('No process') &&
|
|
415
|
+
(pm2Status.includes('online') || pm2Status.includes('stopped') ||
|
|
416
|
+
pm2Status.includes('errored') || pm2Status.includes('launching'));
|
|
417
|
+
if (hasPm2Apps) {
|
|
412
418
|
console.log(chalk_1.default.blue('[INFO] === PM2 Services ==='));
|
|
413
419
|
console.log(pm2Status);
|
|
414
420
|
console.log('');
|
|
415
421
|
}
|
|
422
|
+
// Warn if no services are running and try to diagnose why
|
|
423
|
+
if (!hasDockerServices && !hasPm2Apps) {
|
|
424
|
+
console.log(chalk_1.default.yellow('[WARN] No Docker or PM2 services are running!'));
|
|
425
|
+
// First, check for saved errors from setup (fast - just read a file)
|
|
426
|
+
const savedErrors = sshExec(target.ipAddress, keyPath, 'cat ~/.genbox-errors 2>/dev/null || echo ""', 10);
|
|
427
|
+
if (savedErrors && savedErrors.trim()) {
|
|
428
|
+
// Found saved errors from setup
|
|
429
|
+
console.log(chalk_1.default.red('[ERROR] Build failed during setup:'));
|
|
430
|
+
console.log(savedErrors);
|
|
431
|
+
console.log('');
|
|
432
|
+
console.log(chalk_1.default.dim(' Fix the error and run: cd ~/goodpass/api && docker compose up -d'));
|
|
433
|
+
}
|
|
434
|
+
else {
|
|
435
|
+
// No saved errors - try docker compose build to diagnose (uses cache, fails fast)
|
|
436
|
+
console.log(chalk_1.default.dim(' Checking for build errors...'));
|
|
437
|
+
console.log('');
|
|
438
|
+
const buildResult = sshExec(target.ipAddress, keyPath, 'cd ~/goodpass/api 2>/dev/null && docker compose build 2>&1 | tail -60 || echo "No compose file found"', 120);
|
|
439
|
+
if (buildResult && buildResult.trim()) {
|
|
440
|
+
// Check for common error patterns
|
|
441
|
+
if (buildResult.includes('ERROR') || buildResult.includes('TS2') ||
|
|
442
|
+
buildResult.includes('failed') || buildResult.includes('ELIFECYCLE') ||
|
|
443
|
+
buildResult.includes('exit code') || buildResult.includes('did not complete successfully')) {
|
|
444
|
+
console.log(chalk_1.default.red('[ERROR] Docker build failed:'));
|
|
445
|
+
// Extract just the error portion
|
|
446
|
+
const lines = buildResult.split('\n');
|
|
447
|
+
const errorLines = lines.filter(line => line.includes('ERROR') || line.includes('TS2') || line.includes('TS1') ||
|
|
448
|
+
line.includes('failed') || line.includes('exit code') ||
|
|
449
|
+
line.includes('>') || line.includes('^') ||
|
|
450
|
+
line.match(/^\s*\d+\s*\|/) // Source code lines with line numbers
|
|
451
|
+
).slice(-20);
|
|
452
|
+
if (errorLines.length > 0) {
|
|
453
|
+
console.log(errorLines.join('\n'));
|
|
454
|
+
}
|
|
455
|
+
else {
|
|
456
|
+
// Show last 25 lines if no specific error pattern found
|
|
457
|
+
console.log(lines.slice(-25).join('\n'));
|
|
458
|
+
}
|
|
459
|
+
console.log('');
|
|
460
|
+
console.log(chalk_1.default.dim(' Fix the error and run: cd ~/goodpass/api && docker compose up -d'));
|
|
461
|
+
}
|
|
462
|
+
else if (buildResult.includes('Built') || buildResult.includes('FINISHED')) {
|
|
463
|
+
// Build succeeded, services just need to start
|
|
464
|
+
console.log(chalk_1.default.dim(' Build OK. Start services with: cd ~/goodpass/api && docker compose up -d'));
|
|
465
|
+
}
|
|
466
|
+
else {
|
|
467
|
+
console.log(chalk_1.default.dim(buildResult));
|
|
468
|
+
console.log('');
|
|
469
|
+
console.log(chalk_1.default.dim(' Try: cd ~/goodpass/api && docker compose up -d'));
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
else {
|
|
473
|
+
console.log(chalk_1.default.dim(' Try: cd ~/goodpass/api && docker compose up -d'));
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
console.log('');
|
|
477
|
+
}
|
|
416
478
|
// Show URLs if available
|
|
417
479
|
if (target.urls && Object.keys(target.urls).length > 0) {
|
|
418
480
|
console.log(chalk_1.default.blue('[INFO] === Service URLs ==='));
|
|
@@ -786,13 +848,17 @@ exports.statusCommand = new commander_1.Command('status')
|
|
|
786
848
|
console.log(chalk_1.default.dim(pkgError));
|
|
787
849
|
}
|
|
788
850
|
console.log('');
|
|
789
|
-
// Show PM2 processes
|
|
790
|
-
|
|
791
|
-
const
|
|
792
|
-
|
|
851
|
+
// Show PM2 processes (only if there are actual apps)
|
|
852
|
+
const pm2List = sshExec(target.ipAddress, keyPath, "source ~/.nvm/nvm.sh 2>/dev/null; pm2 list 2>/dev/null || echo ''", 10);
|
|
853
|
+
const hasPm2Apps = pm2List && pm2List.trim() &&
|
|
854
|
+
!pm2List.includes('No process') &&
|
|
855
|
+
(pm2List.includes('online') || pm2List.includes('stopped') ||
|
|
856
|
+
pm2List.includes('errored') || pm2List.includes('launching'));
|
|
857
|
+
if (hasPm2Apps) {
|
|
858
|
+
console.log(chalk_1.default.blue('[INFO] === Running Services (PM2) ==='));
|
|
793
859
|
console.log(pm2List);
|
|
860
|
+
console.log('');
|
|
794
861
|
}
|
|
795
|
-
console.log('');
|
|
796
862
|
// Show URLs if available
|
|
797
863
|
if (target.urls && Object.keys(target.urls).length > 0) {
|
|
798
864
|
console.log(chalk_1.default.blue('[INFO] === Service URLs ==='));
|