Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 124 additions & 0 deletions .github/workflows/lint-powershell.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
name: PowerShell Install Scripts

on:
push:
paths:
- 'install/powershell/*.ps1'
- '.github/workflows/lint-powershell.yml'
pull_request:
paths:
- 'install/powershell/*.ps1'
- '.github/workflows/lint-powershell.yml'
workflow_dispatch:

jobs:
parse-windows-powershell-51:
name: Parse under Windows PowerShell 5.1
runs-on: windows-latest
steps:
- uses: actions/checkout@v4

# The install scripts are fetched over HTTP and executed on stock Windows,
# where powershell.exe (5.1) is the default shell. 5.1 reads BOM-less files
# as ANSI (CP1252), so a non-ASCII glyph such as U+2713 decodes into a smart
# quote that silently terminates the enclosing string. The resulting parse
# errors point at unrelated lines far below the real cause, so parse every
# script with 5.1 explicitly rather than relying on PowerShell 7.
- name: Parse each script with 5.1
shell: powershell
run: |
$ErrorActionPreference = 'Stop'
Write-Host "PowerShell $($PSVersionTable.PSVersion) / ANSI codepage $([System.Text.Encoding]::Default.WebName)"

$failed = $false
Get-ChildItem 'install/powershell' -Filter *.ps1 | Sort-Object Name | ForEach-Object {
$errors = $null
[System.Management.Automation.Language.Parser]::ParseFile(
$_.FullName, [ref]$null, [ref]$errors) | Out-Null

if ($errors -and $errors.Count -gt 0) {
$failed = $true
Write-Host "FAIL $($_.Name): $($errors.Count) parse error(s)"
$errors | Sort-Object { $_.Extent.StartLineNumber } | ForEach-Object {
Write-Host " L$($_.Extent.StartLineNumber): $($_.Message)"
}
} else {
Write-Host "OK $($_.Name)"
}
}

if ($failed) { exit 1 }

# A BOM would fix file-based parsing but break `irm ... | iex`: PowerShell 5.1
# decodes an HTTP text/* body with no charset as ISO-8859-1, turning the BOM
# into literal "i>>?" text and failing with
# "The term 'i>>?$ErrorActionPreference' is not recognized".
# Keeping the sources pure ASCII is what makes both paths safe, so enforce it.
- name: Require pure ASCII and no BOM
shell: powershell
run: |
$ErrorActionPreference = 'Stop'
$failed = $false

Get-ChildItem 'install/powershell' -Filter *.ps1 | Sort-Object Name | ForEach-Object {
$fileBad = $false
$bytes = [System.IO.File]::ReadAllBytes($_.FullName)

if ($bytes.Length -ge 3 -and $bytes[0] -eq 0xEF -and $bytes[1] -eq 0xBB -and $bytes[2] -eq 0xBF) {
$failed = $true
$fileBad = $true
Write-Host "FAIL $($_.Name): has a UTF-8 BOM (breaks 'irm | iex'); remove it"
}

# Report line/column of any byte outside ASCII so the fix is obvious.
$text = [System.Text.Encoding]::UTF8.GetString($bytes)
$lineNo = 0
foreach ($line in ($text -split "`r?`n")) {
$lineNo++
for ($i = 0; $i -lt $line.Length; $i++) {
if ([int]$line[$i] -gt 127) {
$failed = $true
$fileBad = $true
$cp = "U+{0:X4}" -f [int]$line[$i]
Write-Host "FAIL $($_.Name) L${lineNo} C$($i + 1): non-ASCII $cp"
Write-Host " build it from its code point instead, e.g. `$CHECK = [char]0x2713"
break
}
}
}

if (-not $fileBad) { Write-Host "OK $($_.Name)" }
}

if ($failed) { exit 1 }

parse-powershell-7:
name: Parse under PowerShell 7
runs-on: windows-latest
steps:
- uses: actions/checkout@v4

- name: Parse each script with 7
shell: pwsh
run: |
$ErrorActionPreference = 'Stop'
Write-Host "PowerShell $($PSVersionTable.PSVersion)"

$failed = $false
Get-ChildItem 'install/powershell' -Filter *.ps1 | Sort-Object Name | ForEach-Object {
$errors = $null
[System.Management.Automation.Language.Parser]::ParseFile(
$_.FullName, [ref]$null, [ref]$errors) | Out-Null

if ($errors -and $errors.Count -gt 0) {
$failed = $true
Write-Host "FAIL $($_.Name): $($errors.Count) parse error(s)"
$errors | Sort-Object { $_.Extent.StartLineNumber } | ForEach-Object {
Write-Host " L$($_.Extent.StartLineNumber): $($_.Message)"
}
} else {
Write-Host "OK $($_.Name)"
}
}

