diff --git a/packages/plugin-sdk/cli/commands/validate.mjs b/packages/plugin-sdk/cli/commands/validate.mjs index 60591114c..df9fd1ecc 100644 --- a/packages/plugin-sdk/cli/commands/validate.mjs +++ b/packages/plugin-sdk/cli/commands/validate.mjs @@ -2,8 +2,6 @@ import fs from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { readManifest } from "../lib/plugin-dir.mjs"; -import { LEGACY_TABLE_OWNERS as LEGACY_TABLES } from "../../dist/db.js"; -import { collectOwnedIndexes, findUnownedTableWrites } from "../../dist/ddl.js"; /** * The manifest rules live in src/manifest.ts, which the server uses too, so @@ -37,7 +35,9 @@ export async function validate({ cwd }) { } } - problems.push(...validateMigrations(cwd, raw.id ?? path.basename(cwd))); + problems.push( + ...(await validateMigrations(cwd, raw.id ?? path.basename(cwd))), + ); problems.push(...validateNativeDependencies(cwd, raw)); problems.push(...validatePackage(cwd, raw)); @@ -115,7 +115,12 @@ const DIALECTS = ["sqlite", "postgres", "mysql"]; * never ran this command. Here it is a build-time error with a file name * attached, rather than a plugin that fails to activate later. */ -function validateMigrations(cwd, pluginId) { +async function validateMigrations(cwd, pluginId) { + const { LEGACY_TABLE_OWNERS: LEGACY_TABLES } = + await import("../../dist/db.js"); + const { collectOwnedIndexes, findUnownedTableWrites } = + await import("../../dist/ddl.js"); + const problems = []; const legacy = new Set( Object.entries(LEGACY_TABLES) diff --git a/scripts/plugin-cli-bootstrap.test.ts b/scripts/plugin-cli-bootstrap.test.ts new file mode 100644 index 000000000..98cd62916 --- /dev/null +++ b/scripts/plugin-cli-bootstrap.test.ts @@ -0,0 +1,79 @@ +import { afterEach, describe, expect, it } from "vitest"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { spawnSync } from "node:child_process"; + +const roots: string[] = []; +afterEach(() => { + for (const root of roots.splice(0)) + fs.rmSync(root, { recursive: true, force: true }); +}); + +function fixture() { + const root = fs.mkdtempSync(path.join(os.tmpdir(), "termix-cli-bootstrap-")); + roots.push(root); + const repo = path.resolve(import.meta.dirname, ".."); + fs.cpSync( + path.join(repo, "packages/plugin-sdk/cli"), + path.join(root, "packages/plugin-sdk/cli"), + { recursive: true }, + ); + fs.symlinkSync( + path.join(repo, "node_modules"), + path.join(root, "node_modules"), + "dir", + ); + fs.mkdirSync(path.join(root, "scripts")); + fs.copyFileSync( + path.join(repo, "scripts/apply-plugin-patches.cjs"), + path.join(root, "scripts/apply-plugin-patches.cjs"), + ); + const plugin = path.join(root, "plugins/fixture"); + fs.mkdirSync(path.join(plugin, "patches"), { recursive: true }); + fs.writeFileSync( + path.join(plugin, "manifest.json"), + JSON.stringify({ id: "fixture" }), + ); + fs.writeFileSync( + path.join(plugin, "package.json"), + JSON.stringify({ name: "fixture" }), + ); + fs.writeFileSync( + path.join(plugin, "patches/check.cjs"), + 'require("node:fs").writeFileSync("patched", "ok");', + ); + const run = (...args: string[]) => + spawnSync(process.execPath, args, { cwd: plugin, encoding: "utf8" }); + return { + root, + plugin, + run, + cli: path.join(root, "packages/plugin-sdk/cli/index.mjs"), + }; +} + +describe("plugin CLI before the SDK is built", () => { + it("runs the postinstall plugin patches without SDK dist files", () => { + const { root, plugin, run } = fixture(); + expect(fs.existsSync(path.join(root, "packages/plugin-sdk/dist"))).toBe( + false, + ); + const result = run(path.join(root, "scripts/apply-plugin-patches.cjs")); + expect(result.status, result.stderr).toBe(0); + expect(fs.readFileSync(path.join(plugin, "patched"), "utf8")).toBe("ok"); + }); + it("prints help without SDK dist files", () => { + const { cli, run } = fixture(); + const result = run(cli, "--help"); + expect(result.status, result.stderr).toBe(0); + expect(result.stdout).toContain("Usage: termix-plugin"); + }); + it("still requires a built SDK for validation with an actionable error", () => { + const { cli, run } = fixture(); + const result = run(cli, "validate"); + expect(result.status).toBe(1); + expect(result.stderr).toContain("Run: npm run build:sdk"); + expect(result.stderr).not.toContain("ERR_MODULE_NOT_FOUND"); + }); +}); diff --git a/src/ui/sidebar/tree/HostItem/HostItem.tsx b/src/ui/sidebar/tree/HostItem/HostItem.tsx index 7e0b1a5bf..9d4db9d20 100644 --- a/src/ui/sidebar/tree/HostItem/HostItem.tsx +++ b/src/ui/sidebar/tree/HostItem/HostItem.tsx @@ -1167,18 +1167,11 @@ export function HostItem({ )} - {isCompact && - !selectionMode && - !shouldUseClickTray && - !actionsOnly && ( - - {host.ip} - - )} - {isCompact && selectionMode && ( - + {isCompact && ( + {host.ip} )} diff --git a/src/ui/tests/sidebar/tree/HostItem/HostItem.test.tsx b/src/ui/tests/sidebar/tree/HostItem/HostItem.test.tsx index b6f651570..30a4c6b79 100644 --- a/src/ui/tests/sidebar/tree/HostItem/HostItem.test.tsx +++ b/src/ui/tests/sidebar/tree/HostItem/HostItem.test.tsx @@ -187,6 +187,30 @@ describe("HostItem density parity", () => { }, ); + it.each(["hover", "click", "always", "actionsOnly"] as const)( + "keeps the compact host address visible with %s actions", + (trayTrigger) => { + const props = { + host: baseHost, + onOpenTab: noop, + onDelete: noop, + onDuplicate: noop, + density: "compact" as const, + trayTrigger, + }; + const { rerender } = render(); + expect(screen.getByText(baseHost.ip).classList.contains("hidden")).toBe( + false, + ); + rerender(); + expect(screen.getByText(baseHost.ip).classList.contains("hidden")).toBe( + false, + ); + rerender(); + expect(screen.getAllByText(baseHost.ip)).toHaveLength(1); + }, + ); + it("shows tags in both densities when showTags is true", () => { renderHostItem("comfortable"); expect(screen.getByText("prod")).toBeTruthy();