Import current workspace
This commit is contained in:
204
README.md
Normal file
204
README.md
Normal file
@@ -0,0 +1,204 @@
|
||||
# Portable Caffeine (PowerShell 5.1 + Embedded C#)
|
||||
|
||||
Portable Windows implementation of the Zhorn Software Caffeine utility, built as a single `caffeine.ps1` script with embedded C# (`WinForms` + Win32 API calls).
|
||||
|
||||
## What It Does
|
||||
|
||||
Prevents sleep/idle by either:
|
||||
|
||||
- Sending periodic key pulses (default `F15`)
|
||||
- Using `SetThreadExecutionState` (`-stes`)
|
||||
|
||||
It also includes a tray icon app, status dialog, timers, rule-based activation, and single-instance app control commands.
|
||||
|
||||
## Files
|
||||
|
||||
- `caffeine.ps1`: main portable app
|
||||
- `run-caffeine.bat`: launcher that runs with `-ExecutionPolicy Bypass -STA`
|
||||
- `build-caffeine-exe.ps1`: extracts embedded C# and compiles a standalone `.exe`
|
||||
- `sign-caffeine-exe.ps1`: creates/reuses a self-signed code-signing cert, optionally trusts it, and signs the `.exe`
|
||||
|
||||
## Requirements
|
||||
|
||||
- Windows
|
||||
- Windows PowerShell 5.1
|
||||
- Desktop session (uses `System.Windows.Forms` + tray icon)
|
||||
|
||||
## Quick Start
|
||||
|
||||
Run with the batch launcher:
|
||||
|
||||
```bat
|
||||
run-caffeine.bat -showdlg -notify
|
||||
```
|
||||
|
||||
Or directly:
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -STA -File .\caffeine.ps1 -showdlg -notify
|
||||
```
|
||||
|
||||
## Execution Policy Note
|
||||
|
||||
If PowerShell blocks script execution, use the launcher (`run-caffeine.bat`) or run:
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -STA -File .\caffeine.ps1
|
||||
```
|
||||
|
||||
Optional:
|
||||
|
||||
```powershell
|
||||
Unblock-File .\caffeine.ps1
|
||||
```
|
||||
|
||||
## Supported Features / Switches
|
||||
|
||||
Core:
|
||||
|
||||
- `-on` / `-off`
|
||||
- `-activefor:N`
|
||||
- `-inactivefor:N`
|
||||
- `-exitafter:N`
|
||||
- `-lock`
|
||||
- `-notify`
|
||||
- `-showdlg`
|
||||
- `-ontaskbar`
|
||||
- `-replace`
|
||||
|
||||
Single-instance app commands:
|
||||
|
||||
- `-appexit`
|
||||
- `-appon`
|
||||
- `-appoff`
|
||||
- `-apptoggle`
|
||||
- `-apptoggleshowdlg`
|
||||
|
||||
Activity methods:
|
||||
|
||||
- Default F15 key pulse
|
||||
- `-useshift`
|
||||
- `-leftshift`
|
||||
- `-key:NN` / `-keypress:NN`
|
||||
- `-keyshift[:NN]`
|
||||
- `-stes`
|
||||
- `-allowss`
|
||||
|
||||
Conditional activation:
|
||||
|
||||
- `-watchwindow:TEXT`
|
||||
- `-activeperiods:RANGES`
|
||||
- `-activehours:RANGES` (alias)
|
||||
- `-onac`
|
||||
- `-cpu:N`
|
||||
|
||||
Tray / icon behavior:
|
||||
|
||||
- Custom coffee-cup tray icon (active/inactive variants)
|
||||
- `-nohicon` keeps the same tray icon in both states
|
||||
- Double-click tray icon toggles active/inactive
|
||||
|
||||
Compatibility:
|
||||
|
||||
- `-allowlocal` is recognized (compatibility flag)
|
||||
|
||||
## Examples
|
||||
|
||||
```bat
|
||||
run-caffeine.bat -showdlg -notify
|
||||
run-caffeine.bat -activefor:30
|
||||
run-caffeine.bat -watchwindow:Notepad -on
|
||||
run-caffeine.bat -activeperiods:08:00-12:00,13:00-17:00
|
||||
run-caffeine.bat -stes -allowss
|
||||
run-caffeine.bat -apptoggle
|
||||
run-caffeine.bat -replace -off
|
||||
```
|
||||
|
||||
## Compile To EXE (Windows)
|
||||
|
||||
You can build a standalone executable from the embedded C# code in `caffeine.ps1`:
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\build-caffeine-exe.ps1
|
||||
```
|
||||
|
||||
Output (default):
|
||||
|
||||
- `dist\PortableCaffeine.exe`
|
||||
|
||||
Useful options:
|
||||
|
||||
```powershell
|
||||
# Keep the extracted .cs file used for compilation
|
||||
.\build-caffeine-exe.ps1 -KeepExtractedSource
|
||||
|
||||
# Build a console-targeted EXE instead of WinExe
|
||||
.\build-caffeine-exe.ps1 -ConsoleTarget
|
||||
|
||||
# Custom output name/path
|
||||
.\build-caffeine-exe.ps1 -OutputDir .\out -ExeName Caffeine.exe
|
||||
```
|
||||
|
||||
Notes:
|
||||
|
||||
- The build script looks for `.NET Framework` `csc.exe` on Windows (Framework64 first, then Framework).
|
||||
- The compiled EXE uses the same tray UI, switches, and behavior as the PowerShell-hosted version.
|
||||
|
||||
## Self-Signed Code Signing (Dev/Test)
|
||||
|
||||
You can sign the compiled EXE with a locally generated self-signed code-signing certificate.
|
||||
|
||||
Default behavior:
|
||||
|
||||
- Reuses an existing cert matching the configured subject (if present)
|
||||
- Otherwise creates a new self-signed code-signing cert
|
||||
- Exports the public cert to `dist\PortableCaffeine-dev-signing.cer`
|
||||
- Installs trust in `CurrentUser\Root` and `CurrentUser\TrustedPublisher`
|
||||
- Signs `dist\PortableCaffeine.exe` with `Set-AuthenticodeSignature`
|
||||
|
||||
Command:
|
||||
|
||||
```powershell
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\sign-caffeine-exe.ps1
|
||||
```
|
||||
|
||||
Machine-wide trust (all users, requires admin):
|
||||
|
||||
```powershell
|
||||
# Run PowerShell as Administrator
|
||||
.\sign-caffeine-exe.ps1 -TrustScope LocalMachine
|
||||
```
|
||||
|
||||
Notes on admin requirements:
|
||||
|
||||
- `CurrentUser` trust/store operations do not require admin.
|
||||
- `LocalMachine` trust (`Root` / `TrustedPublisher`) requires an elevated PowerShell session.
|
||||
- Creating the private key in `LocalMachine\My` also requires admin (`-PrivateKeyStoreScope LocalMachine`).
|
||||
|
||||
Useful options:
|
||||
|
||||
```powershell
|
||||
# Force creation of a new self-signed certificate instead of reusing an existing one
|
||||
.\sign-caffeine-exe.ps1 -ForceNewCertificate
|
||||
|
||||
# Sign without installing trust stores (signature will be present but likely untrusted)
|
||||
.\sign-caffeine-exe.ps1 -TrustScope None
|
||||
|
||||
# Export a PFX (password required)
|
||||
.\sign-caffeine-exe.ps1 -PfxExportPath .\dist\PortableCaffeine-dev-signing.pfx -PfxPassword (Read-Host -AsSecureString)
|
||||
|
||||
# Use a timestamp server (optional)
|
||||
.\sign-caffeine-exe.ps1 -TimestampServer http://timestamp.digicert.com
|
||||
```
|
||||
|
||||
Security note:
|
||||
|
||||
- Trusting a self-signed certificate in `Root` makes your machine trust anything signed by that certificate.
|
||||
- Use a dedicated dev/test certificate, protect the private key, and remove trust when no longer needed.
|
||||
- This is suitable for local/internal testing, not a substitute for a public CA-issued code-signing certificate.
|
||||
|
||||
## Notes
|
||||
|
||||
- This project is intended to be portable (no install required).
|
||||
- The implementation targets Windows PowerShell 5.1 and Windows desktop APIs.
|
||||
- Exact visual/behavioral parity with the original Zhorn Caffeine may differ slightly in some edge cases.
|
||||
44
agent.md
Normal file
44
agent.md
Normal file
@@ -0,0 +1,44 @@
|
||||
|
||||
## Workflow Orchestration
|
||||
### 1. Plan Node Default
|
||||
- Enter plan mode for ANY non-trivial task (3+ steps or architectural decisions)
|
||||
- If something goes sideways, STOP and re-plan immediately - don't keep pushing
|
||||
- Use plan mode for verification steps, not just building
|
||||
- Write detailed specs upfront to reduce ambiguity
|
||||
### 2. Subagent Strategy
|
||||
- Use subagents liberally to keep main context window clean
|
||||
- Offload research, exploration, and parallel analysis to subagents
|
||||
- For complex problems, throw more compute at it via subagents
|
||||
- One tack per subagent for focused execution
|
||||
### 3. Self-Improvement Loop
|
||||
- After ANY correction from the user: update tasks/lessons.md
|
||||
with the pattern
|
||||
- Write rules for yourself that prevent the same mistake
|
||||
- Ruthlessly iterate on these lessons until mistake rate drops
|
||||
- Review lessons at session start for relevant project
|
||||
### 4. Verification Before Done
|
||||
- Never mark a task complete without proving it works
|
||||
- Diff behavior between main and your changes when relevant
|
||||
- Ask yourself: "Would a staff engineer approve this?"
|
||||
- Run tests, check logs, demonstrate correctness
|
||||
### 5. Demand Elegance (Balanced)
|
||||
- For non-trivial changes: pause and ask "is there a more elegant way?"
|
||||
- If a fix feels hacky: "Knowing everything I know now, implement the elegant solution"
|
||||
- Skip this for simple, obvious fixes - don't over-engineer
|
||||
- Challenge your own work before presenting it
|
||||
### 6. Autonomous Bug Fizing
|
||||
- When given a bug report: just fix it. Don't ask for hand-holding
|
||||
- Point at logs, errors, failing tests - then resolve them
|
||||
- Zero context switching required from the user
|
||||
- Go fix failing CI tests without being told how
|
||||
## Task Management
|
||||
1. **Plan First**: Write plan to "tasks/todo.md with checkable items
|
||||
2. **Verify Plan**: Check in before starting implementation
|
||||
3. **Track Progress**: Mark items complete as you go
|
||||
4. **Explain Changes**: High-level summary at each step
|
||||
5. **Document Results**: Add review section to tasks/todo.md"
|
||||
6. **Capture Lessons**: Update tasks/lessons. md after corrections
|
||||
## Core Principles
|
||||
- **Simplicity First**: Make every change as simple as possible. Impact minimal code.
|
||||
- **No Laziness**: Find root causes. No temporary fixes, Senior developer standards.
|
||||
- **Minimat Impact**: Changes should only touch what's necessary. Avoid introducing bugs.
|
||||
101
build-caffeine-exe.ps1
Normal file
101
build-caffeine-exe.ps1
Normal file
@@ -0,0 +1,101 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$SourceScript = (Join-Path $PSScriptRoot 'caffeine.ps1'),
|
||||
[string]$OutputDir = (Join-Path $PSScriptRoot 'dist'),
|
||||
[string]$BuildDir = (Join-Path $PSScriptRoot 'build'),
|
||||
[string]$ExeName = 'PortableCaffeine.exe',
|
||||
[switch]$ConsoleTarget,
|
||||
[switch]$KeepExtractedSource
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Get-CSharpFromCaffeineScript {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Path)) {
|
||||
throw "Source script not found: $Path"
|
||||
}
|
||||
|
||||
$raw = Get-Content -LiteralPath $Path -Raw -Encoding UTF8
|
||||
|
||||
$pattern = '(?s)\$csharp\s*=\s*@''\r?\n(?<code>.*?)\r?\n''@'
|
||||
$match = [regex]::Match($raw, $pattern)
|
||||
if (-not $match.Success) {
|
||||
throw "Could not locate embedded C# here-string (`$csharp = @' ... '@) in $Path"
|
||||
}
|
||||
|
||||
return $match.Groups['code'].Value
|
||||
}
|
||||
|
||||
function Get-CscPath {
|
||||
$candidates = @(
|
||||
(Join-Path $env:WINDIR 'Microsoft.NET\Framework64\v4.0.30319\csc.exe'),
|
||||
(Join-Path $env:WINDIR 'Microsoft.NET\Framework\v4.0.30319\csc.exe')
|
||||
)
|
||||
|
||||
foreach ($candidate in $candidates) {
|
||||
if ($candidate -and (Test-Path -LiteralPath $candidate)) {
|
||||
return $candidate
|
||||
}
|
||||
}
|
||||
|
||||
$cmd = Get-Command csc.exe -ErrorAction SilentlyContinue
|
||||
if ($cmd) {
|
||||
return $cmd.Source
|
||||
}
|
||||
|
||||
throw "csc.exe not found. Install .NET Framework build tools (or run on Windows with .NET Framework 4.x)."
|
||||
}
|
||||
|
||||
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
|
||||
throw "This build script is intended for Windows because it compiles WinForms code against .NET Framework (csc.exe)."
|
||||
}
|
||||
|
||||
New-Item -ItemType Directory -Force -Path $OutputDir | Out-Null
|
||||
New-Item -ItemType Directory -Force -Path $BuildDir | Out-Null
|
||||
|
||||
$csharpSource = Get-CSharpFromCaffeineScript -Path $SourceScript
|
||||
$generatedCsPath = Join-Path $BuildDir 'PortableCaffeine.generated.cs'
|
||||
$outExePath = Join-Path $OutputDir $ExeName
|
||||
$targetKind = if ($ConsoleTarget) { 'exe' } else { 'winexe' }
|
||||
$cscPath = Get-CscPath
|
||||
|
||||
[System.IO.File]::WriteAllText($generatedCsPath, $csharpSource, (New-Object System.Text.UTF8Encoding($false)))
|
||||
|
||||
$arguments = @(
|
||||
'/nologo'
|
||||
"/target:$targetKind"
|
||||
'/optimize+'
|
||||
'/platform:anycpu'
|
||||
"/out:$outExePath"
|
||||
'/r:System.dll'
|
||||
'/r:System.Windows.Forms.dll'
|
||||
'/r:System.Drawing.dll'
|
||||
$generatedCsPath
|
||||
)
|
||||
|
||||
Write-Host "Compiling with: $cscPath"
|
||||
Write-Host "Target: $targetKind"
|
||||
Write-Host "Output: $outExePath"
|
||||
|
||||
& $cscPath @arguments
|
||||
$exitCode = $LASTEXITCODE
|
||||
if ($exitCode -ne 0) {
|
||||
throw "csc.exe failed with exit code $exitCode"
|
||||
}
|
||||
|
||||
if (-not $KeepExtractedSource) {
|
||||
Remove-Item -LiteralPath $generatedCsPath -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Build succeeded."
|
||||
Write-Host "EXE: $outExePath"
|
||||
if ($KeepExtractedSource) {
|
||||
Write-Host "Extracted source kept at: $generatedCsPath"
|
||||
}
|
||||
1982
caffeine.ps1
Normal file
1982
caffeine.ps1
Normal file
File diff suppressed because it is too large
Load Diff
13
run-caffeine.bat
Normal file
13
run-caffeine.bat
Normal file
@@ -0,0 +1,13 @@
|
||||
@echo off
|
||||
setlocal
|
||||
|
||||
set "SCRIPT_DIR=%~dp0"
|
||||
set "PS_SCRIPT=%SCRIPT_DIR%caffeine.ps1"
|
||||
|
||||
if not exist "%PS_SCRIPT%" (
|
||||
echo Could not find "%PS_SCRIPT%".
|
||||
exit /b 1
|
||||
)
|
||||
|
||||
powershell.exe -NoProfile -ExecutionPolicy Bypass -STA -File "%PS_SCRIPT%" %*
|
||||
exit /b %ERRORLEVEL%
|
||||
378
sign-caffeine-exe.ps1
Normal file
378
sign-caffeine-exe.ps1
Normal file
@@ -0,0 +1,378 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[string]$ExePath = (Join-Path $PSScriptRoot 'dist\PortableCaffeine.exe'),
|
||||
[string]$Subject = 'CN=Portable Caffeine Dev Code Signing',
|
||||
[string]$FriendlyName = 'Portable Caffeine Dev Code Signing (Self-Signed)',
|
||||
[ValidateSet('None', 'CurrentUser', 'LocalMachine')]
|
||||
[string]$TrustScope = 'CurrentUser',
|
||||
[ValidateSet('CurrentUser', 'LocalMachine')]
|
||||
[string]$PrivateKeyStoreScope = 'CurrentUser',
|
||||
[int]$YearsValid = 5,
|
||||
[switch]$ForceNewCertificate,
|
||||
[string]$TimestampServer,
|
||||
[string]$CerExportPath = (Join-Path $PSScriptRoot 'dist\PortableCaffeine-dev-signing.cer'),
|
||||
[string]$PfxExportPath,
|
||||
[securestring]$PfxPassword
|
||||
)
|
||||
|
||||
Set-StrictMode -Version Latest
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if ([Environment]::OSVersion.Platform -ne [PlatformID]::Win32NT) {
|
||||
throw "This script is intended for Windows only."
|
||||
}
|
||||
|
||||
if (-not (Get-Command New-SelfSignedCertificate -ErrorAction SilentlyContinue)) {
|
||||
throw "New-SelfSignedCertificate is not available in this PowerShell session."
|
||||
}
|
||||
|
||||
if (-not (Get-Command Set-AuthenticodeSignature -ErrorAction SilentlyContinue)) {
|
||||
throw "Set-AuthenticodeSignature is not available in this PowerShell session."
|
||||
}
|
||||
|
||||
if (-not ('PortableCaffeineBuild.SigningHelpers' -as [type])) {
|
||||
$helperCode = @"
|
||||
using System;
|
||||
using System.Security.Principal;
|
||||
using System.Security.Cryptography.X509Certificates;
|
||||
|
||||
namespace PortableCaffeineBuild
|
||||
{
|
||||
public static class SigningHelpers
|
||||
{
|
||||
public static bool IsAdministrator()
|
||||
{
|
||||
WindowsIdentity identity = WindowsIdentity.GetCurrent();
|
||||
if (identity == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
WindowsPrincipal principal = new WindowsPrincipal(identity);
|
||||
return principal.IsInRole(WindowsBuiltInRole.Administrator);
|
||||
}
|
||||
|
||||
public static bool StoreContainsThumbprint(string storeName, string storeLocation, string thumbprint)
|
||||
{
|
||||
if (string.IsNullOrEmpty(thumbprint))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
StoreName sn = (StoreName)Enum.Parse(typeof(StoreName), storeName, true);
|
||||
StoreLocation sl = (StoreLocation)Enum.Parse(typeof(StoreLocation), storeLocation, true);
|
||||
|
||||
X509Store store = new X509Store(sn, sl);
|
||||
try
|
||||
{
|
||||
store.Open(OpenFlags.ReadOnly);
|
||||
string normalized = thumbprint.Replace(" ", string.Empty).ToUpperInvariant();
|
||||
foreach (X509Certificate2 cert in store.Certificates)
|
||||
{
|
||||
if (cert == null || string.IsNullOrEmpty(cert.Thumbprint))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (cert.Thumbprint.Replace(" ", string.Empty).ToUpperInvariant() == normalized)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
store.Close();
|
||||
}
|
||||
}
|
||||
|
||||
public static void AddPublicCertificateToStore(byte[] rawData, string storeName, string storeLocation)
|
||||
{
|
||||
if (rawData == null || rawData.Length == 0)
|
||||
{
|
||||
throw new ArgumentException("rawData");
|
||||
}
|
||||
|
||||
X509Certificate2 publicCert = new X509Certificate2(rawData);
|
||||
try
|
||||
{
|
||||
if (StoreContainsThumbprint(storeName, storeLocation, publicCert.Thumbprint))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
StoreName sn = (StoreName)Enum.Parse(typeof(StoreName), storeName, true);
|
||||
StoreLocation sl = (StoreLocation)Enum.Parse(typeof(StoreLocation), storeLocation, true);
|
||||
X509Store store = new X509Store(sn, sl);
|
||||
try
|
||||
{
|
||||
store.Open(OpenFlags.ReadWrite);
|
||||
store.Add(publicCert);
|
||||
}
|
||||
finally
|
||||
{
|
||||
store.Close();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
publicCert.Reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
"@
|
||||
|
||||
Add-Type -TypeDefinition $helperCode -ReferencedAssemblies @(
|
||||
'System.dll'
|
||||
)
|
||||
}
|
||||
|
||||
function Test-IsAdministrator {
|
||||
return [PortableCaffeineBuild.SigningHelpers]::IsAdministrator()
|
||||
}
|
||||
|
||||
function Get-CodeSigningCertificate {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SubjectName,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$StoreScope
|
||||
)
|
||||
|
||||
$storePath = "Cert:\$StoreScope\My"
|
||||
if (-not (Test-Path -LiteralPath $storePath)) {
|
||||
return $null
|
||||
}
|
||||
|
||||
$now = Get-Date
|
||||
$certs = Get-ChildItem -Path $storePath |
|
||||
Where-Object {
|
||||
$_.Subject -eq $SubjectName -and
|
||||
$_.HasPrivateKey -and
|
||||
$_.NotAfter -gt $now
|
||||
} |
|
||||
Sort-Object -Property NotAfter -Descending
|
||||
|
||||
if ($certs) {
|
||||
return @($certs)[0]
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function New-CodeSigningCertificate {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$SubjectName,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Friendly,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$StoreScope,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[int]$ValidYears
|
||||
)
|
||||
|
||||
$certStoreLocation = "Cert:\$StoreScope\My"
|
||||
$notAfter = (Get-Date).AddYears($ValidYears)
|
||||
|
||||
Write-Host "Creating new self-signed code signing certificate..."
|
||||
Write-Host "Subject: $SubjectName"
|
||||
Write-Host "Store: $certStoreLocation"
|
||||
|
||||
$newCert = New-SelfSignedCertificate `
|
||||
-Type CodeSigningCert `
|
||||
-Subject $SubjectName `
|
||||
-FriendlyName $Friendly `
|
||||
-CertStoreLocation $certStoreLocation `
|
||||
-KeyAlgorithm RSA `
|
||||
-KeyLength 2048 `
|
||||
-KeySpec Signature `
|
||||
-HashAlgorithm SHA256 `
|
||||
-KeyExportPolicy Exportable `
|
||||
-NotAfter $notAfter
|
||||
|
||||
if (-not $newCert) {
|
||||
throw "New-SelfSignedCertificate did not return a certificate."
|
||||
}
|
||||
|
||||
return $newCert
|
||||
}
|
||||
|
||||
function Install-CertificateTrust {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[ValidateSet('CurrentUser', 'LocalMachine')]
|
||||
[string]$Scope
|
||||
)
|
||||
|
||||
foreach ($storeName in @('Root', 'TrustedPublisher')) {
|
||||
$alreadyTrusted = [PortableCaffeineBuild.SigningHelpers]::StoreContainsThumbprint($storeName, $Scope, $Certificate.Thumbprint)
|
||||
if ($alreadyTrusted) {
|
||||
Write-Host "Trusted store already contains cert: $Scope\$storeName ($($Certificate.Thumbprint))"
|
||||
continue
|
||||
}
|
||||
|
||||
Write-Host "Adding cert to trusted store: $Scope\$storeName"
|
||||
[PortableCaffeineBuild.SigningHelpers]::AddPublicCertificateToStore($Certificate.RawData, $storeName, $Scope)
|
||||
}
|
||||
}
|
||||
|
||||
function Export-PublicCertificateIfRequested {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
|
||||
[string]$Path
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Path)) {
|
||||
return
|
||||
}
|
||||
|
||||
$dir = Split-Path -Parent $Path
|
||||
if ($dir) {
|
||||
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
||||
}
|
||||
|
||||
$raw = $Certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Cert)
|
||||
[System.IO.File]::WriteAllBytes($Path, $raw)
|
||||
Write-Host "Exported public certificate: $Path"
|
||||
}
|
||||
|
||||
function Export-PfxIfRequested {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
|
||||
[string]$Path,
|
||||
[securestring]$Password
|
||||
)
|
||||
|
||||
if ([string]::IsNullOrWhiteSpace($Path)) {
|
||||
return
|
||||
}
|
||||
|
||||
if (-not $Password) {
|
||||
throw "PfxExportPath was provided but no -PfxPassword was supplied."
|
||||
}
|
||||
|
||||
if (-not (Get-Command Export-PfxCertificate -ErrorAction SilentlyContinue)) {
|
||||
throw "Export-PfxCertificate is not available in this PowerShell session."
|
||||
}
|
||||
|
||||
$dir = Split-Path -Parent $Path
|
||||
if ($dir) {
|
||||
New-Item -ItemType Directory -Force -Path $dir | Out-Null
|
||||
}
|
||||
|
||||
Export-PfxCertificate -Cert $Certificate -FilePath $Path -Password $Password -Force | Out-Null
|
||||
Write-Host "Exported PFX: $Path"
|
||||
}
|
||||
|
||||
function Sign-Executable {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path,
|
||||
[Parameter(Mandatory = $true)]
|
||||
[System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
|
||||
[string]$TimestampUrl
|
||||
)
|
||||
|
||||
$params = @{
|
||||
FilePath = $Path
|
||||
Certificate = $Certificate
|
||||
HashAlgorithm = 'SHA256'
|
||||
}
|
||||
|
||||
if (-not [string]::IsNullOrWhiteSpace($TimestampUrl)) {
|
||||
$params['TimestampServer'] = $TimestampUrl
|
||||
}
|
||||
|
||||
Write-Host "Signing file: $Path"
|
||||
$result = Set-AuthenticodeSignature @params
|
||||
|
||||
if (-not $result) {
|
||||
throw "Set-AuthenticodeSignature did not return a result."
|
||||
}
|
||||
|
||||
Write-Host "Sign status: $($result.Status)"
|
||||
if ($result.StatusMessage) {
|
||||
Write-Host "Status msg: $($result.StatusMessage)"
|
||||
}
|
||||
|
||||
return $result
|
||||
}
|
||||
|
||||
if (-not (Test-Path -LiteralPath $ExePath)) {
|
||||
throw "EXE not found: $ExePath`nBuild it first with .\build-caffeine-exe.ps1"
|
||||
}
|
||||
|
||||
if ($YearsValid -lt 1 -or $YearsValid -gt 50) {
|
||||
throw "YearsValid must be between 1 and 50."
|
||||
}
|
||||
|
||||
$needsAdmin = $false
|
||||
if ($TrustScope -eq 'LocalMachine' -or $PrivateKeyStoreScope -eq 'LocalMachine') {
|
||||
$needsAdmin = $true
|
||||
}
|
||||
|
||||
if ($needsAdmin -and -not (Test-IsAdministrator)) {
|
||||
throw "This operation requires an elevated PowerShell session (Run as Administrator) because LocalMachine certificate stores are being modified/used."
|
||||
}
|
||||
|
||||
$cert = $null
|
||||
if (-not $ForceNewCertificate) {
|
||||
$cert = Get-CodeSigningCertificate -SubjectName $Subject -StoreScope $PrivateKeyStoreScope
|
||||
}
|
||||
|
||||
if (-not $cert) {
|
||||
$cert = New-CodeSigningCertificate -SubjectName $Subject -Friendly $FriendlyName -StoreScope $PrivateKeyStoreScope -ValidYears $YearsValid
|
||||
} else {
|
||||
Write-Host "Reusing existing certificate:"
|
||||
Write-Host "Subject: $($cert.Subject)"
|
||||
Write-Host "Thumbprint: $($cert.Thumbprint)"
|
||||
Write-Host "Expires: $($cert.NotAfter)"
|
||||
Write-Host "Store: Cert:\$PrivateKeyStoreScope\My"
|
||||
}
|
||||
|
||||
Export-PublicCertificateIfRequested -Certificate $cert -Path $CerExportPath
|
||||
Export-PfxIfRequested -Certificate $cert -Path $PfxExportPath -Password $PfxPassword
|
||||
|
||||
if ($TrustScope -ne 'None') {
|
||||
Write-Host ""
|
||||
Write-Host "Installing trust for self-signed certificate..."
|
||||
Install-CertificateTrust -Certificate $cert -Scope $TrustScope
|
||||
} else {
|
||||
Write-Host "Skipping trust-store installation (TrustScope=None)."
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
$signResult = Sign-Executable -Path $ExePath -Certificate $cert -TimestampUrl $TimestampServer
|
||||
$verify = Get-AuthenticodeSignature -FilePath $ExePath
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Verification:"
|
||||
Write-Host "Status: $($verify.Status)"
|
||||
if ($verify.SignerCertificate) {
|
||||
Write-Host "Thumbprint: $($verify.SignerCertificate.Thumbprint)"
|
||||
Write-Host "Subject: $($verify.SignerCertificate.Subject)"
|
||||
}
|
||||
if ($verify.TimeStamperCertificate) {
|
||||
Write-Host "Timestamped: Yes"
|
||||
} else {
|
||||
Write-Host "Timestamped: No"
|
||||
}
|
||||
|
||||
if ($verify.Status -ne 'Valid') {
|
||||
Write-Warning "Signature was applied but verification is not 'Valid'. For self-signed certs, ensure trust was installed in Root + TrustedPublisher for the relevant scope."
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Done."
|
||||
Write-Host "EXE: $ExePath"
|
||||
if ($CerExportPath) {
|
||||
Write-Host "CER: $CerExportPath"
|
||||
}
|
||||
34
tasks/todo.md
Normal file
34
tasks/todo.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Portable Caffeine Clone (PowerShell 5.1 + Embedded C#)
|
||||
|
||||
## Plan
|
||||
|
||||
- [x] Confirm feature parity target from https://www.zhornsoftware.co.uk/caffeine/ (current switches + tray behavior)
|
||||
- [x] Define implementation architecture (single script, embedded C# helpers, tray UI, single-instance IPC)
|
||||
- [x] Implement command-line parsing and normalized parameter model
|
||||
- [x] Implement activity engine (F15/Shift/custom key, key up vs keypress, STES, allow screensaver)
|
||||
- [x] Implement conditional activation rules (`-watchwindow`, `-activeperiods`/`-activehours`, `-onac`, `-cpu`)
|
||||
- [x] Implement timers (`-exitafter`, `-activefor`, `-inactivefor`) and state transitions
|
||||
- [x] Implement tray app UX (double-click toggle, active/inactive menu, timed menu, revert-to-parameters, about, exit)
|
||||
- [x] Implement single-instance behavior and remote app commands (`-appexit`, `-appon`, `-appoff`, `-apptoggle`, `-apptoggleshowdlg`, `-replace`)
|
||||
- [x] Implement notifications (`-notify`) and optional status dialog (`-showdlg`, `-ontaskbar`)
|
||||
- [ ] Verify script loads/parses and document limitations from non-Windows validation environment
|
||||
|
||||
## Progress Notes
|
||||
|
||||
- Initial scope confirmed from Zhorn feature list (includes v1.98-era switches like `-activeperiods` and `-notify`).
|
||||
- Implemented `caffeine.ps1` as a portable single-script app with embedded C# WinForms runtime.
|
||||
- Added single-instance IPC via hidden window + `WM_COPYDATA` for `-app*` commands and `-replace`.
|
||||
- Added conditional activation (`-watchwindow`, `-activeperiods`, `-activehours`, `-onac`, `-cpu`) and timers (`-activefor`, `-inactivefor`, `-exitafter`).
|
||||
- Added activity methods (default F15 key pulse, Shift variants/custom keys, `-stes`, `-allowss`) and tray/status UX.
|
||||
- Updated tray icons to custom coffee-cup active/inactive variants (with `-nohicon` preserving the same icon in both states).
|
||||
- Added `build-caffeine-exe.ps1` to extract embedded C# from `caffeine.ps1` and compile `dist\PortableCaffeine.exe` with `csc.exe`.
|
||||
- Added a `Main()` entry point to the embedded `PortableCaffeine.Program` class so the same source compiles directly as an EXE.
|
||||
- Added `sign-caffeine-exe.ps1` (PowerShell + embedded C# helper) to create/reuse a self-signed code-signing cert, optionally trust it (`CurrentUser`/`LocalMachine`), and sign the compiled EXE.
|
||||
- Static review patch: fixed ambiguous `Timer` type and prevented `-app*` switches from starting a new instance when no instance is running.
|
||||
|
||||
## Review
|
||||
|
||||
- Runtime verification is still pending because this environment is macOS and does not have `pwsh` installed, so I could not run a PowerShell parser check or Windows UI/API smoke test here.
|
||||
- EXE build verification is also pending in this environment (no Windows `.NET Framework` `csc.exe` available here).
|
||||
- Self-signed certificate creation/trust-store installation/signing verification is also pending in this environment (requires Windows cert stores and `Set-AuthenticodeSignature`).
|
||||
- The implementation is intended for Windows PowerShell 5.1 (STA) and relies on `System.Windows.Forms`, `user32.dll`, and `kernel32.dll`.
|
||||
Reference in New Issue
Block a user