Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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: |
Expand Down Expand Up @@ -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."
Expand Down Expand Up @@ -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
Expand Down
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<div align="center">

<img src="https://raw.githubusercontent.com/Termix-SSH/Termix/main/public/icon.svg" width="120" height="120" alt="Termix Logo" />
<img src="https://raw.githubusercontent.com/Termix-SSH/Termix/main/public/icon.svg?v=2" width="120" height="120" alt="Termix Logo" />

<h1>Termix CLI</h1>

Expand Down
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion scripts/generate-release-body.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -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}`;

Expand Down
11 changes: 7 additions & 4 deletions spec/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,9 @@
},
"401": {
"description": "Not signed in."
},
"403": {
"description": "API keys cannot link a desktop."
}
}
}
Expand Down Expand Up @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -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": {
Expand Down
6 changes: 3 additions & 3 deletions spec/plugins/fleets.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": [
{
Expand Down Expand Up @@ -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": [
{
Expand Down Expand Up @@ -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 `(<hostName>/<filename>).` 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 `(<hostName>-<hostId>/<filename>).` Single file only, up to 200 MB per host.",
"tags": ["Fleets"],
"parameters": [
{
Expand Down
9 changes: 8 additions & 1 deletion src/commands/ssh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ const AUTH_STEPS = [
"totp_required",
"totp_retry",
"password_required",
"auth_method_not_available",
"*_auth_required",
];
const MAX_AUTH_ROUNDS = 10;
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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 });
Expand Down
36 changes: 35 additions & 1 deletion test/ssh.test.ts
Original file line number Diff line number Diff line change
@@ -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<typeof import("../src/core/prompt.js")>()),
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 () => {
Expand All @@ -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",
});
});
});
Loading