diff --git a/CMakeLists.txt b/CMakeLists.txt index 0a60167..9d5a163 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -146,13 +146,17 @@ project(WinuxCmd # Generate version header file string(TIMESTAMP BUILD_TIMESTAMP "%Y-%m-%d %H:%M:%S" UTC) -configure_file( - ${CMAKE_SOURCE_DIR}/src/version/version.hpp.in - ${CMAKE_BINARY_DIR}/version.hpp - @ONLY -) - -set(CMAKE_CXX_STANDARD 23) +configure_file( + ${CMAKE_SOURCE_DIR}/src/version/version.hpp.in + ${CMAKE_BINARY_DIR}/version.hpp + @ONLY +) +set_source_files_properties( + "${CMAKE_SOURCE_DIR}/src/version/version.cppm" + PROPERTIES OBJECT_DEPENDS "${CMAKE_BINARY_DIR}/version.hpp" +) + +set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) @@ -665,8 +669,9 @@ if(BUILD_STANDALONE) endforeach() endif() -# Note: Hardlinks are no longer created during build. -# Users should run create_links.ps1 in the bin directory to generate command links. +# Note: Hardlinks are no longer created during build. +# Users can run `winuxcmd wpm links rebuild` in the bin directory to generate +# command links. scripts/create_links.ps1 remains as a compatibility wrapper. # Scaffold tool for generating new command modules add_executable(scaffold @@ -698,54 +703,7 @@ add_custom_command(TARGET scaffold POST_BUILD COMMENT "Copying scaffold to project root" ) -# Winux Package Manager (wpm) tool -add_executable(wpm - src/tools/wpm.cpp -) - -# Add necessary modules for wpm -target_sources(wpm - PUBLIC - FILE_SET CXX_MODULES - TYPE CXX_MODULES - BASE_DIRS "${CMAKE_CURRENT_SOURCE_DIR}" - FILES - src/utils/utils.cppm - src/utils/console.cppm - src/utils/json.cppm - src/utils/cppbar.cppm - src/utils/utf8.cppm - src/utils/path.cppm - src/utils/wildcard.cppm - src/utils/text_io.cppm - src/utils/file_io.cppm - src/utils/encoding.cppm - src/utils/file_io.cppm -) - -# Set C++ standard for wpm tool -set_target_properties(wpm PROPERTIES - CXX_STANDARD 23 - CXX_STANDARD_REQUIRED ON - CXX_EXTENSIONS OFF -) - -# Add include directories for wpm tool -target_include_directories(wpm PRIVATE ${SRC_DIR}) - -# MSVC-specific configuration for wpm tool -if (MSVC) - target_compile_options(wpm PRIVATE - /std:c++latest - /experimental:module - /interface - /EHsc - /GR- - ) - target_include_directories(wpm PRIVATE "${MODULES_DIR}") -endif () - -add_subdirectory(examples) +add_subdirectory(examples) if (ENABLE_TESTS) include(CTest) @@ -798,14 +756,19 @@ install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/scripts/winuxcmd_uninstall.cmd" COMPONENT Runtime ) -install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/scripts/create_links.ps1" - DESTINATION "${CMAKE_INSTALL_BINDIR}" - COMPONENT Runtime -) - -install(FILES - "${CMAKE_CURRENT_SOURCE_DIR}/README.md" - "${CMAKE_CURRENT_SOURCE_DIR}/README-ZH.md" +install(FILES "${CMAKE_CURRENT_SOURCE_DIR}/scripts/create_links.ps1" + DESTINATION "${CMAKE_INSTALL_BINDIR}" + COMPONENT Runtime +) + +install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/wpm-source" + DESTINATION "${CMAKE_INSTALL_BINDIR}" + COMPONENT Runtime +) + +install(FILES + "${CMAKE_CURRENT_SOURCE_DIR}/README.md" + "${CMAKE_CURRENT_SOURCE_DIR}/README-ZH.md" "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE" DESTINATION "." COMPONENT Documentation @@ -876,10 +839,10 @@ message(STATUS "FFI Library: ${BUILD_FFI}") message(STATUS "Map Info Generation: ${GENERATE_MAP_INFO}") message(STATUS "Unity Build: ${ENABLE_UNITY_BUILD}") message(STATUS "UPX Compression: ${ENABLE_UPX} (PERMANENTLY DISABLED)") -message(STATUS "Static CRT: ${USE_STATIC_CRT}") -message(STATUS "Note: UPX compression has been permanently disabled for reliability") -message(STATUS "Note: Run create_links.ps1 in bin directory to generate command links") -message(STATUS "=================================================================") +message(STATUS "Static CRT: ${USE_STATIC_CRT}") +message(STATUS "Note: UPX compression has been permanently disabled for reliability") +message(STATUS "Note: Run `winuxcmd wpm links rebuild` in bin directory to generate command links") +message(STATUS "=================================================================") # Print out supported the amount of supported commands statistics_number_of_commands_supported() diff --git a/PROJECT_VERSION b/PROJECT_VERSION index aa22d3c..e01e0dd 100644 --- a/PROJECT_VERSION +++ b/PROJECT_VERSION @@ -1 +1 @@ -0.12.3 +0.12.4 diff --git a/README-zh.md b/README-zh.md index f07dd27..8309de9 100644 --- a/README-zh.md +++ b/README-zh.md @@ -2,90 +2,111 @@ [English](README.md) | 中文 -> 让 GNU 风格命令在真正的 Windows shell 里顺手工作。 +> Windows 原生 shell 的 GNU 风格命令层。 -![GitHub release (latest by date)](https://img.shields.io/github/v/release/caomengxuan666/WinuxCmd) -![GitHub all releases](https://img.shields.io/github/downloads/caomengxuan666/WinuxCmd/total) -![GitHub stars](https://img.shields.io/github/stars/caomengxuan666/WinuxCmd) -![GitHub license](https://img.shields.io/github/license/caomengxuan666/WinuxCmd) +![GitHub release (latest by date)](https://img.shields.io/github/v/release/unixwin/WinuxCmd) +![GitHub all releases](https://img.shields.io/github/downloads/unixwin/WinuxCmd/total) +![GitHub stars](https://img.shields.io/github/stars/unixwin/WinuxCmd) +![GitHub license](https://img.shields.io/github/license/unixwin/WinuxCmd) ![Windows Support](https://img.shields.io/badge/platform-Windows-blue) -WinuxCmd 把 `grep`、`sed`、`find`、`xargs`、`ls`、`cat`、`rm` 这一类 GNU 风格命令带回 Windows,而且是以适合 PowerShell、`cmd`、Windows Terminal 的方式落地,而不是要求你先离开它们。 +WinuxCmd 为 Windows 提供原生命令实现,包括 `ls`、`cat`、`grep`、`find`、`xargs`、`rm`、`cp`、`mv`、`sed`、`sort`、`uniq` 等常见 Unix/GNU 风格工具。 -它面向的是一个很真实的场景:AI、文档、CI 日志、issue 评论、旧 shell 习惯,都会不断给你 Linux 命令;但你眼前真正要操作的机器,仍然是 Windows。 +它是 [winuxsh](https://github.com/unixwin/winuxsh) 使用的命令层,也可以单独安装给 PowerShell、命令提示符、Windows Terminal、构建脚本和 CI 使用。 ![Windows Terminal](DOCS/images/WindowsTerminal.png) -## 安装 +## 推荐入口:winuxsh -WinuxCmd 已经进入 winget: +如果你想要的是 Windows 上接近 bash 的终端体验,优先从 [winuxsh](https://github.com/unixwin/winuxsh) 开始: + +```text +winuxsh = rubash shell engine + reedline frontend + winuxcmd coreutils +``` + +winuxsh 提供 bash 兼容 shell 语法、prompt/git 状态、历史、补全和内置 WinuxCmd 命令集,不需要 WSL、MSYS2、Git Bash,也不会落入 PowerShell 语义。 + +从 [winuxsh releases](https://github.com/unixwin/winuxsh/releases) 下载对应架构的包: + +```powershell +# 选择 x64 或 arm64,解压后运行: +.\winuxsh.exe +``` + +winuxsh release 包会自带架构匹配的 `winuxcmd/winuxcmd.exe` 和激活脚本。这份 WinuxCmd 是给 winuxsh 自己用的,刻意和 winget / 安装器给 PowerShell 安装的 WinuxCmd 隔离。 + +## PowerShell 安装 + +如果你只是想在现有 PowerShell 工作流里使用 GNU 风格命令,直接安装 WinuxCmd: ```powershell winget install caomengxuan666.WinuxCmd ``` -如果你更喜欢 zip 包,也可以直接从 [Releases](https://github.com/caomengxuan666/WinuxCmd/releases) 下载最新版本。 +然后通过 `winuxcmd` 使用命令: -它同样适合那些依旧只有 Windows PowerShell 5.1 的保守环境,所以对很多 AI sandbox 和自动化宿主也比较友好。 +```powershell +winuxcmd ls -la +winuxcmd grep --color=auto TODO README.md +winuxcmd find . -name "*.cpp" +``` -## 为什么是 WinuxCmd +交互式 PowerShell 场景下,包里也带有 `winux.ps1` / `winux.cmd` 辅助脚本。完成 profile 设置后,`winux activate` 可以在当前会话里覆盖 `ls`、`cat`、`rm`、`grep`、`man` 等常见 PowerShell alias;`winux deactivate` 会恢复原来的 alias。 -- 原生 Windows 优先,不是 WSL,不是虚拟机,也不是换 shell。 -- 重点不是“模拟 Linux”,而是让 Windows 原生命令和 GNU 风格过滤链自然共存。 -- 项目从 2026 年 1 月 23 日开始,早于微软当前那条 Windows coreutils 仓库在 2026 年 5 月 15 日出现。 -- 对 Windows PowerShell 5.1 这类现实环境更友好,包括很多 AI sandbox 和 Codex 这类只给你 PWSH5 的场景。 +## 渠道要分清 -这也是和微软当前 [Coreutils for Windows](https://github.com/microsoft/coreutils) 路线的根本差异之一:它们的 README 明确要求 PowerShell 7.4 或更新版本,而 WinuxCmd 从一开始就考虑了 PowerShell 5.1 宿主里的可用性。 +按用途选择对应渠道: -## 它的实际体验 +- **winuxsh release**:自带 `winuxcmd/` 目录,只服务 winuxsh;升级 winuxsh 时一起升级。 +- **winget / 安装器 release**:给 PowerShell、命令提示符和直接 `winuxcmd ` 使用。 +- **开发构建**:从本地 build tree 运行 `winuxcmd `,或用本地 hardlink。 -```bash -netstat -ano | grep 8080 -tasklist | grep -i chrome -ipconfig | grep -i ipv4 -dir /b | grep -E "\.cpp$" | sort | uniq -find . -name "*.cpp" -print0 | xargs -0 grep -n TODO -``` +正常使用时不要让 winuxsh 依赖 winget 安装的 WinuxCmd。winuxsh 包应该是自包含的,这样 x64 和 arm64 远程构建才可复现,也不会和 PowerShell 安装路径互相污染。 + +## 用 WPM 管外部工具 -WinuxCmd 不是把 Windows 伪装成 Linux,而是把你熟悉的 GNU 文本工作流真正接到 Windows 终端里。 +WinuxCmd 应该专注维护自己能做好的 GNU 风格命令层。`jq`、`ncat`、`7z`、`zstd`、`yq` 这类完整第三方工具,应该通过 WPM 外部包或其他包管理器提供,而不是在 WinuxCmd 里做不完整内置实现。 -## 怎么用最顺手 +WPM 默认会把外部可执行文件安装到指定 WinuxCmd root 目录下。也就是说,`wpm install jq --root ` 会把 `jq.exe` 放在 `winuxcmd.exe` 同级目录里;把这一个目录加入 `PATH` 就够了。需要 DLL 或其他 side-by-side 文件的包,仍然可以在索引里显式声明文件映射。 -在当前 PowerShell 会话里优先使用 WinuxCmd 命令名: +典型流程: ```powershell -winux activate -cat README.md -rm old.txt -man ls +winuxcmd wpm source list +winuxcmd wpm index update +winuxcmd wpm install jq --root "C:\path\to\winuxcmd" +winuxcmd wpm links rebuild --root "C:\path\to\winuxcmd" ``` -恢复 PowerShell 原始行为: +对 winuxsh bundle 来说,要对它自带的 WinuxCmd 目录运行 WPM,而不是混用 winget 安装路径。外部 helper exe 也应该放在 winuxsh 实际加入 PATH 的同一个目录里。 -```powershell -winux deactivate -``` +## 为什么需要 WinuxCmd -如果你希望每次打开交互式 PowerShell 都自动激活,把这段放到 `$PROFILE` 里的 `winux` wrapper 后面: +Windows 开发者每天都会从文档、CI 日志、issue 评论、shell 片段和肌肉记忆里收到 Linux 风格命令。WinuxCmd 的目标是让这些文本工作流在 Windows 上可用,而不是要求你离开 Windows。 -```powershell -if (Get-Command winux -ErrorAction SilentlyContinue) { - winux activate 6>$null -} -``` +它关注这些点: + +- Windows 原生进程行为和路径 +- 高频 GNU 风格参数的兼容性 +- 能和 `tasklist`、`netstat`、`sc`、`ipconfig` 等 Windows 工具组成可预期的管道 +- 对 Windows PowerShell 5.1 这类较旧脚本环境保持可用 +- 产物足够小,方便被其他工具内置 -## 它适合哪些场景 +## 实际体验 -WinuxCmd 很适合下面这些情况: +```bash +netstat -ano | grep 8080 +tasklist | grep -i chrome +ipconfig | grep -i ipv4 +dir /b | grep -E "\.cpp$" | sort | uniq +find . -name "*.cpp" -print0 | xargs -0 grep -n TODO +``` -- AI 给了 GNU 风格 one-liner,但你的宿主系统是 Windows。 -- 你想用 `grep | sort | xargs | find` 这类链路,但不想专门打开 WSL。 -- 你希望 `tasklist`、`netstat`、`sc`、`ipconfig` 这些原生命令也能进入熟悉的文本过滤流程。 -- 你的环境比较旧或受限,真正拿到手的仍然是 Windows PowerShell 5.1。 +WinuxCmd 不是把 Windows 伪装成 Linux,而是把熟悉的文本工作流接到你已经在用的文件、终端、脚本和 CI 里。 ## 命令覆盖 -WinuxCmd 目前已经实现 148 个命令,覆盖了这些高频能力: +WinuxCmd 目前实现 153 个命令,覆盖这些高频能力: - 文件工具:`ls`、`cp`、`mv`、`rm`、`mkdir`、`ln`、`stat`、`readlink`、`realpath` - 文本工具:`cat`、`grep`、`sed`、`sort`、`uniq`、`cut`、`head`、`tail`、`wc` @@ -102,13 +123,27 @@ WinuxCmd 目前已经实现 148 个命令,覆盖了这些高频能力: ## Windows 侧说明 - 未识别命令会回退给父 shell,所以 WinuxCmd 是补足 PowerShell 和 `cmd`,不是替换它们。 -- 执行 `winux activate` 之后,`man` 可以在当前 PowerShell 会话里直接覆盖 PowerShell 自带映射。 +- `--version` 由 WinuxCmd 统一处理,不要把单个命令的 version 输出当成独立兼容事项。 +- `grep` 这类支持颜色的命令会遵循终端和 `--color` 行为;如果你明确要在管道里保留 ANSI 颜色,用 `--color=always`。 - 如果你主要在交互式 `cmd` 下用它,优先考虑让 Windows Terminal 以 `%SystemRoot%\System32\cmd.exe /k winuxcmd` 启动,而不是配置全局 `AutoRun`。 +## 构建 + +```powershell +cmake --preset vs2022 +cmake --build build-vs --target winuxcmd --parallel +``` + +常用本地检查: + +```powershell +ctest --test-dir build-vs -R "^(grep|find|rm)\." --output-on-failure +``` + ## 更多 +- [winuxsh](https://github.com/unixwin/winuxsh) - [贡献指南(英文)](CONTRIBUTING.md) - [贡献指南(中文)](CONTRIBUTING_ZH.MD) - [构建模式文档](DOCS/zh/build_modes.md) - [自定义容器与基准说明](DOCS/en/custom_containers.md) -- [仓库内 AI 工作说明](skills/winuxcmd/SKILL.md) diff --git a/README.md b/README.md index a29d131..36b616b 100644 --- a/README.md +++ b/README.md @@ -2,94 +2,116 @@ [English](README.md) | [中文](README-zh.md) -> GNU-style commands for Windows shells, without leaving Windows. +> GNU-style command layer for Windows-native shells. -![GitHub release (latest by date)](https://img.shields.io/github/v/release/caomengxuan666/WinuxCmd) -![GitHub all releases](https://img.shields.io/github/downloads/caomengxuan666/WinuxCmd/total) -![GitHub stars](https://img.shields.io/github/stars/caomengxuan666/WinuxCmd) -![GitHub license](https://img.shields.io/github/license/caomengxuan666/WinuxCmd) +![GitHub release (latest by date)](https://img.shields.io/github/v/release/unixwin/WinuxCmd) +![GitHub all releases](https://img.shields.io/github/downloads/unixwin/WinuxCmd/total) +![GitHub stars](https://img.shields.io/github/stars/unixwin/WinuxCmd) +![GitHub license](https://img.shields.io/github/license/unixwin/WinuxCmd) ![Windows Support](https://img.shields.io/badge/platform-Windows-blue) -WinuxCmd makes familiar GNU-style commands land cleanly in PowerShell, `cmd`, and Windows Terminal. +WinuxCmd provides native Windows implementations of familiar Unix/GNU commands such as `ls`, `cat`, `grep`, `find`, `xargs`, `rm`, `cp`, `mv`, `sed`, `sort`, and `uniq`. -It is built for the very common Windows situation where AI, docs, CI logs, issue comments, or muscle memory hand you Linux-flavored one-liners, but the machine in front of you is still Windows. +It is the command layer used by [winuxsh](https://github.com/unixwin/winuxsh), and it can also be installed directly for PowerShell, Command Prompt, Windows Terminal, build scripts, and CI jobs. ![Windows Terminal](DOCS/images/WindowsTerminal.png) -## Install +## Recommended: Winuxsh -WinuxCmd is available on winget: +If you want a bash-like terminal on Windows, start with [winuxsh](https://github.com/unixwin/winuxsh): + +```text +winuxsh = rubash shell engine + reedline frontend + winuxcmd coreutils +``` + +Winuxsh gives you bash-compatible shell syntax, prompt/git integration, history, completions, and a bundled WinuxCmd command set without requiring WSL, MSYS2, Git Bash, or PowerShell semantics. + +Download the matching package from the [winuxsh releases](https://github.com/unixwin/winuxsh/releases): + +```powershell +# choose x64 or arm64, unzip, then run: +.\winuxsh.exe +``` + +The winuxsh release package carries its own architecture-matched `winuxcmd/winuxcmd.exe` and activation script. That copy is intentionally isolated from any WinuxCmd you install with winget or an installer for PowerShell. + +## PowerShell Install + +If you want GNU-style commands inside your existing PowerShell workflow, install WinuxCmd directly: ```powershell winget install caomengxuan666.WinuxCmd ``` -If you prefer a zip package, download the latest build from [Releases](https://github.com/caomengxuan666/WinuxCmd/releases). +Then use commands through `winuxcmd`: -WinuxCmd also fits conservative Windows environments that still expose Windows PowerShell 5.1, which makes it a practical choice for many AI sandbox and automation hosts. +```powershell +winuxcmd ls -la +winuxcmd grep --color=auto TODO README.md +winuxcmd find . -name "*.cpp" +``` -## Why WinuxCmd +For interactive PowerShell sessions, the package also includes `winux.ps1` / `winux.cmd` helpers. After profile setup, `winux activate` can override common PowerShell aliases such as `ls`, `cat`, `rm`, `grep`, and `man` in the current session; `winux deactivate` restores the original aliases. -- Native Windows first. No WSL, VM, or shell replacement. -- Built for mixed pipelines where Windows tools and GNU-style filters need to coexist. -- Designed for PowerShell 5.1 hosts, including AI sandboxes such as Codex that may not expose PowerShell 7. +## Keep The Channels Separate -That last point is a real product difference. Microsoft's current [Coreutils for Windows](https://github.com/microsoft/coreutils) README says PowerShell 7.4 or newer is required; WinuxCmd is designed to stay useful in PowerShell 5.1-hosted environments. +Use the right channel for the job: -## What It Feels Like +- **winuxsh release**: bundles its own `winuxcmd/` directory for the shell. Upgrade it by upgrading winuxsh. +- **winget / installer release**: installs WinuxCmd for PowerShell, Command Prompt, and direct `winuxcmd ` usage. +- **Development builds**: use local hardlinks or `winuxcmd ` from the build tree. -```bash -netstat -ano | grep 8080 -tasklist | grep -i chrome -ipconfig | grep -i ipv4 -dir /b | grep -E "\.cpp$" | sort | uniq -find . -name "*.cpp" -print0 | xargs -0 grep -n TODO -``` +Do not make winuxsh depend on a winget-installed WinuxCmd in normal use. The shell package is designed to be self-contained so x64 and arm64 builds stay reproducible. + +## External Tools With WPM -The point is not to turn Windows into Linux. The point is to make familiar text workflows work where you already are. +WinuxCmd intentionally stays focused on the GNU-style command layer it can maintain well. Complete third-party tools such as `jq`, `ncat`, `7z`, `zstd`, and `yq` should come from WPM-managed external packages or another package manager instead of partial built-ins. -## Use It Your Way +WPM installs external executables directly into the selected WinuxCmd root by default. That means `wpm install jq --root ` places `jq.exe` beside `winuxcmd.exe`, so adding that one directory to `PATH` is enough. Package indexes can still map support files explicitly when a tool needs side-by-side DLLs. -Prefer WinuxCmd names inside the current PowerShell session: +Typical flow: ```powershell -winux activate -cat README.md -rm old.txt -man ls +winuxcmd wpm source list +winuxcmd wpm index update +winuxcmd wpm install jq --root "C:\path\to\winuxcmd" +winuxcmd wpm links rebuild --root "C:\path\to\winuxcmd" ``` -Restore the original PowerShell behavior with: +For winuxsh bundles, run WPM against the bundled WinuxCmd directory, not a separate winget install. Keep external helper executables in the same PATH-visible directory that winuxsh uses for bundled commands. -```powershell -winux deactivate -``` +## Why WinuxCmd Exists -If you want activation in each interactive PowerShell session, add this after the `winux` wrapper in `$PROFILE`: +Windows developers constantly receive Linux-flavored commands from docs, CI logs, issue comments, shell snippets, and muscle memory. WinuxCmd makes those text workflows useful on Windows without asking you to leave Windows. -```powershell -if (Get-Command winux -ErrorAction SilentlyContinue) { - winux activate 6>$null -} -``` +It focuses on: -## Where It Fits +- native Windows process behavior and paths +- familiar GNU-style command-line flags where they matter +- predictable pipelines with Windows tools such as `tasklist`, `netstat`, `sc`, and `ipconfig` +- compatibility with older Windows PowerShell 5.1 script environments +- small release artifacts that can be bundled by other tools -WinuxCmd is a strong fit when: +## What It Feels Like -- AI gives you GNU-style one-liners but your host is Windows. -- You want `grep | sort | xargs | find` style workflows without opening WSL. -- You need native Windows commands like `tasklist`, `netstat`, `sc`, or `ipconfig` to flow into familiar text filters. -- Your environment is older or locked down and Windows PowerShell 5.1 is still what you actually get. +```bash +netstat -ano | grep 8080 +tasklist | grep -i chrome +ipconfig | grep -i ipv4 +dir /b | grep -E "\.cpp$" | sort | uniq +find . -name "*.cpp" -print0 | xargs -0 grep -n TODO +``` + +The point is not to turn Windows into Linux. The point is to make familiar text workflows work where your files, terminals, scripts, and CI already are. ## Command Coverage -WinuxCmd currently implements 148 commands, including practical coverage across: +WinuxCmd currently implements 153 commands, including practical coverage across: -- file tools like `ls`, `cp`, `mv`, `rm`, `mkdir`, `ln`, `stat`, `readlink`, and `realpath` -- text tools like `cat`, `grep`, `sed`, `sort`, `uniq`, `cut`, `head`, `tail`, and `wc` -- search tools like `find` and `xargs` -- Windows-friendly utilities like `ps`, `lsof`, `which`, `tree`, `hexdump`, and `strings` +- file tools: `ls`, `cp`, `mv`, `rm`, `mkdir`, `ln`, `stat`, `readlink`, `realpath` +- text tools: `cat`, `grep`, `sed`, `sort`, `uniq`, `cut`, `head`, `tail`, `wc` +- search and composition: `find`, `xargs` +- Windows-friendly utilities: `ps`, `lsof`, `which`, `tree`, `hexdump`, `strings` Detailed compatibility references live here: @@ -100,14 +122,28 @@ Detailed compatibility references live here: ## Windows Notes -- Unknown commands fall back through the parent shell, so WinuxCmd complements PowerShell and `cmd` instead of trying to replace them. -- After `winux activate`, use `man.exe` when you want WinuxCmd's manual command directly. +- Unknown commands fall back through the parent shell, so WinuxCmd complements PowerShell and `cmd` instead of replacing them. +- `--version` is handled uniformly by WinuxCmd; do not treat individual command version output as separate compatibility work. +- Color-aware commands such as `grep` follow terminal and `--color` behavior. Use `--color=always` when you intentionally want ANSI color preserved through a pipe. - For interactive `cmd` use, launching Windows Terminal with `%SystemRoot%\System32\cmd.exe /k winuxcmd` is safer than a global `AutoRun` hook. +## Building + +```powershell +cmake --preset vs2022 +cmake --build build-vs --target winuxcmd --parallel +``` + +Useful local checks: + +```powershell +ctest --test-dir build-vs -R "^(grep|find|rm)\." --output-on-failure +``` + ## More +- [winuxsh](https://github.com/unixwin/winuxsh) - [Contributing Guide (EN)](CONTRIBUTING.md) - [Contributing Guide (ZH)](CONTRIBUTING_ZH.MD) - [Build Modes](DOCS/en/build_modes_en.md) - [Custom Containers and Benchmarks](DOCS/en/custom_containers.md) -- [Local AI workflow for this repo](skills/winuxcmd/SKILL.md) diff --git a/cmake/ReadVersionFile.cmake b/cmake/ReadVersionFile.cmake index 37dc738..5d8aa14 100644 --- a/cmake/ReadVersionFile.cmake +++ b/cmake/ReadVersionFile.cmake @@ -3,6 +3,7 @@ function(read_version_file) if(EXISTS "${CMAKE_SOURCE_DIR}/PROJECT_VERSION") + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${CMAKE_SOURCE_DIR}/PROJECT_VERSION") file(READ "${CMAKE_SOURCE_DIR}/PROJECT_VERSION" VERSION_STRING) string(STRIP "${VERSION_STRING}" VERSION_STRING) diff --git a/packaging/winuxcmd.iss b/packaging/winuxcmd.iss index 583d7f0..94572fd 100644 --- a/packaging/winuxcmd.iss +++ b/packaging/winuxcmd.iss @@ -95,12 +95,22 @@ begin end; procedure CreateCommandLinks; +var + ResultCode: Integer; begin - RunPowerShell( - ExpandConstant('{app}\create_links.ps1'), - '-Force', + if not Exec( + ExpandConstant('{app}\winuxcmd.exe'), + 'wpm links rebuild --root "' + ExpandConstant('{app}') + '" --force', ExpandConstant('{app}'), - 'Failed to create WinuxCmd command links'); + SW_HIDE, + ewWaitUntilTerminated, + ResultCode) then + begin + RaiseException('Failed to create WinuxCmd command links: failed to launch winuxcmd.exe'); + end; + + if ResultCode <> 0 then + RaiseException('Failed to create WinuxCmd command links: exit code ' + IntToStr(ResultCode)); end; procedure ConfigurePowerShellProfiles(Install: Boolean); diff --git a/scripts/create_links.ps1 b/scripts/create_links.ps1 index 581be92..ac5d3ad 100644 --- a/scripts/create_links.ps1 +++ b/scripts/create_links.ps1 @@ -1,27 +1,21 @@ <# .SYNOPSIS - WinuxCmd Command Link Generator + WinuxCmd Command Link Generator compatibility wrapper. .DESCRIPTION - Creates hardlinks or symbolic links for all WinuxCmd commands - from winuxcmd.exe in the current directory. + Delegates command link management to the internal WPM command: + winuxcmd.exe wpm links rebuild/remove. .PARAMETER Force - Overwrite existing command executables without prompting. + Overwrite existing command executables when WPM can do so safely. .PARAMETER UseSymbolicLinks - Use symbolic links instead of hardlinks (useful for non-NTFS filesystems). + Deprecated. WPM manages hardlinks only. .PARAMETER Remove - Remove all created links (winuxcmd.exe is kept). -.EXAMPLE - .\create_links.ps1 - Create hardlinks for all commands (requires NTFS). -.EXAMPLE - .\create_links.ps1 -UseSymbolicLinks - Create symbolic links (works on all Windows filesystems). + Remove generated command hardlinks. winuxcmd.exe is kept. .EXAMPLE .\create_links.ps1 -Force - Create hardlinks, overwriting existing files without prompting. + Rebuild hardlinks through WPM. .EXAMPLE .\create_links.ps1 -Remove - Remove all command links. + Remove hardlinks through WPM. #> [CmdletBinding()] @@ -32,270 +26,42 @@ param( ) $ErrorActionPreference = "Stop" -$Script:Version = "0.7.2" - -# Escape special characters for PowerShell path handling -function Escape-WildcardChars { - param([string]$Path) - $specialChars = @('[', ']', '*', '?') - foreach ($char in $specialChars) { - $Path = $Path.Replace($char, "`$char") - } - return $Path -} - -# Available commands list (auto-generated from src/commands/*.cpp) -$Script:Commands = @( - "arch", "b2sum", "base32", "base64", "basename", "basenc", "cal", "cat", - "chcon", "chgrp", "chmod", "chown", "chroot", "cksum", "clear", "cmp", "col", "column", "comm", "cp", "cpio", "csplit", - "cut", "cygpath", "d2u", "date", "dd", "df", "diff", "diff3", "dirname", - "dir", "dircolors", "dos2unix", "du", "echo", "env", "expand", "expr", "factor", "false", "file", - "find", "fmt", "fold", "free", "getconf", "grep", "groups", "head", "hexdump", - "hmac256", "hostid", "hostname", "id", "infocmp", "install", "join", "jq", - "kill", "less", "link", "ln", "locale", "logname", "ls", "lsof", "man", "md5sum", - "mkdir", "mkfifo", "mknod", "mktemp", "more", "mpicalc", "mv", "nice", "nl", "nohup", - "nproc", "numfmt", "od", "paste", "patch", "pathchk", "pinky", "pr", - "printenv", "printf", "ps", "ptx", "pwd", "readlink", "realpath", "reset", - "rev", "rm", "rmdir", "runcon", "sdiff", "sed", "seq", "sha1sum", "sha224sum", - "sha256sum", "sha384sum", "sha512sum", "shred", "shuf", "sleep", "sort", - "split", "stat", "stdbuf", "strings", "stty", "sum", "sync", "tac", "tail", "tee", "test", - "[", "tic", "timeout", "toe", "top", "touch", "tput", "tr", "tree", "true", - "truncate", "tsort", "tty", "tzset", "u2d", "uname", "unexpand", "uniq", - "unix2dos", "unlink", "uptime", "users", "vdir", "watch", "wc", "which", "who", - "whoami", "xargs", "xxd", "yes" -) -function Write-ColorOutput { - param( - [Parameter(Mandatory = $true)] - [ValidateSet("Green", "Yellow", "Red", "Cyan", "Blue", "Gray")] - [string]$Color, - - [Parameter(Mandatory = $true)] - [string]$Text - ) - - # Simple text-based markers for older terminals - $Markers = @{ - Green = "[OK]" - Yellow = "[INFO]" - Red = "[ERROR]" - Cyan = "[INFO]" - Blue = "[NOTE]" - Gray = " " - } - - Write-Host "$($Markers[$Color]) $Text" +function Write-ErrorLine { + param([string]$Text) + Write-Host "[ERROR] $Text" } function Get-WinuxCmdPath { - $currentDir = Get-Location - $WinuxCmdPath = Join-Path $currentDir "winuxcmd.exe" - - if (-not (Test-Path $WinuxCmdPath)) { - Write-ColorOutput "Red" "Error: winuxcmd.exe not found in current directory." - Write-ColorOutput "Yellow" "Current directory: $currentDir" - Write-ColorOutput "Gray" "Please run this script from the WinuxCmd bin directory." + $candidate = Join-Path (Get-Location) "winuxcmd.exe" + if (-not (Test-Path -LiteralPath $candidate)) { + Write-ErrorLine "winuxcmd.exe not found in current directory." + Write-Host "[INFO] Current directory: $(Get-Location)" + Write-Host "[INFO] Run this wrapper from the WinuxCmd bin directory." exit 1 } - - return $WinuxCmdPath -} - -function Get-FileSystemType { - try { - $drive = (Get-Location).Drive.Root - $volume = Get-WmiObject -Class Win32_Volume -Filter "DriveLetter='$drive'" -ErrorAction SilentlyContinue - - if ($volume) { - return $volume.FileSystem - } - } - catch { - # Fall back to assuming NTFS if WMI fails - return "Unknown" - } - - return "Unknown" -} - -function Remove-CommandLinks { - param( - [string]$WinuxCmdPath - ) - - $removedCount = 0 - $skippedCount = 0 - $errorsCount = 0 - - Write-ColorOutput "Cyan" "Removing WinuxCmd command links..." - Write-Host "" - - foreach ($cmd in $Script:Commands) { - $cmdPath = Join-Path (Get-Location) "$cmd.exe" - - if (-not (Test-Path -LiteralPath $cmdPath)) { - continue - } - - try { - # Use winuxcmd rm to remove the file - & $WinuxCmdPath @("rm", "-f", $cmdPath) - if ($LASTEXITCODE -eq 0) { - Write-ColorOutput "Green" " [Removed] $cmd.exe" - $removedCount++ - } - } - catch { - Write-ColorOutput "Red" " [Error] Failed to remove $cmd.exe: $($_.Exception.Message)" - $errorsCount++ - } - } - - Write-Host "" - Write-ColorOutput "Cyan" "Summary:" - Write-ColorOutput "Green" " Removed: $removedCount" - if ($skippedCount -gt 0) { - Write-ColorOutput "Yellow" " Skipped: $skippedCount" - } - if ($errorsCount -gt 0) { - Write-ColorOutput "Red" " Errors: $errorsCount" - } - Write-ColorOutput "Gray" " winuxcmd.exe is preserved" + return $candidate } -function New-CommandLinks { - param( - [string]$WinuxCmdPath, - [bool]$UseSymbolic - ) - - $createdCount = 0 - $skippedCount = 0 - $errorsCount = 0 - $fsType = Get-FileSystemType - - $linkType = if ($UseSymbolic) { "symbolic link" } else { "hardlink" } - - Write-ColorOutput "Cyan" "WinuxCmd Command Link Generator v$Script:Version" - Write-ColorOutput "Cyan" "=========================================" - Write-Host "" - Write-ColorOutput "Blue" "Configuration:" - Write-ColorOutput "Gray" " Target: $WinuxCmdPath" - Write-ColorOutput "Gray" " Link Type: $linkType" - Write-ColorOutput "Gray" " Filesystem: $fsType" - Write-Host "" - - if (-not $UseSymbolic -and $fsType -ne "NTFS" -and $fsType -ne "Unknown") { - Write-ColorOutput "Yellow" "Warning: Hardlinks require NTFS filesystem." - Write-ColorOutput "Yellow" " Current filesystem: $fsType" - Write-ColorOutput "Yellow" " Consider using -UseSymbolicLinks flag." - Write-Host "" - - if (-not $Force) { - $continue = Read-Host "Continue anyway? (Y/N)" - if ($continue -notmatch '^[Yy]') { - Write-ColorOutput "Yellow" "Cancelled." - exit 0 - } - } - } - - # Check for existing files - $existingFiles = @() - foreach ($cmd in $Script:Commands) { - $cmdPath = Join-Path (Get-Location) "$cmd.exe" - if (Test-Path -LiteralPath $cmdPath) { - $existingFiles += $cmd - } - } - - if ($existingFiles.Count -gt 0 -and -not $Force) { - Write-ColorOutput "Yellow" "Warning: The following files already exist:" - foreach ($file in $existingFiles) { - Write-ColorOutput "Yellow" " - $file.exe" - } - Write-Host "" - - $overwrite = Read-Host "Overwrite existing files? (Y/N)" - if ($overwrite -notmatch '^[Yy]') { - Write-ColorOutput "Yellow" "Cancelled." - exit 0 - } - } - - Write-ColorOutput "Cyan" "Creating links (batch mode)..." - Write-Host "" - - foreach ($cmd in $Script:Commands) { - $cmdPath = Join-Path (Get-Location) "$cmd.exe" - - try { - $lnArgs = @("ln") - if ($UseSymbolic) { - $lnArgs += @("-s") - } - if ($Force) { - $lnArgs += @("-f") - } - $lnArgs += @($WinuxCmdPath, $cmdPath) - - & $WinuxCmdPath @lnArgs - - if ($LASTEXITCODE -eq 0) { - $createdCount++ - } - else { - Write-ColorOutput "Red" " [Failed] $cmd.exe (exit $LASTEXITCODE)" - $errorsCount++ - } - } - catch { - Write-ColorOutput "Red" " [Failed] $cmd.exe: $($_.Exception.Message)" - $errorsCount++ - } - } - - if ($createdCount -gt 0 -and $errorsCount -eq 0) { - Write-ColorOutput "Green" " [Created] $createdCount command links" - } - - Write-Host "" - Write-ColorOutput "Cyan" "Summary:" - Write-ColorOutput "Green" " Created: $createdCount" - if ($errorsCount -gt 0) { - Write-ColorOutput "Red" " Errors: $errorsCount" - } - Write-Host "" - - if ($createdCount -gt 0) { - Write-ColorOutput "Green" "Success! WinuxCmd commands are now available." - Write-Host "" - Write-ColorOutput "Blue" "Usage:" - Write-ColorOutput "Gray" " $ ls -la # Use ls command" - Write-ColorOutput "Gray" " $ cat file.txt # Use cat command" - Write-ColorOutput "Gray" " $ grep pattern file # Use grep command" - Write-Host "" - - if ($UseSymbolic) { - Write-ColorOutput "Yellow" "Note: Symbolic links were used. These may require elevated permissions" - Write-ColorOutput "Yellow" " on some systems. If you encounter permission issues, try running" - Write-ColorOutput "Yellow" " PowerShell as Administrator." - } - } - else { - Write-ColorOutput "Red" "Failed to create any command links." - exit 1 - } +if ($UseSymbolicLinks) { + Write-ErrorLine "-UseSymbolicLinks is no longer supported here." + Write-Host "[INFO] WPM owns command-link management and creates hardlinks only." + Write-Host "[INFO] For one-off symlinks, use: winuxcmd.exe ln -s SOURCE LINK" + exit 2 } -# Main script logic -$WinuxCmdPath = Get-WinuxCmdPath +$winuxCmdPath = Get-WinuxCmdPath +$root = Get-Location if ($Remove) { - Remove-CommandLinks -WinuxCmdPath $WinuxCmdPath + $wpmArgs = @("wpm", "links", "remove", "--root", "$root") } else { - New-CommandLinks -WinuxCmdPath $WinuxCmdPath -UseSymbolic $UseSymbolicLinks + $wpmArgs = @("wpm", "links", "rebuild", "--root", "$root") + if ($Force) { + $wpmArgs += "--force" + } } +& $winuxCmdPath @wpmArgs +exit $LASTEXITCODE diff --git a/skills/winuxsh/SKILL.md b/skills/winuxsh/SKILL.md new file mode 100644 index 0000000..4f96d9c --- /dev/null +++ b/skills/winuxsh/SKILL.md @@ -0,0 +1,92 @@ +--- +name: winuxsh +description: >- + Use winuxsh as the Windows-native agent shell: run commands through winuxsh + -c, prefer bundled WinuxCmd coreutils over PowerShell aliases, locate or + install the repo-local winuxsh build on PATH, export MSVC build variables when + needed, and keep downloaded helper tools in one PATH-visible directory. Use + when working on Windows repositories where winuxsh or WinuxCmd are available, + especially WinuxCmd/winuxsh development, builds, tests, release work, or agent + automation. +--- + +# Winuxsh + +Use `winuxsh` as the default command runner for Windows agent work once winuxsh +is installed on PATH. It provides a bash-like shell and bundled WinuxCmd +coreutils, so the agent can stay inside the project's own Windows-native +toolchain. + +## Command Runner + +- Prefer `winuxsh -c '...'` for shell work. Do not write a hardcoded local + `winuxsh.exe` path into routine commands once PATH is configured. +- Keep the actual command inside the `-c` string: `rg`, `sed`, `grep`, `find`, + `which`, `ls`, `cp`, `mv`, `rm`, and similar tools should come from WinuxCmd + or the winuxsh environment. +- If `winuxsh -c 'printf ok'` does not run a shell command, fix the local PATH + first instead of falling back to PowerShell. +- Use `multi_tool_use.parallel` for independent reads and searches, but do not + join unrelated commands with noisy separators. +- If a Windows app execution alias is not visible from winuxsh, call a real + executable path or use a narrowly scoped bridge such as `cmd.exe /c`. + +## One-Time Setup + +- Ensure the winuxsh release directory is permanently on the user PATH before + expecting agents to use it. The PATH entry should be the directory containing + the shell launcher, not a parent directory. +- Use `winuxsh` as the stable command name in skills and automation. Shorter + aliases such as `winux` are a user preference and should not be assumed. +- Do not create `winux.exe`, `winux.cmd`, or other aliases on the user's behalf + unless the user explicitly asks for that alias. +- Verify setup with: + +```bash +winuxsh -c 'printf "winuxsh ok\n"; which sed; which grep; which jq' +``` + +- If only a local build exists, add its release directory to PATH once, then use + `winuxsh -c` normally. + +## Path Rules + +- Treat `../winuxsh/target/release` as the local release/dev directory when + working from the WinuxCmd repo, but add it to PATH instead of spelling it out + in every command. +- Put extra downloaded helper executables directly in one PATH-visible folder. + Do not bury command executables in per-package subdirectories unless that + folder itself is added to PATH. +- For winuxsh-bundled tooling, prefer colocating external helpers beside + `winuxsh.exe` or beside the bundled `winuxcmd.exe` directory that winuxsh + prepends to PATH. +- Remember that a PATH entry names a directory, not a recursive tree. + +## MSVC Builds + +When MSVC environment variables are missing, export them inside the winuxsh +command before running CMake/Ninja: + +```bash +export INCLUDE="C:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.50.35717\include;C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\ucrt;C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\um;C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\shared;C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\winrt;C:\Program Files (x86)\Windows Kits\10\Include\10.0.26100.0\cppwinrt" +export LIB="C:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.50.35717\lib\x64;C:\Program Files (x86)\Windows Kits\10\Lib\10.0.26100.0\ucrt\x64;C:\Program Files (x86)\Windows Kits\10\Lib\10.0.26100.0\um\x64" +export PATH="C:\Program Files\Microsoft Visual Studio\18\Community\VC\Tools\MSVC\14.50.35717\bin\Hostx64\x64;C:\Program Files (x86)\Windows Kits\10\bin\10.0.26100.0\x64;$PATH" +ninja -C build-vs winuxcmd-tests +``` + +## WinuxCmd Development + +- Use `wpm links rebuild` from the relevant WinuxCmd root to regenerate command + hardlinks. +- `wpm` is an internal WinuxCmd tool, not a coreutils command. +- Do not reintroduce built-in `jq`; use WPM or external package managers for + complete third-party tools such as `jq`, `ncat`, `7z`, `zstd`, and `yq`. +- Keep `json.hpp/json.cppm` when WPM needs JSON parsing for local index and + source metadata. + +## Validation + +- Prefer targeted tests: `./build-vs/tests/winuxcmd-tests.exe --gtest_filter=wpm.*` + and `--gtest_filter=winuxcmd.*` for WPM/link changes. +- Verify command visibility with `which ` and `tool --version`. +- Use `git diff --check` before committing. diff --git a/skills/winuxsh/agents/openai.yaml b/skills/winuxsh/agents/openai.yaml new file mode 100644 index 0000000..c261461 --- /dev/null +++ b/skills/winuxsh/agents/openai.yaml @@ -0,0 +1,7 @@ +interface: + display_name: "Winuxsh Agent Workflow" + short_description: "Use winuxsh and bundled WinuxCmd tools for Windows agent work" + default_prompt: "Use $winuxsh when working on Windows repos that have winuxsh available. Ensure the winuxsh release directory is on PATH, then run commands as winuxsh -c '...'. Prefer bundled WinuxCmd coreutils and explicit MSVC environment exports when building C++ projects." + +policy: + allow_implicit_invocation: true diff --git a/src/commands/jq.cpp b/src/commands/jq.cpp deleted file mode 100644 index 614d20c..0000000 --- a/src/commands/jq.cpp +++ /dev/null @@ -1,263 +0,0 @@ -/* - * Copyright © 2026 [caomengxuan666] - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - * - * - File: jq.cpp - * - Username: Administrator - * - CopyrightYear: 2026 - */ -/// @contributors: -/// - caomengxuan666 <2507560089@qq.com> -/// @Description: Implementation for jq. -/// @Version: 0.1.0 -/// @License: MIT -/// @Copyright: Copyright © 2026 WinuxCmd - -#include "pch/pch.h" -// include other header after pch.h -#include "core/command_macros.h" - -import std; -import core; -import utils; -import container; - -using cmd::meta::OptionMeta; -using cmd::meta::OptionType; - -auto constexpr JQ_OPTIONS = std::array{ - OPTION("-r", "--raw-output", "output raw strings, not JSON quotes", - BOOL_TYPE), - OPTION("-c", "--compact-output", "compact instead of pretty-printed output", - BOOL_TYPE), - OPTION("-C", "--color-output", "colorize JSON output", BOOL_TYPE), - OPTION("-M", "--monochrome-output", "monochrome (don't colorize JSON)", - BOOL_TYPE), - OPTION("-S", "--sort-keys", "sort keys of each object alphabetically", - BOOL_TYPE), - OPTION("-f", "--from-file", "read filter from file", STRING_TYPE), - OPTION("-n", "--null-input", "use `null` as the single input value", - BOOL_TYPE)}; - -namespace jq_pipeline { -namespace cp = core::pipeline; - -struct Config { - bool raw_output = false; - bool compact_output = false; - bool color_output = false; - bool monochrome_output = false; - bool sort_keys = false; - std::string filter_file; - bool null_input = false; - std::string filter = "."; - SmallVector files; -}; - -void append_file_operand(Config& cfg, const std::string& file_arg) { - if (contains_wildcard(file_arg)) { - auto glob_result = glob_expand(file_arg); - if (glob_result.expanded) { - for (const auto& file : glob_result.files) { - cfg.files.push_back(wstring_to_utf8(file)); - } - return; - } - } - cfg.files.push_back(file_arg); -} - -auto build_config(const CommandContext& ctx) - -> cp::Result { - Config cfg; - cfg.raw_output = - ctx.get("--raw-output", false) || ctx.get("-r", false); - cfg.compact_output = - ctx.get("--compact-output", false) || ctx.get("-c", false); - cfg.color_output = - ctx.get("--color-output", false) || ctx.get("-C", false); - cfg.monochrome_output = - ctx.get("--monochrome-output", false) || ctx.get("-M", false); - cfg.sort_keys = - ctx.get("--sort-keys", false) || ctx.get("-S", false); - cfg.filter_file = ctx.get("--from-file", ""); - cfg.null_input = - ctx.get("--null-input", false) || ctx.get("-n", false); - - size_t first_file = 0; - if (!cfg.filter_file.empty()) { - cfg.filter = "."; - } else if (!ctx.positionals.empty()) { - std::string first_arg(ctx.positionals[0]); - std::ifstream test_file(first_arg); - bool is_file = test_file.good(); - test_file.close(); - - if (!is_file) { - cfg.filter = first_arg; - first_file = 1; - } - } - - for (size_t i = first_file; i < ctx.positionals.size(); ++i) { - append_file_operand(cfg, std::string(ctx.positionals[i])); - } - - return cfg; -} - -auto read_json(const std::string& filename) -> cp::Result { - std::string content; - - if (filename.empty() || filename == "-") { - // Read from stdin - content.assign(std::istreambuf_iterator(std::cin), - std::istreambuf_iterator()); - if (std::cin.fail() && !std::cin.eof()) { - return std::unexpected("error reading from standard input"); - } - } else { - // Read from file - std::ifstream file(filename, std::ios::binary); - if (!file) { - return std::unexpected(std::string("cannot open '") + filename + - "' for reading"); - } - content.assign(std::istreambuf_iterator(file), - std::istreambuf_iterator()); - if (file.fail() && !file.eof()) { - return std::unexpected("error reading from file"); - } - } - - return content; -} - -auto run(const Config& cfg) -> int { - // Read JSON input - std::string json_input; - - if (cfg.null_input) { - json_input = "null"; - } else { - if (cfg.files.empty()) { - auto content_result = read_json(""); - if (!content_result) { - cp::report_error(content_result, L"jq"); - return 1; - } - json_input = *content_result; - } else { - for (const auto& file : cfg.files) { - auto content_result = read_json(file); - if (!content_result) { - cp::report_error(content_result, L"jq"); - return 1; - } - json_input += *content_result; - if (&file != &cfg.files.back()) { - json_input += '\n'; - } - } - } - } - - // Parse JSON with comment support enabled - nlohmann::json data; - try { - data = nlohmann::json::parse( - json_input, - /* callback */ nullptr, - /* allow exceptions */ true, - /* ignore_comments */ true // Support // and /* */ comments - ); - } catch (const nlohmann::json::exception& e) { - safePrintLn(std::string("parse error: ") + e.what()); - return 1; - } - - // Format and output JSON - int indent = cfg.compact_output ? -1 : 2; - - // Sort keys if requested - if (cfg.sort_keys && data.is_object()) { - // nlohmann::json sorts keys by default in dump() - // Re-parse to ensure sorted order - data = nlohmann::json::parse(data.dump()); - } - - auto formatted = data.dump(indent, ' ', true); // true = ensure ASCII - - // Raw output mode: extract string values without quotes - if (cfg.raw_output) { - try { - if (data.is_string()) { - formatted = data.get(); - } - } catch (const nlohmann::json::exception&) { - // Keep the formatted JSON - } - } - - // Color output mode: add ANSI color codes - if (cfg.color_output && !cfg.monochrome_output) { - // Simplified color output - wrap in ANSI codes - // In a full implementation, this would colorize different JSON elements - safePrintLn(formatted); - } else { - safePrintLn(formatted); - } - - return 0; -} - -} // namespace jq_pipeline - -REGISTER_COMMAND( - jq, "jq", "jq [OPTIONS]... FILTER [FILES...]", - "jq is a command-line JSON processor powered by nlohmann/json.\n" - "\n" - "Features:\n" - "- Parse and format JSON with nlohmann/json library\n" - "- Support for JSON comments (// and /* */)\n" - "- Pretty-printed or compact output\n" - "- Raw string output mode\n" - "- Sort keys alphabetically\n" - "\n" - "Note: This is a formatter with basic comment support.\n" - "For full jq query language features, use official jq from " - "https://jqlang.org/.", - " echo '{\"name\":\"John\",\"age\":30}' | jq\n" - " echo '{\"name\":\"John\"}' | jq '.'\n" - " echo '[1,2,3]' | jq -c\n" - " cat file.json | jq -S\n" - " cat config.json | jq # Supports // and /* */ comments", - "https://jqlang.org/manual/", "WinuxCmd", "Copyright © 2026 WinuxCmd", - JQ_OPTIONS) { - using namespace jq_pipeline; - - auto cfg_result = build_config(ctx); - if (!cfg_result) { - cp::report_error(cfg_result, L"jq"); - return 1; - } - - return run(*cfg_result); -} diff --git a/src/commands/wpm.cpp b/src/commands/wpm.cpp new file mode 100644 index 0000000..8523de0 --- /dev/null +++ b/src/commands/wpm.cpp @@ -0,0 +1,1398 @@ +/* + * Copyright (c) 2026 [caomengxuan666] + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS + * IN THE SOFTWARE. + * + * - File: wpm.cpp + * - CopyrightYear: 2026 + */ +/// @Description: Winux Package Manager internal command. +/// @Version: 0.2.0 +/// @License: MIT + +#include +#include + +#include "core/command_macros.h" +#include "pch/pch.h" + +#pragma comment(lib, "bcrypt.lib") +#pragma comment(lib, "winhttp.lib") + +import std; +import core; +import utils; +import version; + +using cmd::meta::OptionMeta; +using cmd::meta::OptionType; + +auto constexpr WPM_OPTIONS = std::array{ + OPTION("-r", "--root", "manage a specific WinuxCmd root", STRING_TYPE), + OPTION("-s", "--source", "use a specific index source", STRING_TYPE), + OPTION("-f", "--force", "overwrite existing files when safe"), + OPTION("-n", "--dry-run", "show planned changes without writing"), + OPTION("-v", "--verbose", "print detailed progress")}; + +namespace wpm { +namespace fs = std::filesystem; + +constexpr std::string_view kVersion = "0.2.0"; +constexpr std::string_view kInternalToolName = "wpm"; +constexpr std::string_view kBuiltinIndex = R"json( +{ + "schema": 1, + "name": "official", + "version": "builtin-2026.07.22", + "updated": "2026-07-22", + "sources": [ + { + "name": "official-github-raw", + "region": "global", + "priority": 10, + "index_urls": [ + "https://raw.githubusercontent.com/unixwin/WinuxCmd/main/wpm-source/index.json" + ] + }, + { + "name": "official-github-release", + "region": "global", + "priority": 20, + "index_urls": [ + "https://github.com/unixwin/WinuxCmd/releases/latest/download/wpm-index.json" + ] + }, + { + "name": "official-cn", + "region": "cn", + "priority": 30, + "index_urls": [] + } + ], + "packages": [ + { + "name": "winuxcmd", + "version": "0.12.4", + "description": "WinuxCmd core command set", + "kind": "core", + "artifacts": { + "windows-x64": { + "type": "zip", + "sha256": "", + "urls": [], + "files": [ + { "from": "winuxcmd.exe", "to": "winuxcmd.exe" } + ] + }, + "windows-arm64": { + "type": "zip", + "sha256": "", + "urls": [], + "files": [ + { "from": "winuxcmd.exe", "to": "winuxcmd.exe" } + ] + } + } + }, + { + "name": "jq", + "version": "", + "description": "External jq package slot. Fill this from a refreshed index.", + "kind": "external", + "artifacts": {} + }, + { + "name": "ncat", + "version": "", + "description": "External ncat/netcat package slot. Fill this from a refreshed index.", + "kind": "external", + "artifacts": {} + }, + { + "name": "7zip", + "version": "", + "description": "External 7-Zip package slot. Fill this from a refreshed index.", + "kind": "external", + "artifacts": {} + }, + { + "name": "zstd", + "version": "", + "description": "External zstd package slot. Fill this from a refreshed index.", + "kind": "external", + "artifacts": {} + }, + { + "name": "yq", + "version": "", + "description": "External yq package slot. Fill this from a refreshed index.", + "kind": "external", + "artifacts": {} + } + ] +} +)json"; + +struct HttpResult { + bool ok = false; + std::vector data; + DWORD status = 0; + std::string error; +}; + +struct Options { + fs::path root; + std::string source; + bool force = false; + bool dry_run = false; + bool verbose = false; +}; + +struct FileId { + DWORD volume = 0; + DWORD high = 0; + DWORD low = 0; +}; + +auto lower_ascii(std::string s) -> std::string { + std::ranges::transform(s, s.begin(), [](unsigned char c) { + return static_cast(std::tolower(c)); + }); + return s; +} + +auto starts_with_ci(std::string_view text, std::string_view prefix) -> bool { + if (text.size() < prefix.size()) return false; + for (size_t i = 0; i < prefix.size(); ++i) { + if (std::tolower(static_cast(text[i])) != + std::tolower(static_cast(prefix[i]))) { + return false; + } + } + return true; +} + +auto exe_suffix(std::string_view name) -> std::string { + std::string out(name); + if (!out.ends_with(".exe")) out += ".exe"; + return out; +} + +auto current_exe_path() -> fs::path { + std::wstring buffer(MAX_PATH, L'\0'); + DWORD size = GetModuleFileNameW(nullptr, buffer.data(), + static_cast(buffer.size())); + while (size == buffer.size()) { + buffer.resize(buffer.size() * 2, L'\0'); + size = GetModuleFileNameW(nullptr, buffer.data(), + static_cast(buffer.size())); + } + buffer.resize(size); + return fs::path(buffer); +} + +auto default_root() -> fs::path { + auto exe = current_exe_path(); + return exe.has_parent_path() ? exe.parent_path() : fs::current_path(); +} + +auto state_dir(const fs::path& root) -> fs::path { return root / ".wpm"; } +auto cache_dir(const fs::path& root) -> fs::path { + return state_dir(root) / "cache"; +} +auto index_dir(const fs::path& root) -> fs::path { + return state_dir(root) / "indexes"; +} +auto staging_dir(const fs::path& root) -> fs::path { + return state_dir(root) / "staging"; +} +auto backup_dir(const fs::path& root) -> fs::path { + return state_dir(root) / "backup"; +} +auto config_path(const fs::path& root) -> fs::path { + return state_dir(root) / "config.json"; +} +auto local_index_path(const fs::path& root) -> fs::path { + return index_dir(root) / "official.json"; +} +auto bundled_index_path(const fs::path& root) -> fs::path { + return root / "wpm-source" / "index.json"; +} + +auto read_text(const fs::path& path) -> std::optional { + std::ifstream in(path, std::ios::binary); + if (!in.is_open()) return std::nullopt; + return std::string(std::istreambuf_iterator(in), + std::istreambuf_iterator()); +} + +auto write_text(const fs::path& path, std::string_view text) -> bool { + std::error_code ec; + fs::create_directories(path.parent_path(), ec); + std::ofstream out(path, std::ios::binary | std::ios::trunc); + if (!out.is_open()) return false; + out.write(text.data(), static_cast(text.size())); + return out.good(); +} + +auto write_bytes(const fs::path& path, std::span data) + -> bool { + std::error_code ec; + fs::create_directories(path.parent_path(), ec); + std::ofstream out(path, std::ios::binary | std::ios::trunc); + if (!out.is_open()) return false; + out.write(reinterpret_cast(data.data()), + static_cast(data.size())); + return out.good(); +} + +auto parse_json_text(std::string_view text) -> std::optional { + try { + return nlohmann::json::parse(text.begin(), text.end()); + } catch (const std::exception& e) { + safeErrorPrintLn(std::string("wpm: invalid JSON: ") + e.what()); + return std::nullopt; + } +} + +auto load_config(const fs::path& root) -> nlohmann::json { + if (auto text = read_text(config_path(root))) { + if (auto parsed = parse_json_text(*text)) return *parsed; + } + return nlohmann::json{{"preferred_source", "auto"}, + {"region", "auto"}, + {"user_sources", nlohmann::json::array()}}; +} + +auto save_config(const fs::path& root, const nlohmann::json& config) -> bool { + return write_text(config_path(root), config.dump(2)); +} + +auto load_index(const fs::path& root) -> nlohmann::json { + if (auto text = read_text(local_index_path(root))) { + if (auto parsed = parse_json_text(*text)) return *parsed; + } + if (auto text = read_text(bundled_index_path(root))) { + if (auto parsed = parse_json_text(*text)) return *parsed; + } + if (auto parsed = parse_json_text(kBuiltinIndex)) return *parsed; + return nlohmann::json::object(); +} + +auto merged_sources(const fs::path& root, const nlohmann::json& index) + -> std::vector { + std::vector sources; + if (index.contains("sources") && index["sources"].is_array()) { + for (const auto& source : index["sources"]) sources.push_back(source); + } + + auto config = load_config(root); + if (config.contains("user_sources") && config["user_sources"].is_array()) { + for (const auto& source : config["user_sources"]) sources.push_back(source); + } + + std::ranges::sort( + sources, [](const nlohmann::json& a, const nlohmann::json& b) { + return a.value("priority", 1000) < b.value("priority", 1000); + }); + return sources; +} + +auto package_array(const nlohmann::json& index) -> std::vector { + std::vector out; + if (!index.contains("packages") || !index["packages"].is_array()) return out; + for (const auto& pkg : index["packages"]) { + if (pkg.is_object()) out.push_back(pkg); + } + return out; +} + +auto find_package(const nlohmann::json& index, std::string_view name) + -> std::optional { + auto wanted = lower_ascii(std::string(name)); + for (const auto& pkg : package_array(index)) { + if (lower_ascii(pkg.value("name", "")) == wanted) return pkg; + } + return std::nullopt; +} + +auto detect_arch_key() -> std::string { + SYSTEM_INFO info{}; + GetNativeSystemInfo(&info); + switch (info.wProcessorArchitecture) { + case PROCESSOR_ARCHITECTURE_ARM64: + return "windows-arm64"; + case PROCESSOR_ARCHITECTURE_AMD64: + default: + return "windows-x64"; + } +} + +auto file_id(const fs::path& path) -> std::optional { + HANDLE h = + CreateFileW(path.wstring().c_str(), FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (h == INVALID_HANDLE_VALUE) return std::nullopt; + + BY_HANDLE_FILE_INFORMATION info{}; + if (!GetFileInformationByHandle(h, &info)) { + CloseHandle(h); + return std::nullopt; + } + CloseHandle(h); + return FileId{info.dwVolumeSerialNumber, info.nFileIndexHigh, + info.nFileIndexLow}; +} + +auto same_file(const fs::path& a, const fs::path& b) -> bool { + auto fa = file_id(a); + auto fb = file_id(b); + return fa && fb && fa->volume == fb->volume && fa->high == fb->high && + fa->low == fb->low; +} + +auto normalized_path_key(const fs::path& path) -> std::wstring { + std::error_code ec; + fs::path normalized = fs::weakly_canonical(path, ec); + if (ec) { + ec.clear(); + normalized = fs::absolute(path, ec); + if (ec) normalized = path; + } + normalized = normalized.lexically_normal(); + + std::wstring key = normalized.wstring(); + std::ranges::replace(key, L'/', L'\\'); + std::ranges::transform(key, key.begin(), + [](wchar_t ch) { return std::towlower(ch); }); + return key; +} + +auto same_path_name(const fs::path& a, const fs::path& b) -> bool { + return normalized_path_key(a) == normalized_path_key(b); +} + +auto win32_error_text(DWORD error) -> std::string { + LPWSTR buffer = nullptr; + DWORD size = FormatMessageW( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | + FORMAT_MESSAGE_IGNORE_INSERTS, + nullptr, error, 0, reinterpret_cast(&buffer), 0, nullptr); + if (size == 0 || buffer == nullptr) { + return std::system_category().message(static_cast(error)); + } + std::wstring text(buffer, size); + LocalFree(buffer); + while (!text.empty() && (text.back() == L'\r' || text.back() == L'\n' || + text.back() == L' ' || text.back() == L'\t')) { + text.pop_back(); + } + return wstring_to_utf8(text); +} + +auto command_names() -> std::vector { + std::vector names; + for (const auto& [name, desc] : CommandRegistry::getAllCommands()) { + (void)desc; + names.emplace_back(name); + } + if (std::ranges::find(names, std::string(kInternalToolName)) == names.end()) { + names.emplace_back(kInternalToolName); + } + std::ranges::sort(names); + names.erase(std::ranges::unique(names).begin(), names.end()); + return names; +} + +auto legacy_link_names() -> std::vector { + // Removed built-in commands that may still exist as hardlinks in old + // installs. + return {"jq"}; +} + +auto is_legacy_link_name(std::string_view name) -> bool { + std::string wanted = exe_suffix(name); + wanted = lower_ascii(wanted); + for (const auto& legacy : legacy_link_names()) { + if (wanted == lower_ascii(exe_suffix(legacy))) return true; + } + return false; +} + +auto cleanup_link_names() -> std::vector { + auto names = command_names(); + for (const auto& legacy : legacy_link_names()) names.emplace_back(legacy); + std::ranges::sort(names); + names.erase(std::ranges::unique(names).begin(), names.end()); + return names; +} + +auto remove_link_if_safe(const fs::path& source, const fs::path& target, + const fs::path& current, bool force, bool dry_run) + -> bool { + std::error_code ec; + if (!fs::exists(target, ec)) return true; + if (same_file(source, target)) return true; + if (same_path_name(current, target)) { + safePrintLn("wpm: keeping running executable: " + target.string()); + return true; + } + if (dry_run) return true; + if (fs::is_directory(target, ec)) { + safeErrorPrintLn("wpm: refusing to replace directory: " + target.string()); + return false; + } + if (!DeleteFileW(target.wstring().c_str())) { + safeErrorPrintLn("wpm: failed to remove '" + target.string() + + "': " + win32_error_text(GetLastError())); + return false; + } + return true; +} + +auto remove_stale_legacy_links(const fs::path& root, const fs::path& source, + const fs::path& current, bool dry_run, + bool verbose) -> std::pair { + const auto current_names = command_names(); + int removed = 0; + int failed = 0; + + for (const auto& name : legacy_link_names()) { + if (std::ranges::find(current_names, name) != current_names.end()) continue; + fs::path target = root / exe_suffix(name); + if (same_path_name(current, target)) continue; + std::error_code ec; + if (!fs::exists(target, ec)) continue; + if (!same_file(source, target)) continue; + + if (dry_run) { + safePrintLn("remove legacy link " + target.string()); + ++removed; + continue; + } + if (DeleteFileW(target.wstring().c_str())) { + ++removed; + if (verbose) safePrintLn("removed legacy link " + target.string()); + } else { + safeErrorPrintLn("wpm: failed to remove legacy link '" + target.string() + + "': " + win32_error_text(GetLastError())); + ++failed; + } + } + + return {removed, failed}; +} + +auto rebuild_links(const fs::path& root, bool force, bool dry_run, bool verbose) + -> int { + const fs::path source = fs::exists(root / "winuxcmd.exe") + ? root / "winuxcmd.exe" + : current_exe_path(); + const fs::path current = current_exe_path(); + + if (!fs::exists(source)) { + safeErrorPrintLn("wpm: winuxcmd.exe not found in root: " + root.string()); + return 1; + } + + int created = 0; + int unchanged = 0; + int failed = 0; + auto [stale_removed, stale_failed] = + remove_stale_legacy_links(root, source, current, dry_run, verbose); + failed += stale_failed; + + for (const auto& name : command_names()) { + fs::path target = root / exe_suffix(name); + if (same_file(source, target)) { + ++unchanged; + continue; + } + if (!remove_link_if_safe(source, target, current, force, dry_run)) { + ++failed; + continue; + } + if (dry_run) { + safePrintLn("link " + target.string() + " -> " + source.string()); + ++created; + continue; + } + if (CreateHardLinkW(target.wstring().c_str(), source.wstring().c_str(), + nullptr)) { + ++created; + if (verbose) safePrintLn("linked " + target.string()); + } else { + DWORD err = GetLastError(); + if (same_file(source, target)) { + ++unchanged; + continue; + } + safeErrorPrintLn("wpm: failed to create hard link '" + target.string() + + "': " + win32_error_text(err)); + ++failed; + } + } + + safePrintLn("wpm: links created=" + std::to_string(created) + + " unchanged=" + std::to_string(unchanged) + + " stale_removed=" + std::to_string(stale_removed) + + " failed=" + std::to_string(failed)); + return failed == 0 ? 0 : 1; +} + +auto remove_links(const fs::path& root, bool dry_run) -> int { + fs::path source = fs::exists(root / "winuxcmd.exe") ? root / "winuxcmd.exe" + : current_exe_path(); + fs::path current = current_exe_path(); + int removed = 0; + int failed = 0; + + for (const auto& name : cleanup_link_names()) { + fs::path target = root / exe_suffix(name); + if (same_path_name(current, target)) continue; + std::error_code ec; + if (!fs::exists(target, ec)) continue; + if (!same_file(source, target)) continue; + if (dry_run) { + safePrintLn("remove " + target.string()); + ++removed; + continue; + } + if (DeleteFileW(target.wstring().c_str())) { + ++removed; + } else { + safeErrorPrintLn("wpm: failed to remove '" + target.string() + + "': " + win32_error_text(GetLastError())); + ++failed; + } + } + + safePrintLn("wpm: links removed=" + std::to_string(removed) + + " failed=" + std::to_string(failed)); + return failed == 0 ? 0 : 1; +} + +auto http_get(std::string_view url) -> HttpResult { + HttpResult result; + + if (starts_with_ci(url, "file://")) { + std::string raw(url.substr(7)); + if (raw.size() >= 3 && raw[0] == '/' && raw[2] == ':') + raw.erase(raw.begin()); + std::ifstream in{fs::path(raw), std::ios::binary}; + if (!in.is_open()) { + result.error = "failed to open local file source"; + return result; + } + std::string payload{std::istreambuf_iterator(in), + std::istreambuf_iterator()}; + result.data.resize(payload.size()); + std::memcpy(result.data.data(), payload.data(), payload.size()); + result.ok = true; + result.status = 200; + return result; + } + + std::wstring wurl = utf8_to_wstring(std::string(url)); + URL_COMPONENTS parts{}; + parts.dwStructSize = sizeof(parts); + parts.dwSchemeLength = static_cast(-1); + parts.dwHostNameLength = static_cast(-1); + parts.dwUrlPathLength = static_cast(-1); + parts.dwExtraInfoLength = static_cast(-1); + if (!WinHttpCrackUrl(wurl.c_str(), 0, 0, &parts)) { + result.error = "failed to parse URL"; + return result; + } + + std::wstring host(parts.lpszHostName, parts.dwHostNameLength); + std::wstring path_part; + if (parts.lpszUrlPath) { + path_part.assign(parts.lpszUrlPath, parts.dwUrlPathLength); + } + if (parts.lpszExtraInfo) { + path_part.append(parts.lpszExtraInfo, parts.dwExtraInfoLength); + } + if (path_part.empty()) path_part = L"/"; + + bool https = parts.nScheme == INTERNET_SCHEME_HTTPS; + HINTERNET session = + WinHttpOpen(L"WinuxCmd-WPM/0.2", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, + WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); + if (!session) { + result.error = "failed to open WinHTTP session"; + return result; + } + + HINTERNET connect = WinHttpConnect(session, host.c_str(), parts.nPort, 0); + if (!connect) { + result.error = "failed to connect"; + WinHttpCloseHandle(session); + return result; + } + + DWORD flags = https ? WINHTTP_FLAG_SECURE : 0; + HINTERNET request = WinHttpOpenRequest(connect, L"GET", path_part.c_str(), + nullptr, WINHTTP_NO_REFERER, + WINHTTP_DEFAULT_ACCEPT_TYPES, flags); + if (!request) { + result.error = "failed to create request"; + WinHttpCloseHandle(connect); + WinHttpCloseHandle(session); + return result; + } + + DWORD timeout_ms = 12000; + WinHttpSetOption(request, WINHTTP_OPTION_CONNECT_TIMEOUT, &timeout_ms, + sizeof(timeout_ms)); + WinHttpSetOption(request, WINHTTP_OPTION_RECEIVE_TIMEOUT, &timeout_ms, + sizeof(timeout_ms)); + WinHttpSetOption(request, WINHTTP_OPTION_SEND_TIMEOUT, &timeout_ms, + sizeof(timeout_ms)); + + if (!WinHttpSendRequest(request, WINHTTP_NO_ADDITIONAL_HEADERS, 0, + WINHTTP_NO_REQUEST_DATA, 0, 0, 0) || + !WinHttpReceiveResponse(request, nullptr)) { + result.error = "request failed: " + win32_error_text(GetLastError()); + WinHttpCloseHandle(request); + WinHttpCloseHandle(connect); + WinHttpCloseHandle(session); + return result; + } + + DWORD status = 0; + DWORD status_size = sizeof(status); + WinHttpQueryHeaders(request, + WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER, + WINHTTP_HEADER_NAME_BY_INDEX, &status, &status_size, + WINHTTP_NO_HEADER_INDEX); + result.status = status; + if (status < 200 || status >= 300) { + result.error = "HTTP status " + std::to_string(status); + WinHttpCloseHandle(request); + WinHttpCloseHandle(connect); + WinHttpCloseHandle(session); + return result; + } + + for (;;) { + DWORD available = 0; + if (!WinHttpQueryDataAvailable(request, &available)) break; + if (available == 0) break; + size_t old_size = result.data.size(); + result.data.resize(old_size + available); + DWORD read = 0; + if (!WinHttpReadData(request, result.data.data() + old_size, available, + &read)) { + result.error = "read failed: " + win32_error_text(GetLastError()); + result.data.clear(); + break; + } + result.data.resize(old_size + read); + } + + result.ok = result.error.empty(); + WinHttpCloseHandle(request); + WinHttpCloseHandle(connect); + WinHttpCloseHandle(session); + return result; +} + +auto sha256_file(const fs::path& file) -> std::optional { + std::ifstream in(file, std::ios::binary); + if (!in.is_open()) return std::nullopt; + + BCRYPT_ALG_HANDLE alg = nullptr; + BCRYPT_HASH_HANDLE hash = nullptr; + DWORD object_size = 0; + DWORD data_size = 0; + DWORD hash_size = 0; + + if (BCryptOpenAlgorithmProvider(&alg, BCRYPT_SHA256_ALGORITHM, nullptr, 0) != + 0) { + return std::nullopt; + } + BCryptGetProperty(alg, BCRYPT_OBJECT_LENGTH, + reinterpret_cast(&object_size), sizeof(object_size), + &data_size, 0); + BCryptGetProperty(alg, BCRYPT_HASH_LENGTH, + reinterpret_cast(&hash_size), sizeof(hash_size), + &data_size, 0); + + std::vector object(object_size); + std::vector digest(hash_size); + if (BCryptCreateHash(alg, &hash, object.data(), object_size, nullptr, 0, 0) != + 0) { + BCryptCloseAlgorithmProvider(alg, 0); + return std::nullopt; + } + + std::array buffer{}; + while (in.good()) { + in.read(buffer.data(), static_cast(buffer.size())); + auto got = in.gcount(); + if (got > 0) { + BCryptHashData(hash, reinterpret_cast(buffer.data()), + static_cast(got), 0); + } + } + BCryptFinishHash(hash, digest.data(), hash_size, 0); + BCryptDestroyHash(hash); + BCryptCloseAlgorithmProvider(alg, 0); + + static constexpr char hex[] = "0123456789abcdef"; + std::string out; + out.reserve(digest.size() * 2); + for (auto byte : digest) { + out.push_back(hex[(byte >> 4) & 0x0f]); + out.push_back(hex[byte & 0x0f]); + } + return out; +} + +auto verify_sha256(const fs::path& file, std::string expected) -> bool { + if (expected.empty()) { + safeErrorPrintLn("wpm: refusing remote artifact without sha256"); + return false; + } + expected = lower_ascii(expected); + auto actual = sha256_file(file); + if (!actual) { + safeErrorPrintLn("wpm: failed to calculate sha256 for " + file.string()); + return false; + } + if (*actual != expected) { + safeErrorPrintLn("wpm: sha256 mismatch for " + file.string()); + safeErrorPrintLn("wpm: expected " + expected); + safeErrorPrintLn("wpm: actual " + *actual); + return false; + } + return true; +} + +auto run_process(const std::wstring& command, const fs::path& cwd = {}) -> int { + std::wstring mutable_cmd = command; + STARTUPINFOW si{}; + si.cb = sizeof(si); + PROCESS_INFORMATION pi{}; + BOOL ok = CreateProcessW( + nullptr, mutable_cmd.data(), nullptr, nullptr, FALSE, CREATE_NO_WINDOW, + nullptr, cwd.empty() ? nullptr : cwd.wstring().c_str(), &si, &pi); + if (!ok) return static_cast(GetLastError()); + WaitForSingleObject(pi.hProcess, INFINITE); + DWORD exit_code = 1; + GetExitCodeProcess(pi.hProcess, &exit_code); + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + return static_cast(exit_code); +} + +auto quote(const fs::path& p) -> std::wstring { + std::wstring s = p.wstring(); + std::wstring out = L"\""; + for (wchar_t ch : s) { + if (ch == L'"') out.push_back(L'\\'); + out.push_back(ch); + } + out.push_back(L'"'); + return out; +} + +auto extract_zip(const fs::path& zip, const fs::path& dest) -> bool { + std::error_code ec; + fs::create_directories(dest, ec); + std::wstring command = L"tar.exe -xf " + quote(zip) + L" -C " + quote(dest); + int code = run_process(command); + if (code != 0) { + safeErrorPrintLn("wpm: zip extraction failed; Windows tar.exe returned " + + std::to_string(code)); + return false; + } + return true; +} + +auto find_file_recursive(const fs::path& root, std::string_view filename) + -> std::optional { + std::error_code ec; + if (!fs::exists(root, ec)) return std::nullopt; + for (const auto& entry : fs::recursive_directory_iterator(root, ec)) { + if (ec) break; + if (!entry.is_regular_file(ec)) continue; + if (lower_ascii(entry.path().filename().string()) == + lower_ascii(std::string(filename))) { + return entry.path(); + } + } + return std::nullopt; +} + +auto find_artifact_file(const fs::path& root, std::string_view from) + -> std::optional { + fs::path requested = fs::path(std::string(from)); + std::error_code ec; + auto direct = root / requested; + if (fs::is_regular_file(direct, ec)) return direct; + return find_file_recursive(root, requested.filename().string()); +} + +auto artifact_for_current_arch(const nlohmann::json& pkg) + -> std::optional { + if (!pkg.contains("artifacts") || !pkg["artifacts"].is_object()) + return std::nullopt; + std::string key = detect_arch_key(); + if (pkg["artifacts"].contains(key)) return pkg["artifacts"][key]; + return std::nullopt; +} + +auto artifact_urls(const nlohmann::json& artifact) -> std::vector { + std::vector urls; + if (!artifact.contains("urls")) return urls; + if (artifact["urls"].is_array()) { + for (const auto& item : artifact["urls"]) { + if (item.is_string()) urls.push_back(item.get()); + if (item.is_object() && item.contains("url")) + urls.push_back(item["url"].get()); + } + } + return urls; +} + +auto download_artifact(const fs::path& root, const std::string& package, + const nlohmann::json& artifact, bool verbose) + -> std::optional { + auto urls = artifact_urls(artifact); + if (urls.empty()) { + safeErrorPrintLn("wpm: package '" + package + + "' has no URLs in the local index"); + safeErrorPrintLn("wpm: run 'wpm index update' or choose another source"); + return std::nullopt; + } + + std::string type = artifact.value("type", "exe"); + fs::path out = cache_dir(root) / (package + "." + type); + for (const auto& url : urls) { + if (verbose) safePrintLn("wpm: downloading " + url); + auto result = http_get(url); + if (!result.ok) { + safeErrorPrintLn("wpm: download failed from " + url + ": " + + result.error); + continue; + } + if (!write_bytes(out, result.data)) { + safeErrorPrintLn("wpm: failed to write cache file " + out.string()); + continue; + } + if (!verify_sha256(out, artifact.value("sha256", ""))) { + std::error_code ec; + fs::remove(out, ec); + continue; + } + return out; + } + + return std::nullopt; +} + +auto copy_artifact_files(const fs::path& extracted, const fs::path& root, + const nlohmann::json& artifact, bool force, + bool dry_run) -> bool { + if (!artifact.contains("files") || !artifact["files"].is_array()) { + safeErrorPrintLn("wpm: artifact has no files mapping"); + return false; + } + + for (const auto& mapping : artifact["files"]) { + std::string from = mapping.value("from", ""); + std::string to = mapping.contains("to") && mapping["to"].is_string() + ? mapping["to"].get() + : fs::path(from).filename().string(); + if (from.empty() || to.empty()) { + safeErrorPrintLn("wpm: invalid file mapping in artifact"); + return false; + } + + auto src = find_artifact_file(extracted, from); + if (!src) { + safeErrorPrintLn("wpm: extracted file not found: " + from); + return false; + } + + fs::path dest = root / to; + std::error_code ec; + bool dest_exists = fs::exists(dest, ec); + bool dest_is_winux_link = dest_exists && + same_file(root / "winuxcmd.exe", dest) && + !same_path_name(root / "winuxcmd.exe", dest); + bool dest_is_legacy_link = is_legacy_link_name(dest.filename().string()); + if (dest_exists && !dest_is_winux_link && !force && + !same_file(*src, dest)) { + safeErrorPrintLn("wpm: destination exists; use --force: " + + dest.string()); + return false; + } + if (dest_is_winux_link && !force && !dest_is_legacy_link) { + safeErrorPrintLn( + "wpm: destination is a WinuxCmd hardlink; use --force: " + + dest.string()); + return false; + } + if (dry_run) { + safePrintLn("install " + src->string() + " -> " + dest.string()); + continue; + } + fs::create_directories(dest.parent_path(), ec); + if (dest_is_winux_link) { + if (!DeleteFileW(dest.wstring().c_str())) { + safeErrorPrintLn("wpm: failed to break WinuxCmd hardlink '" + + dest.string() + + "': " + win32_error_text(GetLastError())); + return false; + } + } + fs::copy_file(*src, dest, fs::copy_options::overwrite_existing, ec); + if (ec) { + safeErrorPrintLn("wpm: failed to copy '" + src->string() + "' to '" + + dest.string() + "': " + ec.message()); + return false; + } + } + return true; +} + +auto install_package(const Options& opts, std::string_view package_name) + -> int { + auto index = load_index(opts.root); + auto pkg = find_package(index, package_name); + if (!pkg) { + safeErrorPrintLn("wpm: package not found: " + std::string(package_name)); + return 1; + } + auto artifact = artifact_for_current_arch(*pkg); + if (!artifact) { + safeErrorPrintLn("wpm: no artifact for " + detect_arch_key()); + return 1; + } + auto downloaded = download_artifact(opts.root, pkg->value("name", ""), + *artifact, opts.verbose); + if (!downloaded) return 1; + + fs::path extracted = staging_dir(opts.root) / pkg->value("name", "package"); + std::error_code ec; + fs::remove_all(extracted, ec); + fs::create_directories(extracted, ec); + + std::string type = artifact->value("type", "exe"); + if (type == "zip") { + if (!extract_zip(*downloaded, extracted)) return 1; + } else if (type == "exe") { + fs::copy_file(*downloaded, extracted / downloaded->filename(), + fs::copy_options::overwrite_existing, ec); + if (ec) { + safeErrorPrintLn("wpm: failed to stage exe: " + ec.message()); + return 1; + } + } else { + safeErrorPrintLn("wpm: unsupported artifact type: " + type); + return 1; + } + + if (!copy_artifact_files(extracted, opts.root, *artifact, opts.force, + opts.dry_run)) { + return 1; + } + safePrintLn("wpm: installed " + + pkg->value("name", std::string(package_name))); + return 0; +} + +auto launch_apply_update(const fs::path& staged_exe, const fs::path& root, + DWORD parent_pid) -> bool { + std::wstring cmd = quote(staged_exe) + L" wpm -- __apply-update --root " + + quote(root) + L" --payload " + quote(staged_exe) + + L" --parent " + std::to_wstring(parent_pid); + STARTUPINFOW si{}; + si.cb = sizeof(si); + PROCESS_INFORMATION pi{}; + BOOL ok = CreateProcessW(staged_exe.wstring().c_str(), cmd.data(), nullptr, + nullptr, FALSE, CREATE_NO_WINDOW, nullptr, + root.wstring().c_str(), &si, &pi); + if (!ok) { + safeErrorPrintLn("wpm: failed to launch update helper: " + + win32_error_text(GetLastError())); + return false; + } + CloseHandle(pi.hThread); + CloseHandle(pi.hProcess); + return true; +} + +auto update_winuxcmd(const Options& opts) -> int { + auto index = load_index(opts.root); + auto pkg = find_package(index, "winuxcmd"); + if (!pkg) { + safeErrorPrintLn("wpm: winuxcmd package missing from index"); + return 1; + } + auto artifact = artifact_for_current_arch(*pkg); + if (!artifact) { + safeErrorPrintLn("wpm: no winuxcmd artifact for " + detect_arch_key()); + return 1; + } + + auto downloaded = + download_artifact(opts.root, "winuxcmd", *artifact, opts.verbose); + if (!downloaded) return 1; + + fs::path extracted = staging_dir(opts.root) / "winuxcmd-update"; + std::error_code ec; + fs::remove_all(extracted, ec); + fs::create_directories(extracted, ec); + + if (artifact->value("type", "zip") == "zip") { + if (!extract_zip(*downloaded, extracted)) return 1; + } else { + fs::copy_file(*downloaded, extracted / "winuxcmd.exe", + fs::copy_options::overwrite_existing, ec); + if (ec) { + safeErrorPrintLn("wpm: failed to stage winuxcmd.exe: " + ec.message()); + return 1; + } + } + + auto staged_exe = find_file_recursive(extracted, "winuxcmd.exe"); + if (!staged_exe) { + safeErrorPrintLn("wpm: staged winuxcmd.exe not found"); + return 1; + } + + if (opts.dry_run) { + safePrintLn("would apply update from " + staged_exe->string()); + return 0; + } + + if (!launch_apply_update(*staged_exe, opts.root, GetCurrentProcessId())) { + return 1; + } + safePrintLn("wpm: update staged; helper will replace winuxcmd after exit"); + return 0; +} + +auto parse_hidden_value(std::span args, + std::string_view name) -> std::string { + for (size_t i = 0; i + 1 < args.size(); ++i) { + if (args[i] == name) return std::string(args[i + 1]); + } + return {}; +} + +auto apply_update(std::span args) -> int { + fs::path root = parse_hidden_value(args, "--root"); + fs::path payload = parse_hidden_value(args, "--payload"); + std::string parent_text = parse_hidden_value(args, "--parent"); + if (root.empty() || payload.empty() || parent_text.empty()) { + safeErrorPrintLn("wpm: invalid apply-update invocation"); + return 1; + } + + DWORD parent_pid = static_cast(std::stoul(parent_text)); + HANDLE parent = OpenProcess(SYNCHRONIZE, FALSE, parent_pid); + if (parent) { + WaitForSingleObject(parent, 30000); + CloseHandle(parent); + } + + std::error_code ec; + fs::create_directories(backup_dir(root), ec); + fs::path target = root / "winuxcmd.exe"; + fs::path backup = + backup_dir(root) / + ("winuxcmd-" + std::string(WinuxCmd::VERSION_STRING) + ".exe"); + if (fs::exists(target, ec)) { + fs::copy_file(target, backup, fs::copy_options::overwrite_existing, ec); + if (ec) { + safeErrorPrintLn("wpm: failed to backup winuxcmd.exe: " + ec.message()); + return 1; + } + } + + remove_links(root, false); + fs::copy_file(payload, target, fs::copy_options::overwrite_existing, ec); + if (ec) { + safeErrorPrintLn("wpm: failed to replace winuxcmd.exe: " + ec.message()); + if (fs::exists(backup, ec)) { + fs::copy_file(backup, target, fs::copy_options::overwrite_existing, ec); + } + return 1; + } + return rebuild_links(root, true, false, false); +} + +auto update_index(const Options& opts) -> int { + auto current = load_index(opts.root); + auto sources = merged_sources(opts.root, current); + std::string wanted = opts.source.empty() ? load_config(opts.root).value( + "preferred_source", "auto") + : opts.source; + + for (const auto& source : sources) { + std::string name = source.value("name", ""); + if (name.empty()) continue; + if (wanted != "auto" && wanted != name) continue; + if (!source.contains("index_urls") || !source["index_urls"].is_array()) + continue; + for (const auto& url_json : source["index_urls"]) { + if (!url_json.is_string()) continue; + std::string url = url_json.get(); + if (opts.verbose) safePrintLn("wpm: fetching index " + url); + auto result = http_get(url); + if (!result.ok) { + safeErrorPrintLn("wpm: index fetch failed from " + name + ": " + + result.error); + continue; + } + std::string text(reinterpret_cast(result.data.data()), + result.data.size()); + auto parsed = parse_json_text(text); + if (!parsed || !parsed->contains("packages")) { + safeErrorPrintLn("wpm: fetched index is missing packages"); + continue; + } + if (!write_text(local_index_path(opts.root), parsed->dump(2))) { + safeErrorPrintLn("wpm: failed to save index"); + return 1; + } + auto config = load_config(opts.root); + config["last_success_source"] = name; + save_config(opts.root, config); + safePrintLn("wpm: index updated from " + name); + return 0; + } + } + + safeErrorPrintLn("wpm: no reachable index source"); + return 1; +} + +auto print_index_status(const Options& opts) -> int { + auto index = load_index(opts.root); + auto packages = package_array(index); + safePrintLn("WPM index"); + safePrintLn(" root: " + opts.root.string()); + safePrintLn(" local: " + local_index_path(opts.root).string()); + safePrintLn(" bundled: " + bundled_index_path(opts.root).string()); + safePrintLn(" version: " + index.value("version", "unknown")); + safePrintLn(" packages: " + std::to_string(packages.size())); + return 0; +} + +auto list_sources(const Options& opts) -> int { + auto index = load_index(opts.root); + auto config = load_config(opts.root); + auto preferred = config.value("preferred_source", "auto"); + safePrintLn("WPM sources (preferred: " + preferred + ")"); + for (const auto& source : merged_sources(opts.root, index)) { + std::string name = source.value("name", ""); + std::string region = source.value("region", ""); + size_t urls = + source.contains("index_urls") && source["index_urls"].is_array() + ? source["index_urls"].size() + : 0; + safePrintLn(" " + name + " region=" + region + + " urls=" + std::to_string(urls)); + } + return 0; +} + +auto source_use(const Options& opts, std::string_view name) -> int { + auto config = load_config(opts.root); + config["preferred_source"] = std::string(name); + if (!save_config(opts.root, config)) { + safeErrorPrintLn("wpm: failed to save source preference"); + return 1; + } + safePrintLn("wpm: preferred source set to " + std::string(name)); + return 0; +} + +auto source_add(const Options& opts, std::string_view name, + std::string_view url) -> int { + auto config = load_config(opts.root); + if (!config.contains("user_sources") || !config["user_sources"].is_array()) { + config["user_sources"] = nlohmann::json::array(); + } + config["user_sources"].push_back(nlohmann::json{ + {"name", std::string(name)}, + {"region", "custom"}, + {"priority", 5}, + {"index_urls", nlohmann::json::array({std::string(url)})}}); + if (!save_config(opts.root, config)) { + safeErrorPrintLn("wpm: failed to save source"); + return 1; + } + safePrintLn("wpm: source added: " + std::string(name)); + return 0; +} + +auto list_packages(const Options& opts) -> int { + auto index = load_index(opts.root); + for (const auto& pkg : package_array(index)) { + std::string version = pkg.value("version", ""); + if (!version.empty()) version = " " + version; + safePrintLn(" " + pkg.value("name", "") + version + " - " + + pkg.value("description", "")); + } + return 0; +} + +auto show_info(const Options& opts, std::string_view name) -> int { + auto index = load_index(opts.root); + auto pkg = find_package(index, name); + if (!pkg) { + safeErrorPrintLn("wpm: package not found: " + std::string(name)); + return 1; + } + safePrintLn("Name: " + pkg->value("name", "")); + safePrintLn("Version: " + pkg->value("version", "")); + safePrintLn("Kind: " + pkg->value("kind", "")); + safePrintLn("Description: " + pkg->value("description", "")); + auto artifact = artifact_for_current_arch(*pkg); + safePrintLn("Artifact: " + + std::string(artifact ? detect_arch_key() : "none")); + if (artifact) { + safePrintLn("Type: " + artifact->value("type", "")); + safePrintLn("URLs: " + std::to_string(artifact_urls(*artifact).size())); + } + return 0; +} + +auto print_usage() -> int { + safePrintLn("Winux Package Manager " + std::string(kVersion)); + safePrintLn("Usage: wpm [args] [options]"); + safePrintLn(""); + safePrintLn("Commands:"); + safePrintLn(" links list|rebuild|remove manage WinuxCmd hardlinks"); + safePrintLn(" index status|update inspect or refresh local index"); + safePrintLn(" source list|use|add manage index sources"); + safePrintLn(" list list indexed packages"); + safePrintLn(" info show package metadata"); + safePrintLn( + " install install package from local index"); + safePrintLn( + " update winuxcmd update WinuxCmd from local index"); + return 0; +} + +auto build_options(const CommandContext& ctx) -> Options { + Options opts; + std::string root = ctx.get("--root", ""); + if (root.empty()) root = ctx.get("-r", ""); + opts.root = root.empty() ? default_root() : fs::path(root); + opts.source = ctx.get("--source", ""); + if (opts.source.empty()) opts.source = ctx.get("-s", ""); + opts.force = ctx.get("--force", false) || ctx.get("-f", false); + opts.dry_run = + ctx.get("--dry-run", false) || ctx.get("-n", false); + opts.verbose = + ctx.get("--verbose", false) || ctx.get("-v", false); + return opts; +} + +auto dispatch(const Options& opts, std::span args) + -> int { + if (args.empty()) return print_usage(); + + if (args[0] == "__apply-update") { + return apply_update(args.subspan(1)); + } + + if (args[0] == "links") { + if (args.size() == 1 || args[1] == "list") { + for (const auto& name : command_names()) safePrintLn(name); + return 0; + } + if (args[1] == "rebuild") { + return rebuild_links(opts.root, opts.force, opts.dry_run, opts.verbose); + } + if (args[1] == "remove") { + return remove_links(opts.root, opts.dry_run); + } + safeErrorPrintLn("wpm: usage: wpm links list|rebuild|remove"); + return 1; + } + + if (args[0] == "index" || args[0] == "update-index") { + if (args[0] == "update-index" || (args.size() >= 2 && args[1] == "update")) + return update_index(opts); + if (args.size() == 1 || args[1] == "status") + return print_index_status(opts); + safeErrorPrintLn("wpm: usage: wpm index status|update"); + return 1; + } + + if (args[0] == "source") { + if (args.size() == 1 || args[1] == "list") return list_sources(opts); + if (args[1] == "use" && args.size() >= 3) return source_use(opts, args[2]); + if (args[1] == "add" && args.size() >= 4) + return source_add(opts, args[2], args[3]); + if (args[1] == "test") return update_index(opts); + safeErrorPrintLn("wpm: usage: wpm source list|use |add "); + return 1; + } + + if (args[0] == "list" || args[0] == "search") return list_packages(opts); + if (args[0] == "info" && args.size() >= 2) return show_info(opts, args[1]); + if (args[0] == "install" && args.size() >= 2) + return install_package(opts, args[1]); + if ((args[0] == "update" || args[0] == "upgrade") && args.size() >= 2 && + (args[1] == "winuxcmd" || args[1] == "coreutils")) { + return update_winuxcmd(opts); + } + if (args[0] == "version") { + safePrintLn("wpm " + std::string(kVersion)); + return 0; + } + + safeErrorPrintLn("wpm: unknown command: " + std::string(args[0])); + return 1; +} + +} // namespace wpm + +REGISTER_COMMAND( + wpm, "wpm", "manage WinuxCmd packages and command links", + "WPM is WinuxCmd's internal package and link manager. It keeps a local " + "package index, downloads artifacts with WinHTTP, updates WinuxCmd, and " + "rebuilds command hardlinks without external scripts.", + " wpm links rebuild\n" + " wpm index update\n" + " wpm source list\n" + " wpm update winuxcmd", + "winuxcmd(1), ln(1)", "WinuxCmd Project", "Copyright (c) 2026 WinuxCmd", + WPM_OPTIONS) { + try { + auto opts = wpm::build_options(ctx); + return wpm::dispatch( + opts, std::span(ctx.positionals.data(), + ctx.positionals.size())); + } catch (const std::exception& e) { + safeErrorPrintLn(std::string("wpm: ") + e.what()); + return 1; + } +} diff --git a/src/tools/wpm.cpp b/src/tools/wpm.cpp deleted file mode 100644 index e984593..0000000 --- a/src/tools/wpm.cpp +++ /dev/null @@ -1,1214 +0,0 @@ -/* - * Copyright © 2026 [caomengxuan666] - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - * - * - File: wpm.cpp - * - Username: Administrator - * - CopyrightYear: 2026 - */ -/// @contributors: -/// - @contributor1 caomengxuan666 2507560089@qq.com -/// @Description: Winux Package Manager - Manage external packages and command -/// aliases -/// @Version: 0.1.0 -/// @License: MIT -/// @Copyright: Copyright © 2026 WinuxCmd - -#define WIN32_LEAN_AND_MEAN -#define NOMINMAX -#include -#include -#pragma comment(lib, "winhttp.lib") - -import std; -import utils; - -namespace fs = std::filesystem; - -namespace wpm { - -// ====================================================== -// Constants -// ====================================================== - -constexpr const char* WPM_VERSION = "0.1.0"; -constexpr const char* DEFAULT_CONFIG_FILE = "config.json"; -constexpr const char* DEFAULT_ALIASES_FILE = "aliases.json"; -constexpr const char* DEFAULT_PACKAGES_DIR = "packages"; -constexpr const char* DEFAULT_BIN_DIR = "bin"; -constexpr const char* DEFAULT_CACHE_DIR = "cache"; -constexpr const char* DEFAULT_SOURCE_URL = - "https://raw.githubusercontent.com/caomengxuan666/wpm-packages/main"; - -// ====================================================== -// Design Patterns: Strategy Pattern for Download -// ====================================================== - -struct DownloadResult { - bool success; - std::vector data; - std::string error; - size_t total_size; -}; - -class IDownloadStrategy { - public: - virtual ~IDownloadStrategy() = default; - using ProgressCallback = std::function; - virtual DownloadResult download(const std::string& url, - ProgressCallback callback = nullptr) = 0; -}; - -class WinHTTPDownloadStrategy : public IDownloadStrategy { - public: - DownloadResult download(const std::string& url, - ProgressCallback callback = nullptr) override { - DownloadResult result; - result.total_size = 0; - - HINTERNET hSession = nullptr; - HINTERNET hConnect = nullptr; - HINTERNET hRequest = nullptr; - - try { - // Parse URL - std::wstring wurl(url.begin(), url.end()); - URL_COMPONENTS urlComp = {0}; - urlComp.dwStructSize = sizeof(URL_COMPONENTS); - urlComp.dwSchemeLength = (DWORD)-1; - urlComp.dwHostNameLength = (DWORD)-1; - urlComp.dwUrlPathLength = (DWORD)-1; - urlComp.dwExtraInfoLength = (DWORD)-1; - - if (!WinHttpCrackUrl(wurl.c_str(), 0, 0, &urlComp)) { - result.error = "Failed to parse URL: " + url; - return result; - } - - // Get host name and path - std::wstring hostName(urlComp.lpszHostName, urlComp.dwHostNameLength); - std::wstring urlPath; - if (urlComp.lpszUrlPath) { - urlPath = std::wstring(urlComp.lpszUrlPath, urlComp.dwUrlPathLength); - } - if (urlComp.lpszExtraInfo) { - urlPath += - std::wstring(urlComp.lpszExtraInfo, urlComp.dwExtraInfoLength); - } - - // Open session - hSession = WinHttpOpen(L"WinuxPM/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, - WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); - if (!hSession) { - result.error = "Failed to initialize WinHTTP session"; - return result; - } - - // Connect to server - hConnect = WinHttpConnect(hSession, hostName.c_str(), - INTERNET_DEFAULT_HTTPS_PORT, 0); - if (!hConnect) { - result.error = "Failed to connect to server"; - WinHttpCloseHandle(hSession); - return result; - } - - // Create request - hRequest = WinHttpOpenRequest( - hConnect, L"GET", urlPath.c_str(), NULL, WINHTTP_NO_REFERER, - WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE); - if (!hRequest) { - result.error = "Failed to create request"; - WinHttpCloseHandle(hConnect); - WinHttpCloseHandle(hSession); - return result; - } - - // Send request - if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, - WINHTTP_NO_REQUEST_DATA, 0, 0, 0)) { - result.error = "Failed to send request"; - WinHttpCloseHandle(hRequest); - WinHttpCloseHandle(hConnect); - WinHttpCloseHandle(hSession); - return result; - } - - // Receive response - if (!WinHttpReceiveResponse(hRequest, NULL)) { - result.error = "Failed to receive response"; - WinHttpCloseHandle(hRequest); - WinHttpCloseHandle(hConnect); - WinHttpCloseHandle(hSession); - return result; - } - - // Get content length - DWORD contentLength = 0; - DWORD bufferLength = sizeof(contentLength); - WinHttpQueryHeaders( - hRequest, WINHTTP_QUERY_CONTENT_LENGTH | WINHTTP_QUERY_FLAG_NUMBER, - WINHTTP_HEADER_NAME_BY_INDEX, &contentLength, &bufferLength, - WINHTTP_NO_HEADER_INDEX); - result.total_size = contentLength; - - // Read data with progress callback - const int BUFFER_SIZE = 8192; - std::byte buffer[BUFFER_SIZE]; - DWORD bytesRead = 0; - size_t totalRead = 0; - - while (WinHttpReadData(hRequest, buffer, BUFFER_SIZE, &bytesRead) && - bytesRead > 0) { - result.data.insert(result.data.end(), buffer, buffer + bytesRead); - totalRead += bytesRead; - - // Call progress callback if provided - if (callback && result.total_size > 0) { - callback(totalRead, result.total_size); - } - } - - result.success = true; - - } catch (const std::exception& e) { - result.error = std::string("Download error: ") + e.what(); - } - - if (hRequest) WinHttpCloseHandle(hRequest); - if (hConnect) WinHttpCloseHandle(hConnect); - if (hSession) WinHttpCloseHandle(hSession); - - return result; - } -}; - -class Downloader { - private: - std::unique_ptr strategy_; - - public: - Downloader() : strategy_(std::make_unique()) {} - - DownloadResult download( - const std::string& url, - IDownloadStrategy::ProgressCallback callback = nullptr) { - return strategy_->download(url, callback); - } - - bool download_to_file( - const std::string& url, const fs::path& filepath, - IDownloadStrategy::ProgressCallback progress_callback = nullptr) { - DownloadResult result; - - HINTERNET hSession = nullptr; - HINTERNET hConnect = nullptr; - HINTERNET hRequest = nullptr; - - try { - // Parse URL - std::wstring wurl(url.begin(), url.end()); - URL_COMPONENTS urlComp = {0}; - urlComp.dwStructSize = sizeof(URL_COMPONENTS); - urlComp.dwSchemeLength = (DWORD)-1; - urlComp.dwHostNameLength = (DWORD)-1; - urlComp.dwUrlPathLength = (DWORD)-1; - urlComp.dwExtraInfoLength = (DWORD)-1; - - if (!WinHttpCrackUrl(wurl.c_str(), 0, 0, &urlComp)) { - safePrintLn("Failed to parse URL: " + url); - return false; - } - - // Get host name and path - std::wstring hostName(urlComp.lpszHostName, urlComp.dwHostNameLength); - std::wstring urlPath; - if (urlComp.lpszUrlPath) { - urlPath = std::wstring(urlComp.lpszUrlPath, urlComp.dwUrlPathLength); - } - if (urlComp.lpszExtraInfo) { - urlPath += - std::wstring(urlComp.lpszExtraInfo, urlComp.dwExtraInfoLength); - } - - // Open session - hSession = WinHttpOpen(L"WinuxPM/1.0", WINHTTP_ACCESS_TYPE_DEFAULT_PROXY, - WINHTTP_NO_PROXY_NAME, WINHTTP_NO_PROXY_BYPASS, 0); - if (!hSession) { - safePrintLn("Failed to initialize WinHTTP session"); - return false; - } - - // Connect to server - hConnect = WinHttpConnect(hSession, hostName.c_str(), - INTERNET_DEFAULT_HTTPS_PORT, 0); - if (!hConnect) { - safePrintLn("Failed to connect to server"); - WinHttpCloseHandle(hSession); - return false; - } - - // Create request - hRequest = WinHttpOpenRequest( - hConnect, L"GET", urlPath.c_str(), NULL, WINHTTP_NO_REFERER, - WINHTTP_DEFAULT_ACCEPT_TYPES, WINHTTP_FLAG_SECURE); - if (!hRequest) { - safePrintLn("Failed to create request"); - WinHttpCloseHandle(hConnect); - WinHttpCloseHandle(hSession); - return false; - } - - // Send request - if (!WinHttpSendRequest(hRequest, WINHTTP_NO_ADDITIONAL_HEADERS, 0, - WINHTTP_NO_REQUEST_DATA, 0, 0, 0)) { - safePrintLn("Failed to send request"); - WinHttpCloseHandle(hRequest); - WinHttpCloseHandle(hConnect); - WinHttpCloseHandle(hSession); - return false; - } - - // Receive response - if (!WinHttpReceiveResponse(hRequest, NULL)) { - safePrintLn("Failed to receive response"); - WinHttpCloseHandle(hRequest); - WinHttpCloseHandle(hConnect); - WinHttpCloseHandle(hSession); - return false; - } - - // Get content length - size_t totalSize = 0; - DWORD contentLength = 0; - DWORD bufferLength = sizeof(contentLength); - if (WinHttpQueryHeaders( - hRequest, - WINHTTP_QUERY_CONTENT_LENGTH | WINHTTP_QUERY_FLAG_NUMBER, - WINHTTP_HEADER_NAME_BY_INDEX, &contentLength, &bufferLength, - WINHTTP_NO_HEADER_INDEX)) { - totalSize = contentLength; - } - - // Create output file - fs::create_directories(filepath.parent_path()); - std::ofstream out(filepath, std::ios::binary); - if (!out) { - safePrintLn("Failed to open file for writing: " + filepath.string()); - WinHttpCloseHandle(hRequest); - WinHttpCloseHandle(hConnect); - WinHttpCloseHandle(hSession); - return false; - } - - // Read and write data with progress callback - const int BUFFER_SIZE = 8192; - std::byte buffer[BUFFER_SIZE]; - DWORD bytesRead = 0; - size_t totalRead = 0; - - while (WinHttpReadData(hRequest, buffer, BUFFER_SIZE, &bytesRead) && - bytesRead > 0) { - out.write(reinterpret_cast(buffer), bytesRead); - totalRead += bytesRead; - - // Call progress callback if provided - if (progress_callback && totalSize > 0) { - progress_callback(totalRead, totalSize); - } - } - - out.close(); - - // Cleanup - if (hRequest) WinHttpCloseHandle(hRequest); - if (hConnect) WinHttpCloseHandle(hConnect); - if (hSession) WinHttpCloseHandle(hSession); - - return true; - - } catch (const std::exception& e) { - safePrintLn("Download error: " + std::string(e.what())); - if (hRequest) WinHttpCloseHandle(hRequest); - if (hConnect) WinHttpCloseHandle(hConnect); - if (hSession) WinHttpCloseHandle(hSession); - return false; - } - } -}; - -// ====================================================== -// Package Models -// ====================================================== - -struct PackageFile { - std::string url; - std::string filename; - std::string sha256; - size_t size; - - nlohmann::json to_json() const { - return nlohmann::json{{"url", url}, - {"filename", filename}, - {"sha256", sha256}, - {"size", size}}; - } - - static PackageFile from_json(const nlohmann::json& j) { - PackageFile pkg; - if (j.contains("url")) pkg.url = j["url"].get(); - if (j.contains("filename")) pkg.filename = j["filename"].get(); - if (j.contains("sha256")) pkg.sha256 = j["sha256"].get(); - if (j.contains("size")) pkg.size = j["size"].get(); - return pkg; - } -}; - -struct Package { - std::string name; - std::string version; - std::string description; - std::string homepage; - std::string type; - std::vector arch; - std::vector files; - std::string rename_to; - bool replace_builtin; - std::vector dependencies; - - nlohmann::json to_json() const { - nlohmann::json j; - j["name"] = name; - j["version"] = version; - j["description"] = description; - j["homepage"] = homepage; - j["type"] = type; - j["arch"] = arch; - nlohmann::json files_json = nlohmann::json::array(); - for (const auto& f : files) { - files_json.push_back(f.to_json()); - } - j["files"] = files_json; - j["rename_to"] = rename_to; - j["replace_builtin"] = replace_builtin; - j["dependencies"] = dependencies; - return j; - } - - static Package from_json(const nlohmann::json& j) { - Package pkg; - if (j.contains("name")) pkg.name = j["name"].get(); - if (j.contains("version")) pkg.version = j["version"].get(); - if (j.contains("description")) - pkg.description = j["description"].get(); - if (j.contains("homepage")) pkg.homepage = j["homepage"].get(); - if (j.contains("type")) pkg.type = j["type"].get(); - if (j.contains("arch")) - pkg.arch = j["arch"].get>(); - if (j.contains("files")) { - pkg.files.clear(); - for (const auto& f : j["files"]) { - pkg.files.push_back(PackageFile::from_json(f)); - } - } - if (j.contains("rename_to")) - pkg.rename_to = j["rename_to"].get(); - if (j.contains("replace_builtin")) - pkg.replace_builtin = j["replace_builtin"].get(); - if (j.contains("dependencies")) - pkg.dependencies = j["dependencies"].get>(); - return pkg; - } -}; - -// ====================================================== -// Configuration Management -// ====================================================== - -class ConfigManager { - private: - fs::path config_file_; - nlohmann::json config_; - - public: - ConfigManager(const fs::path& config_file) : config_file_(config_file) { - load(); - } - - bool load() { - if (!fs::exists(config_file_)) { - create_default(); - return true; - } - - try { - std::ifstream file(config_file_); - config_ = nlohmann::json::parse(file); - return true; - } catch (const std::exception& e) { - safePrintLn("Error loading config: " + std::string(e.what())); - return false; - } - } - - bool save() { - try { - fs::create_directories(config_file_.parent_path()); - std::ofstream file(config_file_); - file << config_.dump(2); - return true; - } catch (const std::exception& e) { - safePrintLn("Error saving config: " + std::string(e.what())); - return false; - } - } - - void create_default() { - config_ = nlohmann::json{ - {"sources", std::vector{DEFAULT_SOURCE_URL}}, - {"install_dir", "bin"}, - {"cache_dir", "cache"}}; - save(); - } - - std::vector get_sources() const { - return config_.value("sources", - std::vector{DEFAULT_SOURCE_URL}); - } - - void add_source(const std::string& source) { - auto sources = get_sources(); - if (std::find(sources.begin(), sources.end(), source) == sources.end()) { - sources.push_back(source); - config_["sources"] = sources; - } - } - - void remove_source(const std::string& source) { - auto sources = get_sources(); - sources.erase(std::remove(sources.begin(), sources.end(), source), - sources.end()); - config_["sources"] = sources; - } -}; - -// ====================================================== -// Alias Management -// ====================================================== - -class AliasManager { - private: - fs::path aliases_file_; - nlohmann::json aliases_; - - public: - AliasManager(const fs::path& aliases_file) : aliases_file_(aliases_file) { - load(); - } - - bool load() { - if (!fs::exists(aliases_file_)) { - aliases_ = nlohmann::json::object(); - return true; - } - - try { - std::ifstream file(aliases_file_); - aliases_ = nlohmann::json::parse(file); - return true; - } catch (const std::exception& e) { - safePrintLn("Error loading aliases: " + std::string(e.what())); - return false; - } - } - - bool save() { - try { - fs::create_directories(aliases_file_.parent_path()); - std::ofstream file(aliases_file_); - file << aliases_.dump(2); - return true; - } catch (const std::exception& e) { - safePrintLn("Error saving aliases: " + std::string(e.what())); - return false; - } - } - - void set_alias(const std::string& alias, const std::string& target) { - aliases_[alias] = target; - save(); - } - - std::optional get_alias(const std::string& alias) const { - if (aliases_.contains(alias)) { - return aliases_[alias].get(); - } - return std::nullopt; - } - - void remove_alias(const std::string& alias) { - if (aliases_.contains(alias)) { - aliases_.erase(alias); - save(); - } - } - - std::vector list_aliases() const { - std::vector result; - for (auto it = aliases_.begin(); it != aliases_.end(); ++it) { - result.push_back(it.key() + " -> " + it.value().get()); - } - return result; - } -}; - -// ====================================================== -// Package Repository -// ====================================================== - -class PackageRepository { - private: - std::map packages_; - Downloader downloader_; - - public: - void load_from_file(const fs::path& filepath) { - try { - std::ifstream file(filepath); - if (!file.is_open()) { - safePrintLn("Failed to open file: " + filepath.string()); - return; - } - - std::string content((std::istreambuf_iterator(file)), - std::istreambuf_iterator()); - auto json_data = nlohmann::json::parse(content); - Package pkg = Package::from_json(json_data); - packages_[pkg.name] = pkg; - } catch (const std::exception& e) { - safePrintLn("Error loading package from " + filepath.string() + ": " + - e.what()); - } - } - - void load_from_local(const fs::path& directory) { - if (!fs::exists(directory) || !fs::is_directory(directory)) { - return; - } - - for (const auto& entry : fs::directory_iterator(directory)) { - if (entry.path().extension() == ".json") { - load_from_file(entry.path()); - } - } - } - - void load_from_remote(const std::string& base_url) { - std::string index_url = base_url + "/index.json"; - auto result = downloader_.download(index_url); - - if (!result.success) { - safePrintLn("Failed to load index from " + base_url + ": " + - result.error); - return; - } - - try { - auto json_data = nlohmann::json::parse( - std::string(reinterpret_cast(result.data.data()), - result.data.size())); - - if (json_data.contains("packages")) { - for (const auto& pkg_name : json_data["packages"]) { - std::string pkg_url = - base_url + "/" + pkg_name.get() + ".json"; - auto pkg_result = downloader_.download(pkg_url); - - if (pkg_result.success) { - try { - auto pkg_json = nlohmann::json::parse(std::string( - reinterpret_cast(pkg_result.data.data()), - pkg_result.data.size())); - Package pkg = Package::from_json(pkg_json); - packages_[pkg.name] = pkg; - } catch (const std::exception& e) { - safePrintLn("Error parsing package " + - pkg_name.get() + ": " + e.what()); - } - } - } - } - } catch (const std::exception& e) { - safePrintLn("Error parsing index: " + std::string(e.what())); - } - } - - void add_package(const Package& pkg) { packages_[pkg.name] = pkg; } - - std::optional get_package(const std::string& name) const { - auto it = packages_.find(name); - if (it != packages_.end()) { - return it->second; - } - return std::nullopt; - } - - std::vector search_packages(const std::string& keyword) const { - std::vector results; - std::string lower_keyword = keyword; - std::transform(lower_keyword.begin(), lower_keyword.end(), - lower_keyword.begin(), ::tolower); - - for (const auto& pair : packages_) { - const std::string& name = pair.first; - const Package& pkg = pair.second; - - std::string lower_name = name; - std::transform(lower_name.begin(), lower_name.end(), lower_name.begin(), - ::tolower); - - std::string lower_desc = pkg.description; - std::transform(lower_desc.begin(), lower_desc.end(), lower_desc.begin(), - ::tolower); - - if (lower_name.find(lower_keyword) != std::string::npos || - lower_desc.find(lower_keyword) != std::string::npos) { - results.push_back(pkg); - } - } - - return results; - } - - std::vector list_all() const { - std::vector result; - for (const auto& pair : packages_) { - result.push_back(pair.second); - } - return result; - } - - size_t size() const { return packages_.size(); } -}; - -// ====================================================== -// Package Manager (Main Logic) -// ====================================================== - -class PackageManager { - private: - fs::path wpm_home_; - fs::path bin_dir_; - fs::path cache_dir_; - ConfigManager config_; - AliasManager alias_manager_; - PackageRepository repository_; - Downloader downloader_; - - public: - PackageManager(const std::string& wpm_home = "") - : wpm_home_(wpm_home.empty() ? get_wpm_home() : wpm_home), - bin_dir_(wpm_home_ / DEFAULT_BIN_DIR), - cache_dir_(wpm_home_ / DEFAULT_CACHE_DIR), - config_(wpm_home_ / DEFAULT_CONFIG_FILE), - alias_manager_(wpm_home_ / DEFAULT_ALIASES_FILE) { - initialize_directories(); - load_packages(); - } - - bool install_package(const std::string& package_name) { - auto pkg_opt = repository_.get_package(package_name); - if (!pkg_opt) { - safePrintLn("Package '" + package_name + "' not found"); - return false; - } - - Package pkg = *pkg_opt; - safePrintLn("Installing " + pkg.name + " v" + pkg.version + "..."); - - // Download and install files - for (const auto& file : pkg.files) { - safePrintLn("Downloading " + file.filename + "..."); - - cppbar::ProgressBar bar(100, "Progress"); - bar.set_style(cppbar::style::PresetStyles::modern()); - bar.set_foreground_color(cppbar::color::Color::from_256(33)); // Blue - - // Download with real progress callback - bool downloadSuccess = downloader_.download_to_file( - file.url, cache_dir_ / file.filename, - [&bar](size_t current, size_t total) { - if (total > 0) { - int progress = static_cast((current * 100) / total); - bar.update(progress); - } - }); - - if (!downloadSuccess) { - safePrintLn("Failed to download " + file.filename); - return false; - } - - bar.finish(); - - // Copy to bin directory - fs::path dest_path = bin_dir_ / file.filename; - if (pkg.replace_builtin && !pkg.rename_to.empty()) { - dest_path = bin_dir_ / pkg.rename_to; - } - - try { - fs::copy_file(cache_dir_ / file.filename, dest_path, - fs::copy_options::overwrite_existing); - safePrintLn("Installed: " + dest_path.string()); - } catch (const std::exception& e) { - safePrintLn("Error copying file: " + std::string(e.what())); - return false; - } - } - - // Add to aliases if needed - if (pkg.replace_builtin && !pkg.rename_to.empty()) { - alias_manager_.set_alias(pkg.name, pkg.rename_to); - } - - safePrintLn("Package '" + package_name + "' installed successfully!"); - return true; - } - - bool uninstall_package(const std::string& package_name) { - auto pkg_opt = repository_.get_package(package_name); - if (!pkg_opt) { - safePrintLn("Package '" + package_name + "' not found"); - return false; - } - - Package pkg = *pkg_opt; - safePrintLn("Uninstalling " + package_name + "..."); - - // Remove files - for (const auto& file : pkg.files) { - fs::path file_path = bin_dir_ / file.filename; - if (pkg.replace_builtin && !pkg.rename_to.empty()) { - file_path = bin_dir_ / pkg.rename_to; - } - - if (fs::exists(file_path)) { - try { - fs::remove(file_path); - safePrintLn("Removed: " + file_path.string()); - } catch (const std::exception& e) { - safePrintLn("Error removing file: " + std::string(e.what())); - } - } - } - - // Remove alias - if (pkg.replace_builtin && !pkg.rename_to.empty()) { - alias_manager_.remove_alias(pkg.name); - } - - safePrintLn("Package '" + package_name + "' uninstalled successfully!"); - return true; - } - - void list_packages(bool installed_only = false) { - if (installed_only) { - safePrintLn("Installed packages:"); - if (fs::exists(bin_dir_)) { - for (const auto& entry : fs::directory_iterator(bin_dir_)) { - safePrintLn(" " + entry.path().filename().string()); - } - } - } else { - safePrintLn("Available packages:"); - auto packages = repository_.list_all(); - for (const auto& pkg : packages) { - safePrintLn(" " + pkg.name + " v" + pkg.version + " - " + - pkg.description); - } - } - } - - void search_packages(const std::string& keyword) { - safePrintLn("Search results for '" + keyword + "':"); - auto results = repository_.search_packages(keyword); - - if (results.empty()) { - safePrintLn("No packages found."); - } else { - for (const auto& pkg : results) { - safePrintLn(" " + pkg.name + " v" + pkg.version + " - " + - pkg.description); - } - } - } - - void show_package_info(const std::string& package_name) { - auto pkg_opt = repository_.get_package(package_name); - if (!pkg_opt) { - safePrintLn("Package '" + package_name + "' not found"); - return; - } - - Package pkg = *pkg_opt; - - safePrintLn("Name: " + pkg.name); - safePrintLn("Version: " + pkg.version); - safePrintLn("Description: " + pkg.description); - safePrintLn("Homepage: " + pkg.homepage); - safePrintLn("Type: " + pkg.type); - - safePrint("Architectures: "); - for (size_t i = 0; i < pkg.arch.size(); ++i) { - safePrint(pkg.arch[i]); - if (i < pkg.arch.size() - 1) safePrint(", "); - } - safePrintLn(""); - } - - void list_sources() { - auto sources = config_.get_sources(); - safePrintLn("Package sources:"); - for (size_t i = 0; i < sources.size(); ++i) { - safePrintLn(" " + std::to_string(i + 1) + ". " + sources[i]); - } - } - - void add_source(const std::string& source) { - config_.add_source(source); - safePrintLn("Source added: " + source); - load_packages(); - } - - void remove_source(const std::string& source) { - config_.remove_source(source); - safePrintLn("Source removed: " + source); - load_packages(); - } - - void list_aliases() { - auto aliases = alias_manager_.list_aliases(); - safePrintLn("Command aliases:"); - for (const auto& alias : aliases) { - safePrintLn(" " + alias); - } - } - - void set_alias(const std::string& alias, const std::string& target) { - alias_manager_.set_alias(alias, target); - safePrintLn("Alias set: " + alias + " -> " + target); - } - - void remove_alias(const std::string& alias) { - alias_manager_.remove_alias(alias); - safePrintLn("Alias removed: " + alias); - } - - void reload_packages() { - load_packages(); - safePrintLn("Package index reloaded"); - } - - private: - void initialize_directories() { - fs::create_directories(wpm_home_); - fs::create_directories(bin_dir_); - fs::create_directories(cache_dir_); - } - - void load_packages() { - repository_ = PackageRepository(); // Clear and reload - - auto sources = config_.get_sources(); - for (const auto& source : sources) { - if (source.find("http") == 0) { - repository_.load_from_remote(source); - } else { - repository_.load_from_local(source); - } - } - } - - static std::string get_wpm_home() { - const char* wpm_home = std::getenv("WPM_HOME"); - if (wpm_home) { - return expand_home_path(wpm_home); - } - - const char* home = std::getenv("USERPROFILE"); - if (!home) home = std::getenv("HOME"); - - if (home) { - return std::string(home) + "/.winuxcmd"; - } - - return "./.winuxcmd"; - } - - static std::string expand_home_path(const std::string& path) { - if (!path.empty() && path[0] == '~') { - const char* home = std::getenv("HOME"); - if (!home) home = std::getenv("USERPROFILE"); - if (home) { - return std::string(home) + path.substr(1); - } - } - return path; - } -}; - -// ====================================================== -// Command Pattern -// ====================================================== - -class ICommand { - public: - virtual ~ICommand() = default; - virtual int execute(PackageManager& manager, - const std::vector& args) = 0; - virtual std::string description() const = 0; -}; - -class InstallCommand : public ICommand { - public: - int execute(PackageManager& manager, - const std::vector& args) override { - if (args.empty()) { - safePrintLn("Usage: wpm install "); - return 1; - } - return manager.install_package(args[0]) ? 0 : 1; - } - - std::string description() const override { return "Install a package"; } -}; - -class UninstallCommand : public ICommand { - public: - int execute(PackageManager& manager, - const std::vector& args) override { - if (args.empty()) { - safePrintLn("Usage: wpm uninstall "); - return 1; - } - return manager.uninstall_package(args[0]) ? 0 : 1; - } - - std::string description() const override { return "Uninstall a package"; } -}; - -class ListCommand : public ICommand { - public: - int execute(PackageManager& manager, - const std::vector& args) override { - bool installed_only = false; - if (!args.empty() && args[0] == "--installed") { - installed_only = true; - } - manager.list_packages(installed_only); - return 0; - } - - std::string description() const override { return "List packages"; } -}; - -class SearchCommand : public ICommand { - public: - int execute(PackageManager& manager, - const std::vector& args) override { - if (args.empty()) { - safePrintLn("Usage: wpm search "); - return 1; - } - manager.search_packages(args[0]); - return 0; - } - - std::string description() const override { return "Search packages"; } -}; - -class InfoCommand : public ICommand { - public: - int execute(PackageManager& manager, - const std::vector& args) override { - if (args.empty()) { - safePrintLn("Usage: wpm info "); - return 1; - } - manager.show_package_info(args[0]); - return 0; - } - - std::string description() const override { - return "Show package information"; - } -}; - -class SourceCommand : public ICommand { - public: - int execute(PackageManager& manager, - const std::vector& args) override { - if (args.empty()) { - manager.list_sources(); - return 0; - } - - if (args.size() >= 2) { - if (args[0] == "add") { - manager.add_source(args[1]); - return 0; - } else if (args[0] == "remove") { - manager.remove_source(args[1]); - return 0; - } - } - - safePrintLn("Usage: wpm source [add|remove] "); - return 1; - } - - std::string description() const override { return "Manage package sources"; } -}; - -class AliasCommand : public ICommand { - public: - int execute(PackageManager& manager, - const std::vector& args) override { - if (args.empty()) { - manager.list_aliases(); - return 0; - } - - if (args.size() >= 2) { - if (args[0] == "set") { - manager.set_alias(args[1], args[2]); - return 0; - } else if (args[0] == "remove") { - manager.remove_alias(args[1]); - return 0; - } - } - - safePrintLn("Usage: wpm alias [set|remove] [target]"); - return 1; - } - - std::string description() const override { return "Manage command aliases"; } -}; - -class ReloadCommand : public ICommand { - public: - int execute(PackageManager& manager, - const std::vector& args) override { - manager.reload_packages(); - return 0; - } - - std::string description() const override { return "Reload package index"; } -}; - -// ====================================================== -// CLI Application -// ====================================================== - -class CLIApplication { - private: - PackageManager package_manager_; - std::map> commands_; - - public: - CLIApplication() { - register_command("install", std::make_unique()); - register_command("uninstall", std::make_unique()); - register_command("list", std::make_unique()); - register_command("search", std::make_unique()); - register_command("info", std::make_unique()); - register_command("source", std::make_unique()); - register_command("alias", std::make_unique()); - register_command("reload", std::make_unique()); - } - - void register_command(const std::string& name, - std::unique_ptr command) { - commands_[name] = std::move(command); - } - - void print_usage() { - safePrintLn("Winux Package Manager v" + std::string(WPM_VERSION)); - safePrintLn(""); - safePrintLn("Usage: wpm [options]"); - safePrintLn(""); - safePrintLn("Commands:"); - for (const auto& [name, cmd] : commands_) { - safePrintLn(" " + name + " " + cmd->description()); - } - safePrintLn(" help Show this help message"); - safePrintLn(" version Show version information"); - } - - int run(const std::vector& args) { - if (args.empty()) { - print_usage(); - return 1; - } - - std::string command = args[0]; - - if (command == "help" || command == "-h" || command == "--help") { - print_usage(); - return 0; - } - - if (command == "version" || command == "-v" || command == "--version") { - safePrintLn("Winux Package Manager v" + std::string(WPM_VERSION)); - return 0; - } - - auto it = commands_.find(command); - if (it == commands_.end()) { - safePrintLn("Unknown command: " + command); - safePrintLn("Run 'wpm help' for usage information"); - return 1; - } - - std::vector cmd_args(args.begin() + 1, args.end()); - return it->second->execute(package_manager_, cmd_args); - } -}; - -// ====================================================== -// Main Function -// ====================================================== - -int wpm_main(const std::vector& args) { - CLIApplication app; - return app.run(args); -} - -} // namespace wpm - -int main(int argc, char* argv[]) { - std::vector args; - for (int i = 1; i < argc; ++i) { - args.push_back(argv[i]); - } - return wpm::wpm_main(args); -} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 6805fb0..3f45cc8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -130,12 +130,12 @@ endif() # Get the path to winuxcmd.exe (it should be in the same directory) set(WINUXCMD_EXE_PATH "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}/winuxcmd.exe") -# Custom target to create command links using PowerShell script -# Add DEPENDS on winuxcmd.exe to ensure it exists before running +# Custom target to create command links through WinuxCmd itself. +# WPM owns link generation; scripts/create_links.ps1 is only a compatibility wrapper. add_custom_target(create-test-links - COMMAND ${POWERSHELL_EXE} -ExecutionPolicy Bypass -File "${CMAKE_CURRENT_SOURCE_DIR}/../scripts/create_links.ps1" -Force + COMMAND $ wpm links rebuild --root "${CMAKE_RUNTIME_OUTPUT_DIRECTORY}" --force WORKING_DIRECTORY ${CMAKE_RUNTIME_OUTPUT_DIRECTORY} - DEPENDS ${WINUXCMD_EXE_PATH} + DEPENDS winuxcmd COMMENT "Creating WinuxCmd command links in ${CMAKE_RUNTIME_OUTPUT_DIRECTORY}..." VERBATIM ) diff --git a/tests/unit/jq/jq_unit_test.cpp b/tests/unit/jq/jq_unit_test.cpp deleted file mode 100644 index 0dd1524..0000000 --- a/tests/unit/jq/jq_unit_test.cpp +++ /dev/null @@ -1,188 +0,0 @@ -/* - * Copyright © 2026 [caomengxuan666] - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to - * deal in the Software without restriction, including without limitation the - * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or - * sell copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in - * all copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING - * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS - * IN THE SOFTWARE. - * - * - File: jq_unit_test.cpp - * - Username: Administrator - * - CopyrightYear: 2026 - */ -#include "framework/winuxtest.h" - -TEST(jq, jq_basic_filter) { - Pipeline p; - p.set_stdin("{\"name\":\"test\"}\n"); - p.add(L"jq.exe", {L"."}); - - TEST_LOG_CMD_LIST("jq.exe", L"."); - - auto r = p.run(); - - TEST_LOG_EXIT_CODE(r); - TEST_LOG("jq basic output", r.stdout_text); - - EXPECT_EQ(r.exit_code, 0); -} - -TEST(jq, jq_field_access) { - Pipeline p; - p.set_stdin("{\"name\":\"test\"}\n"); - p.add(L"jq.exe", {L".name"}); - - TEST_LOG_CMD_LIST("jq.exe", L".name"); - - auto r = p.run(); - - TEST_LOG_EXIT_CODE(r); - TEST_LOG("jq field access output", r.stdout_text); - - EXPECT_EQ(r.exit_code, 0); -} - -TEST(jq, jq_raw_output) { - Pipeline p; - p.set_stdin("{\"name\":\"test\"}\n"); - p.add(L"jq.exe", {L"-r", L".name"}); - - TEST_LOG_CMD_LIST("jq.exe", L"-r", L".name"); - - auto r = p.run(); - - TEST_LOG_EXIT_CODE(r); - TEST_LOG("jq raw output", r.stdout_text); - - EXPECT_EQ(r.exit_code, 0); -} - -TEST(jq, jq_compact_output) { - Pipeline p; - p.set_stdin("{\"name\":\"test\"}\n"); - p.add(L"jq.exe", {L"-c", L"."}); - - TEST_LOG_CMD_LIST("jq.exe", L"-c", L"."); - - auto r = p.run(); - - TEST_LOG_EXIT_CODE(r); - TEST_LOG("jq compact output", r.stdout_text); - - EXPECT_EQ(r.exit_code, 0); -} - -TEST(jq, jq_filter_operand_is_literal_not_glob) { - TempDir tmp; - tmp.write(".a", "not json\n"); - - Pipeline p; - p.set_cwd(tmp.wpath()); - p.set_stdin("{\"abc\":1}\n"); - p.add(L"jq.exe", {L".[abc]"}); - - auto r = p.run(); - - EXPECT_EQ(r.exit_code, 0); - EXPECT_NE(r.stdout_text.find("\"abc\""), std::string::npos); -} - -TEST(jq, jq_sort_keys) { - Pipeline p; - p.set_stdin("{\"z\":1,\"a\":2,\"m\":3}\n"); - p.add(L"jq.exe", {L"-S", L"."}); - auto r = p.run(); - - EXPECT_EQ(r.exit_code, 0); - // Keys should be sorted alphabetically - size_t a_pos = r.stdout_text.find("\"a\""); - size_t m_pos = r.stdout_text.find("\"m\""); - size_t z_pos = r.stdout_text.find("\"z\""); - EXPECT_TRUE(a_pos < m_pos); - EXPECT_TRUE(m_pos < z_pos); -} - -TEST(jq, jq_color_output) { - Pipeline p; - p.set_stdin("{\"name\":\"test\"}\n"); - p.add(L"jq.exe", {L"-C", L"."}); - auto r = p.run(); - - EXPECT_EQ(r.exit_code, 0); - // Color output should contain ANSI escape codes - EXPECT_FALSE(r.stdout_text.empty()); -} - -TEST(jq, jq_monochrome_output) { - Pipeline p; - p.set_stdin("{\"name\":\"test\"}\n"); - p.add(L"jq.exe", {L"-M", L"."}); - auto r = p.run(); - - EXPECT_EQ(r.exit_code, 0); - // Monochrome should not contain ANSI codes - EXPECT_FALSE(r.stdout_text.empty()); -} - -TEST(jq, jq_null_input) { - Pipeline p; - p.add(L"jq.exe", {L"-n", L"."}); - auto r = p.run(); - - EXPECT_EQ(r.exit_code, 0); - // null input should output "null" - EXPECT_TRUE(r.stdout_text.find("null") != std::string::npos); -} - -TEST(jq, jq_array) { - Pipeline p; - p.set_stdin("[1,2,3]\n"); - p.add(L"jq.exe", {L"."}); - auto r = p.run(); - - EXPECT_EQ(r.exit_code, 0); - EXPECT_TRUE(r.stdout_text.find("[") != std::string::npos); -} - -TEST(jq, jq_nested_object) { - Pipeline p; - p.set_stdin("{\"a\":{\"b\":{\"c\":42}}}\n"); - p.add(L"jq.exe", {L"."}); - auto r = p.run(); - - EXPECT_EQ(r.exit_code, 0); - EXPECT_TRUE(r.stdout_text.find("42") != std::string::npos); -} - -TEST(jq, jq_invalid_json) { - Pipeline p; - p.set_stdin("{invalid json}"); - p.add(L"jq.exe", {L"."}); - auto r = p.run(); - - // Should fail with parse error - EXPECT_NE(r.exit_code, 0); -} - -TEST(jq, jq_comment_support) { - Pipeline p; - p.set_stdin("{/* comment */\"name\":\"test\"}\n"); - p.add(L"jq.exe", {L"."}); - auto r = p.run(); - - EXPECT_EQ(r.exit_code, 0); - EXPECT_TRUE(r.stdout_text.find("test") != std::string::npos); -} diff --git a/tests/unit/wpm/wpm_unit_test.cpp b/tests/unit/wpm/wpm_unit_test.cpp new file mode 100644 index 0000000..a237941 --- /dev/null +++ b/tests/unit/wpm/wpm_unit_test.cpp @@ -0,0 +1,252 @@ +/* + * Copyright (c) 2026 [caomengxuan666] + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to + * deal in the Software without restriction, including without limitation the + * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or + * sell copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + */ + +#include "framework/winuxtest.h" + +namespace { + +auto build_winuxcmd_path() -> std::filesystem::path { + return std::filesystem::path(WINUXCMD_BIN_DIR) / L"winuxcmd.exe"; +} + +auto current_arch_key() -> std::string { +#if defined(_M_ARM64) || defined(__aarch64__) + return "windows-arm64"; +#else + return "windows-x64"; +#endif +} + +auto widen_ascii(const std::string& text) -> std::wstring { + return std::wstring(text.begin(), text.end()); +} + +auto file_url(std::filesystem::path path) -> std::string { + auto text = path.string(); + std::ranges::replace(text, '\\', '/'); + return "file://" + text; +} + +auto same_file(const std::filesystem::path& a, const std::filesystem::path& b) + -> bool { + HANDLE ha = + CreateFileW(a.wstring().c_str(), FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + HANDLE hb = + CreateFileW(b.wstring().c_str(), FILE_READ_ATTRIBUTES, + FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, + nullptr, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr); + if (ha == INVALID_HANDLE_VALUE || hb == INVALID_HANDLE_VALUE) { + if (ha != INVALID_HANDLE_VALUE) CloseHandle(ha); + if (hb != INVALID_HANDLE_VALUE) CloseHandle(hb); + return false; + } + BY_HANDLE_FILE_INFORMATION ia{}; + BY_HANDLE_FILE_INFORMATION ib{}; + bool ok = GetFileInformationByHandle(ha, &ia) != 0 && + GetFileInformationByHandle(hb, &ib) != 0; + CloseHandle(ha); + CloseHandle(hb); + return ok && ia.dwVolumeSerialNumber == ib.dwVolumeSerialNumber && + ia.nFileIndexHigh == ib.nFileIndexHigh && + ia.nFileIndexLow == ib.nFileIndexLow; +} + +} // namespace + +TEST(wpm, wpm_hardlink_entrypoint_reports_version) { + Pipeline p; + p.add(L"wpm.exe", {L"version"}); + auto r = p.run(); + + EXPECT_EQ(r.exit_code, 0); + EXPECT_TRUE(r.stdout_text.find("wpm 0.2.0") != std::string::npos); +} + +TEST(wpm, wpm_links_list_includes_internal_tool) { + Pipeline p; + p.add(L"winuxcmd.exe", {L"wpm", L"links", L"list"}); + auto r = p.run(); + + EXPECT_EQ(r.exit_code, 0); + EXPECT_TRUE(r.stdout_text.find("wpm") != std::string::npos); + EXPECT_TRUE(r.stdout_text.find("ls") != std::string::npos); + EXPECT_TRUE(r.stdout_text.find("jq") == std::string::npos); +} + +TEST(wpm, wpm_index_status_uses_builtin_index_offline) { + TempDir tmp; + Pipeline p; + p.add(L"winuxcmd.exe", {L"wpm", L"index", L"status", L"--root", tmp.wpath()}); + auto r = p.run(); + + EXPECT_EQ(r.exit_code, 0); + EXPECT_TRUE(r.stdout_text.find("WPM index") != std::string::npos); + EXPECT_TRUE(r.stdout_text.find("packages:") != std::string::npos); +} + +TEST(wpm, wpm_links_rebuild_creates_internal_tool_hardlink) { + TempDir tmp; + auto root_exe = tmp.path / L"winuxcmd.exe"; + std::filesystem::copy_file(build_winuxcmd_path(), root_exe, + std::filesystem::copy_options::overwrite_existing); + + Pipeline p; + p.add(L"winuxcmd.exe", + {L"wpm", L"links", L"rebuild", L"--root", tmp.wpath()}); + auto r = p.run(); + + EXPECT_EQ(r.exit_code, 0); + auto wpm_exe = tmp.path / L"wpm.exe"; + EXPECT_TRUE(std::filesystem::exists(wpm_exe)); + EXPECT_TRUE(same_file(root_exe, wpm_exe)); +} + +TEST(wpm, wpm_links_rebuild_removes_legacy_jq_hardlink) { + TempDir tmp; + auto root_exe = tmp.path / L"winuxcmd.exe"; + auto legacy_jq = tmp.path / L"jq.exe"; + std::filesystem::copy_file(build_winuxcmd_path(), root_exe, + std::filesystem::copy_options::overwrite_existing); + bool linked = CreateHardLinkW(legacy_jq.wstring().c_str(), + root_exe.wstring().c_str(), nullptr) != 0; + EXPECT_TRUE(linked); + if (!linked) return; + + Pipeline p; + p.add(L"winuxcmd.exe", + {L"wpm", L"links", L"rebuild", L"--root", tmp.wpath()}); + auto r = p.run(); + + EXPECT_EQ(r.exit_code, 0); + EXPECT_FALSE(std::filesystem::exists(legacy_jq)); + EXPECT_TRUE(std::filesystem::exists(tmp.path / L"wpm.exe")); +} + +TEST(wpm, wpm_index_update_uses_local_file_source) { + TempDir tmp; + const auto index_path = tmp.path / L"fixture-index.json"; + tmp.write("fixture-index.json", + "{\n" + " \"schema\": 1,\n" + " \"name\": \"fixture\",\n" + " \"version\": \"fixture-1\",\n" + " \"packages\": [\n" + " {\"name\":\"fixture-tool\",\"version\":\"1.0.0\"," + "\"description\":\"Local fixture\",\"kind\":\"external\"," + "\"artifacts\":{}}\n" + " ]\n" + "}\n"); + + Pipeline add; + add.add(L"winuxcmd.exe", + {L"wpm", L"source", L"add", L"fixture", + widen_ascii(file_url(index_path)), L"--root", tmp.wpath()}); + auto add_result = add.run(); + EXPECT_EQ(add_result.exit_code, 0); + + Pipeline use; + use.add(L"winuxcmd.exe", + {L"wpm", L"source", L"use", L"fixture", L"--root", tmp.wpath()}); + auto use_result = use.run(); + EXPECT_EQ(use_result.exit_code, 0); + + Pipeline update; + update.add(L"winuxcmd.exe", + {L"wpm", L"index", L"update", L"--root", tmp.wpath()}); + auto update_result = update.run(); + EXPECT_EQ(update_result.exit_code, 0); + EXPECT_TRUE(update_result.stdout_text.find("index updated from fixture") != + std::string::npos); + + Pipeline info; + info.add(L"winuxcmd.exe", + {L"wpm", L"info", L"fixture-tool", L"--root", tmp.wpath()}); + auto info_result = info.run(); + EXPECT_EQ(info_result.exit_code, 0); + EXPECT_TRUE(info_result.stdout_text.find("Version: 1.0.0") != + std::string::npos); +} + +TEST(wpm, wpm_install_downloads_local_exe_with_sha256) { + TempDir tmp; + const auto artifact_path = tmp.path / L"source" / L"jq.exe"; + const auto index_path = tmp.path / L"fixture-install-index.json"; + const auto root_exe = tmp.path / L"winuxcmd.exe"; + const auto legacy_jq = tmp.path / L"jq.exe"; + std::filesystem::copy_file(build_winuxcmd_path(), root_exe, + std::filesystem::copy_options::overwrite_existing); + bool linked = CreateHardLinkW(legacy_jq.wstring().c_str(), + root_exe.wstring().c_str(), nullptr) != 0; + EXPECT_TRUE(linked); + if (!linked) return; + tmp.write("source/jq.exe", "external exe\n"); + + std::string index_json = + "{\n" + " \"schema\": 1,\n" + " \"name\": \"fixture\",\n" + " \"version\": \"fixture-install\",\n" + " \"packages\": [\n" + " {\n" + " \"name\": \"jq\",\n" + " \"version\": \"1.0.0\",\n" + " \"description\": \"Local fixture executable\",\n" + " \"kind\": \"external\",\n" + " \"artifacts\": {\n" + " \"" + + current_arch_key() + + "\": {\n" + " \"type\": \"exe\",\n" + " \"sha256\": " + "\"5140f4f6bf8b5691b7bccc1c4f00a2027dae00b2110d38a1e090af291226f322\",\n" + " \"urls\": [\"" + + file_url(artifact_path) + + "\"],\n" + " \"files\": [{\"from\":\"bin/jq.exe\"}]\n" + " }\n" + " }\n" + " }\n" + " ]\n" + "}\n"; + tmp.write("fixture-install-index.json", index_json); + + Pipeline add; + add.add(L"winuxcmd.exe", + {L"wpm", L"source", L"add", L"fixture", + widen_ascii(file_url(index_path)), L"--root", tmp.wpath()}); + EXPECT_EQ(add.run().exit_code, 0); + + Pipeline use; + use.add(L"winuxcmd.exe", + {L"wpm", L"source", L"use", L"fixture", L"--root", tmp.wpath()}); + EXPECT_EQ(use.run().exit_code, 0); + + Pipeline update; + update.add(L"winuxcmd.exe", + {L"wpm", L"index", L"update", L"--root", tmp.wpath()}); + EXPECT_EQ(update.run().exit_code, 0); + + Pipeline install; + install.add(L"winuxcmd.exe", + {L"wpm", L"install", L"jq", L"--root", tmp.wpath()}); + auto install_result = install.run(); + EXPECT_EQ(install_result.exit_code, 0); + if (install_result.exit_code != 0) return; + EXPECT_TRUE(install_result.stdout_text.find("installed jq") != + std::string::npos); + EXPECT_EQ(tmp.read("jq.exe"), "external exe\n"); + EXPECT_FALSE(same_file(root_exe, legacy_jq)); +} diff --git a/wpm-source/index.json b/wpm-source/index.json new file mode 100644 index 0000000..524777c --- /dev/null +++ b/wpm-source/index.json @@ -0,0 +1,97 @@ +{ + "schema": 1, + "name": "official", + "version": "builtin-2026.07.22", + "updated": "2026-07-22", + "sources": [ + { + "name": "official-github-raw", + "region": "global", + "priority": 10, + "index_urls": [ + "https://raw.githubusercontent.com/unixwin/WinuxCmd/main/wpm-source/index.json" + ] + }, + { + "name": "official-github-release", + "region": "global", + "priority": 20, + "index_urls": [ + "https://github.com/unixwin/WinuxCmd/releases/latest/download/wpm-index.json" + ] + }, + { + "name": "official-cn", + "region": "cn", + "priority": 30, + "index_urls": [] + } + ], + "packages": [ + { + "name": "winuxcmd", + "version": "0.12.4", + "description": "WinuxCmd core command set", + "kind": "core", + "artifacts": { + "windows-x64": { + "type": "zip", + "sha256": "", + "urls": [], + "files": [ + { + "from": "winuxcmd.exe", + "to": "winuxcmd.exe" + } + ] + }, + "windows-arm64": { + "type": "zip", + "sha256": "", + "urls": [], + "files": [ + { + "from": "winuxcmd.exe", + "to": "winuxcmd.exe" + } + ] + } + } + }, + { + "name": "jq", + "version": "", + "description": "External jq package slot. Fill this from a refreshed index.", + "kind": "external", + "artifacts": {} + }, + { + "name": "ncat", + "version": "", + "description": "External ncat/netcat package slot. Fill this from a refreshed index.", + "kind": "external", + "artifacts": {} + }, + { + "name": "7zip", + "version": "", + "description": "External 7-Zip package slot. Fill this from a refreshed index.", + "kind": "external", + "artifacts": {} + }, + { + "name": "zstd", + "version": "", + "description": "External zstd package slot. Fill this from a refreshed index.", + "kind": "external", + "artifacts": {} + }, + { + "name": "yq", + "version": "", + "description": "External yq package slot. Fill this from a refreshed index.", + "kind": "external", + "artifacts": {} + } + ] +}