From 3c897d138549fc049a0bbbe9fae6675af53d33e6 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Fri, 9 Oct 2026 07:35:50 -0500 Subject: [PATCH 1/5] fix: ask for a password when a host has no saved login --- CHANGELOG.md | 6 ++++++ src/commands/ssh.ts | 9 ++++++++- test/ssh.test.ts | 36 +++++++++++++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00184cf..b6d65b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 1.1.1 + +### Fixed + +- `ssh` asks for a password when the host has no saved login, instead of hanging until it times out + ## 1.1.0 Works with Termix plugins. Needs Termix 2.9 or newer. diff --git a/src/commands/ssh.ts b/src/commands/ssh.ts index 22bdbad..8c50f67 100644 --- a/src/commands/ssh.ts +++ b/src/commands/ssh.ts @@ -117,6 +117,7 @@ const AUTH_STEPS = [ "totp_required", "totp_retry", "password_required", + "auth_method_not_available", "*_auth_required", ]; const MAX_AUTH_ROUNDS = 10; @@ -159,7 +160,7 @@ export async function decideHostKey( * passphrase, a verification code or a keyboard-interactive password. A * browser sign-in cannot happen in a terminal, so that fails straight away. */ -async function completeAuth( +export async function completeAuth( socket: TerminalSocket, connect: { cols: number; rows: number; hostConfig: HostConfig }, trustHostKey: boolean, @@ -187,6 +188,12 @@ async function completeAuth( ); break; } + case "auth_method_not_available": { + // The host has no saved login the server can use, so ask for one. + const password = await promptHidden("Password: "); + socket.send("reconnect_with_credentials", { ...connect, password }); + break; + } case "passphrase_required": { const keyPassword = await promptHidden("Key passphrase: "); socket.send("reconnect_with_credentials", { ...connect, keyPassword }); diff --git a/test/ssh.test.ts b/test/ssh.test.ts index 444cb96..87d0fcd 100644 --- a/test/ssh.test.ts +++ b/test/ssh.test.ts @@ -1,5 +1,12 @@ import { describe, it, expect, vi } from "vitest"; -import { decideHostKey } from "../src/commands/ssh.js"; + +vi.mock("../src/core/prompt.js", async (importOriginal) => ({ + ...(await importOriginal()), + promptHidden: vi.fn(async () => "secret"), +})); + +import { completeAuth, decideHostKey } from "../src/commands/ssh.js"; +import type { TerminalSocket } from "../src/terminal/ws-client.js"; describe("decideHostKey", () => { it("trusts a new key with --trust-host-key without asking", async () => { @@ -23,3 +30,30 @@ describe("decideHostKey", () => { } }); }); + +describe("completeAuth", () => { + const connect = { + cols: 80, + rows: 24, + hostConfig: { id: 1, ip: "10.0.0.1", port: 22, username: "me" }, + }; + + it("asks for a password when the host has no usable login", async () => { + const replies = [ + { type: "auth_method_not_available" }, + { type: "connected" }, + ]; + const send = vi.fn(); + const socket = { + waitFor: vi.fn(async () => replies.shift()), + send, + } as unknown as TerminalSocket; + + await completeAuth(socket, connect, false); + + expect(send).toHaveBeenCalledWith("reconnect_with_credentials", { + ...connect, + password: "secret", + }); + }); +}); From 06415bf4c2e31a67cd5aac9f7c277267450b1694 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Fri, 9 Oct 2026 07:39:27 -0500 Subject: [PATCH 2/5] chore: sync api specs from the docs --- spec/openapi.json | 11 +++++++---- spec/plugins/fleets.json | 6 +++--- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/spec/openapi.json b/spec/openapi.json index 7bf29b7..091cdab 100644 --- a/spec/openapi.json +++ b/spec/openapi.json @@ -186,6 +186,9 @@ }, "401": { "description": "Not signed in." + }, + "403": { + "description": "API keys cannot link a desktop." } } } @@ -676,7 +679,7 @@ "localVersion": { "type": "string", "description": "Version of the running instance. Use this field for client compatibility checks.", - "example": "2.7.1" + "example": "26.10.0" }, "status": { "type": "string", @@ -697,13 +700,13 @@ "remoteVersion": { "type": "string", "description": "Newest release version on the channel. Present only when the update check succeeds.", - "example": "2.8.0" + "example": "26.10.0" }, "version": { "type": "string", "deprecated": true, "description": "Legacy alias of remoteVersion, not the running instance's version. Present only when the update check succeeds. Use localVersion for the instance or remoteVersion for the latest release.", - "example": "2.8.0" + "example": "26.10.0" }, "latest_release": { "type": "object", @@ -6601,7 +6604,7 @@ "/credential-sidebar/preferences": { "get": { "summary": "Get the credential sidebar preferences for the current user", - "description": "Returns the current user's saved credential sidebar preferences (sort, filters, open folders, display settings). Unlike /host-sidebar/preferences, there is no legacy-column migration to perform here — credentials never had exploded preference columns on userPreferences — so a first-time GET simply returns the defaults without writing a row; a row is only created once the user actually changes something via PUT.", + "description": "Returns the current user's saved credential sidebar preferences (sort, filters, open folders, display settings). Unlike /host-sidebar/preferences, there is no legacy-column migration to perform here, credentials never had exploded preference columns on userPreferences, so a first-time GET simply returns the defaults without writing a row; a row is only created once the user actually changes something via PUT.", "tags": ["Credential Sidebar"], "responses": { "200": { diff --git a/spec/plugins/fleets.json b/spec/plugins/fleets.json index a8bac42..22eb0bb 100644 --- a/spec/plugins/fleets.json +++ b/spec/plugins/fleets.json @@ -331,7 +331,7 @@ "/plugin-api/fleets/{id}/execute": { "post": { "summary": "Run a command across every host in a fleet", - "description": "Fans out concurrently to every effective member host the caller has edit-level access to. $HOST/$USER/$PORT/$NAME/$INPUT_n substitution is applied per host, same grammar as snippet execution. One host failing does not stop the others.", + "description": "Runs on up to 10 hosts at a time, across every effective member host the caller has edit-level access to. $HOST/$USER/$PORT/$NAME/$INPUT_n substitution is applied per host, same grammar as snippet execution. One host failing does not stop the others.", "tags": ["Fleets"], "parameters": [ { @@ -374,7 +374,7 @@ "/plugin-api/fleets/{id}/transfer/push": { "post": { "summary": "Push an uploaded file to the same remote path on every host in a fleet", - "description": "Fans out concurrently to every effective member host the caller has edit-level access to. Single file only (v1) - the file is buffered once server-side and written to each host via SFTP.", + "description": "Runs on up to 10 hosts at a time, across every effective member host the caller has edit-level access to. Single file only (v1) - the file is buffered once server-side and written to each host via SFTP.", "tags": ["Fleets"], "parameters": [ { @@ -418,7 +418,7 @@ "/plugin-api/fleets/{id}/transfer/pull": { "post": { "summary": "Pull the same remote path from every host in a fleet", - "description": "Fans out concurrently to every effective member host the caller has edit-level access to, reads remotePath via SFTP from each, and returns a single zip archive with one entry per successful host `(/).` Single file only (v1).", + "description": "Runs on up to 10 hosts at a time, across every effective member host the caller has edit-level access to, reads remotePath via SFTP from each, and returns a single zip archive with one entry per successful host `(-/).` Single file only, up to 200 MB per host.", "tags": ["Fleets"], "parameters": [ { From 42b4b5310560d4bcd015e845872cba07914d3899 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Fri, 9 Oct 2026 10:33:49 -0500 Subject: [PATCH 3/5] chore: tag cli releases as v --- .github/workflows/release.yml | 6 +++--- scripts/generate-release-body.cjs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 642b8bb..102fe0c 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -59,7 +59,7 @@ jobs: fi echo "dev_branch=$DEV_BRANCH" >> "$GITHUB_OUTPUT" echo "version=$VERSION" >> "$GITHUB_OUTPUT" - echo "release_tag=release-$VERSION-tag" >> "$GITHUB_OUTPUT" + echo "release_tag=v$VERSION" >> "$GITHUB_OUTPUT" - name: Check CHANGELOG.md has notes for this version run: | @@ -147,7 +147,7 @@ jobs: GH_TOKEN: ${{ secrets.TERMIX_PAT || github.token }} run: | DEV_BRANCH="${{ needs.prep.outputs.dev_branch }}" - TITLE="release-${{ needs.prep.outputs.version }}" + TITLE="v${{ needs.prep.outputs.version }}" if [ "${{ inputs.mode }}" = "Overwrite release" ]; then echo "OVERWRITE: no merge; building from main as-is." @@ -303,7 +303,7 @@ jobs: GH_TOKEN: ${{ github.token }} run: | TAG="${{ needs.prep.outputs.release_tag }}" - TITLE="release-${{ needs.prep.outputs.version }}" + TITLE="v${{ needs.prep.outputs.version }}" if gh release view "$TAG" --repo ${{ github.repository }} >/dev/null 2>&1; then gh release edit "$TAG" --repo ${{ github.repository }} \ --title "$TITLE" --notes-file RELEASE_BODY.md diff --git a/scripts/generate-release-body.cjs b/scripts/generate-release-body.cjs index 286da08..ded6cbc 100644 --- a/scripts/generate-release-body.cjs +++ b/scripts/generate-release-body.cjs @@ -44,7 +44,7 @@ function changelogSection(changelog, version) { * there. */ function buildTable(version) { - const tag = `release-${version}-tag`; + const tag = `v${version}`; const base = `https://github.com/Termix-SSH/CLI/releases/download/${tag}`; const url = (file) => `${base}/${file}`; From 4feccb4308984c29838104b3434d91b2b518cb16 Mon Sep 17 00:00:00 2001 From: LukeGus Date: Sat, 10 Oct 2026 17:04:50 -0500 Subject: [PATCH 4/5] fix: bust cached readme icon --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fb4ca4d..8850aa3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@
-Termix Logo +Termix Logo

Termix CLI

From 17ff281e0df26a286591ff8567d57d845b5d633c Mon Sep 17 00:00:00 2001 From: LukeGus Date: Sat, 10 Oct 2026 23:30:20 +0000 Subject: [PATCH 5/5] chore: lint, format, and bump version to 1.1.1 --- package-lock.json | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 4712439..e61bfb0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@termix-ssh/cli", - "version": "1.1.0", + "version": "1.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@termix-ssh/cli", - "version": "1.1.0", + "version": "1.1.1", "license": "Apache-2.0", "dependencies": { "axios": "^1.19.0", diff --git a/package.json b/package.json index ba4ea53..eeeffd0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@termix-ssh/cli", - "version": "1.1.0", + "version": "1.1.1", "description": "Manage your Termix servers from the command line.", "license": "Apache-2.0", "type": "module",