diff --git a/README.md b/README.md index f486aa27..1e88a1f2 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,26 @@ IMPORTANT: If you just installed pi, make sure you've configured at least one mo pi install npm:taskplane ``` +As of pi `0.75.0`, this installs Taskplane into pi's private extension directory (`~/.pi/agent/npm/node_modules/`) rather than the system npm-global root. To make the `taskplane` CLI available on your shell PATH, add pi's bin dir once: + +```bash +# bash / zsh +echo 'export PATH="$HOME/.pi/agent/npm/node_modules/.bin:$PATH"' >> ~/.bashrc # or ~/.zshrc +``` + +```powershell +# PowerShell +[Environment]::SetEnvironmentVariable( + "PATH", + "$HOME\.pi\agent\npm\node_modules\.bin;" + [Environment]::GetEnvironmentVariable("PATH", "User"), + "User" +) +``` + +With Pi's bin dir on PATH, `pi update` keeps Taskplane current automatically — there's no second update command to remember. + +> **Note for users who previously ran `npm install -g taskplane`:** That puts a second copy of Taskplane in your system npm-global, separate from pi's private copy. The two copies drift independently (`pi update` only refreshes the pi-private one), and your shell will resolve `taskplane` to whichever is earlier on PATH. Run `taskplane doctor` — it now detects this duplication and prints a remediation. The fix is `npm uninstall -g taskplane` plus the PATH change above. + ### Option B: Single Project-Local Install ```bash @@ -68,6 +88,8 @@ cd my-project pi install -l npm:taskplane ``` +Project-local installs land in `.pi/npm/node_modules/`. Either invoke via `npx taskplane `, run the binary directly at `.pi/npm/node_modules/.bin/taskplane`, or add that dir to PATH (project-scoped — e.g. via a `.envrc` if you use direnv). + ## Quickstart ### 1. Initialize a project (to scaffold settings) diff --git a/bin/taskplane.mjs b/bin/taskplane.mjs index 0b0c7302..11eeff49 100644 --- a/bin/taskplane.mjs +++ b/bin/taskplane.mjs @@ -2789,6 +2789,36 @@ function cmdDoctor() { ` ${OK} taskplane package installed ${c.dim}(v${pkgVersion}, ${installType})${c.reset}`, ); + // Duplication check: when both the Pi-private and the npm-global copies + // exist with different versions, `pi update` only refreshes the Pi-private + // one and the system-wide CLI silently drifts behind. Surface this clearly + // so operators don't run a stale CLI thinking `pi update` covered it. + // Background: Pi 0.75.0 (2026-05-17) moved user-scoped pi packages from + // npm's global root to `~/.pi/agent/npm/` to avoid system-Node permission + // errors. Users who installed via `npm install -g taskplane` before that + // landed (or who installed via both paths) end up with two on-disk copies. + const duplication = detectDuplicateTaskplaneInstall(); + if (duplication) { + console.log(); + console.log(` ${WARN} taskplane is installed in TWO locations with different versions:`); + for (const loc of duplication.locations) { + console.log( + ` ${c.dim}${loc.label}:${c.reset} v${loc.version} ${c.dim}(${loc.path})${c.reset}`, + ); + } + console.log( + ` ${c.dim}→ ${c.reset}\`pi update\`${c.dim} only refreshes the Pi-private copy.${c.reset}`, + ); + console.log( + ` ${c.dim}→ Recommended fix: drop the npm-global copy and put Pi's bin dir on PATH:${c.reset}`, + ); + console.log(` ${c.cyan} npm uninstall -g taskplane${c.reset}`); + console.log( + ` ${c.cyan} export PATH="$HOME/.pi/agent/npm/node_modules/.bin:$PATH"${c.reset} ${c.dim}# add to ~/.bashrc / ~/.zshrc${c.reset}`, + ); + issues++; + } + if (isWorkspaceMode) { console.log(); if (wsResult.error) { @@ -3332,17 +3362,28 @@ function cmdVersion() { console.log(`\ntaskplane ${c.bold}v${pkgVersion}${c.reset}`); console.log(` Package: ${installType}`); - // Check for project config + // Check for project config. Only render the `version` / `installedAt` + // fields when they're actually present and non-empty — some marker files + // (e.g. taskplane's own source repo, where `.pi/taskplane.json` carries + // only migration history rather than init metadata) would otherwise + // produce the user-visible "vundefined, initialized unknown" placeholders. const projectRoot = process.cwd(); const tpJson = path.join(projectRoot, ".pi", "taskplane.json"); if (fs.existsSync(tpJson)) { try { const info = JSON.parse(fs.readFileSync(tpJson, "utf-8")); - console.log( - ` Config: .pi/taskplane.json (v${info.version}, initialized ${info.installedAt?.slice(0, 10) || "unknown"})`, - ); + const parts = []; + if (typeof info.version === "string" && info.version.length > 0) { + parts.push(`v${info.version}`); + } + if (typeof info.installedAt === "string" && info.installedAt.length > 0) { + parts.push(`initialized ${info.installedAt.slice(0, 10)}`); + } + const suffix = + parts.length > 0 ? ` (${parts.join(", ")})` : ` ${c.dim}(metadata only)${c.reset}`; + console.log(` Config: .pi/taskplane.json${suffix}`); } catch { - console.log(` Config: .pi/taskplane.json (unreadable)`); + console.log(` Config: .pi/taskplane.json ${c.dim}(unreadable)${c.reset}`); } } else { console.log(` Config: ${c.dim}not initialized (run taskplane init)${c.reset}`); @@ -3366,6 +3407,75 @@ function getPackageVersion() { } } +/** + * Detect a duplicate taskplane install: one copy at Pi's private extension + * directory (`~/.pi/agent/npm/node_modules/taskplane/`) and another at the + * system npm-global root, with DIFFERENT versions. Same-version installs + * are harmless and intentionally not flagged (they may drift later but + * aren't a problem in the present tense). + * + * Returns null when there's no detectable duplication, or an object + * describing the two locations + their versions when there is. + * + * Best-effort: silently returns null if `npm root -g` is unreachable or + * either package.json is unreadable, since the rest of `taskplane doctor` + * should never crash on a side check. + * + * Background: Pi 0.75.0 (2026-05-17) moved user-scoped pi packages from + * npm's global root to `~/.pi/agent/npm/` to avoid permission errors with + * system-managed Node installs (Pi changelog: "Fixed user-scoped npm pi + * packages to install under `~/.pi/agent/npm/` instead of npm's global + * package root"). Users who had previously run `npm install -g taskplane` + * (or who continue to do so per older docs) now have two on-disk copies + * that drift independently: `pi update` only refreshes the Pi-private one. + */ +function detectDuplicateTaskplaneInstall() { + const piPrivatePath = path.join(homedir(), ".pi", "agent", "npm", "node_modules", "taskplane"); + + let npmRootGlobal = null; + try { + npmRootGlobal = execSync("npm root -g", { + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"], + }) + .toString() + .trim(); + } catch { + return null; + } + if (!npmRootGlobal) return null; + const npmGlobalPath = path.join(npmRootGlobal, "taskplane"); + + // Skip if Pi-private and npm-global resolve to the same path on disk. + // This can happen on installs where Pi hasn't (yet) migrated to its + // private directory or where the two paths point at the same install. + if (path.resolve(piPrivatePath) === path.resolve(npmGlobalPath)) return null; + + const candidates = [ + { path: piPrivatePath, label: "Pi-private" }, + { path: npmGlobalPath, label: "npm-global" }, + ]; + + const found = []; + for (const candidate of candidates) { + try { + const pkgJsonPath = path.join(candidate.path, "package.json"); + if (!fs.existsSync(pkgJsonPath)) continue; + const pkg = JSON.parse(fs.readFileSync(pkgJsonPath, "utf-8")); + if (typeof pkg.version !== "string" || pkg.version.length === 0) continue; + found.push({ path: candidate.path, label: candidate.label, version: pkg.version }); + } catch { + /* unreadable — skip this candidate */ + } + } + + // Need both copies to be a duplication. Same-version pair is not flagged. + if (found.length < 2) return null; + if (found[0].version === found[1].version) return null; + + return { locations: found }; +} + // ─── dashboard ────────────────────────────────────────────────────────────── function cmdDashboard(args) { diff --git a/docs/tutorials/install.md b/docs/tutorials/install.md index 63c11f9d..6c2818a1 100644 --- a/docs/tutorials/install.md +++ b/docs/tutorials/install.md @@ -22,7 +22,9 @@ Use this if you want Taskplane commands available in every pi session. pi install npm:taskplane ``` -> You can also install the CLI directly with `npm install -g taskplane`, but the recommended path is `pi install npm:taskplane` because it also registers the package for pi extension/skill auto-discovery. +> **Recommended.** This registers the package for pi extension/skill auto-discovery AND keeps Taskplane updateable via `pi update` (a single source of truth). + +> **Avoid `npm install -g taskplane`** unless you have a specific reason. As of pi `0.75.0`, `pi install` puts Taskplane in pi's private extension directory (`~/.pi/agent/npm/node_modules/`). A separate `npm install -g taskplane` creates a **second** on-disk copy in the system npm-global root, and the two drift independently — `pi update` only refreshes the pi-private copy. If you're already in this state, `taskplane doctor` detects it and prints a remediation. ### Option B — Project-local install (recommended for teams) @@ -45,18 +47,38 @@ From the project root: taskplane init ``` -If `taskplane` is not on your PATH (common with project-local installs), run: +If `taskplane` is not on your PATH (common with both global Pi installs and project-local installs as of pi `0.75.0+`), you have three options: + +**1. One-shot via npx** (works anywhere): ```bash npx taskplane init ``` -Or: +**2. Invoke pi's bin shim directly:** ```bash +# Global Pi install +~/.pi/agent/npm/node_modules/.bin/taskplane init + +# Project-local install .pi/npm/node_modules/.bin/taskplane init ``` +**3. Add the appropriate bin dir to PATH** (one-time, recommended for global installs): + +```bash +# bash / zsh — add to ~/.bashrc or ~/.zshrc +export PATH="$HOME/.pi/agent/npm/node_modules/.bin:$PATH" +``` + +```powershell +# PowerShell — add to your $PROFILE +$env:PATH = "$HOME\.pi\agent\npm\node_modules\.bin;" + $env:PATH +``` + +With Pi's bin dir on PATH, `pi update` keeps Taskplane current and you can call `taskplane ` from any shell. + ### Mode Auto-Detection `taskplane init` automatically detects your project layout: