diff --git a/.claude/skills/vibeshell/references/docker-containers.md b/.claude/skills/vibeshell/references/docker-containers.md index 2a82d18..035a9bc 100644 --- a/.claude/skills/vibeshell/references/docker-containers.md +++ b/.claude/skills/vibeshell/references/docker-containers.md @@ -1,6 +1,6 @@ # Docker Containers — docker-containers -Plugin `docker-containers` version `1.4.0`. +Plugin `docker-containers` version `1.5.0`. Inspect and manage containers, images, live resource usage and recent container logs through the remote Docker CLI. @@ -18,6 +18,116 @@ Required permissions: `["remote_exec","local_exec"]`. Session types: `["ssh","lo `describe` returns machine-readable action input schemas. `docs` regenerates the current reference, including imported plugins. `run` reuses the selected session. `--confirm` is only for an action the user has explicitly approved; `--sudo` is opt-in and also needs confirmation. No operation bypasses installation, enablement, permission or input checks. Output is bounded and carries timing/truncation metadata. Local targets require a running GUI-owned local session. +## `running-containers` + +List running containers with full IDs; paused and restarting containers are not shell-ready. + +```sh +vibeshell plugins run docker-containers running-containers --session SESSION_ID --inputs '{}' +``` + +Replace SESSION_ID and supply all fields marked required below. Do not execute placeholder values. Append `--confirm` only after consent for this exact action. + +```json +{ + "allowSudo": true, + "description": "List running containers with full IDs; paused and restarting containers are not shell-ready.", + "elevate": false, + "id": "running-containers", + "inputSchema": { + "additionalProperties": false, + "properties": {}, + "required": [], + "type": "object" + }, + "name": "Running containers", + "output": { + "columns": [ + "ID", + "Name", + "Image", + "State", + "Status", + "Ports" + ], + "delimiter": "\t", + "kind": "table" + }, + "requiresConfirmation": false +} +``` + +## `exited-containers` + +Inspect exited containers separately without starting or restarting them. + +```sh +vibeshell plugins run docker-containers exited-containers --session SESSION_ID --inputs '{}' +``` + +Replace SESSION_ID and supply all fields marked required below. Do not execute placeholder values. Append `--confirm` only after consent for this exact action. + +```json +{ + "allowSudo": true, + "description": "Inspect exited containers separately without starting or restarting them.", + "elevate": false, + "id": "exited-containers", + "inputSchema": { + "additionalProperties": false, + "properties": {}, + "required": [], + "type": "object" + }, + "name": "Exited containers", + "output": { + "columns": [ + "ID", + "Name", + "Image", + "State", + "Status", + "Ports" + ], + "delimiter": "\t", + "kind": "table" + }, + "requiresConfirmation": false +} +``` + +## `container-inventory` + +Read all container states and full IDs as one JSON object per line, not a JSON array. This does not create a container session. + +```sh +vibeshell plugins run docker-containers container-inventory --session SESSION_ID --inputs '{}' +``` + +Replace SESSION_ID and supply all fields marked required below. Do not execute placeholder values. Append `--confirm` only after consent for this exact action. + +```json +{ + "allowSudo": true, + "description": "Read all container states and full IDs as one JSON object per line, not a JSON array. This does not create a container session.", + "elevate": false, + "id": "container-inventory", + "inputSchema": { + "additionalProperties": false, + "properties": {}, + "required": [], + "type": "object" + }, + "name": "Container inventory (JSON Lines)", + "output": { + "columns": [], + "delimiter": "\t", + "kind": "text" + }, + "requiresConfirmation": false +} +``` + ## `containers` List running and stopped containers. @@ -215,7 +325,7 @@ Replace SESSION_ID and supply all fields marked required below. Do not execute p ## `exec-command` -Run one non-interactive shell command inside a container. +Run one non-interactive shell command inside a container. This does not create a persistent container session or change the host session. ```sh vibeshell plugins run docker-containers exec-command --session SESSION_ID --inputs '{}' @@ -226,7 +336,7 @@ Replace SESSION_ID and supply all fields marked required below. Do not execute p ```json { "allowSudo": true, - "description": "Run one non-interactive shell command inside a container.", + "description": "Run one non-interactive shell command inside a container. This does not create a persistent container session or change the host session.", "elevate": false, "id": "exec-command", "inputSchema": { diff --git a/.codex/skills/vibeshell/references/docker-containers.md b/.codex/skills/vibeshell/references/docker-containers.md index 2a82d18..035a9bc 100644 --- a/.codex/skills/vibeshell/references/docker-containers.md +++ b/.codex/skills/vibeshell/references/docker-containers.md @@ -1,6 +1,6 @@ # Docker Containers — docker-containers -Plugin `docker-containers` version `1.4.0`. +Plugin `docker-containers` version `1.5.0`. Inspect and manage containers, images, live resource usage and recent container logs through the remote Docker CLI. @@ -18,6 +18,116 @@ Required permissions: `["remote_exec","local_exec"]`. Session types: `["ssh","lo `describe` returns machine-readable action input schemas. `docs` regenerates the current reference, including imported plugins. `run` reuses the selected session. `--confirm` is only for an action the user has explicitly approved; `--sudo` is opt-in and also needs confirmation. No operation bypasses installation, enablement, permission or input checks. Output is bounded and carries timing/truncation metadata. Local targets require a running GUI-owned local session. +## `running-containers` + +List running containers with full IDs; paused and restarting containers are not shell-ready. + +```sh +vibeshell plugins run docker-containers running-containers --session SESSION_ID --inputs '{}' +``` + +Replace SESSION_ID and supply all fields marked required below. Do not execute placeholder values. Append `--confirm` only after consent for this exact action. + +```json +{ + "allowSudo": true, + "description": "List running containers with full IDs; paused and restarting containers are not shell-ready.", + "elevate": false, + "id": "running-containers", + "inputSchema": { + "additionalProperties": false, + "properties": {}, + "required": [], + "type": "object" + }, + "name": "Running containers", + "output": { + "columns": [ + "ID", + "Name", + "Image", + "State", + "Status", + "Ports" + ], + "delimiter": "\t", + "kind": "table" + }, + "requiresConfirmation": false +} +``` + +## `exited-containers` + +Inspect exited containers separately without starting or restarting them. + +```sh +vibeshell plugins run docker-containers exited-containers --session SESSION_ID --inputs '{}' +``` + +Replace SESSION_ID and supply all fields marked required below. Do not execute placeholder values. Append `--confirm` only after consent for this exact action. + +```json +{ + "allowSudo": true, + "description": "Inspect exited containers separately without starting or restarting them.", + "elevate": false, + "id": "exited-containers", + "inputSchema": { + "additionalProperties": false, + "properties": {}, + "required": [], + "type": "object" + }, + "name": "Exited containers", + "output": { + "columns": [ + "ID", + "Name", + "Image", + "State", + "Status", + "Ports" + ], + "delimiter": "\t", + "kind": "table" + }, + "requiresConfirmation": false +} +``` + +## `container-inventory` + +Read all container states and full IDs as one JSON object per line, not a JSON array. This does not create a container session. + +```sh +vibeshell plugins run docker-containers container-inventory --session SESSION_ID --inputs '{}' +``` + +Replace SESSION_ID and supply all fields marked required below. Do not execute placeholder values. Append `--confirm` only after consent for this exact action. + +```json +{ + "allowSudo": true, + "description": "Read all container states and full IDs as one JSON object per line, not a JSON array. This does not create a container session.", + "elevate": false, + "id": "container-inventory", + "inputSchema": { + "additionalProperties": false, + "properties": {}, + "required": [], + "type": "object" + }, + "name": "Container inventory (JSON Lines)", + "output": { + "columns": [], + "delimiter": "\t", + "kind": "text" + }, + "requiresConfirmation": false +} +``` + ## `containers` List running and stopped containers. @@ -215,7 +325,7 @@ Replace SESSION_ID and supply all fields marked required below. Do not execute p ## `exec-command` -Run one non-interactive shell command inside a container. +Run one non-interactive shell command inside a container. This does not create a persistent container session or change the host session. ```sh vibeshell plugins run docker-containers exec-command --session SESSION_ID --inputs '{}' @@ -226,7 +336,7 @@ Replace SESSION_ID and supply all fields marked required below. Do not execute p ```json { "allowSudo": true, - "description": "Run one non-interactive shell command inside a container.", + "description": "Run one non-interactive shell command inside a container. This does not create a persistent container session or change the host session.", "elevate": false, "id": "exec-command", "inputSchema": { diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2dcdd6c..dcd44f0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,11 +1,10 @@ -name: CI - -on: - pull_request: - branches: [dev, main] - push: - branches: [dev, main] - workflow_dispatch: +name: CI +run-name: Manual CI · ${{ github.ref_name }} + +# Maintainers explicitly run this on the PR head branch or exact release tag. +# PR Target remains automatic; compilation never runs on a push or PR event. +on: + workflow_dispatch: permissions: contents: read @@ -14,9 +13,29 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true -jobs: - check-frontend: - name: Frontend Check +jobs: + status-start: + name: Initialize required CI statuses + runs-on: ubuntu-latest + permissions: + contents: read + actions: read + statuses: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} + persist-credentials: false + - uses: actions/setup-node@v4 + with: + node-version: '22' + - run: node scripts/report-ci-status.mjs start + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + check-frontend: + name: Frontend Check + needs: status-start runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -25,14 +44,16 @@ jobs: node-version: '22' cache: npm - run: npm ci - - run: node scripts/check-release.mjs + - run: node scripts/check-release.mjs + - run: node --test scripts/tests/*.test.mjs - run: npm test - run: npm run build - name: Release helper tests run: python3 -m unittest discover -s scripts/tests -p 'test_*.py' - check-backend: - name: Rust Check (${{ matrix.platform }}) + check-backend: + name: Rust Check (${{ matrix.platform }}) + needs: status-start strategy: fail-fast: false matrix: @@ -66,8 +87,9 @@ jobs: rustup target add aarch64-apple-ios-sim cargo check --locked --manifest-path src-tauri/Cargo.toml --target aarch64-apple-ios-sim - check-clippy: - name: Clippy Lint + check-clippy: + name: Clippy Lint + needs: status-start runs-on: ubuntu-22.04 steps: - uses: actions/checkout@v4 @@ -86,5 +108,26 @@ jobs: rustup toolchain install stable --profile minimal --component clippy rustup default stable - run: cargo clippy --workspace --all-targets --locked -- -D warnings - - name: Isolated OpenSSH compatibility - run: bash scripts/test-ssh-compatibility.sh + - name: Isolated OpenSSH compatibility + run: bash scripts/test-ssh-compatibility.sh + + status-finish: + name: Report verified CI statuses + needs: [status-start, check-frontend, check-backend, check-clippy] + if: always() + runs-on: ubuntu-latest + permissions: + contents: read + actions: read + statuses: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.sha }} + persist-credentials: false + - uses: actions/setup-node@v4 + with: + node-version: '22' + - run: node scripts/report-ci-status.mjs finish + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 5970dc0..1b23ed9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,35 +1,40 @@ -name: Release - -on: - push: - tags: ['v*'] - workflow_dispatch: +name: Release +run-name: Manual release · ${{ inputs.tag }} · publish=${{ inputs.publish }} + +on: + workflow_dispatch: inputs: tag: description: Existing vX.Y.Z tag on main (no automatic version bump) - required: true - type: string + required: true + type: string + publish: + description: Publish after verification (false builds and uploads a draft only) + required: true + type: boolean + default: false permissions: contents: read concurrency: - group: release-${{ inputs.tag || github.ref_name }} + group: release-${{ inputs.tag }} cancel-in-progress: false jobs: prepare: name: Validate release and create draft + if: github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest permissions: contents: write - checks: read + actions: read outputs: tag: ${{ steps.metadata.outputs.tag }} version: ${{ steps.metadata.outputs.version }} sha: ${{ steps.metadata.outputs.sha }} env: - TAG: ${{ inputs.tag || github.ref_name }} + TAG: ${{ inputs.tag }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: - uses: actions/checkout@v4 @@ -44,23 +49,19 @@ jobs: set -euo pipefail [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]] || { echo 'Expected an existing vX.Y.Z tag'; exit 1; } git fetch origin main --tags - SHA=$(git rev-parse --verify "refs/tags/$TAG^{commit}") - git merge-base --is-ancestor "$SHA" origin/main - git checkout --detach "$SHA" + SHA=$(git rev-parse --verify "refs/tags/$TAG^{commit}") + git merge-base --is-ancestor "$SHA" origin/main + # Keep the current gate even when the selected tag predates it. + cp scripts/check-release-ci.mjs "$RUNNER_TEMP/check-release-ci.mjs" + git checkout --detach "$SHA" node scripts/check-release.mjs "$TAG" echo "tag=$TAG" >> "$GITHUB_OUTPUT" echo "version=${TAG#v}" >> "$GITHUB_OUTPUT" echo "sha=$SHA" >> "$GITHUB_OUTPUT" - gh api "repos/$GITHUB_REPOSITORY/commits/$SHA/check-runs?per_page=100" > "$RUNNER_TEMP/checks.json" - node - "$RUNNER_TEMP/checks.json" <<'NODE' - const fs = require('node:fs'); - const checks = JSON.parse(fs.readFileSync(process.argv[2])).check_runs; - for (const name of ['Frontend Check', 'Clippy Lint', 'Rust Check (ubuntu-22.04)', 'Rust Check (windows-latest)', 'Rust Check (macos-latest)']) { - if (!checks.some(c => c.name === name && c.app.slug === 'github-actions' && c.conclusion === 'success')) { - throw new Error(`Required CI has not passed for the tagged commit: ${name}`); - } - } - NODE + gh api "repos/$GITHUB_REPOSITORY/actions/workflows/ci.yml/runs?event=workflow_dispatch&head_sha=$SHA&per_page=100" > "$RUNNER_TEMP/ci-runs.json" + RUN_ID=$(node "$RUNNER_TEMP/check-release-ci.mjs" run "$RUNNER_TEMP/ci-runs.json" "$SHA" "$GITHUB_REPOSITORY") + gh api "repos/$GITHUB_REPOSITORY/actions/runs/$RUN_ID/jobs?filter=latest&per_page=100" > "$RUNNER_TEMP/ci-jobs.json" + node "$RUNNER_TEMP/check-release-ci.mjs" jobs "$RUNNER_TEMP/ci-jobs.json" - uses: actions/setup-node@v4 with: node-version: '22' @@ -212,7 +213,7 @@ jobs: retention-days: 7 publish: - name: Verify all assets and publish + name: Verify assets and finalize draft or publication needs: [prepare, build, source] runs-on: ubuntu-latest permissions: @@ -221,6 +222,7 @@ jobs: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAG: ${{ needs.prepare.outputs.tag }} VERSION: ${{ needs.prepare.outputs.version }} + PUBLISH_RELEASE: ${{ inputs.publish }} HAS_APPLE_SIGNING: ${{ secrets.APPLE_CERTIFICATE != '' || secrets.APPLE_SIGNING_IDENTITY != '' }} HAS_APPLE_NOTARIZATION: ${{ secrets.APPLE_ID != '' && secrets.APPLE_PASSWORD != '' && secrets.APPLE_TEAM_ID != '' }} steps: @@ -246,7 +248,7 @@ jobs: cp LICENSE NOTICE assets/ cp licenses/legacy-MIT.txt assets/legacy-MIT.txt (cd assets && sha256sum ./* > SHA256SUMS.txt) - - name: Publish only after all required jobs succeed + - name: Upload verified assets; publish only when explicitly requested run: | set -euo pipefail gh release view "$TAG" --json isDraft > "$RUNNER_TEMP/release.json" @@ -261,4 +263,10 @@ jobs: printf '\nApple signing and notarization were configured; successful bundling completed before publication.\n' >> "$RUNNER_TEMP/notes.md" fi gh release upload "$TAG" assets/* --clobber - gh release edit "$TAG" --notes-file "$RUNNER_TEMP/notes.md" --draft=false --latest + if [[ "$PUBLISH_RELEASE" == 'true' ]]; then + gh release edit "$TAG" --notes-file "$RUNNER_TEMP/notes.md" --draft=false --latest + echo "Published verified release $TAG." >> "$GITHUB_STEP_SUMMARY" + else + gh release edit "$TAG" --notes-file "$RUNNER_TEMP/notes.md" + echo "Built and uploaded $TAG as a draft. Nothing was published or marked latest." >> "$GITHUB_STEP_SUMMARY" + fi diff --git a/AGENTS.md b/AGENTS.md index 184a9a3..ccf2461 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -5,7 +5,7 @@ VibeShell is a modern SSH/SFTP desktop terminal built with **Tauri 2** (Rust bac ## Branch and release workflow -All feature, fix and documentation PRs target `dev`, the default integration branch. `main` accepts release promotions from the same repository's `dev`; `master` is historical. See CONTRIBUTING.md. Version tags are explicit; ordinary pushes must not auto-bump or publish releases. VibeShell 1.1.0 is GPL-3.0-only; preserve NOTICE and third-party attribution. +All feature, fix and documentation PRs target `dev`, the default integration branch. `main` accepts release promotions from the same repository's `dev`; `master` is historical. See CONTRIBUTING.md. CI compilation and release packaging/publication are **manual-only** (`workflow_dispatch`); neither pushes, PRs nor tags trigger them. PR Target remains automatic and all existing required CI checks remain enforced: a maintainer runs CI on the PR head before merging. Release runs from `main`, requires successful manual CI on the exact tagged commit, and defaults to an unpublished draft unless `publish=true` is explicitly selected. Do not auto-bump versions or dispatch publication without authorization. VibeShell 1.1.0 is GPL-3.0-only; preserve NOTICE and third-party attribution. ## Architecture diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a0927dc..660c6a8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -26,6 +26,7 @@ Use Node.js 22.12+ and current stable Rust, with Tauri's platform prerequisites npm ci cargo run --locked -p vibeshell-plugins --example export_references -- --check node scripts/check-release.mjs +node --test scripts/tests/*.test.mjs npm test npm run build cargo fmt --all -- --check @@ -37,6 +38,18 @@ For SSH transport changes, run `bash scripts/test-ssh-compatibility.sh` with Doc A successful build is not proof of UI correctness or universal server compatibility. For UI changes, exercise focus, keyboard navigation, reduced motion, resize and unsaved-edit handling. Include sanitized screenshots when useful. Never publish real server details, tokens, private keys or session recordings in fixtures or test logs. +## Manual GitHub checks + +Pushing code or opening a PR does **not** compile the project. After reviewing the diff, a maintainer explicitly runs **Actions → CI → Run workflow** on the PR's head branch, or: + +```bash +gh workflow run ci.yml --ref fix/short-description +``` + +All existing required checks remain in branch protection; making them manual does not waive them. A new commit needs a new manual run. Do not use `--admin`, fabricate check results, or remove required checks to merge a pending PR. The small, metadata-only **PR Target** check remains automatic and never checks out contributor code. For a fork PR, use a reviewed same-repository branch with the exact head SHA (or a separate integration PR after resolving conflicts); do not run unreviewed fork code with release secrets. + +GitHub does not count `workflow_dispatch` job checks directly toward PR requirements. The manual workflow first marks the five required commit statuses pending, then reports each actual job's result using the GitHub Actions token. Missing, failed, cancelled, skipped or incomplete evidence never produces success; a superseded run cannot intentionally replace a newer run's results. Only the two status-reporting jobs have `statuses: write`; build/test jobs remain read-only. Each status links to its real run. This reporting is not an override and does not start CI automatically. + ## Plugin and documentation changes The validated manifests in `plugins/builtin/` define the built-in plugin catalog. Each plugin must expose machine-readable actions and current reference documentation. Details belong in `references/.md`, not in the main Skill. @@ -63,4 +76,4 @@ Report exploitable vulnerabilities through the repository's private security rep ## Releases -Version changes go through `dev`, then a tested promotion to `main`. Do not bump a version automatically on every branch push. Only an explicit matching `vX.Y.Z` tag on `main` starts publication. See [RELEASING](docs/RELEASING.md). +Version changes go through `dev`, then a tested promotion to `main`. Pushing a `vX.Y.Z` tag does **not** build or publish anything. Run CI manually on the exact tagged commit, then run Release from `main` with that existing tag. Release defaults to a draft; only an explicit `publish=true` run publishes after every verification/build succeeds. See [RELEASING](docs/RELEASING.md). diff --git a/README.ja.md b/README.ja.md index c841057..2512c1f 100644 --- a/README.ja.md +++ b/README.ja.md @@ -1,76 +1,103 @@
VibeShell

VibeShell

-

あなたのターミナルと Agent を、同じワークスペースに。

-

人とコーディング Agent のためのローカルファーストな SSH/SFTP ワークスペース。操作を可視化し、セッション、ファイル、プラグインをつなぎます。

+

サーバーも、ファイルも、AI との作業も、同じ場所に。

+

自分で使いやすく、Agent と一緒に使っても作業を見失わない SSH ワークスペース。

