taskplane 0.2.7 → 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.
- package/bin/taskplane.mjs +264 -6
- package/dashboard/server.cjs +50 -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}
|
|
1276
|
-
${c.cyan}doctor${c.reset}
|
|
1277
|
-
${c.cyan}
|
|
1278
|
-
${c.cyan}
|
|
1279
|
-
${c.cyan}
|
|
1280
|
-
${c.cyan}
|
|
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/dashboard/server.cjs
CHANGED
|
@@ -76,11 +76,59 @@ function resolveTaskFolder(task, state) {
|
|
|
76
76
|
const laneNum = task.laneNumber;
|
|
77
77
|
const lane = (state?.lanes || []).find((l) => l.laneNumber === laneNum);
|
|
78
78
|
if (!lane || !lane.worktreePath) return task.taskFolder;
|
|
79
|
+
|
|
80
|
+
// In workspace mode, the worktree is inside a specific repo, not the workspace root.
|
|
81
|
+
// The task folder path needs to be made relative to the repo root (parent of the worktree),
|
|
82
|
+
// not the workspace root. Detect this by finding the repo root from the worktree path.
|
|
79
83
|
const taskFolderAbs = path.resolve(task.taskFolder);
|
|
84
|
+
const worktreeAbs = path.resolve(lane.worktreePath);
|
|
85
|
+
|
|
86
|
+
// Try to find the repo root: walk up from the worktree path looking for which
|
|
87
|
+
// ancestor is a prefix of the task folder. The worktree is at <repoRoot>/.worktrees/<name>
|
|
88
|
+
// or a sibling, so the repo root is typically 2 levels up from a subdirectory worktree.
|
|
89
|
+
// Heuristic: find the longest common ancestor between taskFolder and worktree's repo root.
|
|
80
90
|
const repoRootAbs = path.resolve(REPO_ROOT);
|
|
81
|
-
|
|
91
|
+
|
|
92
|
+
// First try: relative to workspace root (works in repo mode where workspace = repo)
|
|
93
|
+
let rel = path.relative(repoRootAbs, taskFolderAbs);
|
|
82
94
|
if (!rel || rel.startsWith("..") || path.isAbsolute(rel)) return task.taskFolder;
|
|
83
|
-
|
|
95
|
+
|
|
96
|
+
// Check if joining with worktree produces a valid path
|
|
97
|
+
const candidate = path.join(worktreeAbs, rel);
|
|
98
|
+
try {
|
|
99
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
100
|
+
} catch { /* fall through */ }
|
|
101
|
+
|
|
102
|
+
// Second try: the worktree is inside a repo subdirectory of the workspace root.
|
|
103
|
+
// Strip the repo prefix from the task folder path to get the repo-relative path.
|
|
104
|
+
// e.g., taskFolder = "workspace/platform-docs/task-mgmt/DOC-001/"
|
|
105
|
+
// worktree = "workspace/platform-docs/.worktrees/wt-1/"
|
|
106
|
+
// repo-relative = "task-mgmt/DOC-001/"
|
|
107
|
+
// Find the repo by checking which workspace repo path is a prefix of the task folder.
|
|
108
|
+
const repoRoots = [];
|
|
109
|
+
try {
|
|
110
|
+
const stateMode = state.mode;
|
|
111
|
+
if (stateMode === "workspace" && state.repos) {
|
|
112
|
+
for (const r of state.repos) repoRoots.push(path.resolve(r.path));
|
|
113
|
+
}
|
|
114
|
+
} catch { /* no repo info in state */ }
|
|
115
|
+
|
|
116
|
+
// Also try inferring repo root from worktree path pattern:
|
|
117
|
+
// .worktrees/<name> → parent is repo root; sibling worktrees → shared parent
|
|
118
|
+
const worktreeParent = path.dirname(worktreeAbs);
|
|
119
|
+
const worktreeGrandparent = path.dirname(worktreeParent);
|
|
120
|
+
for (const possibleRepoRoot of [worktreeGrandparent, ...repoRoots]) {
|
|
121
|
+
const repoRel = path.relative(possibleRepoRoot, taskFolderAbs);
|
|
122
|
+
if (repoRel && !repoRel.startsWith("..") && !path.isAbsolute(repoRel)) {
|
|
123
|
+
const repoCandidate = path.join(worktreeAbs, repoRel);
|
|
124
|
+
try {
|
|
125
|
+
if (fs.existsSync(repoCandidate)) return repoCandidate;
|
|
126
|
+
} catch { continue; }
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Fallback: return original task folder (might work if not in worktree)
|
|
131
|
+
return task.taskFolder;
|
|
84
132
|
}
|
|
85
133
|
|
|
86
134
|
function parseStatusMd(taskFolder) {
|