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
5 changes: 5 additions & 0 deletions .changeset/sync-herdr-plugin-on-update.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@leesangb/wt": minor
---

Keep an installed wt Herdr plugin in sync with the wt release selected by `wt update`.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,11 @@ Skip removing quarantine attribute for standalone binary installs:
wt update --no-remove-quarantine
```

If the `wt.herdr` plugin was installed from `leesangb/wt`, `wt update` also
syncs it to the same release tag. Locally linked plugins and plugins from other
repositories are left unchanged. Use `wt update --force` to reinstall an
already matching plugin version.

### List all worktrees

```bash
Expand Down
51 changes: 51 additions & 0 deletions src/app/use-cases/update-installation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ describe("update installation use case", () => {
const delegatedCommands: string[] = [];
const executedPlans: Array<{ args: string[]; displayCommand: string }> = [];
const refreshedBinaries: string[] = [];
const versionedBinaries: string[] = [];
const syncedVersions: string[] = [];

const result = await updateInstallation("0.3.0", {}, {
processInfo: {
Expand All @@ -26,13 +28,21 @@ describe("update installation use case", () => {
},
onBeforeHomebrewUpdate: (command) => delegatedCommands.push(command),
runHomebrewUpdate: (plan) => executedPlans.push(plan),
readInstalledVersion: (binaryPath) => {
versionedBinaries.push(binaryPath);
return "0.4.0";
},
refreshShellWrappers: (binaryPath) => {
refreshedBinaries.push(binaryPath);
return {
refreshedShells: ["zsh"],
warnings: [],
};
},
syncHerdrPlugin: async (version) => {
syncedVersions.push(version);
return { status: "updated", version };
},
});

expect(result).toEqual({
Expand All @@ -42,6 +52,7 @@ describe("update installation use case", () => {
refreshedShells: ["zsh"],
warnings: [],
},
herdrPlugin: { status: "updated", version: "0.4.0" },
});
expect(delegatedCommands).toEqual(["brew upgrade wt"]);
expect(executedPlans).toEqual([
Expand All @@ -51,13 +62,16 @@ describe("update installation use case", () => {
},
]);
expect(refreshedBinaries).toEqual(["/opt/homebrew/bin/wt"]);
expect(versionedBinaries).toEqual(["/opt/homebrew/bin/wt"]);
expect(syncedVersions).toEqual(["0.4.0"]);
});

test("refreshes existing shell wrappers after a standalone binary update", async () => {
const downloads: string[] = [];
const replacements: Array<{ tempPath: string; execPath: string }> = [];
const quarantines: string[] = [];
const refreshedBinaries: string[] = [];
const syncedHerdrPlugins: Array<{ version: string; force: boolean }> = [];

const result = await updateInstallation(
"0.6.0",
Expand Down Expand Up @@ -86,6 +100,10 @@ describe("update installation use case", () => {
warnings: [],
};
},
syncHerdrPlugin: async (version, { force }) => {
syncedHerdrPlugins.push({ version, force });
return { status: "updated", version };
},
}
);

Expand All @@ -100,6 +118,7 @@ describe("update installation use case", () => {
]);
expect(quarantines).toEqual(["/Users/test/.local/bin/wt"]);
expect(refreshedBinaries).toEqual(["/Users/test/.local/bin/wt"]);
expect(syncedHerdrPlugins).toEqual([{ version: "0.6.1", force: false }]);
expect(result).toEqual({
strategy: "standalone",
currentVersion: "0.6.0",
Expand All @@ -110,6 +129,38 @@ describe("update installation use case", () => {
refreshedShells: ["bash", "zsh"],
warnings: [],
},
herdrPlugin: {
status: "updated",
version: "0.6.1",
},
});
});

test("syncs the Herdr plugin when the standalone binary is already current", async () => {
const syncedVersions: string[] = [];

const result = await updateInstallation(
"0.6.1",
{ version: "0.6.1" },
{
arch: "arm64",
platform: "darwin",
processInfo: {
argv0: "wt",
execPath: "/Users/test/.local/bin/wt",
},
syncHerdrPlugin: async (version) => {
syncedVersions.push(version);
return { status: "up-to-date", version };
},
}
);

expect(syncedVersions).toEqual(["0.6.1"]);
expect(result).toMatchObject({
strategy: "standalone",
updated: false,
herdrPlugin: { status: "up-to-date", version: "0.6.1" },
});
});
});
66 changes: 62 additions & 4 deletions src/app/use-cases/update-installation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ import {
type HomebrewUpdatePlan,
} from "../../infra/update/homebrew.js";
import { compareVersions } from "../../infra/update/version.js";
import {
syncInstalledWtHerdrPlugin,
type HerdrPluginSyncResult,
} from "../../infra/herdr/client.js";
import { readInstalledWtVersion } from "../../infra/update/installed-version.js";

const BINARY_NAME = "wt";
const RUNTIME_LAUNCHERS = new Set(["bun", "node"]);
Expand All @@ -40,12 +45,14 @@ export interface StandaloneUpdateInstallationResult {
updated: boolean;
assetName?: string;
shellIntegration?: RefreshShellWrappersResult;
herdrPlugin?: HerdrPluginSyncResult;
}

export interface HomebrewDelegatedUpdateResult {
strategy: "homebrew";
delegatedCommand: string;
shellIntegration: RefreshShellWrappersResult;
herdrPlugin?: HerdrPluginSyncResult;
}

export type UpdateInstallationResult =
Expand All @@ -61,9 +68,14 @@ export interface UpdateInstallationContext {
downloadBinary?: (url: string) => Promise<string>;
onBeforeHomebrewUpdate?: (command: string) => void;
refreshShellWrappers?: (binaryPath: string) => RefreshShellWrappersResult;
readInstalledVersion?: (binaryPath: string) => string;
removeMacosQuarantine?: (execPath: string) => Promise<void>;
replaceCurrentBinary?: (tempPath: string, execPath: string) => void;
runHomebrewUpdate?: (plan: HomebrewUpdatePlan) => void;
syncHerdrPlugin?: (
version: string,
options: { force: boolean }
) => Promise<HerdrPluginSyncResult>;
}

function assertVersionFormat(version: string, label: string): void {
Expand Down Expand Up @@ -132,6 +144,24 @@ function refreshShellWrappers(
)(binaryPath);
}

async function syncHerdrPlugin(
version: string,
options: UpdateInstallationOptions,
context: UpdateInstallationContext
): Promise<HerdrPluginSyncResult | undefined> {
try {
return await (context.syncHerdrPlugin ?? syncInstalledWtHerdrPlugin)(
version,
{ force: options.force === true }
);
} catch (error) {
return {
status: "failed",
warning: error instanceof Error ? error.message : String(error),
};
}
}

async function updateStandaloneInstallation(
currentVersion: string,
options: UpdateInstallationOptions,
Expand Down Expand Up @@ -174,12 +204,18 @@ async function updateStandaloneInstallation(
assertVersionFormat(targetVersion, "release");

if (compareVersions(targetVersion, currentVersion) <= 0 && !options.force) {
const herdrPlugin = await syncHerdrPlugin(
currentVersion,
options,
context
);
return {
strategy: "standalone",
currentVersion,
targetVersion,
updated: false,
assetName,
...(herdrPlugin ? { herdrPlugin } : {}),
};
}

Expand All @@ -196,12 +232,18 @@ async function updateStandaloneInstallation(
assertVersionFormat(targetVersion, "requested");

if (compareVersions(targetVersion, currentVersion) <= 0 && !options.force) {
const herdrPlugin = await syncHerdrPlugin(
currentVersion,
options,
context
);
return {
strategy: "standalone",
currentVersion,
targetVersion,
updated: false,
assetName,
...(herdrPlugin ? { herdrPlugin } : {}),
};
}

Expand All @@ -223,6 +265,7 @@ async function updateStandaloneInstallation(
}

const shellIntegration = refreshShellWrappers(binaryPath, context);
const herdrPlugin = await syncHerdrPlugin(targetVersion, options, context);

return {
strategy: "standalone",
Expand All @@ -231,6 +274,7 @@ async function updateStandaloneInstallation(
updated: true,
assetName,
shellIntegration,
...(herdrPlugin ? { herdrPlugin } : {}),
};
}

Expand All @@ -246,15 +290,29 @@ export async function updateInstallation(

context.onBeforeHomebrewUpdate?.(plan.displayCommand);
(context.runHomebrewUpdate ?? runHomebrewUpdate)(plan);
const shellIntegration = refreshShellWrappers(
resolveHomebrewShellBinaryPath(context.processInfo),
context
);
const binaryPath = resolveHomebrewShellBinaryPath(context.processInfo);
const shellIntegration = refreshShellWrappers(binaryPath, context);
let installedVersion: string | undefined;
let herdrPlugin: HerdrPluginSyncResult | undefined;

try {
installedVersion = (
context.readInstalledVersion ?? readInstalledWtVersion
)(binaryPath);
assertVersionFormat(installedVersion, "installed");
herdrPlugin = await syncHerdrPlugin(installedVersion, options, context);
} catch (error) {
herdrPlugin = {
status: "failed",
warning: `Could not sync the wt Herdr plugin: ${error instanceof Error ? error.message : String(error)}`,
};
}

return {
strategy: "homebrew",
delegatedCommand: plan.displayCommand,
shellIntegration,
...(herdrPlugin ? { herdrPlugin } : {}),
};
}

Expand Down
29 changes: 29 additions & 0 deletions src/commands/update.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,32 @@ function printShellIntegrationRefresh(result: UpdateInstallationResult): void {
}
}

function printHerdrPluginSync(result: UpdateInstallationResult): void {
const plugin = result.herdrPlugin;

if (!plugin) {
return;
}

if (plugin.status === "updated") {
console.log(
chalk.green(`✓ Updated wt Herdr plugin to version ${plugin.version}.`)
);
return;
}

if (plugin.status === "up-to-date") {
console.log(
chalk.dim(`wt Herdr plugin is up to date (${plugin.version}).`)
);
return;
}

if (plugin.warning) {
console.log(chalk.yellow(`Warning: ${plugin.warning}`));
}
}

export async function updateCommand(
options: UpdateInstallationOptions
): Promise<void> {
Expand All @@ -55,6 +81,7 @@ export async function updateCommand(
},
});
printShellIntegrationRefresh(result);
printHerdrPluginSync(result);
return;
}

Expand Down Expand Up @@ -82,11 +109,13 @@ export async function updateCommand(
);
console.log(chalk.dim("Use --force to re-download the current version."));
}
printHerdrPluginSync(result);
return;
}

console.log(chalk.green(`✓ Updated wt to version ${result.targetVersion}`));
printShellIntegrationRefresh(result);
printHerdrPluginSync(result);
console.log(chalk.dim("Run: wt --version to verify."));
});
}
Loading
Loading