feat(ssh): rework remote instance setup and fix remote installs

Adding an SSH connection now starts from the hosts in the SSH config
instead of a blank command field, ports and install options sit behind
Advanced settings, and each connection reports one of three states with
the failure text and an action that resolves it.

Remote installs no longer touch the root-owned global npm prefix: npm is
pinned to a prefix under $HOME and bun is resolved at its known location,
because an SSH login shell exposes neither on PATH. The opencode CLI is
resolved the same way and handed to the remote server through
OPENCODE_BINARY, and the server is started and stopped through the
resolved binary rather than PATH — the HTTP shutdown route sits behind UI
authentication and never stopped anything.

A managed remote server can also be published to the remote machine's own
network. That requires a UI password, enforced in the form and again in
the SSH manager.
This commit is contained in:
Bohdan Triapitsyn
2026-08-22 16:45:56 +03:00
parent eed317f18d
commit f7a0d1f0f6
16 changed files with 1234 additions and 302 deletions
@@ -407,14 +407,14 @@ export const settingsDict = {
'settings.remoteInstances.page.field.modePlaceholder': 'モードを選択',
'settings.remoteInstances.page.field.modeManaged': '自動起動',
'settings.remoteInstances.page.field.modeExternal': '既に実行中',
'settings.remoteInstances.page.field.preferredRemotePort': '優先リモートポート',
'settings.remoteInstances.page.field.preferredRemotePortHint': 'リモートマシンで使用するポート。空の場合は自動的に選択されます。',
'settings.remoteInstances.page.field.preferredRemotePort': 'リモートマシンのポート',
'settings.remoteInstances.page.field.preferredRemotePortHint': 'OpenChamber がリモートマシンで使うポート。空のままにすると自動で選ばれます。',
'settings.remoteInstances.page.field.keepServerRunning': 'サーバーを実行したままにする',
'settings.remoteInstances.page.field.keepServerRunningHint': '切断後もリモートマシンで OpenChamber を実行し続けます。',
'settings.remoteInstances.page.field.bindHost': 'バインドホスト',
'settings.remoteInstances.page.field.bindHostHint': 'ローカル接続の待受先。LAN アクセスが必要でない限り、127.0.0.1 または localhost を使用してください。',
'settings.remoteInstances.page.field.preferredLocalPort': '優先ローカルポート',
'settings.remoteInstances.page.field.preferredLocalPortHint': 'この接続に使用するローカルポート。空の場合は自動的に選択されます。',
'settings.remoteInstances.page.field.keepServerRunningHint': '切断後もリモートサーバーを動かしたままにします。オフの場合は切断時に停止し、次の接続時に再び起動します。',
'settings.remoteInstances.page.field.bindHost': 'アクセスできる範囲',
'settings.remoteInstances.page.field.bindHostHint': 'このコンピュータの転送アドレスを誰が開けるか。リモートマシン自体は、どちらの場合も SSH トンネル経由でのみ到達できます。',
'settings.remoteInstances.page.field.preferredLocalPort': 'このコンピュータのポート',
'settings.remoteInstances.page.field.preferredLocalPortHint': 'トンネル用にこのコンピュータで開くポート。空のままにすると自動で選ばれます。',
'settings.remoteInstances.page.field.forwardType': '転送タイプ',
'settings.remoteInstances.page.field.localHostPlaceholder': '127.0.0.1',
'settings.remoteInstances.page.field.remoteHostPlaceholder': '127.0.0.1',
@@ -1236,8 +1236,8 @@ export const settingsDict = {
'settings.remoteInstances.page.section.actionsDescription': '接続、再接続、ログ表示、またはこの接続の削除。',
'settings.remoteInstances.page.section.remoteServer': 'リモートマシン上の OpenChamber',
'settings.remoteInstances.page.section.remoteServerDescription': 'SSH 接続後に OpenChamber をどのように実行するか選択します。',
'settings.remoteInstances.page.section.mainTunnel': 'ローカルアクセス',
'settings.remoteInstances.page.section.mainTunnelDescription': 'このリモート OpenChamber サーバーを開くために使用するローカルアドレスを選択します。',
'settings.remoteInstances.page.section.mainTunnel': 'このコンピュータからのアクセス',
'settings.remoteInstances.page.section.mainTunnelDescription': 'OpenChamber はリモートマシン上で動きます。ここで設定するのは、SSH トンネル経由でそこへつながる、このコンピュータ側のアドレスだけです。',
'settings.remoteInstances.page.section.authentication': '認証',
'settings.remoteInstances.page.section.authenticationDescription': 'SSH およびリモート OpenChamber UI のオプションの認証情報。',
'settings.remoteInstances.page.section.portForwards': 'ポート転送',
@@ -1250,8 +1250,6 @@ export const settingsDict = {
'settings.remoteInstances.page.field.installMethod': 'インストール方法',
'settings.remoteInstances.page.field.installMethodHint': 'このアプリがリモートマシンで OpenChamber を起動する際の配置方法。',
'settings.remoteInstances.page.field.selectInstallMethodPlaceholder': 'インストール方法を選択',
'settings.remoteInstances.page.field.installMethodDownloadRelease': 'リリースをダウンロード',
'settings.remoteInstances.page.field.installMethodUploadBundle': 'バンドルをアップロード',
'settings.remoteInstances.page.field.selectBindHostPlaceholder': 'バインドホストを選択',
'settings.remoteInstances.page.field.sshPasswordOptional': 'SSH パスワード(任意)',
'settings.remoteInstances.page.field.sshPasswordPlaceholder': 'SSH パスワードを入力',
@@ -1275,7 +1273,44 @@ export const settingsDict = {
'settings.remoteInstances.page.actions.enableForwardAria': '転送を有効化',
'settings.remoteInstances.page.actions.openLocal': 'ローカルを開く',
'settings.remoteInstances.page.actions.addForward': '転送を追加',
'settings.remoteInstances.page.import.sectionTitle': '保存された SSH ホスト',
'settings.remoteInstances.page.addDialog.description': 'SSH 設定からホストを選ぶか、接続を自分で入力します。',
'settings.remoteInstances.page.addDialog.sourceLabel': '接続の取得元',
'settings.remoteInstances.page.addDialog.tab.saved': 'SSH 設定から',
'settings.remoteInstances.page.addDialog.tab.manual': '自分で入力',
'settings.remoteInstances.page.addDialog.searchPlaceholder': 'ホストを検索',
'settings.remoteInstances.page.addDialog.emptySaved': 'SSH 設定にホストが見つかりません。接続を自分で入力してください。',
'settings.remoteInstances.page.addDialog.searchEmpty': '検索に一致するホストはありません。',
'settings.remoteInstances.page.addDialog.use': '使用',
'settings.remoteInstances.page.state.notConnected': '未接続',
'settings.remoteInstances.page.state.connecting': '接続中',
'settings.remoteInstances.page.state.ready': '接続済み',
'settings.remoteInstances.page.state.problem': '対応が必要',
'settings.remoteInstances.page.section.advanced': '詳細設定',
'settings.remoteInstances.page.section.advancedHint': 'ポート、インストール方法、パスワード、追加の転送。ほとんどの接続は初期値のままで動作します。',
'settings.remoteInstances.page.field.installMethodAuto': '自動',
'settings.remoteInstances.page.error.hint.noRuntime': 'リモートマシンに bun も npm もありません。どちらかをそこにインストールするか、この接続を「すでに実行中」に切り替えてください。',
'settings.remoteInstances.page.error.hint.noOpencode': 'リモートマシンに opencode CLI がインストールされていません。そこにインストールしてから(opencode.ai を参照)、もう一度接続してください。',
'settings.remoteInstances.page.error.action.setUiPassword': 'UI パスワードを設定',
'settings.remoteInstances.page.error.action.pickRandomPort': '別のローカルポートを使う',
'settings.remoteInstances.page.error.action.setRemotePort': 'リモートポートを設定',
'settings.remoteInstances.page.validation.externalPortRequired': '先にリモートポートを指定してください。「すでに実行中」モードでは、サーバーが待ち受けるポートを OpenChamber が知る必要があります。',
'settings.remoteInstances.page.empty.noInstances': 'SSH 接続はまだありません。',
'settings.remoteInstances.page.field.uiPasswordRequired': 'UI パスワード(必須)',
'settings.remoteInstances.page.field.uiPasswordMissingForLan': 'リモートサーバーがそのネットワークから到達可能な間は必須です。',
'settings.remoteInstances.page.field.remoteLanAccess': 'リモート側ネットワークから到達可能',
'settings.remoteInstances.page.field.remoteLanAccessHint': 'リモートマシンのネットワーク上の他の端末が、SSH トンネルなしでこの OpenChamber を直接開けるようにします。UI パスワードが必要です。',
'settings.remoteInstances.page.field.remoteLanAccessWarning': 'そのネットワーク上の誰もがリモートの OpenChamber に到達できます。守るのは下の UI パスワードだけです。',
'settings.remoteInstances.page.validation.remoteLanNeedsPassword': '先に UI パスワードを設定してください。設定しないと、リモートの OpenChamber はそのネットワークの全端末に開かれます。',
'settings.remoteInstances.page.field.bindHostOption.loopback': 'このコンピュータのみ (127.0.0.1)',
'settings.remoteInstances.page.field.bindHostOption.localhost': 'このコンピュータのみ (localhost)',
'settings.remoteInstances.page.field.bindHostOption.lan': 'ネットワーク上のすべての端末 (0.0.0.0)',
'settings.remoteInstances.page.field.sshPasswordHint': 'SSH 鍵ではなくパスワードを求めるホストの場合だけ必要です。',
'settings.remoteInstances.page.field.uiPasswordHintManaged': 'リモートの OpenChamber 画面を保護するパスワード。OpenChamber が起動するサーバーにこれを設定します。',
'settings.remoteInstances.page.field.uiPasswordHintExternal': 'リモートマシンですでに動いている OpenChamber サーバーにサインインするためのパスワード。',
'settings.remoteInstances.page.tunnelPreview.caption': 'この接続の転送:',
'settings.remoteInstances.page.empty.noInstancesWithOneImport': 'SSH 接続はまだありません。SSH 設定から 1 件のホストをインポートできます。',
'settings.remoteInstances.page.empty.noInstancesWithImports': 'SSH 接続はまだありません。SSH 設定から {count} 件のホストをインポートできます。',
'settings.remoteInstances.page.state.loadingInstances': '接続を読み込み中...',
'settings.remoteInstances.page.import.loading': 'SSH ホストを読み込み中...',
'settings.remoteInstances.page.import.noneFound': 'SSH ホストが見つかりません。',
'settings.remoteInstances.page.import.noneAvailable': 'インポート可能な SSH ホストがありません。',