anthropic-skills/skills/vscode/scripts/vscode-cli.ps1

1400 lines
55 KiB
PowerShell

# vscode-cli.ps1 - VS Code skill driver script.
#
# Areas: info, ext, settings, python, ssh, wsl, tasks, launch, extensions
# Products: vscode (default), vscode-insiders, or any name resolved via config.
#
# Path resolution order:
# 1. Explicit env vars: VSCODE_CODE_CLI / VSCODE_INSIDERS_CLI
# 2. config.products.<name>.cliPath (config file at $VSCODE_SKILL_CONFIG
# or the platform default location)
# 3. Built-in defaults for vscode and vscode-insiders on Windows
# 4. Get-Command lookup on PATH
#
# Per-machine preferences (default product, ssh defaults, etc.) come from the
# same config file under the matching top-level keys.
[CmdletBinding()]
param(
[Parameter(Position = 0)]
[ValidateSet("info", "ext", "python", "ssh", "wsl", "settings", "tasks", "launch", "extensions", "settings-sync", "workspace", "profile")]
[string]$Area = "info",
[Parameter(Position = 1)]
[string]$Action,
[string]$Id,
[string]$Path,
[string]$ProjectPath,
[string]$VenvName,
[string]$ExpectedVenvName,
[string]$HostName,
[string]$Distro,
[string]$LinuxPath,
[string]$Command,
[string]$Scope,
[string]$FontFamily,
[string]$FontSize,
[string]$LineHeight,
[string]$ColorTheme,
[string]$IconTheme,
[string]$ProductIconTheme,
[string]$WordWrap,
[string]$TabSize,
[string]$FormatOnSave,
[string]$AutoSave,
[string]$Minimap,
[string]$SettingKey,
[string]$SettingValue,
[string]$Template,
[string]$Stack,
[string]$Profile,
[string]$Folder,
[switch]$Force,
[switch]$Apply,
[switch]$ForceOpenVsx,
[string]$Product,
[Parameter(ValueFromRemainingArguments = $true)]
[string[]]$Rest
)
$ErrorActionPreference = "Stop"
# === Argument normalization ==================================================
if ($Action -and $Action -match "^--") {
$Rest = @($Action) + $Rest
$Action = $null
}
for ($i = 0; $i -lt $Rest.Count; $i++) {
$name = $Rest[$i]
if ($name -notmatch "^--") {
throw "Unexpected argument '$name'. Use named options such as --Id value."
}
$key = $name.Substring(2).ToLowerInvariant()
if ($key -eq "force") { $Force = $true; continue }
if ($key -eq "apply") { $Apply = $true; continue }
if ($key -eq "forceopenvsx") { $ForceOpenVsx = $true; continue }
if ($i + 1 -ge $Rest.Count) {
throw "Missing value for argument '$name'."
}
$value = $Rest[++$i]
switch ($key) {
"id" { $Id = $value }
"path" { $Path = $value }
"projectpath" { $ProjectPath = $value }
"project-path" { $ProjectPath = $value }
"venvname" { $VenvName = $value }
"venv-name" { $VenvName = $value }
"expectedvenvname" { $ExpectedVenvName = $value }
"expected-venv-name" { $ExpectedVenvName = $value }
"hostname" { $HostName = $value }
"host-name" { $HostName = $value }
"distro" { $Distro = $value }
"linuxpath" { $LinuxPath = $value }
"linux-path" { $LinuxPath = $value }
"command" { $Command = $value }
"scope" { $Scope = $value }
"fontfamily" { $FontFamily = $value }
"font-family" { $FontFamily = $value }
"fontsize" { $FontSize = $value }
"font-size" { $FontSize = $value }
"lineheight" { $LineHeight = $value }
"line-height" { $LineHeight = $value }
"theme" { $ColorTheme = $value }
"colortheme" { $ColorTheme = $value }
"color-theme" { $ColorTheme = $value }
"icontheme" { $IconTheme = $value }
"icon-theme" { $IconTheme = $value }
"producticontheme" { $ProductIconTheme = $value }
"product-icon-theme" { $ProductIconTheme = $value }
"wordwrap" { $WordWrap = $value }
"word-wrap" { $WordWrap = $value }
"tabsize" { $TabSize = $value }
"tab-size" { $TabSize = $value }
"formatonsave" { $FormatOnSave = $value }
"format-on-save" { $FormatOnSave = $value }
"autosave" { $AutoSave = $value }
"auto-save" { $AutoSave = $value }
"minimap" { $Minimap = $value }
"settingkey" { $SettingKey = $value }
"setting-key" { $SettingKey = $value }
"key" { $SettingKey = $value }
"settingvalue" { $SettingValue = $value }
"setting-value" { $SettingValue = $value }
"value" { $SettingValue = $value }
"product" { $Product = $value }
"template" { $Template = $value }
"stack" { $Stack = $value }
"profile" { $Profile = $value }
"folder" { $Folder = $value }
default { throw "Unknown argument '$name'." }
}
}
if (-not $Product) {
$Product = "auto"
}
# === Configuration resolution ===============================================
function Get-DefaultConfigPath {
if ($env:VSCODE_SKILL_CONFIG) {
return $env:VSCODE_SKILL_CONFIG
}
if ($env:APPDATA) {
return (Join-Path $env:APPDATA "vscode-skill\config.json")
}
$xdg = $env:XDG_CONFIG_HOME
if ($xdg) {
return (Join-Path $xdg "vscode-skill/config.json")
}
if ($env:HOME) {
return (Join-Path $env:HOME ".config/vscode-skill/config.json")
}
return $null
}
function Read-SkillConfig {
$path = Get-DefaultConfigPath
if (-not $path) { return $null }
if (-not (Test-Path -LiteralPath $path)) { return $null }
try {
$raw = Get-Content -LiteralPath $path -Encoding UTF8 -Raw
} catch {
return $null
}
if ([string]::IsNullOrWhiteSpace($raw)) { return $null }
try {
return ($raw | ConvertFrom-Json)
} catch {
Write-Warning "Config file at $path is not valid JSON; ignoring."
return $null
}
}
function Get-ConfigDefaultProduct {
if ($env:VSCODE_SKILL_PRODUCT) {
return $env:VSCODE_SKILL_PRODUCT.ToLowerInvariant()
}
$cfg = Read-SkillConfig
if ($cfg -and $cfg.PSObject.Properties.Match('product').Count -and $cfg.product) {
return ([string]$cfg.product).ToLowerInvariant()
}
return $null
}
function Get-ConfigProductCli {
param([Parameter(Mandatory = $true)][string]$ProductName)
$cfg = Read-SkillConfig
if (-not $cfg) { return $null }
if (-not $cfg.PSObject.Properties.Match('products').Count) { return $null }
$products = $cfg.products
if (-not $products.PSObject.Properties.Match($ProductName).Count) { return $null }
$entry = $products.$ProductName
if ($entry -and $entry.PSObject.Properties.Match('cliPath').Count -and $entry.cliPath) {
return [string]$entry.cliPath
}
return $null
}
function Get-ConfigSshDefaults {
$cfg = Read-SkillConfig
if ($cfg -and $cfg.PSObject.Properties.Match('ssh').Count -and $cfg.ssh) {
return $cfg.ssh
}
return $null
}
function Get-ConfigWslDefaults {
$cfg = Read-SkillConfig
if ($cfg -and $cfg.PSObject.Properties.Match('wsl').Count -and $cfg.wsl) {
return $cfg.wsl
}
return $null
}
# === Product CLI resolution =================================================
function Resolve-VsCodeCli {
if ($env:VSCODE_CODE_CLI -and (Test-Path -LiteralPath $env:VSCODE_CODE_CLI)) {
return (Resolve-Path -LiteralPath $env:VSCODE_CODE_CLI).Path
}
$cfgPath = Get-ConfigProductCli -ProductName "vscode"
if ($cfgPath -and (Test-Path -LiteralPath $cfgPath)) {
return (Resolve-Path -LiteralPath $cfgPath).Path
}
$candidates = @()
$cmd = Get-Command "code.cmd" -ErrorAction SilentlyContinue
if ($cmd) { $candidates += $cmd.Source }
$cmd = Get-Command "code" -ErrorAction SilentlyContinue
if ($cmd) { $candidates += $cmd.Source }
if ($env:LOCALAPPDATA) {
$candidates += (Join-Path $env:LOCALAPPDATA "Programs\Microsoft VS Code\bin\code.cmd")
}
foreach ($candidate in ($candidates | Where-Object { $_ } | Select-Object -Unique)) {
if (Test-Path -LiteralPath $candidate) {
return (Resolve-Path -LiteralPath $candidate).Path
}
}
throw "Cannot find VS Code CLI. Set VSCODE_CODE_CLI or add 'products.vscode.cliPath' to your config."
}
function Resolve-VsCodeInsidersCli {
if ($env:VSCODE_INSIDERS_CLI -and (Test-Path -LiteralPath $env:VSCODE_INSIDERS_CLI)) {
return (Resolve-Path -LiteralPath $env:VSCODE_INSIDERS_CLI).Path
}
$cfgPath = Get-ConfigProductCli -ProductName "vscode-insiders"
if ($cfgPath -and (Test-Path -LiteralPath $cfgPath)) {
return (Resolve-Path -LiteralPath $cfgPath).Path
}
$candidates = @()
$cmd = Get-Command "code-insiders.cmd" -ErrorAction SilentlyContinue
if ($cmd) { $candidates += $cmd.Source }
$cmd = Get-Command "code-insiders" -ErrorAction SilentlyContinue
if ($cmd) { $candidates += $cmd.Source }
if ($env:LOCALAPPDATA) {
$candidates += (Join-Path $env:LOCALAPPDATA "Programs\Microsoft VS Code Insiders\bin\code-insiders.cmd")
}
foreach ($candidate in ($candidates | Where-Object { $_ } | Select-Object -Unique)) {
if (Test-Path -LiteralPath $candidate) {
return (Resolve-Path -LiteralPath $candidate).Path
}
}
throw "Cannot find VS Code Insiders CLI. Set VSCODE_INSIDERS_CLI or add 'products.vscode-insiders.cliPath' to your config."
}
function Resolve-GenericProductCli {
param([Parameter(Mandatory = $true)][string]$ProductName)
$cfgPath = Get-ConfigProductCli -ProductName $ProductName
if (-not $cfgPath) {
throw "Product '$ProductName' has no CLI path configured. Add 'products.$ProductName.cliPath' to your config file or set VSCODE_SKILL_CONFIG pointing at one that includes it."
}
if (-not (Test-Path -LiteralPath $cfgPath)) {
throw "Configured CLI for '$ProductName' does not exist: $cfgPath"
}
return (Resolve-Path -LiteralPath $cfgPath).Path
}
function Resolve-ProductCli {
$active = Get-ActiveProduct
switch ($active) {
"vscode" { return (Resolve-VsCodeCli) }
"vscode-insiders" { return (Resolve-VsCodeInsidersCli) }
default { return (Resolve-GenericProductCli -ProductName $active) }
}
}
function Get-ActiveProduct {
if ($Product -ne "auto") {
return $Product
}
$default = Get-ConfigDefaultProduct
if ($default -and $default -ne "auto") {
return $default
}
try {
$null = Resolve-VsCodeCli
return "vscode"
} catch {
}
try {
$null = Resolve-VsCodeInsidersCli
return "vscode-insiders"
} catch {
}
throw "Cannot auto-detect a VS Code-family CLI. Pass --Product, set VSCODE_SKILL_PRODUCT, or configure cliPath in your config file."
}
function Get-ProductLabel {
$active = Get-ActiveProduct
switch ($active) {
"vscode" { return "VS Code" }
"vscode-insiders" { return "VS Code Insiders" }
default { return $active }
}
}
function Get-ProductUserDataPath {
$active = Get-ActiveProduct
if ($env:VSCODE_USER_DATA) {
return $env:VSCODE_USER_DATA
}
switch ($active) {
"vscode" {
if ($env:APPDATA) { return (Join-Path $env:APPDATA "Code\User") }
throw "VSCODE_USER_DATA not set and APPDATA unavailable."
}
"vscode-insiders" {
if ($env:APPDATA) { return (Join-Path $env:APPDATA "Code - Insiders\User") }
throw "VSCODE_USER_DATA not set and APPDATA unavailable."
}
default {
$cfg = Read-SkillConfig
if ($cfg -and $cfg.products -and $cfg.products.$active -and $cfg.products.$active.userDataPath) {
return [string]$cfg.products.$active.userDataPath
}
throw "Product '$active' has no userDataPath configured. Set VSCODE_USER_DATA or add 'products.$active.userDataPath' to your config."
}
}
}
function Get-ProductExtensionDataPath {
$active = Get-ActiveProduct
if ($env:VSCODE_EXTENSIONS_DIR) {
return $env:VSCODE_EXTENSIONS_DIR
}
$userHome = $env:USERPROFILE
if (-not $userHome) { $userHome = $env:HOME }
if (-not $userHome) { throw "USERPROFILE/HOME not set." }
switch ($active) {
"vscode" { return (Join-Path $userHome ".vscode\extensions") }
"vscode-insiders" { return (Join-Path $userHome ".vscode-insiders\extensions") }
default {
$cfg = Read-SkillConfig
if ($cfg -and $cfg.products -and $cfg.products.$active -and $cfg.products.$active.extensionsPath) {
return [string]$cfg.products.$active.extensionsPath
}
return (Join-Path $userHome ".$active\extensions")
}
}
}
function Invoke-ProductCli {
param([Parameter(ValueFromRemainingArguments = $true)][string[]]$CliArgs)
$cli = Resolve-ProductCli
& $cli @CliArgs
$script:LastProductExitCode = $LASTEXITCODE
}
# === Output helpers =========================================================
function Write-Section {
param([string]$Name)
Write-Host ""
Write-Host "== $Name =="
}
# === info ===================================================================
function Invoke-Info {
$active = Get-ActiveProduct
$label = Get-ProductLabel
$cli = Resolve-ProductCli
Write-Section "$label CLI"
try {
[pscustomobject]@{
Product = $active
CliPath = $cli
UserSettings = (Join-Path (Get-ProductUserDataPath) "settings.json")
UserKeybindings = (Join-Path (Get-ProductUserDataPath) "keybindings.json")
UserSnippets = (Join-Path (Get-ProductUserDataPath) "snippets")
Extensions = (Get-ProductExtensionDataPath)
ConfigFile = (Get-DefaultConfigPath)
} | Format-List
} catch {
Write-Warning "Could not resolve user data path: $($_.Exception.Message)"
}
Write-Section "Version"
& $cli --version
Write-Section "Paths"
$paths = @()
try { $paths += (Get-ProductUserDataPath) } catch { }
try { $paths += (Get-ProductExtensionDataPath) } catch { }
$pathRows = foreach ($p in ($paths | Where-Object { $_ } | Select-Object -Unique)) {
[pscustomobject]@{ Path = $p; Exists = (Test-Path -LiteralPath $p) }
}
if ($pathRows) {
$pathRows | Format-Table -AutoSize
} else {
Write-Host "(no path information available)"
}
}
# === ext ====================================================================
function Get-ExtensionsJsonRows {
$extensionsJson = Join-Path (Get-ProductExtensionDataPath) "extensions.json"
if (-not (Test-Path -LiteralPath $extensionsJson)) {
return @()
}
try {
$raw = Get-Content -LiteralPath $extensionsJson -Encoding UTF8 -Raw
} catch {
return @()
}
if ([string]::IsNullOrWhiteSpace($raw)) {
return @()
}
try {
$items = $raw | ConvertFrom-Json
} catch {
return @()
}
if (-not $items) { return @() }
return @($items)
}
function Find-InstalledExtensionPath {
param([string]$ExtensionId)
$extensionRoot = Get-ProductExtensionDataPath
if (-not (Test-Path -LiteralPath $extensionRoot)) {
return $null
}
$safeId = $ExtensionId.ToLowerInvariant()
$candidates = Get-ChildItem -LiteralPath $extensionRoot -Directory -ErrorAction SilentlyContinue |
Where-Object { $_.Name.ToLowerInvariant().StartsWith("$safeId-") -or $_.Name.ToLowerInvariant() -eq $safeId } |
Sort-Object LastWriteTime -Descending
if ($candidates) {
return $candidates[0].FullName
}
return $null
}
function Test-ExtensionInJson {
param([string]$ExtensionId)
$rows = Get-ExtensionsJsonRows
foreach ($row in $rows) {
if ($row.identifier -and $row.identifier.id -eq $ExtensionId) {
return $true
}
}
return $false
}
function Save-OpenVsxVsix {
param([string]$ExtensionId)
$parts = $ExtensionId.Split(".")
if ($parts.Count -lt 2) {
throw "Open VSX fallback requires publisher.extension id (got '$ExtensionId')."
}
$publisher = $parts[0]
$name = ($parts[1..($parts.Count - 1)] -join ".")
$apiUrl = "https://open-vsx.org/api/$publisher/$name"
$metadata = Invoke-RestMethod -Uri $apiUrl -Method Get
$version = $metadata.version
if (-not $version) {
throw "Open VSX metadata did not include a version for $ExtensionId."
}
$fileName = "$publisher.$name-$version@win32-x64.vsix"
$downloadUrl = "https://open-vsx.org/api/$publisher/$name/win32-x64/$version/file/$fileName"
$downloadDir = if ($env:VSCODE_SKILL_TEMP) {
(Join-Path $env:VSCODE_SKILL_TEMP "vsix")
} elseif ($env:TEMP) {
(Join-Path $env:TEMP "vscode-skill-vsix")
} else {
New-Item -ItemType Directory -Path "vscode-skill-vsix" -Force | Out-Null
(Resolve-Path -LiteralPath "vscode-skill-vsix").Path
}
New-Item -ItemType Directory -Path $downloadDir -Force | Out-Null
$outPath = Join-Path $downloadDir $fileName
try {
Invoke-WebRequest -Uri $downloadUrl -OutFile $outPath
} catch {
if ($metadata.files.download) {
Invoke-WebRequest -Uri $metadata.files.download -OutFile $outPath
} else {
throw
}
}
return $outPath
}
function Invoke-Ext {
switch ($Action) {
"list" {
Invoke-ProductCli --list-extensions --show-versions
if ($script:LastProductExitCode -ne 0) {
throw "Failed to list extensions for $(Get-ProductLabel)."
}
return
}
"locate" {
if (-not $Id) { throw "ext locate requires --Id." }
$located = Find-InstalledExtensionPath -ExtensionId $Id
if (-not $located) {
throw "Extension $Id was not found under $(Get-ProductExtensionDataPath)."
}
[pscustomobject]@{ Extension = $Id; Path = $located } | Format-List
return
}
"install" {
if (-not $Id -and -not $Path) { throw "ext install requires --Id or --Path." }
$target = if ($Path) { $Path } else { $Id }
$args = @("--install-extension", $target)
if ($Force) { $args += "--force" }
Write-Section "Install via $(Get-ProductLabel) CLI"
$cli = Resolve-ProductCli
& $cli @args
$exit = $LASTEXITCODE
if ($exit -ne 0 -and $Id -and -not $Path -and $ForceOpenVsx) {
Write-Warning "Direct gallery install failed. Retrying with Open VSX VSIX for $Id (--ForceOpenVsx)."
$vsix = Save-OpenVsxVsix -ExtensionId $Id
& $cli --install-extension $vsix
$exit = $LASTEXITCODE
} elseif ($exit -ne 0 -and $Id -and -not $Path) {
Write-Warning "Install failed. Re-run with --ForceOpenVsx to fall back to open-vsx.org if your environment blocks the official gallery."
}
if ($exit -ne 0) {
throw "Extension install failed for $target with exit code $exit."
}
if ($Id) {
Write-Section "Verify"
$locatedPath = Find-InstalledExtensionPath -ExtensionId $Id
$inJson = Test-ExtensionInJson -ExtensionId $Id
[pscustomobject]@{
Extension = $Id
LocatePath = $locatedPath
InExtensionsJson = $inJson
} | Format-List
if (-not $locatedPath) {
throw "Extension install verification failed for $Id."
}
}
return
}
default {
throw "Unknown ext action '$Action'. Use list, locate, or install."
}
}
}
# === settings ===============================================================
function Write-JsonFile {
param([string]$Path, [object]$Object)
$json = $Object | ConvertTo-Json -Depth 20
Set-Content -LiteralPath $Path -Value $json -Encoding UTF8
}
function Get-JsonObjectAsOrderedMap {
param([object]$Object)
$map = [ordered]@{}
if ($Object) {
foreach ($property in $Object.PSObject.Properties) {
$map[$property.Name] = $property.Value
}
}
return $map
}
function Convert-SettingValue {
param([string]$Value)
if ($null -eq $Value) { return $null }
$trimmed = $Value.Trim()
if ($trimmed -eq "") { return "" }
if ($trimmed -match "^(?i:true|false)$") { return [bool]::Parse($trimmed) }
if ($trimmed -match "^-?\d+$") { return [int]$trimmed }
if ($trimmed -match "^-?\d+\.\d+$") { return [double]$trimmed }
if (($trimmed.StartsWith("{") -and $trimmed.EndsWith("}")) -or ($trimmed.StartsWith("[") -and $trimmed.EndsWith("]"))) {
return ($trimmed | ConvertFrom-Json)
}
return $Value
}
function Get-SettingsTargetPath {
if (-not $Scope) { $Scope = "user" }
switch ($Scope.ToLowerInvariant()) {
"user" {
return (Join-Path (Get-ProductUserDataPath) "settings.json")
}
"project" {
if (-not $ProjectPath) { throw "Project settings require --ProjectPath." }
$resolvedProject = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ProjectPath)
return (Join-Path $resolvedProject ".vscode\settings.json")
}
default {
throw "Unknown settings scope '$Scope'. Use user or project."
}
}
}
function Read-SettingsMap {
param([string]$SettingsPath)
if (-not (Test-Path -LiteralPath $SettingsPath)) {
return [ordered]@{}
}
$raw = Get-Content -LiteralPath $SettingsPath -Encoding UTF8 -Raw
if ([string]::IsNullOrWhiteSpace($raw)) {
return [ordered]@{}
}
return (Get-JsonObjectAsOrderedMap -Object ($raw | ConvertFrom-Json))
}
function Write-SettingsMap {
param([string]$SettingsPath, [System.Collections.IDictionary]$Settings)
$parent = Split-Path -Parent $SettingsPath
if ($parent) {
New-Item -ItemType Directory -Path $parent -Force | Out-Null
}
Write-JsonFile -Path $SettingsPath -Object $Settings
}
function Set-SettingEntry {
param(
[System.Collections.IDictionary]$Settings,
[string]$Key,
[object]$Value
)
if ([string]::IsNullOrWhiteSpace($Key)) { return }
if ($null -eq $Value) { return }
$Settings[$Key] = $Value
}
function Add-EditorSettingsFromParameters {
param([System.Collections.IDictionary]$Settings)
Set-SettingEntry -Settings $Settings -Key "editor.fontFamily" -Value $FontFamily
if ($FontSize) { Set-SettingEntry -Settings $Settings -Key "editor.fontSize" -Value (Convert-SettingValue $FontSize) }
if ($LineHeight) { Set-SettingEntry -Settings $Settings -Key "editor.lineHeight" -Value (Convert-SettingValue $LineHeight) }
Set-SettingEntry -Settings $Settings -Key "workbench.colorTheme" -Value $ColorTheme
Set-SettingEntry -Settings $Settings -Key "workbench.iconTheme" -Value $IconTheme
Set-SettingEntry -Settings $Settings -Key "workbench.productIconTheme" -Value $ProductIconTheme
Set-SettingEntry -Settings $Settings -Key "editor.wordWrap" -Value $WordWrap
if ($TabSize) { Set-SettingEntry -Settings $Settings -Key "editor.tabSize" -Value (Convert-SettingValue $TabSize) }
if ($FormatOnSave) { Set-SettingEntry -Settings $Settings -Key "editor.formatOnSave" -Value (Convert-SettingValue $FormatOnSave) }
Set-SettingEntry -Settings $Settings -Key "files.autoSave" -Value $AutoSave
if ($Minimap) { Set-SettingEntry -Settings $Settings -Key "editor.minimap.enabled" -Value (Convert-SettingValue $Minimap) }
if ($SettingKey) { Set-SettingEntry -Settings $Settings -Key $SettingKey -Value (Convert-SettingValue $SettingValue) }
}
function Invoke-SettingsArea {
switch ($Action) {
"path" {
$settingsPath = Get-SettingsTargetPath
[pscustomobject]@{
Scope = $(if ($Scope) { $Scope } else { "user" })
Path = $settingsPath
Exists = (Test-Path -LiteralPath $settingsPath)
} | Format-List
return
}
"get" {
$settingsPath = Get-SettingsTargetPath
$settings = Read-SettingsMap -SettingsPath $settingsPath
Write-Section "Settings file"
[pscustomobject]@{ Path = $settingsPath; Exists = (Test-Path -LiteralPath $settingsPath) } | Format-List
Write-Section "Core editor settings"
$keys = @(
"editor.fontFamily",
"editor.fontSize",
"editor.lineHeight",
"editor.fontWeight",
"workbench.colorTheme",
"workbench.iconTheme",
"workbench.productIconTheme",
"editor.wordWrap",
"editor.tabSize",
"editor.formatOnSave",
"files.autoSave",
"editor.minimap.enabled"
)
$rows = foreach ($key in $keys) {
[pscustomobject]@{ Key = $key; Value = $settings[$key] }
}
$rows | Format-Table -AutoSize
return
}
"set" {
if (-not $Apply) { throw "settings set is a write operation. Pass --Apply." }
$settingsPath = Get-SettingsTargetPath
$settings = Read-SettingsMap -SettingsPath $settingsPath
Add-EditorSettingsFromParameters -Settings $settings
Write-SettingsMap -SettingsPath $settingsPath -Settings $settings
[pscustomobject]@{ Path = $settingsPath; Written = $true } | Format-List
return
}
"smoke" {
$oldScope = $Scope
$Scope = "user"
$settingsPath = Get-SettingsTargetPath
$parent = Split-Path -Parent $settingsPath
New-Item -ItemType Directory -Path $parent -Force | Out-Null
$existed = Test-Path -LiteralPath $settingsPath
$original = if ($existed) { Get-Content -LiteralPath $settingsPath -Encoding UTF8 -Raw } else { $null }
try {
$settings = Read-SettingsMap -SettingsPath $settingsPath
$settings["editor.fontFamily"] = "Consolas, 'Courier New', monospace"
$settings["editor.fontSize"] = 15
$settings["editor.lineHeight"] = 22
$settings["workbench.colorTheme"] = "Default Dark Modern"
$settings["editor.wordWrap"] = "on"
$settings["editor.tabSize"] = 2
$settings["editor.formatOnSave"] = $false
$settings["files.autoSave"] = "off"
$settings["editor.minimap.enabled"] = $false
Write-SettingsMap -SettingsPath $settingsPath -Settings $settings
$verify = Read-SettingsMap -SettingsPath $settingsPath
$ok = (
$verify["editor.fontFamily"] -eq "Consolas, 'Courier New', monospace" -and
[int]$verify["editor.fontSize"] -eq 15 -and
[int]$verify["editor.lineHeight"] -eq 22 -and
$verify["workbench.colorTheme"] -eq "Default Dark Modern" -and
$verify["editor.wordWrap"] -eq "on" -and
[int]$verify["editor.tabSize"] -eq 2 -and
[bool]$verify["editor.formatOnSave"] -eq $false -and
$verify["files.autoSave"] -eq "off" -and
[bool]$verify["editor.minimap.enabled"] -eq $false
)
if (-not $ok) {
throw "Settings smoke verification failed after write."
}
[pscustomobject]@{
Path = $settingsPath
TemporaryWriteVerified = $true
Restored = $false
} | Format-List
} finally {
if ($existed) {
Set-Content -LiteralPath $settingsPath -Value $original -Encoding UTF8 -NoNewline
} elseif (Test-Path -LiteralPath $settingsPath) {
Remove-Item -LiteralPath $settingsPath -Force
}
$Scope = $oldScope
}
$restored = if ($existed) {
(Get-Content -LiteralPath $settingsPath -Encoding UTF8 -Raw) -eq $original
} else {
-not (Test-Path -LiteralPath $settingsPath)
}
[pscustomobject]@{
Path = $settingsPath
RestoredOriginal = $restored
} | Format-List
if (-not $restored) {
throw "Settings smoke failed to restore original settings.json."
}
return
}
default {
throw "Unknown settings action '$Action'. Use path, get, set, or smoke."
}
}
}
# === python =================================================================
function Get-PythonExeForVenv {
param([string]$ProjectPath, [string]$VenvName)
$candidate = Join-Path $ProjectPath "$VenvName\Scripts\python.exe"
if (-not (Test-Path -LiteralPath $candidate)) {
throw "Python interpreter not found: $candidate"
}
return (Resolve-Path -LiteralPath $candidate).Path
}
function Invoke-PythonArea {
if (-not $ProjectPath) { throw "python action requires --ProjectPath." }
$ProjectPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ProjectPath)
switch ($Action) {
"make-venvs" {
New-Item -ItemType Directory -Path $ProjectPath -Force | Out-Null
New-Item -ItemType Directory -Path (Join-Path $ProjectPath ".vscode") -Force | Out-Null
$probe = @'
import json
import os
import sys
print(json.dumps({
"executable": sys.executable,
"prefix": sys.prefix,
"cwd": os.getcwd()
}, ensure_ascii=False))
'@
Set-Content -LiteralPath (Join-Path $ProjectPath "probe.py") -Value $probe -Encoding UTF8
foreach ($name in @(".venv-a", ".venv-b")) {
$venvPath = Join-Path $ProjectPath $name
if (-not (Test-Path -LiteralPath (Join-Path $venvPath "Scripts\python.exe"))) {
python -m venv $venvPath
}
}
[pscustomobject]@{
ProjectPath = $ProjectPath
VenvA = (Get-PythonExeForVenv -ProjectPath $ProjectPath -VenvName ".venv-a")
VenvB = (Get-PythonExeForVenv -ProjectPath $ProjectPath -VenvName ".venv-b")
Probe = (Join-Path $ProjectPath "probe.py")
} | Format-List
return
}
"set-interpreter" {
if (-not $VenvName) { throw "python set-interpreter requires --VenvName." }
$pythonExe = Get-PythonExeForVenv -ProjectPath $ProjectPath -VenvName $VenvName
$settingsPath = Join-Path $ProjectPath ".vscode\settings.json"
New-Item -ItemType Directory -Path (Split-Path -Parent $settingsPath) -Force | Out-Null
$settings = [ordered]@{}
if (Test-Path -LiteralPath $settingsPath) {
$raw = Get-Content -LiteralPath $settingsPath -Encoding UTF8 -Raw
if (-not [string]::IsNullOrWhiteSpace($raw)) {
$existing = $raw | ConvertFrom-Json
foreach ($property in $existing.PSObject.Properties) {
$settings[$property.Name] = $property.Value
}
}
}
$settings["python.defaultInterpreterPath"] = $pythonExe
Write-JsonFile -Path $settingsPath -Object $settings
[pscustomobject]@{ Settings = $settingsPath; Interpreter = $pythonExe } | Format-List
return
}
"verify" {
if (-not $ExpectedVenvName) { throw "python verify requires --ExpectedVenvName." }
$expected = Get-PythonExeForVenv -ProjectPath $ProjectPath -VenvName $ExpectedVenvName
$settingsPath = Join-Path $ProjectPath ".vscode\settings.json"
$probePath = Join-Path $ProjectPath "probe.py"
if (-not (Test-Path -LiteralPath $settingsPath)) { throw "Missing settings.json at $settingsPath." }
if (-not (Test-Path -LiteralPath $probePath)) { throw "Missing probe.py at $probePath." }
$settings = Get-Content -LiteralPath $settingsPath -Encoding UTF8 -Raw | ConvertFrom-Json
$configured = $settings."python.defaultInterpreterPath"
if ($configured -ne $expected) {
throw "Configured interpreter '$configured' does not match expected '$expected'."
}
Push-Location $ProjectPath
try {
$output = & $configured $probePath
} finally {
Pop-Location
}
if ($output -notmatch [regex]::Escape($ExpectedVenvName)) {
throw "Probe output does not contain expected venv '$ExpectedVenvName': $output"
}
[pscustomobject]@{
ProjectPath = $ProjectPath
ExpectedVenv = $ExpectedVenvName
Interpreter = $configured
ProbeOutput = $output
} | Format-List
return
}
default {
throw "Unknown python action '$Action'. Use make-venvs, set-interpreter, or verify."
}
}
}
# === ssh ====================================================================
function ConvertTo-SshRemoteUri {
param([string]$HostAlias, [string]$RemotePath)
if (-not $HostAlias) { throw "SSH URI requires --HostName." }
if (-not $RemotePath) { $RemotePath = "~" }
if ($RemotePath -eq "~") {
return "vscode-remote://ssh-remote+$HostAlias~"
}
if (-not $RemotePath.StartsWith("/")) {
throw "LinuxPath for SSH must be '~' or an absolute remote path such as /home/user/project."
}
return "vscode-remote://ssh-remote+$HostAlias$RemotePath"
}
function Invoke-SshArea {
if (-not $HostName) {
$sshDefaults = Get-ConfigSshDefaults
if ($sshDefaults -and $sshDefaults.PSObject.Properties.Match('defaultHost').Count -and $sshDefaults.defaultHost) {
$HostName = [string]$sshDefaults.defaultHost
} else {
$HostName = "localhost"
}
}
switch ($Action) {
"open" {
$uri = ConvertTo-SshRemoteUri -HostAlias $HostName -RemotePath $LinuxPath
Write-Section "Open $(Get-ProductLabel) SSH remote"
[pscustomobject]@{ Uri = $uri } | Format-List
Invoke-ProductCli --folder-uri $uri
if ($script:LastProductExitCode -ne 0) {
throw "$(Get-ProductLabel) SSH open failed with exit code $script:LastProductExitCode."
}
return
}
"smoke" {
Write-Section "ssh -V"
ssh -V
Write-Section "ssh -G $HostName"
ssh -G $HostName | Select-Object -First 60
Write-Section "Test-NetConnection"
$port = 22
$g = ssh -G $HostName 2>$null
foreach ($line in $g) {
if ($line -match "^port\s+(\d+)") {
$port = [int]$Matches[1]
break
}
}
if (Get-Command Test-NetConnection -ErrorAction SilentlyContinue) {
Test-NetConnection $HostName -Port $port |
Select-Object ComputerName, RemoteAddress, RemotePort, TcpTestSucceeded |
Format-List
} else {
Write-Host "(Test-NetConnection unavailable on this platform; skipping port probe.)"
}
if ($Command) {
Write-Section "ssh command"
ssh $HostName $Command
}
return
}
default {
throw "Unknown ssh action '$Action'. Use open or smoke."
}
}
}
# === wsl ====================================================================
function ConvertTo-WslRemoteUri {
param([string]$DistroName, [string]$RemotePath)
if (-not $DistroName) { throw "WSL URI requires --Distro." }
if (-not $RemotePath) { throw "WSL URI requires --LinuxPath." }
if (-not $RemotePath.StartsWith("/")) {
throw "LinuxPath must be an absolute Linux path such as /home/user/project."
}
return "vscode-remote://wsl+$DistroName$RemotePath"
}
function Invoke-WslCommand {
param(
[Parameter(Mandatory = $true)][string]$DistroName,
[Parameter(Mandatory = $true)][string]$Script
)
$encoded = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($Script))
wsl -d $DistroName -- bash -lc "printf '%s' '$encoded' | base64 -d | bash" 2>&1 |
ForEach-Object { $_ -replace "`0", "" }
$script:LastWslExitCode = $LASTEXITCODE
}
function Show-WslList {
wsl -l -v | ForEach-Object { $_ -replace "`0", "" }
}
function Invoke-WslArea {
switch ($Action) {
"list" {
Write-Section "wsl -l -v"
Show-WslList
return
}
"open" {
if (-not $Distro) {
$wslDefaults = Get-ConfigWslDefaults
if ($wslDefaults -and $wslDefaults.PSObject.Properties.Match('defaultDistro').Count -and $wslDefaults.defaultDistro) {
$Distro = [string]$wslDefaults.defaultDistro
}
}
$uri = ConvertTo-WslRemoteUri -DistroName $Distro -RemotePath $LinuxPath
Write-Section "Open $(Get-ProductLabel) WSL remote"
[pscustomobject]@{ Uri = $uri } | Format-List
Invoke-ProductCli --folder-uri $uri
if ($script:LastProductExitCode -ne 0) {
throw "$(Get-ProductLabel) WSL open failed with exit code $script:LastProductExitCode."
}
return
}
"smoke" {
if (-not $Distro) { throw "wsl smoke requires --Distro." }
if (-not $LinuxPath) { $LinuxPath = "~" }
Write-Section "WSL distro"
Show-WslList
Write-Section "WSL command smoke"
$quotedPath = $LinuxPath.Replace("'", "'\''")
$scriptTemplate = @'
set -e
echo "distro=$WSL_DISTRO_NAME"
echo "user=$(whoami)"
echo "pwd=$(pwd)"
if [ -d '__WSL_PATH__' ]; then
echo "project_exists=true"
cd '__WSL_PATH__'
echo "project_pwd=$(pwd)"
else
echo "project_exists=false"
fi
if command -v python3 >/dev/null 2>&1; then
echo "python3=$(command -v python3)"
else
echo "python3=missing"
fi
'@
$script = $scriptTemplate.Replace("__WSL_PATH__", $quotedPath)
Invoke-WslCommand -DistroName $Distro -Script $script
if ($script:LastWslExitCode -ne 0) {
throw "WSL smoke failed for distro $Distro."
}
return
}
default {
throw "Unknown wsl action '$Action'. Use list, open, or smoke."
}
}
}
# === tasks / launch / extensions =============================================
function Get-SkillRoot {
return (Resolve-Path -LiteralPath (Join-Path $PSScriptRoot "..")).Path
}
function Get-TemplateDir {
param([Parameter(Mandatory = $true)][string]$Kind)
return (Join-Path (Get-SkillRoot) "references/templates/$Kind")
}
function Get-AvailableTemplates {
param([Parameter(Mandatory = $true)][string]$Kind)
$dir = Get-TemplateDir -Kind $Kind
if (-not (Test-Path -LiteralPath $dir)) { return @() }
return @(Get-ChildItem -LiteralPath $dir -Filter "*.json" | ForEach-Object { $_.BaseName } | Sort-Object)
}
function Get-ExtensionPacks {
$packs = [ordered]@{}
$file = Join-Path (Get-SkillRoot) "references/extension-packs.md"
if (-not (Test-Path -LiteralPath $file)) { return $packs }
$currentStack = $null
$lines = Get-Content -LiteralPath $file -Encoding UTF8
foreach ($line in $lines) {
$headerMatch = [regex]::Match($line, '^### `([a-z0-9-]+)`')
if ($headerMatch.Success) {
$currentStack = $headerMatch.Groups[1].Value
$packs[$currentStack] = [System.Collections.Generic.List[object]]::new()
continue
}
if ($null -eq $currentStack) { continue }
if (-not $line.StartsWith("|")) { continue }
$stripped = $line.Trim().Trim('|')
$rawCells = $stripped.Split('|')
$cells = @()
foreach ($c in $rawCells) { $cells += ,$c.Trim().Trim('`') }
if ($cells.Count -lt 2) { continue }
$first = $cells[0]
if ($first -eq "ID" -or $first -eq "---") { continue }
if ($first -match '^[a-zA-Z0-9_-]+\.[a-zA-Z0-9_.-]+$') {
$packs[$currentStack].Add([pscustomobject]@{ id = $first; purpose = $cells[1] })
}
}
return $packs
}
function Invoke-TasksArea {
switch ($Action) {
"list" {
[pscustomobject]@{ templates = (Get-AvailableTemplates -Kind "tasks") } | ConvertTo-Json
return
}
"init" {
if (-not $Template) { throw "tasks init requires --Template." }
if (-not $ProjectPath) { throw "tasks init requires --ProjectPath." }
$resolved = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ProjectPath)
$sourcePath = Join-Path (Get-TemplateDir -Kind "tasks") "$Template.json"
if (-not (Test-Path -LiteralPath $sourcePath)) {
throw "Template '$Template' not found. Available: $((Get-AvailableTemplates -Kind 'tasks') -join ', ')"
}
$targetDir = Join-Path $resolved ".vscode"
$target = Join-Path $targetDir "tasks.json"
if ((Test-Path -LiteralPath $target) -and -not $Force) {
throw "$target already exists. Pass --Force to overwrite."
}
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
Copy-Item -LiteralPath $sourcePath -Destination $target -Force
[pscustomobject]@{ Path = $target; Template = $Template; Written = $true } | ConvertTo-Json
return
}
default { throw "Unknown tasks action '$Action'. Use list or init." }
}
}
function Invoke-LaunchArea {
switch ($Action) {
"list" {
[pscustomobject]@{ templates = (Get-AvailableTemplates -Kind "launch") } | ConvertTo-Json
return
}
"init" {
if (-not $Template) { throw "launch init requires --Template." }
if (-not $ProjectPath) { throw "launch init requires --ProjectPath." }
$resolved = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ProjectPath)
$sourcePath = Join-Path (Get-TemplateDir -Kind "launch") "$Template.json"
if (-not (Test-Path -LiteralPath $sourcePath)) {
throw "Template '$Template' not found. Available: $((Get-AvailableTemplates -Kind 'launch') -join ', ')"
}
$targetDir = Join-Path $resolved ".vscode"
$target = Join-Path $targetDir "launch.json"
if ((Test-Path -LiteralPath $target) -and -not $Force) {
throw "$target already exists. Pass --Force to overwrite."
}
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
Copy-Item -LiteralPath $sourcePath -Destination $target -Force
[pscustomobject]@{ Path = $target; Template = $Template; Written = $true } | ConvertTo-Json
return
}
default { throw "Unknown launch action '$Action'. Use list or init." }
}
}
function Invoke-ExtensionsArea {
switch ($Action) {
"stacks" {
$packs = Get-ExtensionPacks
[pscustomobject]@{ stacks = @($packs.Keys | Sort-Object) } | ConvertTo-Json
return
}
"recommend" {
if (-not $Stack) { throw "extensions recommend requires --Stack." }
$packs = Get-ExtensionPacks
if (-not $packs.Contains($Stack)) {
throw "Unknown stack '$Stack'. Available: $((@($packs.Keys) | Sort-Object) -join ', ')"
}
[pscustomobject]@{ stack = $Stack; extensions = $packs[$Stack] } | ConvertTo-Json -Depth 5
return
}
"install" {
if (-not $Stack) { throw "extensions install requires --Stack." }
$packs = Get-ExtensionPacks
if (-not $packs.Contains($Stack)) {
throw "Unknown stack '$Stack'. Available: $((@($packs.Keys) | Sort-Object) -join ', ')"
}
$cli = Resolve-ProductCli
$failures = @()
foreach ($entry in $packs[$Stack]) {
Write-Host "installing $($entry.id)..." -ForegroundColor DarkGray
& $cli --install-extension $entry.id
if ($LASTEXITCODE -ne 0) { $failures += $entry }
}
[pscustomobject]@{
stack = $Stack
total = $packs[$Stack].Count
failures = $failures
} | ConvertTo-Json -Depth 5
if ($failures.Count -gt 0) { exit 1 }
return
}
"pin" {
if (-not $Stack) { throw "extensions pin requires --Stack." }
if (-not $ProjectPath) { throw "extensions pin requires --ProjectPath." }
$packs = Get-ExtensionPacks
if (-not $packs.Contains($Stack)) {
throw "Unknown stack '$Stack'. Available: $((@($packs.Keys) | Sort-Object) -join ', ')"
}
$resolved = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ProjectPath)
$targetDir = Join-Path $resolved ".vscode"
$target = Join-Path $targetDir "extensions.json"
$existing = [ordered]@{}
if (Test-Path -LiteralPath $target) {
$raw = Get-Content -LiteralPath $target -Encoding UTF8 -Raw
if (-not [string]::IsNullOrWhiteSpace($raw)) {
try { $existing = Get-JsonObjectAsOrderedMap -Object ($raw | ConvertFrom-Json) } catch { $existing = [ordered]@{} }
}
}
$recs = @()
if ($existing.Contains("recommendations")) {
$recs = @($existing["recommendations"])
}
foreach ($entry in $packs[$Stack]) {
if ($entry.id -notin $recs) { $recs += $entry.id }
}
$existing["recommendations"] = $recs
New-Item -ItemType Directory -Path $targetDir -Force | Out-Null
Write-JsonFile -Path $target -Object $existing
[pscustomobject]@{ Path = $target; Stack = $Stack; Recommendations = $recs } | ConvertTo-Json -Depth 5
return
}
default { throw "Unknown extensions action '$Action'. Use stacks, recommend, install, or pin." }
}
}
function Invoke-SettingsSyncArea {
switch ($Action) {
"status" {
$paths = @()
$candidates = @(
(Join-Path (Get-ProductUserDataPath) "sync"),
(Join-Path (Join-Path $env:APPDATA "Code - Insiders\User") "sync")
)
foreach ($p in $candidates) {
if (Test-Path -LiteralPath $p) { $paths += $p }
}
[pscustomobject]@{
syncStorageFound = ($paths.Count -gt 0)
locations = $paths
note = "Settings sync is controlled via the VS Code UI (Accounts -> Backup and Sync Settings). The skill does not enable or disable sync programmatically to avoid accidental data leaks."
} | ConvertTo-Json
return
}
default { throw "Unknown settings-sync action '$Action'. Use status." }
}
}
function Invoke-WorkspaceArea {
switch ($Action) {
"info" {
if (-not $ProjectPath) { throw "workspace info requires --ProjectPath." }
$resolved = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ProjectPath)
$wsFile = Get-ChildItem -LiteralPath $resolved -Filter "*.code-workspace" -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $wsFile) {
[pscustomobject]@{
ProjectPath = $resolved
IsMultiRoot = $false
note = "No .code-workspace file found. This is a single-root workspace; use 'settings set --Scope project'."
} | ConvertTo-Json
return
}
$data = Get-Content -LiteralPath $wsFile.FullName -Encoding UTF8 -Raw | ConvertFrom-Json
$folders = @($data.folders)
[pscustomobject]@{
ProjectPath = $resolved
IsMultiRoot = ($folders.Count -gt 1)
WorkspaceFile = $wsFile.FullName
Folders = @($folders | ForEach-Object { $_.path })
FolderSettings = @($folders | Where-Object { $_.settings } | ForEach-Object {
[pscustomobject]@{ path = $_.path; settings = $_.settings }
})
} | ConvertTo-Json -Depth 10
return
}
"folder-settings-get" {
if (-not $ProjectPath -or -not $Folder) { throw "workspace folder-settings-get requires --ProjectPath and --Folder." }
$resolved = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ProjectPath)
$wsFile = Get-ChildItem -LiteralPath $resolved -Filter "*.code-workspace" -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $wsFile) { throw "Not a multi-root workspace (no .code-workspace file)." }
$data = Get-Content -LiteralPath $wsFile.FullName -Encoding UTF8 -Raw | ConvertFrom-Json
$entry = @($data.folders) | Where-Object { $_.path -eq $Folder } | Select-Object -First 1
if (-not $entry) { throw "Folder '$Folder' not found in workspace file." }
$folderSettings = if ($entry.settings) { $entry.settings } else { @{} }
if ($SettingKey) {
[pscustomobject]@{ key = $SettingKey; value = $folderSettings.$SettingKey } | ConvertTo-Json
} else {
[pscustomobject]@{ folder = $Folder; settings = $folderSettings } | ConvertTo-Json
}
return
}
"folder-settings-set" {
if (-not $ProjectPath -or -not $Folder -or -not $SettingKey) {
throw "workspace folder-settings-set requires --ProjectPath, --Folder, --SettingKey."
}
if (-not $Apply) { throw "workspace folder-settings-set is a write operation. Pass --Apply." }
$resolved = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($ProjectPath)
$wsFile = Get-ChildItem -LiteralPath $resolved -Filter "*.code-workspace" -ErrorAction SilentlyContinue | Select-Object -First 1
if (-not $wsFile) { throw "Not a multi-root workspace (no .code-workspace file)." }
$data = Get-Content -LiteralPath $wsFile.FullName -Encoding UTF8 -Raw | ConvertFrom-Json
$found = $false
foreach ($f in @($data.folders)) {
if ($f.path -eq $Folder) {
if (-not $f.settings) {
$f | Add-Member -NotePropertyName "settings" -NotePropertyValue ([ordered]@{}) -Force
}
$coerced = Convert-SettingValue $SettingValue
$f.settings.$SettingKey = $coerced
$found = $true
break
}
}
if (-not $found) { throw "Folder '$Folder' not found in workspace file." }
$data | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $wsFile.FullName -Encoding UTF8
[pscustomobject]@{
WorkspaceFile = $wsFile.FullName
Folder = $Folder
Key = $SettingKey
Value = $coerced
} | ConvertTo-Json
return
}
default { throw "Unknown workspace action '$Action'. Use info, folder-settings-get, or folder-settings-set." }
}
}
function Invoke-ProfileArea {
switch ($Action) {
"list" {
$profilesDir = Join-Path (Get-ProductUserDataPath) "profiles"
if (-not (Test-Path -LiteralPath $profilesDir)) {
[pscustomobject]@{ profiles = @(); note = "No profiles directory found; no profiles defined." } | ConvertTo-Json
return
}
$entries = @()
foreach ($child in (Get-ChildItem -LiteralPath $profilesDir -Directory | Sort-Object Name)) {
$entry = [ordered]@{
name = $child.Name
settingsFileExists = (Test-Path -LiteralPath (Join-Path $child.FullName "settings.json"))
}
$settingsPath = Join-Path $child.FullName "settings.json"
if (Test-Path -LiteralPath $settingsPath) {
try {
$settings = Get-Content -LiteralPath $settingsPath -Encoding UTF8 -Raw | ConvertFrom-Json
$entry["settingCount"] = @($settings.PSObject.Properties).Count
$entry["sampleKeys"] = @($settings.PSObject.Properties | Select-Object -First 5 -ExpandProperty Name)
} catch {
$entry["parseError"] = "settings.json is not valid JSON"
}
}
$entries += [pscustomobject]$entry
}
[pscustomobject]@{ profilesDir = $profilesDir; profiles = $entries } | ConvertTo-Json
return
}
"show" {
if (-not $Profile) { throw "profile show requires --Profile." }
$profilesDir = Join-Path (Get-ProductUserDataPath) "profiles"
$settingsPath = Join-Path (Join-Path $profilesDir $Profile) "settings.json"
if (-not (Test-Path -LiteralPath $settingsPath)) {
throw "Profile '$Profile' not found or has no settings.json."
}
$settings = Get-Content -LiteralPath $settingsPath -Encoding UTF8 -Raw | ConvertFrom-Json
[pscustomobject]@{
profile = $Profile
settingsFile = $settingsPath
settings = $settings
} | ConvertTo-Json -Depth 10
return
}
default { throw "Unknown profile action '$Action'. Use list or show." }
}
}
# === dispatch ===============================================================
switch ($Area) {
"info" { Invoke-Info }
"ext" { Invoke-Ext }
"python" { Invoke-PythonArea }
"ssh" { Invoke-SshArea }
"wsl" { Invoke-WslArea }
"settings" { Invoke-SettingsArea }
"tasks" { Invoke-TasksArea }
"launch" { Invoke-LaunchArea }
"extensions" { Invoke-ExtensionsArea }
"settings-sync" { Invoke-SettingsSyncArea }
"workspace" { Invoke-WorkspaceArea }
"profile" { Invoke-ProfileArea }
default { throw "Unknown area '$Area'. Use info, ext, python, ssh, wsl, settings, tasks, launch, extensions, settings-sync, workspace, or profile." }
}