public
imalexnord
read
ms.open
Adds Mac "Open ." command to Windows
Languages
Repository composition by tracked source files.
PowerShell
100%
Create file
Wiki Documentation
Clone
https://nobgit.com/user/imalexnord/ms.open.git
ssh://[email protected]:2222/user/imalexnord/ms.open.git
Commit
Added more mac commmands
a6e9016
License.md | 41 +++++
bin/MSOpen.ps1 | 465 ++++++++++++++++++++++++++++++++++++++++++++++++++++++
bin/copypath.cmd | 3 +
bin/killport.cmd | 3 +
bin/msopen.cmd | 3 +
bin/open.cmd | 3 +
bin/paths.cmd | 3 +
bin/pbcopy.cmd | 3 +
bin/pbpaste.cmd | 3 +
bin/ports.cmd | 3 +
bin/reveal.cmd | 3 +
bin/serve.cmd | 3 +
bin/sha256.cmd | 3 +
bin/which.cmd | 3 +
install.ps1 | 126 +++++++++++++++
readme.md | 188 ++++++++++++++++++++++
shell/MS.Open.ps1 | 25 +++
uninstall.ps1 | 75 +++++++++
18 files changed, 956 insertions(+)
create mode 100644 License.md
create mode 100644 bin/MSOpen.ps1
create mode 100644 bin/copypath.cmd
create mode 100644 bin/killport.cmd
create mode 100644 bin/msopen.cmd
create mode 100644 bin/open.cmd
create mode 100644 bin/paths.cmd
create mode 100644 bin/pbcopy.cmd
create mode 100644 bin/pbpaste.cmd
create mode 100644 bin/ports.cmd
create mode 100644 bin/reveal.cmd
create mode 100644 bin/serve.cmd
create mode 100644 bin/sha256.cmd
create mode 100644 bin/which.cmd
create mode 100644 install.ps1
create mode 100644 readme.md
create mode 100644 shell/MS.Open.ps1
create mode 100644 uninstall.ps1
Diff
diff --git a/License.md b/License.md
new file mode 100644
index 0000000..d27a421
--- /dev/null
+++ b/License.md
@@ -0,0 +1,41 @@
+MIT License
+
+
+
+Copyright (c) 2026 Alex Nord
+
+
+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.
+
diff --git a/bin/MSOpen.ps1 b/bin/MSOpen.ps1
new file mode 100644
index 0000000..f3eabdc
--- /dev/null
+++ b/bin/MSOpen.ps1
@@ -0,0 +1,465 @@
+[CmdletBinding()]
+param(
+ [Parameter(Position = 0, Mandatory = $true)]
+ [string]$Command,
+
+ [Parameter(Position = 1, ValueFromRemainingArguments = $true)]
+ [string[]]$Arguments
+)
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+$Arguments = @($Arguments)
+
+function Resolve-MSOpenPath {
+ param(
+ [Parameter(Mandatory)]
+ [string]$Path
+ )
+
+ $resolved = Resolve-Path -LiteralPath $Path -ErrorAction Stop
+ return $resolved.Path
+}
+
+function Invoke-Open {
+ param([string[]]$Args)
+
+ $target = if ($Args.Count -gt 0) {
+ $Args[0]
+ }
+ else {
+ (Get-Location).Path
+ }
+
+ if (Test-Path -LiteralPath $target) {
+ $target = Resolve-MSOpenPath -Path $target
+ }
+
+ Start-Process -FilePath $target
+}
+
+function Invoke-Reveal {
+ param([string[]]$Args)
+
+ if ($Args.Count -lt 1) {
+ throw 'Usage: reveal <file-or-folder>'
+ }
+
+ $target = Resolve-MSOpenPath -Path $Args[0]
+
+ if (Test-Path -LiteralPath $target -PathType Container) {
+ Start-Process -FilePath 'explorer.exe' -ArgumentList "`"$target`""
+ return
+ }
+
+ Start-Process -FilePath 'explorer.exe' -ArgumentList "/select,`"$target`""
+}
+
+function Invoke-CopyPath {
+ param([string[]]$Args)
+
+ $target = if ($Args.Count -gt 0) {
+ Resolve-MSOpenPath -Path $Args[0]
+ }
+ else {
+ (Get-Location).Path
+ }
+
+ Set-Clipboard -Value $target
+ Write-Output $target
+}
+
+function Invoke-Paths {
+ $entries = $env:Path -split ';' |
+ ForEach-Object { $_.Trim() } |
+ Where-Object { $_ }
+
+ $index = 1
+ foreach ($entry in $entries) {
+ [pscustomobject]@{
+ Number = $index
+ Path = $entry
+ Exists = Test-Path -LiteralPath $entry
+ }
+ $index++
+ }
+}
+
+function Invoke-Which {
+ param([string[]]$Args)
+
+ if ($Args.Count -lt 1) {
+ throw 'Usage: which <command>'
+ }
+
+ $commands = Get-Command -Name $Args[0] -All -ErrorAction Stop
+
+ foreach ($item in $commands) {
+ $location = $null
+
+ if ($item.PSObject.Properties.Name -contains 'Path' -and $item.Path) {
+ $location = $item.Path
+ }
+ elseif ($item.Source) {
+ $location = $item.Source
+ }
+ elseif ($item.Definition) {
+ $location = $item.Definition
+ }
+
+ [pscustomobject]@{
+ CommandType = $item.CommandType
+ Name = $item.Name
+ Location = $location
+ }
+ }
+}
+
+function Get-ListeningPorts {
+ $connections = Get-NetTCPConnection -State Listen -ErrorAction Stop |
+ Sort-Object LocalPort, LocalAddress
+
+ foreach ($connection in $connections) {
+ $processName = '<unknown>'
+
+ if ($connection.OwningProcess -eq 4) {
+ $processName = 'System'
+ }
+ else {
+ try {
+ $processName = (Get-Process -Id $connection.OwningProcess -ErrorAction Stop).ProcessName
+ }
+ catch {
+ $processName = '<unavailable>'
+ }
+ }
+
+ [pscustomobject]@{
+ Protocol = 'TCP'
+ Address = $connection.LocalAddress
+ Port = $connection.LocalPort
+ PID = $connection.OwningProcess
+ Process = $processName
+ }
+ }
+}
+
+function Invoke-Ports {
+ Get-ListeningPorts | Format-Table -AutoSize
+}
+
+function Invoke-KillPort {
+ param([string[]]$Args)
+
+ if ($Args.Count -lt 1) {
+ throw 'Usage: killport <port> [--force]'
+ }
+
+ $port = 0
+ if (-not [int]::TryParse($Args[0], [ref]$port) -or $port -lt 1 -or $port -gt 65535) {
+ throw "Invalid port: $($Args[0])"
+ }
+
+ $force = $Args -contains '--force'
+ $connections = Get-NetTCPConnection -State Listen -LocalPort $port -ErrorAction SilentlyContinue
+ $processIds = @($connections | Select-Object -ExpandProperty OwningProcess -Unique)
+
+ if ($processIds.Count -eq 0) {
+ throw "Nothing is listening on port $port."
+ }
+
+ $targets = foreach ($processId in $processIds) {
+ $name = '<unknown>'
+ try {
+ $name = (Get-Process -Id $processId -ErrorAction Stop).ProcessName
+ }
+ catch {
+ $name = '<unavailable>'
+ }
+
+ [pscustomobject]@{
+ Port = $port
+ PID = $processId
+ Process = $name
+ }
+ }
+
+ $targets | Format-Table -AutoSize
+
+ $protected = @($targets | Where-Object { $_.PID -in 0, 4 })
+ if ($protected.Count -gt 0) {
+ throw 'Refusing to terminate a protected Windows system process.'
+ }
+
+ if (-not $force) {
+ $answer = Read-Host "Terminate the process owning port $port? [y/N]"
+ if ($answer -notmatch '^(y|yes)$') {
+ Write-Host 'Cancelled.'
+ return
+ }
+ }
+
+ foreach ($target in $targets) {
+ Stop-Process -Id $target.PID -Force -ErrorAction Stop
+ Write-Host "Stopped $($target.Process) (PID $($target.PID))."
+ }
+}
+
+function Invoke-Sha256 {
+ param([string[]]$Args)
+
+ if ($Args.Count -lt 1) {
+ throw 'Usage: sha256 <file>'
+ }
+
+ $target = Resolve-MSOpenPath -Path $Args[0]
+
+ if (-not (Test-Path -LiteralPath $target -PathType Leaf)) {
+ throw "Not a file: $target"
+ }
+
+ $hash = Get-FileHash -LiteralPath $target -Algorithm SHA256
+ Write-Output "$($hash.Hash.ToLowerInvariant()) $target"
+}
+
+function Invoke-Pbcopy {
+ param([string[]]$Args)
+
+ $text = if ($Args.Count -gt 0) {
+ $Args -join ' '
+ }
+ elseif ([Console]::IsInputRedirected) {
+ [Console]::In.ReadToEnd()
+ }
+ else {
+ ''
+ }
+
+ Set-Clipboard -Value $text
+}
+
+function Invoke-Pbpaste {
+ $value = Get-Clipboard -Raw
+ if ($null -ne $value) {
+ [Console]::Out.Write($value)
+ }
+}
+
+function Get-MimeType {
+ param([string]$Path)
+
+ switch ([IO.Path]::GetExtension($Path).ToLowerInvariant()) {
+ '.html' { 'text/html; charset=utf-8' }
+ '.htm' { 'text/html; charset=utf-8' }
+ '.css' { 'text/css; charset=utf-8' }
+ '.js' { 'text/javascript; charset=utf-8' }
+ '.json' { 'application/json; charset=utf-8' }
+ '.xml' { 'application/xml; charset=utf-8' }
+ '.txt' { 'text/plain; charset=utf-8' }
+ '.md' { 'text/markdown; charset=utf-8' }
+ '.svg' { 'image/svg+xml' }
+ '.png' { 'image/png' }
+ '.jpg' { 'image/jpeg' }
+ '.jpeg' { 'image/jpeg' }
+ '.gif' { 'image/gif' }
+ '.webp' { 'image/webp' }
+ '.ico' { 'image/x-icon' }
+ '.pdf' { 'application/pdf' }
+ '.wasm' { 'application/wasm' }
+ '.zip' { 'application/zip' }
+ default { 'application/octet-stream' }
+ }
+}
+
+function Send-TextResponse {
+ param(
+ [Parameter(Mandatory)]
+ [System.Net.HttpListenerResponse]$Response,
+
+ [Parameter(Mandatory)]
+ [int]$StatusCode,
+
+ [Parameter(Mandatory)]
+ [string]$Text,
+
+ [string]$ContentType = 'text/plain; charset=utf-8'
+ )
+
+ $bytes = [Text.Encoding]::UTF8.GetBytes($Text)
+ $Response.StatusCode = $StatusCode
+ $Response.ContentType = $ContentType
+ $Response.ContentLength64 = $bytes.Length
+ $Response.OutputStream.Write($bytes, 0, $bytes.Length)
+}
+
+function Get-DirectoryListingHtml {
+ param(
+ [Parameter(Mandatory)]
+ [string]$Directory,
+
+ [Parameter(Mandatory)]
+ [string]$RequestPath
+ )
+
+ $encodedTitle = [Net.WebUtility]::HtmlEncode("Index of $RequestPath")
+ $builder = [Text.StringBuilder]::new()
+
+ [void]$builder.AppendLine('<!doctype html>')
+ [void]$builder.AppendLine('<html lang="en"><head><meta charset="utf-8">')
+ [void]$builder.AppendLine("<title>$encodedTitle</title>")
+ [void]$builder.AppendLine('<style>body{font-family:system-ui,sans-serif;max-width:900px;margin:40px auto;padding:0 20px}li{margin:8px 0}a{text-decoration:none}a:hover{text-decoration:underline}</style>')
+ [void]$builder.AppendLine('</head><body>')
+ [void]$builder.AppendLine("<h1>$encodedTitle</h1><ul>")
+
+ if ($RequestPath -ne '/') {
+ [void]$builder.AppendLine('<li><a href="../">../</a></li>')
+ }
+
+ foreach ($item in Get-ChildItem -LiteralPath $Directory | Sort-Object @{ Expression = { -not $_.PSIsContainer } }, Name) {
+ $displayName = $item.Name + $(if ($item.PSIsContainer) { '/' } else { '' })
+ $hrefName = [Uri]::EscapeDataString($item.Name) + $(if ($item.PSIsContainer) { '/' } else { '' })
+ $encodedName = [Net.WebUtility]::HtmlEncode($displayName)
+ [void]$builder.AppendLine("<li><a href=`"$hrefName`">$encodedName</a></li>")
+ }
+
+ [void]$builder.AppendLine('</ul></body></html>')
+ return $builder.ToString()
+}
+
+function Invoke-Serve {
+ param([string[]]$Args)
+
+ $port = 8000
+ if ($Args.Count -gt 0) {
+ if (-not [int]::TryParse($Args[0], [ref]$port) -or $port -lt 1 -or $port -gt 65535) {
+ throw "Invalid port: $($Args[0])"
+ }
+ }
+
+ $root = [IO.Path]::GetFullPath((Get-Location).Path)
+ $rootWithSeparator = $root.TrimEnd('\') + '\'
+ $prefix = "http://127.0.0.1:$port/"
+ $listener = [System.Net.HttpListener]::new()
+ $listener.Prefixes.Add($prefix)
+
+ try {
+ $listener.Start()
+ Write-Host "Serving $root"
+ Write-Host $prefix
+ Write-Host 'Press Ctrl+C to stop.'
+
+ while ($listener.IsListening) {
+ $context = $listener.GetContext()
+ $response = $context.Response
+
+ try {
+ $requestPath = [Uri]::UnescapeDataString($context.Request.Url.AbsolutePath)
+ $relativePath = $requestPath.TrimStart('/').Replace('/', '\')
+ $candidate = [IO.Path]::GetFullPath((Join-Path $root $relativePath))
+
+ $insideRoot = $candidate.Equals($root, [StringComparison]::OrdinalIgnoreCase) -or
+ $candidate.StartsWith($rootWithSeparator, [StringComparison]::OrdinalIgnoreCase)
+
+ if (-not $insideRoot) {
+ Send-TextResponse -Response $response -StatusCode 403 -Text 'Forbidden'
+ continue
+ }
+
+ if (Test-Path -LiteralPath $candidate -PathType Container) {
+ $indexFile = @('index.html', 'index.htm') |
+ ForEach-Object { Join-Path $candidate $_ } |
+ Where-Object { Test-Path -LiteralPath $_ -PathType Leaf } |
+ Select-Object -First 1
+
+ if ($indexFile) {
+ $candidate = $indexFile
+ }
+ else {
+ $html = Get-DirectoryListingHtml -Directory $candidate -RequestPath $requestPath
+ Send-TextResponse -Response $response -StatusCode 200 -Text $html -ContentType 'text/html; charset=utf-8'
+ continue
+ }
+ }
+
+ if (-not (Test-Path -LiteralPath $candidate -PathType Leaf)) {
+ Send-TextResponse -Response $response -StatusCode 404 -Text 'Not found'
+ continue
+ }
+
+ $fileInfo = Get-Item -LiteralPath $candidate
+ $response.StatusCode = 200
+ $response.ContentType = Get-MimeType -Path $candidate
+ $response.ContentLength64 = $fileInfo.Length
+
+ $stream = [IO.File]::OpenRead($candidate)
+ try {
+ $stream.CopyTo($response.OutputStream)
+ }
+ finally {
+ $stream.Dispose()
+ }
+ }
+ catch {
+ if ($response.OutputStream.CanWrite) {
+ Send-TextResponse -Response $response -StatusCode 500 -Text 'Internal server error'
+ }
+ }
+ finally {
+ $response.OutputStream.Close()
+ }
+ }
+ }
+ finally {
+ if ($listener.IsListening) {
+ $listener.Stop()
+ }
+ $listener.Close()
+ }
+}
+
+function Show-Help {
+ @'
+MS.Open commands
+
+ open [path] Open a folder, file, or URL
+ reveal <path> Show a file in Explorer
+ copypath [path] Copy a full path to the clipboard
+ which <command> Explain which command will run
+ paths Print PATH entries one per row
+ serve [port] Serve the current folder at 127.0.0.1
+ ports Show listening TCP ports
+ killport <port> Stop the process listening on a port
+ sha256 <file> Calculate a SHA-256 hash
+ pbcopy [text] Copy arguments or piped input
+ pbpaste Print clipboard text
+
+PowerShell functions installed separately:
+
+ mkcd <folder> Create a directory and enter it
+ reload Reload the current PowerShell profile
+'@
+}
+
+try {
+ switch ($Command.ToLowerInvariant()) {
+ 'open' { Invoke-Open -Args $Arguments }
+ 'reveal' { Invoke-Reveal -Args $Arguments }
+ 'copypath' { Invoke-CopyPath -Args $Arguments }
+ 'which' { Invoke-Which -Args $Arguments }
+ 'paths' { Invoke-Paths }
+ 'serve' { Invoke-Serve -Args $Arguments }
+ 'ports' { Invoke-Ports }
+ 'killport' { Invoke-KillPort -Args $Arguments }
+ 'sha256' { Invoke-Sha256 -Args $Arguments }
+ 'pbcopy' { Invoke-Pbcopy -Args $Arguments }
+ 'pbpaste' { Invoke-Pbpaste }
+ 'help' { Show-Help }
+ '--help' { Show-Help }
+ '-h' { Show-Help }
+ default { throw "Unknown command: $Command`n`n$(Show-Help)" }
+ }
+}
+catch {
+ Write-Error $_.Exception.Message
+ exit 1
+}
diff --git a/bin/copypath.cmd b/bin/copypath.cmd
new file mode 100644
index 0000000..85ac924
--- /dev/null
+++ b/bin/copypath.cmd
@@ -0,0 +1,3 @@
+@echo off
+powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0MSOpen.ps1" copypath %*
+exit /b %errorlevel%
diff --git a/bin/killport.cmd b/bin/killport.cmd
new file mode 100644
index 0000000..fad74b2
--- /dev/null
+++ b/bin/killport.cmd
@@ -0,0 +1,3 @@
+@echo off
+powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0MSOpen.ps1" killport %*
+exit /b %errorlevel%
diff --git a/bin/msopen.cmd b/bin/msopen.cmd
new file mode 100644
index 0000000..1e1ac89
--- /dev/null
+++ b/bin/msopen.cmd
@@ -0,0 +1,3 @@
+@echo off
+powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0MSOpen.ps1" help
+exit /b %errorlevel%
diff --git a/bin/open.cmd b/bin/open.cmd
new file mode 100644
index 0000000..eb20f70
--- /dev/null
+++ b/bin/open.cmd
@@ -0,0 +1,3 @@
+@echo off
+powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0MSOpen.ps1" open %*
+exit /b %errorlevel%
diff --git a/bin/paths.cmd b/bin/paths.cmd
new file mode 100644
index 0000000..865dfe3
--- /dev/null
+++ b/bin/paths.cmd
@@ -0,0 +1,3 @@
+@echo off
+powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0MSOpen.ps1" paths %*
+exit /b %errorlevel%
diff --git a/bin/pbcopy.cmd b/bin/pbcopy.cmd
new file mode 100644
index 0000000..15a4fd1
--- /dev/null
+++ b/bin/pbcopy.cmd
@@ -0,0 +1,3 @@
+@echo off
+powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0MSOpen.ps1" pbcopy %*
+exit /b %errorlevel%
diff --git a/bin/pbpaste.cmd b/bin/pbpaste.cmd
new file mode 100644
index 0000000..477eade
--- /dev/null
+++ b/bin/pbpaste.cmd
@@ -0,0 +1,3 @@
+@echo off
+powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0MSOpen.ps1" pbpaste %*
+exit /b %errorlevel%
diff --git a/bin/ports.cmd b/bin/ports.cmd
new file mode 100644
index 0000000..db35e5c
--- /dev/null
+++ b/bin/ports.cmd
@@ -0,0 +1,3 @@
+@echo off
+powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0MSOpen.ps1" ports %*
+exit /b %errorlevel%
diff --git a/bin/reveal.cmd b/bin/reveal.cmd
new file mode 100644
index 0000000..c9aa994
--- /dev/null
+++ b/bin/reveal.cmd
@@ -0,0 +1,3 @@
+@echo off
+powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0MSOpen.ps1" reveal %*
+exit /b %errorlevel%
diff --git a/bin/serve.cmd b/bin/serve.cmd
new file mode 100644
index 0000000..fd4b41f
--- /dev/null
+++ b/bin/serve.cmd
@@ -0,0 +1,3 @@
+@echo off
+powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0MSOpen.ps1" serve %*
+exit /b %errorlevel%
diff --git a/bin/sha256.cmd b/bin/sha256.cmd
new file mode 100644
index 0000000..6a4f1d4
--- /dev/null
+++ b/bin/sha256.cmd
@@ -0,0 +1,3 @@
+@echo off
+powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0MSOpen.ps1" sha256 %*
+exit /b %errorlevel%
diff --git a/bin/which.cmd b/bin/which.cmd
new file mode 100644
index 0000000..3c5c431
--- /dev/null
+++ b/bin/which.cmd
@@ -0,0 +1,3 @@
+@echo off
+powershell.exe -NoLogo -NoProfile -ExecutionPolicy Bypass -File "%~dp0MSOpen.ps1" which %*
+exit /b %errorlevel%
diff --git a/install.ps1 b/install.ps1
new file mode 100644
index 0000000..622747e
--- /dev/null
+++ b/install.ps1
@@ -0,0 +1,126 @@
+[CmdletBinding()]
+param()
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+$root = $PSScriptRoot
+$binPath = Join-Path $root 'bin'
+$shellScript = Join-Path $root 'shell\MS.Open.ps1'
+
+if (-not (Test-Path -LiteralPath $binPath -PathType Container)) {
+ throw "Missing bin directory: $binPath"
+}
+
+if (-not (Test-Path -LiteralPath $shellScript -PathType Leaf)) {
+ throw "Missing shell integration script: $shellScript"
+}
+
+function Normalize-PathEntry {
+ param([string]$Value)
+
+ if ([string]::IsNullOrWhiteSpace($Value)) {
+ return ''
+ }
+
+ return $Value.Trim().TrimEnd('\')
+}
+
+function Add-UserPath {
+ param([string]$PathToAdd)
+
+ $current = [Environment]::GetEnvironmentVariable('Path', 'User')
+ $entries = @(
+ $current -split ';' |
+ ForEach-Object { Normalize-PathEntry $_ } |
+ Where-Object { $_ }
+ )
+
+ $normalizedTarget = Normalize-PathEntry $PathToAdd
+ $alreadyPresent = $entries | Where-Object {
+ $_.Equals($normalizedTarget, [StringComparison]::OrdinalIgnoreCase)
+ }
+
+ if (-not $alreadyPresent) {
+ $newPath = (@($entries) + $normalizedTarget) -join ';'
+ [Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
+ Write-Host "Added to user PATH: $PathToAdd"
+ }
+ else {
+ Write-Host "Already in user PATH: $PathToAdd"
+ }
+
+ $processEntries = @($env:Path -split ';')
+ $inCurrentProcess = $processEntries | Where-Object {
+ (Normalize-PathEntry $_).Equals($normalizedTarget, [StringComparison]::OrdinalIgnoreCase)
+ }
+
+ if (-not $inCurrentProcess) {
+ $env:Path = "$env:Path;$PathToAdd"
+ }
+}
+
+function Update-PowerShellProfile {
+ param(
+ [Parameter(Mandatory)]
+ [string]$ProfilePath,
+
+ [Parameter(Mandatory)]
+ [string]$IntegrationScript
+ )
+
+ $directory = Split-Path -Parent $ProfilePath
+ New-Item -ItemType Directory -Path $directory -Force | Out-Null
+
+ $beginMarker = '# >>> MS.Open >>>'
+ $endMarker = '# <<< MS.Open <<<'
+ $escapedScript = $IntegrationScript.Replace("'", "''")
+
+ $block = @"
+$beginMarker
+`$msOpenShell = '$escapedScript'
+if (Test-Path -LiteralPath `$msOpenShell) {
+ . `$msOpenShell
+}
+$endMarker
+"@
+
+ $content = ''
+ if (Test-Path -LiteralPath $ProfilePath) {
+ $content = Get-Content -LiteralPath $ProfilePath -Raw
+ $timestamp = Get-Date -Format 'yyyyMMdd-HHmmss'
+ Copy-Item -LiteralPath $ProfilePath -Destination "$ProfilePath.msopen-backup-$timestamp"
+ }
+
+ $pattern = "(?ms)^$([regex]::Escape($beginMarker)).*?^$([regex]::Escape($endMarker))\s*"
+ $cleaned = [regex]::Replace($content, $pattern, '').TrimEnd()
+
+ if ($cleaned) {
+ $newContent = "$cleaned`r`n`r`n$block`r`n"
+ }
+ else {
+ $newContent = "$block`r`n"
+ }
+
+ Set-Content -LiteralPath $ProfilePath -Value $newContent -Encoding UTF8
+ Write-Host "Configured PowerShell profile: $ProfilePath"
+}
+
+Add-UserPath -PathToAdd $binPath
+
+$documents = [Environment]::GetFolderPath('MyDocuments')
+$profiles = @(
+ (Join-Path $documents 'WindowsPowerShell\Microsoft.PowerShell_profile.ps1'),
+ (Join-Path $documents 'PowerShell\Microsoft.PowerShell_profile.ps1')
+) | Select-Object -Unique
+
+foreach ($profilePath in $profiles) {
+ Update-PowerShellProfile -ProfilePath $profilePath -IntegrationScript $shellScript
+}
+
+Write-Host ''
+Write-Host 'MS.Open is installed.'
+Write-Host 'Restart your terminal, then try:'
+Write-Host ' open .'
+Write-Host ' mkcd test-folder'
+Write-Host ' paths'
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..4bd85f0
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,188 @@
+\# MS.Open
+
+
+
+MS.Open brings the macOS-style `open` command to Windows.
+
+
+
+Use `open .` from PowerShell, Command Prompt, or Windows Terminal to open the current directory in File Explorer. It can also open folders, files, and URLs using their default Windows applications.
+
+
+
+\## Installation
+
+
+
+1\. Download or clone this repository.
+
+
+
+2\. Place the repository in a permanent location, for example:
+
+
+
+```text
+
+C:\\MS.Open
+
+```
+
+
+
+The command file should then be located at:
+
+
+
+```text
+
+C:\\MS.Open\\open.cmd
+
+```
+
+
+
+3\. Add the folder to your Windows `PATH`:
+
+
+
+```text
+
+C:\\MS.Open
+
+```
+
+
+
+Add the folder itself, not the full path to `open.cmd`.
+
+
+
+To edit your `PATH`:
+
+
+
+1\. Search Windows for \*\*Environment Variables\*\*.
+
+2\. Open \*\*Edit the system environment variables\*\*.
+
+3\. Select \*\*Environment Variables\*\*.
+
+4\. Under \*\*User variables\*\*, select `Path`.
+
+5\. Click \*\*Edit\*\*, then \*\*New\*\*.
+
+6\. Add:
+
+
+
+```text
+
+C:\\MS.Open
+
+```
+
+
+
+7\. Save the changes and restart your terminal.
+
+
+
+\## Verify Installation
+
+
+
+In PowerShell, run:
+
+
+
+```powershell
+
+Get-Command open
+
+```
+
+
+
+It should return:
+
+
+
+```text
+
+C:\\MS.Open\\open.cmd
+
+```
+
+
+
+\## Usage
+
+
+
+Open the current directory:
+
+
+
+```powershell
+
+open .
+
+```
+
+
+
+Open the parent directory:
+
+
+
+```powershell
+
+open ..
+
+```
+
+
+
+Open another folder:
+
+
+
+```powershell
+
+open "C:\\Projects"
+
+```
+
+
+
+Open a file:
+
+
+
+```powershell
+
+open README.md
+
+```
+
+
+
+Open a website:
+
+
+
+```powershell
+
+open https://nobgit.com
+
+```
+
+
+
+\## Uninstall
+
+
+
+Remove `C:\\MS.Open` from your Windows `PATH`, then delete the folder.
+
diff --git a/shell/MS.Open.ps1 b/shell/MS.Open.ps1
new file mode 100644
index 0000000..138ee38
--- /dev/null
+++ b/shell/MS.Open.ps1
@@ -0,0 +1,25 @@
+# MS.Open PowerShell integration
+
+function mkcd {
+ [CmdletBinding()]
+ param(
+ [Parameter(Mandatory, Position = 0)]
+ [string]$Path
+ )
+
+ $directory = New-Item -ItemType Directory -Path $Path -Force
+ Set-Location -LiteralPath $directory.FullName
+}
+
+function reload {
+ [CmdletBinding()]
+ param()
+
+ if (-not (Test-Path -LiteralPath $PROFILE)) {
+ Write-Warning "No PowerShell profile exists at: $PROFILE"
+ return
+ }
+
+ . $PROFILE
+ Write-Host "Reloaded $PROFILE"
+}
diff --git a/uninstall.ps1 b/uninstall.ps1
new file mode 100644
index 0000000..85e7a79
--- /dev/null
+++ b/uninstall.ps1
@@ -0,0 +1,75 @@
+[CmdletBinding()]
+param()
+
+Set-StrictMode -Version Latest
+$ErrorActionPreference = 'Stop'
+
+$root = $PSScriptRoot
+$binPath = Join-Path $root 'bin'
+
+function Normalize-PathEntry {
+ param([string]$Value)
+
+ if ([string]::IsNullOrWhiteSpace($Value)) {
+ return ''
+ }
+
+ return $Value.Trim().TrimEnd('\')
+}
+
+function Remove-UserPath {
+ param([string]$PathToRemove)
+
+ $current = [Environment]::GetEnvironmentVariable('Path', 'User')
+ $normalizedTarget = Normalize-PathEntry $PathToRemove
+
+ $entries = @(
+ $current -split ';' |
+ ForEach-Object { Normalize-PathEntry $_ } |
+ Where-Object {
+ $_ -and -not $_.Equals($normalizedTarget, [StringComparison]::OrdinalIgnoreCase)
+ }
+ )
+
+ [Environment]::SetEnvironmentVariable('Path', ($entries -join ';'), 'User')
+ Write-Host "Removed from user PATH: $PathToRemove"
+}
+
+function Remove-ProfileBlock {
+ param([string]$ProfilePath)
+
+ if (-not (Test-Path -LiteralPath $ProfilePath)) {
+ return
+ }
+
+ $beginMarker = '# >>> MS.Open >>>'
+ $endMarker = '# <<< MS.Open <<<'
+ $content = Get-Content -LiteralPath $ProfilePath -Raw
+ $pattern = "(?ms)^$([regex]::Escape($beginMarker)).*?^$([regex]::Escape($endMarker))\s*"
+ $cleaned = [regex]::Replace($content, $pattern, '').TrimEnd()
+
+ if ($cleaned) {
+ Set-Content -LiteralPath $ProfilePath -Value "$cleaned`r`n" -Encoding UTF8
+ }
+ else {
+ Set-Content -LiteralPath $ProfilePath -Value '' -Encoding UTF8
+ }
+
+ Write-Host "Removed MS.Open from profile: $ProfilePath"
+}
+
+Remove-UserPath -PathToRemove $binPath
+
+$documents = [Environment]::GetFolderPath('MyDocuments')
+$profiles = @(
+ (Join-Path $documents 'WindowsPowerShell\Microsoft.PowerShell_profile.ps1'),
+ (Join-Path $documents 'PowerShell\Microsoft.PowerShell_profile.ps1')
+) | Select-Object -Unique
+
+foreach ($profilePath in $profiles) {
+ Remove-ProfileBlock -ProfilePath $profilePath
+}
+
+Write-Host ''
+Write-Host 'MS.Open has been uninstalled from PATH and PowerShell profiles.'
+Write-Host 'Restart your terminal. You can then delete this folder.'