ghostterm 1.1.3 → 1.2.1
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/CHANGELOG.md +11 -1
- package/README.md +13 -4
- package/bin/ghostterm.js +152 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
-
## 1.1
|
|
3
|
+
## 1.2.1 (2026-03-17)
|
|
4
|
+
|
|
5
|
+
### Bug Fixes
|
|
6
|
+
- **Windows background mode** — fixed node-pty crash in detached mode by using `windowsHide` instead of raw detach
|
|
7
|
+
- **Supervisor crash loop** — fixed `--supervisor` arg being caught by "already running" check
|
|
8
|
+
|
|
9
|
+
## 1.2.0 (2026-03-17)
|
|
4
10
|
|
|
5
11
|
### Features
|
|
12
|
+
- **Background mode by default** — `npx ghostterm` now runs in the background automatically, no terminal window
|
|
13
|
+
- **CLI commands** — `npx ghostterm stop`, `npx ghostterm status`, `npx ghostterm logs`
|
|
14
|
+
- **First-run login** — opens browser for Google sign-in in foreground, then auto-restarts as daemon
|
|
15
|
+
- **Duplicate detection** — prevents starting multiple instances
|
|
6
16
|
- **12345 numpad popup** — quick number selection for CLI surveys/prompts (raw keypress without Enter)
|
|
7
17
|
- **Screen button** — renamed from "Shot" for clarity
|
|
8
18
|
- **Paste button shows filename** — after uploading, displays shortened filename instead of generic "Paste"
|
package/README.md
CHANGED
|
@@ -38,13 +38,22 @@ No VPN. No Tailscale. No port forwarding. Just one command.
|
|
|
38
38
|
npx ghostterm
|
|
39
39
|
```
|
|
40
40
|
|
|
41
|
-
First run opens a browser for Google sign-in. After that, it
|
|
41
|
+
First run opens a browser for Google sign-in. After that, it runs in the background automatically — no terminal window.
|
|
42
42
|
|
|
43
|
-
|
|
43
|
+
```
|
|
44
|
+
$ npx ghostterm
|
|
45
|
+
✅ GhostTerm running in background (PID: 12345)
|
|
46
|
+
📱 Open: ghostterm.pages.dev
|
|
47
|
+
🛑 Stop: npx ghostterm stop
|
|
48
|
+
📋 Logs: npx ghostterm logs
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Other commands:
|
|
44
52
|
|
|
45
53
|
```bash
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
npx ghostterm status # check if running
|
|
55
|
+
npx ghostterm stop # stop the background process
|
|
56
|
+
npx ghostterm logs # show recent logs
|
|
48
57
|
```
|
|
49
58
|
|
|
50
59
|
### 2. On your phone
|
package/bin/ghostterm.js
CHANGED
|
@@ -433,8 +433,150 @@ function handleRelayMessage(msg) {
|
|
|
433
433
|
}
|
|
434
434
|
}
|
|
435
435
|
|
|
436
|
+
// ==================== Background Mode ====================
|
|
437
|
+
const PID_FILE = path.join(CRED_DIR, 'ghostterm.pid');
|
|
438
|
+
const LOG_FILE = path.join(CRED_DIR, 'ghostterm.log');
|
|
439
|
+
|
|
440
|
+
function isRunning(pid) {
|
|
441
|
+
try { process.kill(pid, 0); return true; } catch { return false; }
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function handleSubcommand() {
|
|
445
|
+
const arg = process.argv[2];
|
|
446
|
+
|
|
447
|
+
if (arg === 'stop') {
|
|
448
|
+
if (fs.existsSync(PID_FILE)) {
|
|
449
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8'));
|
|
450
|
+
if (isRunning(pid)) {
|
|
451
|
+
process.kill(pid);
|
|
452
|
+
fs.unlinkSync(PID_FILE);
|
|
453
|
+
console.log(` GhostTerm stopped (PID: ${pid})`);
|
|
454
|
+
} else {
|
|
455
|
+
fs.unlinkSync(PID_FILE);
|
|
456
|
+
console.log(' GhostTerm was not running');
|
|
457
|
+
}
|
|
458
|
+
} else {
|
|
459
|
+
console.log(' GhostTerm is not running');
|
|
460
|
+
}
|
|
461
|
+
process.exit(0);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
if (arg === 'status') {
|
|
465
|
+
if (fs.existsSync(PID_FILE)) {
|
|
466
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8'));
|
|
467
|
+
if (isRunning(pid)) {
|
|
468
|
+
console.log(` GhostTerm is running (PID: ${pid})`);
|
|
469
|
+
} else {
|
|
470
|
+
fs.unlinkSync(PID_FILE);
|
|
471
|
+
console.log(' GhostTerm is not running');
|
|
472
|
+
}
|
|
473
|
+
} else {
|
|
474
|
+
console.log(' GhostTerm is not running');
|
|
475
|
+
}
|
|
476
|
+
process.exit(0);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
if (arg === 'logs') {
|
|
480
|
+
if (fs.existsSync(LOG_FILE)) {
|
|
481
|
+
const lines = fs.readFileSync(LOG_FILE, 'utf8').split('\n').slice(-50);
|
|
482
|
+
console.log(lines.join('\n'));
|
|
483
|
+
} else {
|
|
484
|
+
console.log(' No logs found');
|
|
485
|
+
}
|
|
486
|
+
process.exit(0);
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// No subcommand = start in background (unless already daemonized via --daemon or --supervisor)
|
|
490
|
+
if (arg !== '--daemon' && arg !== '--supervisor') {
|
|
491
|
+
// Check if already running
|
|
492
|
+
if (fs.existsSync(PID_FILE)) {
|
|
493
|
+
const pid = parseInt(fs.readFileSync(PID_FILE, 'utf8'));
|
|
494
|
+
if (isRunning(pid)) {
|
|
495
|
+
console.log(` GhostTerm is already running (PID: ${pid})`);
|
|
496
|
+
console.log(' Stop: npx ghostterm stop');
|
|
497
|
+
process.exit(0);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
// First-time login needs foreground (browser opens)
|
|
502
|
+
const needsLogin = !fs.existsSync(CRED_FILE);
|
|
503
|
+
if (needsLogin) {
|
|
504
|
+
// Run in foreground for first login, then restart as daemon
|
|
505
|
+
return 'foreground-first-login';
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// Spawn self as supervisor in background
|
|
509
|
+
const { spawn, execSync } = require('child_process');
|
|
510
|
+
const out = fs.openSync(LOG_FILE, 'a');
|
|
511
|
+
|
|
512
|
+
if (os.platform() === 'win32') {
|
|
513
|
+
// Windows: spawn with hidden window (windowsHide) but NOT detached
|
|
514
|
+
// node-pty requires a console; detached:true removes the console and crashes
|
|
515
|
+
// windowsHide:true creates a hidden console window
|
|
516
|
+
const child = spawn(process.execPath, [__filename, '--supervisor'], {
|
|
517
|
+
detached: true,
|
|
518
|
+
windowsHide: true,
|
|
519
|
+
stdio: ['ignore', out, out],
|
|
520
|
+
env: { ...process.env },
|
|
521
|
+
});
|
|
522
|
+
child.unref();
|
|
523
|
+
fs.writeFileSync(PID_FILE, String(child.pid));
|
|
524
|
+
} else {
|
|
525
|
+
// macOS/Linux: detached works fine
|
|
526
|
+
const child = spawn(process.execPath, [__filename, '--supervisor'], {
|
|
527
|
+
detached: true,
|
|
528
|
+
stdio: ['ignore', out, out],
|
|
529
|
+
env: { ...process.env },
|
|
530
|
+
});
|
|
531
|
+
child.unref();
|
|
532
|
+
fs.writeFileSync(PID_FILE, String(child.pid));
|
|
533
|
+
}
|
|
534
|
+
const pid = fs.existsSync(PID_FILE) ? fs.readFileSync(PID_FILE, 'utf8').trim() : '?';
|
|
535
|
+
console.log('');
|
|
536
|
+
console.log(` ✅ GhostTerm running in background (PID: ${pid})`);
|
|
537
|
+
console.log(' 📱 Open: ghostterm.pages.dev');
|
|
538
|
+
console.log(' 🛑 Stop: npx ghostterm stop');
|
|
539
|
+
console.log(' 📋 Logs: npx ghostterm logs');
|
|
540
|
+
console.log('');
|
|
541
|
+
process.exit(0);
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// --supervisor: watchdog that restarts worker on crash
|
|
545
|
+
if (arg === '--supervisor') {
|
|
546
|
+
fs.writeFileSync(PID_FILE, String(process.pid));
|
|
547
|
+
const { spawn } = require('child_process');
|
|
548
|
+
let restartCount = 0;
|
|
549
|
+
|
|
550
|
+
function spawnWorker() {
|
|
551
|
+
const logFd = fs.openSync(LOG_FILE, 'a');
|
|
552
|
+
const worker = spawn(process.execPath, [__filename, '--daemon'], {
|
|
553
|
+
stdio: ['ignore', logFd, logFd],
|
|
554
|
+
env: { ...process.env },
|
|
555
|
+
});
|
|
556
|
+
worker.on('exit', (code) => {
|
|
557
|
+
if (code === 0) {
|
|
558
|
+
// Clean exit (e.g. from stop command), don't restart
|
|
559
|
+
process.exit(0);
|
|
560
|
+
}
|
|
561
|
+
restartCount++;
|
|
562
|
+
const delay = Math.min(restartCount * 3000, 30000); // 3s, 6s, 9s... max 30s
|
|
563
|
+
console.log(` [supervisor] Worker exited (code ${code}), restarting in ${delay / 1000}s... (restart #${restartCount})`);
|
|
564
|
+
setTimeout(spawnWorker, delay);
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
spawnWorker();
|
|
569
|
+
return;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
// --daemon: write PID file and continue to main()
|
|
573
|
+
return 'daemon';
|
|
574
|
+
}
|
|
575
|
+
|
|
436
576
|
// ==================== Main ====================
|
|
437
577
|
async function main() {
|
|
578
|
+
const mode = handleSubcommand();
|
|
579
|
+
|
|
438
580
|
console.log('');
|
|
439
581
|
console.log(' ╔═══════════════════════════════════╗');
|
|
440
582
|
console.log(' ║ GhostTerm Companion ║');
|
|
@@ -449,6 +591,16 @@ async function main() {
|
|
|
449
591
|
console.log(' Will use pairing code instead');
|
|
450
592
|
}
|
|
451
593
|
|
|
594
|
+
// First login done in foreground — now restart in background
|
|
595
|
+
if (mode === 'foreground-first-login' && googleToken) {
|
|
596
|
+
console.log('');
|
|
597
|
+
console.log(' Login successful! Starting in background...');
|
|
598
|
+
// Re-run ourselves which will take the normal background start path
|
|
599
|
+
const { execSync } = require('child_process');
|
|
600
|
+
execSync(`"${process.execPath}" "${__filename}"`, { stdio: 'inherit' });
|
|
601
|
+
process.exit(0);
|
|
602
|
+
}
|
|
603
|
+
|
|
452
604
|
connectToRelay();
|
|
453
605
|
}
|
|
454
606
|
|