UNPKG

@simon_he/pi

Version:

Project-aware CLI that detects npm, pnpm, yarn, bun, Go, Rust and Python projects, then routes installs, scripts, builds and workspace commands automatically.

1,385 lines (1,374 loc) 75.1 kB
import { createRequire } from "node:module"; import path from "node:path"; import process from "node:process"; import { isFile } from "lazy-js-utils"; import { getPkg, getPkgTool, jsShell, useNodeWorker } from "lazy-js-utils/node"; import color from "picocolors"; import readline from "node:readline"; import { spawnSync } from "node:child_process"; import { log } from "node:console"; import fs from "node:fs/promises"; import os from "node:os"; import fs$1 from "node:fs"; import { fileURLToPath } from "node:url"; //#region package.json var version = "0.2.19"; //#endregion //#region src/tty.ts const isZh$6 = process.env.PI_Lang === "zh"; function isInteractive() { return Boolean(process.stdin.isTTY && process.stdout.isTTY); } function stripAnsi(input) { let output = ""; for (let i = 0; i < input.length; i++) { const ch = input[i]; if (ch === "\x1B" && input[i + 1] === "[") { let j = i + 2; while (j < input.length && /[0-9;]/.test(input[j])) j++; if (input[j] === "m") { i = j; continue; } } output += ch; } return output; } function charWidth(ch) { return (ch.codePointAt(0) ?? 0) >= 4352 ? 2 : 1; } function stringWidth(input) { const clean = stripAnsi(input); let width = 0; for (const ch of clean) width += charWidth(ch); return width; } function rowCount(input, columns) { const width = stringWidth(input); return Math.max(1, Math.ceil(width / columns)); } function fuzzyScore(option, query) { const optionLower = option.toLowerCase(); const queryLower = query.toLowerCase(); let score = 0; let lastIndex = -1; for (const ch of queryLower) { const idx = optionLower.indexOf(ch, lastIndex + 1); if (idx === -1) return null; score += idx === lastIndex + 1 ? 5 : 1; score -= idx; lastIndex = idx; } return score; } function getMatchIndices(option, query) { if (!query) return []; const optionLower = option.toLowerCase(); const queryLower = query.toLowerCase(); const indices = []; let lastIndex = -1; for (const ch of queryLower) { const idx = optionLower.indexOf(ch, lastIndex + 1); if (idx === -1) return []; indices.push(idx); lastIndex = idx; } return indices; } function highlightMatchTruncated(option, query, active, maxWidth = Number.POSITIVE_INFINITY) { if (maxWidth <= 0) return ""; const matchSet = query ? new Set(getMatchIndices(option, query)) : null; let output = ""; let width = 0; let truncated = false; const ellipsis = "…"; const ellipsisWidth = charWidth(ellipsis); const limit = Number.isFinite(maxWidth) ? Math.max(0, maxWidth - ellipsisWidth) : maxWidth; for (let i = 0; i < option.length; i++) { const ch = option[i]; const w = charWidth(ch); if (Number.isFinite(limit) && width + w > limit) { truncated = true; break; } if (matchSet && matchSet.has(i)) output += color.bold(color.yellow(ch)); else output += active ? color.cyan(ch) : ch; width += w; } if (truncated) { if (maxWidth <= ellipsisWidth) return color.dim(ellipsis); output += color.dim(ellipsis); } return output; } function filterOptions(options, query) { if (!query) return options; const ranked = options.map((option, index) => { const score = fuzzyScore(option, query); return score === null ? null : { option, score, index }; }).filter(Boolean); ranked.sort((a, b) => { if (b.score !== a.score) return b.score - a.score; return a.index - b.index; }); return ranked.map((item) => item.option); } function getVisibleWindow(total, cursor, limit) { if (total <= limit) return { start: 0, end: total }; let start = cursor - Math.floor(limit / 2); let end = start + limit; if (start < 0) { start = 0; end = limit; } if (end > total) { end = total; start = Math.max(0, end - limit); } return { start, end }; } async function runSelect(options, config) { if (!isInteractive()) return null; if (options.length === 0) return null; const stdin = process.stdin; const stdout = process.stdout; let columns = stdout.columns || 80; let rows = stdout.rows || 24; const updateDimensions = () => { columns = stdout.columns || 80; rows = stdout.rows || 24; }; updateDimensions(); const requestCursorPosition = async () => { return await new Promise((resolve) => { let buffer = ""; let timer; const tryParseCursorPosition = (input) => { const start = input.lastIndexOf("\x1B["); if (start === -1) return null; let i = start + 2; let rowStr = ""; while (i < input.length && input[i] >= "0" && input[i] <= "9") rowStr += input[i++]; if (!rowStr || input[i] !== ";") return null; i++; let colStr = ""; while (i < input.length && input[i] >= "0" && input[i] <= "9") colStr += input[i++]; if (!colStr || input[i] !== "R") return null; return { row: Number(rowStr), col: Number(colStr) }; }; function onData(chunk) { buffer += chunk.toString("utf8"); const parsed = tryParseCursorPosition(buffer); if (parsed) { cleanup(); resolve(parsed); } } function cleanup() { if (timer) clearTimeout(timer); stdin.off("data", onData); } timer = setTimeout(() => { cleanup(); resolve(null); }, 80); stdin.on("data", onData); stdout.write("\x1B[6n"); }); }; readline.emitKeypressEvents(stdin, { escapeCodeTimeout: 50 }); if (stdin.isTTY) stdin.setRawMode(true); stdout.write("\x1B[?25l"); let anchor = await requestCursorPosition(); if (anchor) { const prompt = "> "; const header = `? ${config.placeholder}`; const hintLine = `${config.mode === "multiple" ? isZh$6 ? "↑/↓ 选择,空格标记,Tab 补全,/ 搜索,回车确认,Esc 取消" : "Use ↑/↓ to move, Space to toggle, Tab to complete, / to search, Enter to confirm, Esc to cancel" : isZh$6 ? "↑/↓ 选择,Tab 补全,/ 搜索,回车确认,Esc 取消" : "Use ↑/↓ to move, Tab to complete, / to search, Enter to confirm, Esc to cancel"} (1/${options.length})`; const headerRows = rowCount(header, columns); const inputRows = rowCount(`${prompt}`, columns); const hintRows = rowCount(hintLine, columns); const minNeeded = headerRows + inputRows + hintRows + 10 + 2; if (rows >= minNeeded) { const maxAnchorRow = Math.max(1, rows - minNeeded + 1); if (anchor.row > maxAnchorRow) anchor = { ...anchor, row: maxAnchorRow, col: 1 }; } else anchor = { ...anchor, row: 1, col: 1 }; } let query = ""; let inputCursor = 0; let filtered = options; let cursor = 0; const selected = /* @__PURE__ */ new Set(); let searchMode = false; const updateFiltered = () => { filtered = filterOptions(options, query); if (filtered.length === 0) cursor = 0; else if (cursor >= filtered.length) cursor = filtered.length - 1; }; const render = () => { updateFiltered(); if (query.length > 0) searchMode = true; const prompt = searchMode ? "/ " : "> "; const header = `? ${config.placeholder}`; const inputLine = `${prompt}${query}`; const hintLine = (() => { const position = ` (${Math.min(cursor + 1, filtered.length)}/${filtered.length})`; if (config.mode === "multiple") { if (isZh$6) return color.dim("↑/↓ 选择,") + color.bold(color.cyan("空格")) + color.dim(" 标记,Tab 补全,/ 搜索,回车确认,Esc 取消") + color.dim(position); return color.dim("Use ↑/↓ to move, ") + color.bold(color.cyan("Space")) + color.dim(" to toggle, Tab to complete, / to search, Enter to confirm, Esc to cancel") + color.dim(position); } const hint = isZh$6 ? "↑/↓ 选择,Tab 补全,/ 搜索,回车确认,Esc 取消" : "Use ↑/↓ to move, Tab to complete, / to search, Enter to confirm, Esc to cancel"; return color.dim(`${hint}${position}`); })(); const headerRows = rowCount(header, columns); const inputRows = rowCount(inputLine, columns); const hintRows = rowCount(stripAnsi(hintLine), columns); const availableBelow = anchor ? Math.max(1, rows - anchor.row + 1) : rows; const optionAreaRows = Math.max(1, availableBelow - headerRows - inputRows - hintRows); const needsScroll = filtered.length > optionAreaRows; const minVisibleTarget = 10; const ellipsisReserve = needsScroll ? 2 : 0; const maxVisibleRaw = needsScroll ? Math.max(minVisibleTarget, optionAreaRows - ellipsisReserve) : filtered.length; const maxVisible = Math.max(1, Math.min(maxVisibleRaw, optionAreaRows, filtered.length)); const lines = [header, inputLine]; if (filtered.length === 0) lines.push(color.dim(isZh$6 ? "没有匹配项" : "No matches")); else { const window = getVisibleWindow(filtered.length, cursor, maxVisible); const visible = filtered.slice(window.start, window.end); if (window.start > 0) lines.push(color.dim("…")); visible.forEach((option, index) => { const active = window.start + index === cursor; const picked = selected.has(option); const prefix = config.mode === "multiple" ? picked ? "[x] " : "[ ] " : ""; const indicator = active ? ">" : " "; const prefixPlain = `${indicator} ${prefix}`; const optionWidth = Math.max(0, columns - stringWidth(prefixPlain)); const renderedOption = highlightMatchTruncated(option, query, active, optionWidth); const renderedPrefix = active ? color.cyan(prefix) : prefix; const content = `${active ? color.cyan(indicator) : indicator} ${renderedPrefix}${renderedOption}`; lines.push(content); }); if (window.end < filtered.length) lines.push(color.dim("…")); } lines.push(hintLine); if (anchor) stdout.write(`\x1B[${anchor.row};1H`); else readline.cursorTo(stdout, 0); readline.clearScreenDown(stdout); stdout.write(lines.join("\n")); const promptWidth = stringWidth(prompt); const beforeWidth = stringWidth(query.slice(0, inputCursor)); const inputRowOffset = Math.floor((promptWidth + beforeWidth) / columns); const inputCol = (promptWidth + beforeWidth) % columns; const targetRowOffset = headerRows + inputRowOffset; if (anchor) stdout.write(`\x1B[${anchor.row + targetRowOffset};${inputCol + 1}H`); else readline.cursorTo(stdout, inputCol); }; return new Promise((resolve) => { let onKeypress; let onResize; const done = (value) => { if (stdin.isTTY) stdin.setRawMode(false); stdin.off("keypress", onKeypress); process.off("SIGWINCH", onResize); stdout.write("\x1B[?25h"); if (anchor) stdout.write(`\x1B[${anchor.row};1H`); else readline.cursorTo(stdout, 0); readline.clearScreenDown(stdout); stdout.write("\n"); resolve(value); }; const confirmSelection = () => { if (filtered.length === 0) return done(null); if (config.mode === "multiple") { const picked = options.filter((option) => selected.has(option)); if (picked.length > 0) return done(picked); return done([filtered[cursor]]); } return done(filtered[cursor]); }; const cancelSelection = () => done(null); onKeypress = (input, key) => { if (key?.ctrl && key.name === "c") return cancelSelection(); if (key?.name === "escape") { if (query.length > 0) { query = ""; inputCursor = 0; return render(); } return cancelSelection(); } if (key?.name === "return") return confirmSelection(); if (key?.name === "up") { if (filtered.length > 0) cursor = (cursor - 1 + filtered.length) % filtered.length; return render(); } if (key?.name === "down") { if (filtered.length > 0) cursor = (cursor + 1) % filtered.length; return render(); } if (config.mode === "multiple" && key?.name === "space") { const option = filtered[cursor]; if (option) if (selected.has(option)) selected.delete(option); else selected.add(option); return render(); } if (key?.name === "tab") { if (filtered.length > 0) { query = filtered[cursor] || filtered[0]; inputCursor = query.length; searchMode = true; return render(); } return; } if (input === "/" && !key?.ctrl && !key?.meta) { if (!searchMode) { searchMode = true; query = ""; inputCursor = 0; return render(); } if (searchMode && query.length === 0) { searchMode = false; return render(); } } if (key?.name === "left") { if (inputCursor > 0) inputCursor -= 1; return render(); } if (key?.name === "right") { if (inputCursor < query.length) inputCursor += 1; return render(); } if (key?.name === "home" || key?.ctrl && key?.name === "a") { inputCursor = 0; return render(); } if (key?.name === "end" || key?.ctrl && key?.name === "e") { inputCursor = query.length; return render(); } if (key?.name === "backspace") { if (query) { query = query.slice(0, Math.max(0, inputCursor - 1)) + query.slice(inputCursor); inputCursor = Math.max(0, inputCursor - 1); return render(); } return; } if (key?.name === "delete") { if (query && inputCursor < query.length) { query = query.slice(0, inputCursor) + query.slice(inputCursor + 1); return render(); } return; } if (key?.ctrl && key?.name === "u") { query = ""; inputCursor = 0; return render(); } if (input && !key?.ctrl && !key?.meta && input.length === 1) { if (config.mode !== "multiple" || input !== " ") { query = query.slice(0, inputCursor) + input + query.slice(inputCursor); inputCursor += input.length; searchMode = true; } return render(); } }; onResize = () => { updateDimensions(); render(); }; process.on("SIGWINCH", onResize); stdin.on("keypress", onKeypress); render(); }); } async function ttySelect(options, placeholder) { const result = await runSelect(options, { placeholder, mode: "single" }); return typeof result === "string" ? result : null; } async function ttyMultiSelect(options, placeholder) { const result = await runSelect(options, { placeholder, mode: "multiple" }); return Array.isArray(result) ? result : null; } function renderBox(lines, options = {}) { const width = options.width ?? Math.max(...lines.map((line) => line.length), 0); const paddingX = options.paddingX ?? 2; const paddingY = options.paddingY ?? 1; const marginX = options.marginX ?? 0; const marginY = options.marginY ?? 0; const align = options.align ?? "left"; const innerWidth = width + paddingX * 2; const margin = " ".repeat(marginX); const top = `${margin}+${"-".repeat(innerWidth)}+`; const bottom = top; const emptyLine = `${margin}|${" ".repeat(innerWidth)}|`; const alignedLines = lines.map((line) => { const space = Math.max(0, width - line.length); const leftPad = align === "center" ? Math.floor(space / 2) : 0; const rightPad = space - leftPad; return `${margin}|${" ".repeat(paddingX + leftPad)}${line}${" ".repeat(paddingX + rightPad)}|`; }); const output = []; for (let i = 0; i < marginY; i++) output.push(""); output.push(top); for (let i = 0; i < paddingY; i++) output.push(emptyLine); output.push(...alignedLines); for (let i = 0; i < paddingY; i++) output.push(emptyLine); output.push(bottom); for (let i = 0; i < marginY; i++) output.push(""); return output.join("\n"); } //#endregion //#region src/help.ts const isZh$5 = process.env.PI_Lang === "zh"; async function help(argv) { const arg = argv[0]; if (arg === "-v" || arg === "--version") { const message = isZh$5 ? [ `pi 版本: ${version}`, "请为我的努力点一个行 🌟", "谢谢 🤟" ] : [ `pi version: ${version}`, "Please give me a 🌟 for my efforts", "Thank you 🤟" ]; console.log(renderBox(message, { align: "center", width: 50, marginX: 2, marginY: 1, paddingX: 4, paddingY: 2 })); process.exit(0); } else if (arg === "-h" || arg === "--help") { console.log(renderBox([ "PI Commands:", "~ pi: install or update with the current project package manager", "~ pi --choose-tool: choose package manager for this workspace", "~ pi --choose-tool bun: choose the tool directly", "~ pi --forget-tool: clear saved package manager choice", "~ pi --show-tool: show current workspace package manager", "~ pi --show-tool --json: show current tool as JSON", "~ pi --list-tools: list detected package-manager candidates", "~ pi --list-tools --json: list candidates as JSON", "~ pix: npx package", "~ pui: uninstall package", "~ prun: run package script or language entry file", "~ prun --doctor: show shell/history diagnostics", "~ pinit: package init", "~ pbuild: go build | cargo build", "~ pfind: find and run workspace scripts", "~ pa: deprecated alias; delegates to `na` when installed", "~ pu: deprecated alias; delegates to `nu` when installed", "~ pci: compatibility alias of `pi`; prefer `pi`", "~ pci --choose-tool: re-pick tool before install", "~ pci --choose-tool bun: choose the tool directly", "~ pci --forget-tool: clear saved tool before install", "~ pci --show-tool: show current tool before install", "~ pci --show-tool --json: show current tool as JSON", "~ pci --list-tools: list detected candidates", "~ pci --list-tools --json: list candidates as JSON", "~ pil: package latest install", "~ pil --choose-tool: re-pick tool before latest install", "~ pil --choose-tool bun: choose the tool directly", "~ pil --forget-tool: clear saved tool before latest install", "~ pil --show-tool: show current tool before latest install", "~ pil --show-tool --json: show current tool as JSON", "~ pil --list-tools: list detected candidates", "~ pil --list-tools --json: list candidates as JSON" ], { align: "left", width: 76, marginX: 2, marginY: 1, paddingX: 1, paddingY: 1 })); process.exit(0); } } //#endregion //#region src/pa.ts function hasCommand$1(command) { return (process.platform === "win32" ? spawnSync("where", [command], { stdio: "ignore" }) : spawnSync("sh", ["-c", `command -v ${command}`], { stdio: "ignore" })).status === 0; } function pa(params = "") { console.warn(color.yellow("[pi] `pa` is deprecated and only kept as a compatibility bridge to `na`. Install/use `na` directly if you still need that workflow.")); if (!hasCommand$1("na")) { console.error(color.red("[pi] `pa` delegates to `na`, but `na` is not installed. Install the package that provides `na`, or stop using the deprecated `pa` alias.")); process.exitCode = 1; return; } return jsShell(`na${params ? ` ${params}` : ""}`, "inherit"); } //#endregion //#region src/detectNode.ts async function detectNode() { try { await getPkg(); } catch { const cwd = process.cwd(); console.log(color.red(`当前目录: ${cwd} 没有package.json文件`)); process.exit(1); } } //#endregion //#region src/pkgManager.ts const resolvedToolCache = /* @__PURE__ */ new Map(); const toolIndicatorMap = { pnpm: ["pnpm-workspace.yaml", "pnpm-lock.yaml"], yarn: ["yarn.lock", ".yarnrc.yml"], bun: ["bun.lock", "bun.lockb"], npm: ["package-lock.json", "npm-shrinkwrap.json"] }; const isZh$4 = process.env.PI_Lang === "zh"; const supportedPkgTools = Object.keys(toolIndicatorMap); function isEnabled(value) { if (!value) return false; const normalized = value.toLowerCase(); return normalized === "1" || normalized === "true" || normalized === "yes"; } function normalizeDir$1(dir) { return path.resolve(dir); } function isSameOrInsideDir(base, target) { const relative = path.relative(base, target); return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative); } function findUpSync$1(startDir, predicate) { let current = normalizeDir$1(startDir); while (true) { if (predicate(current)) return current; const parent = path.dirname(current); if (parent === current) return null; current = parent; } } function findToolCandidate(cwd, tool) { const indicators = toolIndicatorMap[tool]; if (!indicators?.length) return null; const root = findUpSync$1(cwd, (dir) => indicators.some((indicator) => isFile(path.join(dir, indicator)))); if (!root) return null; const foundIndicators = indicators.filter((indicator) => isFile(path.join(root, indicator))); return { tool, root, indicators: foundIndicators.length ? foundIndicators : indicators.slice(0, 1) }; } function getToolCandidates(cwd) { return Object.keys(toolIndicatorMap).map((tool) => findToolCandidate(cwd, tool)).filter(Boolean); } function getPreferenceWorkspaceKey(cwd, candidates) { const roots = [...new Set(candidates.map((candidate) => normalizeDir$1(candidate.root)))]; if (roots.length === 1) return roots[0]; return normalizeDir$1(cwd); } function findStoredWorkspaceKey(cwd, preferences) { return Object.keys(preferences.workspaces).filter((workspaceKey) => isSameOrInsideDir(workspaceKey, cwd)).sort((a, b) => b.length - a.length)[0] || null; } function resolveWorkspaceKey(cwd, candidates, preferences) { if (candidates.length > 0) return getPreferenceWorkspaceKey(cwd, candidates); return findStoredWorkspaceKey(cwd, preferences) || normalizeDir$1(cwd); } function getConfigHome() { const home = process.env.HOME || os.homedir(); if (process.platform === "win32") return process.env.APPDATA || path.join(home, "AppData", "Roaming"); return process.env.XDG_CONFIG_HOME || path.join(home, ".config"); } function getWorkspaceToolPreferencePath() { return path.join(getConfigHome(), "pi", "workspace-tools.json"); } async function readWorkspaceToolPreferences() { const configPath = getWorkspaceToolPreferencePath(); try { const raw = await fs.readFile(configPath, "utf8"); const parsed = JSON.parse(raw); return { version: 1, workspaces: parsed.workspaces && typeof parsed.workspaces === "object" ? parsed.workspaces : {} }; } catch { return { version: 1, workspaces: {} }; } } async function writeWorkspaceToolPreference(workspaceKey, tool) { const configPath = getWorkspaceToolPreferencePath(); const data = await readWorkspaceToolPreferences(); data.workspaces[workspaceKey] = tool; await fs.mkdir(path.dirname(configPath), { recursive: true }); await fs.writeFile(configPath, JSON.stringify(data, null, 2), "utf8"); resolvedToolCache.clear(); } async function deleteWorkspaceToolPreference(workspaceKey) { const configPath = getWorkspaceToolPreferencePath(); const data = await readWorkspaceToolPreferences(); if (!(workspaceKey in data.workspaces)) return; delete data.workspaces[workspaceKey]; await fs.mkdir(path.dirname(configPath), { recursive: true }); await fs.writeFile(configPath, JSON.stringify(data, null, 2), "utf8"); resolvedToolCache.clear(); } async function forgetPkgToolPreference() { const workspaceKey = findStoredWorkspaceKey(normalizeDir$1(process.cwd()), await readWorkspaceToolPreferences()); if (!workspaceKey) return false; await deleteWorkspaceToolPreference(workspaceKey); return true; } function getSupportedPkgToolNames() { return supportedPkgTools.slice(); } function getPreferredToolFromEnv(candidates) { const preferred = process.env.PI_DEFAULT; if (!preferred) return null; return candidates.some((candidate) => candidate.tool === preferred) ? preferred : null; } function getExplicitPreferredTool(value) { if (!value) return null; return supportedPkgTools.includes(value) ? value : null; } function validateExplicitPreferredTool(preferredTool, candidates) { if (candidates.length === 0) return true; return candidates.some((candidate) => candidate.tool === preferredTool); } function logInvalidPreferredTool(preferredTool, candidates) { const names = candidates.map((candidate) => candidate.tool).join(", "); console.log(color.red(isZh$4 ? `当前 workspace 可选的包管理器是: ${names},不能直接指定 ${preferredTool}。` : `This workspace supports: ${names}. ${preferredTool} cannot be selected directly here.`)); } function getDetectedToolFallback(detected, candidates) { if (candidates.some((candidate) => candidate.tool === detected)) return detected; return candidates[0]?.tool || detected; } function formatCandidateLabel(candidate, cwd) { const indicators = candidate.indicators.join(", "); const relativeRoot = path.relative(cwd, candidate.root) || "."; if (relativeRoot === ".") return `${candidate.tool}: ${indicators}`; return `${candidate.tool}: ${indicators} (${relativeRoot})`; } function toCandidateInfo(candidates) { return candidates.map((candidate) => ({ tool: candidate.tool, indicators: candidate.indicators, root: candidate.root })); } async function selectToolCandidate(candidates, cwd) { if (!isInteractive()) return { status: "unavailable" }; const options = candidates.map((candidate) => formatCandidateLabel(candidate, cwd)); const labelToTool = new Map(candidates.map((candidate) => [formatCandidateLabel(candidate, cwd), candidate.tool])); const choice = await ttySelect(options, isZh$4 ? "🤔检测到多个包管理环境,请选择当前 workspace 使用的安装方式" : "Multiple package managers were detected, choose one for this workspace."); if (!choice) return { status: "cancelled" }; const tool = labelToTool.get(choice); if (!tool) return { status: "cancelled" }; return { status: "selected", tool }; } function logAmbiguousToolFallback(candidates, tool) { const names = candidates.map((candidate) => candidate.tool).join(", "); console.log(color.yellow(isZh$4 ? `检测到多个包管理环境(${names}),当前不是交互式终端,临时使用 ${tool}。可在交互式终端运行一次 pi 以保存当前 workspace 的选择。` : `Detected multiple package managers (${names}). Using ${tool} for now because no interactive TTY is available. Run pi once in an interactive shell to save this workspace preference.`)); } function getSourceLabel(source) { switch (source) { case "saved-preference": return isZh$4 ? "已保存的 workspace 选择" : "saved workspace choice"; case "fresh-selection": return isZh$4 ? "本次重新选择并保存" : "picked now and saved"; case "single-candidate": return isZh$4 ? "当前只检测到一种包管理器标记" : "single detected package-manager indicator"; case "env-default": return isZh$4 ? "PI_DEFAULT 兜底" : "PI_DEFAULT fallback"; case "detected-tool": return isZh$4 ? "环境自动检测结果" : "environment auto-detection"; case "non-interactive-fallback": return isZh$4 ? "非交互终端下的临时兜底" : "non-interactive fallback"; } } function logWorkspaceToolSelected(tool, forceChoose) { console.log(color.green(forceChoose ? isZh$4 ? `当前 workspace 已切换为使用 ${tool},并保存了这个选择。` : `This workspace now uses ${tool}, and the choice has been saved.` : isZh$4 ? `已为当前 workspace 记住 ${tool} 作为包管理器。` : `Saved ${tool} as the package manager for this workspace.`)); } function logWorkspaceToolResolved(tool) { console.log(color.green(isZh$4 ? `当前 workspace 使用 ${tool} 作为包管理器。` : `This workspace uses ${tool} as the package manager.`)); } function logStaleWorkspaceToolRemoved(tool) { console.log(color.yellow(isZh$4 ? `检测到之前保存的 ${tool} 已不再适用于当前 workspace,已自动清除旧记录。` : `The saved ${tool} choice no longer matches this workspace and was removed automatically.`)); } async function preparePkgToolContext(forgetPreference = false) { const cwd = normalizeDir$1(process.cwd()); const originalDetected = await getPkgTool() || "npm"; const candidates = getToolCandidates(cwd); const preferences = await readWorkspaceToolPreferences(); const workspaceKey = resolveWorkspaceKey(cwd, candidates, preferences); let detected = originalDetected; if (forgetPreference) await deleteWorkspaceToolPreference(workspaceKey); if (forgetPreference) delete preferences.workspaces[workspaceKey]; let rememberedTool = preferences.workspaces[workspaceKey]; if (rememberedTool && !candidates.some((candidate) => candidate.tool === rememberedTool)) { await deleteWorkspaceToolPreference(workspaceKey); delete preferences.workspaces[workspaceKey]; logStaleWorkspaceToolRemoved(rememberedTool); rememberedTool = void 0; } if (detected === "npm" && candidates.length === 1) detected = candidates[0].tool; return { detected, candidates, rememberedTool }; } async function getPkgToolStatus() { const { detected, candidates, rememberedTool } = await preparePkgToolContext(); const candidateInfo = toCandidateInfo(candidates); if (rememberedTool) return { status: "resolved", detected, tool: rememberedTool, source: "saved-preference", candidates: candidateInfo }; if (candidates.length <= 1) { const fallback = process.env.PI_DEFAULT; return { status: "resolved", detected, tool: detected === "npm" && fallback ? fallback : detected, source: detected === "npm" && fallback ? "env-default" : candidates.length === 1 ? "single-candidate" : "detected-tool", candidates: candidateInfo }; } const envPreferredTool = getPreferredToolFromEnv(candidates); if (envPreferredTool) return { status: "resolved", detected, tool: envPreferredTool, source: "env-default", candidates: candidateInfo }; if (!isInteractive()) return { status: "resolved", detected, tool: getDetectedToolFallback(detected, candidates), source: "non-interactive-fallback", candidates: candidateInfo }; return { status: "needs-selection", detected, candidates: candidateInfo }; } function printPkgToolStatus(status, options = {}) { if (options.json) { console.log(JSON.stringify({ ...status, sourceLabel: status.status === "resolved" ? getSourceLabel(status.source) : void 0 }, null, 2)); return; } if (status.status === "resolved") { const candidateNames = status.candidates.map((candidate) => candidate.tool).join(", "); console.log(color.green(isZh$4 ? `当前 workspace 使用 ${status.tool} 作为包管理器。` : `This workspace uses ${status.tool} as the package manager.`)); console.log(color.cyan(`${isZh$4 ? "来源" : "Source"}: ${getSourceLabel(status.source)}`)); if (candidateNames) console.log(color.dim(`${isZh$4 ? "候选项" : "Candidates"}: ${candidateNames}`)); return; } const candidateNames = status.candidates.map((candidate) => candidate.tool).join(", "); console.log(color.yellow(isZh$4 ? "当前 workspace 还没有固定包管理器选择。" : "This workspace does not have a locked package-manager choice yet.")); console.log(color.cyan(`${isZh$4 ? "原因" : "Reason"}: ${isZh$4 ? "检测到了多个包管理器标记,且当前没有保存的选择。" : "Multiple package-manager indicators were found and no saved choice exists yet."}`)); console.log(color.dim(`${isZh$4 ? "候选项" : "Candidates"}: ${candidateNames}`)); console.log(color.dim(isZh$4 ? "可执行 `pi --choose-tool` 来保存当前 workspace 的选择。" : "Run `pi --choose-tool` to save a choice for this workspace.")); } function printPkgToolCandidates(status, options = {}) { if (options.json) { console.log(JSON.stringify({ ...status, sourceLabel: status.status === "resolved" ? getSourceLabel(status.source) : void 0 }, null, 2)); return; } if (status.status === "resolved") { console.log(color.green(isZh$4 ? `当前 workspace 使用 ${status.tool} 作为包管理器。` : `This workspace uses ${status.tool} as the package manager.`)); console.log(color.cyan(`${isZh$4 ? "来源" : "Source"}: ${getSourceLabel(status.source)}`)); } else console.log(color.yellow(isZh$4 ? "当前 workspace 还没有固定包管理器选择。" : "This workspace does not have a locked package-manager choice yet.")); if (status.candidates.length === 0) { console.log(color.dim(isZh$4 ? "当前 workspace 没有检测到明确的 lockfile / workspace 候选。" : "No explicit lockfile or workspace candidates were detected in this workspace.")); return; } console.log(color.bold(isZh$4 ? "候选工具:" : "Candidate tools:")); for (const candidate of status.candidates) { const indicators = candidate.indicators.join(", "); console.log(`- ${candidate.tool}`); console.log(color.dim(` ${isZh$4 ? "root" : "root"}: ${candidate.root}`)); console.log(color.dim(` ${isZh$4 ? "indicators" : "indicators"}: ${indicators}`)); } } async function resolvePkgTool(options = {}) { const cwd = normalizeDir$1(process.cwd()); const forceChoose = options.forceChoose || isEnabled(process.env.PI_FORCE_PICK_TOOL); const forgetPreference = options.forgetPreference || isEnabled(process.env.PI_FORGET_PICK_TOOL); const preferredTool = getExplicitPreferredTool(options.preferredTool || process.env.PI_PREFERRED_TOOL); const shouldBypassCache = forceChoose || forgetPreference || Boolean(preferredTool); const cached = resolvedToolCache.get(cwd); if (!shouldBypassCache && cached) return cached; const pending = (async () => { const { detected, candidates, rememberedTool } = await preparePkgToolContext(forgetPreference); if (preferredTool) { if (!validateExplicitPreferredTool(preferredTool, candidates)) { logInvalidPreferredTool(preferredTool, candidates); process.exit(1); } await writeWorkspaceToolPreference(resolveWorkspaceKey(normalizeDir$1(process.cwd()), candidates, await readWorkspaceToolPreferences()), preferredTool); logWorkspaceToolSelected(preferredTool, true); return { detected, tool: preferredTool, source: "fresh-selection" }; } if (!forceChoose && rememberedTool) return { detected, tool: rememberedTool, source: "saved-preference" }; if (candidates.length <= 1) { const fallback = process.env.PI_DEFAULT; const tool = detected === "npm" && fallback ? fallback : detected; if (forceChoose) logWorkspaceToolResolved(tool); return { detected, tool, source: detected === "npm" && fallback ? "env-default" : candidates.length === 1 ? "single-candidate" : "detected-tool" }; } const envPreferredTool = getPreferredToolFromEnv(candidates); if (!forceChoose && envPreferredTool) return { detected, tool: envPreferredTool, source: "env-default" }; const cwdForSelection = normalizeDir$1(process.cwd()); const selection = await selectToolCandidate(candidates, cwd); if (selection.status === "selected") { await writeWorkspaceToolPreference(resolveWorkspaceKey(cwdForSelection, candidates, await readWorkspaceToolPreferences()), selection.tool); logWorkspaceToolSelected(selection.tool, forceChoose); return { detected, tool: selection.tool, source: "fresh-selection" }; } if (selection.status === "cancelled") { console.log(color.dim(isZh$4 ? "已取消" : "Cancelled")); process.exit(0); } const tool = getDetectedToolFallback(detected, candidates); logAmbiguousToolFallback(candidates, tool); return { detected, tool, source: "non-interactive-fallback" }; })(); resolvedToolCache.set(cwd, pending); return pending; } function getInstallCommand(tool, hasParams) { const action = hasParams ? "add" : "install"; switch (tool) { case "pnpm": return `pnpm ${action}`; case "yarn": return `yarn ${action}`; case "bun": return `bun ${action}`; case "npm": return "npm install"; default: return `${tool} ${action}`; } } function getRemoveCommand(tool) { switch (tool) { case "pnpm": return "pnpm remove"; case "yarn": return "yarn remove"; case "bun": return "bun remove"; case "npm": return "npm uninstall"; default: return `${tool} remove`; } } //#endregion //#region src/utils.ts const DW = /\s-DW/g; const W = /\s-W/g; const Dw = /\s-Dw/g; const w = /\s-w/g; const D = /\s-D(?!w)/g; const d = /\s-d(?!w)/g; const isZh$3 = process.env.PI_Lang === "zh"; const log$1 = console.log; function normalizeDir(dir) { return path.resolve(dir); } function isSameDir(a, b) { return normalizeDir(a) === normalizeDir(b); } function findUpSync(startDir, predicate) { let current = normalizeDir(startDir); while (true) { if (predicate(current)) return current; const parent = path.dirname(current); if (parent === current) return null; current = parent; } } async function findUpAsync(startDir, predicate) { let current = normalizeDir(startDir); while (true) { if (await predicate(current)) return current; const parent = path.dirname(current); if (parent === current) return null; current = parent; } } async function getParams(params) { const cwd = process.cwd(); try { const { tool } = await resolvePkgTool(); switch (tool) { case "pnpm": { const pnpmWorkspaceRoot = findUpSync(cwd, (dir) => isFile(path.join(dir, "pnpm-workspace.yaml"))); const inPnpmWorkspace = Boolean(pnpmWorkspaceRoot); const isPnpmWorkspaceRoot = pnpmWorkspaceRoot ? isSameDir(pnpmWorkspaceRoot, cwd) : false; if (!inPnpmWorkspace) { if (DW.test(params)) return params.replace(DW, " -D"); if (Dw.test(params)) return params.replace(Dw, " -D"); if (W.test(params)) return params.replace(W, ""); if (w.test(params)) return params.replace(w, ""); if (d.test(params)) return params.replace(d, " -D"); return params; } let out = params; if (DW.test(out)) out = out.replace(DW, " -Dw"); if (W.test(out)) out = out.replace(W, " -w"); if (isPnpmWorkspaceRoot) { if (D.test(out)) out = out.replace(D, " -Dw"); if (d.test(out)) out = out.replace(d, " -Dw"); if (!out || Dw.test(out) || w.test(out)) return out; return `${out} -w`; } if (d.test(out)) out = out.replace(d, " -D"); return out; } case "yarn": { const yarnWorkspaceRoot = await findUpAsync(cwd, async (dir) => { try { return Boolean((await getPkg(path.join(dir, "package.json")))?.workspaces); } catch { return false; } }); const inYarnWorkspace = Boolean(yarnWorkspaceRoot); const isYarnWorkspaceRoot = yarnWorkspaceRoot ? isSameDir(yarnWorkspaceRoot, cwd) : false; if (!inYarnWorkspace) { if (Dw.test(params)) return params.replace(Dw, " -D"); if (DW.test(params)) return params.replace(DW, " -D"); if (W.test(params)) return params.replace(W, ""); if (w.test(params)) return params.replace(w, ""); if (d.test(params)) return params.replace(d, " -D"); return params; } let out = params; if (w.test(out)) out = out.replace(w, " -W"); if (Dw.test(out)) out = out.replace(Dw, " -DW"); if (W.test(out)) out = out.replace(W, " -W"); if (isYarnWorkspaceRoot) { if (D.test(out)) out = out.replace(D, " -DW"); if (d.test(out)) out = out.replace(d, " -DW"); if (!out || W.test(out) || DW.test(out)) return out; return `${out} -W`; } if (d.test(out)) out = out.replace(d, " -D"); return out; } default: return d.test(params) ? params.replace(d, " -D") : params; } } catch { console.log(color.red(`${isZh$3 ? "package.json并不存在,在以下目录中:" : "package.json has not been found in"} ${process.cwd()}`)); process.exit(1); } } async function loading(text, isSilent = false) { const { color, spinner } = await getStyle(); const ora = (await import("ora")).default; return ora({ text, spinner, color, isSilent, discardStdin: true }).start(); } async function getStyle() { const { PI_COLOR: color = "yellow", PI_SPINNER: spinner = "star" } = process.env; return { color, spinner }; } async function getLatestVersion(pkg, isZh = true) { const data = []; for (const p of pkg.replace(/\s+/, " ").split(" ")) { const [pName, v] = p.split("$"); let { status, result } = await jsShell(`npm view ${pName}`, [ "inherit", "pipe", "inherit" ]); if (status === 0) { if (result.startsWith("@")) result = result.slice(1); const item = isZh ? `${pName} ${color.gray(v)} -> ${result.match(/@(\S+)/)[1]}` : `Installed ${pName} ${color.dim(v)} -> latest version:${result.match(/@(\S+)/)[1]}`; data.push(item); } else throw new Error(result); } return `${data.join(" ")}${isZh ? " 安装成功! 😊" : " successfully! 😊"}`; } async function pushHistory(command) { log$1(color.bold(color.blue(`${isZh$3 ? "快捷指令" : "shortcut command"}: ${command}`))); const historyHint = process.env.CCOMMAND_HISTORY_HINT || path.join(process.env.XDG_CACHE_HOME || path.join(process.env.HOME || os.homedir(), ".cache"), "ccommand", "last-history"); try { fs$1.mkdirSync(path.dirname(historyHint), { recursive: true }); fs$1.writeFileSync(historyHint, `${Date.now()}\t${command}\n`, "utf8"); } catch {} const shellName = (process.env.SHELL || "/bin/bash").split("/").pop() || "bash"; let historyFile = ""; let historyFormat = "bash"; const home = process.env.HOME || os.homedir(); switch (shellName) { case "zsh": historyFile = path.join(home, ".zsh_history"); historyFormat = "zsh"; break; case "bash": historyFile = process.env.HISTFILE || path.join(home, ".bash_history"); historyFormat = "bash"; break; case "fish": historyFile = path.join(home, ".local", "share", "fish", "fish_history"); historyFormat = "fish"; break; default: historyFile = process.env.HISTFILE || path.join(home, ".bash_history"); historyFormat = "bash"; } try { if (!fs$1.existsSync(historyFile)) { log$1(color.yellow(`${isZh$3 ? `未找到 ${shellName} 历史文件` : `${shellName} history file not found`}`)); return; } const raw = fs$1.readFileSync(historyFile, "utf8"); const timestamp = Math.floor(Date.now() / 1e3); let newEntry = ""; if (historyFormat === "zsh") newEntry = `: ${timestamp}:0;${command}`; else if (historyFormat === "fish") newEntry = `- cmd: ${command}\n when: ${timestamp}`; else if (process.env.HISTTIMEFORMAT) newEntry = `#${timestamp}\n${command}`; else newEntry = command; function parseEntries(content) { if (historyFormat === "fish") { const lines = content.split(/\r?\n/); const blocks = []; let buffer = []; for (const line of lines) if (line.startsWith("- cmd: ")) { if (buffer.length) { blocks.push(buffer.join("\n")); buffer = []; } buffer.push(line); } else if (buffer.length) buffer.push(line); else if (line.trim() !== "") blocks.push(line); if (buffer.length) blocks.push(buffer.join("\n")); return blocks.filter(Boolean); } else if (historyFormat === "zsh") return content.split(/\r?\n/).map((l) => l.trim()).filter(Boolean); else { const lines = content.split(/\r?\n/); const entries = []; for (let i = 0; i < lines.length; i++) { const line = lines[i]; if (line.startsWith("#")) { const next = lines[i + 1] ?? ""; entries.push(`${line}\n${next}`); i++; } else if (line.trim() !== "") entries.push(line); } return entries; } } const entries = parseEntries(raw); function extractCommand(entry) { if (historyFormat === "fish") { const m = entry.split("\n")[0].match(/^- cmd: (.*)$/); return m ? m[1] : entry; } else if (historyFormat === "zsh") { const m = entry.match(/^[^;]*;(.+)$/); return m ? m[1] : entry; } else { if (entry.startsWith("#")) { const parts = entry.split(/\r?\n/); return parts[1] ?? parts[0]; } return entry; } } const newEntries = []; const newCmd = extractCommand(newEntry); let existingFishBlock = null; for (const e of entries) { if (extractCommand(e) === newCmd) { if (historyFormat === "fish") { existingFishBlock = e; continue; } continue; } newEntries.push(e); } if (historyFormat === "fish" && existingFishBlock) { const lines = existingFishBlock.split("\n"); let hasWhen = false; const updated = lines.map((line) => { if (line.trim().startsWith("when:") || line.startsWith(" when:")) { hasWhen = true; return ` when: ${timestamp}`; } return line; }); if (!hasWhen) updated.splice(1, 0, ` when: ${timestamp}`); newEntries.push(updated.join("\n")); } else newEntries.push(newEntry); let finalContent = ""; if (historyFormat === "fish") finalContent = `${newEntries.map((e) => e.trimEnd()).join("\n")}\n`; else finalContent = `${newEntries.join("\n")}\n`; const tmpPath = `${historyFile}.ccommand.tmp`; fs$1.writeFileSync(tmpPath, finalContent, "utf8"); fs$1.renameSync(tmpPath, historyFile); } catch (err) { log$1(color.red(`${isZh$3 ? `❌ 添加到 ${shellName} 历史记录失败` : `❌ Failed to add to ${shellName} history`}${err ? `: ${String(err)}` : ""}`)); } } //#endregion //#region src/pi.ts const isZh$2 = process.env.PI_Lang === "zh"; async function pi(params, pkg, executor = "pi") { await detectNode(); const text = pkg ? `Installing ${params} ...` : "Updating dependency ..."; const isLatest = executor === "pil"; const start = Date.now(); let successMsg = ""; if (isLatest) successMsg = await getLatestVersion(pkg, isZh$2); else successMsg = pkg ? isZh$2 ? `${pkg} 安装成功! 😊` : `Installed ${pkg} successfully! 😊` : isZh$2 ? "依赖更新成功! 😊" : "Updated dependency successfully! 😊"; const failMsg = pkg ? isZh$2 ? `${params} 安装失败 😭` : `Failed to install ${params} 😭` : isZh$2 ? "依赖更新失败 😭" : "Failed to update dependency 😭"; const isSilent = process.env.PI_SILENT === "true"; let stdio = isSilent ? "inherit" : [ "inherit", "pipe", "inherit" ]; let loading_status; const { PI_DEFAULT, PI_MaxSockets: sockets } = process.env; const { tool } = await resolvePkgTool(); const maxSockets = sockets || 4; if (tool === "npm" && !PI_DEFAULT) stdio = "inherit"; else loading_status = await loading(text, isSilent); executor = getInstallCommand(tool, Boolean(params)); const newParams = isLatest ? "" : await getParams(params); const runSockets = tool === "npm" ? ` --max-sockets=${maxSockets}` : ""; const latestParams = Array.isArray(params) ? params : params ? [params] : []; const cmdList = isLatest ? latestParams.map((p) => `${executor} ${p}`) : [`${executor}${newParams ? ` ${newParams}` : runSockets}`]; const runCmd = isLatest ? cmdList.join(" & ") : cmdList[0]; const runCommands = async (commands) => { const results = await Promise.all(commands.map((command) => useNodeWorker({ params: command, stdio, errorExit: false }))); const failed = results.find((r) => r.status !== 0); const merged = results.map((r) => r.result).filter(Boolean).join("\n"); return { status: failed ? failed.status : 0, result: failed?.result || merged }; }; let { status, result } = await runCommands(cmdList); if (result && result.includes("pnpm versions with respective Node.js version support")) { log(result); log(color.yellow(isZh$2 ? "正在尝试使用 npm 再次执行..." : "Trying to use npm to run again...")); const fallbackCommands = isLatest ? latestParams.map((p) => `npm install ${p}`) : [`npm install${newParams ? ` ${newParams}` : runSockets}`]; const fallbackResults = await Promise.all(fallbackCommands.map((command) => jsShell(command, { stdio }))); const fallbackFailed = fallbackResults.find((r) => r.status !== 0); const fallbackMerged = fallbackResults.map((r) => r.result).filter(Boolean).join("\n"); status = fallbackFailed ? fallbackFailed.status : 0; result = fallbackFailed?.result || fallbackMerged; } if (stdio === "inherit") loading_status = await loading(""); const costTime = (Date.now() - start) / 1e3; successMsg += color.blue(` ---- ⏰:${costTime}s`); if (status === 0) { loading_status.succeed(color.green(successMsg)); pushHistory(runCmd); } else if (result && result.includes("Not Found - 404")) { const _pkg = result.match(/\/[^/:]+:/)?.[0].slice(1, -1); const _result = isZh$2 ? `${_pkg} 包名可能有误或者版本号不存在,并不能在npm中搜索到,请检查` : `${_pkg} the package name may be wrong, and cannot be found in npm, please check`; loading_status.fail(color.red(result ? `${failMsg}\n${_result}` : failMsg)); } else loading_status.fail(color.red(result ? `${failMsg}\n${result}` : failMsg)); if (result) { const match = result.match(/ERR_PNPM_NO_MATCHING_VERSION_INSIDE_WORKSPACE\u2009 In : No matching version found for\s+([^@]+)/); if (match) { const dep = match[1]; jsShell(`pi ${dep}@latest`); } } process.exit(); } //#endregion //#region src/pci.ts function pci(params, pkg) { return pi(params, pkg); } //#endregion //#region src/require.ts const base = fileURLToPath(import.meta.url); const localRequire = createRequire(base); function getCcommand() { return localRequire("ccommand"); } //#endregion //#region src/prun.ts async function prun(params) { ensurePrunAutoInit(); const prevNoHistory = process.env.CCOMMAND_NO_HISTORY; if (!shouldSuppressHistory$1()) delete process.env.CCOMMAND_NO_HISTORY; else process.env.CCOMMAND_NO_HISTORY = "1"; const { ccommand } = getCcommand(); try { await ccommand(params); } finally { if (prevNoHistory == null) delete process.env.CCOMMAND_NO_HISTORY; else process.env.CCOMMAND_NO_HISTORY = prevNoHistory; } } const isZh$1 = process.env.PI_Lang === "zh"; const safeShellValue = /^[\w./:@%+=,-]+$/; function isNoHistory$1(value) { if (!value) return false; const normalized = value.toLowerCase(); return normalized === "1" || normalized === "true" || normalized === "yes"; } function shouldSuppressHistory$1() { return isNoHistory$1(process.env.CCOMMAND_NO_HISTORY) || isNoHistory$1(process.env.NO_HISTORY); } function hasTruthyEnv(...values) { return values.some(isNoHistory$1); } function shellQuote(value) { if (value === "") return "''"; if (safeShellValue.test(value)) return value; return `'${value.replace(/'/g, `'\\''`)}'`; } function powerShellQuote(value) { if (value === "") return "''"; return `'${value.replace(/'/g, "''")}'`; } function splitCommand(value) { const parts = []; let current = ""; let quote = null; let hasValue = false; const pushCurrent = () => { if (!hasValue) return; parts.push(current); current = ""; hasValue = false; }; for (let i = 0; i < value.length; i++) { const char = value[i]; if (quote) { if (char === quote) { quote = null; hasValue = true; continue; } if (quote === "\"" && char === "\\") { const next = value[i + 1]; if (next) { current += next; hasValue = true; i++; continue; } } current += char; hasValue = true; continue; } if (char === "\"" || char === "'") { quote = char; hasValue = true; continue; } if (/\s/.test(char)) { pushCurrent(); while (i + 1 < value.length && /\s/.test(value[i + 1])) i++; continue; } if (char === "\\") { const next = value[i + 1]; if (next) { current += next; hasValue = true; i++; continue; } } current += char; hasValue = true; } pushCurrent(); return parts; } function normalizeShellName(value) { const shell = (value || "").toLowerCase().replace(/\.exe$/, ""); if (shell === "powershell") return "powershell"; if (shell === "pwsh") return "pwsh"; if (shell === "fish" || shell === "zsh" || shell === "bash") return shell; return shell; } function detectShell() { const envShell = normalizeShellName(path.basename(process.env.SHELL || "")); if (process.env.FISH_VERSION) return "fish"; if (process.env.ZSH_VERSION) return "zsh