@simon_he/pi 0.2.9 → 0.2.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.mjs CHANGED
@@ -7,16 +7,17 @@ import { getPkg, getPkgTool, hasPkg, isGo, isRust, jsShell, useNodeWorker } from
7
7
  import color from "picocolors";
8
8
  import readline from "node:readline";
9
9
  import { log } from "node:console";
10
- import fs from "node:fs";
10
+ import fs from "node:fs/promises";
11
11
  import os from "node:os";
12
+ import fs$1 from "node:fs";
12
13
  import { fileURLToPath } from "node:url";
13
14
 
14
15
  //#region package.json
15
- var version = "0.2.9";
16
+ var version = "0.2.11";
16
17
 
17
18
  //#endregion
18
19
  //#region src/tty.ts
19
- const isZh$6 = process.env.PI_Lang === "zh";
20
+ const isZh$7 = process.env.PI_Lang === "zh";
20
21
  function isInteractive() {
21
22
  return Boolean(process.stdin.isTTY && process.stdout.isTTY);
22
23
  }
@@ -198,7 +199,7 @@ async function runSelect(options, config) {
198
199
  if (anchor) {
199
200
  const prompt = "> ";
200
201
  const header = `? ${config.placeholder}`;
201
- 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})`;
202
+ const hintLine = `${config.mode === "multiple" ? isZh$7 ? "↑/↓ 选择,空格标记,Tab 补全,/ 搜索,回车确认,Esc 取消" : "Use ↑/↓ to move, Space to toggle, Tab to complete, / to search, Enter to confirm, Esc to cancel" : isZh$7 ? "↑/↓ 选择,Tab 补全,/ 搜索,回车确认,Esc 取消" : "Use ↑/↓ to move, Tab to complete, / to search, Enter to confirm, Esc to cancel"} (1/${options.length})`;
202
203
  const headerRows = rowCount(header, columns);
203
204
  const inputRows = rowCount(`${prompt}`, columns);
204
205
  const hintRows = rowCount(hintLine, columns);
@@ -236,10 +237,10 @@ async function runSelect(options, config) {
236
237
  const hintLine = (() => {
237
238
  const position = ` (${Math.min(cursor + 1, filtered.length)}/${filtered.length})`;
238
239
  if (config.mode === "multiple") {
239
- if (isZh$6) return color.dim("↑/↓ 选择,") + color.bold(color.cyan("空格")) + color.dim(" 标记,Tab 补全,/ 搜索,回车确认,Esc 取消") + color.dim(position);
240
+ if (isZh$7) return color.dim("↑/↓ 选择,") + color.bold(color.cyan("空格")) + color.dim(" 标记,Tab 补全,/ 搜索,回车确认,Esc 取消") + color.dim(position);
240
241
  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);
241
242
  }
242
- const hint = isZh$6 ? "↑/↓ 选择,Tab 补全,/ 搜索,回车确认,Esc 取消" : "Use ↑/↓ to move, Tab to complete, / to search, Enter to confirm, Esc to cancel";
243
+ const hint = isZh$7 ? "↑/↓ 选择,Tab 补全,/ 搜索,回车确认,Esc 取消" : "Use ↑/↓ to move, Tab to complete, / to search, Enter to confirm, Esc to cancel";
243
244
  return color.dim(`${hint}${position}`);
244
245
  })();
245
246
  const headerRows = rowCount(header, columns);
@@ -253,7 +254,7 @@ async function runSelect(options, config) {
253
254
  const maxVisibleRaw = needsScroll ? Math.max(minVisibleTarget, optionAreaRows - ellipsisReserve) : filtered.length;
254
255
  const maxVisible = Math.max(1, Math.min(maxVisibleRaw, optionAreaRows, filtered.length));
255
256
  const lines = [header, inputLine];
256
- if (filtered.length === 0) lines.push(color.dim(isZh$6 ? "没有匹配项" : "No matches"));
257
+ if (filtered.length === 0) lines.push(color.dim(isZh$7 ? "没有匹配项" : "No matches"));
257
258
  else {
258
259
  const window = getVisibleWindow(filtered.length, cursor, maxVisible);
259
260
  const visible = filtered.slice(window.start, window.end);
@@ -454,11 +455,11 @@ function renderBox(lines, options = {}) {
454
455
 
455
456
  //#endregion
456
457
  //#region src/help.ts
457
- const isZh$5 = process.env.PI_Lang === "zh";
458
+ const isZh$6 = process.env.PI_Lang === "zh";
458
459
  async function help(argv) {
459
460
  const arg = argv[0];
460
461
  if (arg === "-v" || arg === "--version") {
461
- const message = isZh$5 ? [
462
+ const message = isZh$6 ? [
462
463
  `pi 版本: ${version}`,
463
464
  "请为我的努力点一个行 🌟",
464
465
  "谢谢 🤟"
@@ -480,6 +481,13 @@ async function help(argv) {
480
481
  console.log(renderBox([
481
482
  "PI Commands:",
482
483
  "~ pi: install package",
484
+ "~ pi --choose-tool: choose package manager for this workspace",
485
+ "~ pi --choose-tool bun: choose the tool directly",
486
+ "~ pi --forget-tool: clear saved package manager choice",
487
+ "~ pi --show-tool: show current workspace package manager",
488
+ "~ pi --show-tool --json: show current tool as JSON",
489
+ "~ pi --list-tools: list detected package-manager candidates",
490
+ "~ pi --list-tools --json: list candidates as JSON",
483
491
  "~ pix: npx package",
484
492
  "~ pui: uninstall package",
485
493
  "~ prun: run package script",
@@ -489,10 +497,24 @@ async function help(argv) {
489
497
  "~ pa: agent alias",
490
498
  "~ pu: package upgrade",
491
499
  "~ pci: package clean install",
492
- "~ pil: package latest install"
500
+ "~ pci --choose-tool: re-pick tool before clean install",
501
+ "~ pci --choose-tool bun: choose the tool directly",
502
+ "~ pci --forget-tool: clear saved tool before clean install",
503
+ "~ pci --show-tool: show current tool before clean install",
504
+ "~ pci --show-tool --json: show current tool as JSON",
505
+ "~ pci --list-tools: list detected candidates",
506
+ "~ pci --list-tools --json: list candidates as JSON",
507
+ "~ pil: package latest install",
508
+ "~ pil --choose-tool: re-pick tool before latest install",
509
+ "~ pil --choose-tool bun: choose the tool directly",
510
+ "~ pil --forget-tool: clear saved tool before latest install",
511
+ "~ pil --show-tool: show current tool before latest install",
512
+ "~ pil --show-tool --json: show current tool as JSON",
513
+ "~ pil --list-tools: list detected candidates",
514
+ "~ pil --list-tools --json: list candidates as JSON"
493
515
  ], {
494
516
  align: "left",
495
- width: 50,
517
+ width: 76,
496
518
  marginX: 2,
497
519
  marginY: 1,
498
520
  paddingX: 1,
@@ -522,9 +544,27 @@ async function detectNode() {
522
544
 
523
545
  //#endregion
524
546
  //#region src/pkgManager.ts
547
+ const resolvedToolCache = /* @__PURE__ */ new Map();
548
+ const toolIndicatorMap = {
549
+ pnpm: ["pnpm-workspace.yaml", "pnpm-lock.yaml"],
550
+ yarn: ["yarn.lock", ".yarnrc.yml"],
551
+ bun: ["bun.lock", "bun.lockb"],
552
+ npm: ["package-lock.json", "npm-shrinkwrap.json"]
553
+ };
554
+ const isZh$5 = process.env.PI_Lang === "zh";
555
+ const supportedPkgTools$1 = Object.keys(toolIndicatorMap);
556
+ function isEnabled(value) {
557
+ if (!value) return false;
558
+ const normalized = value.toLowerCase();
559
+ return normalized === "1" || normalized === "true" || normalized === "yes";
560
+ }
525
561
  function normalizeDir$1(dir) {
526
562
  return path.resolve(dir);
527
563
  }
564
+ function isSameOrInsideDir(base, target) {
565
+ const relative = path.relative(base, target);
566
+ return relative === "" || !relative.startsWith("..") && !path.isAbsolute(relative);
567
+ }
528
568
  function findUpSync$1(startDir, predicate) {
529
569
  let current = normalizeDir$1(startDir);
530
570
  while (true) {
@@ -534,24 +574,332 @@ function findUpSync$1(startDir, predicate) {
534
574
  current = parent;
535
575
  }
536
576
  }
537
- function inferToolFromRepoLayout(cwd) {
538
- if (findUpSync$1(cwd, (dir) => isFile(path.join(dir, "pnpm-workspace.yaml")) || isFile(path.join(dir, "pnpm-lock.yaml")))) return "pnpm";
539
- if (findUpSync$1(cwd, (dir) => isFile(path.join(dir, "yarn.lock")) || isFile(path.join(dir, ".yarnrc.yml")))) return "yarn";
540
- if (findUpSync$1(cwd, (dir) => isFile(path.join(dir, "bun.lockb")))) return "bun";
541
- return null;
542
- }
543
- async function resolvePkgTool() {
544
- let detected = await getPkgTool() || "npm";
545
- if (detected === "npm") {
546
- const inferred = inferToolFromRepoLayout(process.cwd());
547
- if (inferred) detected = inferred;
577
+ function findToolCandidate(cwd, tool) {
578
+ const indicators = toolIndicatorMap[tool];
579
+ if (!indicators?.length) return null;
580
+ const root = findUpSync$1(cwd, (dir) => indicators.some((indicator) => isFile(path.join(dir, indicator))));
581
+ if (!root) return null;
582
+ const foundIndicators = indicators.filter((indicator) => isFile(path.join(root, indicator)));
583
+ return {
584
+ tool,
585
+ root,
586
+ indicators: foundIndicators.length ? foundIndicators : indicators.slice(0, 1)
587
+ };
588
+ }
589
+ function getToolCandidates(cwd) {
590
+ return Object.keys(toolIndicatorMap).map((tool) => findToolCandidate(cwd, tool)).filter(Boolean);
591
+ }
592
+ function getPreferenceWorkspaceKey(cwd, candidates) {
593
+ const roots = [...new Set(candidates.map((candidate) => normalizeDir$1(candidate.root)))];
594
+ if (roots.length === 1) return roots[0];
595
+ return normalizeDir$1(cwd);
596
+ }
597
+ function findStoredWorkspaceKey(cwd, preferences) {
598
+ return Object.keys(preferences.workspaces).filter((workspaceKey) => isSameOrInsideDir(workspaceKey, cwd)).sort((a, b) => b.length - a.length)[0] || null;
599
+ }
600
+ function resolveWorkspaceKey(cwd, candidates, preferences) {
601
+ if (candidates.length > 0) return getPreferenceWorkspaceKey(cwd, candidates);
602
+ return findStoredWorkspaceKey(cwd, preferences) || normalizeDir$1(cwd);
603
+ }
604
+ function getConfigHome() {
605
+ const home = process.env.HOME || os.homedir();
606
+ if (process.platform === "win32") return process.env.APPDATA || path.join(home, "AppData", "Roaming");
607
+ return process.env.XDG_CONFIG_HOME || path.join(home, ".config");
608
+ }
609
+ function getWorkspaceToolPreferencePath() {
610
+ return path.join(getConfigHome(), "pi", "workspace-tools.json");
611
+ }
612
+ async function readWorkspaceToolPreferences() {
613
+ const configPath = getWorkspaceToolPreferencePath();
614
+ try {
615
+ const raw = await fs.readFile(configPath, "utf8");
616
+ const parsed = JSON.parse(raw);
617
+ return {
618
+ version: 1,
619
+ workspaces: parsed.workspaces && typeof parsed.workspaces === "object" ? parsed.workspaces : {}
620
+ };
621
+ } catch {
622
+ return {
623
+ version: 1,
624
+ workspaces: {}
625
+ };
548
626
  }
549
- const fallback = process.env.PI_DEFAULT;
627
+ }
628
+ async function writeWorkspaceToolPreference(workspaceKey, tool) {
629
+ const configPath = getWorkspaceToolPreferencePath();
630
+ const data = await readWorkspaceToolPreferences();
631
+ data.workspaces[workspaceKey] = tool;
632
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
633
+ await fs.writeFile(configPath, JSON.stringify(data, null, 2), "utf8");
634
+ resolvedToolCache.clear();
635
+ }
636
+ async function deleteWorkspaceToolPreference(workspaceKey) {
637
+ const configPath = getWorkspaceToolPreferencePath();
638
+ const data = await readWorkspaceToolPreferences();
639
+ if (!(workspaceKey in data.workspaces)) return;
640
+ delete data.workspaces[workspaceKey];
641
+ await fs.mkdir(path.dirname(configPath), { recursive: true });
642
+ await fs.writeFile(configPath, JSON.stringify(data, null, 2), "utf8");
643
+ resolvedToolCache.clear();
644
+ }
645
+ async function forgetPkgToolPreference() {
646
+ const workspaceKey = findStoredWorkspaceKey(normalizeDir$1(process.cwd()), await readWorkspaceToolPreferences());
647
+ if (!workspaceKey) return false;
648
+ await deleteWorkspaceToolPreference(workspaceKey);
649
+ return true;
650
+ }
651
+ function getSupportedPkgToolNames() {
652
+ return supportedPkgTools$1.slice();
653
+ }
654
+ function getPreferredToolFromEnv(candidates) {
655
+ const preferred = process.env.PI_DEFAULT;
656
+ if (!preferred) return null;
657
+ return candidates.some((candidate) => candidate.tool === preferred) ? preferred : null;
658
+ }
659
+ function getExplicitPreferredTool(value) {
660
+ if (!value) return null;
661
+ return supportedPkgTools$1.includes(value) ? value : null;
662
+ }
663
+ function validateExplicitPreferredTool(preferredTool, candidates) {
664
+ if (candidates.length === 0) return true;
665
+ return candidates.some((candidate) => candidate.tool === preferredTool);
666
+ }
667
+ function logInvalidPreferredTool(preferredTool, candidates) {
668
+ const names = candidates.map((candidate) => candidate.tool).join(", ");
669
+ console.log(color.red(isZh$5 ? `当前 workspace 可选的包管理器是: ${names},不能直接指定 ${preferredTool}。` : `This workspace supports: ${names}. ${preferredTool} cannot be selected directly here.`));
670
+ }
671
+ function getDetectedToolFallback(detected, candidates) {
672
+ if (candidates.some((candidate) => candidate.tool === detected)) return detected;
673
+ return candidates[0]?.tool || detected;
674
+ }
675
+ function formatCandidateLabel(candidate, cwd) {
676
+ const indicators = candidate.indicators.join(", ");
677
+ const relativeRoot = path.relative(cwd, candidate.root) || ".";
678
+ if (relativeRoot === ".") return `${candidate.tool}: ${indicators}`;
679
+ return `${candidate.tool}: ${indicators} (${relativeRoot})`;
680
+ }
681
+ function toCandidateInfo(candidates) {
682
+ return candidates.map((candidate) => ({
683
+ tool: candidate.tool,
684
+ indicators: candidate.indicators,
685
+ root: candidate.root
686
+ }));
687
+ }
688
+ async function selectToolCandidate(candidates, cwd) {
689
+ if (!isInteractive()) return { status: "unavailable" };
690
+ const options = candidates.map((candidate) => formatCandidateLabel(candidate, cwd));
691
+ const labelToTool = new Map(candidates.map((candidate) => [formatCandidateLabel(candidate, cwd), candidate.tool]));
692
+ const choice = await ttySelect(options, isZh$5 ? "🤔检测到多个包管理环境,请选择当前 workspace 使用的安装方式" : "Multiple package managers were detected, choose one for this workspace.");
693
+ if (!choice) return { status: "cancelled" };
694
+ const tool = labelToTool.get(choice);
695
+ if (!tool) return { status: "cancelled" };
550
696
  return {
697
+ status: "selected",
698
+ tool
699
+ };
700
+ }
701
+ function logAmbiguousToolFallback(candidates, tool) {
702
+ const names = candidates.map((candidate) => candidate.tool).join(", ");
703
+ console.log(color.yellow(isZh$5 ? `检测到多个包管理环境(${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.`));
704
+ }
705
+ function getSourceLabel(source) {
706
+ switch (source) {
707
+ case "saved-preference": return isZh$5 ? "已保存的 workspace 选择" : "saved workspace choice";
708
+ case "fresh-selection": return isZh$5 ? "本次重新选择并保存" : "picked now and saved";
709
+ case "single-candidate": return isZh$5 ? "当前只检测到一种包管理器标记" : "single detected package-manager indicator";
710
+ case "env-default": return isZh$5 ? "PI_DEFAULT 兜底" : "PI_DEFAULT fallback";
711
+ case "detected-tool": return isZh$5 ? "环境自动检测结果" : "environment auto-detection";
712
+ case "non-interactive-fallback": return isZh$5 ? "非交互终端下的临时兜底" : "non-interactive fallback";
713
+ }
714
+ }
715
+ function logWorkspaceToolSelected(tool, forceChoose) {
716
+ console.log(color.green(forceChoose ? isZh$5 ? `当前 workspace 已切换为使用 ${tool},并保存了这个选择。` : `This workspace now uses ${tool}, and the choice has been saved.` : isZh$5 ? `已为当前 workspace 记住 ${tool} 作为包管理器。` : `Saved ${tool} as the package manager for this workspace.`));
717
+ }
718
+ function logWorkspaceToolResolved(tool) {
719
+ console.log(color.green(isZh$5 ? `当前 workspace 使用 ${tool} 作为包管理器。` : `This workspace uses ${tool} as the package manager.`));
720
+ }
721
+ function logStaleWorkspaceToolRemoved(tool) {
722
+ console.log(color.yellow(isZh$5 ? `检测到之前保存的 ${tool} 已不再适用于当前 workspace,已自动清除旧记录。` : `The saved ${tool} choice no longer matches this workspace and was removed automatically.`));
723
+ }
724
+ async function preparePkgToolContext(forgetPreference = false) {
725
+ const cwd = normalizeDir$1(process.cwd());
726
+ const originalDetected = await getPkgTool() || "npm";
727
+ const candidates = getToolCandidates(cwd);
728
+ const preferences = await readWorkspaceToolPreferences();
729
+ const workspaceKey = resolveWorkspaceKey(cwd, candidates, preferences);
730
+ let detected = originalDetected;
731
+ if (forgetPreference) await deleteWorkspaceToolPreference(workspaceKey);
732
+ if (forgetPreference) delete preferences.workspaces[workspaceKey];
733
+ let rememberedTool = preferences.workspaces[workspaceKey];
734
+ if (rememberedTool && !candidates.some((candidate) => candidate.tool === rememberedTool)) {
735
+ await deleteWorkspaceToolPreference(workspaceKey);
736
+ delete preferences.workspaces[workspaceKey];
737
+ logStaleWorkspaceToolRemoved(rememberedTool);
738
+ rememberedTool = void 0;
739
+ }
740
+ if (detected === "npm" && candidates.length === 1) detected = candidates[0].tool;
741
+ return {
742
+ detected,
743
+ candidates,
744
+ rememberedTool
745
+ };
746
+ }
747
+ async function getPkgToolStatus() {
748
+ const { detected, candidates, rememberedTool } = await preparePkgToolContext();
749
+ const candidateInfo = toCandidateInfo(candidates);
750
+ if (rememberedTool) return {
751
+ status: "resolved",
752
+ detected,
753
+ tool: rememberedTool,
754
+ source: "saved-preference",
755
+ candidates: candidateInfo
756
+ };
757
+ if (candidates.length <= 1) {
758
+ const fallback = process.env.PI_DEFAULT;
759
+ return {
760
+ status: "resolved",
761
+ detected,
762
+ tool: detected === "npm" && fallback ? fallback : detected,
763
+ source: detected === "npm" && fallback ? "env-default" : candidates.length === 1 ? "single-candidate" : "detected-tool",
764
+ candidates: candidateInfo
765
+ };
766
+ }
767
+ const envPreferredTool = getPreferredToolFromEnv(candidates);
768
+ if (envPreferredTool) return {
769
+ status: "resolved",
551
770
  detected,
552
- tool: detected === "npm" && fallback ? fallback : detected
771
+ tool: envPreferredTool,
772
+ source: "env-default",
773
+ candidates: candidateInfo
774
+ };
775
+ if (!isInteractive()) return {
776
+ status: "resolved",
777
+ detected,
778
+ tool: getDetectedToolFallback(detected, candidates),
779
+ source: "non-interactive-fallback",
780
+ candidates: candidateInfo
781
+ };
782
+ return {
783
+ status: "needs-selection",
784
+ detected,
785
+ candidates: candidateInfo
553
786
  };
554
787
  }
788
+ function printPkgToolStatus(status, options = {}) {
789
+ if (options.json) {
790
+ console.log(JSON.stringify({
791
+ ...status,
792
+ sourceLabel: status.status === "resolved" ? getSourceLabel(status.source) : void 0
793
+ }, null, 2));
794
+ return;
795
+ }
796
+ if (status.status === "resolved") {
797
+ const candidateNames = status.candidates.map((candidate) => candidate.tool).join(", ");
798
+ console.log(color.green(isZh$5 ? `当前 workspace 使用 ${status.tool} 作为包管理器。` : `This workspace uses ${status.tool} as the package manager.`));
799
+ console.log(color.cyan(`${isZh$5 ? "来源" : "Source"}: ${getSourceLabel(status.source)}`));
800
+ if (candidateNames) console.log(color.dim(`${isZh$5 ? "候选项" : "Candidates"}: ${candidateNames}`));
801
+ return;
802
+ }
803
+ const candidateNames = status.candidates.map((candidate) => candidate.tool).join(", ");
804
+ console.log(color.yellow(isZh$5 ? "当前 workspace 还没有固定包管理器选择。" : "This workspace does not have a locked package-manager choice yet."));
805
+ console.log(color.cyan(`${isZh$5 ? "原因" : "Reason"}: ${isZh$5 ? "检测到了多个包管理器标记,且当前没有保存的选择。" : "Multiple package-manager indicators were found and no saved choice exists yet."}`));
806
+ console.log(color.dim(`${isZh$5 ? "候选项" : "Candidates"}: ${candidateNames}`));
807
+ console.log(color.dim(isZh$5 ? "可执行 `pi --choose-tool` 来保存当前 workspace 的选择。" : "Run `pi --choose-tool` to save a choice for this workspace."));
808
+ }
809
+ function printPkgToolCandidates(status, options = {}) {
810
+ if (options.json) {
811
+ console.log(JSON.stringify({
812
+ ...status,
813
+ sourceLabel: status.status === "resolved" ? getSourceLabel(status.source) : void 0
814
+ }, null, 2));
815
+ return;
816
+ }
817
+ if (status.status === "resolved") {
818
+ console.log(color.green(isZh$5 ? `当前 workspace 使用 ${status.tool} 作为包管理器。` : `This workspace uses ${status.tool} as the package manager.`));
819
+ console.log(color.cyan(`${isZh$5 ? "来源" : "Source"}: ${getSourceLabel(status.source)}`));
820
+ } else console.log(color.yellow(isZh$5 ? "当前 workspace 还没有固定包管理器选择。" : "This workspace does not have a locked package-manager choice yet."));
821
+ if (status.candidates.length === 0) {
822
+ console.log(color.dim(isZh$5 ? "当前 workspace 没有检测到明确的 lockfile / workspace 候选。" : "No explicit lockfile or workspace candidates were detected in this workspace."));
823
+ return;
824
+ }
825
+ console.log(color.bold(isZh$5 ? "候选工具:" : "Candidate tools:"));
826
+ for (const candidate of status.candidates) {
827
+ const indicators = candidate.indicators.join(", ");
828
+ console.log(`- ${candidate.tool}`);
829
+ console.log(color.dim(` ${isZh$5 ? "root" : "root"}: ${candidate.root}`));
830
+ console.log(color.dim(` ${isZh$5 ? "indicators" : "indicators"}: ${indicators}`));
831
+ }
832
+ }
833
+ async function resolvePkgTool(options = {}) {
834
+ const cwd = normalizeDir$1(process.cwd());
835
+ const forceChoose = options.forceChoose || isEnabled(process.env.PI_FORCE_PICK_TOOL);
836
+ const forgetPreference = options.forgetPreference || isEnabled(process.env.PI_FORGET_PICK_TOOL);
837
+ const preferredTool = getExplicitPreferredTool(options.preferredTool || process.env.PI_PREFERRED_TOOL);
838
+ const shouldBypassCache = forceChoose || forgetPreference || Boolean(preferredTool);
839
+ const cached = resolvedToolCache.get(cwd);
840
+ if (!shouldBypassCache && cached) return cached;
841
+ const pending = (async () => {
842
+ const { detected, candidates, rememberedTool } = await preparePkgToolContext(forgetPreference);
843
+ if (preferredTool) {
844
+ if (!validateExplicitPreferredTool(preferredTool, candidates)) {
845
+ logInvalidPreferredTool(preferredTool, candidates);
846
+ process.exit(1);
847
+ }
848
+ await writeWorkspaceToolPreference(resolveWorkspaceKey(normalizeDir$1(process.cwd()), candidates, await readWorkspaceToolPreferences()), preferredTool);
849
+ logWorkspaceToolSelected(preferredTool, true);
850
+ return {
851
+ detected,
852
+ tool: preferredTool,
853
+ source: "fresh-selection"
854
+ };
855
+ }
856
+ if (!forceChoose && rememberedTool) return {
857
+ detected,
858
+ tool: rememberedTool,
859
+ source: "saved-preference"
860
+ };
861
+ if (candidates.length <= 1) {
862
+ const fallback = process.env.PI_DEFAULT;
863
+ const tool = detected === "npm" && fallback ? fallback : detected;
864
+ if (forceChoose) logWorkspaceToolResolved(tool);
865
+ return {
866
+ detected,
867
+ tool,
868
+ source: detected === "npm" && fallback ? "env-default" : candidates.length === 1 ? "single-candidate" : "detected-tool"
869
+ };
870
+ }
871
+ const envPreferredTool = getPreferredToolFromEnv(candidates);
872
+ if (!forceChoose && envPreferredTool) return {
873
+ detected,
874
+ tool: envPreferredTool,
875
+ source: "env-default"
876
+ };
877
+ const cwdForSelection = normalizeDir$1(process.cwd());
878
+ const selection = await selectToolCandidate(candidates, cwd);
879
+ if (selection.status === "selected") {
880
+ await writeWorkspaceToolPreference(resolveWorkspaceKey(cwdForSelection, candidates, await readWorkspaceToolPreferences()), selection.tool);
881
+ logWorkspaceToolSelected(selection.tool, forceChoose);
882
+ return {
883
+ detected,
884
+ tool: selection.tool,
885
+ source: "fresh-selection"
886
+ };
887
+ }
888
+ if (selection.status === "cancelled") {
889
+ console.log(color.dim(isZh$5 ? "已取消" : "Cancelled"));
890
+ process.exit(0);
891
+ }
892
+ const tool = getDetectedToolFallback(detected, candidates);
893
+ logAmbiguousToolFallback(candidates, tool);
894
+ return {
895
+ detected,
896
+ tool,
897
+ source: "non-interactive-fallback"
898
+ };
899
+ })();
900
+ resolvedToolCache.set(cwd, pending);
901
+ return pending;
902
+ }
555
903
  function getInstallCommand(tool, hasParams) {
556
904
  const action = hasParams ? "add" : "install";
557
905
  switch (tool) {
@@ -609,12 +957,7 @@ async function findUpAsync(startDir, predicate) {
609
957
  async function getParams(params) {
610
958
  const cwd = process.cwd();
611
959
  try {
612
- let tool = await getPkgTool() || "npm";
613
- if (tool === "npm") {
614
- if (findUpSync(cwd, (dir) => isFile(path.join(dir, "pnpm-workspace.yaml")) || isFile(path.join(dir, "pnpm-lock.yaml")))) tool = "pnpm";
615
- else if (findUpSync(cwd, (dir) => isFile(path.join(dir, "yarn.lock")) || isFile(path.join(dir, ".yarnrc.yml")))) tool = "yarn";
616
- else if (findUpSync(cwd, (dir) => isFile(path.join(dir, "bun.lockb")))) tool = "bun";
617
- }
960
+ const { tool } = await resolvePkgTool();
618
961
  switch (tool) {
619
962
  case "pnpm": {
620
963
  const pnpmWorkspaceRoot = findUpSync(cwd, (dir) => isFile(path.join(dir, "pnpm-workspace.yaml")));
@@ -737,11 +1080,11 @@ async function pushHistory(command) {
737
1080
  historyFormat = "bash";
738
1081
  }
739
1082
  try {
740
- if (!fs.existsSync(historyFile)) {
1083
+ if (!fs$1.existsSync(historyFile)) {
741
1084
  log$1(color.yellow(`${isZh$4 ? `未找到 ${shellName} 历史文件` : `${shellName} history file not found`}`));
742
1085
  return;
743
1086
  }
744
- const raw = fs.readFileSync(historyFile, "utf8");
1087
+ const raw = fs$1.readFileSync(historyFile, "utf8");
745
1088
  const timestamp = Math.floor(Date.now() / 1e3);
746
1089
  let newEntry = "";
747
1090
  if (historyFormat === "zsh") newEntry = `: ${timestamp}:0;${command}`;
@@ -824,8 +1167,8 @@ async function pushHistory(command) {
824
1167
  if (historyFormat === "fish") finalContent = `${newEntries.map((e) => e.trimEnd()).join("\n")}\n`;
825
1168
  else finalContent = `${newEntries.join("\n")}\n`;
826
1169
  const tmpPath = `${historyFile}.ccommand.tmp`;
827
- fs.writeFileSync(tmpPath, finalContent, "utf8");
828
- fs.renameSync(tmpPath, historyFile);
1170
+ fs$1.writeFileSync(tmpPath, finalContent, "utf8");
1171
+ fs$1.renameSync(tmpPath, historyFile);
829
1172
  } catch (err) {
830
1173
  log$1(color.red(`${isZh$4 ? `❌ 添加到 ${shellName} 历史记录失败` : `❌ Failed to add to ${shellName} history`}${err ? `: ${String(err)}` : ""}`));
831
1174
  }
@@ -851,9 +1194,9 @@ async function pi(params, pkg, executor = "pi") {
851
1194
  ];
852
1195
  let loading_status;
853
1196
  const { PI_DEFAULT, PI_MaxSockets: sockets } = process.env;
854
- const { detected, tool } = await resolvePkgTool();
1197
+ const { tool } = await resolvePkgTool();
855
1198
  const maxSockets = sockets || 4;
856
- if (detected === "npm" && !PI_DEFAULT) stdio = "inherit";
1199
+ if (tool === "npm" && !PI_DEFAULT) stdio = "inherit";
857
1200
  else loading_status = await loading(text, isSilent);
858
1201
  executor = getInstallCommand(tool, Boolean(params));
859
1202
  const newParams = isLatest ? "" : await getParams(params);
@@ -921,16 +1264,419 @@ function getCcommand() {
921
1264
  }
922
1265
 
923
1266
  //#endregion
924
- //#region src/pfind.ts
1267
+ //#region src/prun.ts
1268
+ async function prun(params) {
1269
+ ensurePrunAutoInit();
1270
+ const hadNoHistoryEnv = process.env.CCOMMAND_NO_HISTORY != null || process.env.NO_HISTORY != null;
1271
+ const initialNoHistory = process.env.CCOMMAND_NO_HISTORY ?? process.env.NO_HISTORY;
1272
+ const prevNoHistory = process.env.CCOMMAND_NO_HISTORY;
1273
+ if (!(hadNoHistoryEnv && isNoHistory$1(initialNoHistory))) delete process.env.CCOMMAND_NO_HISTORY;
1274
+ else process.env.CCOMMAND_NO_HISTORY = "1";
1275
+ const { ccommand } = getCcommand();
1276
+ try {
1277
+ await ccommand(params);
1278
+ } finally {
1279
+ if (prevNoHistory == null) delete process.env.CCOMMAND_NO_HISTORY;
1280
+ else process.env.CCOMMAND_NO_HISTORY = prevNoHistory;
1281
+ }
1282
+ }
1283
+ const isZh$2 = process.env.PI_Lang === "zh";
1284
+ const safeShellValue = /^[\w./:@%+=,-]+$/;
925
1285
  function isNoHistory$1(value) {
926
1286
  if (!value) return false;
927
1287
  const normalized = value.toLowerCase();
928
1288
  return normalized === "1" || normalized === "true" || normalized === "yes";
929
1289
  }
1290
+ function shellQuote(value) {
1291
+ if (value === "") return "''";
1292
+ if (safeShellValue.test(value)) return value;
1293
+ return `'${value.replace(/'/g, `'\\''`)}'`;
1294
+ }
1295
+ function powerShellQuote(value) {
1296
+ if (value === "") return "''";
1297
+ return `'${value.replace(/'/g, "''")}'`;
1298
+ }
1299
+ function splitCommand(value) {
1300
+ const parts = [];
1301
+ let current = "";
1302
+ let quote = null;
1303
+ let hasValue = false;
1304
+ const pushCurrent = () => {
1305
+ if (!hasValue) return;
1306
+ parts.push(current);
1307
+ current = "";
1308
+ hasValue = false;
1309
+ };
1310
+ for (let i = 0; i < value.length; i++) {
1311
+ const char = value[i];
1312
+ if (quote) {
1313
+ if (char === quote) {
1314
+ quote = null;
1315
+ hasValue = true;
1316
+ continue;
1317
+ }
1318
+ if (quote === "\"" && char === "\\") {
1319
+ const next = value[i + 1];
1320
+ if (next) {
1321
+ current += next;
1322
+ hasValue = true;
1323
+ i++;
1324
+ continue;
1325
+ }
1326
+ }
1327
+ current += char;
1328
+ hasValue = true;
1329
+ continue;
1330
+ }
1331
+ if (char === "\"" || char === "'") {
1332
+ quote = char;
1333
+ hasValue = true;
1334
+ continue;
1335
+ }
1336
+ if (/\s/.test(char)) {
1337
+ pushCurrent();
1338
+ while (i + 1 < value.length && /\s/.test(value[i + 1])) i++;
1339
+ continue;
1340
+ }
1341
+ if (char === "\\") {
1342
+ const next = value[i + 1];
1343
+ if (next) {
1344
+ current += next;
1345
+ hasValue = true;
1346
+ i++;
1347
+ continue;
1348
+ }
1349
+ }
1350
+ current += char;
1351
+ hasValue = true;
1352
+ }
1353
+ pushCurrent();
1354
+ return parts;
1355
+ }
1356
+ function normalizeShellName(value) {
1357
+ const shell = (value || "").toLowerCase().replace(/\.exe$/, "");
1358
+ if (shell === "powershell") return "powershell";
1359
+ if (shell === "pwsh") return "pwsh";
1360
+ if (shell === "fish" || shell === "zsh" || shell === "bash") return shell;
1361
+ return shell;
1362
+ }
1363
+ function detectShell() {
1364
+ const envShell = normalizeShellName(path.basename(process.env.SHELL || ""));
1365
+ if (process.env.FISH_VERSION) return "fish";
1366
+ if (process.env.ZSH_VERSION) return "zsh";
1367
+ if (process.env.BASH_VERSION) return "bash";
1368
+ if (process.env.POWERSHELL_DISTRIBUTION_CHANNEL) return "pwsh";
1369
+ if (envShell) return envShell;
1370
+ if (process.platform === "win32") return "powershell";
1371
+ return "zsh";
1372
+ }
1373
+ function getPowerShellProfilePath(shell, home) {
1374
+ if (process.platform === "win32") {
1375
+ const documentsHome = process.env.USERPROFILE || home;
1376
+ const profileDir = shell === "pwsh" ? "PowerShell" : "WindowsPowerShell";
1377
+ return path.join(documentsHome, "Documents", profileDir, "Microsoft.PowerShell_profile.ps1");
1378
+ }
1379
+ const configHome = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
1380
+ return path.join(configHome, "powershell", "Microsoft.PowerShell_profile.ps1");
1381
+ }
1382
+ function ensurePrunAutoInit() {
1383
+ if (!shouldAutoInit()) return;
1384
+ const shell = detectShell();
1385
+ const home = process.env.HOME || os.homedir();
1386
+ let rcFile = "";
1387
+ let initLine = "";
1388
+ if (shell === "zsh") {
1389
+ const zdotdir = process.env.ZDOTDIR || home;
1390
+ rcFile = path.join(zdotdir, ".zshrc");
1391
+ initLine = "eval \"$(prun --init zsh)\"";
1392
+ } else if (shell === "bash") {
1393
+ rcFile = path.join(home, ".bashrc");
1394
+ initLine = "eval \"$(prun --init bash)\"";
1395
+ } else if (shell === "fish") {
1396
+ const configHome = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
1397
+ rcFile = path.join(configHome, "fish", "config.fish");
1398
+ initLine = "prun --init fish | source";
1399
+ } else if (shell === "powershell" || shell === "pwsh") {
1400
+ rcFile = getPowerShellProfilePath(shell, home);
1401
+ initLine = `prun --init ${shell} | Out-String | Invoke-Expression`;
1402
+ } else return;
1403
+ try {
1404
+ const dir = path.dirname(rcFile);
1405
+ if (!fs$1.existsSync(dir)) fs$1.mkdirSync(dir, { recursive: true });
1406
+ const content = fs$1.existsSync(rcFile) ? fs$1.readFileSync(rcFile, "utf8") : "";
1407
+ if (!/prun\s+--init/.test(content)) {
1408
+ const prefix = content.length && !content.endsWith("\n") ? "\n" : "";
1409
+ fs$1.appendFileSync(rcFile, `${prefix}${initLine}\n`, "utf8");
1410
+ }
1411
+ } catch {}
1412
+ }
1413
+ function shouldAutoInit() {
1414
+ const auto = process.env.PI_AUTO_INIT || process.env.PRUN_AUTO_INIT;
1415
+ if (auto != null) return isNoHistory$1(auto);
1416
+ if (isNoHistory$1(process.env.PI_NO_AUTO_INIT || process.env.PRUN_NO_AUTO_INIT)) return false;
1417
+ if (process.env.CI) return false;
1418
+ if (!process.stdout.isTTY || !process.stdin.isTTY) return false;
1419
+ return true;
1420
+ }
1421
+ function printPrunInit(args = []) {
1422
+ const shellArg = normalizeShellName(args[0]);
1423
+ const binArg = args[1] || process.env.PRUN_BIN || "prun";
1424
+ const bin = shellQuote(binArg);
1425
+ const shell = shellArg || detectShell() || "zsh";
1426
+ const historyHintExpr = "${CCOMMAND_HISTORY_HINT:-${XDG_CACHE_HOME:-$HOME/.cache}/ccommand/last-history}";
1427
+ let script = "";
1428
+ if (shell === "zsh") script = [
1429
+ "prun() {",
1430
+ ` local bin=${bin}`,
1431
+ " local -a cmd",
1432
+ " cmd=(${=bin})",
1433
+ " command \"${cmd[@]}\" \"$@\"",
1434
+ "}",
1435
+ "__prun_sync_history() {",
1436
+ " local history_disable=${CCOMMAND_NO_HISTORY:-${NO_HISTORY:-\"\"}}",
1437
+ " local history_disable_lower=${history_disable:l}",
1438
+ " if [[ $history_disable_lower == \"1\" || $history_disable_lower == \"true\" || $history_disable_lower == \"yes\" ]]; then",
1439
+ " return",
1440
+ " fi",
1441
+ ` local history_hint=${historyHintExpr}`,
1442
+ " if [[ ! -f $history_hint ]]; then",
1443
+ " return",
1444
+ " fi",
1445
+ " local line",
1446
+ " line=$(<\"$history_hint\")",
1447
+ " local hint_ts=${line%%$'\\t'*}",
1448
+ " local hint_cmd=${line#*$'\\t'}",
1449
+ " if [[ -z $hint_ts || $hint_ts == $line ]]; then",
1450
+ " hint_cmd=$line",
1451
+ " hint_ts=\"\"",
1452
+ " fi",
1453
+ " if [[ -n $hint_ts && $hint_ts == ${__PRUN_HISTORY_HINT_TS:-\"\"} ]]; then",
1454
+ " return",
1455
+ " fi",
1456
+ " __PRUN_HISTORY_HINT_TS=$hint_ts",
1457
+ " fc -R",
1458
+ " if [[ $hint_cmd != pfind* && $hint_cmd != prun* ]]; then",
1459
+ " return",
1460
+ " fi",
1461
+ " local last_line",
1462
+ " last_line=$(fc -l -1 2>/dev/null)",
1463
+ " local last_cmd",
1464
+ " last_cmd=$(printf \"%s\" \"$last_line\" | sed -E \"s/^[[:space:]]*[0-9]+[[:space:]]*//\")",
1465
+ " if [[ $last_cmd == \"$hint_cmd\" ]]; then",
1466
+ " return",
1467
+ " fi",
1468
+ " if [[ $last_cmd == prun || $last_cmd == prun\\ * ]]; then",
1469
+ " local last_num",
1470
+ " last_num=$(printf \"%s\" \"$last_line\" | sed -E \"s/^[[:space:]]*([0-9]+).*/\\1/\")",
1471
+ " if [[ -n $last_num ]]; then",
1472
+ " history -d $last_num 2>/dev/null",
1473
+ " fi",
1474
+ " fi",
1475
+ " print -s -- \"$hint_cmd\"",
1476
+ "}",
1477
+ "",
1478
+ "if ! typeset -f __prun_precmd >/dev/null; then",
1479
+ " __prun_precmd() { __prun_sync_history }",
1480
+ " autoload -Uz add-zsh-hook",
1481
+ " add-zsh-hook precmd __prun_precmd",
1482
+ "fi"
1483
+ ].join("\n");
1484
+ else if (shell === "bash") script = [
1485
+ "prun() {",
1486
+ ` local bin=${bin}`,
1487
+ " local -a cmd",
1488
+ " read -r -a cmd <<< \"$bin\"",
1489
+ " command \"${cmd[@]}\" \"$@\"",
1490
+ "}",
1491
+ "__prun_sync_history() {",
1492
+ " local history_disable=${CCOMMAND_NO_HISTORY:-${NO_HISTORY:-\"\"}}",
1493
+ " local history_disable_lower",
1494
+ " history_disable_lower=$(printf '%s' \"$history_disable\" | tr '[:upper:]' '[:lower:]')",
1495
+ " if [[ $history_disable_lower == \"1\" || $history_disable_lower == \"true\" || $history_disable_lower == \"yes\" ]]; then",
1496
+ " return",
1497
+ " fi",
1498
+ ` local history_hint=${historyHintExpr}`,
1499
+ " if [[ ! -f $history_hint ]]; then",
1500
+ " return",
1501
+ " fi",
1502
+ " local line",
1503
+ " line=$(<\"$history_hint\")",
1504
+ " local hint_ts=\"${line%%$'\\t'*}\"",
1505
+ " local hint_cmd=\"${line#*$'\\t'}\"",
1506
+ " if [[ -z $hint_ts || $hint_ts == \"$line\" ]]; then",
1507
+ " hint_cmd=\"$line\"",
1508
+ " hint_ts=\"\"",
1509
+ " fi",
1510
+ " if [[ -n $hint_ts && $hint_ts == \"${__PRUN_HISTORY_HINT_TS:-}\" ]]; then",
1511
+ " return",
1512
+ " fi",
1513
+ " __PRUN_HISTORY_HINT_TS=$hint_ts",
1514
+ " if [[ $hint_cmd != pfind* && $hint_cmd != prun* ]]; then",
1515
+ " return",
1516
+ " fi",
1517
+ " history -n",
1518
+ " local last_line",
1519
+ " last_line=$(history 1)",
1520
+ " local last_cmd",
1521
+ " last_cmd=$(printf \"%s\" \"$last_line\" | sed -E \"s/^[[:space:]]*[0-9]+[[:space:]]*//\")",
1522
+ " if [[ $last_cmd == \"$hint_cmd\" ]]; then",
1523
+ " return",
1524
+ " fi",
1525
+ " if [[ $last_cmd == prun || $last_cmd == prun\\ * ]]; then",
1526
+ " local last_num",
1527
+ " last_num=$(printf \"%s\" \"$last_line\" | sed -E \"s/^[[:space:]]*([0-9]+).*/\\1/\")",
1528
+ " if [[ -n $last_num ]]; then",
1529
+ " history -d \"$last_num\" 2>/dev/null",
1530
+ " fi",
1531
+ " fi",
1532
+ " history -s -- \"$hint_cmd\"",
1533
+ "}",
1534
+ "",
1535
+ "if [[ -z \"${__PRUN_PROMPT_INSTALLED:-}\" ]]; then",
1536
+ " __PRUN_PROMPT_INSTALLED=1",
1537
+ " if [[ -n \"${PROMPT_COMMAND:-}\" ]]; then",
1538
+ " PROMPT_COMMAND=\"__prun_sync_history;${PROMPT_COMMAND}\"",
1539
+ " else",
1540
+ " PROMPT_COMMAND=\"__prun_sync_history\"",
1541
+ " fi",
1542
+ "fi"
1543
+ ].join("\n");
1544
+ else if (shell === "fish") script = [
1545
+ "function prun",
1546
+ ` set -l bin ${bin}`,
1547
+ " set -l cmd (string split -- \" \" $bin)",
1548
+ " command $cmd $argv",
1549
+ " set -l history_disable $CCOMMAND_NO_HISTORY",
1550
+ " if test -z \"$history_disable\"",
1551
+ " set history_disable $NO_HISTORY",
1552
+ " end",
1553
+ " set history_disable (string lower -- (string trim -- \"$history_disable\"))",
1554
+ " if test \"$history_disable\" != \"1\" -a \"$history_disable\" != \"true\" -a \"$history_disable\" != \"yes\"",
1555
+ " history --merge",
1556
+ " set -l history_hint $CCOMMAND_HISTORY_HINT",
1557
+ " if test -z \"$history_hint\"",
1558
+ " set -l cache_home $XDG_CACHE_HOME",
1559
+ " if test -z \"$cache_home\"",
1560
+ " set cache_home \"$HOME/.cache\"",
1561
+ " end",
1562
+ " set history_hint \"$cache_home/ccommand/last-history\"",
1563
+ " end",
1564
+ " if test -f \"$history_hint\"",
1565
+ " set -l last_cmd (string trim -- (cat \"$history_hint\"))",
1566
+ " set -l last_cmd (string replace -r \"^[0-9]+\\t\" \"\" -- \"$last_cmd\")",
1567
+ " if string match -q \"pfind*\" -- \"$last_cmd\"; or string match -q \"prun*\" -- \"$last_cmd\"",
1568
+ " set -l last_hist (history --max=1)",
1569
+ " if test \"$last_hist\" != \"$last_cmd\"",
1570
+ " history add -- \"$last_cmd\"",
1571
+ " end",
1572
+ " end",
1573
+ " end",
1574
+ " end",
1575
+ "end"
1576
+ ].join("\n");
1577
+ else if (shell === "powershell" || shell === "pwsh") script = [
1578
+ `$script:__prun_bin = @(${splitCommand(binArg).map(powerShellQuote).join(", ") || powerShellQuote("prun")})`,
1579
+ "",
1580
+ "function global:prun {",
1581
+ " param([Parameter(ValueFromRemainingArguments = $true)] [string[]] $CliArgs)",
1582
+ " $command = $script:__prun_bin[0]",
1583
+ " if ($command -ieq \"prun\") {",
1584
+ " $resolved = Get-Command -Name $command -CommandType Application,ExternalScript -ErrorAction SilentlyContinue | Select-Object -First 1",
1585
+ " if ($null -ne $resolved) {",
1586
+ " if ($resolved.Path) { $command = $resolved.Path }",
1587
+ " elseif ($resolved.Definition) { $command = $resolved.Definition }",
1588
+ " elseif ($resolved.Source) { $command = $resolved.Source }",
1589
+ " }",
1590
+ " }",
1591
+ " $extra = @()",
1592
+ " if ($script:__prun_bin.Count -gt 1) {",
1593
+ " $extra = $script:__prun_bin[1..($script:__prun_bin.Count - 1)]",
1594
+ " }",
1595
+ " & $command @extra @CliArgs",
1596
+ "}",
1597
+ "",
1598
+ "function global:__prun_sync_history {",
1599
+ " $historyDisable = $env:CCOMMAND_NO_HISTORY",
1600
+ " if ([string]::IsNullOrWhiteSpace($historyDisable)) {",
1601
+ " $historyDisable = $env:NO_HISTORY",
1602
+ " }",
1603
+ " $historyDisable = \"$historyDisable\".Trim().ToLowerInvariant()",
1604
+ " if ($historyDisable -eq \"1\" -or $historyDisable -eq \"true\" -or $historyDisable -eq \"yes\") {",
1605
+ " return",
1606
+ " }",
1607
+ " $historyHint = $env:CCOMMAND_HISTORY_HINT",
1608
+ " if ([string]::IsNullOrWhiteSpace($historyHint)) {",
1609
+ " $cacheHome = $env:XDG_CACHE_HOME",
1610
+ " if ([string]::IsNullOrWhiteSpace($cacheHome)) {",
1611
+ " $cacheHome = Join-Path $HOME \".cache\"",
1612
+ " }",
1613
+ " $historyHint = Join-Path (Join-Path $cacheHome \"ccommand\") \"last-history\"",
1614
+ " }",
1615
+ " if (-not (Test-Path -LiteralPath $historyHint)) {",
1616
+ " return",
1617
+ " }",
1618
+ " $line = (Get-Content -LiteralPath $historyHint -Raw -ErrorAction SilentlyContinue)",
1619
+ " if ([string]::IsNullOrWhiteSpace($line)) {",
1620
+ " return",
1621
+ " }",
1622
+ " $line = $line.Trim()",
1623
+ " $hintTs = \"\"",
1624
+ " $hintCmd = $line",
1625
+ " $parts = $line -split \"`t\", 2",
1626
+ " if ($parts.Count -eq 2 -and $parts[0] -match \"^\\d+$\") {",
1627
+ " $hintTs = $parts[0]",
1628
+ " $hintCmd = $parts[1]",
1629
+ " }",
1630
+ " if (-not $hintCmd.StartsWith(\"prun\") -and -not $hintCmd.StartsWith(\"pfind\")) {",
1631
+ " return",
1632
+ " }",
1633
+ " if ($hintTs -and $script:__PRUN_HISTORY_HINT_TS -eq $hintTs) {",
1634
+ " return",
1635
+ " }",
1636
+ " $script:__PRUN_HISTORY_HINT_TS = $hintTs",
1637
+ " $psReadLineType = \"Microsoft.PowerShell.PSConsoleReadLine\" -as [type]",
1638
+ " if ($null -eq $psReadLineType) {",
1639
+ " return",
1640
+ " }",
1641
+ " $psReadLineType::AddToHistory($hintCmd)",
1642
+ "}",
1643
+ "",
1644
+ "if (-not $script:__PRUN_PROMPT_INSTALLED) {",
1645
+ " $script:__PRUN_PROMPT_INSTALLED = $true",
1646
+ " if (-not $script:__prun_original_prompt) {",
1647
+ " if (Test-Path Function:\\prompt) {",
1648
+ " $script:__prun_original_prompt = $function:prompt",
1649
+ " }",
1650
+ " }",
1651
+ " function global:prompt {",
1652
+ " __prun_sync_history",
1653
+ " if ($script:__prun_original_prompt) {",
1654
+ " & $script:__prun_original_prompt",
1655
+ " return",
1656
+ " }",
1657
+ " \"PS $($executionContext.SessionState.Path.CurrentLocation)> \"",
1658
+ " }",
1659
+ "}"
1660
+ ].join("\n");
1661
+ else {
1662
+ console.log(color.red(isZh$2 ? `不支持的 shell: ${shell}` : `Unsupported shell: ${shell}`));
1663
+ return;
1664
+ }
1665
+ console.log(script);
1666
+ }
1667
+
1668
+ //#endregion
1669
+ //#region src/pfind.ts
1670
+ function isNoHistory(value) {
1671
+ if (!value) return false;
1672
+ const normalized = value.toLowerCase();
1673
+ return normalized === "1" || normalized === "true" || normalized === "yes";
1674
+ }
930
1675
  async function pfind(params) {
1676
+ ensurePrunAutoInit();
931
1677
  const hadNoHistoryEnv = process.env.CCOMMAND_NO_HISTORY != null || process.env.NO_HISTORY != null;
932
1678
  const initialNoHistory = process.env.CCOMMAND_NO_HISTORY ?? process.env.NO_HISTORY;
933
- const shouldWriteHistory = !(hadNoHistoryEnv && isNoHistory$1(initialNoHistory));
1679
+ const shouldWriteHistory = !(hadNoHistoryEnv && isNoHistory(initialNoHistory));
934
1680
  const prevNoHistory = process.env.CCOMMAND_NO_HISTORY;
935
1681
  if (shouldWriteHistory) delete process.env.CCOMMAND_NO_HISTORY;
936
1682
  else process.env.CCOMMAND_NO_HISTORY = "1";
@@ -1052,117 +1798,6 @@ async function pix(params) {
1052
1798
  }
1053
1799
  }
