diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89d942b..93c4c98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -10,6 +10,17 @@ concurrency: cancel-in-progress: true jobs: + format: + name: Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + - run: npm ci + - run: npm run format:check + rust: name: Rust runs-on: ubuntu-latest @@ -91,7 +102,6 @@ jobs: docker rm -f ssh-core-password ssh-core-twofactor ssh-core-silent ssh-core-http ssh-core-bastion ssh-core-target docker network rm ssh-core-net - android: name: Android runs-on: ubuntu-latest @@ -150,6 +160,9 @@ jobs: - name: Install module dependencies run: npm ci + - name: Build the module + run: npm run build + - name: Install example dependencies working-directory: example run: npm ci @@ -185,6 +198,9 @@ jobs: - name: Install module dependencies run: npm ci + - name: Build the module + run: npm run build + - name: Install example dependencies working-directory: example run: npm ci diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..5465e92 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,209 @@ +name: Release + +on: + workflow_dispatch: + inputs: + dist-tag: + description: npm dist-tag to publish under + required: true + type: string + default: next + push: + tags: + - 'v*' + +permissions: + contents: read + +concurrency: + group: release-${{ github.ref }} + cancel-in-progress: false + +jobs: + ios: + name: iOS xcframework + runs-on: macos-latest + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + + - name: Install the Rust targets + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: stable + targets: aarch64-apple-ios,aarch64-apple-ios-sim,x86_64-apple-ios + + - name: Cache the Rust build + uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 + with: + workspaces: rust + + - name: Build the xcframework and the Swift bindings + run: scripts/build-ios.sh + + - name: Upload the iOS artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: ios + path: ios + if-no-files-found: error + retention-days: 7 + + android: + name: Android jniLibs + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + + - name: Install the JDK + uses: actions/setup-java@cf277c60eb25467037889841efdb72551f06f6c3 + with: + distribution: temurin + java-version: '17' + + - name: Install the Android SDK packages + run: | + set -euo pipefail + sdk_root="${ANDROID_HOME:-${ANDROID_SDK_ROOT:-/usr/local/lib/android/sdk}}" + sdkmanager="$(ls -d "$sdk_root"/cmdline-tools/*/bin/sdkmanager | sort -V | tail -n 1)" + yes | "$sdkmanager" --licenses > /dev/null || true + "$sdkmanager" --install \ + "platform-tools" \ + "platforms;android-37.0" \ + "build-tools;37.0.0" \ + "ndk;27.1.12297006" + { + echo "ANDROID_HOME=$sdk_root" + echo "ANDROID_SDK_ROOT=$sdk_root" + echo "ANDROID_NDK_HOME=$sdk_root/ndk/27.1.12297006" + } >> "$GITHUB_ENV" + + - name: Install the Rust targets + uses: dtolnay/rust-toolchain@v1 + with: + toolchain: stable + targets: aarch64-linux-android,x86_64-linux-android + + - name: Cache the Rust build + uses: Swatinem/rust-cache@d1527c8f67ef9af357290475d4af4b0fab8f12e0 + with: + workspaces: rust + + - name: Install cargo-ndk + run: cargo install cargo-ndk --locked + + - name: Build the jniLibs and the Kotlin bindings + run: scripts/build-android.sh + + - name: Upload the jniLibs + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: android-jniLibs + path: android/src/main/jniLibs + if-no-files-found: error + retention-days: 7 + + - name: Upload the Kotlin bindings + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 + with: + name: android-bindings + path: android/src/main/java/ai/kilo/reactnativessh/uniffi + if-no-files-found: error + retention-days: 7 + + publish: + name: Publish to npm + needs: [ios, android] + runs-on: ubuntu-latest + permissions: + contents: read + id-token: write + steps: + - name: Checkout + uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 + + - name: Install Node with the npm registry + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 + with: + node-version: 22 + registry-url: https://registry.npmjs.org + + - name: Download the iOS artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: ios + path: ios + + - name: Download the jniLibs + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: android-jniLibs + path: android/src/main/jniLibs + + - name: Download the Kotlin bindings + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 + with: + name: android-bindings + path: android/src/main/java/ai/kilo/reactnativessh/uniffi + + - name: Install the dependencies + run: npm ci + + - name: Build the TypeScript + run: npm run build + + - name: Verify the artifacts the package ships + run: | + set -euo pipefail + xcframework=ios/Frameworks/SshCore.xcframework + test -f "$xcframework/Info.plist" + test -f "$xcframework/ios-arm64/libssh_core.a" + test -f "$xcframework/ios-arm64_x86_64-simulator/libssh_core.a" + test -f ios/Generated/SshCore.swift + test -f android/src/main/jniLibs/arm64-v8a/libssh_core.so + test -f android/src/main/jniLibs/x86_64/libssh_core.so + test -f android/src/main/java/ai/kilo/reactnativessh/uniffi/ssh_core.kt + test -f build/index.js + test -f build/index.d.ts + + - name: Resolve the dist-tag + id: dist + env: + INPUT_TAG: ${{ inputs.dist-tag }} + EVENT_NAME: ${{ github.event_name }} + REF_NAME: ${{ github.ref_name }} + run: | + set -euo pipefail + if [ "$EVENT_NAME" = "workflow_dispatch" ]; then + tag="$INPUT_TAG" + elif [[ "$REF_NAME" == *-* ]]; then + tag=next + else + tag=latest + fi + if ! [[ "$tag" =~ ^[a-z][a-z0-9._-]*$ ]]; then + echo "Not a usable dist-tag: $tag" >&2 + exit 1 + fi + echo "tag=$tag" >> "$GITHUB_OUTPUT" + + - name: Check the tag against the package version + if: github.event_name == 'push' + env: + REF_NAME: ${{ github.ref_name }} + run: | + set -euo pipefail + version="$(node -p 'require("./package.json").version')" + if [ "v$version" != "$REF_NAME" ]; then + echo "Tag $REF_NAME does not match the package version $version" >&2 + exit 1 + fi + + - name: List the tarball contents + run: npm pack --dry-run 2>&1 | tee pack-dry-run.log + + - name: Publish + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: npm publish --provenance --access public --tag "${{ steps.dist.outputs.tag }}" diff --git a/.npmignore b/.npmignore deleted file mode 100644 index b9d6160..0000000 --- a/.npmignore +++ /dev/null @@ -1,35 +0,0 @@ -# Exclude all top-level hidden files and directories by convention -/.* - -# Exclude nested package ignore files -**/.gitignore -**/.npmignore - -# Exclude TypeScript project configuration and incremental build caches -**/tsconfig.json -**/tsconfig.all.json -**/*.tsbuildinfo - -# Exclude monorepo build configuration -/turbo.json -/pnpm-workspace.yaml - -# Exclude test configuration -**/jest.config.* -**/jest.setup.* -**/jest-setup.* - -# Exclude tarballs generated by `npm pack` -/*.tgz - -__mocks__ -__tests__ - -/DEVELOPMENT.md - -/babel.config.js -/internal/module_scripts/ -/android/src/androidTest/** -/android/src/test/** -/android/build/** -/example/ diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..a518d7c --- /dev/null +++ b/.prettierignore @@ -0,0 +1,11 @@ +node_modules +build +package-lock.json +example/package-lock.json +example/android +example/ios +android/src/main/jniLibs +android/src/main/java/ai/kilo/reactnativessh/uniffi +ios/Frameworks +ios/Generated +rust \ No newline at end of file diff --git a/README.md b/README.md index 3205405..73dae1f 100644 --- a/README.md +++ b/README.md @@ -12,14 +12,14 @@ releases. ## Layout -| Path | Contents | -| --- | --- | -| `rust/ssh-core` | The Rust library (`core_version`, `generate_ed25519_key`) | -| `rust/uniffi-bindgen` | Bindings generator binary used by the build scripts | -| `android/`, `ios/` | The Expo module and its native build files | -| `src/` | TypeScript API | -| `example/` | Expo app that prints the core version and a generated key | -| `scripts/` | Native build scripts | +| Path | Contents | +| --------------------- | --------------------------------------------------------- | +| `rust/ssh-core` | The Rust library (`core_version`, `generate_ed25519_key`) | +| `rust/uniffi-bindgen` | Bindings generator binary used by the build scripts | +| `android/`, `ios/` | The Expo module and its native build files | +| `src/` | TypeScript API | +| `example/` | Expo app that prints the core version and a generated key | +| `scripts/` | Native build scripts | ## Building @@ -45,6 +45,29 @@ npx expo prebuild --platform android npx expo run:android ``` +## Releasing + +The tarball ships the generated native artifacts, so a release is assembled from +two builders rather than from a developer machine. `.github/workflows/release.yml` +runs on a `v*` tag or by hand with a `dist-tag` input (default `next`): + +1. macOS runs `scripts/build-ios.sh` and uploads `ios/`. +2. Linux runs `scripts/build-android.sh` and uploads the jniLibs and the Kotlin + bindings. +3. One job downloads both into the checkout, runs `npm run build`, lists + `npm pack --dry-run`, asserts the xcframework slices, both `.so` files, the + Kotlin and Swift bindings and the compiled `build/` are present, then runs + `npm publish --provenance --access public --tag `. + +The publish job needs an `NPM_TOKEN` secret with write access to `@kilocode` and +`id-token: write` for the provenance attestation. A tag must equal the +`package.json` version with a `v` prefix; a tag containing a hyphen publishes +under `next`, any other tag under `latest`. + +Packing by hand works too, but only after both scripts have run: the `files` +allowlist ships whatever is in `android/src/main/jniLibs`, `ios/Frameworks` and +`ios/Generated` at that moment, and none of them are committed. + ## Terminal input Shell input and resize requests share one ordered queue. Shutdown stops accepting @@ -116,10 +139,10 @@ const backup = JSON.parse(await decryptExport(await readAsStringAsync(path), pas Rejections carry a `code`: -| `code` | Cause | -| --- | --- | -| `WrongPassphrase` | The passphrase does not open the file. | -| `InvalidExport` | The payload exceeds 16 MiB, the passphrase is shorter than 8 characters when encrypting, or the input is not an intact age file. | +| `code` | Cause | +| ----------------- | -------------------------------------------------------------------------------------------------------------------------------- | +| `WrongPassphrase` | The passphrase does not open the file. | +| `InvalidExport` | The payload exceeds 16 MiB, the passphrase is shorter than 8 characters when encrypting, or the input is not an intact age file. | ## License diff --git a/android/build.gradle b/android/build.gradle index d30279c..2d8f880 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -4,13 +4,13 @@ plugins { } group = 'ai.kilo.reactnativessh' -version = '0.0.0' +version = new groovy.json.JsonSlurper().parse(file('../package.json')).version android { namespace "ai.kilo.reactnativessh" defaultConfig { versionCode 1 - versionName "0.0.0" + versionName project.version } lintOptions { abortOnError false diff --git a/example/App.tsx b/example/App.tsx index 4a73dbe..93f265b 100644 --- a/example/App.tsx +++ b/example/App.tsx @@ -39,7 +39,13 @@ export default function App() { const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#101418', paddingTop: 40 }, tabs: { flexDirection: 'row', gap: 6, paddingHorizontal: 10, paddingBottom: 8 }, - tab: { flex: 1, paddingVertical: 8, alignItems: 'center', backgroundColor: '#1b2027', borderRadius: 6 }, + tab: { + flex: 1, + paddingVertical: 8, + alignItems: 'center', + backgroundColor: '#1b2027', + borderRadius: 6, + }, tabSelected: { backgroundColor: '#2f6f4f' }, tabLabel: { color: '#d8dee9', fontSize: 12 }, }); diff --git a/example/SshFilesDemo.tsx b/example/SshFilesDemo.tsx index 1720bd3..225a0d2 100644 --- a/example/SshFilesDemo.tsx +++ b/example/SshFilesDemo.tsx @@ -100,9 +100,7 @@ export default function SshFilesDemo() { try { const entries = await listDirectory(sftpId, REMOTE_DIR); - entries.forEach((entry) => - append(`${entry.kind} ${entry.size} ${entry.name}`) - ); + entries.forEach((entry) => append(`${entry.kind} ${entry.size} ${entry.name}`)); } catch (cause) { setError(describe(cause)); } diff --git a/example/SshTerminalDemo.tsx b/example/SshTerminalDemo.tsx index 1c5d1f3..d434b48 100644 --- a/example/SshTerminalDemo.tsx +++ b/example/SshTerminalDemo.tsx @@ -42,7 +42,11 @@ export default function SshTerminalDemo() { 'Unknown host key', `${request.algorithm}\n${request.fingerprintSha256}`, [ - { text: 'Reject', style: 'cancel', onPress: () => answerHostKey(request.requestId, false) }, + { + text: 'Reject', + style: 'cancel', + onPress: () => answerHostKey(request.requestId, false), + }, { text: 'Accept', onPress: () => answerHostKey(request.requestId, true) }, ], { cancelable: false } @@ -117,12 +121,15 @@ export default function SshTerminalDemo() { if (sessionId) disconnect(sessionId).catch(() => undefined); }, []); - useEffect(() => () => { - const shellId = shellRef.current; - const sessionId = sessionRef.current; - if (shellId) closeShell(shellId); - if (sessionId) disconnect(sessionId).catch(() => undefined); - }, []); + useEffect( + () => () => { + const shellId = shellRef.current; + const sessionId = sessionRef.current; + if (shellId) closeShell(shellId); + if (sessionId) disconnect(sessionId).catch(() => undefined); + }, + [] + ); return ( diff --git a/example/TerminalDemo.tsx b/example/TerminalDemo.tsx index aaa276a..493d56f 100644 --- a/example/TerminalDemo.tsx +++ b/example/TerminalDemo.tsx @@ -136,6 +136,12 @@ const styles = StyleSheet.create({ status: { color: '#5a6472', fontSize: 11 }, terminal: { flex: 1 }, actions: { flexDirection: 'row', gap: 8, padding: 12, backgroundColor: '#1b2027' }, - action: { flex: 1, paddingVertical: 10, alignItems: 'center', backgroundColor: '#2a323d', borderRadius: 6 }, + action: { + flex: 1, + paddingVertical: 10, + alignItems: 'center', + backgroundColor: '#2a323d', + borderRadius: 6, + }, actionLabel: { color: '#d8dee9', fontSize: 12 }, }); diff --git a/package-lock.json b/package-lock.json index 34a9169..e649a32 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { - "name": "react-native-ssh", - "version": "0.0.0", + "name": "@kilocode/react-native-ssh", + "version": "0.1.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "react-native-ssh", - "version": "0.0.0", + "name": "@kilocode/react-native-ssh", + "version": "0.1.0", "license": "Apache-2.0", "devDependencies": { "@babel/core": "^7.26.0", diff --git a/package.json b/package.json index 9473f3c..94777bb 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,29 @@ { "name": "@kilocode/react-native-ssh", - "version": "0.0.0", + "version": "0.1.0", "description": "SSH client for React Native: Rust core with uniffi bindings", "main": "build/index.js", "types": "build/index.d.ts", + "files": [ + "build", + "src", + "android/build.gradle", + "android/src/main", + "ios", + "expo-module.config.json", + "app.plugin.js", + "README.md", + "LICENSE", + "THIRD_PARTY_NOTICES.md" + ], "scripts": { "build": "node internal/module_scripts/build.js", "clean": "node internal/module_scripts/clean.js", + "format": "prettier --write \"**/*.{js,cjs,mjs,ts,tsx,json,md,yml,yaml}\"", + "format:check": "prettier --check \"**/*.{js,cjs,mjs,ts,tsx,json,md,yml,yaml}\"", "lint": "eslint src/", + "prepack": "node internal/module_scripts/prepare.js", "test": "node internal/module_scripts/test.js", - "prepare": "node internal/module_scripts/prepare.js", "open:ios": "node internal/module_scripts/open-ios.js", "open:android": "node internal/module_scripts/open-android.js" }, diff --git a/src/ReactNativeSshModule.ts b/src/ReactNativeSshModule.ts index 8d0aece..4e23a4a 100644 --- a/src/ReactNativeSshModule.ts +++ b/src/ReactNativeSshModule.ts @@ -61,14 +61,8 @@ declare class ReactNativeSshModule extends NativeModule { ): Promise; cancelTransfer(transferId: string): void; - startLocalForward( - sessionId: string, - config: NativeForwardConfig - ): Promise; - startRemoteForward( - sessionId: string, - config: NativeForwardConfig - ): Promise; + startLocalForward(sessionId: string, config: NativeForwardConfig): Promise; + startRemoteForward(sessionId: string, config: NativeForwardConfig): Promise; stopForward(forwardId: string): Promise; } diff --git a/src/ReactNativeSshModule.web.ts b/src/ReactNativeSshModule.web.ts index 6dba61c..63f5eba 100644 --- a/src/ReactNativeSshModule.web.ts +++ b/src/ReactNativeSshModule.web.ts @@ -31,10 +31,7 @@ class ReactNativeSshModule extends NativeModule { return unsupported(); } - encryptExport( - _plaintextJson: string, - _passphrase: string - ): Promise { + encryptExport(_plaintextJson: string, _passphrase: string): Promise { return Promise.reject(new Error(unavailable)); } @@ -46,10 +43,7 @@ class ReactNativeSshModule extends NativeModule { return Promise.reject(new Error(unavailable)); } - connectVia( - _jumpSessionId: string, - _config: NativeConnectConfig - ): Promise { + connectVia(_jumpSessionId: string, _config: NativeConnectConfig): Promise { return Promise.reject(new Error(unavailable)); } @@ -158,10 +152,7 @@ class ReactNativeSshModule extends NativeModule { unsupported(); } - startLocalForward( - _sessionId: string, - _config: NativeForwardConfig - ): Promise { + startLocalForward(_sessionId: string, _config: NativeForwardConfig): Promise { return Promise.reject(new Error(unavailable)); } diff --git a/src/errors.ts b/src/errors.ts index acaa567..6ed239e 100644 --- a/src/errors.ts +++ b/src/errors.ts @@ -1,7 +1,4 @@ -import { - SSH_ERROR_CODES, - type SshErrorCode, -} from './ReactNativeSsh.types'; +import { SSH_ERROR_CODES, type SshErrorCode } from './ReactNativeSsh.types'; import ReactNativeSsh from './ReactNativeSshModule'; /** A failed SSH call, categorised by [`SshErrorCode`]. */ diff --git a/src/export.ts b/src/export.ts index 944cc45..0f22990 100644 --- a/src/export.ts +++ b/src/export.ts @@ -9,10 +9,7 @@ import { guard } from './errors'; * is shorter than 8 characters. The key derivation runs natively, off the * JavaScript thread. */ -export function encryptExport( - plaintextJson: string, - passphrase: string -): Promise { +export function encryptExport(plaintextJson: string, passphrase: string): Promise { return guard(() => ReactNativeSsh.encryptExport(plaintextJson, passphrase)); } @@ -23,9 +20,6 @@ export function encryptExport( * with `InvalidExport` when the input is not an age file, is damaged, or holds * more than 16 MiB. */ -export function decryptExport( - armored: string, - passphrase: string -): Promise { +export function decryptExport(armored: string, passphrase: string): Promise { return guard(() => ReactNativeSsh.decryptExport(armored, passphrase)); } diff --git a/src/forward.ts b/src/forward.ts index 6b89984..a7ad24f 100644 --- a/src/forward.ts +++ b/src/forward.ts @@ -1,7 +1,4 @@ -import type { - NativeForwardConfig, - NativeForwardStart, -} from './ReactNativeSsh.types'; +import type { NativeForwardConfig, NativeForwardStart } from './ReactNativeSsh.types'; import ReactNativeSsh from './ReactNativeSshModule'; import { guard } from './errors'; @@ -25,23 +22,13 @@ export type Forward = { const DEFAULT_BIND_HOST = '127.0.0.1'; /** Listens on the device and reaches `destHost:destPort` from the server. */ -export function startLocalForward( - sessionId: string, - config: ForwardConfig -): Promise { - return track(() => - ReactNativeSsh.startLocalForward(sessionId, nativeConfig(config)) - ); +export function startLocalForward(sessionId: string, config: ForwardConfig): Promise { + return track(() => ReactNativeSsh.startLocalForward(sessionId, nativeConfig(config))); } /** Asks the server to listen and delivers connections to the device. */ -export function startRemoteForward( - sessionId: string, - config: ForwardConfig -): Promise { - return track(() => - ReactNativeSsh.startRemoteForward(sessionId, nativeConfig(config)) - ); +export function startRemoteForward(sessionId: string, config: ForwardConfig): Promise { + return track(() => ReactNativeSsh.startRemoteForward(sessionId, nativeConfig(config))); } async function track(start: () => Promise): Promise { diff --git a/src/sftp.ts b/src/sftp.ts index f40dd63..b834fe2 100644 --- a/src/sftp.ts +++ b/src/sftp.ts @@ -12,10 +12,7 @@ export function closeSftp(sftpId: string): Promise { return guard(() => ReactNativeSsh.closeSftp(sftpId)); } -export function listDirectory( - sftpId: string, - path: string -): Promise { +export function listDirectory(sftpId: string, path: string): Promise { return guard(() => ReactNativeSsh.sftpList(sftpId, path)); } @@ -32,11 +29,7 @@ export function makeDirectory(sftpId: string, path: string): Promise { return guard(() => ReactNativeSsh.sftpMkdir(sftpId, path)); } -export function renamePath( - sftpId: string, - from: string, - to: string -): Promise { +export function renamePath(sftpId: string, from: string, to: string): Promise { return guard(() => ReactNativeSsh.sftpRename(sftpId, from, to)); } @@ -60,8 +53,7 @@ export function download( options: TransferOptions = {} ): Promise { return transfer( - (transferId) => - ReactNativeSsh.sftpDownload(sftpId, remotePath, localFileUrl, transferId), + (transferId) => ReactNativeSsh.sftpDownload(sftpId, remotePath, localFileUrl, transferId), options ); } @@ -77,8 +69,7 @@ export function upload( options: TransferOptions = {} ): Promise { return transfer( - (transferId) => - ReactNativeSsh.sftpUpload(sftpId, localFileUrl, remotePath, transferId), + (transferId) => ReactNativeSsh.sftpUpload(sftpId, localFileUrl, remotePath, transferId), options ); } @@ -90,9 +81,7 @@ async function transfer( options: TransferOptions ): Promise { const transferId = `transfer-${(transfers += 1)}`; - const subscription = options.onProgress - ? listen(transferId, options.onProgress) - : undefined; + const subscription = options.onProgress ? listen(transferId, options.onProgress) : undefined; const abort = () => ReactNativeSsh.cancelTransfer(transferId); options.signal?.addEventListener('abort', abort); @@ -104,10 +93,7 @@ async function transfer( } } -function listen( - transferId: string, - onProgress: NonNullable -) { +function listen(transferId: string, onProgress: NonNullable) { return ReactNativeSsh.addListener('onTransferProgress', (event) => { if (event.transferId === transferId) { onProgress({ transferred: event.transferred, total: event.total }); diff --git a/src/ssh.ts b/src/ssh.ts index 9b5f7ad..c9e61f2 100644 --- a/src/ssh.ts +++ b/src/ssh.ts @@ -34,13 +34,8 @@ export function connect(options: ConnectOptions): Promise { * reaches the target. Chaining bastions works the same way: each jump session * may itself have been opened through another one. */ -export function connectVia( - jumpSessionId: string, - options: ConnectOptions -): Promise { - return guard(() => - ReactNativeSsh.connectVia(jumpSessionId, nativeConnectConfig(options)) - ); +export function connectVia(jumpSessionId: string, options: ConnectOptions): Promise { + return guard(() => ReactNativeSsh.connectVia(jumpSessionId, nativeConnectConfig(options))); } /** Runs one command against a fresh session channel. */ @@ -171,8 +166,7 @@ function nativeConnectConfig(options: ConnectOptions): NativeConnectConfig { username: options.username, auth: nativeAuth(options.auth), connectTimeoutMs: options.connectTimeoutMs ?? DEFAULT_CONNECT_TIMEOUT_MS, - keepaliveIntervalS: - options.keepaliveIntervalS ?? DEFAULT_KEEPALIVE_INTERVAL_S, + keepaliveIntervalS: options.keepaliveIntervalS ?? DEFAULT_KEEPALIVE_INTERVAL_S, }; } @@ -190,4 +184,3 @@ function nativeAuth(auth: ConnectOptions['auth']): NativeAuth { return { type: 'keyboardInteractive' }; } } - diff --git a/src/terminal/TerminalView.tsx b/src/terminal/TerminalView.tsx index 8283c86..7b672ad 100644 --- a/src/terminal/TerminalView.tsx +++ b/src/terminal/TerminalView.tsx @@ -28,7 +28,7 @@ export const TerminalView = forwardRef(fu onClosed, ...viewProps }, - ref, + ref ) { const nativeTheme = useMemo(() => resolveTheme(theme), [theme]); @@ -45,8 +45,12 @@ export const TerminalView = forwardRef(fu onLinkPress={onLinkPress && ((event) => onLinkPress(event.nativeEvent.url))} onBell={onBell} onTitleChange={onTitleChange && ((event) => onTitleChange(event.nativeEvent.title))} - onSelectionChange={onSelectionChange && ((event) => onSelectionChange(event.nativeEvent.selecting))} - onFontSizeChange={onFontSizeChange && ((event) => onFontSizeChange(event.nativeEvent.fontSize))} + onSelectionChange={ + onSelectionChange && ((event) => onSelectionChange(event.nativeEvent.selecting)) + } + onFontSizeChange={ + onFontSizeChange && ((event) => onFontSizeChange(event.nativeEvent.fontSize)) + } onClosed={onClosed && ((event) => onClosed(event.nativeEvent))} /> );