if ($failed) { exit 1 }
16 changes: 11 additions & 5 deletions install/powershell/examples.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@ if (-not $env:GRAY) { $GRAY = "$([char]27)[38;2;128;128;128m" } els
if (-not $env:BOLD) { $BOLD = "$([char]27)[1m" } else { $BOLD = $env:BOLD }
if (-not $env:NC) { $NC = "$([char]27)[0m" } else { $NC = $env:NC }

# Symbols built from code points so this file stays pure ASCII. Windows PowerShell 5.1
# reads BOM-less files as ANSI (CP1252), where a literal U+2713 decodes to a smart quote
# that silently terminates the enclosing string and breaks parsing.
$CHECK = [char]0x2713
$CROSS = [char]0x2717

# Examples configuration
$EXAMPLES_FOLDER_NAME = "plainlang-examples"
$EXAMPLES_DOWNLOAD_URL = "https://codeplain.ai/examples/windows"
Expand Down Expand Up @@ -40,7 +46,7 @@ if (-not (Test-Path $EXTRACT_PATH -PathType Container)) {
try {
New-Item -ItemType Directory -Path $EXTRACT_PATH -Force | Out-Null
} catch {
Write-Host " ${RED} Failed to create directory: ${EXTRACT_PATH}${NC}"
Write-Host " ${RED}${CROSS} Failed to create directory: ${EXTRACT_PATH}${NC}"
Write-Host " ${GRAY}Skipping example download.${NC}"
$SKIP_DOWNLOAD = $true
}
Expand Down Expand Up @@ -73,7 +79,7 @@ if (-not $SKIP_DOWNLOAD) {

Clear-Host
Write-Host ""
Write-Host " ${GREEN} Examples downloaded successfully!${NC}"
Write-Host " ${GREEN}${CHECK} Examples downloaded successfully!${NC}"
Write-Host ""
Write-Host " ${GRAY}Examples are in: ${EXTRACTED_DIR}${NC}"
Write-Host ""
Expand All @@ -86,15 +92,15 @@ if (-not $SKIP_DOWNLOAD) {
Write-Host " ${GRAY}See hello-world/python/README.md for details.${NC}"
Write-Host ""
} catch {
Write-Host " ${RED} Failed to extract examples.${NC}"
Write-Host " ${RED}${CROSS} Failed to extract examples.${NC}"
}

Remove-Item -Path $TEMP_ZIP -Force -ErrorAction SilentlyContinue
} else {
Write-Host " ${RED} Failed to download examples.${NC}"
Write-Host " ${RED}${CROSS} Failed to download examples.${NC}"
}
} catch {
Write-Host " ${RED} Failed to download examples.${NC}"
Write-Host " ${RED}${CROSS} Failed to download examples.${NC}"
Remove-Item -Path $TEMP_ZIP -Force -ErrorAction SilentlyContinue
}

Expand Down
34 changes: 20 additions & 14 deletions install/powershell/install.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,12 @@ $GRAY_LIGHT = "$ESC[38;2;211;211;211m" # #D3D3D3
$BOLD = "$ESC[1m"
$NC = "$ESC[0m" # No Color / Reset

# Symbols built from code points so this file stays pure ASCII. Windows PowerShell 5.1
# reads BOM-less files as ANSI (CP1252), where a literal U+2713 decodes to a smart quote
# that silently terminates the enclosing string and breaks parsing.
$CHECK = [char]0x2713
$ROCKET = [System.Char]::ConvertFromUtf32(0x1F680)

