diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
new file mode 100644
index 0000000..36bd853
--- /dev/null
+++ b/.github/FUNDING.yml
@@ -0,0 +1 @@
+github: [StartAutomating]
diff --git a/.github/workflows/Build.yml b/.github/workflows/Build.yml
new file mode 100644
index 0000000..5809b2a
--- /dev/null
+++ b/.github/workflows/Build.yml
@@ -0,0 +1,512 @@
+
+name: Build Module
+on:
+ push:
+ pull_request:
+ workflow_dispatch:
+jobs:
+ TestPowerShellOnLinux:
+ runs-on: ubuntu-latest
+ steps:
+ - name: InstallPester
+ id: InstallPester
+ shell: pwsh
+ run: |
+ $Parameters = @{}
+ $Parameters.PesterMaxVersion = ${env:PesterMaxVersion}
+ foreach ($k in @($parameters.Keys)) {
+ if ([String]::IsNullOrEmpty($parameters[$k])) {
+ $parameters.Remove($k)
+ }
+ }
+ Write-Host "::debug:: InstallPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')"
+ & {<#
+ .Synopsis
+ Installs Pester
+ .Description
+ Installs Pester
+ #>
+ param(
+ # The maximum pester version. Defaults to 4.99.99.
+ [string]
+ $PesterMaxVersion = '4.99.99'
+ )
+ [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
+ Install-Module -Name Pester -Repository PSGallery -Force -Scope CurrentUser -MaximumVersion $PesterMaxVersion -SkipPublisherCheck -AllowClobber
+ Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion} @Parameters
+ - name: Check out repository
+ uses: actions/checkout@v4
+ - name: RunPester
+ id: RunPester
+ shell: pwsh
+ run: |
+ $Parameters = @{}
+ $Parameters.ModulePath = ${env:ModulePath}
+ $Parameters.PesterMaxVersion = ${env:PesterMaxVersion}
+ $Parameters.NoCoverage = ${env:NoCoverage}
+ $Parameters.NoCoverage = $parameters.NoCoverage -match 'true';
+ foreach ($k in @($parameters.Keys)) {
+ if ([String]::IsNullOrEmpty($parameters[$k])) {
+ $parameters.Remove($k)
+ }
+ }
+ Write-Host "::debug:: RunPester $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')"
+ & {<#
+ .Synopsis
+ Runs Pester
+ .Description
+ Runs Pester tests after importing a PowerShell module
+ #>
+ param(
+ # The module path. If not provided, will default to the second half of the repository ID.
+ [string]
+ $ModulePath,
+ # The Pester max version. By default, this is pinned to 4.99.99.
+ [string]
+ $PesterMaxVersion = '4.99.99',
+
+ # If set, will not collect code coverage.
+ [switch]
+ $NoCoverage
+ )
+
+ $global:ErrorActionPreference = 'continue'
+ $global:ProgressPreference = 'silentlycontinue'
+
+ $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/"
+ if (-not $ModulePath) { $ModulePath = ".\$moduleName.psd1" }
+ $importedPester = Import-Module Pester -Force -PassThru -MaximumVersion $PesterMaxVersion
+ $importedModule = Import-Module $ModulePath -Force -PassThru
+ $importedPester, $importedModule | Out-Host
+
+ $codeCoverageParameters = @{
+ CodeCoverage = "$($importedModule | Split-Path)\*-*.ps1"
+ CodeCoverageOutputFile = ".\$moduleName.Coverage.xml"
+ }
+
+ if ($NoCoverage) {
+ $codeCoverageParameters = @{}
+ }
+
+
+ $result =
+ Invoke-Pester -PassThru -Verbose -OutputFile ".\$moduleName.TestResults.xml" -OutputFormat NUnitXml @codeCoverageParameters
+
+ if ($result.FailedCount -gt 0) {
+ "::debug:: $($result.FailedCount) tests failed"
+ foreach ($r in $result.TestResult) {
+ if (-not $r.Passed) {
+ "::error::$($r.describe, $r.context, $r.name -join ' ') $($r.FailureMessage)"
+ }
+ }
+ throw "::error:: $($result.FailedCount) tests failed"
+ }
+ } @Parameters
+ - name: PublishTestResults
+ uses: actions/upload-artifact@main
+ with:
+ name: PesterResults
+ path: '**.TestResults.xml'
+ if: ${{always()}}
+ TagReleaseAndPublish:
+ runs-on: ubuntu-latest
+ if: ${{ success() }}
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v2
+ - name: TagModuleVersion
+ id: TagModuleVersion
+ shell: pwsh
+ run: |
+ $Parameters = @{}
+ $Parameters.ModulePath = ${env:ModulePath}
+ $Parameters.UserEmail = ${env:UserEmail}
+ $Parameters.UserName = ${env:UserName}
+ $Parameters.TagVersionFormat = ${env:TagVersionFormat}
+ $Parameters.TagAnnotationFormat = ${env:TagAnnotationFormat}
+ foreach ($k in @($parameters.Keys)) {
+ if ([String]::IsNullOrEmpty($parameters[$k])) {
+ $parameters.Remove($k)
+ }
+ }
+ Write-Host "::debug:: TagModuleVersion $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')"
+ & {param(
+ [string]
+ $ModulePath,
+
+ # The user email associated with a git commit.
+ [string]
+ $UserEmail,
+
+ # The user name associated with a git commit.
+ [string]
+ $UserName,
+
+ # The tag version format (default value: 'v$(imported.Version)')
+ # This can expand variables. $imported will contain the imported module.
+ [string]
+ $TagVersionFormat = 'v$($imported.Version)',
+
+ # The tag version format (default value: '$($imported.Name) $(imported.Version)')
+ # This can expand variables. $imported will contain the imported module.
+ [string]
+ $TagAnnotationFormat = '$($imported.Name) $($imported.Version)'
+ )
+
+
+ $gitHubEvent = if ($env:GITHUB_EVENT_PATH) {
+ [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json
+ } else { $null }
+
+
+ @"
+ ::group::GitHubEvent
+ $($gitHubEvent | ConvertTo-Json -Depth 100)
+ ::endgroup::
+ "@ | Out-Host
+
+ if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and
+ (-not $gitHubEvent.psobject.properties['inputs'])) {
+ "::warning::Pull Request has not merged, skipping Tagging" | Out-Host
+ return
+ }
+
+
+
+ $imported =
+ if (-not $ModulePath) {
+ $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/"
+ Import-Module ".\$moduleName.psd1" -Force -PassThru -Global
+ } else {
+ Import-Module $modulePath -Force -PassThru -Global
+ }
+
+ if (-not $imported) { return }
+
+ $targetVersion =$ExecutionContext.InvokeCommand.ExpandString($TagVersionFormat)
+ $existingTags = git tag --list
+
+ @"
+ Target Version: $targetVersion
+
+ Existing Tags:
+ $($existingTags -join [Environment]::NewLine)
+ "@ | Out-Host
+
+ $versionTagExists = $existingTags | Where-Object { $_ -match $targetVersion }
+
+ if ($versionTagExists) {
+ "::warning::Version $($versionTagExists)"
+ return
+ }
+
+ if (-not $UserName) { $UserName = $env:GITHUB_ACTOR }
+ if (-not $UserEmail) { $UserEmail = "$UserName@github.com" }
+ git config --global user.email $UserEmail
+ git config --global user.name $UserName
+
+ git tag -a $targetVersion -m $ExecutionContext.InvokeCommand.ExpandString($TagAnnotationFormat)
+ git push origin --tags
+
+ if ($env:GITHUB_ACTOR) {
+ exit 0
+ }} @Parameters
+ - name: ReleaseModule
+ id: ReleaseModule
+ shell: pwsh
+ run: |
+ $Parameters = @{}
+ $Parameters.ModulePath = ${env:ModulePath}
+ $Parameters.UserEmail = ${env:UserEmail}
+ $Parameters.UserName = ${env:UserName}
+ $Parameters.TagVersionFormat = ${env:TagVersionFormat}
+ $Parameters.ReleaseNameFormat = ${env:ReleaseNameFormat}
+ $Parameters.ReleaseAsset = ${env:ReleaseAsset}
+ $Parameters.ReleaseAsset = $parameters.ReleaseAsset -split ';' -replace '^[''"]' -replace '[''"]$'
+ foreach ($k in @($parameters.Keys)) {
+ if ([String]::IsNullOrEmpty($parameters[$k])) {
+ $parameters.Remove($k)
+ }
+ }
+ Write-Host "::debug:: ReleaseModule $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')"
+ & {param(
+ [string]
+ $ModulePath,
+
+ # The user email associated with a git commit.
+ [string]
+ $UserEmail,
+
+ # The user name associated with a git commit.
+ [string]
+ $UserName,
+
+ # The tag version format (default value: 'v$(imported.Version)')
+ # This can expand variables. $imported will contain the imported module.
+ [string]
+ $TagVersionFormat = 'v$($imported.Version)',
+
+ # The release name format (default value: '$($imported.Name) $($imported.Version)')
+ [string]
+ $ReleaseNameFormat = '$($imported.Name) $($imported.Version)',
+
+ # Any assets to attach to the release. Can be a wildcard or file name.
+ [string[]]
+ $ReleaseAsset
+ )
+
+
+ $gitHubEvent = if ($env:GITHUB_EVENT_PATH) {
+ [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json
+ } else { $null }
+
+
+ @"
+ ::group::GitHubEvent
+ $($gitHubEvent | ConvertTo-Json -Depth 100)
+ ::endgroup::
+ "@ | Out-Host
+
+ if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and
+ (-not $gitHubEvent.psobject.properties['inputs'])) {
+ "::warning::Pull Request has not merged, skipping GitHub release" | Out-Host
+ return
+ }
+
+
+
+ $imported =
+ if (-not $ModulePath) {
+ $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/"
+ Import-Module ".\$moduleName.psd1" -Force -PassThru -Global
+ } else {
+ Import-Module $modulePath -Force -PassThru -Global
+ }
+
+ if (-not $imported) { return }
+
+ $targetVersion =$ExecutionContext.InvokeCommand.ExpandString($TagVersionFormat)
+ $targetReleaseName = $targetVersion
+ $releasesURL = 'https://api.github.com/repos/${{github.repository}}/releases'
+ "Release URL: $releasesURL" | Out-Host
+ $listOfReleases = Invoke-RestMethod -Uri $releasesURL -Method Get -Headers @{
+ "Accept" = "application/vnd.github.v3+json"
+ "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}'
+ }
+
+ $releaseExists = $listOfReleases | Where-Object tag_name -eq $targetVersion
+
+ if ($releaseExists) {
+ "::warning::Release '$($releaseExists.Name )' Already Exists" | Out-Host
+ $releasedIt = $releaseExists
+ } else {
+ $releasedIt = Invoke-RestMethod -Uri $releasesURL -Method Post -Body (
+ [Ordered]@{
+ owner = '${{github.owner}}'
+ repo = '${{github.repository}}'
+ tag_name = $targetVersion
+ name = $ExecutionContext.InvokeCommand.ExpandString($ReleaseNameFormat)
+ body =
+ if ($env:RELEASENOTES) {
+ $env:RELEASENOTES
+ } elseif ($imported.PrivateData.PSData.ReleaseNotes) {
+ $imported.PrivateData.PSData.ReleaseNotes
+ } else {
+ "$($imported.Name) $targetVersion"
+ }
+ draft = if ($env:RELEASEISDRAFT) { [bool]::Parse($env:RELEASEISDRAFT) } else { $false }
+ prerelease = if ($env:PRERELEASE) { [bool]::Parse($env:PRERELEASE) } else { $false }
+ } | ConvertTo-Json
+ ) -Headers @{
+ "Accept" = "application/vnd.github.v3+json"
+ "Content-type" = "application/json"
+ "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}'
+ }
+ }
+
+
+
+
+
+ if (-not $releasedIt) {
+ throw "Release failed"
+ } else {
+ $releasedIt | Out-Host
+ }
+
+ $releaseUploadUrl = $releasedIt.upload_url -replace '\{.+$'
+
+ if ($ReleaseAsset) {
+ $fileList = Get-ChildItem -Recurse
+ $filesToRelease =
+ @(:nextFile foreach ($file in $fileList) {
+ foreach ($relAsset in $ReleaseAsset) {
+ if ($relAsset -match '[\*\?]') {
+ if ($file.Name -like $relAsset) {
+ $file; continue nextFile
+ }
+ } elseif ($file.Name -eq $relAsset -or $file.FullName -eq $relAsset) {
+ $file; continue nextFile
+ }
+ }
+ })
+
+ $releasedFiles = @{}
+ foreach ($file in $filesToRelease) {
+ if ($releasedFiles[$file.Name]) {
+ Write-Warning "Already attached file $($file.Name)"
+ continue
+ } else {
+ $fileBytes = [IO.File]::ReadAllBytes($file.FullName)
+ $releasedFiles[$file.Name] =
+ Invoke-RestMethod -Uri "${releaseUploadUrl}?name=$($file.Name)" -Headers @{
+ "Accept" = "application/vnd.github+json"
+ "Authorization" = 'Bearer ${{ secrets.GITHUB_TOKEN }}'
+ } -Body $fileBytes -ContentType Application/octet-stream
+ $releasedFiles[$file.Name]
+ }
+ }
+
+ "Attached $($releasedFiles.Count) file(s) to release" | Out-Host
+ }
+
+
+
+ } @Parameters
+ - name: PublishPowerShellGallery
+ id: PublishPowerShellGallery
+ shell: pwsh
+ run: |
+ $Parameters = @{}
+ $Parameters.ModulePath = ${env:ModulePath}
+ $Parameters.Exclude = ${env:Exclude}
+ $Parameters.Exclude = $parameters.Exclude -split ';' -replace '^[''"]' -replace '[''"]$'
+ foreach ($k in @($parameters.Keys)) {
+ if ([String]::IsNullOrEmpty($parameters[$k])) {
+ $parameters.Remove($k)
+ }
+ }
+ Write-Host "::debug:: PublishPowerShellGallery $(@(foreach ($p in $Parameters.GetEnumerator()) {'-' + $p.Key + ' ' + $p.Value}) -join ' ')"
+ & {param(
+ [string]
+ $ModulePath,
+
+ [string[]]
+ $Exclude = @('*.png', '*.mp4', '*.jpg','*.jpeg', '*.gif', 'docs[/\]*')
+ )
+
+ $gitHubEvent = if ($env:GITHUB_EVENT_PATH) {
+ [IO.File]::ReadAllText($env:GITHUB_EVENT_PATH) | ConvertFrom-Json
+ } else { $null }
+
+ if (-not $Exclude) {
+ $Exclude = @('*.png', '*.mp4', '*.jpg','*.jpeg', '*.gif','docs[/\]*')
+ }
+
+
+ @"
+ ::group::GitHubEvent
+ $($gitHubEvent | ConvertTo-Json -Depth 100)
+ ::endgroup::
+ "@ | Out-Host
+
+ @"
+ ::group::PSBoundParameters
+ $($PSBoundParameters | ConvertTo-Json -Depth 100)
+ ::endgroup::
+ "@ | Out-Host
+
+ if (-not ($gitHubEvent.head_commit.message -match "Merge Pull Request #(?\d+)") -and
+ (-not $gitHubEvent.psobject.properties['inputs'])) {
+ "::warning::Pull Request has not merged, skipping Gallery Publish" | Out-Host
+ return
+ }
+
+
+ $imported =
+ if (-not $ModulePath) {
+ $orgName, $moduleName = $env:GITHUB_REPOSITORY -split "/"
+ Import-Module ".\$moduleName.psd1" -Force -PassThru -Global
+ } else {
+ Import-Module $modulePath -Force -PassThru -Global
+ }
+
+ if (-not $imported) { return }
+
+ $foundModule = try { Find-Module -Name $imported.Name -ErrorAction SilentlyContinue} catch {}
+
+ if ($foundModule -and (([Version]$foundModule.Version) -ge ([Version]$imported.Version))) {
+ "::warning::Gallery Version of $moduleName is more recent ($($foundModule.Version) >= $($imported.Version))" | Out-Host
+ } else {
+
+ $gk = '${{secrets.GALLERYKEY}}'
+
+ $rn = Get-Random
+ $moduleTempFolder = Join-Path $pwd "$rn"
+ $moduleTempPath = Join-Path $moduleTempFolder $moduleName
+ New-Item -ItemType Directory -Path $moduleTempPath -Force | Out-Host
+
+ Write-Host "Staging Directory: $ModuleTempPath"
+
+ $imported | Split-Path |
+ Get-ChildItem -Force |
+ Where-Object Name -NE $rn |
+ Copy-Item -Destination $moduleTempPath -Recurse
+
+ $moduleGitPath = Join-Path $moduleTempPath '.git'
+ Write-Host "Removing .git directory"
+ if (Test-Path $moduleGitPath) {
+ Remove-Item -Recurse -Force $moduleGitPath
+ }
+
+ if ($Exclude) {
+ "::notice::Attempting to Exlcude $exclude" | Out-Host
+ Get-ChildItem $moduleTempPath -Recurse |
+ Where-Object {
+ foreach ($ex in $exclude) {
+ if ($_.FullName -like $ex) {
+ "::notice::Excluding $($_.FullName)" | Out-Host
+ return $true
+ }
+ }
+ } |
+ Remove-Item
+ }
+
+ Write-Host "Module Files:"
+ Get-ChildItem $moduleTempPath -Recurse
+ Write-Host "Publishing $moduleName [$($imported.Version)] to Gallery"
+ Publish-Module -Path $moduleTempPath -NuGetApiKey $gk
+ if ($?) {
+ Write-Host "Published to Gallery"
+ } else {
+ Write-Host "Gallery Publish Failed"
+ exit 1
+ }
+ }
+ } @Parameters
+ BuildNeocities:
+ runs-on: ubuntu-latest
+ if: ${{ success() }}
+ steps:
+ - name: Check out repository
+ uses: actions/checkout@v2
+ - name: GitLogger
+ uses: GitLogging/GitLoggerAction@main
+ id: GitLogger
+ - name: Use PSSVG Action
+ uses: StartAutomating/PSSVG@main
+ id: PSSVG
+ - name: Use PipeScript Action
+ uses: StartAutomating/PipeScript@main
+ id: PipeScript
+ - name: UseEZOut
+ uses: StartAutomating/EZOut@master
+ - name: UseHelpOut
+ uses: StartAutomating/HelpOut@master
+ - name: Use PSJekyll Action
+ uses: PowerShellWeb/PSJekyll@main
+ id: PSJekyll
+env:
+ REGISTRY: ghcr.io
+ IMAGE_NAME: ${{ github.repository }}
diff --git a/Build/GitHub/Jobs/BuildNeocities.psd1 b/Build/GitHub/Jobs/BuildNeocities.psd1
new file mode 100644
index 0000000..8eaba5a
--- /dev/null
+++ b/Build/GitHub/Jobs/BuildNeocities.psd1
@@ -0,0 +1,39 @@
+@{
+ "runs-on" = "ubuntu-latest"
+ if = '${{ success() }}'
+ steps = @(
+ @{
+ name = 'Check out repository'
+ uses = 'actions/checkout@v2'
+ },
+ @{
+ name = 'GitLogger'
+ uses = 'GitLogging/GitLoggerAction@main'
+ id = 'GitLogger'
+ },
+ @{
+ name = 'Use PSSVG Action'
+ uses = 'StartAutomating/PSSVG@main'
+ id = 'PSSVG'
+ },
+ @{
+ name = 'Use PipeScript Action'
+ uses = 'StartAutomating/PipeScript@main'
+ id = 'PipeScript'
+ },
+ 'RunEZOut',
+ 'RunHelpOut',
+ @{
+ name = 'Use PSJekyll Action'
+ uses = 'PowerShellWeb/PSJekyll@main'
+ id = 'PSJekyll'
+ }
+ <#@{
+ name = 'Run WebSocket (on branch)'
+ if = '${{github.ref_name != ''main''}}'
+ uses = './'
+ id = 'WebSocketAction'
+ },#>
+ # 'BuildAndPublishContainer'
+ )
+}
\ No newline at end of file
diff --git a/Build/GitHub/Steps/PublishTestResults.psd1 b/Build/GitHub/Steps/PublishTestResults.psd1
new file mode 100644
index 0000000..e8111e8
--- /dev/null
+++ b/Build/GitHub/Steps/PublishTestResults.psd1
@@ -0,0 +1,10 @@
+@{
+ name = 'PublishTestResults'
+ uses = 'actions/upload-artifact@main'
+ with = @{
+ name = 'PesterResults'
+ path = '**.TestResults.xml'
+ }
+ if = '${{always()}}'
+}
+
diff --git a/Build/Neocities.GitHubWorkflow.PSDevOps.ps1 b/Build/Neocities.GitHubWorkflow.PSDevOps.ps1
new file mode 100644
index 0000000..8e4c551
--- /dev/null
+++ b/Build/Neocities.GitHubWorkflow.PSDevOps.ps1
@@ -0,0 +1,15 @@
+#requires -Module PSDevOps
+Import-BuildStep -SourcePath (
+ Join-Path $PSScriptRoot 'GitHub'
+) -BuildSystem GitHubWorkflow
+
+Push-Location ($PSScriptRoot | Split-Path)
+
+New-GitHubWorkflow -Name "Build Module" -On Push,
+ PullRequest, Demand -Job TestPowerShellOnLinux,
+ TagReleaseAndPublish, BuildNeocities -Environment ([Ordered]@{
+ REGISTRY = 'ghcr.io'
+ IMAGE_NAME = '${{ github.repository }}'
+ }) -OutputPath .\.github\workflows\Build.yml
+
+Pop-Location
\ No newline at end of file
diff --git a/Build/Neocities.HelpOut.ps1 b/Build/Neocities.HelpOut.ps1
new file mode 100644
index 0000000..ae37d2e
--- /dev/null
+++ b/Build/Neocities.HelpOut.ps1
@@ -0,0 +1,14 @@
+#requires -Module HelpOut
+
+#region Load the Module
+$ModuleName = 'Neocities'
+Push-Location ($PSScriptRoot | Split-Path)
+if (-not (Get-Module $ModuleName)) {
+ Import-Module .\ -Global -PassThru | Out-Host
+}
+#endregion Load the Module
+
+# This will save the MarkdownHelp to the docs folder, and output all of the files created.
+Save-MarkdownHelp -PassThru -Module $ModuleName -ExcludeCommandType Alias
+
+Pop-Location
\ No newline at end of file
diff --git a/Build/Neocities.ezout.ps1 b/Build/Neocities.ezout.ps1
new file mode 100644
index 0000000..480ccb0
--- /dev/null
+++ b/Build/Neocities.ezout.ps1
@@ -0,0 +1,39 @@
+#requires -Module EZOut
+# Install-Module EZOut or https://github.com/StartAutomating/EZOut
+$myFile = $MyInvocation.MyCommand.ScriptBlock.File
+$myRoot = $myFile | Split-Path | Split-Path
+$myModuleName = $myFile | Split-Path | Split-Path | Split-Path -Leaf
+Push-Location $myRoot
+$formatting = @(
+ # Add your own Write-FormatView here,
+ # or put them in a Formatting or Views directory
+ foreach ($potentialDirectory in 'Formatting','Views','Types') {
+ Join-Path $myRoot $potentialDirectory |
+ Get-ChildItem -ea ignore |
+ Import-FormatView -FilePath {$_.Fullname}
+ }
+)
+
+$destinationRoot = $myRoot
+
+if ($formatting) {
+ $myFormatFilePath = Join-Path $destinationRoot "$myModuleName.format.ps1xml"
+ # You can also output to multiple paths by passing a hashtable to -OutputPath.
+ $formatting | Out-FormatData -Module $MyModuleName -OutputPath $myFormatFilePath
+}
+
+$types = @(
+ # Add your own Write-TypeView statements here
+ # or declare them in the 'Types' directory
+ Join-Path $myRoot Types |
+ Get-Item -ea ignore |
+ Import-TypeView
+
+)
+
+if ($types) {
+ $myTypesFilePath = Join-Path $destinationRoot "$myModuleName.types.ps1xml"
+ # You can also output to multiple paths by passing a hashtable to -OutputPath.
+ $types | Out-TypeData -OutputPath $myTypesFilePath
+}
+Pop-Location
diff --git a/Commands/Connect-Neocities.ps1 b/Commands/Connect-Neocities.ps1
new file mode 100644
index 0000000..4b3d528
--- /dev/null
+++ b/Commands/Connect-Neocities.ps1
@@ -0,0 +1,33 @@
+function Connect-Neocities
+{
+ <#
+ .SYNOPSIS
+ Connect to Neocities
+ .DESCRIPTION
+ Connect to Neocities using a credential object.
+
+ This will create a session that can be used to authenticate to the Neocities API.
+ .LINK
+ Get-Neocities
+ #>
+ param(
+ # The Neocities credential
+ [Parameter(ValueFromPipelineByPropertyName)]
+ [Alias(
+ 'Credentials', # Plural aliases are nice
+ 'PSCredential', # so are parameters that match the type name.
+ 'NeocitiesCredential', # A contextual alias is a good idea, too.
+ 'NeocitiesCredentials' # And you may need to pluralize that contextual alias.
+ )]
+ [PSCredential]
+ $Credential
+ )
+
+ begin {
+ $NeocitiesApi = "https://neocities.org/api"
+ }
+
+ process {
+ Invoke-RestMethod -Uri ($NeocitiesApi,'key' -join '/') -Credential $Credential -Authentication Basic
+ }
+}
diff --git a/Commands/Get-Neocities.ps1 b/Commands/Get-Neocities.ps1
new file mode 100644
index 0000000..1c43cb9
--- /dev/null
+++ b/Commands/Get-Neocities.ps1
@@ -0,0 +1,122 @@
+function Get-Neocities
+{
+ <#
+ .SYNOPSIS
+ Gets neocities information
+ .DESCRIPTION
+ Gets neocities information from the neocities API, or lists the files in your neocities site.
+ .EXAMPLE
+ Get-Neocities
+ .EXAMPLE
+ Get-Neocities -Credential $neocitiesCredential
+ .EXAMPLE
+ Get-Neocities -List
+ #>
+ [Alias('neocities')]
+ [CmdletBinding(DefaultParameterSetName='info')]
+ param(
+ # If set, will list the files in your neocities site
+ [Parameter(Mandatory,ValueFromPipelineByPropertyName,ParameterSetName='list')]
+ [switch]
+ $List,
+
+ # The credential used to connect.
+ # This only needs to be provided once per module session
+ # (every time the module is imported)
+ [Parameter(ValueFromPipelineByPropertyName)]
+ [Alias(
+ 'Credentials', # Plural aliases are nice
+ 'PSCredential', # so are parameters that match the type name.
+ 'NeocitiesCredential', # A contextual alias is a good idea, too.
+ 'NeocitiesCredentials' # And you may need to pluralize that contextual alias.
+ )]
+ [PSCredential]
+ $Credential,
+
+ # The access token used to connect.
+ # This only needs to be provided once per module session
+ # (every time the module is imported)
+ [Parameter(ValueFromPipelineByPropertyName)]
+ [string]
+ $AccessToken
+ )
+
+ begin {
+ $NeocitiesApi = "https://neocities.org/api"
+ }
+
+ process {
+ # The parameter set name contains the route
+ $parameterSet = $PSCmdlet.ParameterSetName
+ # and we want to use this to decorate all returned values with a type name
+ $psuedoNamespace = "neocities"
+ $pseudoType = "$parameterSet"
+ # Start by constructing the parameters for Invoke-RestMethod
+ $InvokeSplat = [Ordered]@{
+ Uri = "$NeocitiesApi", $PSCmdlet.ParameterSetName -join '/'
+ }
+
+ # If an access token was provided
+ if ($AccessToken)
+ {
+ # use it
+ $InvokeSplat.Headers = @{Authorization = "Bearer $AccessToken"}
+ # and cache it for later use
+ $script:NeocitiesAccessToken = $AccessToken
+ }
+ elseif ($Credential)
+ {
+ # If a credential was provided, use it
+ $InvokeSplat.Credential = $Credential
+ # and cache it for later use
+ $script:NeoCitiesCredential = $Credential
+ # (don't forget to set authentication to basic)
+ $InvokeSplat.Authentication = 'Basic'
+ }
+ elseif ($script:NeocitiesAccessToken) {
+ # If we had a cached access token, use it
+ $InvokeSplat.Headers = @{Authorization = "Bearer $($script:NeocitiesAccessToken)"}
+ }
+ elseif ($script:NeoCitiesCredential) {
+ # If we had a cached credential, use it
+ $InvokeSplat.Credential = $script:NeoCitiesCredential
+ # and don't forget to set authentication to basic.
+ $InvokeSplat.Authentication = 'Basic'
+ }
+
+ # If neither an access token nor a credential was provided, we can't do anything.
+ if (-not $InvokeSplat.Credential -and -not $InvokeSplat.Headers)
+ {
+ # so error out.
+ Write-Error "No -Credential provided"
+ return
+ }
+
+
+ # Write a little verbose message to let the user know what we're doing
+ Write-Verbose "Requesting $($InvokeSplat.Uri)"
+ # and get a response from neocities.
+ $neocitiesResponse = Invoke-RestMethod @InvokeSplat
+ switch ($parameterSet) {
+ info {
+ # If we're getting info, we want to return the info object
+ $neocitiesResponse = $neocitiesResponse.info
+ }
+ list {
+ # If we're listing files, we want to return the files object
+ $neocitiesResponse = @($neocitiesResponse.files)
+ # and we want to return each as a 'file', not a 'list'
+ $pseudoType = 'file'
+ }
+ }
+
+ # Go over each response
+ foreach ($neoResponse in $neocitiesResponse) {
+ # and decorate them with the type name
+ $neoResponse.pstypenames.clear()
+ $neoResponse.pstypenames.insert(0, ($psuedoNamespace, $pseudoType -join '.'))
+ # and output the response.
+ $neoResponse
+ }
+ }
+}
diff --git a/Commands/Remove-Neocities.ps1 b/Commands/Remove-Neocities.ps1
new file mode 100644
index 0000000..cc59be6
--- /dev/null
+++ b/Commands/Remove-Neocities.ps1
@@ -0,0 +1,115 @@
+function Remove-Neocities
+{
+ <#
+ .SYNOPSIS
+ Removes files from neocities
+ .DESCRIPTION
+ Removes files from a neocities site using the neocities API.
+ #>
+ [CmdletBinding(DefaultParameterSetName='delete',SupportsShouldProcess,ConfirmImpact='High')]
+ param(
+ # The name of the file to remove.
+ [Parameter(Mandatory,ValueFromPipelineByPropertyName)]
+ [Alias('FullName','Path')]
+ [string[]]
+ $FileName,
+
+
+ # The neocities credential
+ [Parameter(ValueFromPipelineByPropertyName)]
+ [Alias(
+ 'Credentials', # Plural aliases are nice
+ 'PSCredential', # so are parameters that match the type name.
+ 'NeocitiesCredential', # A contextual alias is a good idea, too.
+ 'NeocitiesCredentials' # And you may need to pluralize that contextual alias.
+ )]
+ [PSCredential]
+ $Credential,
+
+ # The neocities access token.
+ [Parameter(ValueFromPipelineByPropertyName)]
+ [string]
+ $AccessToken
+ )
+
+ begin {
+ $NeocitiesApi = "https://neocities.org/api"
+ }
+
+ process {
+ $parameterSet = $PSCmdlet.ParameterSetName
+ $psuedoNamespace = "neocities"
+ $pseudoType = "$parameterSet"
+ $InvokeSplat = [Ordered]@{
+ Uri = "$NeocitiesApi", $PSCmdlet.ParameterSetName -join '/'
+ Method = 'POST'
+ }
+
+ # If an access token was provided
+ if ($AccessToken)
+ {
+ # use it
+ $InvokeSplat.Headers = @{Authorization = "Bearer $AccessToken"}
+ # and cache it for later use
+ $script:NeocitiesAccessToken = $AccessToken
+ }
+ elseif ($Credential)
+ {
+ # If a credential was provided, use it
+ $InvokeSplat.Credential = $Credential
+ # and cache it for later use
+ $script:NeoCitiesCredential = $Credential
+ # (don't forget to set authentication to basic)
+ $InvokeSplat.Authentication = 'Basic'
+ }
+ elseif ($script:NeocitiesAccessToken) {
+ # If we had a cached access token, use it
+ $InvokeSplat.Headers = @{Authorization = "Bearer $($script:NeocitiesAccessToken)"}
+ }
+ elseif ($script:NeoCitiesCredential) {
+ # If we had a cached credential, use it
+ $InvokeSplat.Credential = $script:NeoCitiesCredential
+ # and don't forget to set authentication to basic.
+ $InvokeSplat.Authentication = 'Basic'
+ }
+
+ # If neither an access token nor a credential was provided, we can't do anything.
+ if (-not $InvokeSplat.Credential -and -not $InvokeSplat.Headers)
+ {
+ # so error out.
+ Write-Error "No -Credential provided"
+ return
+ }
+
+ # For every file name provided, we need to remove it from the neocities site.
+ foreach ($file in $fileName) {
+ # Despite the name taking an array, we need to remove one file at a time.
+ $InvokeSplat.Body =
+ [web.httputility]::UrlEncode("filenames[]"),'=',[web.httputility]::UrlEncode($file) -join ''
+ Write-Verbose "Requesting $($InvokeSplat.Uri)"
+ # If -WhatIf was specified, we need to remove the credential and headers from the splat
+ if ($WhatIfPreference) {
+ $splatCopy = [Ordered]@{} + $InvokeSplat
+ $splatCopy.Remove('Credential')
+ $splatCopy.Remove('Headers')
+ # and then output the splat to the pipeline
+ $splatCopy
+ continue
+ }
+ # If we did not confirm the deletion
+ if (-not $PSCmdlet.ShouldProcess("Delete $file")) {
+ # skip it.
+ continue
+ }
+
+ # Get the response from neocities.
+ $neocitiesResponse = Invoke-RestMethod @InvokeSplat
+ # and decorate any response so that we know it was a deletion.
+ foreach ($neoResponse in $neocitiesResponse) {
+ $neoResponse.pstypenames.clear()
+ $neoResponse.pstypenames.insert(0, ($psuedoNamespace, $pseudoType -join '.'))
+ $neoResponse
+ }
+ }
+ }
+}
diff --git a/Commands/Set-Neocities.ps1 b/Commands/Set-Neocities.ps1
new file mode 100644
index 0000000..1aa8885
--- /dev/null
+++ b/Commands/Set-Neocities.ps1
@@ -0,0 +1,166 @@
+function Set-Neocities
+{
+ <#
+ .SYNOPSIS
+ Sets Neocities files
+ .DESCRIPTION
+ Sets files on Neocities website using the neocities API.
+ #>
+ [CmdletBinding(DefaultParameterSetName='upload')]
+ param(
+ # The path to the file to upload, or a dictionary of files and their contents.
+ [Parameter(ValueFromPipelineByPropertyName)]
+ [Alias('Fullname','FilePath','Path')]
+ [PSObject]
+ $File,
+
+ # The neocities credential
+ [Parameter(ValueFromPipelineByPropertyName)]
+ [Alias(
+ 'Credentials', # Plural aliases are nice
+ 'PSCredential', # so are parameters that match the type name.
+ 'NeocitiesCredential', # A contextual alias is a good idea, too.
+ 'NeocitiesCredentials' # And you may need to pluralize that contextual alias.
+ )]
+ [PSCredential]
+ $Credential,
+
+ # The access token
+ [Parameter(ValueFromPipelineByPropertyName)]
+ [string]
+ $AccessToken
+ )
+
+ begin {
+ $NeocitiesApi = "https://neocities.org/api"
+ $multiparts = [Ordered]@{}
+ $boundary = "boundary"
+ $contentType = "multipart/form-data; boundary=`"$boundary`""
+
+ }
+ process {
+ $parameterSet = $PSCmdlet.ParameterSetName
+ $psuedoNamespace = "neocities"
+ $pseudoType = "$parameterSet"
+ $InvokeSplat = [Ordered]@{
+ Uri = "$NeocitiesApi", $PSCmdlet.ParameterSetName -join '/'
+ }
+
+ # If an access token was provided
+ if ($AccessToken)
+ {
+ # use it
+ $InvokeSplat.Headers = @{Authorization = "Bearer $AccessToken"}
+ # and cache it for later use
+ $script:NeocitiesAccessToken = $AccessToken
+ }
+ elseif ($Credential)
+ {
+ # If a credential was provided, use it
+ $InvokeSplat.Credential = $Credential
+ # and cache it for later use
+ $script:NeoCitiesCredential = $Credential
+ # (don't forget to set authentication to basic)
+ $InvokeSplat.Authentication = 'Basic'
+ }
+ elseif ($script:NeocitiesAccessToken) {
+ # If we had a cached access token, use it
+ $InvokeSplat.Headers = @{Authorization = "Bearer $($script:NeocitiesAccessToken)"}
+ }
+ elseif ($script:NeoCitiesCredential) {
+ # If we had a cached credential, use it
+ $InvokeSplat.Credential = $script:NeoCitiesCredential
+ # and don't forget to set authentication to basic.
+ $InvokeSplat.Authentication = 'Basic'
+ }
+
+ # If neither an access token nor a credential was provided, we can't do anything.
+ if (-not $InvokeSplat.Credential -and -not $InvokeSplat.Headers)
+ {
+ # so error out.
+ Write-Error "No -Credential provided"
+ return
+ }
+
+ $InvokeSplat.ContentType = $contentType
+
+ # If we were piped in a file
+ if ($_ -is [IO.FileInfo]) {
+ $file = $_ # set the parameter directly
+ }
+
+ # For every file passed in, we need to make a unique request.
+ foreach ($fileInfo in $file) {
+ # If this is a file, this is easy
+ if ($fileInfo -is [IO.FileInfo]) {
+ # just get the string representation of the file's bytes and add them to the multipart collection
+ $multiparts[$file.Name] = $OutputEncoding.GetString([IO.File]::ReadAllBytes($file))
+ }
+ # If the file was a path, we need to get the file's contents
+ elseif ($fileInfo -is [string] -and (Test-Path $fileInfo)) {
+ $multiparts[$fileInfo] = Get-Content -Raw $fileInfo
+ }
+ # If the file was a dictionary, treat each key as a file name and each value as the file's contents
+ elseif ($fileInfo -is [Collections.IDictionary]) {
+ foreach ($keyValuePair in $fileInfo.GetEnumerator()) {
+ # If the value is a byte array, convert it to a string
+ if ($keyValuePair.Value -is [byte[]]) {
+ $multiparts[$keyValuePair.Key] = $OutputEncoding.GetString($keyValuePair.Value)
+ }
+ # If the value is a file, read the file's bytes and convert them to a string
+ elseif ($keyValuePair.Value -is [IO.FileInfo]) {
+ $multiparts[$keyValuePair.Key] = $OutputEncoding.GetString([IO.File]::ReadAllBytes($keyValuePair.Value))
+ }
+ # If the value is a pth to file, read the file's bytes and convert them to a string
+ elseif ($keyValuePair.Value -is [string] -and (Test-Path $keyValuePair.Value)) {
+ $multiparts[$keyValuePair.Key] = Get-Content -Raw $keyValuePair.Value
+ }
+ # last but not least, stringify the value and add it to the collection
+ else
+ {
+ $multiparts[$keyValuePair.Key] = "$($keyValuePair.Value)"
+ }
+ }
+ }
+ }
+
+ }
+
+ end {
+ # Despite the content type being multipart, we can actually only send one part at a time:
+
+ # Any way we slice it, we'll need to POST the data to the API.
+ $InvokeSplat.Method = 'POST'
+
+
+ # For each part we've found
+ foreach ($filePart in $multiparts.GetEnumerator()) {
+ $InvokeSplat.Body = @(
+ # Create a bounary
+ "--$boundary"
+ # Add the file name and content type to the header
+ "Content-Disposition: form-data; name=`"$($filePart.Key)`"; filename=`"$($filePart.Key)`""
+ # We're always uploading this a text/plain with neocities, and we need to set the encoding to whatever we passed.
+ "Content-Type: text/plain; charset=$($OutputEncoding.WebName)"
+ # The bounary MIME data must be followed by a newline
+ "`r`n"
+ # followed by the file contents
+ $filePart.Value
+ # followed by an additional boundary
+ "--$boundary--"
+ ) -join "`r`n" # (all of these pieces are joined by a newline)
+
+ # If -WhatIf was passed, don't actually upload the file, just show the splatted parameters.
+ if ($WhatIfPreference) {
+ # (Remove the headers and credential from the splatted parameters, so we don't leak any sensitive information)
+ $InvokeSplat.Remove('Headers')
+ $InvokeSplat.Remove('Credential')
+ $InvokeSplat
+ continue
+ }
+
+ # Invoke-RestMethod with our splatted parameters, and we'll upload the file.
+ Invoke-RestMethod @InvokeSplat
+ }
+ }
+}
\ No newline at end of file
diff --git a/Neocities.format.ps1xml b/Neocities.format.ps1xml
new file mode 100644
index 0000000..4e6a12f
--- /dev/null
+++ b/Neocities.format.ps1xml
@@ -0,0 +1,96 @@
+
+
+
+
+ neocities.file
+
+ neocities.file
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Path
+
+
+ Size
+
+
+ CreatedAt
+
+
+ UpdatedAt
+
+
+
+
+
+
+
+ neocities.info
+
+ neocities.info
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ if ($psStyle) {
+ $PSStyle.FormatHyperlink($_.SiteName, $_.Url)
+ } else {
+ $_.SiteName
+ }
+
+
+
+ Views
+
+
+ Hits
+
+
+ CreatedAt
+
+
+ UpdatedAt
+
+
+ Url
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Neocities.ps.psm1 b/Neocities.ps.psm1
new file mode 100644
index 0000000..562e155
--- /dev/null
+++ b/Neocities.ps.psm1
@@ -0,0 +1,26 @@
+$commandsPath = Join-Path $PSScriptRoot Commands
+[include('*-*')]$commandsPath
+
+$myModule = $MyInvocation.MyCommand.ScriptBlock.Module
+$ExecutionContext.SessionState.PSVariable.Set($myModule.Name, $myModule)
+$myModule.pstypenames.insert(0, $myModule.Name)
+
+New-PSDrive -Name $MyModule.Name -PSProvider FileSystem -Scope Global -Root $PSScriptRoot -ErrorAction Ignore
+
+if ($home) {
+ $MyModuleProfileDirectory = Join-Path ([Environment]::GetFolderPath("LocalApplicationData")) $MyModule.Name
+ if (-not (Test-Path $MyModuleProfileDirectory)) {
+ $null = New-Item -ItemType Directory -Path $MyModuleProfileDirectory -Force
+ }
+ New-PSDrive -Name "My$($MyModule.Name)" -PSProvider FileSystem -Scope Global -Root $MyModuleProfileDirectory -ErrorAction Ignore
+}
+
+# Set a script variable of this, set to the module
+# (so all scripts in this scope default to the correct `$this`)
+$script:this = $myModule
+
+#region Custom
+#endregion Custom
+
+Export-ModuleMember -Alias * -Function * -Variable $myModule.Name
+
diff --git a/Neocities.psd1 b/Neocities.psd1
new file mode 100644
index 0000000..88a90cc
--- /dev/null
+++ b/Neocities.psd1
@@ -0,0 +1,35 @@
+@{
+ ModuleVersion = '0.1'
+ RootModule = 'Neocities.psm1'
+ Guid = 'd62958e7-1cc7-470b-bde3-da29e96579fd'
+ Author = 'James Brundage'
+ CompanyName = 'Start-Automating'
+ Copyright = '2025 Start-Automating'
+ Description = 'Neocities PowerShell - Personal Webpages in PowerShell'
+ FunctionsToExport = @('Connect-Neocities','Get-Neocities','Set-Neocities','Remove-Neocities')
+ AliasesToExport = @('Neocities')
+ FormatsToProcess = @('Neocities.format.ps1xml')
+ TypesToProcess = @('Neocities.types.ps1xml')
+ PrivateData = @{
+ PSData = @{
+ Tags = @('neocities', 'PowerShell', 'Web', 'PowerShellWeb')
+ ProjectURI = 'https://github.com/PowerShellWeb/Neocities'
+ LicenseURI = 'https://github.com/PowerShellWeb/Neocities/blob/main/LICENSE'
+ ReleaseNotes = @'
+> Like It? [Star It](https://github.com/PowerShellWeb/Neocities)
+> Love It? [Support It](https://github.com/sponsors/StartAutomating)
+
+## Neocities 0.1
+
+Initial Neocities module (#1):
+
+* Get-Neocities (#2)
+* Set-Neocities (#3)
+* Remove-Neocities (#5)
+* Connect-Neocities (#4)
+
+---
+'@
+ }
+ }
+}
\ No newline at end of file
diff --git a/Neocities.psm1 b/Neocities.psm1
new file mode 100644
index 0000000..41a1efb
--- /dev/null
+++ b/Neocities.psm1
@@ -0,0 +1,36 @@
+$commandsPath = Join-Path $PSScriptRoot Commands
+:ToIncludeFiles foreach ($file in (Get-ChildItem -Path "$commandsPath" -Filter "*-*" -Recurse)) {
+ if ($file.Extension -ne '.ps1') { continue } # Skip if the extension is not .ps1
+ foreach ($exclusion in '\.[^\.]+\.ps1$') {
+ if (-not $exclusion) { continue }
+ if ($file.Name -match $exclusion) {
+ continue ToIncludeFiles # Skip excluded files
+ }
+ }
+ . $file.FullName
+}
+
+$myModule = $MyInvocation.MyCommand.ScriptBlock.Module
+$ExecutionContext.SessionState.PSVariable.Set($myModule.Name, $myModule)
+$myModule.pstypenames.insert(0, $myModule.Name)
+
+New-PSDrive -Name $MyModule.Name -PSProvider FileSystem -Scope Global -Root $PSScriptRoot -ErrorAction Ignore
+
+if ($home) {
+ $MyModuleProfileDirectory = Join-Path ([Environment]::GetFolderPath("LocalApplicationData")) $MyModule.Name
+ if (-not (Test-Path $MyModuleProfileDirectory)) {
+ $null = New-Item -ItemType Directory -Path $MyModuleProfileDirectory -Force
+ }
+ New-PSDrive -Name "My$($MyModule.Name)" -PSProvider FileSystem -Scope Global -Root $MyModuleProfileDirectory -ErrorAction Ignore
+}
+
+# Set a script variable of this, set to the module
+# (so all scripts in this scope default to the correct `$this`)
+$script:this = $myModule
+
+#region Custom
+#endregion Custom
+
+Export-ModuleMember -Alias * -Function * -Variable $myModule.Name
+
+
diff --git a/Neocities.types.ps1xml b/Neocities.types.ps1xml
new file mode 100644
index 0000000..b59f50e
--- /dev/null
+++ b/Neocities.types.ps1xml
@@ -0,0 +1,78 @@
+
+
+
+ neocities.file
+
+
+ CreatedAt
+
+ <#
+.SYNOPSIS
+ Gets the creation time
+.DESCRIPTION
+ Gets the creation time, as a `[DateTime]` object.
+#>
+$this.created_at -as [DateTime]
+
+
+
+
+ UpdatedAt
+
+ <#
+.SYNOPSIS
+ Gets the last update time
+.DESCRIPTION
+ Gets the last update time, as a `[DateTime]` object.
+#>
+$this.updated_at -as [DateTime]
+
+
+
+
+
+
+ neocities.info
+
+
+ CreatedAt
+
+ <#
+.SYNOPSIS
+ Gets the creation time
+.DESCRIPTION
+ Gets the creation time, as a `[DateTime]` object.
+#>
+$this.created_at -as [DateTime]
+
+
+
+
+ UpdatedAt
+
+ <#
+.SYNOPSIS
+ Gets the last update time
+.DESCRIPTION
+ Gets the last update time, as a `[DateTime]` object.
+#>
+$this.last_updated -as [DateTime]
+
+
+
+
+ Url
+
+ param()
+if ($this.Domain) {
+ "https://$($this.Domain)/"
+}
+elseif ($this.sitename) {
+ "https://$($this.sitename).neocities.org/"
+}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/README.md b/README.md
index 28fe077..a476851 100644
--- a/README.md
+++ b/README.md
@@ -1,2 +1,34 @@
-# Neocities
+## Neocities
+
Manage Neocities with PowerShell
+
+[https://neocities.org](Neocities) is a wonderful site for free personal webpages.
+
+It's bringing back the ethos of old internet: one of the most popular free hosting services of the 90s was [geocities](https://en.wikipedia.org/wiki/GeoCities).
+
+## The Neocities Module
+
+Neocities is also the name a PowerShell module to manage Neocities (no official relation).
+
+The Neocities module PowerShell is built atop the [neocities api](https://neocities.org/api).
+
+### Installing and Importing
+
+You can install the Neocities module by using the [PowerShell Gallery](https://powershellgallery.com)
+
+~~~PowerShell
+Install-Module Neocities
+~~~
+
+Once installed, you can import it with:
+
+~~~PowerShell
+Import-Module Neocities -PassThru
+~~~
+
+### Neocities Commands
+
+* [Get-Neocities](docs/Get-Neocities.md) gets neocities content
+* [Set-Neocities](docs/Set-Neocities.md) sets neocities content
+* [Remove-Neocities](docs/Remove-Neocities.md) removes neocities content
+* [Connect-Neocities](docs/Connect-Neocities.md) connects with a credential and gives you an access token
\ No newline at end of file
diff --git a/Types/neocities.file/get_CreatedAt.ps1 b/Types/neocities.file/get_CreatedAt.ps1
new file mode 100644
index 0000000..26dd3b7
--- /dev/null
+++ b/Types/neocities.file/get_CreatedAt.ps1
@@ -0,0 +1,7 @@
+<#
+.SYNOPSIS
+ Gets the creation time
+.DESCRIPTION
+ Gets the creation time, as a `[DateTime]` object.
+#>
+$this.created_at -as [DateTime]
diff --git a/Types/neocities.file/get_UpdatedAt.ps1 b/Types/neocities.file/get_UpdatedAt.ps1
new file mode 100644
index 0000000..5e85a62
--- /dev/null
+++ b/Types/neocities.file/get_UpdatedAt.ps1
@@ -0,0 +1,7 @@
+<#
+.SYNOPSIS
+ Gets the last update time
+.DESCRIPTION
+ Gets the last update time, as a `[DateTime]` object.
+#>
+$this.updated_at -as [DateTime]
diff --git a/Types/neocities.file/neocities.file.format.ps1 b/Types/neocities.file/neocities.file.format.ps1
new file mode 100644
index 0000000..08dbbc7
--- /dev/null
+++ b/Types/neocities.file/neocities.file.format.ps1
@@ -0,0 +1 @@
+Write-FormatView -TypeName 'neocities.file' -Property Path, Size, CreatedAt, UpdatedAt -AutoSize
diff --git a/Types/neocities.info/get_CreatedAt.ps1 b/Types/neocities.info/get_CreatedAt.ps1
new file mode 100644
index 0000000..26dd3b7
--- /dev/null
+++ b/Types/neocities.info/get_CreatedAt.ps1
@@ -0,0 +1,7 @@
+<#
+.SYNOPSIS
+ Gets the creation time
+.DESCRIPTION
+ Gets the creation time, as a `[DateTime]` object.
+#>
+$this.created_at -as [DateTime]
diff --git a/Types/neocities.info/get_UpdatedAt.ps1 b/Types/neocities.info/get_UpdatedAt.ps1
new file mode 100644
index 0000000..dfd7077
--- /dev/null
+++ b/Types/neocities.info/get_UpdatedAt.ps1
@@ -0,0 +1,7 @@
+<#
+.SYNOPSIS
+ Gets the last update time
+.DESCRIPTION
+ Gets the last update time, as a `[DateTime]` object.
+#>
+$this.last_updated -as [DateTime]
diff --git a/Types/neocities.info/get_Url.ps1 b/Types/neocities.info/get_Url.ps1
new file mode 100644
index 0000000..12e08fc
--- /dev/null
+++ b/Types/neocities.info/get_Url.ps1
@@ -0,0 +1,7 @@
+param()
+if ($this.Domain) {
+ "https://$($this.Domain)/"
+}
+elseif ($this.sitename) {
+ "https://$($this.sitename).neocities.org/"
+}
diff --git a/Types/neocities.info/neocities.info.format.ps1 b/Types/neocities.info/neocities.info.format.ps1
new file mode 100644
index 0000000..bc2337b
--- /dev/null
+++ b/Types/neocities.info/neocities.info.format.ps1
@@ -0,0 +1,9 @@
+Write-FormatView -TypeName 'neocities.info' -Property SiteName, Views, Hits, CreatedAt, UpdatedAt, Url -AutoSize -VirtualProperty @{
+ SiteName = {
+ if ($psStyle) {
+ $PSStyle.FormatHyperlink($_.SiteName, $_.Url)
+ } else {
+ $_.SiteName
+ }
+ }
+}
diff --git a/docs/Connect-Neocities.md b/docs/Connect-Neocities.md
new file mode 100644
index 0000000..62b4020
--- /dev/null
+++ b/docs/Connect-Neocities.md
@@ -0,0 +1,39 @@
+Connect-Neocities
+-----------------
+
+### Synopsis
+Connect to Neocities
+
+---
+
+### Description
+
+Connect to Neocities using a credential object.
+
+This will create a session that can be used to authenticate to the Neocities API.
+
+---
+
+### Related Links
+* [Get-Neocities](Get-Neocities.md)
+
+---
+
+### Parameters
+#### **Credential**
+The Neocities credential
+Plural aliases are nice
+so are parameters that match the type name.
+A contextual alias is a good idea, too.
+And you may need to pluralize that contextual alias.
+
+|Type |Required|Position|PipelineInput |Aliases |
+|----------------|--------|--------|---------------------|-----------------------------------------------------------------------------|
+|`[PSCredential]`|false |1 |true (ByPropertyName)|Credentials
PSCredential
NeocitiesCredential
NeocitiesCredentials|
+
+---
+
+### Syntax
+```PowerShell
+Connect-Neocities [[-Credential] ] []
+```
diff --git a/docs/Get-Neocities.md b/docs/Get-Neocities.md
new file mode 100644
index 0000000..39cc348
--- /dev/null
+++ b/docs/Get-Neocities.md
@@ -0,0 +1,72 @@
+Get-Neocities
+-------------
+
+### Synopsis
+Gets neocities information
+
+---
+
+### Description
+
+Gets neocities information from the neocities API, or lists the files in your neocities site.
+
+---
+
+### Examples
+> EXAMPLE 1
+
+```PowerShell
+Get-Neocities
+```
+> EXAMPLE 2
+
+```PowerShell
+Get-Neocities -Credential $neocitiesCredential
+```
+> EXAMPLE 3
+
+```PowerShell
+Get-Neocities -List
+```
+
+---
+
+### Parameters
+#### **List**
+If set, will list the files in your neocities site
+
+|Type |Required|Position|PipelineInput |
+|----------|--------|--------|---------------------|
+|`[Switch]`|true |named |true (ByPropertyName)|
+
+#### **Credential**
+The credential used to connect.
+This only needs to be provided once per module session
+(every time the module is imported)
+Plural aliases are nice
+so are parameters that match the type name.
+A contextual alias is a good idea, too.
+And you may need to pluralize that contextual alias.
+
+|Type |Required|Position|PipelineInput |Aliases |
+|----------------|--------|--------|---------------------|-----------------------------------------------------------------------------|
+|`[PSCredential]`|false |named |true (ByPropertyName)|Credentials
PSCredential
NeocitiesCredential
NeocitiesCredentials|
+
+#### **AccessToken**
+The access token used to connect.
+This only needs to be provided once per module session
+(every time the module is imported)
+
+|Type |Required|Position|PipelineInput |
+|----------|--------|--------|---------------------|
+|`[String]`|false |named |true (ByPropertyName)|
+
+---
+
+### Syntax
+```PowerShell
+Get-Neocities [-Credential ] [-AccessToken ] []
+```
+```PowerShell
+Get-Neocities -List [-Credential ] [-AccessToken ] []
+```
diff --git a/docs/README.md b/docs/README.md
new file mode 100644
index 0000000..26b278b
--- /dev/null
+++ b/docs/README.md
@@ -0,0 +1,34 @@
+## Neocities
+
+Manage Neocities with PowerShell
+
+[https://neocities.org](Neocities) is a wonderful site for free personal webpages.
+
+It's bringing back the ethos of old internet: one of the most popular free hosting services of the 90s was [geocities](https://en.wikipedia.org/wiki/GeoCities).
+
+## The Neocities Module
+
+Neocities is also the name a PowerShell module to manage Neocities (no official relation).
+
+The Neocities module PowerShell is built atop the [neocities api](https://neocities.org/api).
+
+### Installing and Importing
+
+You can install the Neocities module by using the [PowerShell Gallery](https://powershellgallery.com)
+
+~~~PowerShell
+Install-Module Neocities
+~~~
+
+Once installed, you can import it with:
+
+~~~PowerShell
+Import-Module Neocities -PassThru
+~~~
+
+### Neocities Commands
+
+* [Get-Neocities](Get-Neocities.md) gets neocities content
+* [Set-Neocities](Set-Neocities.md) sets neocities content
+* [Remove-Neocities](Remove-Neocities.md) removes neocities content
+* [Connect-Neocities](Connect-Neocities.md) connects with a credential and gives you an access token
diff --git a/docs/Remove-Neocities.md b/docs/Remove-Neocities.md
new file mode 100644
index 0000000..f29b305
--- /dev/null
+++ b/docs/Remove-Neocities.md
@@ -0,0 +1,57 @@
+Remove-Neocities
+----------------
+
+### Synopsis
+Removes files from neocities
+
+---
+
+### Description
+
+Removes files from a neocities site using the neocities API.
+
+---
+
+### Parameters
+#### **FileName**
+The name of the file to remove.
+
+|Type |Required|Position|PipelineInput |Aliases |
+|------------|--------|--------|---------------------|-----------------|
+|`[String[]]`|true |1 |true (ByPropertyName)|FullName
Path|
+
+#### **Credential**
+The neocities credential
+Plural aliases are nice
+so are parameters that match the type name.
+A contextual alias is a good idea, too.
+And you may need to pluralize that contextual alias.
+
+|Type |Required|Position|PipelineInput |Aliases |
+|----------------|--------|--------|---------------------|-----------------------------------------------------------------------------|
+|`[PSCredential]`|false |2 |true (ByPropertyName)|Credentials
PSCredential
NeocitiesCredential
NeocitiesCredentials|
+
+#### **AccessToken**
+The neocities access token.
+
+|Type |Required|Position|PipelineInput |
+|----------|--------|--------|---------------------|
+|`[String]`|false |3 |true (ByPropertyName)|
+
+#### **WhatIf**
+-WhatIf is an automatic variable that is created when a command has ```[CmdletBinding(SupportsShouldProcess)]```.
+-WhatIf is used to see what would happen, or return operations without executing them
+#### **Confirm**
+-Confirm is an automatic variable that is created when a command has ```[CmdletBinding(SupportsShouldProcess)]```.
+-Confirm is used to -Confirm each operation.
+
+If you pass ```-Confirm:$false``` you will not be prompted.
+
+If the command sets a ```[ConfirmImpact("Medium")]``` which is lower than ```$confirmImpactPreference```, you will not be prompted unless -Confirm is passed.
+
+---
+
+### Syntax
+```PowerShell
+Remove-Neocities [-FileName] [[-Credential] ] [[-AccessToken] ] [-WhatIf] [-Confirm] []
+```
diff --git a/docs/Set-Neocities.md b/docs/Set-Neocities.md
new file mode 100644
index 0000000..b8a5f2d
--- /dev/null
+++ b/docs/Set-Neocities.md
@@ -0,0 +1,46 @@
+Set-Neocities
+-------------
+
+### Synopsis
+Sets Neocities files
+
+---
+
+### Description
+
+Sets files on Neocities website using the neocities API.
+
+---
+
+### Parameters
+#### **File**
+The path to the file to upload, or a dictionary of files and their contents.
+
+|Type |Required|Position|PipelineInput |Aliases |
+|------------|--------|--------|---------------------|------------------------------|
+|`[PSObject]`|false |1 |true (ByPropertyName)|Fullname
FilePath
Path|
+
+#### **Credential**
+The neocities credential
+Plural aliases are nice
+so are parameters that match the type name.
+A contextual alias is a good idea, too.
+And you may need to pluralize that contextual alias.
+
+|Type |Required|Position|PipelineInput |Aliases |
+|----------------|--------|--------|---------------------|-----------------------------------------------------------------------------|
+|`[PSCredential]`|false |2 |true (ByPropertyName)|Credentials
PSCredential
NeocitiesCredential
NeocitiesCredentials|
+
+#### **AccessToken**
+The access token
+
+|Type |Required|Position|PipelineInput |
+|----------|--------|--------|---------------------|
+|`[String]`|false |3 |true (ByPropertyName)|
+
+---
+
+### Syntax
+```PowerShell
+Set-Neocities [[-File] ] [[-Credential] ] [[-AccessToken] ] []
+```
diff --git a/docs/_data/Help/Connect-Neocities.json b/docs/_data/Help/Connect-Neocities.json
new file mode 100644
index 0000000..fd56cbc
--- /dev/null
+++ b/docs/_data/Help/Connect-Neocities.json
@@ -0,0 +1,35 @@
+{
+ "Synopsis": "Connect to Neocities",
+ "Description": "Connect to Neocities using a credential object. \n\nThis will create a session that can be used to authenticate to the Neocities API.",
+ "Parameters": [
+ {
+ "Name": null,
+ "Type": null,
+ "Description": "",
+ "Required": false,
+ "Position": 0,
+ "Aliases": null,
+ "DefaultValue": null,
+ "Globbing": false,
+ "PipelineInput": null,
+ "variableLength": false
+ }
+ ],
+ "Notes": [
+ null
+ ],
+ "CommandType": "Function",
+ "Component": [
+ null
+ ],
+ "Inputs": [
+ null
+ ],
+ "Outputs": [
+ null
+ ],
+ "Links": [
+ null
+ ],
+ "Examples": []
+}
\ No newline at end of file
diff --git a/docs/_data/Help/Get-Neocities.json b/docs/_data/Help/Get-Neocities.json
new file mode 100644
index 0000000..8a113e1
--- /dev/null
+++ b/docs/_data/Help/Get-Neocities.json
@@ -0,0 +1,49 @@
+{
+ "Synopsis": "Gets neocities information",
+ "Description": "Gets neocities information from the neocities API, or lists the files in your neocities site.",
+ "Parameters": [
+ {
+ "Name": null,
+ "Type": null,
+ "Description": "",
+ "Required": false,
+ "Position": 0,
+ "Aliases": null,
+ "DefaultValue": null,
+ "Globbing": false,
+ "PipelineInput": null,
+ "variableLength": false
+ }
+ ],
+ "Notes": [
+ null
+ ],
+ "CommandType": "Function",
+ "Component": [
+ null
+ ],
+ "Inputs": [
+ null
+ ],
+ "Outputs": [
+ null
+ ],
+ "Links": [],
+ "Examples": [
+ {
+ "Title": "EXAMPLE 1",
+ "Markdown": "",
+ "Code": "Get-Neocities"
+ },
+ {
+ "Title": "EXAMPLE 2",
+ "Markdown": "",
+ "Code": "Get-Neocities -Credential $neocitiesCredential"
+ },
+ {
+ "Title": "EXAMPLE 3",
+ "Markdown": "",
+ "Code": "Get-Neocities -List"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/docs/_data/Help/Remove-Neocities.json b/docs/_data/Help/Remove-Neocities.json
new file mode 100644
index 0000000..292a64c
--- /dev/null
+++ b/docs/_data/Help/Remove-Neocities.json
@@ -0,0 +1,33 @@
+{
+ "Synopsis": "Removes files from neocities",
+ "Description": "Removes files from a neocities site using the neocities API.",
+ "Parameters": [
+ {
+ "Name": null,
+ "Type": null,
+ "Description": "",
+ "Required": false,
+ "Position": 0,
+ "Aliases": null,
+ "DefaultValue": null,
+ "Globbing": false,
+ "PipelineInput": null,
+ "variableLength": false
+ }
+ ],
+ "Notes": [
+ null
+ ],
+ "CommandType": "Function",
+ "Component": [
+ null
+ ],
+ "Inputs": [
+ null
+ ],
+ "Outputs": [
+ null
+ ],
+ "Links": [],
+ "Examples": []
+}
\ No newline at end of file
diff --git a/docs/_data/Help/Set-Neocities.json b/docs/_data/Help/Set-Neocities.json
new file mode 100644
index 0000000..dc80558
--- /dev/null
+++ b/docs/_data/Help/Set-Neocities.json
@@ -0,0 +1,33 @@
+{
+ "Synopsis": "Sets Neocities files",
+ "Description": "Sets files on Neocities website using the neocities API.",
+ "Parameters": [
+ {
+ "Name": null,
+ "Type": null,
+ "Description": "",
+ "Required": false,
+ "Position": 0,
+ "Aliases": null,
+ "DefaultValue": null,
+ "Globbing": false,
+ "PipelineInput": null,
+ "variableLength": false
+ }
+ ],
+ "Notes": [
+ null
+ ],
+ "CommandType": "Function",
+ "Component": [
+ null
+ ],
+ "Inputs": [
+ null
+ ],
+ "Outputs": [
+ null
+ ],
+ "Links": [],
+ "Examples": []
+}
\ No newline at end of file
diff --git a/docs/neocities/file/README.md b/docs/neocities/file/README.md
new file mode 100644
index 0000000..be859e2
--- /dev/null
+++ b/docs/neocities/file/README.md
@@ -0,0 +1,8 @@
+## neocities.file
+
+
+### Script Properties
+
+
+* [get_CreatedAt](get_CreatedAt.md)
+* [get_UpdatedAt](get_UpdatedAt.md)
diff --git a/docs/neocities/file/get_CreatedAt.md b/docs/neocities/file/get_CreatedAt.md
new file mode 100644
index 0000000..eab9266
--- /dev/null
+++ b/docs/neocities/file/get_CreatedAt.md
@@ -0,0 +1,13 @@
+get_CreatedAt
+-------------
+
+### Synopsis
+Gets the creation time
+
+---
+
+### Description
+
+Gets the creation time, as a `[DateTime]` object.
+
+---
diff --git a/docs/neocities/file/get_UpdatedAt.md b/docs/neocities/file/get_UpdatedAt.md
new file mode 100644
index 0000000..d3841b5
--- /dev/null
+++ b/docs/neocities/file/get_UpdatedAt.md
@@ -0,0 +1,13 @@
+get_UpdatedAt
+-------------
+
+### Synopsis
+Gets the last update time
+
+---
+
+### Description
+
+Gets the last update time, as a `[DateTime]` object.
+
+---
diff --git a/docs/neocities/info/README.md b/docs/neocities/info/README.md
new file mode 100644
index 0000000..c644323
--- /dev/null
+++ b/docs/neocities/info/README.md
@@ -0,0 +1,8 @@
+## neocities.info
+
+
+### Script Properties
+
+
+* [get_CreatedAt](get_CreatedAt.md)
+* [get_UpdatedAt](get_UpdatedAt.md)
diff --git a/docs/neocities/info/get_CreatedAt.md b/docs/neocities/info/get_CreatedAt.md
new file mode 100644
index 0000000..eab9266
--- /dev/null
+++ b/docs/neocities/info/get_CreatedAt.md
@@ -0,0 +1,13 @@
+get_CreatedAt
+-------------
+
+### Synopsis
+Gets the creation time
+
+---
+
+### Description
+
+Gets the creation time, as a `[DateTime]` object.
+
+---
diff --git a/docs/neocities/info/get_UpdatedAt.md b/docs/neocities/info/get_UpdatedAt.md
new file mode 100644
index 0000000..d3841b5
--- /dev/null
+++ b/docs/neocities/info/get_UpdatedAt.md
@@ -0,0 +1,13 @@
+get_UpdatedAt
+-------------
+
+### Synopsis
+Gets the last update time
+
+---
+
+### Description
+
+Gets the last update time, as a `[DateTime]` object.
+
+---