[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) - [![CI](https://github.com/veithly/vibeshell/actions/workflows/ci.yml/badge.svg?branch=dev)](https://github.com/veithly/vibeshell/actions/workflows/ci.yml) + [![Manual CI](https://github.com/veithly/vibeshell/actions/workflows/ci.yml/badge.svg?branch=dev&event=workflow_dispatch)](https://github.com/veithly/vibeshell/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/veithly/vibeshell)](https://github.com/veithly/vibeshell/releases) [![GPLv3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE) - [ダウンロード](https://github.com/veithly/vibeshell/releases) · [1.1 の変更点](CHANGELOG.md) · [Agent ガイド](skills/vibeshell/SKILL.md) · [開発への参加](CONTRIBUTING.md) + [ダウンロード](https://github.com/veithly/vibeshell/releases) · [Agent / CLI ガイド](skills/vibeshell/SKILL.md) · [変更履歴](CHANGELOG.md) · [開発への参加](CONTRIBUTING.md)
-![ターミナルワークスペース](docs/assets/screenshots/terminal-workspace.png) +![ターミナル、ホストの状態、Agent の操作履歴をまとめたワークスペース](docs/assets/screenshots/tour-collaboration.png) -## SSH の周辺作業を、一つの場所で +*実際の VibeShell コンポーネントに、架空の Northstar プロジェクトのデータを入れた画面です。隔離したブラウザで表示しており、実サーバー接続、認証情報の読み出し、モデル呼び出し、サービス再起動は行っていません。Agent の会話や実行結果も説明用の例で、実際の Agent 実行の記録ではありません。[再現方法](scripts/readme-demo/README.md)。* -暗号化通信、認証、転送、リモート実行は SSH 自体の機能です。VibeShell はプロトコルを置き換えたり、ネットワーク速度の向上を約束したりするものではありません。**作業中のセッション、編集中のファイル、Agent が実行した操作**を同じ画面とデータモデルで扱います。 +## ツール間で説明し直す時間を減らす -| コマンドラインだけでは個別に調整する作業 | VibeShell | -| --- | --- | -| 人と Agent が別々の端末を使う | GUI、ネイティブ CLI、MCP から保存済みの接続先とセッションを共有 | -| Agent の直前の操作を確認する | コマンド、対象、状態を通知と永続履歴で確認 | -| Agent の新しい接続を見つける | 新規セッションを別タブとして表示し、現在のタブのフォーカスを保持 | -| ファイルやトンネルのためにツールを切り替える | ファイル、SFTP、転送、プラグインをターミナルの隣で操作 | -| 自動化にプラグインの使い方を教える | アクションの入力スキーマと最新の参照文書を取得 | -| 同じサイズの変更を同期で見落とす | 内容を比較し、削除時には除外パスを保護 | +サーバーの調査は、コマンドを数個打つだけでは終わりません。ログを見て、設定を探し、エディタで修正し、Agent に相談する。そのたびに「どのマシンの、どのセッションか」を確認する必要があります。 + +VibeShell は SSH、ローカルターミナル、コーディング Agent、リモートファイル、Git の差分、運用パネルを同じタブ付きワークスペースにまとめます。普通のターミナルとして使い、必要な場面だけ AI を加えられます。通常の端末・ファイル操作にモデルの契約は必要ありません。 + +新しい SSH プロトコルでも、通信速度を上げる仕組みでもありません。OpenSSH、tmux、エディタ、スクリプトでも多くの同じ作業はできます。VibeShell が減らしたいのは、その間の設定、コピー、ウィンドウ切り替えです。 + +## Agent に任せても、何をしているか分かる + +### コマンドと対象セッションを見える場所に + +GUI、ネイティブ CLI、MCP は保存済みサーバーを共有し、既存セッションを発見できます。Agent の操作は通知と履歴に現れ、コマンド、セッション、時刻、状態を確認できます。同じコマンドの再実行は別の記録として残り、複数行の内容も確認できます。 + +同じプロンプトで一緒に作業するときは共有ターミナルへ入力し、人の作業を邪魔せず調査するときは独立した実行を使えます。どちらも履歴に表示されますが、**入力送信とコマンドの正常終了は区別されます。** + +Agent が別のセッションを作ると新しいタブに反映されます。人が選んだタブのフォーカスは奪いません。同じサーバーへの複数接続もセッションとして別々に管理し、履歴はページ送りや UI 再起動後の読み出しに対応します。 + +### 承認するのは、曖昧な依頼ではなく具体的な操作 + +承認画面には、実行予定のコマンドと確認が必要な理由を表示します。その一回を許可するか、拒否するかを判断でき、あとからログで再起動を知る必要はありません。CLI と MCP のプラグイン操作にも権限と確認のチェックがあります。 + +![実行予定のコマンドと確認理由を示す Agent 承認画面](docs/assets/screenshots/tour-agent-approval.png) -OpenSSH、tmux、エディタ、スクリプトでも同様の環境は構築できます。VibeShell は、その連携を最初から使えるワークフローとして提供します。 +*画像はデモの承認要求です。再起動は実行していません。コマンド分類と承認はサンドボックスではなく、あらゆるリスクを自動検知する保証でもありません。* -## 1.1 のワークフロー +## いつものコーディング Agent と、隣にある差分 -### Agent の作業を見失わない +別途インストールした **Claude Code、Codex、OpenCode、Pi** などを、実際のローカルターミナルで起動できます。作業ディレクトリと最初の依頼を設定し、選択したツールが対応する新規・直前の続き・過去のセッション選択を使えます。アクセスモードも起動前に明示されます。 -CLI と MCP の操作履歴には、コマンド、セッション、時刻、開始・成功・失敗の状態が残ります。同じコマンドを複数回実行しても別の記録として保持し、複数行コマンド、ページ送り、UI の再起動後の読み出しに対応します。履歴はローカルで暗号化され、クラウド同期には含まれません。 +![作業ディレクトリ、セッションとアクセスモード、最初の依頼を設定する画面](docs/assets/screenshots/tour-agent-launcher.png) -同じシェルで作業する場合は共有ターミナルを使い、人のプロンプトに干渉したくない確認作業には独立した exec を使えます。exec も履歴に表示されます。**入力送信の成功は、リモートコマンドの正常終了を意味しません。** +Agent の説明だけで変更を判断する必要はありません。**Workspace changes** を開くと、ブランチ、変更ファイル、行ごとの差分をターミナルの隣で確認できます。ローカル開発とリモート作業を隣り合うタブに置きつつ、実行環境まで同じものとして扱うことはありません。 -Agent の新規セッションは自動的にタブへ反映されますが、人が選択中のタブを切り替えません。同じサーバーへの複数接続もセッション ID で区別します。接続は所有プロセスに依存します。daemon 所有のセッションは GUI 終了後も継続できますが、GUI 所有の接続はその GUI プロセス終了後には維持できません。 +![説明用 Agent 出力の隣に表示した実際の Git 変更一覧と差分](docs/assets/screenshots/tour-agent-review.png) -### ウィンドウより、作業内容に集中 +*各 Agent のインストール、ログイン、モデル契約は別途必要です。VibeShell が提供するのは起動と作業環境の統合であり、モデルの利用権や実行済み成果の保証ではありません。* -検索可能な接続ランチャーに SSH、ローカルシェル、Agent の起動入口をまとめています。リスト/カード表示、キーボードのフォーカス管理、明暗テーマ、動きを減らす設定に対応します。カードのアニメーションはブラウザ標準 API を使います。 +## 小さな操作は、AI なしでも快適に -ターミナルとファイルを分割したり、別ウィンドウに移したりできます。レイアウト変更時にも未保存の編集を保持し、コマンド履歴、スニペット、コンテキスト操作、明確なエラー表示で作業の引き継ぎを助けます。 +オプションを一つ忘れただけなら、チャットを開く必要はありません。内蔵の補完はコマンド、サブコマンド、オプションを説明付きで提案し、履歴も候補に使います。行内の候補とキーボード操作可能なリストをカーソルの近くに表示します。定型コマンドはスニペットに、短い調査は **Quick Cmd** に任せれば、対話中のプロンプトを占有せず結果を見られます。 -![接続ランチャー](docs/assets/screenshots/server-launcher.png) +追加の支援が欲しい場合は、自分の OpenAI 互換または Claude エンドポイントとモデルを設定して **AI コマンド予測** を有効化できます。入力中の後半を提案する機能で、自動実行はしません。コーディング Agent を起動する機能とは別です。 -### ファイルと接続を切り離さない +**予測は初期状態で無効です。** 有効にすると、現在の入力、最近のコマンド履歴、ローカル補完候補を設定先へ送信します。外部に出せない作業では無効のまま使ってください。通常の補完はモデル API に依存しません。 -SFTP のカラム/アイコン表示、複数選択、パスのコピー、転送進捗に対応します。ローカル/リモートファイルをタブで開き、テキスト、コード、画像、PDF、メディア、アーカイブを確認できます。 +ターミナルは xterm.js を使用し、利用可能なら WebGL で描画します。入出力のバッチ処理も UI の負荷を減らすためのもので、SSH 回線が速くなるという主張ではありません。 -転送は有界のチャンクを使い、ダウンロードはローカル書き込みの完了を待って成功を返します。同期は同サイズの内容変更も検出し、不要ファイルの削除では除外設定とネストした `.gitignore` を保護します。ローカル同期では転送元/転送先の重複を拒否します。内容比較はリモート読み出しを増やす場合があり、帯域削減の保証ではありません。 +## IP アドレスより先に、作業先を見つける -![SFTP ワークフロー](docs/assets/screenshots/sftp-workflow.png) +接続ランチャーは **SSH、ローカル Shell、コーディング Agent** の共通入口です。サーバー検索、リスト/カード表示、グループやタグによる整理に対応します。既存セッションの表示と新規接続用の操作を分け、前の作業に戻る場合と別接続を作る場合を区別できます。 -### 保存済み認証情報を安全に編集 +![グループと既存セッションを確認できる接続カード](docs/assets/screenshots/tour-connections.png) -サーバーを作り直さずにパスワード、秘密鍵ファイル/内容、鍵のパスフレーズを更新できます。変更しない項目は保持し、既存の秘密情報は編集フォームへ読み戻しません。サーバー情報、名前変更、認証情報の保存は一つのトランザクションで成功またはロールバックします。 +OpenSSH、PuTTY、Tabby の設定をプレビューしてから取り込み、プライベートな接続先には踏み台を設定できます。他製品に保存されたパスワードはコピーしません。PuTTY `.ppk` は OpenSSH 形式への変換が必要です。 -変更対象は **VibeShell に保存されたログイン情報**であり、リモート OS のアカウントパスワードそのものではありません。正しい秘密鍵があっても、未知/変更されたホスト鍵の確認は省略できません。 +パスワード、秘密鍵、パスフレーズを変えるためにサーバーを作り直す必要もありません。変更しない値を保ち、既存の秘密情報は編集フォームへ読み戻さず、接続情報と認証情報をまとめて保存またはロールバックします。対象は **VibeShell の保存済みログイン情報**であり、リモート OS のパスワードそのものではありません。 -### ネイティブ CLI と共有セッション +## コマンドの隣に、必要なファイルを -Rust の `vibeshell` バイナリは Node.js やデスクトップウィンドウなしで動作し、必要に応じて daemon を起動します。既存 daemon の接続を GUI が見つけた場合、使用中の socket を置き換えず、ターミナル、SFTP、トンネル、録画、DB 検出を実際の所有プロセスへ振り分けます。 +SFTP でリモート設定を開いたり、**⌘/Ctrl+O** でローカルファイルを開いたりできます。ローカル文書のために SSH 接続を作る必要はなく、最後のターミナルを閉じても文書タブは残ります。 -Claude Code、Codex、OpenCode、Pi などを実際の PTY で起動し、リポジトリ状態と差分も確認できます。各 Agent は別途インストールと設定が必要で、モデルの契約や認証情報は付属しません。 +テキスト/コード編集、構文強調、Markdown のソース・プレビュー・左右比較に対応します。SFTP にはカラム/アイコン表示、複数選択、パスコピー、転送進捗、対応する画像・PDF・メディア・アーカイブの表示機能があります。手順書、ログ、設定を別々のアプリで探し直す手間を減らせます。 -### Agent が直接発見できるプラグイン +ファイルとターミナルを分割・移動し、文書を別ウィンドウに出しても、未保存の編集内容を保持します。ローカルテキストの保存は外部変更を検出すると黙って上書きせず、途中までしか読めていない内容を完全なファイルとして保存することもありません。 -内蔵プラグインと対応する宣言型インポートプラグインは、インストール状態、権限、アクションの入力仕様、最新の使い方を公開します。主 Skill は索引に留め、詳細は必要なプラグインの参照文書から取得します。 +フォルダ転送も整合性を重視します。有界チャンク、ローカル書き込みを待つ完了処理、同サイズの内容変更の検出、不要ファイル削除時の除外パスとネストした `.gitignore` の保護を備えます。内容比較は追加のリモート読み出しを伴う場合があり、帯域削減の保証ではありません。 + +[ローカルファイルと Markdown の対応範囲](docs/local-files-and-css-themes.md) + +## コマンドが便利なときも、一覧で見たいときも + +コンテナや CPU、データベースを確認するたびに、生の出力を読みたいとは限りません。別ツールでサーバーを登録し直す代わりに、使用中のセッションでプラグインを開けます。 + +| 作業 | 内蔵の表示とツール | +| --- | --- | +| ホストの遅延や異常 | Server Performance、Process Explorer、System Logs、Network Inspector、Disk Usage | +| サービスと基盤 | Docker Containers、Kubernetes Pods、Cron Scheduler、Systemd Services | +| データと開発 | Database Inspector、Redis Inspector、Git Workspace | + +**12 個の内蔵プラグイン**は人が押すボタンだけではありません。Agent はインストール状態、入力仕様、現在の使い方を取得し、CLI または MCP から利用できます。 ```bash vibeshell plugins list --installed --json @@ -79,73 +106,60 @@ vibeshell plugins docs server-performance vibeshell plugins run server-performance status --session SESSION_ID --inputs '{}' ``` -プラグインがインストール/有効化されていることを確認し、`SESSION_ID` を実在する値に置き換えます。`describe` は機械可読の入力スキーマ、`docs` は現在の検証済み manifest に対応した Markdown を返します。 +有効な既存セッションを指定し、インストール/有効化状態を確認してください。主 Skill は索引にとどめ、詳細は `references/.md` に置きます。対応する宣言型のインポートプラグインも同じ発見インターフェースを持ち、文書は現在の検証済み manifest から生成します。 -| 分野 | 12 個の内蔵プラグイン | -| --- | --- | -| ホスト管理 | Performance、Process、System Logs、Network、Disk Usage | -| サービス/基盤 | Docker、Kubernetes、Cron、Systemd | -| データ/開発 | Database、Redis、Git Workspace | +文書を読むだけで権限が付いたり、必要なソフトウェアがインストールされたりすることはありません。Docker、Kubernetes、データベースの利用には対象環境と権限が必要です。ホストのリモート性能収集は現在 Linux `/proc` を前提とします。 + +[プラグイン仕様](docs/plugin-spec.md) · [Agent とプラグインの索引](skills/vibeshell/SKILL.md#plugin-discovery-and-references) + +## 自分の作業に合わせて整える + +**ウィンドウの山ではなく、作業のまとまりを。** ターミナル、文書、プラグインを分割・並べ替え・別ウィンドウ化し、レイアウトを保存できます。レイアウトの復元は、所有プロセスを終了したネットワーク接続の継続を保証するものではありません。 -CLI と MCP は共通の状態/権限/入力検証を使います。使用例を読んだだけではインストール、権限付与、sudo は実行しません。MCP の承認は人の確認経路を通し、モデル自身の承認フラグを信用しません。対象マシンには対応ツールと権限が必要です。 +**長時間でも使いやすく。** 明暗テーマ、システム外観への追従、端末フォントとカーソル、キーボード操作、動きを減らす設定に対応します。アプリ UI は英語と簡体字中国語です。この日本語 README は文書の翻訳であり、日本語 UI 対応を意味しません。 -[プラグイン仕様](docs/plugin-spec.md) · [共同作業と API](docs/AGENT_COLLABORATION.md) · [参照文書の索引](skills/vibeshell/SKILL.md#plugin-discovery-and-references) +**色のプリセットだけで終わらない。** カスタム CSS はライブプレビュー、適用保存、インポート/エクスポート、ローカル背景画像に対応し、余白、角丸、文書の文字組みも調整できます。テーマが操作部を隠した場合は **⌘/Ctrl+Shift+F12** またはネイティブの *Disable Custom CSS* メニューで無効にできます。CSS の外部 URL は通信を発生させるため、信頼するテーマだけを適用してください。 -## インストールと最初の接続 +[CSS と復旧方法](docs/local-files-and-css-themes.md) · [スターターテーマ](themes/vibecode-starter.css) -[Releases](https://github.com/veithly/vibeshell/releases) から公開済みの対応パッケージを選びます。未完成のドラフトは使用しません。 +## SSH の基本機能も、そのまま -| プラットフォーム | デスクトップ | 独立 CLI | +ローカル転送、SOCKS5、リバース転送、セッションの記録と再生に対応します。トンネル設定を保存し、セッション終了時には関連トンネルと記録も終了します。loopback の外へ公開する前に bind アドレスを確認してください。 + +任意の **Gist / WebDAV 暗号化同期**で、サーバーメタデータ、グループ、スニペット、プラグイン導入情報を自分の環境間で共有できます。VibeShell のホスト型 SSH 中継ではありません。認証情報、ホスト鍵の信頼情報、実行中ターミナル、Agent 履歴は同期対象外です。提供元のトークン、復旧材料、エクスポート内容は慎重に管理してください。 + +SSH は認証前にホストを検証し、踏み台経由でも実際の接続先を確認します。認証情報と Agent 履歴はローカル暗号化保存ですが、OS Keychain 保管や侵害済みローカルアカウントからの保護ではありません。正しい秘密鍵があっても、想定外のホスト指紋を無条件で許可してはいけません。 + +## いつものサーバーから始める + +[Releases](https://github.com/veithly/vibeshell/releases) から対応するデスクトップまたは CLI を取得できます。 + +| プラットフォーム | デスクトップ | ネイティブ CLI | | --- | --- | --- | | macOS Apple Silicon / Intel | アーキテクチャ別 `.dmg` | `.tar.gz` | | Windows x64 | `.exe` / `.msi` | `.zip` | | Linux x64 | `.AppImage` / `.deb` | `.tar.gz` | -Apple Developer ID 署名と公証の有無はリリースノートを確認してください。ad-hoc 署名は Apple の公証ではありません。モバイル対応は実験的で、デスクトップと同じ機能範囲ではありません。 - -CLI アーカイブの `install.sh` / `install.ps1` は内容を確認して実行してください。[CLI ガイド](cli/README.md) に詳細があります。デスクトップには CLI が同梱され、Skill インストーラーが主文書とプラグイン参照文書を対応 Agent ディレクトリに配置します。 +デスクトップは CLI を同梱します。独立 CLI の `install.sh` / `install.ps1` は内容を読んでから実行してください。Rust の CLI 自体は Node.js に依存しません。[CLI インストール](cli/README.md) ```bash -vibeshell version -vibeshell import auto --dry-run -# プレビューを確認してからインポートします。 -vibeshell import auto +vibeshell import auto --dry-run # 確認後に --dry-run を外して取り込みます。 vibeshell servers vibeshell ssh my-server -``` - -`my-server` は保存済みの名前に置き換えます。OpenSSH、PuTTY、Tabby のプロファイルを取り込めますが、他製品に保存されたパスワードはコピーしません。PuTTY `.ppk` は OpenSSH 形式に変換してください。GUI からも追加/編集できます。CLI のサーバー作成/削除と Teleport は 1.1.0 に含まれません。 - -```bash vibeshell ssh my-server -- uname -a vibeshell sessions -# 実際に返された別名を使用します。必ずしも 001 ではありません。 -vibeshell ssh-session 001 -- pwd vibeshell sftp my-server ls /srv/app -vibeshell sftp my-server get /srv/app/config.toml ./config.toml ``` -複雑な引用符や複数行コマンドは `--command-file ./remote-command.sh` / `--command-stdin` を使います。`--new` は別の接続が必要な場合だけ指定し、秘密情報は引数やプロンプトに書かないでください。 - -## 転送、記録、任意の暗号化同期 +`my-server` は保存済み名に置き換えます。`sessions` に表示された別名で `vibeshell ssh-session ALIAS -- pwd` を実行し、別接続が必要な場合だけ `--new` を使います。複雑な引用符には `--command-file` / `--command-stdin` を使用し、秘密情報を引数や Agent の依頼文に入れないでください。 -ローカル転送、SOCKS5、リバース転送はリスナー準備、キャンセル、半閉鎖を扱い、セッション終了時に関連トンネルと記録も終了します。loopback 以外へ公開する前に bind アドレスを確認してください。 +CLI は必要に応じて daemon を起動し、GUI は既存セッションに接続できます。接続の所有プロセスは生きている必要があります。daemon 所有の接続は GUI 終了後も継続できますが、GUI 所有の接続はその終了で切れます。デスクトップと CLI は一緒に更新し、再起動前に作業を保存してください。 -任意の Gist/WebDAV 同期はサーバーメタデータ、グループ、スニペット、プラグイン導入情報を暗号化します。VibeShell のホスト型 SSH 中継はありません。ログイン認証情報、ホスト鍵の信頼情報、稼働中セッション、Agent 履歴は同期対象外です。トークン、復旧材料、ローカルエクスポートを保護し、プラグイン設定にも機密情報があり得ることに注意してください。 +Apple 署名/公証の状況は各リリースノートを参照してください。ad-hoc 署名は Apple 公証ではありません。モバイルは実験的で、全 MFA、ハードウェアトークン、SSH 実装への対応を保証しません。提案中の CLI サーバー作成/削除と Teleport は現在の 1.1.0 には含まれません。 -## 安全性と対応範囲 +## 動かす、開発する、実装を読む -認証前にホスト鍵を確認し、踏み台経由でも実際の宛先を検証します。ローカル認証情報は暗号化され、Unix ではファイル権限を制限します。OS Keychain 保存や、侵害されたユーザーアカウントからの保護を保証するものではありません。 - -隔離 OpenSSH テストは一般的なパスワード、鍵、PAM keyboard-interactive、PTY、SFTP、転送を対象とします。全 MFA、ハードウェアトークン、ネットワーク機器、SSH 実装の互換性を示すものではありません。リモート性能収集は Linux `/proc` を前提とします。 - -危険なコマンドの分類はサンドボックスではありません。`send-secret` は本当の機密入力を履歴から除外できますが、サーバー側のエコーは防げず、コマンドを隠すために使ってはいけません。不明な応答消失後に変更操作を自動再実行しないでください。 - -問題は [非公開のセキュリティ報告](https://github.com/veithly/vibeshell/security/advisories/new) へ。秘密鍵や本番環境の機密情報を公開 Issue に投稿しないでください。 - -## 開発と貢献 - -機能、修正、文書の **PR はすべて `dev` 宛て**です。`main` は安定版で、本リポジトリの `dev` からのリリース昇格を受け付けます。`master` は履歴用です。 +Tauri 2、Rust、React、TypeScript、xterm.js で構成しています。Node.js 22.12+、stable Rust、OS ごとの [Tauri 前提条件](https://v2.tauri.app/start/prerequisites/) が必要です。 ```bash git clone --branch dev https://github.com/veithly/vibeshell.git @@ -154,25 +168,18 @@ npm ci npm run tauri -- dev ``` -Node.js 22.12+、stable Rust(manifest の最小要件は 1.89)、OS ごとの [Tauri 前提条件](https://v2.tauri.app/start/prerequisites/) が必要です。 +PR 前に `node scripts/check-release.mjs`、`npm test`、`npm run build`、`cargo fmt --all -- --check`、`cargo clippy --workspace --all-targets --locked -- -D warnings`、`cargo test --workspace --locked` を実行します。SSH 変更には loopback 限定の Docker テスト `bash scripts/test-ssh-compatibility.sh` もあります。本物の認証情報ストアをテストに使わないでください。 -```bash -node scripts/check-release.mjs -npm test -npm run build -cargo fmt --all -- --check -cargo clippy --workspace --all-targets --locked -- -D warnings -cargo test --workspace --locked -# 任意の Docker / loopback 専用テスト -bash scripts/test-ssh-compatibility.sh -``` +**通常の PR は `dev` 宛てです。** `main` は本リポジトリの `dev` からの検証済みリリース昇格を受け付け、`master` は履歴用です。ビルド操作は使用中アプリの置換や SSH 切断を許可するものではありません。 -CLI: `cargo build --release --locked -p vshell --bin vibeshell`。デスクトップ: `npm run build:desktop`。ビルドは使用中アプリの上書きや SSH の切断を許可する操作ではありません。 +CI ビルドとリリースは**手動実行のみ**です。マージ前に PR の最新コミットで CI を実行してください。push やタグ作成ではビルドも公開も始まりません。Release は `main` から実行し、明示的に公開を選択しない限りドラフトのままです。必須チェック、署名、対応ソースの検証は維持されます。 -[貢献ガイド](CONTRIBUTING.md) · [リリース手順](docs/RELEASING.md) · [アーキテクチャ](AGENTS.md) +Docker の一覧改善と、独立コンテナーセッション・送信プロキシの残作業は [9 月の issue レビュー](docs/ISSUE_TRIAGE_2026-09.md) にまとめています。Docker プラグインのコマンド実行は永続セッションではなく、SSH の SOCKS 転送リスナーは送信プロキシ設定ではありません。 -## ライセンス +[貢献ガイド](CONTRIBUTING.md) · [アーキテクチャ](AGENTS.md) · [リリース手順](docs/RELEASING.md) · [共同作業 API](docs/AGENT_COLLABORATION.md) -1.1.0 以降の VibeShell 全体は **GNU GPL バージョン 3 のみ(`GPL-3.0-only`)**です。[LICENSE](LICENSE) と [NOTICE](NOTICE) を参照してください。以前の MIT リリースに付与済みの権利は取り消しません。[旧 MIT 通知](licenses/legacy-MIT.txt) は該当部分について保持し、第三者コンポーネントは各自のライセンスと著作権表示に従います。 +セキュリティ上の問題は [非公開報告](https://github.com/veithly/vibeshell/security/advisories/new) へ。承認はサンドボックスではなく、保護された入力もリモートプログラムのエコーを防げません。応答消失後に変更操作を自動で再実行しないでください。 + +## ライセンス -リリースには対応するソースとライセンス通知を用意します。法律が認める範囲で、本ソフトウェアは無保証です。 +VibeShell 1.1.0 以降の全体は **GPL-3.0-only** です。[LICENSE](LICENSE)、[NOTICE](NOTICE)、[保存された MIT 通知](licenses/legacy-MIT.txt) を参照してください。既存の MIT 許諾は取り消さず、第三者はそれぞれのライセンスを保持します。リリースには対応ソースと通知を用意し、法律で許される範囲で無保証とします。 diff --git a/README.md b/README.md index cd563d2..5cc80a6 100644 --- a/README.md +++ b/README.md @@ -1,76 +1,103 @@
VibeShell

VibeShell

-

Your terminal. Your agent. The same workspace.

-

A local-first SSH/SFTP workspace for people and coding agents — with visible operations, shared sessions, integrated files, and discoverable plugins.

+

Keep your servers, files, and AI work in the same place.

+

An SSH terminal you can work in yourself, share with an agent, and make your own.

[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) - [![CI](https://github.com/veithly/vibeshell/actions/workflows/ci.yml/badge.svg?branch=dev)](https://github.com/veithly/vibeshell/actions/workflows/ci.yml) + [![Manual CI](https://github.com/veithly/vibeshell/actions/workflows/ci.yml/badge.svg?branch=dev&event=workflow_dispatch)](https://github.com/veithly/vibeshell/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/veithly/vibeshell)](https://github.com/veithly/vibeshell/releases) [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE) - [Download](https://github.com/veithly/vibeshell/releases) · [What's new in 1.1](CHANGELOG.md) · [Agent guide](skills/vibeshell/SKILL.md) · [Contribute to dev](CONTRIBUTING.md) + [Download](https://github.com/veithly/vibeshell/releases) · [Agent / CLI guide](skills/vibeshell/SKILL.md) · [Changelog](CHANGELOG.md) · [Contribute](CONTRIBUTING.md)
-![VibeShell terminal workspace](docs/assets/screenshots/terminal-workspace.png) +![Terminal, host status and visible agent operations in one VibeShell workspace](docs/assets/screenshots/tour-collaboration.png) -## Why a workspace, not another SSH command? +*Real VibeShell components, synthetic Northstar demo data. The gallery uses an isolated browser fixture: no live servers, credentials, model calls, or service restarts. Agent transcripts and command results are examples, not recordings of an actual agent run. [Reproduce the screenshots](scripts/readme-demo/README.md).* -SSH already provides secure transport, authentication, forwarding, and remote command execution. VibeShell does not replace that protocol or claim a faster network. It brings the work around SSH into one place: **the session you are using, the files you are editing, and the actions your agent is taking**. +## Less passing context between tools -| In a command-line-only workflow | In VibeShell | -| --- | --- | -| A person and an agent may work through unrelated terminals. | Desktop, native CLI and MCP share saved targets and discoverable sessions. | -| You need to ask what the agent just ran. | Commands and operation states appear in an activity strip and durable history. | -| A new agent connection is invisible to the desktop. | New sessions become separate tabs without stealing your active tab. | -| Files, tunnels and commands require switching tools. | Local/remote file tabs, SFTP, forwarding and plugin views live alongside terminals. | -| Each automation needs handwritten command knowledge. | Plugins expose action schemas and current usage references to agents. | -| A sync tool can mistake equal file sizes for equal content. | Directory sync compares content and protects excluded paths during deletion. | +A routine server task rarely stays in one terminal. You check a log, find a configuration file, open an editor, ask an agent for help, then work out which machine and session each tool is using. + +VibeShell keeps that work together. SSH and local terminals, coding agents, remote files, Git changes, and operations dashboards share a tabbed workspace. Use it as a normal terminal; bring AI into the parts where it helps. You do not need a model account for ordinary terminal and file work. + +The difference is the workflow, not a new SSH protocol. OpenSSH, tmux, editors and scripts can cover many of the same jobs. VibeShell reduces the setup, copying and window switching needed to make them work together. + +## Let an agent help without losing the thread + +### See what it ran, and where + +Desktop, native CLI and MCP can use the same saved servers and discover existing sessions. When an agent runs a command, the activity strip and history show the command, session, time and operation state. Repeated attempts remain separate records; multiline commands are not reduced to a one-line label. + +There are two ways to collaborate. Send input into a shared interactive shell when you want to work in that prompt together. Use independent execution when the agent should inspect something without typing over your work. Both have visible activity; “input sent” is kept distinct from “command completed.” + +An agent-created session appears as another tab without switching away from the tab you selected. Two connections to the same server remain two sessions, not one ambiguous server-name tab. History can be paged through and recovered after reopening the UI. + +### Approve the actual operation, not a vague request + +The approval dialog puts the proposed command and the reasons for review in front of you. You can allow that operation or reject it instead of discovering a service restart in the transcript afterward. CLI and MCP plugin actions also retain their permission and confirmation checks. + +![Actual agent approval dialog showing the proposed restart and reasons for review](docs/assets/screenshots/tour-agent-approval.png) + +*Demo request only. The service restart in this image was never executed. Command classification and approval are useful controls, not a sandbox or a guarantee that every risky command will be recognized.* -You can assemble similar workflows from OpenSSH, tmux, editors and scripts. VibeShell makes their coordination a product feature, rather than a setup task. +## Bring your coding agent, not another chat window -## The 1.1 experience +Launch separately installed tools such as **Claude Code, Codex, OpenCode and Pi** in real local terminals. Pick a project directory, add an initial brief, and choose the start modes the selected tool supports: a new session, continuing the latest one, or choosing a previous session. Access modes are explicit rather than hidden in a command copied from a tutorial. -### Work with an agent without losing visibility +![Coding-agent launcher with project, session mode, access mode and initial brief](docs/assets/screenshots/tour-agent-launcher.png) -Agent and CLI operations show the command, target session, time and lifecycle state. Repeated commands remain separate records; long, multiline commands are retained, with pagination and recovery after reopening the UI. The activity history is encrypted locally and is not part of cloud sync. +The agent's terminal stays beside the rest of your work. Open **Workspace changes** to see the branch, changed files and line-by-line diff. You can read the proposed edit instead of relying on the agent's summary of it. Local development and remote operations can live in neighboring tabs, but they are not silently treated as the same execution environment. -Use the shared interactive terminal when you need to collaborate in the same shell. Use independent exec for inspection without typing into the person's prompt; it remains visible in activity history. A successful input operation means **bytes were delivered**, not that the remote command completed successfully. +![A synthetic coding-agent transcript beside the real Git changes and diff interface](docs/assets/screenshots/tour-agent-review.png) -When an agent opens another session, its tab appears without changing the person's selection. Session identity, not server name, distinguishes parallel work. Closing an owner process ends its connections: daemon-owned sessions can outlive the GUI, while GUI-owned sessions cannot survive quitting that GUI process. +*Agent tools need their own installation, sign-in and subscriptions. VibeShell provides the workspace and launch integration; it does not bundle a model subscription or claim the sample transcript is live output.* -### Less window management, more context +## A terminal that helps with the small things -A searchable connection launcher brings SSH servers, local shells and coding-agent entry points together. Switch between list and card views; use keyboard focus management, light/dark themes and reduced-motion support. Card animations use browser-native APIs rather than a separate animation runtime. +You should not need AI just to remember a flag. Built-in completion suggests commands, subcommands and options, with descriptions and command-history matches. Inline suggestions and a keyboard-driven completion list keep the answer near the cursor. Frequently used commands can become snippets, while **Quick Cmd** runs a short inspection and shows its output without taking over your interactive shell. -Split terminals and file views, detach work into another window, and keep unsaved file edits when reorganizing the workspace. Command history, snippets, contextual actions and explicit error messages reduce repetitive copying and ambiguous “success” notifications. +For an extra nudge, enable **AI command prediction** with your own OpenAI-compatible or Claude endpoint and model. It proposes a suffix to what you are typing; it does not run the suggestion for you. This is separate from launching a coding agent. -![Connection launcher](docs/assets/screenshots/server-launcher.png) +**Prediction is off by default.** When enabled, the current input, recent command history and local completion candidates are sent to the provider you configure. Leave it disabled for work that must not leave the machine; ordinary local completion remains available. -### Files are part of the session +Terminal rendering uses xterm.js, a WebGL renderer where available, and batched input/output paths to reduce UI overhead. These are responsiveness choices, not a claim that VibeShell makes the SSH network faster. -Browse SFTP in columns or icon views, select multiple files, copy paths, and follow transfer progress. Open local and remote files in workspace tabs; supported viewers include text/code, images, PDF, media and archives. +## Find a connection without remembering an address -The transport is designed around integrity: transfers use bounded chunks; a download waits for local writes before reporting completion; sync detects same-size edits; excluded paths and nested `.gitignore` rules are protected when deleting extras. Local directory transfers reject overlapping source and destination roots. Content comparison can add remote reads — this is an integrity choice, not a bandwidth-saving claim. +The connection launcher brings **SSH, local shells and coding agents** into one place. Search your saved servers, switch between compact lists and cards, and organize targets with groups and tags. Existing-session indicators and a separate new-session action help distinguish “return to my work” from “open another connection.” -![SFTP workflow](docs/assets/screenshots/sftp-workflow.png) +![Searchable connection cards with groups, saved targets and existing-session indicators](docs/assets/screenshots/tour-connections.png) -### Edit saved credentials safely +Bring existing profiles from OpenSSH, PuTTY or Tabby, preview an import before applying it, and configure a jump host for private targets. Stored passwords from those other applications are deliberately not copied; PuTTY `.ppk` keys need conversion to OpenSSH format. -Change a saved password, replace a private key, or update/clear a key passphrase without recreating the server. Untouched fields preserve their previous values; existing secrets are not fetched into the edit form. Metadata, renames and credential changes commit together or roll back together. +Changing a saved password, private key or passphrase does not require deleting the server. Untouched fields keep their values, existing secrets are not loaded into the edit form, and profile/credential changes save together or roll back together. This changes **VibeShell's saved login information**, not the remote account's password. -This edits **VibeShell's saved login information**, not the remote operating-system account's password. Unknown or changed host keys still require the appropriate trust decision; a correct login key does not replace host identity verification. +## Files belong next to the command that uses them -### Native automation, not a second server inventory +Open a remote configuration through SFTP, or use **⌘/Ctrl+O** for local files without opening an SSH connection at all. Documents get their own tabs, so closing the last terminal does not close your local notes. -The Rust `vibeshell` executable can operate without Node.js or a desktop window. It starts a native daemon when required and reuses saved profiles and sessions. When a daemon already owns SSH sessions, the GUI attaches without replacing its live socket; terminal, SFTP, tunnel, recording and database-probe operations route to the owning process. +The file workspace offers editable text/code, syntax highlighting, and Markdown source, preview, or both side by side. SFTP includes column/icon browsing, multiple selection, path copying, upload/download progress, and viewers for supported images, PDFs, media and archives. Read a runbook, inspect a log and edit a config without maintaining a separate mental map of windows. -Local coding-agent launchers support tools such as Claude Code, Codex, OpenCode and Pi through real PTYs, alongside repository status/diff views. Those agents are separately installed products: VibeShell does not include model subscriptions or their credentials. +Move file and terminal panes around, split the workspace, or detach a document into another window. Unsaved text stays with its editing buffer during layout changes. Local text saves detect changes made by another program and refuse to silently overwrite them; truncated reads are not offered as a full-file save. -### Plugins an agent can actually discover +Folder transfer is also about correctness: bounded chunks, downloads that wait for local writes, content comparison for same-size edits, and protection for excluded paths and nested `.gitignore` rules when deleting extras. Content comparison can add remote reads; it is not a promise of lower bandwidth. -Every supported built-in or imported declarative plugin exposes installation state, permissions, action inputs and a current reference. **The main Skill is an index, not an encyclopedia**; detailed instructions live in per-plugin reference documents. +[Local files, Markdown behavior and editing limits](docs/local-files-and-css-themes.md) + +## Go from a command to a useful view + +Sometimes a terminal is the right view; sometimes a container list or a CPU graph is faster to understand. Open a plugin for the session you are already using instead of configuring the same server in another dashboard. + +| What you are working on | Built-in views and tools | +| --- | --- | +| A slow or unhealthy host | Server Performance, Process Explorer, System Logs, Network Inspector, Disk Usage | +| Services and infrastructure | Docker Containers, Kubernetes Pods, Cron Scheduler, Systemd Services | +| Data and code | Database Inspector, Redis Inspector, Git Workspace | + +These **12 built-ins** are more than UI buttons. Agents can discover what is installed, read an action's inputs, fetch its current instructions and use it through the native CLI or MCP: ```bash vibeshell plugins list --installed --json @@ -79,75 +106,60 @@ vibeshell plugins docs server-performance vibeshell plugins run server-performance status --session SESSION_ID --inputs '{}' ``` -Replace `SESSION_ID` with an existing session, and check the plugin is installed and enabled first. `describe` returns machine-readable schemas; `docs` reflects the current validated manifest, including imported plugins. +Use an existing session ID and check the plugin is installed and enabled first. The main Skill contains an index; detailed usage lives in `references/.md`. Supported imported declarative plugins expose the same discovery interface, with documentation generated from their current validated manifest. -| Built-in area | Plugins | -| --- | --- | -| Host operations | Server Performance, Process Explorer, System Logs, Network Inspector, Disk Usage | -| Services and infrastructure | Docker Containers, Kubernetes Pods, Cron Scheduler, Systemd Services | -| Data and development | Database Inspector, Redis Inspector, Git Workspace | +Reading a plugin's documentation does not grant permissions, install remote tools or approve a destructive action. Docker, Kubernetes and database tools still need the appropriate target environment and access. Remote host-performance collection currently assumes Linux `/proc`. + +[Plugin specification](docs/plugin-spec.md) · [Agent and plugin reference index](skills/vibeshell/SKILL.md#plugin-discovery-and-references) + +## Arrange it around the way you work + +**Keep context, not a pile of windows.** Terminals, documents and plugins can be split, rearranged and moved into separate windows. Save a layout and return to it; restoring the workspace is not a promise that an old network connection survives a process restart. -CLI and MCP execution enforce the same enabled-state, permission and input checks. Explicit confirmation and sudo opt-in are not implied by a documentation example. MCP uses the human approval gateway, not a model-provided approval flag. Plugins need the corresponding tools and permissions on the target; a manifest is not an installed Docker or Kubernetes environment. +**Make long sessions comfortable.** Choose light or dark themes, follow the system appearance, adjust terminal fonts and cursors, and use keyboard navigation or reduced-motion settings. The application has English and Simplified Chinese UI; the Japanese README is a documentation translation, not a claim of Japanese UI support. -[Plugin specification](docs/plugin-spec.md) · [Collaboration and API details](docs/AGENT_COLLABORATION.md) · [Plugin reference index](skills/vibeshell/SKILL.md#plugin-discovery-and-references) +**Go beyond a color preset.** The custom CSS editor supports live preview, apply/save, import/export and local background images. Change spacing, corners and document typography as well as colors. If a theme hides the controls, **⌘/Ctrl+Shift+F12** or the native *Disable Custom CSS* menu can turn it off. Only apply trusted CSS: remote URLs in a theme can make network requests. -## Start with your existing servers +[Custom CSS guide and recovery](docs/local-files-and-css-themes.md) · [Starter theme](themes/vibecode-starter.css) -Download the desktop installer or native CLI archive matching your platform from [Releases](https://github.com/veithly/vibeshell/releases). +## The SSH essentials are still here -| Platform | Desktop | Standalone CLI | +Local forwarding, SOCKS5 and reverse forwarding sit alongside session recording and playback. Saved tunnel configurations reduce repetitive setup; session cleanup also tears down associated tunnels and recordings. Check bind addresses before exposing a service beyond loopback. + +Optional encrypted **Gist/WebDAV sync** carries server metadata, groups, snippets and plugin installations between your own setups. It is not a VibeShell-hosted SSH relay. Login credentials, host-key trust, live terminal sessions and agent activity history stay outside that sync. Protect provider tokens and recovery material, and review plugin settings before sharing an export. + +SSH host identity is checked before authentication, including the real destination behind a jump host. Credentials and agent activity use encrypted local storage. That is not OS Keychain custody or protection from a compromised local account. A correct private key does not justify accepting an unexpected server fingerprint. + +## Start with a server you already use + +Get the desktop installer or standalone CLI from [Releases](https://github.com/veithly/vibeshell/releases). + +| Platform | Desktop | Native CLI | | --- | --- | --- | -| macOS Apple Silicon / Intel | Architecture-specific `.dmg` | Architecture-specific `.tar.gz` | +| macOS Apple Silicon / Intel | Architecture-specific `.dmg` | `.tar.gz` | | Windows x64 | `.exe` / `.msi` | `.zip` | | Linux x64 | `.AppImage` / `.deb` | `.tar.gz` | -Use published assets, not an unfinished draft. Apple Developer ID signing/notarization availability is stated in the release notes; a local ad-hoc signature is not Apple notarization. Mobile targets remain experimental and do not have desktop feature parity. - -For a native CLI archive, inspect and run its included `install.sh` or `install.ps1`. See [CLI installation and commands](cli/README.md). Desktop packages include the CLI sidecar; Skill installation writes the bundled guide and plugin references to supported agent directories. +The desktop includes a CLI sidecar. Standalone CLI archives include `install.sh` / `install.ps1`; inspect the script before running it. The native Rust CLI itself does not need Node.js. [CLI installation](cli/README.md) ```bash -vibeshell version -vibeshell import auto --dry-run -# Review the preview before importing. -vibeshell import auto +vibeshell import auto --dry-run # Review, then run without --dry-run to import. vibeshell servers vibeshell ssh my-server -``` - -Replace `my-server` with a saved name. OpenSSH, PuTTY and Tabby profile imports are supported; third-party stored passwords are deliberately not copied. PuTTY `.ppk` keys need conversion to OpenSSH format. You can also add and edit servers in the GUI. CLI server-create/delete and Teleport support are not part of 1.1.0. - -For a command on a saved server or an existing session: - -```bash vibeshell ssh my-server -- uname -a vibeshell sessions -# Use an alias actually returned above, not necessarily 001. -vibeshell ssh-session 001 -- pwd vibeshell sftp my-server ls /srv/app -vibeshell sftp my-server get /srv/app/config.toml ./config.toml ``` -For complex quoting, use `--command-file ./remote-command.sh` or `--command-stdin`. Request `--new` only when you need a separate connection. Never put passwords or private-key contents into command-line arguments or agent prompts. +Replace `my-server` with a saved name. Use an alias returned by `sessions` with `vibeshell ssh-session ALIAS -- pwd`; add `--new` when you need a separate connection. For complex quoting, use `--command-file` or `--command-stdin`. Never put passwords or private keys in command-line arguments or agent prompts. -## Forwarding, recording and optional sync +The CLI can start a daemon when needed; the GUI can attach to its sessions. The process owning a connection must remain alive: daemon-owned sessions can outlive the GUI, while GUI-owned ones end when that GUI exits. Upgrade the desktop and CLI together, and save ongoing work before restarting. -Local forwarding, SOCKS5 and reverse forwarding include listener-readiness checks, cancellation and half-close handling. Session cleanup also tears down associated tunnels and recordings. Inspect bind addresses before exposing a service beyond loopback. +Apple signing/notarization details are in each release's notes. An ad-hoc signature is not Apple notarization. Mobile targets remain experimental. Common OpenSSH paths are tested, but not every MFA flow, hardware token or SSH implementation. Proposed CLI server-create/delete and Teleport support are not part of the current 1.1.0 release. -Optional encrypted sync uses your configured Gist or WebDAV provider for server metadata, groups, snippets and plugin installations. It is not a VibeShell-hosted SSH relay. Login credentials, host-key trust, live terminal sessions and agent activity history stay outside that sync. Protect your provider token and recovery material; do not assume all plugin settings are harmless. +## Build, contribute, or explore the implementation -## Security and compatibility boundaries - -VibeShell checks SSH host keys before authentication, including the actual destination behind a jump host. Device keys and saved credentials use local encrypted storage; on Unix, private material is permission-restricted. This is **not** a claim of OS Keychain storage or protection from a compromised user account. - -Common OpenSSH password/key and PAM keyboard-interactive paths are exercised by an isolated test matrix, along with PTY, SFTP and forwarding. This does not establish support for every MFA flow, hardware token, network appliance or SSH implementation. Remote performance collection currently assumes Linux `/proc`. - -Agents still need supervision. Dangerous actions may require approval; textual command classification is not a sandbox. `send-secret` can keep genuine prompt input out of the activity log, but cannot stop a remote program from echoing it. Automatic retries must not replay a mutating command after an ambiguous response loss. - -[Report a security concern privately](https://github.com/veithly/vibeshell/security/advisories/new) rather than posting credentials, private keys or exploitable production details in an issue. - -## Develop and contribute - -**Feature, fix and documentation PRs target `dev`.** `main` is the stable release branch and accepts release promotions from this repository's `dev` branch. `master` is historical; it is not an additional development branch. +Built with Tauri 2, Rust, React, TypeScript and xterm.js. Use Node.js 22.12+, current stable Rust and the [Tauri platform prerequisites](https://v2.tauri.app/start/prerequisites/). ```bash git clone --branch dev https://github.com/veithly/vibeshell.git @@ -156,25 +168,18 @@ npm ci npm run tauri -- dev ``` -Use Node.js 22.12+ and a current stable Rust toolchain (the Rust manifest requires at least 1.89). Install the [Tauri platform prerequisites](https://v2.tauri.app/start/prerequisites/) for your OS. +Before a PR: `node scripts/check-release.mjs`, `npm test`, `npm run build`, `cargo fmt --all -- --check`, `cargo clippy --workspace --all-targets --locked -- -D warnings`, and `cargo test --workspace --locked`. SSH changes also have a loopback-only Docker fixture: `bash scripts/test-ssh-compatibility.sh`. Never run credential tests against a real saved-server store. -```bash -node scripts/check-release.mjs -npm test -npm run build -cargo fmt --all -- --check -cargo clippy --workspace --all-targets --locked -- -D warnings -cargo test --workspace --locked -# Optional: Docker-backed, loopback-only OpenSSH regression tests. -bash scripts/test-ssh-compatibility.sh -``` +**All normal PRs target `dev`.** `main` receives tested release promotions from this repository's `dev`; `master` is historical. Build commands do not authorize replacing an installed app or ending someone's SSH sessions. -Build the native CLI with `cargo build --release --locked -p vshell --bin vibeshell`; build a desktop package including its sidecar with `npm run build:desktop`. These are build commands, not permission to overwrite an installed application or close a user's connections. +CI builds and releases are **manual-only**. Maintainers run CI on the PR head before merging; pushing code or a version tag does not compile or publish. Release runs from `main` and defaults to a draft unless publication is explicitly selected. Required checks, signatures and complete-source validation remain in place. -[Contribution workflow](CONTRIBUTING.md) · [Release process](docs/RELEASING.md) · [Architecture and agent conventions](AGENTS.md) +Docker inventory improvements and the remaining container-session/proxy work are tracked in the [September issue review](docs/ISSUE_TRIAGE_2026-09.md). A Docker plugin command is not a persistent container session; an SSH SOCKS forwarding listener is not an outbound proxy setting. -## License +[Contributing](CONTRIBUTING.md) · [Architecture](AGENTS.md) · [Release process](docs/RELEASING.md) · [Collaboration API](docs/AGENT_COLLABORATION.md) -VibeShell 1.1.0 and later are distributed as a whole under **GNU GPL version 3 only (`GPL-3.0-only`)**. See [LICENSE](LICENSE) and [NOTICE](NOTICE). Earlier MIT releases retain their original permissions; the [legacy MIT notice](licenses/legacy-MIT.txt) remains for previously licensed portions. Third-party components retain their own notices and licenses. +Report security concerns through [private security reporting](https://github.com/veithly/vibeshell/security/advisories/new), not with real credentials in an issue. Approval controls are not a sandbox; protected input cannot prevent a remote program from echoing it. Do not automatically replay a mutating command after an ambiguous response loss. + +## License -Release downloads include matching source and license notices. VibeShell is provided without warranty to the extent permitted by law. +VibeShell 1.1.0 and later are distributed as a whole under **GPL-3.0-only**. See [LICENSE](LICENSE), [NOTICE](NOTICE) and the [preserved MIT notice](licenses/legacy-MIT.txt). Earlier MIT permissions are not revoked; third-party components keep their own licenses. Release downloads include corresponding source and notices. No warranty is provided to the extent permitted by law. diff --git a/README.zh-CN.md b/README.zh-CN.md index 380007e..61988e1 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -1,76 +1,103 @@
VibeShell

VibeShell

-

你的终端,你的 Agent,同一个工作区。

-

为人和编程 Agent 共同使用而设计的本地优先 SSH/SFTP 工作区:操作看得见,会话能共享,文件不脱节,插件可按需发现。

+

连服务器、改文件、和 AI 一起做事,不必来回换地方。

+

一个自己用顺手、和 Agent 共用也看得明白的 SSH 工作区。

[English](README.md) · [简体中文](README.zh-CN.md) · [日本語](README.ja.md) - [![CI](https://github.com/veithly/vibeshell/actions/workflows/ci.yml/badge.svg?branch=dev)](https://github.com/veithly/vibeshell/actions/workflows/ci.yml) + [![手动 CI](https://github.com/veithly/vibeshell/actions/workflows/ci.yml/badge.svg?branch=dev&event=workflow_dispatch)](https://github.com/veithly/vibeshell/actions/workflows/ci.yml) [![Release](https://img.shields.io/github/v/release/veithly/vibeshell)](https://github.com/veithly/vibeshell/releases) [![GPLv3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE) - [下载安装](https://github.com/veithly/vibeshell/releases) · [1.1 更新记录](CHANGELOG.md) · [Agent 使用入口](skills/vibeshell/SKILL.md) · [向 dev 贡献](CONTRIBUTING.md) + [下载](https://github.com/veithly/vibeshell/releases) · [Agent / CLI 指南](skills/vibeshell/SKILL.md) · [更新记录](CHANGELOG.md) · [参与开发](CONTRIBUTING.md)
-![VibeShell 终端工作区](docs/assets/screenshots/terminal-workspace.png) +![同一工作区里的终端、主机状态与 Agent 操作历史](docs/assets/screenshots/tour-collaboration.png) -## 不只是再封装一个 ssh 命令 +*这些是实际 VibeShell 组件的截图,填入的是虚构 Northstar 项目数据。演示在隔离浏览器里运行,没有连接真实服务器、读取凭据、调用模型或重启服务。Agent 对话和命令结果是示例,不是一次真实 Agent 执行的录像。[复现截图](scripts/readme-demo/README.md)。* -SSH 本身已经提供加密传输、认证、转发和远程执行。VibeShell 不替换协议,也不宣称让网络变快;它把 **你正在使用的会话、正在编辑的文件,以及 Agent 正在执行的操作** 放进同一个工作区。 +## 少一点来回复制,多一点把事情做完 -| 只用命令行时需要自行协调的事情 | VibeShell 的做法 | -| --- | --- | -| 人和 Agent 可能使用互不关联的终端。 | 桌面、原生 CLI 和 MCP 共享已保存目标,并发现现有会话。 | -| 需要追问 Agent 刚才执行了什么。 | 操作通知条与持久历史显示命令、目标和状态。 | -| Agent 新开的连接在桌面不可见。 | 新会话自动成为独立标签,不抢走人的当前焦点。 | -| 文件、隧道和命令分散在多个工具里。 | 本地/远程文件标签、SFTP、转发和插件视图与终端并列。 | -| 每个自动化脚本都要重新理解插件用法。 | 插件提供机器可读参数和当前参考文档。 | -| 仅按文件大小比较可能漏掉同长度修改。 | 目录同步比较内容,删除多余文件时保护排除项。 | +一次普通的服务器排错,往往不止敲几个命令:先看日志,再找配置,切到编辑器改两行,问一下 Agent,最后还要确认它操作的是哪台机器、哪个会话。 + +VibeShell 想把这些事放在一起。SSH、本地终端、编程 Agent、远程文件、Git 差异和运维面板都可以成为工作区里的标签。你可以把它当作普通终端,也可以在需要时让 AI 加入;不使用 AI,并不妨碍日常终端和文件操作。 + +它的区别不在于重新发明 SSH,也不是宣称网络更快。用 OpenSSH、tmux、编辑器和脚本,同样能组合出很多能力。VibeShell 把它们之间的衔接做好,让你少配置、少复制、少丢上下文。 + +## AI 可以动手,你不用闭眼 + +### 它执行了什么,在哪个会话里,都能看到 + +桌面、原生 CLI 和 MCP 共用已保存的目标,并能发现现有会话。Agent 执行操作时,通知条和历史里会出现命令、Session、时间和状态。相同命令跑两次,留下两条记录;多行命令也能完整查看。 + +需要一起操作同一个提示符时,可以使用共享交互终端。不想打断你的输入时,让 Agent 独立执行检查命令,操作仍然出现在历史里。**“输入已发送”和“命令已完成”是两件事**,不会用一个含糊的成功提示混过去。 + +Agent 新开的会话会自动变成标签,但不会把你从当前标签拉走。同一服务器的两个连接也按 Session 分开管理。历史支持分页,重新打开 UI 后仍可读取。 + +### 要批准的是这条命令,不是一句“让我处理一下” + +审批窗口直接展示准备执行的命令和需要确认的原因。你可以允许这一次,也可以拒绝,不必等看到输出才发现服务被重启了。CLI 和 MCP 的插件操作同样遵守各自的权限与确认要求。 + +![实际审批界面展示拟执行命令、风险原因和允许或拒绝入口](docs/assets/screenshots/tour-agent-approval.png) -这些体验也可以用 OpenSSH、tmux、编辑器和脚本组合出来。VibeShell 的区别是把协作本身做成产品功能,减少手工配置与上下文切换。 +*图中只是演示请求,没有实际执行重启。命令识别和审批不是沙箱,也不代表所有风险都能被自动识别。* -## 1.1 的核心体验 +## 用你熟悉的编程 Agent,旁边就是它改的代码 -### 和 Agent 一起操作,也能知道它在做什么 +VibeShell 可以通过真实本地终端启动单独安装的 **Claude Code、Codex、OpenCode、Pi** 等工具。先选项目目录,写下这次要做什么,再选择工具支持的新会话、继续最近一次或选择历史会话;访问模式也在启动前明确给出。 -CLI 与 MCP 的操作都会进入活动历史,记录命令、Session、时间和生命周期状态。重复运行同一命令不会被去重;多行命令完整保留,历史可以分页查看,重新打开 UI 后仍可恢复。活动记录在本地加密,不进入云同步。 +![编程 Agent 启动器:项目目录、会话模式、访问模式和初始提示词](docs/assets/screenshots/tour-agent-launcher.png) -需要在同一个 shell 里协作时使用共享交互终端;不想打断人的提示符时使用独立 exec,命令仍然出现在活动历史中。**输入发送成功不等于远端命令执行成功**,界面和接口区分这两种语义。 +Agent 工作时,不必一直相信它的口头总结。打开 **Workspace changes / 工作区变更**,旁边就能看到分支、变动文件和逐行差异。一个标签里写本地代码,另一个标签里查看远程环境,既保留上下文,也不会偷偷把本地项目和远端执行环境混为一谈。 -Agent 新建会话后,UI 自动补上标签,但保留人的当前选择。同一服务器的多个连接按 Session 身份分别管理。会话能否在关闭 GUI 后继续存在取决于所属进程:daemon 持有的会话可以继续运行,GUI 持有的连接不会在退出其所属进程后凭空保留。 +![示例 Agent 终端旁的真实 Git 变更列表与逐行差异](docs/assets/screenshots/tour-agent-review.png) -### 少管理窗口,多保留上下文 +*这些 Agent 需要各自的安装、登录和模型订阅。VibeShell 提供启动和工作区整合,不附送模型账户;图中的 Agent 输出为明确标注的演示文本。* -统一连接启动器支持搜索、列表/卡片视图,将 SSH、本地 shell 和编程 Agent 入口放在一起。提供键盘焦点管理、明暗主题与减少动态效果设置;卡片动画使用浏览器原生能力。 +## 小事情也顺手,没开 AI 也一样 -终端与文件可以分屏、独立窗口展示。调整布局时保留尚未保存的文件编辑,配合指令历史、片段、上下文操作和明确的错误提示,减少复制粘贴和不确定的“成功”。 +只是忘了一个参数,不应该先打开聊天框。内置补全可以提示命令、子命令和选项,附带说明,并结合指令历史给出候选;行内提示和键盘可操作的列表就放在光标附近。经常用的命令可以保存为片段,临时检查则用 **Quick Cmd / 快捷命令** 查看输出,不占用正在操作的交互提示符。 -![连接启动器](docs/assets/screenshots/server-launcher.png) +还可以单独开启 **AI 命令预测**,配置自己的 OpenAI 兼容接口或 Claude 接口、模型和密钥,让它补出你正在输入的后半句。它只给建议,不会替你执行;这和启动一个完整的编程 Agent 是两个入口。 -### 文件工作就在会话旁边 +**AI 预测默认关闭。** 开启后,当前输入、近期命令历史和本地补全候选会发送给你配置的提供商。不能离开本机的内容不要用于这个功能;普通补全不依赖模型接口。 -SFTP 支持分栏和图标浏览、多选、复制路径及传输进度;本地和远程文件可作为工作区标签打开,支持文本/代码、图片、PDF、媒体和归档预览。 +终端使用 xterm.js,在可用时使用 WebGL,并对输入和输出做批处理来减少界面开销。这些是为了交互响应,不是“让 SSH 带宽翻倍”的承诺。 -传输优先保证完整性:有界分块读写,下载等待本地写入完成后才报告成功;同步识别同大小内容变更,删除时遵守排除规则和嵌套 `.gitignore`;本地同步拒绝重叠的源/目标目录。内容比较可能增加远程读取流量,这是完整性与流量之间的明确取舍,并非节省带宽的承诺。 +## 找连接,不必先想 IP -![SFTP 工作流](docs/assets/screenshots/sftp-workflow.png) +统一启动器把 **SSH、本地 Shell 和编程 Agent** 放在一起。搜索已保存的服务器,在紧凑列表和卡片之间切换,用分组与标签整理环境。已有连接的提示和单独的新会话入口,让“回到刚才的工作”和“再开一个连接”更好区分。 -### 改密码或私钥,不必删掉服务器重建 +![可搜索的连接卡片、分组与已有会话入口](docs/assets/screenshots/tour-connections.png) -服务器编辑支持新密码、替换私钥文件/内容,以及修改或清空私钥口令。未修改字段保留旧值,不会把已有秘密读回表单。服务器资料、改名与凭据修改在同一事务中成功或回滚。 +已有 OpenSSH、PuTTY、Tabby 配置可以先预览再导入;内网目标可以配置跳板机。第三方保存的密码不会被顺手复制过来,PuTTY `.ppk` 私钥需要先转换成 OpenSSH 格式。 -这里修改的是 **VibeShell 保存的登录信息**,不是远端系统账号的真实密码。登录私钥正确,也不代表可以绕过服务器主机身份验证;未知或变更的主机指纹仍需正确处理。 +换密码或私钥,也不必删掉服务器重建。编辑时,没动的字段保留原值,不把已有秘密读回表单;服务器资料和凭据一起保存,失败一起回滚。这里改的是 **VibeShell 保存的登录信息**,不是远端系统账号本身的密码。 -### 原生自动化,共用目标和会话 +## 文件就在会话旁边,本地文档也不例外 -Rust 编写的 `vibeshell` 可执行文件不需要 Node.js 或桌面窗口即可运行,按需启动原生 daemon,复用已保存配置。daemon 已持有会话时,GUI 不替换其活动 socket;终端、SFTP、隧道、录制和数据库探测请求交由实际持有连接的进程执行。 +通过 SFTP 打开远程配置,或者按 **⌘/Ctrl+O** 打开本地文件,都能获得独立文件标签。本地文件不需要先建 SSH 连接;关掉最后一个终端,也不会把本地笔记一起关掉。 -本地 Agent 启动器可通过真实 PTY 启动 Claude Code、Codex、OpenCode、Pi 等工具,并展示仓库状态与差异。这些工具需单独安装和配置;VibeShell 不附带模型订阅,也不接管其账户凭据。 +文本和代码可以编辑、语法高亮,Markdown 可以看源码、预览,或左右对照。SFTP 有分栏和图标浏览、多选、复制路径与上传下载进度,还可以查看支持的图片、PDF、媒体和归档。看手册、查日志、改配置,不用分别记住几个窗口的位置。 -### 插件不只有按钮,也有 AI 接口 +终端、文件和插件可以分屏、移动,文档也能放进独立窗口。调整布局不等于丢掉未保存内容;本地文本保存前会检查是否被其他程序改过,发现冲突就拒绝悄悄覆盖,截断读取也不会被当成完整文件保存。 -内置及符合规范的导入插件都提供安装状态、权限、动作参数和当前参考文档。**主 Skill 负责导航,详细用法按插件读取**,避免每次给 Agent 加载一整本手册。 +目录传输同样重视“传对”:分块读写、写完才报下载完成、识别大小相同但内容不同的修改,删除多余文件时保护排除项和嵌套 `.gitignore`。比较内容可能多读一些远程数据,这是可靠性与流量的取舍。 + +[本地文件、Markdown 支持范围与编辑限制](docs/local-files-and-css-themes.md) + +## 有时用命令,有时直接看一张表 + +查看容器、CPU 或数据库时,不一定每次都想读一屏原始输出。可以在当前会话旁打开插件,而不是再去另一个管理工具里填写一遍服务器地址。 + +| 正在处理什么 | 内置视图与工具 | +| --- | --- | +| 主机变慢、服务异常 | 性能、进程、系统日志、网络、磁盘 | +| 服务与基础设施 | Docker 容器、Kubernetes Pod、Cron、Systemd | +| 数据与代码 | 数据库、Redis、Git 工作区 | + +这 **12 个内置插件** 也不是只有人能点的按钮。Agent 可以直接发现安装状态、读取动作参数、按需拿到当前用法,再通过 CLI 或 MCP 使用: ```bash vibeshell plugins list --installed --json @@ -79,73 +106,60 @@ vibeshell plugins docs server-performance vibeshell plugins run server-performance status --session SESSION_ID --inputs '{}' ``` -先确认插件已安装、已启用,并将 `SESSION_ID` 替换为真实会话。`describe` 返回机器可读参数结构;`docs` 从当前有效的插件声明生成文档,也适用于导入插件。 +先确认插件已安装、已启用,再替换真实 Session ID。主 Skill 只做导航,详细说明在 `references/.md`;符合规范的导入插件也走同一套接口,文档来自当前有效的插件声明,不靠 Agent 猜命令。 -| 类别 | 内置插件 | -| --- | --- | -| 主机运维 | 性能、进程、系统日志、网络、磁盘 | -| 服务与基础设施 | Docker、Kubernetes、Cron、Systemd | -| 数据与开发 | 数据库、Redis、Git 工作区 | +读文档不等于自动授权,也不会替你安装远程软件。Docker、Kubernetes 和数据库工具仍需要目标环境及权限;远程主机性能采集当前依赖 Linux `/proc`。 + +[插件规范](docs/plugin-spec.md) · [Agent 和插件参考索引](skills/vibeshell/SKILL.md#plugin-discovery-and-references) + +## 工作区按你的习惯来 + +**少管窗口,多留上下文。** 终端、文档和插件可以分屏、重新排列、移到独立窗口,还能保存布局再回来。恢复布局不等于断掉的网络连接可以跨进程重启继续存在。 -共 12 个内置插件。CLI 和 MCP 复用启用状态、权限与输入校验,不因为文档里有示例就自动安装、授权或提权。MCP 经由真实人类审批通道确认,而不是相信模型传入的批准标志。目标机器仍需具备相应工具和权限;有插件声明不等于已经安装 Docker 或 Kubernetes。 +**长时间工作,也要舒服。** 明暗主题、跟随系统外观、终端字体与光标设置、键盘导航、减少动态效果都在。应用界面有英文和简体中文;日文 README 是文档翻译,不代表已有日文 UI。 -[插件规范](docs/plugin-spec.md) · [协作与接口说明](docs/AGENT_COLLABORATION.md) · [插件参考索引](skills/vibeshell/SKILL.md#plugin-discovery-and-references) +**不只换几个颜色。** 自定义 CSS 支持即时预览、应用保存、导入导出和本地背景图片,可以改间距、圆角、文档排版。如果主题把按钮藏没了,**⌘/Ctrl+Shift+F12** 或原生菜单里的 *Disable Custom CSS* 可以停用它。只应用可信主题,CSS 里的远程 URL 也会产生网络请求。 -## 从已有服务器开始 +[自定义 CSS 与恢复方法](docs/local-files-and-css-themes.md) · [主题起点](themes/vibecode-starter.css) -从 [Releases](https://github.com/veithly/vibeshell/releases) 选择与你平台和架构匹配的已发布版本,不使用尚未完成的草稿产物。 +## 该有的 SSH 工具,没有丢 -| 平台 | 桌面安装包 | 独立 CLI | +本地转发、SOCKS5、反向转发,以及会话录制和回放都在。保存隧道配置,减少重复设置;会话结束也会清理关联隧道和录制。把监听地址从回环改为对外开放之前,先确认影响范围。 + +可选的 **Gist / WebDAV 加密同步** 可以同步服务器元数据、分组、片段和插件安装信息,方便在自己的设备之间延续设置。它不是 VibeShell 托管的 SSH 中继。登录凭据、主机信任、活动终端和 Agent 操作历史不进入这份同步;提供商令牌、恢复材料与导出内容仍需自己妥善保护。 + +SSH 在认证前检查服务器身份,经过跳板时也验证实际目标。凭据和 Agent 操作历史采用本地加密存储,但不是 OS Keychain 托管,也不能抵挡已被攻陷的本机账户。私钥正确,并不是接受陌生服务器指纹的理由。 + +## 从你已有的服务器开始 + +在 [Releases](https://github.com/veithly/vibeshell/releases) 下载对应平台的桌面包或独立 CLI。 + +| 平台 | 桌面安装包 | 原生 CLI | | --- | --- | --- | -| macOS Apple Silicon / Intel | 对应架构 `.dmg` | 对应架构 `.tar.gz` | +| macOS Apple Silicon / Intel | 对应架构 `.dmg` | `.tar.gz` | | Windows x64 | `.exe` / `.msi` | `.zip` | | Linux x64 | `.AppImage` / `.deb` | `.tar.gz` | -Apple Developer ID 签名和公证情况以发布说明为准;本地 ad-hoc 签名不是 Apple 公证。移动端仍属实验性支持,不具备完整桌面功能。 - -独立 CLI 压缩包带有 `install.sh` 或 `install.ps1`,阅读后执行即可;详见 [CLI 安装说明](cli/README.md)。桌面包内置 CLI,Skill 安装器会向支持的 Agent 目录写入主文档和插件参考文档。 +桌面内置 CLI;独立 CLI 压缩包附带 `install.sh` / `install.ps1`,请阅读后执行。Rust 原生 CLI 本身不依赖 Node.js。[CLI 安装说明](cli/README.md) ```bash -vibeshell version -vibeshell import auto --dry-run -# 先审阅预览,再正式导入。 -vibeshell import auto +vibeshell import auto --dry-run # 审阅后去掉 --dry-run 正式导入。 vibeshell servers vibeshell ssh my-server -``` - -将 `my-server` 换成已保存名称。支持导入 OpenSSH、PuTTY、Tabby 配置,但刻意不复制第三方保存的密码;PuTTY `.ppk` 需先转换为 OpenSSH 格式。也可以在 GUI 中添加和编辑服务器。**CLI 新建/删除服务器与 Teleport 不属于 1.1.0 已交付功能。** - -```bash vibeshell ssh my-server -- uname -a vibeshell sessions -# 使用上一步真实返回的别名,不一定是 001。 -vibeshell ssh-session 001 -- pwd vibeshell sftp my-server ls /srv/app -vibeshell sftp my-server get /srv/app/config.toml ./config.toml ``` -复杂引号或多行脚本使用 `--command-file ./remote-command.sh` 或 `--command-stdin`。只在需要独立连接时使用 `--new`。不要把密码和私钥写进命令行参数或 Agent 提示词。 - -## 转发、录制和可选同步 +把 `my-server` 换成已保存名称。使用 `sessions` 返回的别名执行 `vibeshell ssh-session ALIAS -- pwd`;需要另开连接时再加 `--new`。复杂引号或多行脚本使用 `--command-file` / `--command-stdin`。不要把密码和私钥写进参数或 Agent 提示词。 -本地转发、SOCKS5、反向转发提供监听就绪检查、取消和半关闭处理;会话结束时清理关联隧道与录制。将监听地址从回环地址改为对外开放前,应确认影响范围。 +CLI 可按需启动 daemon,GUI 能接入已有会话。连接依赖实际持有它的进程:daemon 的会话可在 GUI 关闭后继续,GUI 自己持有的连接则会随该进程退出而结束。升级时应让桌面与 CLI 保持一致,重启前保存正在做的工作。 -可选加密同步通过你配置的 Gist 或 WebDAV 保存服务器元数据、分组、片段和插件安装信息,不通过 VibeShell 托管的 SSH 中继。登录凭据、主机信任、活动终端与 Agent 操作历史不进入该同步。请保护提供商令牌、恢复材料和本地导出文件,也不要假定所有插件设置都不敏感。 +Apple 签名和公证以每次发布说明为准,ad-hoc 签名不是 Apple 公证。移动端仍是实验性支持,常见 OpenSSH 测试也不能覆盖每一种 MFA、硬件令牌或 SSH 实现。提议中的 CLI 新建/删除服务器和 Teleport 不属于当前 1.1.0 发布内容。 -## 安全和兼容性边界 +## 自己运行,或者参与开发 -SSH 认证前验证主机身份,经过跳板时验证真实目标。设备密钥和已保存凭据采用本地加密存储,Unix 下限制文件权限;这**不是 OS Keychain 托管**,也不能保护已经被攻陷的本机用户账户。 - -隔离 OpenSSH 测试覆盖常见密码、私钥、PAM keyboard-interactive、PTY、SFTP 与转发,但不等于支持所有 MFA、硬件令牌、网络设备和 SSH 实现。远程性能采样当前依赖 Linux `/proc`。 - -Agent 仍需要监督:风险命令识别不是沙箱。`send-secret` 可避免真正的敏感提示输入进入活动日志,但不能阻止远程程序回显;也不能用它隐藏命令。收到不确定的网络错误时,不应自动重放可能已经执行过的修改命令。 - -请通过 [私密安全报告](https://github.com/veithly/vibeshell/security/advisories/new) 反馈安全问题,不在公开 Issue 中上传密码、私钥或可利用的生产环境详情。 - -## 开发与贡献 - -**功能、修复、文档 PR 一律先提交到 `dev`。** `main` 是稳定发布分支,只接受本仓库 `dev` 的发布晋级。`master` 保留历史,不再作为另一个开发入口。 +项目使用 Tauri 2、Rust、React、TypeScript 和 xterm.js。开发需要 Node.js 22.12+、当前稳定 Rust,以及系统对应的 [Tauri 前置依赖](https://v2.tauri.app/start/prerequisites/)。 ```bash git clone --branch dev https://github.com/veithly/vibeshell.git @@ -154,25 +168,18 @@ npm ci npm run tauri -- dev ``` -需要 Node.js 22.12+、当前稳定 Rust(清单最低要求 1.89),以及所在系统的 [Tauri 构建前置条件](https://v2.tauri.app/start/prerequisites/)。 +提交前运行 `node scripts/check-release.mjs`、`npm test`、`npm run build`、`cargo fmt --all -- --check`、`cargo clippy --workspace --all-targets --locked -- -D warnings` 和 `cargo test --workspace --locked`。SSH 变更还可运行仅绑定回环地址的 Docker 夹具:`bash scripts/test-ssh-compatibility.sh`,不要拿自己的真实凭据库做回归测试。 -```bash -node scripts/check-release.mjs -npm test -npm run build -cargo fmt --all -- --check -cargo clippy --workspace --all-targets --locked -- -D warnings -cargo test --workspace --locked -# 可选:仅绑定回环地址的 Docker OpenSSH 回归。 -bash scripts/test-ssh-compatibility.sh -``` +**普通 PR 一律先到 `dev`。** `main` 只接受本仓库 `dev` 的发布晋级,`master` 保留历史。构建代码不等于允许覆盖正在使用的应用或结束别人的 SSH 会话。 -构建 CLI:`cargo build --release --locked -p vshell --bin vibeshell`。构建带 sidecar 的桌面包:`npm run build:desktop`。构建不代表可以自动覆盖正在使用的应用或终止现有 SSH。 +CI 构建和发版都改为**手动触发**:维护者在合并前对 PR 的最新提交手动运行 CI,推送代码或版本标签不会编译或发布。Release 必须从 `main` 运行,默认只生成草稿,明确勾选发布才会公开。必需检查、签名和完整源码校验仍然保留。 -[贡献指南](CONTRIBUTING.md) · [发布流程](docs/RELEASING.md) · [架构与 Agent 开发约定](AGENTS.md) +Docker 清单改进以及容器独立会话、出站代理的剩余工作见[九月 issue 审查](docs/ISSUE_TRIAGE_2026-09.md)。Docker 插件执行命令不等于持久容器会话,SSH 的 SOCKS 转发监听也不等于出站代理配置。 -## 许可证 +[贡献指南](CONTRIBUTING.md) · [架构](AGENTS.md) · [发布流程](docs/RELEASING.md) · [协作接口](docs/AGENT_COLLABORATION.md) -VibeShell 从 **1.1.0** 起整体采用 **GNU GPL 第 3 版,仅此版本(`GPL-3.0-only`)**。参见 [LICENSE](LICENSE) 和 [NOTICE](NOTICE)。旧 MIT 版本的既有授权不被追溯撤销,[原 MIT 声明](licenses/legacy-MIT.txt) 对之前按该许可提供的代码部分予以保留;第三方组件保留各自许可与版权声明。 +安全问题请走 [私密报告](https://github.com/veithly/vibeshell/security/advisories/new),不要在 Issue 里贴真实凭据。审批不是沙箱,敏感输入保护也不能阻止远端程序回显;响应丢失后,不应自动重放可能已执行的修改命令。 + +## 许可证 -发布下载提供对应源码和许可声明。在法律允许范围内,本软件不提供担保。 +VibeShell 从 1.1.0 起整体采用 **GPL-3.0-only**。参见 [LICENSE](LICENSE)、[NOTICE](NOTICE) 和 [保留的 MIT 声明](licenses/legacy-MIT.txt)。旧 MIT 授权不追溯撤销,第三方组件保留自己的许可证;发布下载提供对应源码及声明。在法律允许范围内,软件不提供担保。 diff --git a/docs/ISSUE_TRIAGE_2026-09.md b/docs/ISSUE_TRIAGE_2026-09.md new file mode 100644 index 0000000..1b07532 --- /dev/null +++ b/docs/ISSUE_TRIAGE_2026-09.md @@ -0,0 +1,85 @@ +# September 2026 PR and issue review + +Reviewed on **2026-09-29**. This document distinguishes implemented changes from proposed work. The application version remains 1.1.0; changing source does not update an installed application or publish a release. + +## Pull requests + +| PR | Decision | Evidence and remaining work | +| --- | --- | --- | +| [#15](https://github.com/veithly/vibeshell/pull/15) | Merged into `main` | Workflow-only annotated-tag checkout and release metadata fixes; all required checks passed. | +| [#16](https://github.com/veithly/vibeshell/pull/16) | Merged into `dev` | Three-language product tour and five screenshots of real components using synthetic data. The fixture is a separate development-only loopback entry; all required checks passed. | +| [#10](https://github.com/veithly/vibeshell/pull/10) | Keep open; changes required | Head `e8fd8d0` has not changed since the September 18 review and conflicts with `dev`. Do not merge by merely resolving textual conflicts. | +| [#11](https://github.com/veithly/vibeshell/pull/11) | Keep open; changes required | Head `b81e609` is unchanged since review, conflicts with `dev`, and includes #10. Session and file-operation contracts still need implementation and regression fixtures. | + +### #10: preserve CLI usability without credential regressions + +The headless create/delete workflow is useful. The blockers are in the implementation, not in the feature: + +- `cli/src/commands/server.rs::env_nonempty` and `commands/server.rs::add_server_spec` trim passwords/passphrases. Presence checks must not modify secret bytes, including whitespace-only secrets. +- Creating a group, server and encrypted credentials must be one database transaction, including associations and sync outbox changes. Inject a failure at each mutation and assert rollback. Deleting credentials before a failing metadata delete must not leave a broken saved server. +- `AddServerSpec` derives `Debug` and carries credentials; the new IPC variant falls through the existing log-redaction match. Add sentinel tests for password, key material and passphrase, including activity/error paths. +- The proposed GUI adapter drops `ServerInput.credential_id` when constructing `AddServerSpec`. Preserve existing credential associations and add a GUI-backend regression test. +- The new delete CLI currently has no confirmation flag or prompt. Require explicit confirmation, with a deliberate noninteractive option; refuse ambiguous names. Invalid `host:port` inputs must fail rather than becoming literal hostnames. + +Rebase on `dev`, reuse the current transactional storage/credential paths, then run parser, rollback, redaction and full workspace checks. No unsafe contributor code was executed as part of this review. + +### #11: split transport support from unimplemented file semantics + +First resolve #10. A smaller initial Teleport PR should establish a reliable session lifecycle and capability reporting. File features may explicitly report unsupported until their normal contracts are met. + +Use asynchronously drained, bounded stdout/stderr, concurrent stdin, timeouts and cancellation for `tsh`. The current synchronous `Command.output()` and write-before-drain paths can hang or exhaust memory. Authentication failures and EOF must drive real state transitions; process spawn or a fixed sleep is not connection success. Preserve GUI/daemon ownership, quick commands, plugin execution and teardown. + +Do not replace exclusive file creation with `cat >`, binary reads with lossy text, or structured directories with trimmed `ls` lines. Recursive `scp` is not directory synchronization: exclusions, nested ignore rules, deletion policy and actual transfer counts must be honored. Fixtures should cover binary bytes, whitespace/Unicode names, overwrite refusal, ignored-file retention, failed authentication, cancellation and bounded process I/O. + +## #9: Docker containers as independent sessions + +Issue: [#9](https://github.com/veithly/vibeshell/issues/9). + +### Implemented in this change + +The Docker Containers plugin now starts with a **Running containers** action and has a separate **Exited containers** action. Both use full container IDs and expose machine-readable state alongside status. A **Container inventory (JSON Lines)** action returns one Docker-formatted JSON object per line for agent consumers. The existing all-container action remains available; these queries do not start containers or elevate automatically. + +Container operands are separated from Docker options with `--`; inspect is restricted to container objects. Existing mutation confirmations and optional sudo controls remain. Plugin tests cover inventory filters, full IDs, read-only defaults and option-boundary protection. Generated references are shared by all three Skill distributions. + +This is inventory groundwork, **not** one-click container sessions. There is no new collapsible sidebar or container PTY in this change. `exec-command` remains a single noninteractive command. + +### Required next implementation + +Add an explicit container context to a child session: owning process, parent SSH server/session identity, immutable full container ID, selected user and working directory. Do not identify a live session only by a reusable container name. + +The interactive PTY and every independent command path must use that context. Today `Session::exec_command_with_stdin`, quick commands and plugin operations open SSH exec channels on the host. Merely sending `docker exec -it ...` into a terminal would leave agents executing on the host. Route GUI, CLI, MCP, quick commands and plugin execution consistently, and show the container context in tabs and activity records. + +Closing the container session must close its exec process, not stop the container or kill the parent session. Container termination must disconnect the child explicitly, never silently leave a host shell under a container label. Check actual exec success; preserve resize, input, cancellation and output replay. A stopped container stays stopped unless the user explicitly authorizes starting it. Shell selection must handle images without Bash or any shell with a clear error. + +Initially reject container-file operations that are not implemented. Host SFTP must never be presented as the container filesystem. A later file transport needs binary-safe reads, exclusive writes, path validation and real sync semantics. + +The UI should fetch inventory when the container panel opens, with refresh and visible permission/connection errors. Show running entries first and collapsed exited entries; use the full ID when opening a new tab. Avoid automatic polling or sudo on every SSH connection. Test two containers concurrently, agent-created tab synchronization, host/container execution identity, container stop/restart, parent disconnect and independent child cleanup using isolated fixtures. + +## #12: outbound proxy support + +Issue: [#12](https://github.com/veithly/vibeshell/issues/12). **Design only; proxy protocols are not implemented by this change.** Existing SSH SOCKS forwarding is a listener for traffic through an established SSH connection, not an outbound proxy setting. + +### Scope and existing network paths + +| Path | Current implementation | Required integration | +| --- | --- | --- | +| SSH host-key probe and authentication | Shared `SshClient::establish_connection` in `src-tauri/src/ssh/client.rs` | Establish the selected proxy tunnel before SSH; use the same route for probe and authentication. Preserve TOFU and hostname/port identity. | +| SSH exec, SFTP and forwarding | Channels on established SSH connections | Reuse the proxied transport rather than opening accidental direct connections. Include jump-host entry connections. | +| Cloud sync | `reqwest::Client` in `src-tauri/src/cloud_sync/providers.rs` | Apply the same policy, credential handling and bypass rules. | +| Model prediction and update metadata | Frontend `fetch` in `src/lib/aiCommandPrediction.ts` and `src/stores/updateStore.ts` | Route through a controlled native HTTP path or explicitly configured WebView networking; a Rust environment variable alone does not configure browser fetch. | +| Native updater, external agents and remote commands | Separate networking owners | Audit updater downloads separately. Locally launched tools and commands running on a remote host must have explicit scope; do not claim they inherit a global proxy automatically. | + +### Proposed delivery order and safety contract + +1. Define a shared proxy configuration with explicit direct/system/custom modes and per-server overrides. Store proxy credentials with the existing encrypted credential mechanism, not in URLs, command arguments or activity logs. Preserve exact password bytes; provide environment/stdin or a credential reference rather than `--proxy-auth user:pass` in argv. +2. Implement a timeout/cancellation-aware tunnel dialer for HTTP CONNECT and SOCKS5. Reuse the SSH stream connection entry point so host-key checks precede authentication. Distinguish local and proxy-side DNS and document supported authentication. No automatic direct fallback when a proxy is configured. +3. Add HTTPS-proxy TLS verification and SOCKS4/4a with an explicit DNS/IPv6 compatibility matrix. Do not disable certificate verification to support a corporate proxy; expose a deliberate trust configuration. Unsupported destination/protocol combinations must fail clearly. +4. Apply the policy to the HTTP and updater paths above before advertising application-wide coverage. Expose unsupported/external traffic honestly. Redact proxy credentials in connection errors and give actionable authentication, certificate, DNS and timeout diagnostics. + +Use loopback proxy fixtures with generated credentials. Cover CONNECT refusal, SOCKS authentication, malformed replies, timeout, cancellation, IPv4/IPv6, local versus proxy DNS, TLS rejection, exact credential bytes and GUI/CLI parity. Assert that an unavailable proxy never causes a direct connection. No real saved servers or live proxy credentials belong in these tests. + +## Integration and release boundaries + +Prioritize #10's credential-safe foundation and #9's shared execution-context design before adding another independent transport. Proxy support can then use the shared connection boundary; Teleport needs its own bounded process transport rather than pretending to be raw SSH/SFTP. + +CI compilation and releases are now explicitly manual; all required merge checks remain enforced. See [contribution checks](../CONTRIBUTING.md#manual-github-checks) and [release procedure](RELEASING.md). This review does not close #9 or #12, publish a new version, install a new binary, or terminate user sessions. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 3f15732..bc5b418 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -2,31 +2,56 @@ ## Branch contract -`dev` is the default branch for all normal PRs. `main` receives only release promotions from this repository's `dev`. `master` is historical. Branch protection requires the PR-target check and CI; force pushes and branch deletion are disallowed on active branches. +`dev` is the default branch for all normal PRs. `main` receives only release promotions from this repository's `dev`. `master` is historical. Branch protection still requires PR Target, Frontend Check, Clippy Lint and all three Rust Check jobs; force pushes and branch deletion are disallowed on active branches. -A release is an explicit maintainer action, not an automatic side effect of a documentation or source push. There is no auto-increment bot commit. +**Both CI compilation and release packaging/publication are manual-only.** A push, PR or version tag starts neither workflow. The metadata-only PR Target workflow remains automatic. There is no auto-increment bot commit, and no workflow automatically dispatches another workflow. + +Manual CI uses two small reporting jobs to bridge GitHub's PR-check event restriction: initialize the existing required commit statuses as pending, then publish results from actual jobs on the same SHA and run attempt. Builds have no status-write permission. Missing/skipped/failed evidence cannot become green, and superseded runs stop reporting. Branch protection still requires all five CI contexts from GitHub Actions plus PR Target; there is no administrator bypass. See [GitHub's required-check documentation](https://docs.github.com/en/pull-requests/how-tos/merge-and-close-pull-requests/troubleshooting-required-status-checks#checks-from-some-workflow-jobs-are-not-evaluated). ## Prepare and promote Update the workspace version, npm manifest and lockfile, the three local Cargo.lock packages, Tauri version, Codex plugin version and Claude marketplace versions in one PR to `dev`. Independent built-in plugin versions are not the application version and should change only when that plugin changes. -Run `node scripts/check-release.mjs`, regenerate/check plugin references, run frontend build/tests, strict Clippy and all workspace tests. Use the loopback SSH fixture for transport changes. Review dependency advisories, licensing and compatibility; do not hide failed scans in release notes. +Run `node scripts/check-release.mjs`, `node --test scripts/tests/*.test.mjs`, regenerate/check plugin references, run frontend build/tests, strict Clippy and all workspace tests. Use the loopback SSH fixture for transport changes. Review dependency advisories, licensing and compatibility; do not hide failed scans in release notes. + +After reviewing a normal PR, run **Actions → CI → Run workflow**, selecting its head branch, or `gh workflow run ci.yml --ref `. New commits require a fresh manual run. Required checks are not waived just because there is no automatic trigger. See [CONTRIBUTING](../CONTRIBUTING.md#manual-github-checks) for fork PRs. -Open the promotion PR with `gh pr create --base main --head dev`. Wait for required checks. Merge using a merge commit so the tested integration ancestry is preserved. No feature PR goes directly to `main`. +Open the promotion PR with `gh pr create --base main --head dev`, then explicitly run `gh workflow run ci.yml --ref dev`. Wait for all required checks and merge using a merge commit so integration ancestry is preserved. No feature PR goes directly to `main`. Because that merge creates a new commit, its release validation must run on the resulting `main` commit, not merely on the earlier PR head. -## Tag and publish +## Tag, verify and build a draft ```bash git fetch origin git switch main git merge --ff-only origin/main -git tag -a v1.1.0 -m 'VibeShell 1.1.0' -git push origin v1.1.0 +# The version must already have been prepared and promoted above. +TAG="v$(node -p 'require("./package.json").version')" +git tag -a "$TAG" -m "VibeShell ${TAG#v}" +git push origin "$TAG" + +# Pushing the tag does nothing else. Explicitly test that exact commit: +gh workflow run ci.yml --ref "$TAG" +``` + +Never move or recreate an existing tag. After the tag's manual CI run has succeeded, run **Actions → Release → Run workflow**, select branch **main**, enter the existing tag and leave **publish** unchecked. The CLI equivalent is: + +```bash +gh workflow run release.yml --ref main -f tag="$TAG" -f publish=false ``` -Use the actual prepared version instead of copying this example for a different release. Tags are immutable: never move a published tag to a different commit. A failed workflow can be rerun for the same tag through **Release → Run workflow**, specifying that existing tag and running the workflow from `main`. +The workflow only accepts dispatches from `main`. It validates that the tag resolves to a commit on `main` and that its version matches every application manifest. The latest manual CI run for that exact commit must have succeeded in this repository's CI workflow, including frontend, Clippy and Linux/Windows/macOS Rust jobs. An old successful check, an automatic run, a skipped platform or a newer failed attempt is insufficient. CI run/job validation uses the current gate from `main`, even when a selected tag predates that helper. + +Release validates updater signing, creates/retains a draft, builds desktop and native CLI packages for Windows x64, macOS arm64/x64 and Linux x64, and assembles matching source materials. Only after every platform, source bundle, signature and checksum check succeeds are the complete assets uploaded. With the default `publish=false`, the release stays a draft and is not marked latest. + +## Publish explicitly + +To authorize publication, explicitly select **publish** in a manual Release run from `main`, or: + +```bash +gh workflow run release.yml --ref main -f tag="$TAG" -f publish=true +``` -The release workflow validates that the tag resolves to a commit on `main` and its version matches every application manifest. It validates updater signing, creates/retains a draft, builds desktop and native CLI packages for Windows x64, macOS arm64/x64 and Linux x64, and assembles matching source materials. Only after all platforms and source packaging succeed does it publish `latest.json`, checksums and the release. +This is a full verified build-and-publish run, not a shortcut that publishes unchecked files from an earlier draft. A separate draft build is optional; a maintainer may choose `publish=true` on the first authorized run. Pushing a tag, running CI, or completing a draft build never publishes on its own. A failed or partial run must remain a draft. Do not mark it latest or overwrite a previously published release to work around failures. Publishing has no automatic rollback: a regression requires a new version with a tested fix. diff --git a/docs/assets/screenshots/tour-agent-approval.png b/docs/assets/screenshots/tour-agent-approval.png new file mode 100644 index 0000000..5e84b75 Binary files /dev/null and b/docs/assets/screenshots/tour-agent-approval.png differ diff --git a/docs/assets/screenshots/tour-agent-launcher.png b/docs/assets/screenshots/tour-agent-launcher.png new file mode 100644 index 0000000..a5b7e67 Binary files /dev/null and b/docs/assets/screenshots/tour-agent-launcher.png differ diff --git a/docs/assets/screenshots/tour-agent-review.png b/docs/assets/screenshots/tour-agent-review.png new file mode 100644 index 0000000..dac793b Binary files /dev/null and b/docs/assets/screenshots/tour-agent-review.png differ diff --git a/docs/assets/screenshots/tour-collaboration.png b/docs/assets/screenshots/tour-collaboration.png new file mode 100644 index 0000000..ab319ff Binary files /dev/null and b/docs/assets/screenshots/tour-collaboration.png differ diff --git a/docs/assets/screenshots/tour-connections.png b/docs/assets/screenshots/tour-connections.png new file mode 100644 index 0000000..14ac909 Binary files /dev/null and b/docs/assets/screenshots/tour-connections.png differ diff --git a/plugins/builtin/docker-containers/plugin.json b/plugins/builtin/docker-containers/plugin.json index abe2709..1d90136 100644 --- a/plugins/builtin/docker-containers/plugin.json +++ b/plugins/builtin/docker-containers/plugin.json @@ -3,7 +3,7 @@ "id": "docker-containers", "name": "Docker Containers", "description": "Inspect and manage containers, images, live resource usage and recent container logs through the remote Docker CLI.", - "version": "1.4.0", + "version": "1.5.0", "author": "VibeShell", "category": "containers", "icon": "box", @@ -18,6 +18,53 @@ "entry": { "type": "commands", "actions": [ + { + "id": "running-containers", + "name": "Running containers", + "description": "List running containers with full IDs; paused and restarting containers are not shell-ready.", + "program": "docker", + "args": [ + "ps", + "--filter", + "status=running", + "--no-trunc", + "--format", + "{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.State}}\t{{.Status}}\t{{.Ports}}" + ], + "allowSudo": true, + "output": { + "kind": "table", + "columns": ["ID", "Name", "Image", "State", "Status", "Ports"] + } + }, + { + "id": "exited-containers", + "name": "Exited containers", + "description": "Inspect exited containers separately without starting or restarting them.", + "program": "docker", + "args": [ + "ps", + "--all", + "--filter", + "status=exited", + "--no-trunc", + "--format", + "{{.ID}}\t{{.Names}}\t{{.Image}}\t{{.State}}\t{{.Status}}\t{{.Ports}}" + ], + "allowSudo": true, + "output": { + "kind": "table", + "columns": ["ID", "Name", "Image", "State", "Status", "Ports"] + } + }, + { + "id": "container-inventory", + "name": "Container inventory (JSON Lines)", + "description": "Read all container states and full IDs as one JSON object per line, not a JSON array. This does not create a container session.", + "program": "docker", + "args": ["ps", "--all", "--no-trunc", "--format", "{{json .}}"], + "allowSudo": true + }, { "id": "containers", "name": "Containers", @@ -94,6 +141,7 @@ "logs", "--tail", "200", + "--", "{{input.container}}" ], "inputs": [ @@ -113,6 +161,9 @@ "program": "docker", "args": [ "inspect", + "--type", + "container", + "--", "{{input.container}}" ], "inputs": [ @@ -128,10 +179,11 @@ { "id": "exec-command", "name": "Run in container", - "description": "Run one non-interactive shell command inside a container.", + "description": "Run one non-interactive shell command inside a container. This does not create a persistent container session or change the host session.", "program": "docker", "args": [ "exec", + "--", "{{input.container}}", "sh", "-lc", @@ -161,6 +213,7 @@ "program": "docker", "args": [ "start", + "--", "{{input.container}}" ], "inputs": [ @@ -181,6 +234,7 @@ "program": "docker", "args": [ "stop", + "--", "{{input.container}}" ], "inputs": [ @@ -201,6 +255,7 @@ "program": "docker", "args": [ "restart", + "--", "{{input.container}}" ], "inputs": [ diff --git a/plugins/src/lib.rs b/plugins/src/lib.rs index e1334b1..752bbde 100644 --- a/plugins/src/lib.rs +++ b/plugins/src/lib.rs @@ -848,6 +848,9 @@ mod tests { assert_eq!( action_ids, HashSet::from([ + "running-containers", + "exited-containers", + "container-inventory", "containers", "stats", "images", @@ -892,6 +895,82 @@ mod tests { .starts_with("sudo -n docker ps")); } + #[test] + fn docker_inventory_is_read_only_and_keeps_full_container_identity() { + let catalog = builtin_catalog().unwrap(); + let docker = catalog + .iter() + .find(|p| p.id == "docker-containers") + .unwrap(); + let PluginEntry::Commands { actions } = &docker.entry else { + panic!("expected Docker command actions"); + }; + assert_eq!(actions[0].id, "running-containers"); + for (id, state) in [ + ("running-containers", Some("status=running")), + ("exited-containers", Some("status=exited")), + ("container-inventory", None), + ] { + let action = actions.iter().find(|a| a.id == id).unwrap(); + assert_eq!(action.program, "docker"); + assert_eq!(action.args[0], "ps"); + assert!(action.args.iter().any(|arg| arg == "--no-trunc")); + assert!(!action.requires_confirmation); + assert!(!action.elevate); + if let Some(state) = state { + assert!(action + .args + .windows(2) + .any(|pair| pair == ["--filter", state])); + assert_eq!(action.output.kind, PluginOutputKind::Table); + assert_eq!(action.output.columns.len(), 6); + } else { + assert!(action.args.iter().any(|arg| arg == "{{json .}}")); + } + assert_eq!( + action.args.iter().any(|arg| arg == "--all"), + id != "running-containers" + ); + assert!(render_command(action, &BTreeMap::new(), false, false).is_ok()); + } + } + + #[test] + fn docker_container_operands_cannot_be_interpreted_as_options() { + let catalog = builtin_catalog().unwrap(); + let docker = catalog + .iter() + .find(|p| p.id == "docker-containers") + .unwrap(); + let PluginEntry::Commands { actions } = &docker.entry else { + panic!("expected Docker command actions"); + }; + for id in [ + "logs", + "inspect", + "exec-command", + "start-container", + "stop-container", + "restart-container", + ] { + let action = actions.iter().find(|a| a.id == id).unwrap(); + assert!(action + .args + .windows(2) + .any(|pair| pair == ["--", "{{input.container}}"])); + let mut inputs = + BTreeMap::from([("container".to_string(), serde_json::json!("--privileged"))]); + if id == "exec-command" { + inputs.insert("command".to_string(), serde_json::json!("id")); + } + let rendered = render_command(action, &inputs, false, false).unwrap(); + assert!(rendered.contains(" -- --privileged")); + if id == "exec-command" || id.ends_with("-container") { + assert!(action.requires_confirmation); + } + } + } + #[test] fn redis_plugin_falls_back_to_docker_exec() { let catalog = builtin_catalog().expect("built-in catalog should be valid"); diff --git a/scripts/check-release-ci.mjs b/scripts/check-release-ci.mjs new file mode 100644 index 0000000..04ffc4c --- /dev/null +++ b/scripts/check-release-ci.mjs @@ -0,0 +1,64 @@ +#!/usr/bin/env node +// Validate evidence from the CI workflow, not unrelated checks with similar names. +import { readFileSync } from 'node:fs'; +import { pathToFileURL } from 'node:url'; + +export const REQUIRED_JOBS = [ + 'Frontend Check', + 'Clippy Lint', + 'Rust Check (ubuntu-22.04)', + 'Rust Check (windows-latest)', + 'Rust Check (macos-latest)', +]; + +export function latestManualCiRun(payload, sha, repository) { + if (!Array.isArray(payload.workflow_runs)) throw new Error('Invalid CI workflow response'); + const runs = payload.workflow_runs.filter(run => + run.head_sha === sha && run.event === 'workflow_dispatch' + && run.path === '.github/workflows/ci.yml' + && run.head_repository?.full_name === repository + && run.repository?.full_name === repository); + const latest = runs.sort((a, b) => b.id - a.id)[0]; + if (!latest) { + throw new Error('Run CI manually on the exact release tag/commit before releasing'); + } + if (!Number.isSafeInteger(latest.id) || latest.id <= 0) { + throw new Error('The latest manual CI run for this commit has not completed successfully'); + } + return latest; +} + +export function selectManualCiRun(payload, sha, repository) { + const latest = latestManualCiRun(payload, sha, repository); + if (latest.status !== 'completed' || latest.conclusion !== 'success') { + throw new Error('The latest manual CI run for this commit has not completed successfully'); + } + return latest.id; +} + +export function validateCiJobs(payload) { + if (!Array.isArray(payload.jobs) || payload.total_count !== payload.jobs.length) { + throw new Error('Incomplete CI jobs response'); + } + for (const name of REQUIRED_JOBS) { + const job = payload.jobs.find(candidate => candidate.name === name); + if (job?.status !== 'completed' || job.conclusion !== 'success') { + throw new Error(`Required manual CI job has not passed: ${name}`); + } + } +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + try { + const [mode, file, sha, repository] = process.argv.slice(2); + if (!file || !['run', 'jobs'].includes(mode) || (mode === 'run' && (!sha || !repository))) { + throw new Error('Usage: check-release-ci.mjs run | jobs '); + } + const payload = JSON.parse(readFileSync(file, 'utf8')); + if (mode === 'run') console.log(selectManualCiRun(payload, sha, repository)); + else validateCiJobs(payload); + } catch (error) { + console.error(error.message); + process.exitCode = 1; + } +} diff --git a/scripts/readme-demo/README.md b/scripts/readme-demo/README.md new file mode 100644 index 0000000..557fb1b --- /dev/null +++ b/scripts/readme-demo/README.md @@ -0,0 +1,61 @@ +# Reproducing the README gallery + +These are screenshots of the real React application, not separately drawn interface mockups. `main.ts` imports `src/main.tsx` after installing the official Tauri mock bridge. Only the data and native responses are synthetic. + +## Isolation + +Use the dedicated development origin on port **1421**. The fixture refuses a production build or another origin. Do not use a production Tauri window, copy its local storage, or import saved servers. + +The six hosts are fictional documentation addresses in `192.0.2.0/24`. The project paths, account names, status readings, terminal output, agent transcript, Git diff and history entries are examples. No model or agent executable runs. AI prediction is disabled. Unknown native commands throw instead of forwarding to a real backend; example file writes affect only the in-memory map. The page has a visible synthetic-data label and a local-only connection policy. + +The fixture uses the dedicated origin's local storage for workspace layout. Reloading resets only VibeShell demo keys on that origin. It does not touch the installed application's database, key material or other browser origins. + +## Run + +From the repository root, with its normal development dependencies installed: + +```sh +node node_modules/typescript/bin/tsc -p scripts/readme-demo/tsconfig.json --noEmit +node node_modules/vite/bin/vite.js --host 127.0.0.1 --port 1421 --strictPort +``` + +Open `http://127.0.0.1:1421/scripts/readme-demo/index.html?scene=collaboration` in a disposable browser context. Capture the page viewport at **1440 × 900**, device scale **1**, after fonts and content have loaded. English is the default; `&lang=zh` uses the existing Chinese application UI. The gallery's English UI is shared across README translations. + +## Published scenes + +| Screenshot | Starting scene | Actual UI interaction | +| --- | --- | --- | +| `tour-collaboration.png` | `collaboration` | Open **Agent command history**. The terminal, host status and durable-activity components render fixture responses. | +| `tour-agent-approval.png` | `collaboration` | Queue the demo request with `window.__VIBESHELL_DEMO__.approveDemo()`; wait for the real approval dialog. Do not present the request as executed. | +| `tour-agent-review.png` | `coding` | **More actions → Workspace changes**; the first changed file opens in the real diff view. | +| `tour-connections.png` | `coding` | **New Session → SSH → Icon view**. | +| `tour-agent-launcher.png` | `coding` | **New Session → Coding Agent**; enter a sample brief without pressing Start. | + +The fixture also supports `scene=files` for the real Markdown file workspace and light theme. Its file view was inspected through DOM/accessibility output, but no file-workspace screenshot is included in this gallery. The demo-only plugin/status responses are not transport or service integration tests. + +The controller exposes `replay()` to refresh the synthetic terminal output, `newSession()` to exercise the existing session synchronization, and `blocked`, `calls`, `errors` for inspection. A capture must not hide an error or substitute fake UI around a missing response. Successful captured scenes had no unhandled page errors or unimplemented native calls. + +With ego-browser, reuse one task space for the whole gallery and finish it once. Its CLI reads stdin before running `-e`; when using a noninteractive command runner, append `< /dev/null` so the script receives EOF. Do not create repeated task spaces to diagnose a request waiting on stdin. + +## What this demonstrates + +These images show the presentation and user controls, not real SSH authentication, deployment, AI reasoning or a performance benchmark. GUI/CLI integration and native file/tunnel behavior require their own tests. The production entry never imports this fixture; no new runtime dependency was added. + +Feature descriptions in the three READMEs were traced to the actual implementations, including older capabilities rather than just the latest release: + +| Capability | Implementation | +| --- | --- | +| Command completion and optional AI prediction | `src/components/Terminal/useCompletion.ts`, `src/lib/aiCommandPrediction.ts` | +| Local agent launch modes and project selection | `src/components/CodingAgentLauncher/CodingAgentLauncher.tsx` | +| Git status and line-by-line review | `src/components/WorkspaceChangesPanel/WorkspaceChangesPanel.tsx` | +| Shared-session UI and operation history | `src/stores/sessionStore.ts`, `src/stores/agentActivityStore.ts`, `src/components/AgentActivityPanel/` | +| Human approvals | `src/components/AgentApprovalDialog/`, `src-tauri/src/mcp/guard.rs` | +| Quick command output | `src/components/QuickCommandDialog/` | +| Local/remote file tabs, Markdown and draft preservation | `src/components/FileWorkspace/`, `src/stores/fileWorkspaceStore.ts` | +| Custom CSS, preview and emergency recovery | `src/lib/customTheme.ts`, `src/components/Settings/CustomThemeEditor.tsx`, `docs/local-files-and-css-themes.md` | +| SFTP, synchronization integrity and excluded paths | `src/components/SftpPanel/`, `src-tauri/src/sftp/sync.rs` | +| Declarative plugins and agent-readable interfaces | `plugins/builtin/`, `src-tauri/src/plugins/agent.rs`, `cli/src/commands/plugins.rs` | +| Forwarding and session recording | `src-tauri/src/tunnel/`, `src-tauri/src/logging/` | +| Optional vault synchronization | `src/stores/cloudSyncStore.ts`, `src-tauri/src/cloud_sync/` | + +Keep this distinction when updating the gallery: **real components, synthetic data, bounded claims**. diff --git a/scripts/readme-demo/index.html b/scripts/readme-demo/index.html new file mode 100644 index 0000000..f0ec2fe --- /dev/null +++ b/scripts/readme-demo/index.html @@ -0,0 +1,14 @@ + + + + + + + VibeShell · isolated documentation demo + + +
+
DEMO · synthetic data · no server connection
+ + + diff --git a/scripts/readme-demo/main.ts b/scripts/readme-demo/main.ts new file mode 100644 index 0000000..093d497 --- /dev/null +++ b/scripts/readme-demo/main.ts @@ -0,0 +1,203 @@ +/// +// Documentation-only entry. Nothing in src/ imports this fixture. +// Real components + in-memory IPC; never delegate to a native backend. +import { mockIPC, mockWindows } from '@tauri-apps/api/mocks'; +import { emit } from '@tauri-apps/api/event'; +import type { PluginManifest, PluginRecord } from '../../src/plugins/types'; +import type { SessionInfo, LocalShellSessionInfo } from '../../src/stores/sessionStore'; + +if (!import.meta.env.DEV || !['127.0.0.1', 'localhost'].includes(location.hostname) || location.port !== '1421') { + throw new Error('Documentation demo requires the isolated loopback development origin on port 1421.'); +} +const scenario = new URLSearchParams(location.search).get('scene') ?? 'collaboration'; +const lang = new URLSearchParams(location.search).get('lang') === 'zh' ? 'zh' : 'en'; +const light = scenario === 'theme' || scenario === 'files'; +const time = Date.UTC(2026, 8, 18, 9, 42, 0); +const seconds = Math.floor(time / 1000); +const root = '/srv/northstar'; +const workspace = '/workspace/northstar'; +const blocked: string[] = []; +const calls: string[] = []; +const errors: string[] = []; +window.addEventListener('error', event => errors.push(event.message)); +window.addEventListener('unhandledrejection', event => errors.push(String(event.reason))); + +const servers = [ + { id: 'demo-api', name: 'API · staging', host: '192.0.2.10', group_id: 'demo-staging', tags: ['api', 'preview'] }, + { id: 'demo-worker', name: 'Workers · staging', host: '192.0.2.11', group_id: 'demo-staging', tags: ['jobs', 'docker'] }, + { id: 'demo-db', name: 'Postgres · private', host: '192.0.2.20', group_id: 'demo-data', tags: ['postgres', 'via bastion'], jump_host_id: 'demo-bastion' }, + { id: 'demo-cache', name: 'Redis · private', host: '192.0.2.21', group_id: 'demo-data', tags: ['redis', 'cache'], jump_host_id: 'demo-bastion' }, + { id: 'demo-bastion', name: 'Bastion · gateway', host: '192.0.2.30', group_id: 'demo-platform', tags: ['gateway'] }, + { id: 'demo-web', name: 'Web · preview', host: '192.0.2.40', group_id: 'demo-platform', tags: ['frontend', 'preview'] }, +].map(server => ({ port: 22, username: 'demo', auth_type: 'key_with_passphrase', credential_id: null, + created_at: seconds, updated_at: seconds, ...server })); +const ssh: SessionInfo[] = [ + { id: 'demo-session-api', server_id: 'demo-api', server_name: 'API · staging', state: 'connected', created_at: seconds, clients: 2 }, + { id: 'demo-session-worker', server_id: 'demo-worker', server_name: 'Workers · staging', state: 'connected', created_at: seconds, clients: 1 }, +]; +const local: LocalShellSessionInfo[] = [ + { id: 'demo-session-agent', shellId: 'zsh', shellName: 'Codex · northstar', cwd: workspace, agentId: 'codex', state: 'running', createdAt: seconds, clients: 1 }, +]; +const manifestModules = import.meta.glob('../../plugins/builtin/*/plugin.json', { eager: true, import: 'default' }); +const plugins: PluginRecord[] = Object.values(manifestModules).map(value => { + const manifest = value as PluginManifest; + return { manifest, source: 'builtin', installed: true, enabled: true, + grantedPermissions: manifest.permissions, settings: manifest.defaultSettings ?? {}, installedAt: seconds }; +}); +const markdown = `# Northstar deployment notes\n\nA small runbook, kept next to the terminal.\n\n## Before you deploy\n\n- [x] Review the retry change\n- [x] Check the staging health endpoint\n- [ ] Ask a human before restarting the service\n\n## Service map\n\n| Service | Port | Health |\n| --- | --- | --- |\n| API | 8080 | Ready |\n| Worker | 9090 | Ready |\n| Postgres | 5432 | Private network |\n\n## Verify the rollout\n\n\`\`\`sh\ncurl -fsS http://127.0.0.1:8080/health\ndocker compose logs --tail 30 api\n\`\`\`\n\n> Demo runbook. All hostnames and results in this gallery are synthetic.\n`; +const code = `import { setTimeout as delay } from 'node:timers/promises';\n\nexport async function fetchHealth(url: string) {\n for (let attempt = 0; attempt < 3; attempt++) {\n try {\n const response = await fetch(url, {\n signal: AbortSignal.timeout(5000),\n });\n if (!response.ok) throw new Error(\`HTTP \${response.status}\`);\n return await response.json();\n } catch (error) {\n if (attempt === 2) throw error;\n await delay(250 * 2 ** attempt);\n }\n }\n}\n`; +const files: Record = { + [`${root}/RUNBOOK.md`]: markdown, + [`${root}/src/health.ts`]: code, + [`${root}/compose.yaml`]: 'services:\n api:\n image: northstar/api:preview\n ports: ["127.0.0.1:8080:8080"]\n restart: unless-stopped\n worker:\n image: northstar/worker:preview\n environment:\n QUEUE: jobs-preview\n', +}; +const activity = [ + ['mcp:session_exec', 'docker compose ps', 'demo-session-api'], + ['mcp:sftp_read_file', `Read ${root}/src/health.ts`, 'demo-session-api'], + ['cli:session_exec', 'curl -fsS http://127.0.0.1:8080/health', 'demo-session-api'], + ['mcp:plugin_execute', 'docker-containers · logs\ncontainer=api · tail=30', 'demo-session-api'], + ['cli:session_create', 'Opened a separate worker session', 'demo-session-worker'], + ['mcp:session_exec', 'git diff --stat\n# Inspect only; leave the shared prompt alone', 'demo-session-api'], +].map(([tool, summary, sessionId], index) => ({ id: `demo-operation-${index}`, sequence: index + 1, + tool, summary, sessionId, status: 'succeeded' as const, timestamp: time - (6 - index) * 60000 })); + +const settings = { + terminal: { fontSize: 15, fontFamily: 'Monaco', cursorStyle: 'bar', cursorBlink: false, scrollbackLines: 10000 }, + appearance: { theme: light ? 'paper-white' : 'violet-black', themeMode: 'manual', lightTheme: 'paper-white', darkTheme: 'violet-black', windowOpacity: 1 }, + sshDefaults: { defaultPort: 22, connectionTimeout: 30, keepaliveInterval: 60, defaultUsername: 'demo' }, + aiPrediction: { enabled: false, provider: 'openai', apiKey: '', baseUrl: 'https://api.example.test/v1', model: '', debounceMs: 450, maxTokens: 32, minChars: 2 }, +}; +const capabilities = { platform: 'macos', isMobile: false, windowControls: false, localShell: true, + agentGateway: true, desktopUpdater: false, cliIpc: true, directoryTransfer: true, backgroundTunnels: true }; +const active = scenario === 'coding' ? 'demo-session-agent' : 'demo-session-api'; +const fileTab = { id: `demo-session-api\u0000${root}/RUNBOOK.md`, sessionId: 'demo-session-api', + path: `${root}/RUNBOOK.md`, name: 'RUNBOOK.md', kind: 'text', size: markdown.length, dirty: false }; +const pluginTab = { id: 'demo-session-api::server-performance', pluginId: 'server-performance', + sessionId: 'demo-session-api', sessionType: 'ssh', serverName: 'API · staging' }; +const sessionPane = `session:${active}`; +const filePane = `file:${fileTab.id}`; +const pluginPane = `plugin:${pluginTab.id}`; +// Dedicated origin only: reset just this demo's workspace keys on navigation. +for (const key of Object.keys(localStorage)) { + if (key.startsWith('vibeshell.') || key.startsWith('vibeshell-') || key.startsWith('vibeshell_')) localStorage.removeItem(key); +} +localStorage.setItem('vibeshell-lang', lang); +localStorage.setItem('newConnectionTab', 'ssh'); +localStorage.setItem('vibeshell-connection-view', 'icons'); +localStorage.setItem('vibeshell-coding-workspace', workspace); +localStorage.setItem('vibeshell-coding-agent', 'codex'); +localStorage.setItem('vibeshell_command_history:demo-api', JSON.stringify(['docker compose logs --tail 30 api', 'docker compose ps', 'git status --short'])); +localStorage.setItem('vibeshell.workspace-layout.v2', JSON.stringify({ version: 2, + sessions: [...ssh.map(s => ({ id: s.id, serverId: s.server_id, serverName: s.server_name, sessionType: 'ssh' })), + { id: local[0].id, serverId: 'zsh', serverName: local[0].shellName, sessionType: 'local', purpose: 'coding_agent', cwd: workspace }], + files: [fileTab], plugins: [pluginTab], activeSessionId: active, + activeFileId: scenario === 'files' ? fileTab.id : null, activePluginId: null, detached: [], + tree: scenario === 'files' ? { direction: 'row', first: sessionPane, second: filePane, splitPercentage: 43 } + : scenario === 'collaboration' ? { direction: 'row', first: sessionPane, second: pluginPane, splitPercentage: 63 } : sessionPane, + focusedPane: scenario === 'files' ? filePane : sessionPane, +})); +const ansi = { reset: '\x1b[0m', blue: '\x1b[38;5;75m', green: '\x1b[38;5;78m', dim: '\x1b[38;5;245m' }; +const prompt = `${ansi.green}demo@staging-api${ansi.reset} ${ansi.blue}${root}${ansi.reset} $ `; +const terminalText = (id: string) => id === 'demo-session-agent' + ? `${ansi.blue}Codex · Northstar workspace${ansi.reset}\r\n${ansi.dim}Synthetic agent transcript for documentation${ansi.reset}\r\n\r\n› Add a bounded retry to the health check.\r\n Keep the last error and show me the diff.\r\n\r\n Read src/health.ts\r\n Read tests/health.test.ts\r\n\r\n The request used to retry without a limit.\r\n I changed it to three attempts with backoff.\r\n\r\n${ansi.green}✓${ansi.reset} Keep a 5-second timeout per request\r\n${ansi.green}✓${ansi.reset} Preserve the final error\r\n${ansi.green}✓${ansi.reset} Add failure and retry tests\r\n\r\n src/health.ts +9 -2\r\n tests/health.test.ts +8\r\n\r\n Review the changes in the panel beside me.\r\n No deployment or service restart was run.\r\n\r\n› ` + : `${ansi.blue}NORTHSTAR / STAGING${ansi.reset}\r\n${ansi.dim}Documentation fixture · no live SSH connection${ansi.reset}\r\n\r\n${prompt}docker compose ps\r\nNAME IMAGE STATUS\r\napi northstar/api:preview Up 2 hours\r\nworker northstar/worker:preview Up 2 hours\r\npostgres postgres:16 Up 6 days\r\nredis redis:7 Up 6 days\r\n\r\n${prompt}curl -fsS localhost:8080/health\r\n${ansi.green}{"status":"ready","queue":"ok","database":"ok"}${ansi.reset}\r\n\r\n${prompt}git diff --stat\r\n src/health.ts | 11 +++++++++--\r\n tests/health.test.ts | 8 ++++++++\r\n 2 files changed, 17 insertions(+), 2 deletions(-)\r\n\r\n${ansi.dim}The shared prompt stays yours.\r\nIndependent agent commands appear in activity history.${ansi.reset}\r\n\r\n${prompt}`; +const output = async (id: string, text: string) => emit('session-output', { + session_id: id, data: btoa(String.fromCharCode(...new TextEncoder().encode(text))), +}); +const docker: Record = { + version: 'Docker Engine · demo data', + containers: 'a1b2c3\tapi\tnorthstar/api:preview\tUp 2 hours (healthy)\t127.0.0.1:8080->8080/tcp\nd4e5f6\tworker\tnorthstar/worker:preview\tUp 2 hours\t9090/tcp\na7b8c9\tpostgres\tpostgres:16\tUp 6 days (healthy)\t5432/tcp\nd0e1f2\tredis\tredis:7\tUp 6 days\t6379/tcp\na3b4c5\tmigrations\tnorthstar/api:preview\tExited (0) 2 hours ago\t', + stats: 'api\t12.4%\t184 MiB / 2 GiB\nworker\t3.1%\t96 MiB / 1 GiB\npostgres\t1.8%\t312 MiB / 4 GiB\nredis\t0.4%\t24 MiB / 512 MiB', + logs: '09:40:11 INFO health endpoint ready\n09:40:14 INFO database pool connected\n09:41:02 INFO request GET /health 200 8ms\n09:41:36 INFO request GET /api/jobs 200 14ms\n', +}; +mockWindows('main'); +mockIPC(async (cmd, payload) => { + calls.push(cmd); + const args = (payload ?? {}) as Record; + const request = args.request ?? args; + switch (cmd) { + case 'get_runtime_capabilities': return capabilities; + case 'get_app_version': return '1.1.0'; + case 'get_servers': return servers; + case 'get_groups': return [{ id: 'demo-staging', name: 'Staging', color: 'blue' }, { id: 'demo-data', name: 'Data', color: 'green' }, { id: 'demo-platform', name: 'Platform', color: 'purple' }]; + case 'load_settings': return settings; + case 'save_settings': Object.assign(settings, args.settings); return null; + case 'sftp_get_upload_ignore_config': return { excludedPaths: ['node_modules/', '.git/', 'target/', '.env'], respectGitignore: true }; + case 'session_list': return ssh; + case 'local_shell_list_sessions': return local; + case 'session_attach': await output(request.sessionId, '\x1b[2J\x1b[H' + terminalText(request.sessionId)); return ssh.find(s => s.id === request.sessionId); + case 'local_shell_attach': await output(request.sessionId, '\x1b[2J\x1b[H' + terminalText(request.sessionId)); return null; + case 'session_send_input': case 'local_shell_send_input': await output(request.sessionId, request.data); return null; + case 'session_resize': case 'local_shell_resize': case 'session_detach': case 'local_shell_detach': return null; + case 'take_pending_open_files': return []; + case 'workspace_save_handler_ready': return null; + case 'plugin:window|scale_factor': return 1; + case 'plugin:window|inner_size': case 'plugin:window|outer_size': return { width: innerWidth, height: innerHeight }; + case 'plugin:window|outer_position': case 'plugin:window|inner_position': return { x: 0, y: 0 }; + case 'plugin:window|is_fullscreen': case 'plugin:window|is_maximized': return false; + case 'plugin:window|get_all_windows': return ['main']; + case 'plugin:window|available_monitors': return [{ name: 'Demo display', position: { x: 0, y: 0 }, size: { width: 1440, height: 900 }, scaleFactor: 1, workArea: { position: { x: 0, y: 0 }, size: { width: 1440, height: 900 } } }]; + case 'plugin:window|set_position': case 'plugin:window|set_size': return null; + case 'cloud_sync_status': return { unlocked: false, syncing: false, provider: null, endpoint: null, vaultId: null, pendingChanges: 0, conflicts: 0, lastSuccessAt: null, lastError: null }; + case 'agent_activity_list': return activity.filter(item => (!args.after || item.sequence > args.after) && (!args.before || item.sequence < args.before)); + case 'get_agent_guard_status': return { autoApproveUntil: null, pending: [] }; + case 'resolve_agent_approval': await emit('agent-approval-resolved', { id: request.id }); return null; + case 'get_agent_guard_config': return { enabled: true, autoApproveHours: 0 }; + case 'get_agent_gateway_status': return { running: true, endpoint: 'http://127.0.0.1:0/demo-only', manifestPath: '/demo/agent-gateway.json', pid: null, protocolVersion: '2024-11-05' }; + case 'local_shell_list_shells': return [{ id: 'zsh', name: 'Zsh', path: '/bin/zsh', available: true }, { id: 'bash', name: 'Bash', path: '/bin/bash', available: true }]; + case 'local_shell_get_default': return { id: 'zsh', name: 'Zsh', path: '/bin/zsh', available: true }; + case 'coding_agent_list': return ['claude', 'codex', 'opencode', 'pi'].map((id, index) => ({ id, name: ['Claude Code', 'Codex', 'OpenCode', 'Pi'][index], installed: true, executablePath: `/demo/bin/${id}`, startModes: ['new', 'continue_last', 'resume_picker'], accessModes: ['default', 'read_only', 'auto_edit'] })); + case 'pick_workspace_directory': return workspace; + case 'coding_agent_workspace_status': return { root: workspace, branch: 'fix/health-retry', files: [{ path: 'src/health.ts', oldPath: null, kind: 'modified', staged: false, unstaged: true }, { path: 'tests/health.test.ts', oldPath: null, kind: 'added', staged: true, unstaged: false }] }; + case 'coding_agent_workspace_diff': return { path: request.path, oldPath: null, truncated: false, content: 'diff --git a/src/health.ts b/src/health.ts\n--- a/src/health.ts\n+++ b/src/health.ts\n@@ -1,6 +1,13 @@\n+import { setTimeout as delay } from "node:timers/promises";\n+\n export async function fetchHealth(url: string) {\n- const response = await fetch(url);\n- return response.json();\n+ for (let attempt = 0; attempt < 3; attempt++) {\n+ try {\n+ const response = await fetch(url, {\n+ signal: AbortSignal.timeout(5000),\n+ });\n+ if (!response.ok) throw new Error("Health check failed");\n+ return await response.json();\n+ } catch (error) {\n+ if (attempt === 2) throw error;\n+ await delay(250 * 2 ** attempt);\n+ }\n+ }\n }\n' }; + case 'plugin_list': return plugins; + case 'plugin_execute': { + const text = request.pluginId === 'docker-containers' ? docker[request.actionId] : undefined; + if (text === undefined) break; + return { pluginId: request.pluginId, actionId: request.actionId, output: text, durationMs: 38, truncated: false }; + } + case 'get_server_status': return { hostname: 'staging-api', uptimeSeconds: 543210, + cpu: { usagePercent: 18.4, coreCount: 4, loadAverage: [0.72, 0.61, 0.48] }, + memory: { total: 8589934592, used: 3113851289, free: 5476083303, available: 5476083303, usagePercent: 36.25, swapTotal: 0, swapUsed: 0 }, + disks: [{ mountPoint: '/', filesystem: 'ext4', total: 107374182400, used: 39728447488, available: 67645734912, usagePercent: 37 }], + network: [{ interface: 'eth0', rxBytes: 102400000, txBytes: 51800000, rxPackets: 9032, txPackets: 5013 }], collectedAt: seconds }; + case 'sftp_init': return true; + case 'sftp_pwd': return root; + case 'sftp_list_dir': return [ + ...['src', 'public', 'logs'].map(name => ({ name, path: `${root}/${name}`, isDirectory: true, size: 0, modifiedAt: seconds, permissions: 'drwxr-xr-x' })), + ...Object.entries(files).filter(([path]) => path.startsWith(request.path + '/') && !path.slice(request.path.length + 1).includes('/')).map(([path, content]) => ({ name: path.split('/').at(-1), path, isDirectory: false, size: content.length, modifiedAt: seconds, permissions: '-rw-r--r--' })), + ]; + case 'sftp_read_file': case 'local_file_read': { + const content = files[request.path]; + if (content === undefined) break; + return { content, isBinary: false, size: content.length, truncated: false, mimeType: 'text/plain' }; + } + case 'sftp_write_file': if (request.path in files) { files[request.path] = request.content; return null; } break; + case 'get_credential': return null; + case 'list_fingerprints': case 'list_recordings': case 'tunnel_config_list': case 'tunnel_list_active': return []; + case 'is_session_recording': return false; + case 'get_session_recording_id': return null; + case 'history_list': return []; + } + blocked.push(cmd); + throw new Error(`Documentation fixture does not implement ${cmd}; native calls are never forwarded.`); +}, { shouldMockEvents: true }); + +await import('../../src/main'); +const { useSessionStore } = await import('../../src/stores/sessionStore'); +const { usePluginWorkspaceStore } = await import('../../src/stores/pluginWorkspaceStore'); +const { useNavigationStore } = await import('../../src/stores/navigationStore'); +const { useAgentApprovalStore } = await import('../../src/stores/agentApprovalStore'); +const { useSettingsStore } = await import('../../src/stores/settingsStore'); +const { useCustomThemeStore } = await import('../../src/lib/customTheme'); +Object.assign(window, { __VIBESHELL_DEMO__: { + scenario, blocked, calls, errors, + ready: () => document.querySelectorAll('.xterm-screen').length > 0, + approveDemo: () => useAgentApprovalStore.setState({ queue: [{ id: 'demo-approval', tool: 'mcp:session_exec', command: 'sudo systemctl restart northstar-api', reasons: ['Elevated privileges', 'Service restart can interrupt active requests'], sessionId: 'demo-session-api', timestamp: time }] }), + newSession: async () => { ssh.push({ id: 'demo-session-agent-new', server_id: 'demo-api', server_name: 'API · agent inspection', state: 'connected', created_at: seconds + 1, clients: 1 }); await useSessionStore.getState().syncRemoteSessions(); }, + plugin: (pluginId: string) => { usePluginWorkspaceStore.getState().openPluginTab({ pluginId, sessionId: 'demo-session-api', sessionType: 'ssh', serverName: 'API · staging' }); }, + settings: () => useNavigationStore.getState().goToSettings(), + marketplace: () => useNavigationStore.getState().goToPlugins(), + theme: async () => { await useSettingsStore.getState().updateAppearanceSettings({ theme: 'paper-white' }); useCustomThemeStore.getState().save(':root {\n --tokyo-blue: #0d766e !important;\n}\n.session-tabbar [role="tab"] {\n border-radius: 10px;\n}\n', false); }, + replay: async () => { for (const item of [...ssh, ...local]) await output(item.id, '\x1b[2J\x1b[H' + terminalText(item.id)); }, +} }); diff --git a/scripts/readme-demo/tsconfig.json b/scripts/readme-demo/tsconfig.json new file mode 100644 index 0000000..5a9d139 --- /dev/null +++ b/scripts/readme-demo/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "target": "ES2022", + "lib": ["ES2022", "DOM", "DOM.Iterable"] + }, + "include": ["main.ts"], + "references": [] +} diff --git a/scripts/report-ci-status.mjs b/scripts/report-ci-status.mjs new file mode 100644 index 0000000..d10fea8 --- /dev/null +++ b/scripts/report-ci-status.mjs @@ -0,0 +1,89 @@ +#!/usr/bin/env node +// workflow_dispatch job checks are not eligible PR checks. Publish commit +// statuses from actual GitHub job evidence, never from a maintainer override. +import { pathToFileURL } from 'node:url'; +import { latestManualCiRun, REQUIRED_JOBS } from './check-release-ci.mjs'; + +export async function reportCiStatuses(options, request) { + const { phase, repository, sha, runId, runAttempt } = options; + if (!['start', 'finish'].includes(phase) + || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repository ?? '') + || !/^[a-f0-9]{40}$/.test(sha ?? '') + || !Number.isSafeInteger(runId) || runId <= 0 + || !Number.isSafeInteger(runAttempt) || runAttempt <= 0) { + throw new Error('Invalid CI status reporting context'); + } + const base = `/repos/${repository}`; + const runUrl = `https://github.com/${repository}/actions/runs/${runId}`; + const isCurrent = async () => { + const payload = await request('GET', `${base}/actions/workflows/ci.yml/runs?event=workflow_dispatch&head_sha=${sha}&per_page=100`); + const latest = latestManualCiRun(payload, sha, repository); + if (latest.id !== runId) return false; + const run = await request('GET', `${base}/actions/runs/${runId}`); + return run.run_attempt === runAttempt && run.head_sha === sha && run.conclusion !== 'cancelled' + && run.event === 'workflow_dispatch' && run.path === '.github/workflows/ci.yml' + && run.repository?.full_name === repository + && run.head_repository?.full_name === repository; + }; + if (!await isCurrent()) return { superseded: true, statuses: [] }; + + let jobs = []; + if (phase === 'finish') { + const payload = await request('GET', `${base}/actions/runs/${runId}/jobs?filter=latest&per_page=100`); + if (!Array.isArray(payload.jobs) || payload.total_count !== payload.jobs.length) { + throw new Error('Incomplete job evidence; refusing to publish CI success'); + } + jobs = payload.jobs; + } + const statuses = REQUIRED_JOBS.map(name => { + const matches = jobs.filter(job => job.name === name); + const job = matches.length === 1 ? matches[0] : undefined; + const passed = job?.status === 'completed' && job.conclusion === 'success'; + const state = phase === 'start' ? 'pending' : passed ? 'success' : 'failure'; + return { + context: name, state, target_url: runUrl, + description: `Manual CI #${runId}, attempt ${runAttempt}: ${phase === 'start' ? 'running' : job?.conclusion ?? 'missing job'}`, + }; + }); + // A superseded or cancelled run must not overwrite a newer run's statuses. + // Recheck before each write; GitHub's concurrency group cancels older runs. + for (const status of statuses) { + if (!await isCurrent()) return { superseded: true, statuses: [] }; + await request('POST', `${base}/statuses/${sha}`, status); + } + if (phase === 'finish' && statuses.some(status => status.state !== 'success')) { + throw new Error('Required manual CI jobs did not all succeed; see the linked run'); + } + return { superseded: false, statuses }; +} + +if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) { + try { + const token = process.env.GITHUB_TOKEN; + if (!token) throw new Error('GITHUB_TOKEN is required for CI status reporting'); + const request = async (method, path, body) => { + const response = await fetch(`https://api.github.com${path}`, { + method, + headers: { + Authorization: `Bearer ${token}`, Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', 'Content-Type': 'application/json', + }, + body: body === undefined ? undefined : JSON.stringify(body), + signal: AbortSignal.timeout(15000), + }); + // Do not echo request headers or API error bodies into workflow logs. + if (!response.ok) throw new Error(`GitHub CI status API failed (${response.status})`); + return response.json(); + }; + const result = await reportCiStatuses({ + phase: process.argv[2], repository: process.env.GITHUB_REPOSITORY, + sha: process.env.GITHUB_SHA, runId: Number(process.env.GITHUB_RUN_ID), + runAttempt: Number(process.env.GITHUB_RUN_ATTEMPT), + }, request); + console.log(result.superseded ? 'Inactive or superseded CI attempt; no further status updates.' + : `Published ${result.statuses.length} evidence-backed ${process.argv[2]} statuses.`); + } catch (error) { + console.error(error.message); + process.exitCode = 1; + } +} diff --git a/scripts/tests/ci-status.test.mjs b/scripts/tests/ci-status.test.mjs new file mode 100644 index 0000000..f4c5b5c --- /dev/null +++ b/scripts/tests/ci-status.test.mjs @@ -0,0 +1,120 @@ +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { reportCiStatuses } from '../report-ci-status.mjs'; +import { REQUIRED_JOBS } from '../check-release-ci.mjs'; + +const repository = 'owner/repo'; +const sha = 'a'.repeat(40); +const options = { phase: 'finish', repository, sha, runId: 42, runAttempt: 1 }; +function fixture(overrides = {}) { + const run = { + id: 42, head_sha: sha, event: 'workflow_dispatch', path: '.github/workflows/ci.yml', + repository: { full_name: repository }, head_repository: { full_name: repository }, + status: 'in_progress', conclusion: null, run_attempt: 1, + }; + const jobs = REQUIRED_JOBS.map(name => ({ name, status: 'completed', conclusion: 'success' })); + const state = { run, runs: [run], jobs, posts: [], ...overrides }; + state.request = async (method, path, body) => { + if (method === 'POST') { state.posts.push({ path, body }); return {}; } + if (path.includes('/jobs?')) return { jobs: state.jobs, total_count: state.total ?? state.jobs.length }; + if (path.includes('/workflows/')) return { workflow_runs: state.runs }; + if (path.endsWith('/runs/42')) return state.run; + throw new Error(`Unexpected test endpoint: ${path}`); + }; + return state; +} + +test('initialization marks all five required contexts pending, never green', async () => { + const state = fixture(); + await reportCiStatuses({ ...options, phase: 'start' }, state.request); + assert.equal(state.posts.length, REQUIRED_JOBS.length); + assert.deepEqual(state.posts.map(post => post.body.context), REQUIRED_JOBS); + assert.ok(state.posts.every(post => post.body.state === 'pending')); +}); + +test('only real successful jobs publish success for the exact tested commit', async () => { + const state = fixture(); + await reportCiStatuses(options, state.request); + assert.equal(state.posts.length, REQUIRED_JOBS.length); + assert.ok(state.posts.every(post => post.path === `/repos/${repository}/statuses/${sha}`)); + assert.ok(state.posts.every(post => post.body.state === 'success')); + assert.ok(state.posts.every(post => post.body.target_url.endsWith('/actions/runs/42'))); +}); + +test('failure, cancellation, skipping and incomplete jobs can never produce green', async () => { + for (const conclusion of ['failure', 'cancelled', 'skipped', 'neutral', 'timed_out', null]) { + const state = fixture(); + state.jobs[0].conclusion = conclusion; + await assert.rejects(reportCiStatuses(options, state.request), /did not all succeed/); + assert.equal(state.posts[0].body.state, 'failure'); + } + const state = fixture(); + state.jobs[0].status = 'in_progress'; + await assert.rejects(reportCiStatuses(options, state.request), /did not all succeed/); + assert.equal(state.posts[0].body.state, 'failure'); +}); + +test('missing and duplicate required job names fail closed', async () => { + for (const duplicate of [false, true]) { + const state = fixture(); + if (duplicate) state.jobs.push({ ...state.jobs[0] }); + else state.jobs.shift(); + await assert.rejects(reportCiStatuses(options, state.request), /did not all succeed/); + assert.equal(state.posts[0].body.state, 'failure'); + } +}); + +test('incomplete API pages cannot be treated as complete job evidence', async () => { + const state = fixture({ total: 100 }); + await assert.rejects(reportCiStatuses(options, state.request), /Incomplete job evidence/); + assert.equal(state.posts.length, 0); +}); + +test('older runs and stale rerun attempts do not overwrite current statuses', async () => { + const state = fixture(); + state.runs.push({ ...state.run, id: 43 }); + assert.equal((await reportCiStatuses(options, state.request)).superseded, true); + assert.equal(state.posts.length, 0); + const rerun = fixture(); + rerun.run.run_attempt = 2; + assert.equal((await reportCiStatuses(options, rerun.request)).superseded, true); + assert.equal(rerun.posts.length, 0); +}); + +test('a run superseded while fetching evidence cannot write a stale success', async () => { + const state = fixture(); + const request = async (...args) => { + const response = await state.request(...args); + if (args[1].includes('/jobs?')) state.runs.push({ ...state.run, id: 43 }); + return response; + }; + assert.equal((await reportCiStatuses(options, request)).superseded, true); + assert.equal(state.posts.length, 0); +}); + +test('a cancelled workflow cannot publish green even after its test jobs succeeded', async () => { + const state = fixture(); + state.run.conclusion = 'cancelled'; + assert.equal((await reportCiStatuses(options, state.request)).superseded, true); + assert.equal(state.posts.length, 0); +}); + +test('foreign repository, wrong SHA or automatic run evidence is rejected', async () => { + for (const override of [ + { event: 'push' }, { head_sha: 'b'.repeat(40) }, + { head_repository: { full_name: 'fork/repo' } }, + ]) { + const state = fixture(); + Object.assign(state.run, override); + await assert.rejects(reportCiStatuses(options, state.request), /Run CI manually/); + assert.equal(state.posts.length, 0); + } +}); + +test('invalid reporting context is rejected before any API call', async () => { + for (const override of [{ phase: 'force' }, { runId: 0 }, { runAttempt: 0 }, { sha: 'HEAD' }, { repository: 'bad/path/extra' }]) { + await assert.rejects(reportCiStatuses({ ...options, ...override }, () => { + assert.fail('Invalid context must not reach the API'); + }), /Invalid CI status/); + } +}); diff --git a/scripts/tests/workflows.test.mjs b/scripts/tests/workflows.test.mjs new file mode 100644 index 0000000..7a4e459 --- /dev/null +++ b/scripts/tests/workflows.test.mjs @@ -0,0 +1,94 @@ +import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; +import test from 'node:test'; +import { REQUIRED_JOBS, selectManualCiRun, validateCiJobs } from '../check-release-ci.mjs'; + +const sha = 'a'.repeat(40); +const repository = 'owner/repo'; +const run = { + id: 42, head_sha: sha, event: 'workflow_dispatch', path: '.github/workflows/ci.yml', + head_repository: { full_name: repository }, repository: { full_name: repository }, + status: 'completed', conclusion: 'success', +}; +const select = runs => selectManualCiRun({ workflow_runs: runs }, sha, repository); +const jobs = () => ({ + total_count: REQUIRED_JOBS.length, + jobs: REQUIRED_JOBS.map(name => ({ name, status: 'completed', conclusion: 'success' })), +}); + +test('selects a successful manual CI run for the exact repository and commit', () => { + assert.equal(select([run]), 42); +}); + +test('does not accept automatic, foreign, wrong-workflow or wrong-commit evidence', () => { + for (const override of [ + { event: 'push' }, { head_sha: 'b'.repeat(40) }, { path: '.github/workflows/other.yml' }, + { head_repository: { full_name: 'fork/repo' } }, { repository: { full_name: 'fork/repo' } }, + ]) assert.throws(() => select([{ ...run, ...override }]), /Run CI manually/); + assert.throws(() => select([]), /Run CI manually/); +}); + +test('an older success cannot hide a newer failed, cancelled or running attempt', () => { + for (const override of [ + { conclusion: 'failure' }, { conclusion: 'cancelled' }, { conclusion: 'skipped' }, + { status: 'in_progress', conclusion: null }, + ]) assert.throws(() => select([run, { ...run, id: 43, ...override }]), /latest manual CI/); +}); + +test('selects the newest successful run independent of response order', () => { + assert.equal(select([{ ...run, id: 43 }, run]), 43); +}); + +test('requires every platform, frontend and lint job to have really succeeded', () => { + assert.doesNotThrow(() => validateCiJobs(jobs())); + for (const conclusion of ['failure', 'cancelled', 'skipped', null]) { + const payload = jobs(); + payload.jobs[0].conclusion = conclusion; + assert.throws(() => validateCiJobs(payload), /Required manual CI job/); + } + const payload = jobs(); + payload.jobs.pop(); + payload.total_count--; + assert.throws(() => validateCiJobs(payload), /Rust Check/); +}); + +test('rejects malformed or incomplete evidence', () => { + assert.throws(() => selectManualCiRun({}, sha, repository), /Invalid CI/); + assert.throws(() => select([{ ...run, id: -1 }]), /latest manual CI/); + const payload = jobs(); + payload.total_count++; + assert.throws(() => validateCiJobs(payload), /Incomplete CI/); +}); + +// These files deliberately use a block-form, top-level `on:` section. Reject +// shape changes too, so adding an automatic trigger cannot evade this guard. +test('CI and release have only the manual trigger', () => { + for (const file of ['ci.yml', 'release.yml']) { + const text = readFileSync(new URL(`../../.github/workflows/${file}`, import.meta.url), 'utf8').replaceAll('\r\n', '\n'); + const section = text.match(/^on:\n((?:[ \t].*\n|\n)+)/m); + assert.ok(section, `${file}: expected block-form on section`); + const events = [...section[1].matchAll(/^ ([a-z_]+):/gm)].map(match => match[1]); + assert.deepEqual(events, ['workflow_dispatch'], file); + } +}); + +test('release is restricted to main and defaults to an unpublished draft', () => { + const text = readFileSync(new URL('../../.github/workflows/release.yml', import.meta.url), 'utf8'); + assert.match(text, /if: github.event_name == 'workflow_dispatch' && github.ref == 'refs\/heads\/main'/); + assert.match(text, /publish:[\s\S]*?type: boolean\s+default: false/); + assert.match(text, /if \[\[ "\$PUBLISH_RELEASE" == 'true' \]\]; then\s+gh release edit[^\n]*--draft=false --latest/); + assert.doesNotMatch(text, /inputs\.tag \|\| github\.ref_name/); +}); + +test('manual CI reports evidence-backed statuses without granting write access to builds', () => { + const text = readFileSync(new URL('../../.github/workflows/ci.yml', import.meta.url), 'utf8').replaceAll('\r\n', '\n'); + for (const id of ['check-frontend', 'check-backend', 'check-clippy']) { + const section = text.split(` ${id}:\n`)[1]?.split(/^ [a-z-]+:\n/m)[0]; + assert.ok(section, id); + assert.match(section, /needs: status-start/); + assert.doesNotMatch(section, /statuses: write/); + } + assert.match(text, /needs: \[status-start, check-frontend, check-backend, check-clippy\]\s+if: always\(\)/); + assert.match(text, /node scripts\/report-ci-status\.mjs start/); + assert.match(text, /node scripts\/report-ci-status\.mjs finish/); +}); diff --git a/skills/vibeshell/references/docker-containers.md b/skills/vibeshell/references/docker-containers.md index 2a82d18..035a9bc 100644 --- a/skills/vibeshell/references/docker-containers.md +++ b/skills/vibeshell/references/docker-containers.md @@ -1,6 +1,6 @@ # Docker Containers — docker-containers -Plugin `docker-containers` version `1.4.0`. +Plugin `docker-containers` version `1.5.0`. Inspect and manage containers, images, live resource usage and recent container logs through the remote Docker CLI. @@ -18,6 +18,116 @@ Required permissions: `["remote_exec","local_exec"]`. Session types: `["ssh","lo `describe` returns machine-readable action input schemas. `docs` regenerates the current reference, including imported plugins. `run` reuses the selected session. `--confirm` is only for an action the user has explicitly approved; `--sudo` is opt-in and also needs confirmation. No operation bypasses installation, enablement, permission or input checks. Output is bounded and carries timing/truncation metadata. Local targets require a running GUI-owned local session. +## `running-containers` + +List running containers with full IDs; paused and restarting containers are not shell-ready. + +```sh +vibeshell plugins run docker-containers running-containers --session SESSION_ID --inputs '{}' +``` + +Replace SESSION_ID and supply all fields marked required below. Do not execute placeholder values. Append `--confirm` only after consent for this exact action. + +```json +{ + "allowSudo": true, + "description": "List running containers with full IDs; paused and restarting containers are not shell-ready.", + "elevate": false, + "id": "running-containers", + "inputSchema": { + "additionalProperties": false, + "properties": {}, + "required": [], + "type": "object" + }, + "name": "Running containers", + "output": { + "columns": [ + "ID", + "Name", + "Image", + "State", + "Status", + "Ports" + ], + "delimiter": "\t", + "kind": "table" + }, + "requiresConfirmation": false +} +``` + +## `exited-containers` + +Inspect exited containers separately without starting or restarting them. + +```sh +vibeshell plugins run docker-containers exited-containers --session SESSION_ID --inputs '{}' +``` + +Replace SESSION_ID and supply all fields marked required below. Do not execute placeholder values. Append `--confirm` only after consent for this exact action. + +```json +{ + "allowSudo": true, + "description": "Inspect exited containers separately without starting or restarting them.", + "elevate": false, + "id": "exited-containers", + "inputSchema": { + "additionalProperties": false, + "properties": {}, + "required": [], + "type": "object" + }, + "name": "Exited containers", + "output": { + "columns": [ + "ID", + "Name", + "Image", + "State", + "Status", + "Ports" + ], + "delimiter": "\t", + "kind": "table" + }, + "requiresConfirmation": false +} +``` + +## `container-inventory` + +Read all container states and full IDs as one JSON object per line, not a JSON array. This does not create a container session. + +```sh +vibeshell plugins run docker-containers container-inventory --session SESSION_ID --inputs '{}' +``` + +Replace SESSION_ID and supply all fields marked required below. Do not execute placeholder values. Append `--confirm` only after consent for this exact action. + +```json +{ + "allowSudo": true, + "description": "Read all container states and full IDs as one JSON object per line, not a JSON array. This does not create a container session.", + "elevate": false, + "id": "container-inventory", + "inputSchema": { + "additionalProperties": false, + "properties": {}, + "required": [], + "type": "object" + }, + "name": "Container inventory (JSON Lines)", + "output": { + "columns": [], + "delimiter": "\t", + "kind": "text" + }, + "requiresConfirmation": false +} +``` + ## `containers` List running and stopped containers. @@ -215,7 +325,7 @@ Replace SESSION_ID and supply all fields marked required below. Do not execute p ## `exec-command` -Run one non-interactive shell command inside a container. +Run one non-interactive shell command inside a container. This does not create a persistent container session or change the host session. ```sh vibeshell plugins run docker-containers exec-command --session SESSION_ID --inputs '{}' @@ -226,7 +336,7 @@ Replace SESSION_ID and supply all fields marked required below. Do not execute p ```json { "allowSudo": true, - "description": "Run one non-interactive shell command inside a container.", + "description": "Run one non-interactive shell command inside a container. This does not create a persistent container session or change the host session.", "elevate": false, "id": "exec-command", "inputSchema": {