1054
1800
 
1055
- //#endregion
1056
- //#region src/prun.ts
1057
- async function prun(params) {
1058
- ensurePrunAutoInit();
1059
- const hadNoHistoryEnv = process.env.CCOMMAND_NO_HISTORY != null || process.env.NO_HISTORY != null;
1060
- const initialNoHistory = process.env.CCOMMAND_NO_HISTORY ?? process.env.NO_HISTORY;
1061
- const prevNoHistory = process.env.CCOMMAND_NO_HISTORY;
1062
- if (!(hadNoHistoryEnv && isNoHistory(initialNoHistory))) delete process.env.CCOMMAND_NO_HISTORY;
1063
- else process.env.CCOMMAND_NO_HISTORY = "1";
1064
- const { ccommand } = getCcommand();
1065
- try {
1066
- await ccommand(params);
1067
- } finally {
1068
- if (prevNoHistory == null) delete process.env.CCOMMAND_NO_HISTORY;
1069
- else process.env.CCOMMAND_NO_HISTORY = prevNoHistory;
1070
- }
1071
- }
1072
- const isZh$2 = process.env.PI_Lang === "zh";
1073
- const safeShellValue = /^[\w./:@%+=,-]+$/;
1074
- function isNoHistory(value) {
1075
- if (!value) return false;
1076
- const normalized = value.toLowerCase();
1077
- return normalized === "1" || normalized === "true" || normalized === "yes";
1078
- }
1079
- function shellQuote(value) {
1080
- if (value === "") return "''";
1081
- if (safeShellValue.test(value)) return value;
1082
- return `'${value.replace(/'/g, `'\\''`)}'`;
1083
- }
1084
- function detectShell() {
1085
- const envShell = process.env.SHELL || "";
1086
- if (process.env.FISH_VERSION) return "fish";
1087
- if (process.env.ZSH_VERSION) return "zsh";
1088
- if (process.env.BASH_VERSION) return "bash";
1089
- return envShell.split("/").pop() || "zsh";
1090
- }
1091
- function ensurePrunAutoInit() {
1092
- if (!shouldAutoInit()) return;
1093
- const shell = detectShell();
1094
- const home = process.env.HOME || os.homedir();
1095
- let rcFile = "";
1096
- let initLine = "";
1097
- if (shell === "zsh") {
1098
- const zdotdir = process.env.ZDOTDIR || home;
1099
- rcFile = path.join(zdotdir, ".zshrc");
1100
- initLine = "eval \"$(prun --init zsh)\"";
1101
- } else if (shell === "bash") {
1102
- rcFile = path.join(home, ".bashrc");
1103
- initLine = "eval \"$(prun --init bash)\"";
1104
- } else if (shell === "fish") {
1105
- const configHome = process.env.XDG_CONFIG_HOME || path.join(home, ".config");
1106
- rcFile = path.join(configHome, "fish", "config.fish");
1107
- initLine = "prun --init fish | source";
1108
- } else return;
1109
- try {
1110
- const dir = path.dirname(rcFile);
1111
- if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
1112
- const content = fs.existsSync(rcFile) ? fs.readFileSync(rcFile, "utf8") : "";
1113
- if (!/prun\s+--init/.test(content)) {
1114
- const prefix = content.length && !content.endsWith("\n") ? "\n" : "";
1115
- fs.appendFileSync(rcFile, `${prefix}${initLine}\n`, "utf8");
1116
- }
1117
- } catch {}
1118
- }
1119
- function shouldAutoInit() {
1120
- const auto = process.env.PI_AUTO_INIT || process.env.PRUN_AUTO_INIT;
1121
- if (auto != null) return isNoHistory(auto);
1122
- if (isNoHistory(process.env.PI_NO_AUTO_INIT || process.env.PRUN_NO_AUTO_INIT)) return false;
1123
- if (process.env.CI) return false;
1124
- if (!process.stdout.isTTY || !process.stdin.isTTY) return false;
1125
- return true;
1126
- }
1127
- function printPrunInit(args = []) {
1128
- const shellArg = args[0];
1129
- const binArg = args[1];
1130
- const bin = shellQuote(binArg || process.env.PRUN_BIN || "prun");
1131
- const shell = shellArg || detectShell() || "zsh";
1132
- let script = "";
1133
- if (shell === "zsh") script = [
1134
- "prun() {",
1135
- ` local bin=${bin}`,
1136
- " local -a cmd",
1137
- " cmd=(${=bin})",
1138
- " command \"${cmd[@]}\" \"$@\"",
1139
- " fc -R",
1140
- "}"
1141
- ].join("\n");
1142
- else if (shell === "bash") script = [
1143
- "prun() {",
1144
- ` local bin=${bin}`,
1145
- " local -a cmd",
1146
- " read -r -a cmd <<< \"$bin\"",
1147
- " command \"${cmd[@]}\" \"$@\"",
1148
- " history -n",
1149
- "}"
1150
- ].join("\n");
1151
- else if (shell === "fish") script = [
1152
- "function prun",
1153
- ` set -l bin ${bin}`,
1154
- " set -l cmd (string split -- \" \" $bin)",
1155
- " command $cmd $argv",
1156
- " history --merge",
1157
- "end"
1158
- ].join("\n");
1159
- else {
1160
- console.log(color.red(isZh$2 ? `不支持的 shell: ${shell}` : `Unsupported shell: ${shell}`));
1161
- return;
1162
- }
1163
- console.log(script);
1164
- }
1165
-
1166
1801
  //#endregion
1167
1802
  //#region src/pu.ts
1168
1803
  function pu() {
@@ -1233,6 +1868,72 @@ const runMap = {
1233
1868
  "pio.mjs": pio
1234
1869
  };
1235
1870
  const isZh = process.env.PI_Lang === "zh";
1871
+ const pkgToolFlagCommands = new Set([
1872
+ "pi",
1873
+ "pi.mjs",
1874
+ "pil",
1875
+ "pil.mjs",
1876
+ "pci",
1877
+ "pci.mjs"
1878
+ ]);
1879
+ const supportedPkgTools = new Set(getSupportedPkgToolNames());
1880
+ function parsePkgToolFlags(argv) {
1881
+ const hasInspectFlag = argv.includes("--show-tool") || argv.includes("--list-tools");
1882
+ let chooseTool = false;
1883
+ let forgetTool = false;
1884
+ let listTools = false;
1885
+ let showTool = false;
1886
+ let showToolJson = false;
1887
+ let preferredTool = "";
1888
+ let invalidPreferredTool = "";
1889
+ const normalizedArgv = [];
1890
+ for (let i = 0; i < argv.length; i++) {
1891
+ const arg = argv[i];
1892
+ if (arg === "--forget-tool") {
1893
+ forgetTool = true;
1894
+ continue;
1895
+ }
1896
+ if (arg === "--show-tool") {
1897
+ showTool = true;
1898
+ continue;
1899
+ }
1900
+ if (arg === "--list-tools") {
1901
+ listTools = true;
1902
+ continue;
1903
+ }
1904
+ if (arg === "--json" && hasInspectFlag) {
1905
+ showToolJson = true;
1906
+ continue;
1907
+ }
1908
+ if (arg === "--choose-tool") {
1909
+ chooseTool = true;
1910
+ const next = argv[i + 1];
1911
+ if (next && supportedPkgTools.has(next)) {
1912
+ preferredTool = next;
1913
+ i++;
1914
+ }
1915
+ continue;
1916
+ }
1917
+ if (arg.startsWith("--choose-tool=")) {
1918
+ chooseTool = true;
1919
+ const value = arg.slice(14);
1920
+ if (supportedPkgTools.has(value)) preferredTool = value;
1921
+ else invalidPreferredTool = value;
1922
+ continue;
1923
+ }
1924
+ normalizedArgv.push(arg);
1925
+ }
1926
+ return {
1927
+ chooseTool,
1928
+ forgetTool,
1929
+ invalidPreferredTool,
1930
+ listTools,
1931
+ normalizedArgv,
1932
+ preferredTool,
1933
+ showTool,
1934
+ showToolJson
1935
+ };
1936
+ }
1236
1937
  async function setup() {
1237
1938
  const cmd = process.argv[1];
1238
1939
  let exec = "";
@@ -1249,8 +1950,56 @@ async function setup() {
1249
1950
  printPrunInit(argv.slice(1));
1250
1951
  return;
1251
1952
  }
1252
- let params = spaceFormat(argv.join(" ")).trim();
1253
- if (!await hasPkg(rootPath)) {
1953
+ const supportsPkgToolFlags = pkgToolFlagCommands.has(exec);
1954
+ const { chooseTool, forgetTool, invalidPreferredTool, listTools, normalizedArgv, preferredTool, showTool, showToolJson } = supportsPkgToolFlags ? parsePkgToolFlags(argv) : {
1955
+ chooseTool: false,
1956
+ forgetTool: false,
1957
+ invalidPreferredTool: "",
1958
+ listTools: false,
1959
+ normalizedArgv: argv,
1960
+ preferredTool: "",
1961
+ showTool: false,
1962
+ showToolJson: false
1963
+ };
1964
+ if (invalidPreferredTool) {
1965
+ console.log(color.red(isZh ? `不支持直接指定 ${invalidPreferredTool},可选值为: ${getSupportedPkgToolNames().join(", ")}` : `Unsupported tool "${invalidPreferredTool}". Valid values: ${getSupportedPkgToolNames().join(", ")}`));
1966
+ return;
1967
+ }
1968
+ if (chooseTool) process.env.PI_FORCE_PICK_TOOL = "1";
1969
+ else delete process.env.PI_FORCE_PICK_TOOL;
1970
+ if (forgetTool) process.env.PI_FORGET_PICK_TOOL = "1";
1971
+ else delete process.env.PI_FORGET_PICK_TOOL;
1972
+ if (preferredTool) process.env.PI_PREFERRED_TOOL = preferredTool;
1973
+ else delete process.env.PI_PREFERRED_TOOL;
1974
+ let params = spaceFormat(normalizedArgv.join(" ")).trim();
1975
+ const hasPackage = await hasPkg(rootPath);
1976
+ if (supportsPkgToolFlags && (chooseTool || forgetTool || showTool || listTools)) {
1977
+ if (!hasPackage) {
1978
+ console.log(color.yellow(isZh ? "当前命令仅在 Node 项目的包管理场景下可用。" : "This option is only available for package-manager selection in Node projects."));
1979
+ return;
1980
+ }
1981
+ if (showTool || listTools) {
1982
+ if (forgetTool && !chooseTool) {
1983
+ const removed = await forgetPkgToolPreference();
1984
+ console.log(removed ? color.green(isZh ? "已清除当前 workspace 保存的包管理器选择。" : "Cleared the saved package-manager choice for this workspace.") : color.yellow(isZh ? "当前 workspace 没有保存的包管理器选择。" : "No saved package-manager choice was found for this workspace."));
1985
+ }
1986
+ if (chooseTool) await resolvePkgTool();
1987
+ const status = await getPkgToolStatus();
1988
+ if (listTools) printPkgToolCandidates(status, { json: showToolJson });
1989
+ else printPkgToolStatus(status, { json: showToolJson });
1990
+ return;
1991
+ }
1992
+ if (normalizedArgv.length === 0) {
1993
+ if (forgetTool && !chooseTool) {
1994
+ const removed = await forgetPkgToolPreference();
1995
+ console.log(removed ? color.green(isZh ? "已清除当前 workspace 保存的包管理器选择。" : "Cleared the saved package-manager choice for this workspace.") : color.yellow(isZh ? "当前 workspace 没有保存的包管理器选择。" : "No saved package-manager choice was found for this workspace."));
1996
+ return;
1997
+ }
1998
+ await resolvePkgTool();
1999
+ return;
2000
+ }
2001
+ }
2002
+ if (!hasPackage) {
1254
2003
  if (await isGo(rootPath)) {
1255
2004
  if (exec === "pi") {
1256
2005
  const loading_status = await loading(`${isZh ? "正在为您安装" : "Installing"} ${params} ...\n`);
@@ -1302,7 +2051,7 @@ async function setup() {
1302
2051
  else console.log(color.yellow(isZh ? "命令不存在, 请执行 pi -h 查看帮助" : "The command does not exist, please execute pi -h to view the help"));
1303
2052
  return;
1304
2053
  }
1305
- const pkg = argv.filter((v) => !v.startsWith("-")).join(" ");
2054
+ const pkg = normalizedArgv.filter((v) => !v.startsWith("-")).join(" ");
1306
2055
  await handler(params, pkg);
1307
2056
  }
1308
2057
  if (!process.env.PI_TEST) setup().catch((error) => {