genbox 1.0.100 → 1.0.102
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 +24 -0
- package/dist/commands/backup.js +410 -0
- package/dist/commands/backups.js +193 -0
- package/dist/commands/create.js +254 -4
- package/dist/commands/extend.js +10 -5
- package/dist/commands/profiles.js +133 -0
- package/dist/commands/restart.js +424 -0
- package/dist/commands/status.js +89 -9
- package/dist/index.js +7 -1
- package/package.json +1 -1
|
@@ -0,0 +1,424 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.restartCommand = void 0;
|
|
40
|
+
const commander_1 = require("commander");
|
|
41
|
+
const chalk_1 = __importDefault(require("chalk"));
|
|
42
|
+
const ora_1 = __importDefault(require("ora"));
|
|
43
|
+
const api_1 = require("../api");
|
|
44
|
+
const genbox_selector_1 = require("../genbox-selector");
|
|
45
|
+
const child_process_1 = require("child_process");
|
|
46
|
+
const os = __importStar(require("os"));
|
|
47
|
+
const path = __importStar(require("path"));
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
function getPrivateSshKey() {
|
|
50
|
+
const home = os.homedir();
|
|
51
|
+
const potentialKeys = [
|
|
52
|
+
path.join(home, '.ssh', 'id_rsa'),
|
|
53
|
+
path.join(home, '.ssh', 'id_ed25519'),
|
|
54
|
+
];
|
|
55
|
+
for (const keyPath of potentialKeys) {
|
|
56
|
+
if (fs.existsSync(keyPath)) {
|
|
57
|
+
return keyPath;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
throw new Error('No SSH private key found in ~/.ssh/');
|
|
61
|
+
}
|
|
62
|
+
function sshExec(ip, keyPath, command, timeoutSecs = 30) {
|
|
63
|
+
const sshOpts = `-i ${keyPath} -o IdentitiesOnly=yes -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o LogLevel=ERROR -o ConnectTimeout=10`;
|
|
64
|
+
try {
|
|
65
|
+
const result = (0, child_process_1.execSync)(`ssh ${sshOpts} dev@${ip} "${command}"`, {
|
|
66
|
+
encoding: 'utf8',
|
|
67
|
+
timeout: timeoutSecs * 1000,
|
|
68
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
69
|
+
});
|
|
70
|
+
return { stdout: result.trim(), success: true };
|
|
71
|
+
}
|
|
72
|
+
catch (error) {
|
|
73
|
+
const output = error.stdout?.toString().trim() || error.stderr?.toString().trim() || '';
|
|
74
|
+
return { stdout: output, success: false };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
exports.restartCommand = new commander_1.Command('restart')
|
|
78
|
+
.description('Restart apps and services in a genbox')
|
|
79
|
+
.argument('[name]', 'Name of the Genbox (optional - will auto-select or prompt)')
|
|
80
|
+
.option('-a, --all', 'Select from all genboxes (not just current project)')
|
|
81
|
+
.option('--pm2', 'Restart only PM2 processes')
|
|
82
|
+
.option('--docker', 'Restart only Docker containers')
|
|
83
|
+
.action(async (name, options) => {
|
|
84
|
+
try {
|
|
85
|
+
// Select genbox
|
|
86
|
+
const { genbox: target, cancelled } = await (0, genbox_selector_1.selectGenbox)(name, {
|
|
87
|
+
all: options.all,
|
|
88
|
+
selectMessage: 'Select a genbox to restart services:',
|
|
89
|
+
});
|
|
90
|
+
if (cancelled) {
|
|
91
|
+
console.log(chalk_1.default.dim('Cancelled.'));
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
if (!target) {
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
if (target.status !== 'running') {
|
|
98
|
+
console.error(chalk_1.default.red(`Error: Genbox '${target.name}' is not running (status: ${target.status})`));
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
if (!target.ipAddress) {
|
|
102
|
+
console.error(chalk_1.default.red(`Error: Genbox '${target.name}' has no IP address`));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
// Get SSH key
|
|
106
|
+
let keyPath;
|
|
107
|
+
try {
|
|
108
|
+
keyPath = getPrivateSshKey();
|
|
109
|
+
}
|
|
110
|
+
catch (error) {
|
|
111
|
+
console.error(chalk_1.default.red(error.message));
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
const restartPm2 = !options.docker || options.pm2;
|
|
115
|
+
const restartDocker = !options.pm2 || options.docker;
|
|
116
|
+
// Get app configurations from genbox
|
|
117
|
+
const appConfigs = target.appConfigs || [];
|
|
118
|
+
const workspace = target.workspace || 'app';
|
|
119
|
+
// Determine PM2 apps: explicitly set as pm2, OR frontend apps without docker config
|
|
120
|
+
const pm2Apps = appConfigs.filter(app => app.runner === 'pm2' ||
|
|
121
|
+
(!app.runner && !app.docker && (app.type === 'frontend' || ['admin', 'web', 'web-new'].includes(app.name))));
|
|
122
|
+
const dockerApps = appConfigs.filter(app => app.runner === 'docker' ||
|
|
123
|
+
(!app.runner && app.docker) ||
|
|
124
|
+
(!app.runner && app.type === 'backend'));
|
|
125
|
+
console.log(chalk_1.default.blue(`Restarting services on ${target.name}...`));
|
|
126
|
+
if (appConfigs.length > 0) {
|
|
127
|
+
console.log(chalk_1.default.dim(` Apps: ${appConfigs.map(a => a.name).join(', ')}`));
|
|
128
|
+
console.log(chalk_1.default.dim(` PM2 apps: ${pm2Apps.length > 0 ? pm2Apps.map(a => a.name).join(', ') : 'none detected'}`));
|
|
129
|
+
console.log(chalk_1.default.dim(` Docker apps: ${dockerApps.length > 0 ? dockerApps.map(a => a.name).join(', ') : 'none detected'}`));
|
|
130
|
+
}
|
|
131
|
+
console.log('');
|
|
132
|
+
// Start/Restart PM2 processes
|
|
133
|
+
if (restartPm2) {
|
|
134
|
+
const pm2Spinner = (0, ora_1.default)('Checking PM2 processes...').start();
|
|
135
|
+
// First check if PM2 has any processes
|
|
136
|
+
const pm2Check = sshExec(target.ipAddress, keyPath, 'source ~/.nvm/nvm.sh 2>/dev/null; pm2 jlist 2>/dev/null || echo "[]"', 15);
|
|
137
|
+
try {
|
|
138
|
+
const processes = JSON.parse(pm2Check.stdout || '[]');
|
|
139
|
+
if (processes.length === 0 && pm2Apps.length > 0) {
|
|
140
|
+
// No processes running but we have PM2 apps configured - start them
|
|
141
|
+
pm2Spinner.text = `Starting ${pm2Apps.length} PM2 app${pm2Apps.length > 1 ? 's' : ''} from config...`;
|
|
142
|
+
let startedCount = 0;
|
|
143
|
+
const startedNames = [];
|
|
144
|
+
for (const app of pm2Apps) {
|
|
145
|
+
// Try to resolve the correct path - check multiple possible locations
|
|
146
|
+
let appPath = app.path.startsWith('/') ? app.path : `~/${app.path}`;
|
|
147
|
+
// Check if path exists, if not try common variations
|
|
148
|
+
const pathCheck = sshExec(target.ipAddress, keyPath, `test -d ${appPath} && echo "found" || (test -d ~/${workspace}/${app.name} && echo "workspace" || (test -d ~/app/${app.name} && echo "app" || echo "notfound"))`, 5);
|
|
149
|
+
if (pathCheck.stdout === 'workspace') {
|
|
150
|
+
appPath = `~/${workspace}/${app.name}`;
|
|
151
|
+
}
|
|
152
|
+
else if (pathCheck.stdout === 'app') {
|
|
153
|
+
appPath = `~/app/${app.name}`;
|
|
154
|
+
}
|
|
155
|
+
else if (pathCheck.stdout === 'notfound') {
|
|
156
|
+
// Try to find the app directory
|
|
157
|
+
const findResult = sshExec(target.ipAddress, keyPath, `find ~ -maxdepth 3 -type d -name "${app.name}" 2>/dev/null | head -1`, 10);
|
|
158
|
+
if (findResult.stdout && findResult.stdout.trim()) {
|
|
159
|
+
appPath = findResult.stdout.trim();
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const startCmd = app.commands?.dev || app.commands?.start || 'npm run dev';
|
|
163
|
+
// For frontend apps with a configured port, add port argument
|
|
164
|
+
// This is needed for frameworks like Next.js that accept -p/--port
|
|
165
|
+
const portArg = app.port ? ` -- -p ${app.port}` : '';
|
|
166
|
+
// Start the app with PM2
|
|
167
|
+
// Parse the start command - if it's "npm run X", use pm2 start npm -- run X
|
|
168
|
+
// If it's just a script name like "dev", treat it as npm run <script>
|
|
169
|
+
let pm2Cmd;
|
|
170
|
+
if (startCmd.startsWith('npm run ')) {
|
|
171
|
+
const script = startCmd.replace('npm run ', '');
|
|
172
|
+
pm2Cmd = `pm2 start npm --name "${app.name}" -- run ${script}${portArg}`;
|
|
173
|
+
}
|
|
174
|
+
else if (startCmd.startsWith('pnpm ')) {
|
|
175
|
+
const script = startCmd.replace('pnpm ', '');
|
|
176
|
+
pm2Cmd = `pm2 start pnpm --name "${app.name}" -- ${script}${portArg}`;
|
|
177
|
+
}
|
|
178
|
+
else if (startCmd.startsWith('yarn ')) {
|
|
179
|
+
const script = startCmd.replace('yarn ', '');
|
|
180
|
+
pm2Cmd = `pm2 start yarn --name "${app.name}" -- ${script}${portArg}`;
|
|
181
|
+
}
|
|
182
|
+
else if (startCmd.startsWith('bun ')) {
|
|
183
|
+
const script = startCmd.replace('bun ', '');
|
|
184
|
+
pm2Cmd = `pm2 start bun --name "${app.name}" -- ${script}${portArg}`;
|
|
185
|
+
}
|
|
186
|
+
else if (/^[a-z0-9:-]+$/.test(startCmd) && !startCmd.includes('/')) {
|
|
187
|
+
// Single word like "dev", "start", "serve" - treat as npm script name
|
|
188
|
+
pm2Cmd = `pm2 start npm --name "${app.name}" -- run ${startCmd}${portArg}`;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
// Assume it's a direct script/file path
|
|
192
|
+
pm2Cmd = `pm2 start "${startCmd}" --name "${app.name}"`;
|
|
193
|
+
}
|
|
194
|
+
pm2Spinner.text = `Starting ${app.name}...`;
|
|
195
|
+
const fullCmd = `source ~/.nvm/nvm.sh 2>/dev/null; cd ${appPath} && ${pm2Cmd} 2>&1`;
|
|
196
|
+
const startResult = sshExec(target.ipAddress, keyPath, fullCmd, 60);
|
|
197
|
+
// Check for success indicators in PM2 output
|
|
198
|
+
const isSuccess = startResult.success ||
|
|
199
|
+
startResult.stdout.includes('Process successfully started') ||
|
|
200
|
+
startResult.stdout.includes('[PM2] Done') ||
|
|
201
|
+
startResult.stdout.includes('online');
|
|
202
|
+
if (isSuccess) {
|
|
203
|
+
startedCount++;
|
|
204
|
+
startedNames.push(app.name);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
if (startedCount > 0) {
|
|
208
|
+
pm2Spinner.succeed(chalk_1.default.green(`Started ${startedCount} PM2 process${startedCount > 1 ? 'es' : ''}`));
|
|
209
|
+
console.log(chalk_1.default.dim(` Processes: ${startedNames.join(', ')}`));
|
|
210
|
+
}
|
|
211
|
+
else {
|
|
212
|
+
pm2Spinner.warn(chalk_1.default.yellow('Failed to start PM2 processes'));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
else if (processes.length === 0) {
|
|
216
|
+
// No processes and no PM2 apps configured - try ecosystem file fallback
|
|
217
|
+
pm2Spinner.text = 'No PM2 processes running, checking for ecosystem file...';
|
|
218
|
+
// Search in common locations
|
|
219
|
+
const ecosystemCheck = sshExec(target.ipAddress, keyPath, 'find ~ -maxdepth 3 -name "ecosystem.config.*" -o -name "pm2.config.js" 2>/dev/null | head -1', 10);
|
|
220
|
+
if (ecosystemCheck.stdout && ecosystemCheck.stdout.trim()) {
|
|
221
|
+
const ecosystemFile = ecosystemCheck.stdout.trim();
|
|
222
|
+
const ecosystemDir = ecosystemFile.substring(0, ecosystemFile.lastIndexOf('/'));
|
|
223
|
+
pm2Spinner.text = `Starting PM2 from ${ecosystemFile.split('/').pop()}...`;
|
|
224
|
+
const startResult = sshExec(target.ipAddress, keyPath, `source ~/.nvm/nvm.sh 2>/dev/null; cd ${ecosystemDir} && pm2 start ${ecosystemFile} 2>&1`, 60);
|
|
225
|
+
if (startResult.success) {
|
|
226
|
+
const newCheck = sshExec(target.ipAddress, keyPath, 'source ~/.nvm/nvm.sh 2>/dev/null; pm2 jlist 2>/dev/null || echo "[]"', 10);
|
|
227
|
+
const newProcesses = JSON.parse(newCheck.stdout || '[]');
|
|
228
|
+
pm2Spinner.succeed(chalk_1.default.green(`Started ${newProcesses.length} PM2 process${newProcesses.length > 1 ? 'es' : ''}`));
|
|
229
|
+
if (newProcesses.length > 0) {
|
|
230
|
+
const names = newProcesses.map((p) => p.name).join(', ');
|
|
231
|
+
console.log(chalk_1.default.dim(` Processes: ${names}`));
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
pm2Spinner.warn(chalk_1.default.yellow('PM2 start completed with warnings'));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
pm2Spinner.info(chalk_1.default.dim('No PM2 processes or ecosystem file found'));
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
else {
|
|
243
|
+
// Processes running - check if we need to reconfigure (e.g., port changes)
|
|
244
|
+
// If we have PM2 apps with port configs, delete and recreate to ensure correct config
|
|
245
|
+
const appsWithPorts = pm2Apps.filter(app => app.port);
|
|
246
|
+
if (appsWithPorts.length > 0) {
|
|
247
|
+
pm2Spinner.text = 'Reconfiguring PM2 processes with correct ports...';
|
|
248
|
+
// Delete all PM2 processes first
|
|
249
|
+
sshExec(target.ipAddress, keyPath, 'source ~/.nvm/nvm.sh 2>/dev/null; pm2 delete all 2>/dev/null', 15);
|
|
250
|
+
// Start them fresh with correct config
|
|
251
|
+
let startedCount = 0;
|
|
252
|
+
const startedNames = [];
|
|
253
|
+
for (const app of pm2Apps) {
|
|
254
|
+
let appPath = app.path.startsWith('/') ? app.path : `~/${app.path}`;
|
|
255
|
+
// Check if path exists, if not try common variations
|
|
256
|
+
const pathCheck = sshExec(target.ipAddress, keyPath, `test -d ${appPath} && echo "found" || (test -d ~/${workspace}/${app.name} && echo "workspace" || (test -d ~/app/${app.name} && echo "app" || echo "notfound"))`, 5);
|
|
257
|
+
if (pathCheck.stdout === 'workspace') {
|
|
258
|
+
appPath = `~/${workspace}/${app.name}`;
|
|
259
|
+
}
|
|
260
|
+
else if (pathCheck.stdout === 'app') {
|
|
261
|
+
appPath = `~/app/${app.name}`;
|
|
262
|
+
}
|
|
263
|
+
else if (pathCheck.stdout === 'notfound') {
|
|
264
|
+
const findResult = sshExec(target.ipAddress, keyPath, `find ~ -maxdepth 3 -type d -name "${app.name}" 2>/dev/null | head -1`, 10);
|
|
265
|
+
if (findResult.stdout && findResult.stdout.trim()) {
|
|
266
|
+
appPath = findResult.stdout.trim();
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
const startCmd = app.commands?.dev || app.commands?.start || 'npm run dev';
|
|
270
|
+
const portArg = app.port ? ` -- -p ${app.port}` : '';
|
|
271
|
+
let pm2Cmd;
|
|
272
|
+
if (startCmd.startsWith('npm run ')) {
|
|
273
|
+
const script = startCmd.replace('npm run ', '');
|
|
274
|
+
pm2Cmd = `pm2 start npm --name "${app.name}" -- run ${script}${portArg}`;
|
|
275
|
+
}
|
|
276
|
+
else if (startCmd.startsWith('pnpm ')) {
|
|
277
|
+
const script = startCmd.replace('pnpm ', '');
|
|
278
|
+
pm2Cmd = `pm2 start pnpm --name "${app.name}" -- ${script}${portArg}`;
|
|
279
|
+
}
|
|
280
|
+
else if (/^[a-z0-9:-]+$/.test(startCmd) && !startCmd.includes('/')) {
|
|
281
|
+
pm2Cmd = `pm2 start npm --name "${app.name}" -- run ${startCmd}${portArg}`;
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
pm2Cmd = `pm2 start "${startCmd}" --name "${app.name}"`;
|
|
285
|
+
}
|
|
286
|
+
pm2Spinner.text = `Starting ${app.name}${app.port ? ` on port ${app.port}` : ''}...`;
|
|
287
|
+
const startResult = sshExec(target.ipAddress, keyPath, `source ~/.nvm/nvm.sh 2>/dev/null; cd ${appPath} && ${pm2Cmd} 2>&1`, 60);
|
|
288
|
+
const isSuccess = startResult.success ||
|
|
289
|
+
startResult.stdout.includes('Process successfully started') ||
|
|
290
|
+
startResult.stdout.includes('[PM2] Done') ||
|
|
291
|
+
startResult.stdout.includes('online');
|
|
292
|
+
if (isSuccess) {
|
|
293
|
+
startedCount++;
|
|
294
|
+
startedNames.push(app.name);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (startedCount > 0) {
|
|
298
|
+
pm2Spinner.succeed(chalk_1.default.green(`Restarted ${startedCount} PM2 process${startedCount > 1 ? 'es' : ''} with correct ports`));
|
|
299
|
+
console.log(chalk_1.default.dim(` Processes: ${startedNames.join(', ')}`));
|
|
300
|
+
}
|
|
301
|
+
else {
|
|
302
|
+
pm2Spinner.warn(chalk_1.default.yellow('Failed to restart PM2 processes'));
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
else {
|
|
306
|
+
// No port configs, simple restart is fine
|
|
307
|
+
pm2Spinner.text = 'Restarting PM2 processes...';
|
|
308
|
+
const pm2Result = sshExec(target.ipAddress, keyPath, 'source ~/.nvm/nvm.sh 2>/dev/null; pm2 restart all 2>&1', 30);
|
|
309
|
+
if (pm2Result.success) {
|
|
310
|
+
pm2Spinner.succeed(chalk_1.default.green(`Restarted ${processes.length} PM2 process${processes.length > 1 ? 'es' : ''}`));
|
|
311
|
+
const names = processes.map((p) => p.name).join(', ');
|
|
312
|
+
console.log(chalk_1.default.dim(` Processes: ${names}`));
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
pm2Spinner.warn(chalk_1.default.yellow('PM2 restart completed with warnings'));
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
catch {
|
|
321
|
+
pm2Spinner.info(chalk_1.default.dim('No PM2 processes found'));
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
// Start/Restart Docker containers
|
|
325
|
+
if (restartDocker) {
|
|
326
|
+
const dockerSpinner = (0, ora_1.default)('Checking Docker containers...').start();
|
|
327
|
+
// Get list of running containers
|
|
328
|
+
const dockerList = sshExec(target.ipAddress, keyPath, 'docker ps --format "{{.Names}}" 2>/dev/null', 15);
|
|
329
|
+
let containers = dockerList.stdout.split('\n').filter(c => c.trim());
|
|
330
|
+
if (containers.length === 0) {
|
|
331
|
+
// No containers running - find docker-compose file
|
|
332
|
+
dockerSpinner.text = 'No containers running, looking for docker-compose...';
|
|
333
|
+
// First check if we have docker apps configured with known paths
|
|
334
|
+
let composeDir = '';
|
|
335
|
+
if (dockerApps.length > 0) {
|
|
336
|
+
// Use the first docker app's path to find docker-compose
|
|
337
|
+
const firstDockerApp = dockerApps[0];
|
|
338
|
+
const appPath = firstDockerApp.path.startsWith('/') ? firstDockerApp.path : `~/${firstDockerApp.path}`;
|
|
339
|
+
// Check for docker-compose in the app's directory
|
|
340
|
+
const composeCheck = sshExec(target.ipAddress, keyPath, `ls ${appPath}/docker-compose.yml ${appPath}/docker-compose.yaml 2>/dev/null | head -1`, 5);
|
|
341
|
+
if (composeCheck.stdout && composeCheck.stdout.trim()) {
|
|
342
|
+
composeDir = appPath;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
// If not found via app config, search common locations
|
|
346
|
+
if (!composeDir) {
|
|
347
|
+
const findCompose = sshExec(target.ipAddress, keyPath, 'find ~ -maxdepth 4 \\( -name "docker-compose.yml" -o -name "docker-compose.yaml" \\) ! -path "*/security/*" 2>/dev/null | head -1', 10);
|
|
348
|
+
if (findCompose.stdout && findCompose.stdout.trim()) {
|
|
349
|
+
const composePath = findCompose.stdout.trim();
|
|
350
|
+
composeDir = composePath.substring(0, composePath.lastIndexOf('/'));
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (composeDir) {
|
|
354
|
+
dockerSpinner.text = `Starting Docker services from ${composeDir.split('/').pop()}...`;
|
|
355
|
+
const startResult = sshExec(target.ipAddress, keyPath, `cd ${composeDir} && (docker compose up -d 2>&1 || docker-compose up -d 2>&1)`, 180);
|
|
356
|
+
// Check what containers are now running
|
|
357
|
+
const newList = sshExec(target.ipAddress, keyPath, 'docker ps --format "{{.Names}}" 2>/dev/null', 15);
|
|
358
|
+
containers = newList.stdout.split('\n').filter(c => c.trim());
|
|
359
|
+
if (containers.length > 0) {
|
|
360
|
+
dockerSpinner.succeed(chalk_1.default.green(`Started ${containers.length} Docker container${containers.length > 1 ? 's' : ''}`));
|
|
361
|
+
console.log(chalk_1.default.dim(` Containers: ${containers.slice(0, 5).join(', ')}${containers.length > 5 ? ` (+${containers.length - 5} more)` : ''}`));
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
dockerSpinner.warn(chalk_1.default.yellow('docker-compose up completed but no containers running'));
|
|
365
|
+
if (startResult.stdout) {
|
|
366
|
+
const lines = startResult.stdout.split('\n').slice(-3);
|
|
367
|
+
lines.forEach(line => {
|
|
368
|
+
if (line.trim())
|
|
369
|
+
console.log(chalk_1.default.dim(` ${line}`));
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
else {
|
|
375
|
+
dockerSpinner.info(chalk_1.default.dim('No Docker containers or docker-compose file found'));
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
// Containers are running - restart them
|
|
380
|
+
dockerSpinner.text = 'Restarting Docker containers...';
|
|
381
|
+
// Find the docker-compose directory for running containers
|
|
382
|
+
const findCompose = sshExec(target.ipAddress, keyPath, 'find ~ -maxdepth 4 \\( -name "docker-compose.yml" -o -name "docker-compose.yaml" \\) ! -path "*/security/*" 2>/dev/null | head -1', 10);
|
|
383
|
+
if (findCompose.stdout && findCompose.stdout.trim()) {
|
|
384
|
+
const composePath = findCompose.stdout.trim();
|
|
385
|
+
const composeDir = composePath.substring(0, composePath.lastIndexOf('/'));
|
|
386
|
+
const composeResult = sshExec(target.ipAddress, keyPath, `cd ${composeDir} && (docker compose restart 2>&1 || docker-compose restart 2>&1)`, 120);
|
|
387
|
+
if (composeResult.success) {
|
|
388
|
+
dockerSpinner.succeed(chalk_1.default.green(`Restarted ${containers.length} Docker container${containers.length > 1 ? 's' : ''} (docker-compose)`));
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
dockerSpinner.warn(chalk_1.default.yellow('Docker restart completed with warnings'));
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
else {
|
|
395
|
+
// Restart containers individually
|
|
396
|
+
const dockerResult = sshExec(target.ipAddress, keyPath, `docker restart ${containers.join(' ')} 2>&1`, 120);
|
|
397
|
+
if (dockerResult.success) {
|
|
398
|
+
dockerSpinner.succeed(chalk_1.default.green(`Restarted ${containers.length} Docker container${containers.length > 1 ? 's' : ''}`));
|
|
399
|
+
}
|
|
400
|
+
else {
|
|
401
|
+
dockerSpinner.warn(chalk_1.default.yellow('Docker restart completed with warnings'));
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
console.log(chalk_1.default.dim(` Containers: ${containers.slice(0, 5).join(', ')}${containers.length > 5 ? ` (+${containers.length - 5} more)` : ''}`));
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
console.log('');
|
|
408
|
+
console.log(chalk_1.default.green('✓ Restart complete'));
|
|
409
|
+
// Show health check tip
|
|
410
|
+
console.log(chalk_1.default.dim(' Run `gb status` to check service health'));
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
if (error.name === 'ExitPromptError' || error.message?.includes('force closed')) {
|
|
414
|
+
console.log('');
|
|
415
|
+
console.log(chalk_1.default.dim('Cancelled.'));
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
if (error instanceof api_1.AuthenticationError) {
|
|
419
|
+
(0, api_1.handleApiError)(error);
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
console.error(chalk_1.default.red(`Error: ${error.message}`));
|
|
423
|
+
}
|
|
424
|
+
});
|
package/dist/commands/status.js
CHANGED
|
@@ -84,6 +84,17 @@ function formatDuration(secs) {
|
|
|
84
84
|
const remainingSecs = secs % 60;
|
|
85
85
|
return `${mins}m ${remainingSecs}s`;
|
|
86
86
|
}
|
|
87
|
+
function renderBar(percent, width = 10) {
|
|
88
|
+
const filled = Math.round((percent / 100) * width);
|
|
89
|
+
const empty = width - filled;
|
|
90
|
+
const bar = '█'.repeat(filled) + '░'.repeat(empty);
|
|
91
|
+
// Color based on usage level
|
|
92
|
+
if (percent >= 90)
|
|
93
|
+
return chalk_1.default.red(bar);
|
|
94
|
+
if (percent >= 70)
|
|
95
|
+
return chalk_1.default.yellow(bar);
|
|
96
|
+
return chalk_1.default.green(bar);
|
|
97
|
+
}
|
|
87
98
|
function getTimingBreakdown(ip, keyPath) {
|
|
88
99
|
const timing = {
|
|
89
100
|
sshReady: null,
|
|
@@ -294,6 +305,7 @@ exports.statusCommand = new commander_1.Command('status')
|
|
|
294
305
|
currentHourEnd: target.currentHourEnd,
|
|
295
306
|
lastActivityAt: target.lastActivityAt,
|
|
296
307
|
autoDestroyOnInactivity: target.autoDestroyOnInactivity ?? true,
|
|
308
|
+
protectedUntil: target.protectedUntil,
|
|
297
309
|
};
|
|
298
310
|
const now = new Date();
|
|
299
311
|
const minutesUntilBilling = billing.currentHourEnd
|
|
@@ -302,22 +314,90 @@ exports.statusCommand = new commander_1.Command('status')
|
|
|
302
314
|
const minutesInactive = billing.lastActivityAt
|
|
303
315
|
? Math.floor((now.getTime() - new Date(billing.lastActivityAt).getTime()) / (60 * 1000))
|
|
304
316
|
: 0;
|
|
317
|
+
// Format time remaining - show hours if > 60 min
|
|
318
|
+
const formatTimeRemaining = (mins) => {
|
|
319
|
+
if (mins >= 60) {
|
|
320
|
+
const hours = Math.floor(mins / 60);
|
|
321
|
+
const remainingMins = mins % 60;
|
|
322
|
+
return remainingMins > 0 ? `${hours}h ${remainingMins}m` : `${hours}h`;
|
|
323
|
+
}
|
|
324
|
+
return `${mins} min`;
|
|
325
|
+
};
|
|
305
326
|
console.log(chalk_1.default.blue('[INFO] === Billing ==='));
|
|
306
327
|
console.log(` Size: ${target.size} (${billing.creditsPerHour} credit${billing.creditsPerHour > 1 ? 's' : ''}/hr)`);
|
|
307
|
-
console.log(` Current hour ends in: ${chalk_1.default.cyan(minutesUntilBilling
|
|
328
|
+
console.log(` Current hour ends in: ${chalk_1.default.cyan(formatTimeRemaining(minutesUntilBilling))}`);
|
|
308
329
|
console.log(` Last activity: ${minutesInactive < 1 ? 'just now' : minutesInactive + ' min ago'}`);
|
|
309
330
|
console.log(` Total: ${billing.totalHoursUsed} hour${billing.totalHoursUsed !== 1 ? 's' : ''}, ${billing.totalCreditsUsed} credit${billing.totalCreditsUsed !== 1 ? 's' : ''}`);
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
331
|
+
// Auto-destroy protection and warning
|
|
332
|
+
if (billing.autoDestroyOnInactivity) {
|
|
333
|
+
// Check if protected from auto-destroy
|
|
334
|
+
if (billing.protectedUntil) {
|
|
335
|
+
const protectedTime = new Date(billing.protectedUntil);
|
|
336
|
+
const minutesUntilProtectionEnds = Math.ceil((protectedTime.getTime() - now.getTime()) / (60 * 1000));
|
|
337
|
+
if (minutesUntilProtectionEnds > 0) {
|
|
338
|
+
const timeStr = minutesUntilProtectionEnds >= 60
|
|
339
|
+
? `${Math.floor(minutesUntilProtectionEnds / 60)}h ${minutesUntilProtectionEnds % 60}m`
|
|
340
|
+
: `${minutesUntilProtectionEnds} min`;
|
|
341
|
+
console.log(chalk_1.default.green(` Protected from auto-destroy for: ${timeStr}`));
|
|
342
|
+
}
|
|
318
343
|
}
|
|
344
|
+
// Auto-destroy warning (only show if NOT protected and inactive)
|
|
345
|
+
if (billing.currentHourEnd) {
|
|
346
|
+
const isProtected = billing.protectedUntil && new Date(billing.protectedUntil).getTime() > now.getTime();
|
|
347
|
+
if (!isProtected) {
|
|
348
|
+
const currentHourEnd = new Date(billing.currentHourEnd);
|
|
349
|
+
const currentHourStart = new Date(currentHourEnd.getTime() - 60 * 60 * 1000);
|
|
350
|
+
const minutesIntoBillingHour = Math.floor((now.getTime() - currentHourStart.getTime()) / (60 * 1000));
|
|
351
|
+
const minutesUntilDestroy = Math.max(0, 58 - minutesIntoBillingHour);
|
|
352
|
+
if (minutesInactive >= 5) {
|
|
353
|
+
console.log(chalk_1.default.yellow(` Auto-destroy in: ${minutesUntilDestroy} min (inactive)`));
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
console.log(chalk_1.default.dim(` Auto-destroy: disabled`));
|
|
319
360
|
}
|
|
320
361
|
console.log('');
|
|
362
|
+
// Show System Stats if available
|
|
363
|
+
const systemStats = target.systemStats;
|
|
364
|
+
if (systemStats && systemStats.updatedAt) {
|
|
365
|
+
const statsAge = Math.floor((now.getTime() - new Date(systemStats.updatedAt).getTime()) / 1000);
|
|
366
|
+
console.log(chalk_1.default.blue('[INFO] === System Stats ==='));
|
|
367
|
+
// Load average
|
|
368
|
+
if (systemStats.loadAvg && systemStats.loadAvg.length >= 3) {
|
|
369
|
+
const [load1, load5, load15] = systemStats.loadAvg;
|
|
370
|
+
console.log(` Load Avg: ${load1.toFixed(2)}, ${load5.toFixed(2)}, ${load15.toFixed(2)}`);
|
|
371
|
+
}
|
|
372
|
+
// Memory
|
|
373
|
+
if (systemStats.memoryUsagePercent !== undefined) {
|
|
374
|
+
const memUsed = systemStats.memoryUsedMb ? `${(systemStats.memoryUsedMb / 1024).toFixed(1)}G` : '?';
|
|
375
|
+
const memTotal = systemStats.memoryTotalMb ? `${(systemStats.memoryTotalMb / 1024).toFixed(1)}G` : '?';
|
|
376
|
+
const memBar = renderBar(systemStats.memoryUsagePercent);
|
|
377
|
+
console.log(` Memory: ${memBar} ${systemStats.memoryUsagePercent}% (${memUsed}/${memTotal})`);
|
|
378
|
+
}
|
|
379
|
+
// Disk
|
|
380
|
+
if (systemStats.diskUsagePercent !== undefined) {
|
|
381
|
+
const diskUsed = systemStats.diskUsedGb !== undefined ? `${systemStats.diskUsedGb}G` : '?';
|
|
382
|
+
const diskTotal = systemStats.diskTotalGb !== undefined ? `${systemStats.diskTotalGb}G` : '?';
|
|
383
|
+
const diskBar = renderBar(systemStats.diskUsagePercent);
|
|
384
|
+
console.log(` Disk: ${diskBar} ${systemStats.diskUsagePercent}% (${diskUsed}/${diskTotal})`);
|
|
385
|
+
}
|
|
386
|
+
// Uptime
|
|
387
|
+
if (systemStats.uptimeSeconds !== undefined) {
|
|
388
|
+
const days = Math.floor(systemStats.uptimeSeconds / 86400);
|
|
389
|
+
const hours = Math.floor((systemStats.uptimeSeconds % 86400) / 3600);
|
|
390
|
+
const mins = Math.floor((systemStats.uptimeSeconds % 3600) / 60);
|
|
391
|
+
const uptimeStr = days > 0 ? `${days}d ${hours}h ${mins}m` : hours > 0 ? `${hours}h ${mins}m` : `${mins}m`;
|
|
392
|
+
console.log(` Uptime: ${uptimeStr}`);
|
|
393
|
+
}
|
|
394
|
+
// Process count
|
|
395
|
+
if (systemStats.processCount !== undefined) {
|
|
396
|
+
console.log(` Processes: ${systemStats.processCount}`);
|
|
397
|
+
}
|
|
398
|
+
console.log(chalk_1.default.dim(` (updated ${statsAge < 60 ? 'just now' : Math.floor(statsAge / 60) + 'm ago'})`));
|
|
399
|
+
console.log('');
|
|
400
|
+
}
|
|
321
401
|
// Show Docker containers status
|
|
322
402
|
const dockerStatus = sshExec(target.ipAddress, keyPath, 'docker ps --format "{{.Names}}\\t{{.Status}}" 2>/dev/null', 10);
|
|
323
403
|
if (dockerStatus && dockerStatus.trim()) {
|
package/dist/index.js
CHANGED
|
@@ -32,6 +32,9 @@ const ssh_setup_1 = require("./commands/ssh-setup");
|
|
|
32
32
|
const rebuild_1 = require("./commands/rebuild");
|
|
33
33
|
const extend_1 = require("./commands/extend");
|
|
34
34
|
const cleanup_ssh_1 = require("./commands/cleanup-ssh");
|
|
35
|
+
const restart_1 = require("./commands/restart");
|
|
36
|
+
const backup_1 = require("./commands/backup");
|
|
37
|
+
const backups_1 = require("./commands/backups");
|
|
35
38
|
program
|
|
36
39
|
.addCommand(init_1.initCommand)
|
|
37
40
|
.addCommand(create_1.createCommand)
|
|
@@ -56,5 +59,8 @@ program
|
|
|
56
59
|
.addCommand(ssh_setup_1.sshSetupCommand)
|
|
57
60
|
.addCommand(rebuild_1.rebuildCommand)
|
|
58
61
|
.addCommand(extend_1.extendCommand)
|
|
59
|
-
.addCommand(cleanup_ssh_1.cleanupSshCommand)
|
|
62
|
+
.addCommand(cleanup_ssh_1.cleanupSshCommand)
|
|
63
|
+
.addCommand(restart_1.restartCommand)
|
|
64
|
+
.addCommand(backup_1.backupCommand)
|
|
65
|
+
.addCommand(backups_1.backupsCommand);
|
|
60
66
|
program.parse(process.argv);
|