taskplane 0.2.8 → 0.2.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/bin/taskplane.mjs +264 -6
  2. package/package.json +1 -1
package/bin/taskplane.mjs CHANGED
@@ -992,6 +992,251 @@ function parseWorkspaceYaml(raw) {
992
992
  return result;
993
993
  }
994
994
 
995
+ // ─── install-tmux ───────────────────────────────────────────────────────────
996
+
997
+ /**
998
+ * Install or upgrade tmux for Git Bash on Windows.
999
+ *
1000
+ * Downloads tmux and libevent packages from the official MSYS2 package
1001
+ * repository, extracts the required binaries, and places them in ~/bin/.
1002
+ *
1003
+ * Requirements:
1004
+ * - Windows with Git Bash (provides tar and msys-2.0.dll runtime)
1005
+ * - Node.js >= 21.7 (native zstd decompression)
1006
+ *
1007
+ * The install target is ~/bin/ because:
1008
+ * - It's user-writable (no admin rights needed)
1009
+ * - Git Bash includes it in PATH by default
1010
+ * - It doesn't conflict with Git's own /usr/bin/
1011
+ */
1012
+
1013
+ const TMUX_PACKAGES = [
1014
+ {
1015
+ name: "tmux",
1016
+ url: "https://mirror.msys2.org/msys/x86_64/tmux-3.6.a-1-x86_64.pkg.tar.zst",
1017
+ version: "3.6a",
1018
+ files: ["usr/bin/tmux.exe"],
1019
+ },
1020
+ {
1021
+ name: "libevent",
1022
+ url: "https://mirror.msys2.org/msys/x86_64/libevent-2.1.12-4-x86_64.pkg.tar.zst",
1023
+ version: "2.1.12",
1024
+ files: ["usr/bin/msys-event-2-1-7.dll", "usr/bin/msys-event_core-2-1-7.dll"],
1025
+ },
1026
+ ];
1027
+
1028
+ const TMUX_INSTALL_DIR_NAME = "bin";
1029
+
1030
+ function getTmuxInstallDir() {
1031
+ return path.join(process.env.HOME || process.env.USERPROFILE || "", TMUX_INSTALL_DIR_NAME);
1032
+ }
1033
+
1034
+ function detectCurrentTmux() {
1035
+ try {
1036
+ const out = execSync("tmux -V", { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"] }).trim();
1037
+ const match = out.match(/tmux\s+([\d.]+\w*)/);
1038
+ const version = match ? match[1] : "unknown";
1039
+
1040
+ // Find where tmux lives
1041
+ let location = "";
1042
+ try {
1043
+ location = execSync("which tmux", { encoding: "utf-8", timeout: 5000, stdio: ["pipe", "pipe", "pipe"], shell: "C:/Program Files/Git/bin/bash.exe" }).trim();
1044
+ } catch { /* ignore */ }
1045
+
1046
+ return { installed: true, version, location };
1047
+ } catch {
1048
+ return { installed: false, version: null, location: null };
1049
+ }
1050
+ }
1051
+
1052
+ async function httpFollowRedirects(url) {
1053
+ const https = await import("node:https");
1054
+ const http = await import("node:http");
1055
+
1056
+ return new Promise((resolve, reject) => {
1057
+ const mod = url.startsWith("https") ? https.default : http.default;
1058
+ mod.get(url, { headers: { "User-Agent": "taskplane" } }, (res) => {
1059
+ if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
1060
+ return httpFollowRedirects(res.headers.location).then(resolve, reject);
1061
+ }
1062
+ if (res.statusCode !== 200) {
1063
+ return reject(new Error(`HTTP ${res.statusCode} for ${url}`));
1064
+ }
1065
+ const chunks = [];
1066
+ res.on("data", (chunk) => chunks.push(chunk));
1067
+ res.on("end", () => resolve(Buffer.concat(chunks)));
1068
+ res.on("error", reject);
1069
+ }).on("error", reject);
1070
+ });
1071
+ }
1072
+
1073
+ function toPosixPath(p) {
1074
+ return p.replace(/\\/g, "/").replace(/^([A-Z]):/i, (_m, d) => "/" + d.toLowerCase());
1075
+ }
1076
+
1077
+ async function cmdInstallTmux(args) {
1078
+ const checkOnly = args.includes("--check");
1079
+ const force = args.includes("--force");
1080
+
1081
+ console.log(`\n${c.bold}Taskplane — tmux installer${c.reset}\n`);
1082
+
1083
+ // ── Platform check ───────────────────────────────────────────
1084
+ if (process.platform !== "win32") {
1085
+ console.log(` ${INFO} This command is for Windows only.`);
1086
+ console.log(` On macOS: ${c.cyan}brew install tmux${c.reset}`);
1087
+ console.log(` On Linux: ${c.cyan}sudo apt install tmux${c.reset} (or your distro's package manager)\n`);
1088
+ return;
1089
+ }
1090
+
1091
+ // ── Node.js version check (need >= 21.7 for zstd) ───────────
1092
+ const [nodeMajor, nodeMinor] = process.versions.node.split(".").map(Number);
1093
+ if (nodeMajor < 21 || (nodeMajor === 21 && nodeMinor < 7)) {
1094
+ die(`Node.js >= 21.7 required for zstd decompression (found ${process.versions.node}).\n Upgrade Node.js: https://nodejs.org/`);
1095
+ }
1096
+
1097
+ // ── Git Bash check ───────────────────────────────────────────
1098
+ const gitBashBin = "C:/Program Files/Git/bin/bash.exe";
1099
+ if (!fs.existsSync(gitBashBin)) {
1100
+ die(`Git Bash not found at ${gitBashBin}.\n Install Git for Windows: https://git-scm.com/downloads`);
1101
+ }
1102
+
1103
+ // ── MSYS2 runtime check ──────────────────────────────────────
1104
+ const msysDll = "C:/Program Files/Git/usr/bin/msys-2.0.dll";
1105
+ if (!fs.existsSync(msysDll)) {
1106
+ die(`MSYS2 runtime (msys-2.0.dll) not found.\n This should ship with Git for Windows. Reinstall Git if missing.`);
1107
+ }
1108
+
1109
+ // ── Current tmux status ──────────────────────────────────────
1110
+ const current = detectCurrentTmux();
1111
+ if (current.installed) {
1112
+ console.log(` ${OK} tmux ${c.bold}${current.version}${c.reset} found`);
1113
+ if (current.location) {
1114
+ console.log(` Location: ${c.dim}${current.location}${c.reset}`);
1115
+ }
1116
+ } else {
1117
+ console.log(` ${FAIL} tmux not found`);
1118
+ }
1119
+
1120
+ const targetVersion = TMUX_PACKAGES[0].version;
1121
+ console.log(` ${INFO} Available version: ${c.bold}${targetVersion}${c.reset} (MSYS2 package)\n`);
1122
+
1123
+ if (checkOnly) return;
1124
+
1125
+ // ── Skip if already up to date ───────────────────────────────
1126
+ if (current.installed && current.version === targetVersion && !force) {
1127
+ console.log(` ${OK} Already up to date. Use ${c.cyan}--force${c.reset} to reinstall.\n`);
1128
+ return;
1129
+ }
1130
+
1131
+ // ── Download and extract ─────────────────────────────────────
1132
+ const zlib = await import("node:zlib");
1133
+ const os = await import("node:os");
1134
+ const tmpDir = os.default.tmpdir();
1135
+ const extractDir = path.join(tmpDir, "taskplane-tmux-install");
1136
+
1137
+ // Clean previous extract
1138
+ fs.rmSync(extractDir, { recursive: true, force: true });
1139
+ fs.mkdirSync(extractDir, { recursive: true });
1140
+
1141
+ for (const pkg of TMUX_PACKAGES) {
1142
+ process.stdout.write(` ⏳ Downloading ${pkg.name} (v${pkg.version})...`);
1143
+ let buf;
1144
+ try {
1145
+ buf = await httpFollowRedirects(pkg.url);
1146
+ } catch (err) {
1147
+ console.log(` ${c.red}failed${c.reset}`);
1148
+ die(`Download failed: ${err.message}\n URL: ${pkg.url}`);
1149
+ }
1150
+ console.log(` ${c.green}${Math.round(buf.length / 1024)}KB${c.reset}`);
1151
+
1152
+ // Decompress zstd → tar
1153
+ let tar;
1154
+ try {
1155
+ tar = zlib.default.zstdDecompressSync(buf);
1156
+ } catch (err) {
1157
+ die(`zstd decompression failed for ${pkg.name}: ${err.message}`);
1158
+ }
1159
+ const tarPath = path.join(tmpDir, `${pkg.name}.pkg.tar`);
1160
+ fs.writeFileSync(tarPath, tar);
1161
+
1162
+ // Extract needed files
1163
+ const posixTar = toPosixPath(tarPath);
1164
+ const posixExtract = toPosixPath(extractDir);
1165
+ for (const f of pkg.files) {
1166
+ try {
1167
+ execSync(`tar xf "${posixTar}" -C "${posixExtract}" ${f}`, {
1168
+ shell: gitBashBin,
1169
+ timeout: 10000,
1170
+ stdio: ["pipe", "pipe", "pipe"],
1171
+ });
1172
+ } catch (err) {
1173
+ die(`Failed to extract ${f} from ${pkg.name}: ${err.message}`);
1174
+ }
1175
+ }
1176
+
1177
+ // Clean up tar
1178
+ try { fs.unlinkSync(tarPath); } catch { /* best effort */ }
1179
+ }
1180
+
1181
+ // ── Install to ~/bin/ ────────────────────────────────────────
1182
+ const installDir = getTmuxInstallDir();
1183
+ fs.mkdirSync(installDir, { recursive: true });
1184
+
1185
+ const extractBinDir = path.join(extractDir, "usr", "bin");
1186
+ const installedFiles = [];
1187
+ const lockedFiles = [];
1188
+ for (const f of fs.readdirSync(extractBinDir)) {
1189
+ const src = path.join(extractBinDir, f);
1190
+ const dest = path.join(installDir, f);
1191
+ try {
1192
+ fs.copyFileSync(src, dest);
1193
+ installedFiles.push(f);
1194
+ } catch (err) {
1195
+ if (err.code === "EBUSY" || err.code === "EPERM") {
1196
+ // File is locked (e.g., DLL in use by running tmux session).
1197
+ // Try rename-then-copy: rename old file, copy new, delete old.
1198
+ const backup = dest + ".old";
1199
+ try {
1200
+ if (fs.existsSync(backup)) fs.unlinkSync(backup);
1201
+ fs.renameSync(dest, backup);
1202
+ fs.copyFileSync(src, dest);
1203
+ try { fs.unlinkSync(backup); } catch { /* clean up later */ }
1204
+ installedFiles.push(f);
1205
+ } catch {
1206
+ lockedFiles.push(f);
1207
+ }
1208
+ } else {
1209
+ throw err;
1210
+ }
1211
+ }
1212
+ }
1213
+
1214
+ // Clean up extract dir
1215
+ fs.rmSync(extractDir, { recursive: true, force: true });
1216
+
1217
+ // ── Verify ───────────────────────────────────────────────────
1218
+ console.log("");
1219
+ if (lockedFiles.length > 0) {
1220
+ console.log(` ${WARN} Some files are locked (tmux may be running): ${lockedFiles.join(", ")}`);
1221
+ console.log(` Close all tmux sessions and re-run ${c.cyan}taskplane install-tmux --force${c.reset}`);
1222
+ if (installedFiles.length > 0) {
1223
+ console.log(` Updated: ${installedFiles.join(", ")}`);
1224
+ }
1225
+ } else {
1226
+ const verify = detectCurrentTmux();
1227
+ if (verify.installed) {
1228
+ console.log(` ${OK} tmux ${c.bold}${verify.version}${c.reset} installed successfully`);
1229
+ console.log(` Location: ${c.dim}${installDir}${c.reset}`);
1230
+ console.log(` Files: ${installedFiles.join(", ")}`);
1231
+ } else {
1232
+ console.log(` ${WARN} Files installed to ${installDir} but tmux not found on PATH.`);
1233
+ console.log(` Ensure ${c.cyan}~/bin${c.reset} is in your PATH. In Git Bash, add to ~/.bashrc:`);
1234
+ console.log(` ${c.dim}export PATH="$HOME/bin:$PATH"${c.reset}`);
1235
+ }
1236
+ }
1237
+ console.log("");
1238
+ }
1239
+
995
1240
  // ─── doctor ─────────────────────────────────────────────────────────────────
996
1241
 
997
1242
  function cmdDoctor() {
@@ -1026,6 +1271,9 @@ function cmdDoctor() {
1026
1271
  console.log(
1027
1272
  ` ${hasTmux ? OK : `${WARN}`} tmux installed${hasTmux ? ` ${c.dim}(${getVersion("tmux", "-V")})${c.reset}` : ` ${c.dim}(optional — needed for spawn_mode: tmux)${c.reset}`}`
1028
1273
  );
1274
+ if (!hasTmux && process.platform === "win32") {
1275
+ console.log(` ${c.dim}→ Run ${c.cyan}taskplane install-tmux${c.dim} to install${c.reset}`);
1276
+ }
1029
1277
 
1030
1278
  // Check package installation
1031
1279
  const pkgJson = path.join(PACKAGE_ROOT, "package.json");
@@ -1272,12 +1520,13 @@ ${c.bold}Usage:${c.reset}
1272
1520
  taskplane <command> [options]
1273
1521
 
1274
1522
  ${c.bold}Commands:${c.reset}
1275
- ${c.cyan}init${c.reset} Scaffold Taskplane config in the current project
1276
- ${c.cyan}doctor${c.reset} Validate installation and project configuration
1277
- ${c.cyan}version${c.reset} Show version information
1278
- ${c.cyan}dashboard${c.reset} Launch the web-based orchestrator dashboard
1279
- ${c.cyan}uninstall${c.reset} Remove Taskplane project files and/or package install
1280
- ${c.cyan}help${c.reset} Show this help message
1523
+ ${c.cyan}init${c.reset} Scaffold Taskplane config in the current project
1524
+ ${c.cyan}doctor${c.reset} Validate installation and project configuration
1525
+ ${c.cyan}install-tmux${c.reset} Install or upgrade tmux for Git Bash (Windows)
1526
+ ${c.cyan}version${c.reset} Show version information
1527
+ ${c.cyan}dashboard${c.reset} Launch the web-based orchestrator dashboard
1528
+ ${c.cyan}uninstall${c.reset} Remove Taskplane project files and/or package install
1529
+ ${c.cyan}help${c.reset} Show this help message
1281
1530
 
1282
1531
  ${c.bold}Init options:${c.reset}
1283
1532
  --preset <name> Use a preset: minimal, full, runner-only
@@ -1301,7 +1550,13 @@ ${c.bold}Uninstall options:${c.reset}
1301
1550
  --remove-tasks Also remove task area directories from task-runner.yaml
1302
1551
  --all Equivalent to --package + --remove-tasks
1303
1552
 
1553
+ ${c.bold}Install-tmux options:${c.reset}
1554
+ --check Check tmux status without installing
1555
+ --force Reinstall even if tmux is already present
1556
+
1304
1557
  ${c.bold}Examples:${c.reset}
1558
+ taskplane install-tmux # Install or upgrade tmux
1559
+ taskplane install-tmux --check # Check tmux status only
1305
1560
  taskplane init # Interactive project setup
1306
1561
  taskplane init --preset full # Quick setup with defaults
1307
1562
  taskplane init --preset full --tasks-root docs/task-management
@@ -1333,6 +1588,9 @@ switch (command) {
1333
1588
  case "doctor":
1334
1589
  cmdDoctor();
1335
1590
  break;
1591
+ case "install-tmux":
1592
+ await cmdInstallTmux(args);
1593
+ break;
1336
1594
  case "version":
1337
1595
  case "--version":
1338
1596
  case "-v":
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "taskplane",
3
- "version": "0.2.8",
3
+ "version": "0.2.9",
4
4
  "description": "AI agent orchestration for pi — parallel task execution with checkpoint discipline",
5
5
  "keywords": [
6
6
  "pi-package",