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
36 changes: 17 additions & 19 deletions bun.lock

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

16 changes: 16 additions & 0 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,15 @@ case "$(uname -s)" in
*) echo "Windows is not supported" >&2; exit 1 ;;
esac

tmux_install_hint() {
if [ "$os" = "macos" ]; then
echo "brew install tmux"
return
fi

echo "your package manager (for example: apt install tmux)"
}
Comment on lines +58 to +65
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This new function relies on the $os variable. It's worth noting that the case statement that sets $os on lines 52-56 only handles Darwin and Linux. For other Unix-like systems (e.g., FreeBSD), it will fall back to the default case and exit the script with a 'Windows is not supported' error. This prevents the installer from running at all on those platforms.

While the fix is outside of this diff, it's a significant correctness issue affecting this new functionality. The script should be updated to correctly identify other Unix-like systems to ensure the installer works on all supported platforms.


case "$(uname -m)" in
x86_64|amd64) arch="x64" ;;
arm64|aarch64) arch="arm64" ;;
Expand Down Expand Up @@ -153,5 +162,12 @@ if [[ ":$PATH:" != *":$INSTALL_DIR:"* ]]; then
echo " export PATH=\"$INSTALL_DIR:\$PATH\""
fi

if ! command -v tmux >/dev/null 2>&1; then
echo ""
echo "Note: tmux is not installed."
echo "The default 'loop' command opens a paired tmux workspace and will fail until tmux is installed."
echo "Install tmux with: $(tmux_install_hint)"
fi

echo ""
echo "Installation complete. Run: loop --help"
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@
"loop": "./src/cli.ts"
},
"devDependencies": {
"@biomejs/biome": "^2.4.0",
"@types/node": "^25.3.0",
"bun-types": "^1.3.9",
"@biomejs/biome": "^2.4.9",
"@types/node": "^25.5.0",
"bun-types": "^1.3.11",
"typescript": "^5.9.3",
"ultracite": "^7.2.3"
"ultracite": "^7.3.2"
}
}
78 changes: 64 additions & 14 deletions src/install.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { spawnSync } from "node:child_process";
import {
access,
chmod,
Expand All @@ -18,6 +19,46 @@ const CODEX_ALIAS_NAME = IS_WINDOWS ? "codex-loop.cmd" : "codex-loop";
const CANDIDATE_BINARIES = IS_WINDOWS
? ["loop.exe", "loop"]
: ["loop", "loop.exe"];
const TMUX_NOTE = "Note: tmux is not installed.";
const TMUX_DEFAULT_MODE_NOTE =
"The default 'loop' command opens a paired tmux workspace and will fail until tmux is installed.";
const TMUX_MACOS_HINT = "brew install tmux";
const TMUX_LINUX_HINT = "your package manager (for example: apt install tmux)";

const tmuxInstallHint = (
platform: NodeJS.Platform = process.platform
): string => {
if (platform === "darwin") {
return TMUX_MACOS_HINT;
}
if (platform === "linux") {
return TMUX_LINUX_HINT;
}
return "install tmux";
};
Comment on lines +28 to +38
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This function provides helpful installation hints. To improve support for other Unix-like systems, you could provide the generic Linux hint for platforms like FreeBSD and OpenBSD, instead of the less helpful default. A switch statement could make this cleaner.

Also, consider adding test cases for these new platforms in tests/install.test.ts.

const tmuxInstallHint = (
  platform: NodeJS.Platform = process.platform
): string => {
  switch (platform) {
    case "darwin":
      return TMUX_MACOS_HINT;
    case "linux":
    case "freebsd":
    case "openbsd":
    case "sunos":
      return TMUX_LINUX_HINT;
    default:
      return "install tmux";
  }
};


const tmuxNudgeLines = (
platform: NodeJS.Platform = process.platform
): string[] => [
"",
TMUX_NOTE,
TMUX_DEFAULT_MODE_NOTE,
`Install tmux with: ${tmuxInstallHint(platform)}`,
];

const hasTmuxInstalled = (): boolean => {
const result = spawnSync("tmux", ["-V"], { stdio: "ignore" });
return !result.error && result.status === 0;
};

const logMissingTmuxNudge = (): void => {
if (IS_WINDOWS || hasTmuxInstalled()) {
return;
}
for (const line of tmuxNudgeLines()) {
console.log(line);
}
};

const findBuiltBinary = async (): Promise<string> => {
for (const name of CANDIDATE_BINARIES) {
Expand Down Expand Up @@ -76,23 +117,32 @@ const installBinary = async (): Promise<void> => {

if (IS_WINDOWS) {
await copyFile(source, target);
console.log(`Installed loop -> ${target}`);
await installAliases();
return;
}

try {
await symlink(source, target);
} catch {
await copyFile(source, target);
} else {
try {
await symlink(source, target);
} catch {
await copyFile(source, target);
}
}

console.log(`Installed loop -> ${target}`);
await installAliases();
logMissingTmuxNudge();
};

export const installInternals = {
tmuxInstallHint,
tmuxNudgeLines,
};

const main = async (): Promise<void> => {
await installBinary();
};

installBinary().catch((error: unknown) => {
const message = error instanceof Error ? error.message : String(error);
console.error(`[loop] install failed: ${message}`);
process.exit(1);
});
if (import.meta.main) {
main().catch((error: unknown) => {
const message = error instanceof Error ? error.message : String(error);
console.error(`[loop] install failed: ${message}`);
process.exit(1);
});
}
6 changes: 3 additions & 3 deletions src/loop/panel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -407,9 +407,9 @@ const trimText = (text: string, width: number): string => {
};

const rowId = (row: Row): string =>
row.session !== "-"
? `${row.agent}:${row.session}`
: `${row.agent}:${row.pid}:${row.cwd}`;
row.session === "-"
? `${row.agent}:${row.pid}:${row.cwd}`
: `${row.agent}:${row.session}`;

const pidText = (pid: number): string => (pid > 0 ? String(pid) : "-");

Expand Down
21 changes: 21 additions & 0 deletions tests/install.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { expect, test } from "bun:test";
import { installInternals } from "../src/install";

test("tmux install hint uses brew on macOS", () => {
expect(installInternals.tmuxInstallHint("darwin")).toBe("brew install tmux");
});

test("tmux install hint stays generic on Linux", () => {
expect(installInternals.tmuxInstallHint("linux")).toBe(
"your package manager (for example: apt install tmux)"
);
});

test("tmux nudge explains why bare loop fails without tmux", () => {
expect(installInternals.tmuxNudgeLines("linux")).toEqual([
"",
"Note: tmux is not installed.",
"The default 'loop' command opens a paired tmux workspace and will fail until tmux is installed.",
"Install tmux with: your package manager (for example: apt install tmux)",
]);
});
Loading