genbox 1.0.150 → 1.0.152
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/attach.js +178 -12
- package/package.json +2 -1
package/dist/commands/attach.js
CHANGED
|
@@ -47,6 +47,7 @@ const path = __importStar(require("path"));
|
|
|
47
47
|
const fs = __importStar(require("fs"));
|
|
48
48
|
const select_1 = __importDefault(require("@inquirer/select"));
|
|
49
49
|
const confirm_1 = __importDefault(require("@inquirer/confirm"));
|
|
50
|
+
const pty = __importStar(require("node-pty"));
|
|
50
51
|
function getPrivateSshKey() {
|
|
51
52
|
const home = os.homedir();
|
|
52
53
|
const potentialKeys = [
|
|
@@ -205,12 +206,96 @@ GENBOX_TMUX_EOF`, { encoding: 'utf-8' });
|
|
|
205
206
|
// Ignore errors - config is optional enhancement
|
|
206
207
|
}
|
|
207
208
|
}
|
|
209
|
+
// Patterns for detecting local file paths (macOS/Linux)
|
|
210
|
+
const LOCAL_PATH_PATTERNS = [
|
|
211
|
+
/\/var\/folders\/[^\s'"]+/g, // macOS temp folders (screenshots, etc.)
|
|
212
|
+
/\/private\/var\/folders\/[^\s'"]+/g, // macOS private temp folders
|
|
213
|
+
/\/tmp\/[^\s'"]+/g, // Unix temp
|
|
214
|
+
/\/Users\/[^\s'"]+/g, // macOS home directories
|
|
215
|
+
/\/home\/(?!dev)[^\s'"]+/g, // Linux home (but not /home/dev which is remote)
|
|
216
|
+
/~\/[^\s'"]+/g, // Home directory shorthand
|
|
217
|
+
];
|
|
218
|
+
// Image extensions we should auto-upload
|
|
219
|
+
const IMAGE_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.bmp', '.svg', '.ico'];
|
|
220
|
+
function isImageFile(filePath) {
|
|
221
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
222
|
+
return IMAGE_EXTENSIONS.includes(ext);
|
|
223
|
+
}
|
|
224
|
+
function expandPath(filePath) {
|
|
225
|
+
if (filePath.startsWith('~/')) {
|
|
226
|
+
return path.join(os.homedir(), filePath.slice(2));
|
|
227
|
+
}
|
|
228
|
+
return filePath;
|
|
229
|
+
}
|
|
230
|
+
function uploadFile(localPath, ipAddress, keyPath) {
|
|
231
|
+
const expandedPath = expandPath(localPath);
|
|
232
|
+
// Check if file exists locally
|
|
233
|
+
if (!fs.existsSync(expandedPath)) {
|
|
234
|
+
return { localPath, remotePath: '', success: false, error: 'File not found' };
|
|
235
|
+
}
|
|
236
|
+
const stat = fs.statSync(expandedPath);
|
|
237
|
+
if (stat.isDirectory()) {
|
|
238
|
+
return { localPath, remotePath: '', success: false, error: 'Is a directory' };
|
|
239
|
+
}
|
|
240
|
+
// Generate remote path
|
|
241
|
+
const fileName = path.basename(expandedPath);
|
|
242
|
+
const timestamp = Date.now();
|
|
243
|
+
const remotePath = `/home/dev/uploads/${timestamp}-${fileName}`;
|
|
244
|
+
try {
|
|
245
|
+
// Ensure uploads directory exists
|
|
246
|
+
(0, child_process_1.execSync)(`ssh -i "${keyPath}" -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null dev@${ipAddress} "mkdir -p /home/dev/uploads" 2>/dev/null`, { encoding: 'utf-8', timeout: 10000 });
|
|
247
|
+
// Upload file
|
|
248
|
+
const result = (0, child_process_1.spawnSync)('scp', [
|
|
249
|
+
'-i', keyPath,
|
|
250
|
+
'-o', 'StrictHostKeyChecking=no',
|
|
251
|
+
'-o', 'UserKnownHostsFile=/dev/null',
|
|
252
|
+
expandedPath,
|
|
253
|
+
`dev@${ipAddress}:${remotePath}`
|
|
254
|
+
], { timeout: 60000 });
|
|
255
|
+
if (result.status !== 0) {
|
|
256
|
+
return { localPath, remotePath: '', success: false, error: 'SCP failed' };
|
|
257
|
+
}
|
|
258
|
+
return { localPath, remotePath, success: true };
|
|
259
|
+
}
|
|
260
|
+
catch (error) {
|
|
261
|
+
return { localPath, remotePath: '', success: false, error: error.message };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
function detectAndUploadFiles(input, ipAddress, keyPath) {
|
|
265
|
+
const uploads = [];
|
|
266
|
+
let modifiedInput = input;
|
|
267
|
+
// Find all potential file paths
|
|
268
|
+
const allMatches = [];
|
|
269
|
+
for (const pattern of LOCAL_PATH_PATTERNS) {
|
|
270
|
+
let match;
|
|
271
|
+
const regex = new RegExp(pattern.source, pattern.flags);
|
|
272
|
+
while ((match = regex.exec(input)) !== null) {
|
|
273
|
+
allMatches.push({ path: match[0], index: match.index });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
// Deduplicate and sort by position (reverse to replace from end first)
|
|
277
|
+
const uniquePaths = [...new Set(allMatches.map(m => m.path))];
|
|
278
|
+
for (const localPath of uniquePaths) {
|
|
279
|
+
const expandedPath = expandPath(localPath);
|
|
280
|
+
// Only auto-upload if file exists and is an image
|
|
281
|
+
if (fs.existsSync(expandedPath) && isImageFile(expandedPath)) {
|
|
282
|
+
const result = uploadFile(localPath, ipAddress, keyPath);
|
|
283
|
+
uploads.push(result);
|
|
284
|
+
if (result.success) {
|
|
285
|
+
// Replace all occurrences of this path with remote path
|
|
286
|
+
modifiedInput = modifiedInput.split(localPath).join(result.remotePath);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return { modifiedInput, uploads };
|
|
291
|
+
}
|
|
208
292
|
exports.attachCommand = new commander_1.Command('attach')
|
|
209
293
|
.description('Attach to a Claude session in a Genbox (or create new)')
|
|
210
294
|
.argument('[name]', 'Name of the Genbox (optional - will prompt if not provided)')
|
|
211
295
|
.option('-s, --session <session>', 'Tmux session name to attach to directly')
|
|
212
296
|
.option('-n, --new', 'Create a new Claude session')
|
|
213
297
|
.option('-a, --all', 'Select from all genboxes (not just current project)')
|
|
298
|
+
.option('--no-upload', 'Disable automatic file upload for local paths')
|
|
214
299
|
.action(async (name, options) => {
|
|
215
300
|
try {
|
|
216
301
|
// 1. Select Genbox (interactive if no name provided)
|
|
@@ -240,7 +325,7 @@ exports.attachCommand = new commander_1.Command('attach')
|
|
|
240
325
|
if (!created) {
|
|
241
326
|
return;
|
|
242
327
|
}
|
|
243
|
-
await attachToSession(target.ipAddress, keyPath, sessionName, target.name);
|
|
328
|
+
await attachToSession(target.ipAddress, keyPath, sessionName, target.name, options.upload !== false);
|
|
244
329
|
return;
|
|
245
330
|
}
|
|
246
331
|
// 5. List available tmux sessions
|
|
@@ -258,7 +343,7 @@ exports.attachCommand = new commander_1.Command('attach')
|
|
|
258
343
|
if (!created) {
|
|
259
344
|
return;
|
|
260
345
|
}
|
|
261
|
-
await attachToSession(target.ipAddress, keyPath, sessionName, target.name);
|
|
346
|
+
await attachToSession(target.ipAddress, keyPath, sessionName, target.name, options.upload !== false);
|
|
262
347
|
return;
|
|
263
348
|
}
|
|
264
349
|
// 7. Determine which session to attach to
|
|
@@ -302,11 +387,11 @@ exports.attachCommand = new commander_1.Command('attach')
|
|
|
302
387
|
if (!created) {
|
|
303
388
|
return;
|
|
304
389
|
}
|
|
305
|
-
await attachToSession(target.ipAddress, keyPath, newSessionName, target.name);
|
|
390
|
+
await attachToSession(target.ipAddress, keyPath, newSessionName, target.name, options.upload !== false);
|
|
306
391
|
return;
|
|
307
392
|
}
|
|
308
393
|
// 9. Attach to existing session
|
|
309
|
-
await attachToSession(target.ipAddress, keyPath, sessionName, target.name);
|
|
394
|
+
await attachToSession(target.ipAddress, keyPath, sessionName, target.name, options.upload !== false);
|
|
310
395
|
}
|
|
311
396
|
catch (error) {
|
|
312
397
|
if (error instanceof api_1.AuthenticationError) {
|
|
@@ -316,25 +401,106 @@ exports.attachCommand = new commander_1.Command('attach')
|
|
|
316
401
|
console.error(chalk_1.default.red(`Error: ${error.message}`));
|
|
317
402
|
}
|
|
318
403
|
});
|
|
319
|
-
async function attachToSession(ipAddress, keyPath, sessionName, genboxName) {
|
|
404
|
+
async function attachToSession(ipAddress, keyPath, sessionName, genboxName, autoUpload) {
|
|
320
405
|
console.log(chalk_1.default.dim(`\nAttaching to ${chalk_1.default.bold(sessionName)} on ${chalk_1.default.bold(genboxName)}...`));
|
|
321
|
-
|
|
406
|
+
if (autoUpload) {
|
|
407
|
+
console.log(chalk_1.default.dim('Tip: Local image paths will be auto-uploaded. Detach with Ctrl+b d\n'));
|
|
408
|
+
}
|
|
409
|
+
else {
|
|
410
|
+
console.log(chalk_1.default.dim('Tip: Scroll with mouse wheel, detach with Ctrl+b d\n'));
|
|
411
|
+
}
|
|
412
|
+
// Spawn SSH directly with arguments
|
|
322
413
|
const sshArgs = [
|
|
323
|
-
'-t',
|
|
414
|
+
'-t',
|
|
324
415
|
'-i', keyPath,
|
|
325
416
|
'-o', 'StrictHostKeyChecking=no',
|
|
326
417
|
'-o', 'UserKnownHostsFile=/dev/null',
|
|
327
418
|
`dev@${ipAddress}`,
|
|
328
419
|
`tmux attach -t ${sessionName}`
|
|
329
420
|
];
|
|
330
|
-
|
|
421
|
+
// Create a pseudo-terminal for SSH
|
|
422
|
+
const ptyProcess = pty.spawn('ssh', sshArgs, {
|
|
423
|
+
name: 'xterm-256color',
|
|
424
|
+
cols: process.stdout.columns || 80,
|
|
425
|
+
rows: process.stdout.rows || 24,
|
|
426
|
+
cwd: process.cwd(),
|
|
427
|
+
env: process.env,
|
|
428
|
+
});
|
|
429
|
+
// Handle terminal resize
|
|
430
|
+
const onResize = () => {
|
|
431
|
+
ptyProcess.resize(process.stdout.columns || 80, process.stdout.rows || 24);
|
|
432
|
+
};
|
|
433
|
+
process.stdout.on('resize', onResize);
|
|
434
|
+
// Set raw mode on stdin to capture all input
|
|
435
|
+
if (process.stdin.isTTY) {
|
|
436
|
+
process.stdin.setRawMode(true);
|
|
437
|
+
}
|
|
438
|
+
process.stdin.resume();
|
|
439
|
+
// Buffer for detecting multi-character pastes (file paths)
|
|
440
|
+
let inputBuffer = '';
|
|
441
|
+
let bufferTimeout = null;
|
|
442
|
+
const flushBuffer = () => {
|
|
443
|
+
if (inputBuffer.length === 0)
|
|
444
|
+
return;
|
|
445
|
+
const input = inputBuffer;
|
|
446
|
+
inputBuffer = '';
|
|
447
|
+
if (autoUpload && input.length > 10) {
|
|
448
|
+
// Check if input contains local file paths
|
|
449
|
+
const hasLocalPath = LOCAL_PATH_PATTERNS.some(p => p.test(input));
|
|
450
|
+
if (hasLocalPath) {
|
|
451
|
+
const { modifiedInput, uploads } = detectAndUploadFiles(input, ipAddress, keyPath);
|
|
452
|
+
// Log uploads to stderr (won't interfere with terminal)
|
|
453
|
+
for (const upload of uploads) {
|
|
454
|
+
if (upload.success) {
|
|
455
|
+
process.stderr.write(`\r\n${chalk_1.default.green('↑')} Uploaded: ${chalk_1.default.dim(path.basename(upload.localPath))} → ${chalk_1.default.cyan(upload.remotePath)}\r\n`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
ptyProcess.write(modifiedInput);
|
|
459
|
+
return;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
ptyProcess.write(input);
|
|
463
|
+
};
|
|
464
|
+
// Handle input from user
|
|
465
|
+
process.stdin.on('data', (data) => {
|
|
466
|
+
const str = data.toString();
|
|
467
|
+
// Add to buffer
|
|
468
|
+
inputBuffer += str;
|
|
469
|
+
// Clear existing timeout
|
|
470
|
+
if (bufferTimeout) {
|
|
471
|
+
clearTimeout(bufferTimeout);
|
|
472
|
+
}
|
|
473
|
+
// For single characters (typing), flush immediately
|
|
474
|
+
// For multi-character input (paste), wait briefly to collect full paste
|
|
475
|
+
if (str.length === 1 && inputBuffer.length === 1) {
|
|
476
|
+
flushBuffer();
|
|
477
|
+
}
|
|
478
|
+
else {
|
|
479
|
+
// Wait 50ms to collect full paste
|
|
480
|
+
bufferTimeout = setTimeout(flushBuffer, 50);
|
|
481
|
+
}
|
|
482
|
+
});
|
|
483
|
+
// Output from SSH goes to stdout
|
|
484
|
+
ptyProcess.onData((data) => {
|
|
485
|
+
process.stdout.write(data);
|
|
486
|
+
});
|
|
487
|
+
// Handle exit
|
|
331
488
|
return new Promise((resolve) => {
|
|
332
|
-
|
|
333
|
-
|
|
489
|
+
ptyProcess.onExit(({ exitCode }) => {
|
|
490
|
+
// Cleanup
|
|
491
|
+
process.stdout.removeListener('resize', onResize);
|
|
492
|
+
if (process.stdin.isTTY) {
|
|
493
|
+
process.stdin.setRawMode(false);
|
|
494
|
+
}
|
|
495
|
+
process.stdin.pause();
|
|
496
|
+
if (bufferTimeout) {
|
|
497
|
+
clearTimeout(bufferTimeout);
|
|
498
|
+
}
|
|
499
|
+
if (exitCode === 0) {
|
|
334
500
|
console.log(chalk_1.default.dim('\nDetached from session.'));
|
|
335
501
|
}
|
|
336
|
-
else if (
|
|
337
|
-
console.log(chalk_1.default.dim(`\nConnection closed (code ${
|
|
502
|
+
else if (exitCode !== null) {
|
|
503
|
+
console.log(chalk_1.default.dim(`\nConnection closed (code ${exitCode})`));
|
|
338
504
|
}
|
|
339
505
|
resolve();
|
|
340
506
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "genbox",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.152",
|
|
4
4
|
"description": "Genbox CLI - AI-Powered Development Environments",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"fast-glob": "^3.3.3",
|
|
61
61
|
"inquirer": "^13.0.2",
|
|
62
62
|
"js-yaml": "^4.1.1",
|
|
63
|
+
"node-pty": "^1.1.0",
|
|
63
64
|
"ora": "^9.0.0"
|
|
64
65
|
}
|
|
65
66
|
}
|