* feat: AI translations at release, 7 new languages and regional fallback

* fix: proper plural forms for count strings and full translations

* fix: plural forms for selected, hidden and active counts

* fix: synced plugin updates failing on desktop and server-only plugins shown as stopped

* fix: restore rounded corners on app icons and favicon

* feat: desktop app auto updates with automatic, ask first or never modes

* feat: community and custom plugin registries

* feat: plugin guide for AI tools in the SDK with a lint check

* fix: keep the terminal WebSocket open past nginx's 60s default (#1588)

* chore: update changelog

* feat: show plugin download counts only and stop CI builds inflating them

* feat: hide installed plugins in the browse tab by default

* chore: pin bundled plugins to latest releases

* chore: update translations

* fix: focusing a saved password field and saving no longer erases it

* fix: tell desktop users when their 2.8 data needs 2.9 first

* fix: point windows update info at the x64 installer

* fix: keep desktop client tunnel sockets alive behind proxies

* chore: pin bundled plugins to latest releases

* fix: cover client tunnel keepalive in the c2s runtime tests

* fix: typing after opening a host no longer goes to the host search

* fix: accept tab and = separators in SSH config import (#1598)

* chore: update changelog

* chore: drop plugin versions from core changelog

* chore: update translations

* chore: lint, format, and bump version to 26.10.1

---------

Co-authored-by: Darnel Hunter <68697450+innotelinc@users.noreply.github.com>
Co-authored-by: Iain <Iain1970@protonmail.com>
This commit is contained in:
authored and GitHub committed 2026-10-12 11:35:48 -05:00
1 parent 6873bc92bb
commit 62ac931468
174 files changed
+94334 -39995

No files matched your search

@@ -15,6 +15,8 @@ import { PluginLoader } from "../../plugins/loader.js";
const testKey = vi.hoisted(() => ({
privateKey: null as crypto.KeyObject | null,
key: { id: "test", publicKey: "", addedIn: "2.9.0" },
communityPrivateKey: null as crypto.KeyObject | null,
community: { id: "community", publicKey: "", addedIn: "26.10.1" },
}));
vi.mock("../../plugins/trust.js", async (importActual) => {
@@ -23,6 +25,17 @@ vi.mock("../../plugins/trust.js", async (importActual) => {
...actual,
verifyPluginArtifact: (buffer: Buffer, sha256: string, signature: string) =>
actual.verifyPluginArtifact(buffer, sha256, signature, [testKey.key]),
verifyAnyTrusted: (buffer: Buffer, sha256: string, signature: string) => {
const official = actual.verifyPluginArtifact(buffer, sha256, signature, [
testKey.key,
]);
if (official.ok === true) return { ...official, tier: "official" };
const community = actual.verifyPluginArtifact(buffer, sha256, signature, [
testKey.community,
]);
if (community.ok === true) return { ...community, tier: "community" };
return community;
},
};
});
@@ -42,6 +55,12 @@ vi.mock("../../database/repositories/factory.js", () => ({
.export({ format: "der", type: "spki" })
.subarray(12)
.toString("base64");
const community = crypto.generateKeyPairSync("ed25519");
testKey.communityPrivateKey = community.privateKey;
testKey.community.publicKey = community.publicKey
.export({ format: "der", type: "spki" })
.subarray(12)
.toString("base64");
}
const cleanups: Array<() => void> = [];
@@ -68,7 +87,7 @@ function setup() {
async function writeArtifact(
dir: string,
id: string,
sig: "valid" | "wrong-key" | "none",
sig: "valid" | "community" | "wrong-key" | "none",
version = "1.0.0",
): Promise<string> {
const build = tempRoot("termix-build-");
@@ -82,7 +101,9 @@ async function writeArtifact(
const signer =
sig === "valid"
? testKey.privateKey
: crypto.generateKeyPairSync("ed25519").privateKey;
: sig === "community"
? testKey.communityPrivateKey
: crypto.generateKeyPairSync("ed25519").privateKey;
const digest = crypto
.createHash("sha256")
.update(fs.readFileSync(file))
@@ -171,6 +192,27 @@ describe("updating a bundled plugin", () => {
expect(plugin?.source).toBe("bundled");
expect(plugin?.artifact).toBe(file);
expect(plugin?.signedBy).toBe("test");
expect(plugin?.signedTier).toBe("official");
});
it("keeps the bundled copy for a community signed newer archive", async () => {
const { userDir } = setup();
await writeArtifact(userDir, "bundled-one", "community", "2.0.0");
const loader = new PluginLoader();
await loader.loadAll();
expect(loader.get("bundled-one")?.manifest.version).toBe("1.0.0");
});
it("loads a community signed plugin and records its tier", async () => {
const { userDir } = setup();
process.env.TERMIX_REQUIRE_SIGNED_PLUGINS = "true";
await writeArtifact(userDir, "community-one", "community");
const loader = new PluginLoader();
await loader.loadAll();
const plugin = loader.get("community-one");
expect(plugin?.source).toBe("user");
expect(plugin?.signedBy).toBe("community");
expect(plugin?.signedTier).toBe("community");
});
it("keeps the bundled copy for an unsigned newer archive", async () => {