# miniSCP A cross-platform, dual-panel SFTP/local file manager — a small WinSCP-style commander built on **Bun**, **Electrobun**, and **Angular**. Each of the two panels independently shows either the local filesystem or a remote server over SSH/SFTP, so you can move files **local↔remote**, **remote↔remote**, or **local↔local** through one symmetric engine. > **Name & icon are placeholders.** See [Rebranding](#rebranding) — it's a > one-file change. ## Developer docs Dense reference material lives in [docs/](docs/): [architecture](docs/architecture.md) (processes, windows, RPC, every module/service), [build & release](docs/build-and-release.md) (Build-n versioning, per-target release pipeline, CI), [platform notes](docs/platform-notes.md) (macOS/Windows/Linux matrix, runtime deps + smoke tests), and [gotchas](docs/gotchas.md) (known landmines). ## Features - Dual-panel commander UI; either panel can be Local or any saved connection. - **Session tabs**: each tab is an independent left/right workspace; remote sessions stay connected and transfers keep running in background tabs. - Per-panel action toolbar with a direction-explicit send button, plus a VSCode-style status bar showing live transfer progress (click to expand the transfer queue with per-file speed and ETA). - **Overwrite confirmation** on name collisions, with a source-vs-destination comparison (size, modified date, which is newer) and batch replace-all / skip-all. - **Settings** (gear, top-right): light / dark / system theme and speed units (MB/s or Mbps), persisted across restarts. - On macOS the tab bar is fused with the window titlebar; other platforms keep a standard titlebar. - **Menu-bar (tray) icon**: template icon that adapts to light/dark; click it to bring the app to the front. - **Edit files** (F4 / toolbar / File menu): a built-in editor works on local and remote files directly; or pick an external app (Options → Editor → Custom → Choose…). External edits of remote files download to a temp folder, are watched, and every save auto-uploads (visible in the transfer list) until the session disconnects. Leftover temp files from a crash are offered for cleanup at the next start. - Browse, sort (name/size/modified, folders first), keyboard navigation. - Create folder / file, rename, delete (recursive), change permissions (chmod). - Upload, download, and remote→remote send — files **and** whole folders — with a transfer queue, per-job progress, and cancel. - Connection manager: saved connections organised into folders, password or private-key auth (with passphrase). ## Stack | Layer | Choice | | ---------- | ------------------------------------------------------------- | | Runtime | Bun (main process, all SSH/SFTP + file IO) | | Desktop | Electrobun 1.18 (native webview, typed RPC bridge) | | Frontend | Angular 22 (standalone, signals, zoneless) in `ui/` | | Styling | Tailwind CSS v4, shadcn-style tokens (no component library) | | Icons | @ng-icons/mynaui | | SSH/SFTP | `ssh2` (pure-JS under Bun) | The repo is a Bun workspace: the Electrobun app at the root, the Angular app in `ui/`. Shared types and the RPC contract live in `src/shared/` and are imported by both sides. ``` src/shared/ types.ts, rpc.ts (wire contract), branding.ts src/bun/ Electrobun main process fs/ FileSystemProvider + LocalProvider + SftpProvider + registry transfers/ engine.ts (queue/stream copy) + walk.ts (recursive helpers) store/ connections.json persistence ui/src/app/ Angular components + signal services ``` ## Prerequisites - [Bun](https://bun.sh) ≥ 1.3 (`bun --version`). - macOS: **Xcode Command Line Tools** (for the app build). - Linux: **AppIndicator** — the app links it for the tray and will not start without it (`ERR_DLOPEN_FAILED`). `libayatana-appindicator` (Arch), `libayatana-appindicator3-1` (Debian/Ubuntu), `libayatana-appindicator-gtk3` (Fedora), plus system `webkit2gtk`. Details and diagnostics in [platform notes](docs/platform-notes.md). - No Node install required — a preload shim (`scripts/node-version-shim.ts`) satisfies the Angular CLI's Node version check while running under Bun. ## Setup ```bash bun install # installs root + ui workspace deps ``` ## Develop ```bash bun run dev ``` This starts the Angular dev server (hot reload for UI code) and launches the Electrobun app pointed at it. Edits under `ui/` reload instantly; edits to the Bun main process (`src/bun/**`, RPC schema) trigger an app relaunch. ## Releases Versioning is Sublime-style: **Build n**, where n is the commit count (`git rev-list --count HEAD`), regenerated into the gitignored `src/shared/version.gen.ts` by every build script (and `postinstall`). One artifact per invocation, on the matching host — electrobun cannot cross-compile between OSes: ```sh bun run release -- --target mac-arm64 # on the Mac (native) bun run release -- --target mac-x64 # on the same arm64 Mac, via Rosetta 2 bun run release -- --target win-x64 # on a Windows 11 x64 machine bun run release -- --target linux-x64 # on a Linux x64 machine ``` Artifacts land in `release//miniSCP-Build-.*` — a `.dmg` per mac arch; on Windows a `-Setup.zip` installer **and** a `-portable.zip` (unzip anywhere and run); on Linux a portable `.tar.gz` (unpack and run `bin/launcher`). The mac-x64 build fetches electrobun's x64 CLI once into `~/.cache/miniscp/` and runs it under Rosetta so the whole pipeline believes it is on an Intel Mac. CI: `.gitea/workflows/release.yml` runs the same script on a macos + windows runner matrix once act_runners are registered; trigger by pushing a `build-` tag. ## Build a packaged app ```bash bun run build # ng build (ui/dist) + electrobun build -> build/*/miniSCP*.app ``` The Angular bundle is copied into the app under `views://app`. The macOS icon is compiled from `assets/icon.iconset` into `AppIcon.icns` automatically. For a distributable (optimized, with auto-update channel) build, use `bunx electrobun build --env=stable` (see the [Electrobun docs](https://framework.blackboard.sh/electrobun/) for signing, notarization, and release hosting). ## Rebranding Everything user-facing reads from **[`src/shared/branding.ts`](src/shared/branding.ts)**: ```ts export const BRANDING = { appName: "miniSCP", // window title, bundle name, app-data folder identifier: "es.teknes.miniscp", legacyNames: [], // previous names; their data dirs migrate on boot version: "0.1.0", icon: "assets/icon.iconset", } as const; ``` To change the icon: edit `assets/logo.svg` (app icon) and `assets/tray/tray-glyph.svg` (menu-bar glyph), then run `bun scripts/gen-icons.ts` — it regenerates the macOS iconset, `icon.png`, the tray template image, and the tray progress frames from the SVGs (`@resvg/resvg-js`, dev-only dependency). > Renaming `appName` changes the app-data directory. Add the old name to > `legacyNames` and the app moves the previous folder (connections, settings, > edit temp files) into place on the next boot. ## Where connections are stored Saved connections live in a plain JSON file in the OS app-data directory (macOS: `~/Library/Application Support/miniSCP/connections.json`), written with `0600` permissions. > ⚠️ **Passwords are stored in plaintext** in this file (a deliberate v1 > simplicity choice). Anyone who can read the file can read the passwords. If > that isn't acceptable for your use, use key-based auth or don't save the > password (connect ad-hoc). OS-keychain storage is a natural future addition. ## Keyboard shortcuts `Tab` switch panel · `↑`/`↓`/`PgUp`/`PgDn`/`Home`/`End` move cursor · `Space` select · `Enter` open folder · `Backspace` parent · `Ctrl/Cmd+A` select all · `F5` send to other panel · `F2` rename · `F7` new folder · `F8`/`Delete` delete · `Cmd/Ctrl+T` new tab · `Cmd/Ctrl+W` close tab · `Ctrl+Tab` next tab. (On Macs, function keys may need `fn` — every action also has a toolbar button.) ## Same-file protection Copying a file or folder onto itself would truncate it to zero bytes (the destination is opened for writing before the source is read). miniSCP blocks this at several layers: - **Same session / same host**: every connection carries a filesystem identity (`local`, or `resolvedIP:port` — the hostname is DNS-resolved, so `myserver.lan` and its IP compare equal even across two panels/tabs holding separate SSH sessions to the same server). Equal identity + equal or nested paths ⇒ the transfer is refused before anything is opened. - **Hidden aliases** (two *different* addresses that reach the same machine — NAT, split DNS): when identities differ but source and destination paths collide anyway, a probe file is written through one connection and looked up through the other; if it's visible on both, they're the same machine and the copy is refused. (SFTP offers no device/inode identity — the mechanism `cp`/`rsync` use locally — so probing is the only deterministic test.) - The overwrite dialog never offers to "Replace" a file with itself — such items are auto-skipped with a notice. **Known residual limitation**: the same filesystem mounted at *different paths* on two connections (bind mounts, NFS exported twice, shared container volumes) cannot be detected — the paths differ, so no guard or probe triggers. Be careful when two panels reach the same data through different mount points. ## Portability notes Platform seams are isolated behind `process.platform` / `PLATFORM` branches marked with `PORTABILITY:` comments: editor launch (`open` / `cmd /c start` / `xdg-open`), app pickers, owner/group lookups, tray images, window chrome, shell-integration wording, drive listing, and the connections-file ACL. The application menu ([src/bun/menu.ts](src/bun/menu.ts)) is macOS-only for now; the in-app UI covers every menu action. Right-click menus are **HTML, on every platform** ([ui/src/app/context-menu.ts](ui/src/app/context-menu.ts)) rather than native — electrobun's native context menu is a documented no-op on Linux, and one menu implementation beats two that drift. Only reveal / open-with-default / copy-path cross to the backend, through the `shellItemAction` RPC. ## Windows (beta) Windows 11+ x64 is supported as a **beta** target (win-arm64 deferred). All electrobun APIs the app uses are implemented on Windows; known issues are upstream: - **HiDPI blur** ([electrobun#324](https://github.com/blackboardsh/electrobun/issues/324)): WebView2 renders at 1x on scaled displays until upstream adds DPI awareness. - **Notifications** use legacy balloon style, not Windows toast. - **Unsigned binaries**: both the Setup.exe installer and the portable exe trigger SmartScreen/Mark-of-the-Web warnings on first run. No Start Menu entry or uninstaller (installer unpacks to `%LOCALAPPDATA%`). - `connections.json` is protected with an `icacls` current-user ACL instead of POSIX 0600 (which is meaningless on Windows). First-Windows-boot smoke test: drives dropdown lists C:\ etc.; navigate to a drive root ('..' disappears); local↔remote transfers; external editor launch (`cmd /c start`) and the .exe picker; Show in Explorer / Recycle Bin; tray icon renders colored; Permissions absent on local panels; Ctrl+labels in tooltips; portable zip runs from any folder. ## Linux (beta) Linux x64 is supported as a **beta** target and builds a portable tarball. One hard requirement: the AppIndicator library listed under [Prerequisites](#prerequisites) — electrobun links it for the tray, so without it the app aborts at startup with `ERR_DLOPEN_FAILED` rather than merely losing its tray icon. - **No native menus at all** upstream (both the app menubar and context menus are no-ops). The menubar was already macOS-only and the context menus are now HTML, so nothing is missing in practice. - **Tray visibility** depends on the desktop: KDE, XFCE, MATE and Cinnamon show it; GNOME 3.26+ needs the AppIndicator extension. The app works either way. - **Unsigned and unpackaged**: no AppImage, `.deb`, desktop entry or icon installation — unpack the tarball and run `bin/launcher`. - Harmless startup warnings (`GLXBadWindow`, `Failed to create GBM buffer`, an appindicator deprecation notice) are documented in [gotchas](docs/gotchas.md#linux). Full dependency table, diagnostics and the first-boot smoke test: [platform notes](docs/platform-notes.md). ## Known issues - **Rare native crash under load.** A long-running session (multiple remote connections, extended uptime) can hit a Bun panic — a segfault inside Bun 1.3.13's FFI thread-safe callbacks executing Electrobun's native→worker bridge (`FFI_Callback_threadsafe_call` → `web_worker.spin` in the decoded crash report). This is an upstream Bun × Electrobun bug, not app code; a restart recovers cleanly and in-flight work isn't corrupted. Expected to resolve when Electrobun bumps its pinned Bun (currently 1.3.13; 1.3.14+ available). Bun's own crash reporter link is printed to the console on panic. ## Not in v1 Drag-and-drop, file preview/edit, transfer resume, symlink following (symlinks are listed but skipped by transfers), OS keychain, tab persistence across restarts, Linux desktop integration (AppImage/.deb/.desktop entry — the release is a portable tarball).