NobGit
public imalexnord read

ms.open

Adds Mac "Open ." command to Windows

Languages

Repository composition by tracked source files.

PowerShell
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
Alex Nord <[email protected]> 11 days ago
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.'