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,288 lines (1,279 loc) 82.6 kB
//#region \0rolldown/runtime.js var __create = Object.create; var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __getProtoOf = Object.getPrototypeOf; var __hasOwnProp = Object.prototype.hasOwnProperty; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) { key = keys[i]; if (!__hasOwnProp.call(to, key) && key !== except) { __defProp(to, key, { get: ((k) => from[k]).bind(null, key), enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } } } return to; }; var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, mod)); //#endregion let node_path = require("node:path"); node_path = __toESM(node_path); let node_process = require("node:process"); node_process = __toESM(node_process); let lazy_js_utils = require("lazy-js-utils"); let lazy_js_utils_node = require("lazy-js-utils/node"); let picocolors = require("picocolors"); picocolors = __toESM(picocolors); let node_readline = require("node:readline"); node_readline = __toESM(node_readline); let node_child_process = require("node:child_process"); let node_console = require("node:console"); let node_fs_promises = require("node:fs/promises"); node_fs_promises = __toESM(node_fs_promises); let node_os = require("node:os"); node_os = __toESM(node_os); let node_fs = require("node:fs"); node_fs = __toESM(node_fs); let node_module = require("node:module"); let node_url = require("node:url"); //#region package.json var version = "0.2.19"; //#endregion //#region src/tty.ts const isZh$6 = node_process.default.env.PI_Lang === "zh"; function isInteractive() { return Boolean(node_process.default.stdin.isTTY && node_process.default.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 += picocolors.default.bold(picocolors.default.yellow(ch)); else output += active ? picocolors.default.cyan(ch) : ch; width += w; } if (truncated) { if (maxWidth <= ellipsisWidth) return picocolors.default.dim(ellipsis); output += picocolors.default.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 = node_process.default.stdin; const stdout = node_process.default.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"); }); }; node_readline.default.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 picocolors.default.dim("↑/↓ 选择,") + picocolors.default.bold(picocolors.default.cyan("空格")) + picocolors.default.dim(" 标记,Tab 补全,/ 搜索,回车确认,Esc 取消") + picocolors.default.dim(position); return picocolors.default.dim("Use ↑/↓ to move, ") + picocolors.default.bold(picocolors.default.cyan("Space")) + picocolors.default.dim(" to toggle, Tab to complete, / to search, Enter to confirm, Esc to cancel") + picocolors.default.dim(position); } const hint = isZh$6 ? "↑/↓ 选择,Tab 补全,/ 搜索,回车确认,Esc 取消" : "Use ↑/↓ to move, Tab to complete, / to search, Enter to confirm, Esc to cancel"; return picocolors.default.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(picocolors.default.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(picocolors.default.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 ? picocolors.default.cyan(prefix) : prefix; const content = `${active ? picocolors.default.cyan(indicator) : indicator} ${renderedPrefix}${renderedOption}`; lines.push(content); }); if (window.end < filtered.length) lines.push(picocolors.default.dim("…")); } lines.push(hintLine); if (anchor) stdout.write(`\x1B[${anchor.row};1H`); else node_readline.default.cursorTo(stdout, 0); node_readline.default.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 node_readline.default.cursorTo(stdout, inputCol); }; return new Promise((resolve) => { let onKeypress; let onResize; const done = (value) => { if (stdin.isTTY) stdin.setRawMode(false); stdin.off("keypress", onKeypress); node_process.default.off("SIGWINCH", onResize); stdout.write("\x1B[?25h"); if (anchor) stdout.write(`\x1B[${anchor.row};1H`); else node_readline.default.cursorTo(stdout, 0); node_readline.default.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(); }; node_process.default.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 = node_process.default.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 })); node_process.default.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 })); node_process.default.exit(0); } } //#endregion //#region src/pa.ts function hasCommand$1(command) { return (node_process.default.platform === "win32" ? (0, node_child_process.spawnSync)("where", [command], { stdio: "ignore" }) : (0, node_child_process.spawnSync)("sh", ["-c", `command -v ${command}`], { stdio: "ignore" })).status === 0; } function pa(params = "") { console.warn(picocolors.default.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(picocolors.default.red("[pi] `pa` delegates to `na`, but `na` is not installed. Install the package that provides `na`, or stop using the deprecated `pa` alias.")); node_process.default.exitCode = 1; return; } return (0, lazy_js_utils_node.jsShell)(`na${params ? ` ${params}` : ""}`, "inherit"); } //#endregion //#region src/detectNode.ts async function detectNode() { try { await (0, lazy_js_utils_node.getPkg)(); } catch { const cwd = node_process.default.cwd(); console.log(picocolors.default.red(`当前目录: ${cwd} 没有package.json文件`)); node_process.default.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 = node_process.default.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 node_path.default.resolve(dir); } function isSameOrInsideDir(base, target) { const relative = node_path.default.relative(base, target); return relative === "" || !relative.startsWith("..") && !node_path.default.isAbsolute(relative); } function findUpSync$1(startDir, predicate) { let current = normalizeDir$1(startDir); while (true) { if (predicate(current)) return current; const parent = node_path.default.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) => (0, lazy_js_utils.isFile)(node_path.default.join(dir, indicator)))); if (!root) return null; const foundIndicators = indicators.filter((indicator) => (0, lazy_js_utils.isFile)(node_path.default.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 = node_process.default.env.HOME || node_os.default.homedir(); if (node_process.default.platform === "win32") return node_process.default.env.APPDATA || node_path.default.join(home, "AppData", "Roaming"); return node_process.default.env.XDG_CONFIG_HOME || node_path.default.join(home, ".config"); } function getWorkspaceToolPreferencePath() { return node_path.default.join(getConfigHome(), "pi", "workspace-tools.json"); } async function readWorkspaceToolPreferences() { const configPath = getWorkspaceToolPreferencePath(); try { const raw = await node_fs_promises.default.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 node_fs_promises.default.mkdir(node_path.default.dirname(configPath), { recursive: true }); await node_fs_promises.default.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 node_fs_promises.default.mkdir(node_path.default.dirname(configPath), { recursive: true }); await node_fs_promises.default.writeFile(configPath, JSON.stringify(data, null, 2), "utf8"); resolvedToolCache.clear(); } async function forgetPkgToolPreference() { const workspaceKey = findStoredWorkspaceKey(normalizeDir$1(node_process.default.cwd()), await readWorkspaceToolPreferences()); if (!workspaceKey) return false; await deleteWorkspaceToolPreference(workspaceKey); return true; } function getSupportedPkgToolNames() { return supportedPkgTools.slice(); } function getPreferredToolFromEnv(candidates) { const preferred = node_process.default.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(picocolors.default.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 = node_path.default.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(picocolors.default.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(picocolors.default.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(picocolors.default.green(isZh$4 ? `当前 workspace 使用 ${tool} 作为包管理器。` : `This workspace uses ${tool} as the package manager.`)); } function logStaleWorkspaceToolRemoved(tool) { console.log(picocolors.default.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(node_process.default.cwd()); const originalDetected = await (0, lazy_js_utils_node.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 = node_process.default.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(picocolors.default.green(isZh$4 ? `当前 workspace 使用 ${status.tool} 作为包管理器。` : `This workspace uses ${status.tool} as the package manager.`)); console.log(picocolors.default.cyan(`${isZh$4 ? "来源" : "Source"}: ${getSourceLabel(status.source)}`)); if (candidateNames) console.log(picocolors.default.dim(`${isZh$4 ? "候选项" : "Candidates"}: ${candidateNames}`)); return; } const candidateNames = status.candidates.map((candidate) => candidate.tool).join(", "); console.log(picocolors.default.yellow(isZh$4 ? "当前 workspace 还没有固定包管理器选择。" : "This workspace does not have a locked package-manager choice yet.")); console.log(picocolors.default.cyan(`${isZh$4 ? "原因" : "Reason"}: ${isZh$4 ? "检测到了多个包管理器标记,且当前没有保存的选择。" : "Multiple package-manager indicators were found and no saved choice exists yet."}`)); console.log(picocolors.default.dim(`${isZh$4 ? "候选项" : "Candidates"}: ${candidateNames}`)); console.log(picocolors.default.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(picocolors.default.green(isZh$4 ? `当前 workspace 使用 ${status.tool} 作为包管理器。` : `This workspace uses ${status.tool} as the package manager.`)); console.log(picocolors.default.cyan(`${isZh$4 ? "来源" : "Source"}: ${getSourceLabel(status.source)}`)); } else console.log(picocolors.default.yellow(isZh$4 ? "当前 workspace 还没有固定包管理器选择。" : "This workspace does not have a locked package-manager choice yet.")); if (status.candidates.length === 0) { console.log(picocolors.default.dim(isZh$4 ? "当前 workspace 没有检测到明确的 lockfile / workspace 候选。" : "No explicit lockfile or workspace candidates were detected in this workspace.")); return; } console.log(picocolors.default.bold(isZh$4 ? "候选工具:" : "Candidate tools:")); for (const candidate of status.candidates) { const indicators = candidate.indicators.join(", "); console.log(`- ${candidate.tool}`); console.log(picocolors.default.dim(` ${isZh$4 ? "root" : "root"}: ${candidate.root}`)); console.log(picocolors.default.dim(` ${isZh$4 ? "indicators" : "indicators"}: ${indicators}`)); } } async function resolvePkgTool(options = {}) { const cwd = normalizeDir$1(node_process.default.cwd()); const forceChoose = options.forceChoose || isEnabled(node_process.default.env.PI_FORCE_PICK_TOOL); const forgetPreference = options.forgetPreference || isEnabled(node_process.default.env.PI_FORGET_PICK_TOOL); const preferredTool = getExplicitPreferredTool(options.preferredTool || node_process.default.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); node_process.default.exit(1); } await writeWorkspaceToolPreference(resolveWorkspaceKey(normalizeDir$1(node_process.default.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 = node_process.default.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(node_process.default.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(picocolors.default.dim(isZh$4 ? "已取消" : "Cancelled")); node_process.default.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 = node_process.default.env.PI_Lang === "zh"; const log$1 = console.log; function normalizeDir(dir) { return node_path.default.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 = node_path.default.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 = node_path.default.dirname(current); if (parent === current) return null; current = parent; } } async function getParams(params) { const cwd = node_process.default.cwd(); try { const { tool } = await resolvePkgTool(); switch (tool) { case "pnpm": { const pnpmWorkspaceRoot = findUpSync(cwd, (dir) => (0, lazy_js_utils.isFile)(node_path.default.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 (0, lazy_js_utils_node.getPkg)(node_path.default.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(picocolors.default.red(`${isZh$3 ? "package.json并不存在,在以下目录中:" : "package.json has not been found in"} ${node_process.default.cwd()}`)); node_process.default.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" } = node_process.default.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 (0, lazy_js_utils_node.jsShell)(`npm view ${pName}`, [ "inherit", "pipe", "inherit" ]); if (status === 0) { if (result.startsWith("@")) result = result.slice(1); const item = isZh ? `${pName} ${picocolors.default.gray(v)} -> ${result.match(/@(\S+)/)[1]}` : `Installed ${pName} ${picocolors.default.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(picocolors.default.bold(picocolors.default.blue(`${isZh$3 ? "快捷指令" : "shortcut command"}: ${command}`))); const historyHint = node_process.default.env.CCOMMAND_HISTORY_HINT || node_path.default.join(node_process.default.env.XDG_CACHE_HOME || node_path.default.join(node_process.default.env.HOME || node_os.default.homedir(), ".cache"), "ccommand", "last-history"); try { node_fs.default.mkdirSync(node_path.default.dirname(historyHint), { recursive: true }); node_fs.default.writeFileSync(historyHint, `${Date.now()}\t${command}\n`, "utf8"); } catch {} const shellName = (node_process.default.env.SHELL || "/bin/bash").split("/").pop() || "bash"; let historyFile = ""; let historyFormat = "bash"; const home = node_process.default.env.HOME || node_os.default.homedir(); switch (shellName) { case "zsh": historyFile = node_path.default.join(home, ".zsh_history"); historyFormat = "zsh"; break; case "bash": historyFile = node_process.default.env.HISTFILE || node_path.default.join(home, ".bash_history"); historyFormat = "bash"; break; case "fish": historyFile = node_path.default.join(home, ".local", "share", "fish", "fish_history"); historyFormat = "fish"; break; default: historyFile = node_process.default.env.HISTFILE || node_path.default.join(home, ".bash_history"); historyFormat = "bash"; } try { if (!node_fs.default.existsSync(historyFile)) { log$1(picocolors.default.yellow(`${isZh$3 ? `未找到 ${shellName} 历史文件` : `${shellName} history file not found`}`)); return; } const raw = node_fs.default.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 (node_process.default.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`; node_fs.default.writeFileSync(tmpPath, finalContent, "utf8"); node_fs.default.renameSync(tmpPath, historyFile); } catch (err) { log$1(picocolors.default.red(`${isZh$3 ? `❌ 添加到 ${shellName} 历史记录失败` : `❌ Failed to add to ${shellName} history`}${err ? `: ${String(err)}` : ""}`)); } } //#endregion //#region src/pi.ts const isZh$2 = node_process.default.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 = node_process.default.env.PI_SILENT === "true"; let stdio = isSilent ? "inherit" : [ "inherit", "pipe", "inherit" ]; let loading_status; const { PI_DEFAULT, PI_MaxSockets: sockets } = node_process.default.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) => (0, lazy_js_utils_node.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")) { (0, node_console.log)(result); (0, node_console.log)(picocolors.default.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) => (0, lazy_js_utils_node.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 += picocolors.default.blue(` ---- ⏰:${costTime}s`); if (status === 0) { loading_status.succeed(picocolors.default.green(successMsg)); pushHistory(runCmd); } else if (result && result.includes("Not Found - 404")) { const _pkg = result.match(/\/[^/:]+:/)?.[0].slice(1, -1); const _result = isZh$2 ? `${_pk