phattarachai / remote-prompt
A PHPStorm-style remote host panel for the terminal — pick a project's server, open a shell in its deploy path, browse, download and upload files over your own ssh config. One global command for every project.
Requires
- php: ^8.3
- laravel/prompts: ^0.3.13
- symfony/console: ^7.0|^8.0
- symfony/process: ^7.0|^8.0
Requires (Dev)
- laravel/pint: ^1.24
- mockery/mockery: ^1.6
- pestphp/pest: ^3.0|^4.0
- phpstan/phpstan: ^2.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A PHPStorm-style remote host panel for the terminal. Type rh inside a project, pick one of its servers, and open
a shell already in the deploy path, browse files, download a backup or upload a file. Connections come from your own
~/.ssh/config, so keys, ssh-agent, jump hosts and ProxyCommand tunnels work as they do for plain ssh. Built on
Laravel Prompts, but it is a standalone CLI that works in any project, Laravel
or not, from one global install.
~/www/shop-api ❯ rh
~/www/shop-api
┌ Targets Actions Setup ─────────────────────────────┐
│ › qas qas:/var/www/shop qas │
│ uat shop-uat:/var/www/uat-shop uat │
│ prod prod:/var/www/shop ● production │
└──────────────────────────────────────────────────────────┘
Enter on a target opens its shell · Tab switch tab · Esc quit
- Targets per project, committed. A project's targets live in
.rh.jsonat its root, so every checkout, worktree, machine and AI agent knows where it is deployed. A server can serve any number of projects; targets only you need stay in your own config. - Your ssh config is the source of truth. A target only names an ssh alias. Hosts rh creates go to
~/.ssh/config.d/rh.conf, so plainssh,scpandgitknow them too. No passwords are stored anywhere. - One connection per host. rh opens an ssh control master that later calls reuse for ten minutes, so browsing through a slow tunnel (GCP IAP, a jump host) pays the handshake once.
- Production asks first. Uploads, edits and artisan commands on a
productiontarget wait for a yes — and are refused outright without a terminal, so a script or an AI agent can read from production but never write to it.
Installation
composer global require phattarachai/remote-prompt
Make sure Composer's global vendor/bin is on your PATH, then run rh --help.
Getting started
Coming from PHPStorm? Import its Deployment servers — one project, or every project under a folder:
cd ~/www && rh import
Each server's path becomes its root path joined with the mapping's deployment path. Its host is matched to an
existing ~/.ssh/config alias by host name, so tunnels and keys you already set up keep working; unknown hosts get
a new alias in rh.conf. Then check every connection:
rh doctor --all
Or add a target by hand inside a project with rh add: pick an ssh alias, then walk the server's folders from
/var/www — folders named like the project are starred on top. When the app's files belong to another user
(www-data), rh offers it as the target's run_as. Inside a repository the target goes to .rh.json — commit it.
A server you have never connected to: pick New host…. rh writes the alias to ~/.ssh/config.d/rh.conf, tries
your key, and when the server refuses it offers ssh-copy-id — you type the server's password once, into ssh.
Scripts and agents add targets without questions:
rh add --host=shop-prod --path=/var/www/shop-uat [--name=shop-uat] [--env=uat] [--run-as=www-data] \ [--save=project|machine] [--project=~/www/shop] [--force]
The name and environment default to what the interactive flow would pick; a folder that doesn't exist on the
host is refused unless --force.
Commands
| Command | |
|---|---|
rh |
One screen, three tabs: Targets (Enter opens that target's shell), Actions (browse, find, download, upload, compare, .env, log — then which target), Setup (test, add, remove, import, config) |
rh ssh [target[:path]] |
Shell on the server, already in the deploy path |
rh browse [target[:path]] |
Walk folders; view, download, edit or compare a file; find any file below |
rh get <target:path> [local] |
Download a file or folder (default: the current folder) |
rh put <file> [target[:path]] |
Upload a file to the path the project maps it to — diffed first |
rh cat <target:path> |
Read a file in the server's pager — .env never touches your disk |
rh edit <target:path> |
Edit in $EDITOR, see the diff, save back in place |
rh diff <file> [target] |
Compare a local file with its deployed copy |
rh tail [target[:file]] |
tail -F the newest storage/logs/*.log |
rh artisan [target] <command…> |
php artisan in the deploy path, as the target's run_as user |
rh list [--all] [--json] |
This project's targets (and where each comes from), or every project |
rh add / rh remove / rh config |
Manage targets, or edit the machine config file |
rh doctor [--all] |
Connect to every target and check its folder; IAP hosts check gcloud login first; prints the ssh config block for an alias this machine lacks |
A relative path is relative to the target's deploy path: rh get prod:storage/logs/laravel.log. Any ssh alias works
ad hoc with an absolute path: rh get backup-box:/backups/db.sql.gz. An alias that isn't one of the project's
targets counts as production, so writes to it ask first.
On production, rh artisan runs read-only commands (about, route:list, migrate:status, schedule:list,
queue:failed, horizon:status, model:show, db:show, …) as is; anything else asks first, and is refused without a
terminal. With run_as set it runs sudo -u <user> env HOME=/tmp php artisan …, so new log and cache files stay
writable for the app (without a terminal sudo -n, which fails at once rather than waiting for a password).
Configuration
.rh.json — the project's targets, committed
At the root of the repository, so every checkout, worktree and machine — and any AI agent working in the project — knows where it is deployed:
{
"targets": [
{ "name": "qas", "host": "shop-qas", "path": "/var/www/shop", "env": "qas", "run_as": "www-data" },
{ "name": "prod", "host": "shop-prod", "path": "/var/www/shop", "env": "production", "run_as": "www-data" }
],
"hosts": {
"shop-qas": { "gcp_iap": { "instance": "shop-qas-vm", "project": "acme", "zone": "asia-southeast1-b" } },
"shop-prod": { "hostname": "203.0.113.10", "user": "deploy" }
}
}
host is an alias in ~/.ssh/config, which keeps the connection itself (keys, users, proxies) per machine. Use the
same alias names on every machine. hosts is optional: it tells rh doctor how to write the block for an alias a
machine doesn't have yet (gcp_iap rides Identity-Aware Proxy with gcloud's key). It holds no secrets.
Inside a repository, rh add writes here (--save=machine keeps a target to this machine instead).
~/.config/rh/config.json — this machine's projects
For folders without a .rh.json, and for targets only you need. A project's .rh.json targets come first; this
file's targets for the same folder are added after them, unless the names clash:
{
"projects": {
"~/www/shop-api": {
"targets": [
{ "name": "qas", "host": "qas", "path": "/var/www/shop", "env": "qas" },
{ "name": "prod", "host": "prod", "path": "/var/www/shop", "env": "production" }
]
},
"~/www/shop-*": {
"targets": [{ "name": "qas", "host": "qas", "path": "/var/www/shop-web", "env": "qas" }]
}
}
}
env is one of production, uat, qas, staging, dev; a target without one is production. Only production
asks before writes. run_as (optional) is the user rh artisan switches to.
Background calls (listing, reading, checks) run ssh in batch mode, so a host that only takes a password works for
them only while a rh ssh session's shared connection is open — ssh-copy-id <alias> once fixes that for good.
ผู้พัฒนา
พัฒนาและดูแลโดย บริษัท ภัทรชัย อาร์ทิซาน จำกัด (Phattarachai Artisan) บริษัทที่ปรึกษาและพัฒนาเว็บ ที่เรียนรู้และแบ่งปันกับชุมชน Laravel แพ็กเกจนี้แบ่งปันให้ชุมชนนำไปใช้และต่อยอดได้อย่างอิสระ
ดูแพ็กเกจอื่นของเราได้ที่ phattarachai.dev/open-source และติดต่อเราได้ที่ phattarachai.dev