From 39246e0d976bcf3163e36aa6f8d1161430cd77ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 13 Nov 2026 13:49:02 +0100 Subject: [PATCH 1/5] build: publish the native artifacts from the npm package Replace .npmignore with a files allowlist so the tarball carries build/, src/, the Android and iOS sources with their generated bindings, and the Expo config, while keeping the Rust workspace, the example app and the build outputs out. Bump to 0.1.0 and read the Gradle version from package.json so the two cannot drift. --- .npmignore | 35 ----------------------------------- android/build.gradle | 4 ++-- package-lock.json | 8 ++++---- package.json | 14 +++++++++++++- 4 files changed, 19 insertions(+), 42 deletions(-) delete mode 100644 .npmignore 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/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/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..306b615 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,21 @@ { "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", From 8e74a3462828bda80d64649212ea662b1db73bd8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 13 Nov 2026 13:49:40 +0100 Subject: [PATCH 2/5] style: format the repository with its own prettier config The .prettierrc was in place but the sources were never run through it. Add a format:check script, ignore generated and vendored paths, and gate pull requests on the check. --- .github/workflows/ci.yml | 12 +++++++++++- .prettierignore | 11 +++++++++++ README.md | 16 ++++++++-------- example/App.tsx | 8 +++++++- example/SshFilesDemo.tsx | 4 +--- example/SshTerminalDemo.tsx | 21 ++++++++++++++------- example/TerminalDemo.tsx | 8 +++++++- package.json | 2 ++ src/ReactNativeSshModule.ts | 10 ++-------- src/ReactNativeSshModule.web.ts | 10 ++-------- src/errors.ts | 5 +---- src/forward.ts | 23 +++++------------------ src/sftp.ts | 26 ++++++-------------------- src/ssh.ts | 13 +++---------- src/terminal/TerminalView.tsx | 10 +++++++--- 15 files changed, 87 insertions(+), 92 deletions(-) create mode 100644 .prettierignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 89d942b..2ee7fae 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 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 19ceb60..cb40acd 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 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.json b/package.json index 306b615..8c67c35 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "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/", "test": "node internal/module_scripts/test.js", "prepare": "node internal/module_scripts/prepare.js", diff --git a/src/ReactNativeSshModule.ts b/src/ReactNativeSshModule.ts index e04cf4d..2bea03d 100644 --- a/src/ReactNativeSshModule.ts +++ b/src/ReactNativeSshModule.ts @@ -59,14 +59,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 e291827..bbd1d23 100644 --- a/src/ReactNativeSshModule.web.ts +++ b/src/ReactNativeSshModule.web.ts @@ -35,10 +35,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)); } @@ -147,10 +144,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/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))} /> ); From bbef3609b97a6cf9ddc12bb6904bffeb45a199dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 13 Nov 2026 13:50:07 +0100 Subject: [PATCH 3/5] ci: publish the package with its native artifacts Build the xcframework with its Swift bindings on macOS and the jniLibs with their Kotlin bindings on Linux, then assemble, verify and publish from a single job. Triggered by a v* tag or by hand with a dist-tag, and signed with provenance. --- .github/workflows/release.yml | 209 ++++++++++++++++++++++++++++++++++ 1 file changed, 209 insertions(+) create mode 100644 .github/workflows/release.yml 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 }}" From 61c789a39d973b94458a51553adab61bd1076f84 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 13 Nov 2026 14:03:43 +0100 Subject: [PATCH 4/5] build: compile the TypeScript before packing, not on install A prepare script also runs for consumers who install the tarball, where the internal build scripts it calls are absent. prepack covers publish, pack and git dependencies without touching a plain install, and the CI jobs now build the module explicitly. --- .github/workflows/ci.yml | 6 ++++++ README.md | 23 +++++++++++++++++++++++ package.json | 2 +- 3 files changed, 30 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2ee7fae..93c4c98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -160,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 @@ -195,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/README.md b/README.md index cb40acd..22c69a8 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/package.json b/package.json index 8c67c35..94777bb 100644 --- a/package.json +++ b/package.json @@ -22,8 +22,8 @@ "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" }, From 0d4ea380bcce7abf38fe7c6bc90679b11a4bdc4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Igor=20=C5=A0=C4=87eki=C4=87?= Date: Fri, 13 Nov 2026 14:08:07 +0100 Subject: [PATCH 5/5] style: format the export module after merging main --- README.md | 8 ++++---- src/ReactNativeSshModule.web.ts | 5 +---- src/export.ts | 10 ++-------- 3 files changed, 7 insertions(+), 16 deletions(-) diff --git a/README.md b/README.md index e30f118..73dae1f 100644 --- a/README.md +++ b/README.md @@ -139,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/src/ReactNativeSshModule.web.ts b/src/ReactNativeSshModule.web.ts index 7c78434..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)); } 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)); }