# Export colors for child scripts (as environment variables)
$env:YELLOW = $YELLOW
$env:GREEN = $GREEN
Expand Down Expand Up @@ -92,11 +98,11 @@ function Test-ApiKey {
if (-not (Get-Command uv -ErrorAction SilentlyContinue)) {
Write-Host "${GRAY}uv is not installed.${NC}"
Install-Uv
Write-Host "${GREEN}${NC} uv installed successfully"
Write-Host "${GREEN}${CHECK}${NC} uv installed successfully"
Write-Host ""
}

Write-Host "${GREEN}${NC} uv detected"
Write-Host "${GREEN}${CHECK}${NC} uv detected"
Write-Host ""

try {
Expand Down Expand Up @@ -125,16 +131,16 @@ if ($codeplainLine) {
$newLine = @(uv tool list 2>$null) | Where-Object { $_ -match '^codeplain' } | Select-Object -First 1
$newVersion = ($newLine -replace 'codeplain v', '').Trim()
if ($currentVersion -eq $newVersion) {
Write-Host "${GREEN}${NC} codeplain is already up to date (${newVersion})"
Write-Host "${GREEN}${CHECK}${NC} codeplain is already up to date (${newVersion})"
} else {
Write-Host "${GREEN}${NC} codeplain upgraded from ${currentVersion} to ${newVersion}!"
Write-Host "${GREEN}${CHECK}${NC} codeplain upgraded from ${currentVersion} to ${newVersion}!"
}
} else {
Write-Host "Installing codeplain...${NC}"
Write-Host ""
uv tool install codeplain
if (-not $nonInteractive) { Clear-Host }
Write-Host "${GREEN} codeplain installed successfully!${NC}"
Write-Host "${GREEN}${CHECK} codeplain installed successfully!${NC}"
}

# Ensure uv tool bin directory is on user PATH permanently (so codeplain is available)
Expand All @@ -146,12 +152,12 @@ if ($userPath) {
$newPath = ($userPath.TrimEnd(';') + ';' + $uvBinDir)
[Environment]::SetEnvironmentVariable('Path', $newPath, 'User')
$env:Path = $uvBinDir + ';' + $env:Path
Write-Host "${GREEN}${NC} added $uvBinDir to your user PATH"
Write-Host "${GREEN}${CHECK}${NC} added $uvBinDir to your user PATH"
}
} else {
[Environment]::SetEnvironmentVariable('Path', $uvBinDir, 'User')
$env:Path = $uvBinDir + ';' + $env:Path
Write-Host "${GREEN}${NC} added $uvBinDir to your user PATH"
Write-Host "${GREEN}${CHECK}${NC} added $uvBinDir to your user PATH"
}
Write-Host ""

Expand Down Expand Up @@ -181,7 +187,7 @@ if ($env:CODEPLAIN_API_KEY) {
Write-Host "${GRAY}Verifying your existing API key...${NC}"
$existingResult = Test-ApiKey $env:CODEPLAIN_API_KEY
if ($existingResult -eq "valid") {
Write-Host "${GREEN}${NC} Using existing API key."
Write-Host "${GREEN}${CHECK}${NC} Using existing API key."
$skipApiKeySetup = $true
$apiKeyVerified = $true
} elseif ($existingResult -eq "invalid") {
Expand Down Expand Up @@ -225,7 +231,7 @@ if (-not $skipApiKeySetup) {
Write-Host "${GRAY}Verifying your API key...${NC}"
$result = Test-ApiKey $apiKey
if ($result -eq "valid") {
Write-Host "${GREEN}${NC} API key verified."
Write-Host "${GREEN}${CHECK}${NC} API key verified."
Write-Host ""
$apiKeyVerified = $true
break
Expand All @@ -252,7 +258,7 @@ if ($skipApiKeySetup) {

# Persist as user environment variable (survives reboots)
[Environment]::SetEnvironmentVariable('CODEPLAIN_API_KEY', $apiKey, 'User')
Write-Host "${GREEN} API key saved to user environment variables${NC}"
Write-Host "${GREEN}${CHECK} API key saved to user environment variables${NC}"
}

# ASCII Art Welcome
Expand All @@ -268,7 +274,7 @@ Write-Host @'
Write-Host ""
# Only claim success when a verified API key is actually configured.
if ($apiKeyVerified) {
Write-Host "${GREEN} Sign in successful.${NC}"
Write-Host "${GREEN}${CHECK} Sign in successful.${NC}"
Write-Host ""
}
Write-Host " ${WHITE}Welcome to *codeplain!${NC}"
Expand Down Expand Up @@ -392,7 +398,7 @@ $plynInstalled = $false
if ($editorCmds.Count -gt 0 -and $installPlyn -notmatch '^[Nn]$') {
foreach ($editor in $editorCmds) {
& $editor.Cmd --install-extension Codeplain.plyn
Write-Host "${GREEN}${NC} plyn installed for $($editor.Name)"
Write-Host "${GREEN}${CHECK}${NC} plyn installed for $($editor.Name)"
}
$plynInstalled = $true
Write-Host ""
Expand Down Expand Up @@ -435,7 +441,7 @@ if ($env:CODEPLAIN_API_KEY) {
$verifyOk = $false
}
if ($verifyOk) {
Write-Host "${GREEN}${NC} Installation verified."
Write-Host "${GREEN}${CHECK}${NC} Installation verified."
} else {
Write-Host "${RED}Something went wrong during installation.${NC}"
Write-Host "${GRAY}Output of 'codeplain --status':${NC}"
Expand Down Expand Up @@ -473,7 +479,7 @@ Write-Host " ${WHITE}${BOLD}codeplain your-project.plain${NC}"
Write-Host ""
Write-Host " ${GRAY}Discord: https://discord.gg/cgbynb9hFq Docs: https://plainlang.org/${NC}"
Write-Host ""
Write-Host " ${GRAY}Happy development!${NC} 🚀"
Write-Host " ${GRAY}Happy development!${NC} ${ROCKET}"
Write-Host ""

# Refresh environment for this session
Expand Down
Loading
Loading