From 895f2d2785e8df66c56ad1916edd90b40e3b6513 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Thu, 26 Feb 2026 17:37:46 +0000 Subject: [PATCH 01/60] feat: redesign for current awesome-copilot structure The awesome-copilot repository was reorganised -- chatmodes/ and prompts/ no longer exist. Resources are now split into flat categories (agents, instructions, workflows) and subdirectory-based packages (skills, hooks, plugins, cookbook). This commit updates all scripts to reflect that structure and introduces a cleaner global-vs-per-repo publishing model. New scripts: - publish-global.ps1: publishes agents to the VS Code user agents folder (via junction so sync updates are reflected immediately) and skills to ~/.copilot/skills/; supports -DryRun, -SkipAgents, -SkipSkills, -AgentsTarget, -SkillsTarget - init-repo.ps1: interactive per-repo initialiser; lets you select instructions, hooks, workflows and project-level skills from the local cache and installs them into .github/ subfolders; uses Out-GridView with a numbered console-menu fallback; supports -RepoPath, -DryRun, skip flags and pre-selection parameters Updated scripts: - sync-awesome-copilot.ps1: default categories changed to agents,instructions,workflows,hooks,skills; added Get-RepoFiles recursive traversal for subdirectory-based categories; added .sh to extension filter (required for hooks to function) - install-scheduled-task.ps1: default categories updated; -SkipCombine replaced by -SkipPublishGlobal; -IncludeCollections replaced by -IncludePlugins; second scheduled action now runs publish-global.ps1 - normalize-copilot-folders.ps1: added *.agent.md -> agents/ classification Removed: - combine-and-publish-prompts.ps1: superseded by publish-global.ps1 and init-repo.ps1 - publish-to-vscode-profile.ps1: only handled chatmodes/ and prompts/ which no longer exist in awesome-copilot Design rationale: agents and skills are global (agents available across all VS Code workspaces; skills loaded on-demand without noise); instructions/hooks/workflows are per-repo opt-in via init-repo.ps1 to avoid contradicting instruction files being active everywhere. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 25 +++ README.md | 151 +++++++++------ combine-and-publish-prompts.ps1 | 201 -------------------- init-repo.ps1 | 321 ++++++++++++++++++++++++++++++++ install-scheduled-task.ps1 | 28 +-- normalize-copilot-folders.ps1 | 11 +- publish-global.ps1 | 177 ++++++++++++++++++ publish-to-vscode-profile.ps1 | 145 --------------- sync-awesome-copilot.ps1 | 29 ++- 9 files changed, 656 insertions(+), 432 deletions(-) delete mode 100644 combine-and-publish-prompts.ps1 create mode 100644 init-repo.ps1 create mode 100644 publish-global.ps1 delete mode 100644 publish-to-vscode-profile.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 9362609..d1acb77 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,31 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.0] - 2026-02-26 + +### Added +- `publish-global.ps1` — publishes agents to the VS Code user agents folder (via junction so sync updates are reflected immediately) and skills to `~/.copilot/skills/`; supports `-DryRun`, `-SkipAgents`, `-SkipSkills`, `-AgentsTarget`, `-SkillsTarget` +- `init-repo.ps1` — interactive script to initialise a repo with per-repo resources (instructions, hooks, workflows, project-level skills); uses Out-GridView on Windows with a numbered console-menu fallback; supports `-RepoPath`, `-DryRun`, `-SkipInstructions`, `-SkipHooks`, `-SkipWorkflows`, `-SkipSkills` + +### Changed +- Updated default sync categories to match current awesome-copilot repository structure: + - **Added**: `agents`, `workflows`, `hooks`, `skills` + - **Removed**: `chatmodes`, `prompts` (no longer exist in awesome-copilot) +- Added recursive directory traversal in `sync-awesome-copilot.ps1` to support subdirectory-based categories (`skills/`, `hooks/`, `plugins/`) +- Extended file extension filter to include `.sh` files (required for hooks to function — each hook ships shell scripts alongside its `hooks.json`) +- Updated `combine-and-publish-prompts.ps1` categories from `chatmodes/instructions/prompts` to `agents/instructions/workflows`; added deprecation notice at top (superseded by `publish-global.ps1` + `init-repo.ps1`); kept for backwards compatibility +- Updated `normalize-copilot-folders.ps1` to classify `*.agent.md` → `agents/` and ensure `agents/` directory is created on normalize runs +- Updated `install-scheduled-task.ps1`: default categories now `agents,instructions,workflows,hooks,skills`; `-IncludeCollections` replaced by `-IncludePlugins`; `-SkipCombine` replaced by `-SkipPublishGlobal`; scheduled actions now run `publish-global.ps1` after sync + +### Removed +- `combine-and-publish-prompts.ps1` — superseded by `publish-global.ps1` + `init-repo.ps1` +- `publish-to-vscode-profile.ps1` — only handled `chatmodes/` and `prompts/` categories which no longer exist in awesome-copilot; use `publish-global.ps1` instead + + +- `plugins/` and `cookbook/` are available but opt-in via `-IncludePlugins` due to their size +- Hooks are synced as complete packages (README.md + hooks.json + .sh scripts) preserving their directory structure +- **Design rationale**: agents and skills are global (agents available in all VS Code workspaces; skills loaded on-demand); instructions/hooks/workflows are per-repo opt-in via `init-repo.ps1` to avoid conflicts between contradicting instruction files + ## [1.0.0] - 2025-10-20 ### Added diff --git a/README.md b/README.md index 3bb1ced..9943332 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,22 @@ A collection of PowerShell scripts to automatically sync, combine, and publish [ ## 🎯 What This Does -These scripts automate the management of VS Code Copilot custom instructions, chat modes, prompts, and collections by: +These scripts automate the management of VS Code Copilot custom agents, instructions, skills, hooks, and workflows from the [awesome-copilot](https://github.com/github/awesome-copilot) community repository: -1. **Syncing** resources from the awesome-copilot GitHub repository -2. **Combining** multiple resource categories into a unified structure -3. **Publishing** to your VS Code profile(s) via symbolic links or file copies -4. **Normalizing** file organization to prevent duplicates -5. **Automating** the entire process via Windows Task Scheduler +1. **Syncing** all resources from the awesome-copilot GitHub repository to a local cache +2. **Publishing globally** — agents to VS Code's user agents folder (available in all workspaces), skills to `~/.copilot/skills/` +3. **Initialising repos** — interactively adding instructions, hooks, workflows and project-level skills to a specific repo's `.github/` folder +4. **Automating** the sync + publish cycle via Windows Task Scheduler + +### What goes where + +| Resource | Scope | Location | +|---|---|---| +| **Agents** | 🌐 Global | VS Code user agents folder — available in Copilot Chat across all workspaces | +| **Skills** | 🌐 Global | `~/.copilot/skills/` — loaded on-demand by Copilot coding agent & CLI | +| **Instructions** | 📁 Per-repo | `.github/instructions/` — chosen via `init-repo.ps1` | +| **Hooks** | 📁 Per-repo | `.github/hooks//` — chosen via `init-repo.ps1` | +| **Workflows** | 📁 Per-repo | `.github/workflows/` — chosen via `init-repo.ps1` | ## 📋 Prerequisites @@ -29,22 +38,38 @@ git clone cd scripts ``` -### 2. Run Initial Sync +### 2. Publish Agents and Skills Globally ```powershell -# Sync resources from GitHub -.\sync-awesome-copilot.ps1 +# Publish agents to VS Code + skills to ~/.copilot/skills/ +.\publish-global.ps1 +``` + +### 3. Initialise a Repo (optional, interactive) -# Combine resources into unified folder -.\combine-and-publish-prompts.ps1 +```powershell +# Run from inside any repo to add instructions/hooks/workflows +cd C:\Projects\my-app +.\init-repo.ps1 + +# Or specify the path explicitly +.\init-repo.ps1 -RepoPath "C:\Projects\my-app" ``` -### 3. Install Automated Sync (Optional) +A selection UI will appear for each category (Out-GridView on Windows, or a numbered console menu). Items already installed in the repo are marked with `[*]`. + +### 4. Install Automated Sync (Optional) ```powershell -# Install scheduled task (runs every 4 hours by default) +# Install a scheduled task that syncs + publishes globally every 6 hours .\install-scheduled-task.ps1 +# Skip the publish-global step if you manage that manually +.\install-scheduled-task.ps1 -SkipPublishGlobal + +# Also include plugins (opt-in — large download) +.\install-scheduled-task.ps1 -IncludePlugins + # Or customize the interval .\install-scheduled-task.ps1 -Interval "2h" # Every 2 hours .\install-scheduled-task.ps1 -Interval "1d" # Once daily @@ -54,21 +79,18 @@ cd scripts ``` $HOME\.awesome-copilot\ # Local cache -├── chatmodes\ # Chat mode definitions -├── instructions\ # Custom instructions -├── prompts\ # Prompt templates -├── collections\ # Resource collections -├── combined\ # Unified resources (all categories) +├── agents\ # Custom agents (.agent.md) +├── instructions\ # Custom instructions (.instructions.md) +├── workflows\ # Agentic workflow definitions +├── hooks\ # Automated hooks (with .json + .sh scripts) +│ └── \ +├── skills\ # Skill packages +│ └── \ +│ └── SKILL.md └── manifest.json # Sync state tracking %APPDATA%\Code\User\ # VS Code global config └── prompts\ # Junction/symlink to combined folder - -%APPDATA%\Code\User\profiles\ # VS Code profiles -└── \ - ├── chatmodes\ # Linked/copied resources - ├── instructions\ - └── prompts\ ``` ## 📜 Scripts Overview @@ -88,50 +110,62 @@ Syncs resources from the awesome-copilot GitHub repository. .\sync-awesome-copilot.ps1 ``` +Syncs these categories by default: `agents`, `instructions`, `workflows`, `hooks`, `skills`. +Add `plugins` or `cookbook` explicitly via `-Categories` for those larger opt-in collections. + **Environment Variables:** - `GITHUB_TOKEN` (optional) - Personal access token for higher API rate limits --- -### `combine-and-publish-prompts.ps1` -Combines resources from all categories into a unified folder and publishes to VS Code. +### `publish-global.ps1` +Publishes agents globally to VS Code and skills to `~/.copilot/skills/`. **Features:** -- Merges chatmodes, instructions, and prompts into single directory -- Creates junction/symlink to VS Code prompts directory -- Automatic fallback to file copy if linking fails -- Preserves user-created custom files +- Creates a junction/symlink from VS Code's user agents folder to the local cache (no re-running needed after each sync) +- Incrementally copies skills to `~/.copilot/skills/` +- Dry-run mode for previewing changes +- Individual skip flags for each resource type **Usage:** ```powershell -.\combine-and-publish-prompts.ps1 +.\publish-global.ps1 -# Publish to specific profile -.\combine-and-publish-prompts.ps1 -ProfileName "MyProfile" +# Preview changes without applying +.\publish-global.ps1 -DryRun -# Publish to global VS Code config only -.\combine-and-publish-prompts.ps1 -GlobalOnly +# Skills only (agents already published) +.\publish-global.ps1 -SkipAgents + +# Custom target path (e.g. named VS Code profile) +.\publish-global.ps1 -AgentsTarget "$env:APPDATA\Code\User\profiles\Work\agents" ``` --- -### `publish-to-vscode-profile.ps1` -Publishes resources to VS Code profile(s) via symbolic links or copies. +### `init-repo.ps1` +Interactively initialises a repository with instructions, hooks, workflows, and project-level skills. **Features:** -- Creates symbolic links (junctions) for efficient syncing -- Automatic fallback to file copy -- Supports multiple profiles or global config +- Presents available resources from the local cache in a selection UI (Out-GridView on Windows) +- Falls back to a numbered console menu where Out-GridView is unavailable +- Copies selected items to the correct `.github/` subfolder +- Marks already-installed items so you can see what's new +- Dry-run mode for previewing **Usage:** ```powershell -.\publish-to-vscode-profile.ps1 +# Run inside a repo (uses current directory) +.\init-repo.ps1 + +# Target a specific repo +.\init-repo.ps1 -RepoPath "C:\Projects\my-app" -# Publish to specific profile -.\publish-to-vscode-profile.ps1 -ProfileName "Work" +# Preview without writing any files +.\init-repo.ps1 -DryRun -# Publish to global config -.\publish-to-vscode-profile.ps1 -GlobalOnly +# Skip categories you don't need +.\init-repo.ps1 -SkipHooks -SkipWorkflows ``` --- @@ -155,35 +189,29 @@ Cleans up misplaced or duplicated files in VS Code directories. --- ### `install-scheduled-task.ps1` -Creates a Windows scheduled task for automatic syncing. +Creates a Windows scheduled task for automatic syncing and global publishing. **Features:** -- Runs sync and combine scripts on a schedule -- Default: every 4 hours +- Runs `sync-awesome-copilot.ps1` then `publish-global.ps1` on a schedule +- Default: every 6 hours - Customizable interval -- Runs as current user (no SYSTEM account needed) **Usage:** ```powershell -# Install with default 4-hour interval +# Install with defaults (sync + publish-global every 6 hours) .\install-scheduled-task.ps1 # Custom intervals .\install-scheduled-task.ps1 -Interval "2h" # Every 2 hours -.\install-scheduled-task.ps1 -Interval "30m" # Every 30 minutes .\install-scheduled-task.ps1 -Interval "1d" # Once daily +# Sync only (skip publish-global) +.\install-scheduled-task.ps1 -SkipPublishGlobal + # Check task status Get-ScheduledTask -TaskName "AwesomeCopilotSync" ``` -**Interval Format:** -- `30m` - Minutes -- `2h` - Hours -- `1d` - Days - ---- - ### `uninstall-scheduled-task.ps1` Removes the scheduled task. @@ -222,12 +250,13 @@ $Repo = "your-username/your-repo" Resources follow naming patterns for automatic categorization: -- `*.chatmode.md` - Chat mode definitions +- `*.agent.md` - Custom agents - `*.instructions.md` - Custom instructions -- `*.prompt.md` - Prompt templates -- `*.collection.yml` - Resource collections +- `*.chatmode.md` - Chat mode definitions (legacy) +- `*.prompt.md` - Prompt templates (legacy) Files without these suffixes in the combined folder are preserved (assumed to be user-created). +Skills and hooks are directory-based packages and are not combined into the prompts folder. ## 🛠️ Troubleshooting diff --git a/combine-and-publish-prompts.ps1 b/combine-and-publish-prompts.ps1 deleted file mode 100644 index 47ade10..0000000 --- a/combine-and-publish-prompts.ps1 +++ /dev/null @@ -1,201 +0,0 @@ -<# -Combine Copilot Resources Into Unified 'combined' Folder And Publish To Prompts - -CHANGE (2025-09-18): Default publish target is now the global prompts directory: - $env:APPDATA/Code/User/prompts -Previously the script auto-selected the latest profile under profiles/. That -behavior is still available via -UseLatestProfile. Supplying -AllProfiles keeps -publishing to every discovered profile prompts directory (unchanged). - -Steps Implemented: - 1. (Assumes sync already ran) Validate presence of source category folders under root (default $HOME/.awesome-copilot) - 2. Create / refresh a 'combined' folder containing all *.md files from: - chatmodes/, instructions/, prompts/ (collections optional via -IncludeCollections) - - Name collisions resolved by prefixing category (unless identical content) - - Preserves original filenames when unique - 3. Publish the combined folder: - a) By default to global prompts directory (profile-agnostic) - b) Or if -UseLatestProfile, to the latest profile's prompts - c) Or if -AllProfiles, to each profile's prompts - d) Attempt symbolic link (prompts -> combined) then junction then copy (or copy directly with -ForceCopy) - -Result: All items visible under the chosen prompts directory, enabling unified browsing. - -Usage Examples: - Dry run only: - pwsh -File .\scripts\combine-and-publish-prompts.ps1 -DryRun - Publish to global prompts (default): - pwsh -File .\scripts\combine-and-publish-prompts.ps1 - Publish to latest profile prompts (legacy behavior): - pwsh -File .\scripts\combine-and-publish-prompts.ps1 -UseLatestProfile - Publish to all profiles forcing copy: - pwsh -File .\scripts\combine-and-publish-prompts.ps1 -AllProfiles -ForceCopy - Rebuild combined only (no publish): - pwsh -File .\scripts\combine-and-publish-prompts.ps1 -NoPublish - -Flags: - -SourceRoot - -ProfilesBase - -ProfileRoot (explicit profile root; overrides -UseLatestProfile) - -AllProfiles - -UseLatestProfile (restore previous default targeting most recent profile) - -ForceCopy (skip link attempts) - -DryRun (show plan, do not modify) - -NoPublish (build combined set only) - -Prune (remove stale files from existing combined folder before rebuild) - -IncludeCollections (opt-in: also merge markdown files from collections/) - -License: Adapt freely. -#> -[CmdletBinding()] param( - [string]$SourceRoot = "$HOME/.awesome-copilot", - [string]$ProfilesBase = (Join-Path $env:APPDATA 'Code/User/profiles'), - [string]$ProfileRoot, - [switch]$UseLatestProfile, - [switch]$AllProfiles, - [switch]$ForceCopy, - [switch]$DryRun, - [switch]$NoPublish, - [switch]$Prune, - [switch]$IncludeCollections -) - -$ErrorActionPreference = 'Stop' - -function Log($m, [string]$level = 'INFO') { $ts = (Get-Date).ToString('s'); Write-Host "[$ts][$level] $m" -ForegroundColor $(if ($level -eq 'ERROR') { 'Red' } elseif ($level -eq 'WARN') { 'Yellow' } else { 'Cyan' }) } - -if (-not (Test-Path $SourceRoot)) { Log "Source root missing: $SourceRoot" 'ERROR'; exit 1 } -if (-not (Test-Path $ProfilesBase)) { Log "Profiles base missing: $ProfilesBase" 'WARN' } - -$categories = @('chatmodes', 'instructions', 'prompts') -if ($IncludeCollections) { $categories += 'collections' } -$missing = @() -foreach ($c in $categories) { if (-not (Test-Path (Join-Path $SourceRoot $c))) { $missing += $c } } -if ($missing) { Log "Missing category folders: $($missing -join ', ')" 'WARN' } - -# Determine publish targets. We now default to global prompts unless an explicit -# profile strategy is selected. -$globalPrompts = Join-Path (Join-Path $env:APPDATA 'Code/User') 'prompts' -$targetMode = 'Global' -$targets = @() - -if ($AllProfiles) { - $targetMode = 'AllProfiles' - $targets = Get-ChildItem $ProfilesBase -Directory | ForEach-Object { Join-Path $_.FullName 'prompts' } - if (-not $targets) { Log 'No profiles discovered for -AllProfiles.' 'ERROR'; exit 1 } - Log "Will publish to $($targets.Count) profile prompt directories" 'INFO' -} -elseif ($ProfileRoot) { - $targetMode = 'ExplicitProfile' - $targets = @(Join-Path $ProfileRoot 'prompts') - Log "Explicit profile root provided: $ProfileRoot" 'INFO' -} -elseif ($UseLatestProfile) { - $targetMode = 'LatestProfile' - $latest = if (Test-Path $ProfilesBase) { Get-ChildItem $ProfilesBase -Directory | Sort-Object LastWriteTime -Descending | Select-Object -First 1 } else { $null } - if (-not $latest) { Log 'No profiles found for -UseLatestProfile.' 'ERROR'; exit 1 } - $targets = @(Join-Path $latest.FullName 'prompts') - Log "Using latest profile: $($latest.FullName)" 'INFO' -} -else { - $targetMode = 'Global' - $targets = @($globalPrompts) - Log "Defaulting to global prompts directory: $globalPrompts" 'INFO' -} - -Log "Publish mode: $targetMode" 'INFO' - -$combinedRoot = Join-Path $SourceRoot 'combined' - -# Ensure combined directory exists -if (-not (Test-Path $combinedRoot)) { - if ($DryRun) { Log "[DryRun] Would create combined folder: $combinedRoot" 'INFO' } else { New-Item -ItemType Directory -Path $combinedRoot | Out-Null } -} - -# Map of dest relative name -> full path already added (for duplicate detection) -$index = @{} -$added = 0 -$skippedSame = 0 -$renamed = 0 - -foreach ($cat in $categories) { - $srcDir = Join-Path $SourceRoot $cat - if (-not (Test-Path $srcDir)) { continue } - $files = Get-ChildItem $srcDir -File -Filter '*.md' - foreach ($f in $files) { - $destName = $f.Name - $destPath = Join-Path $combinedRoot $destName - if (Test-Path $destPath) { - # Compare content hash; if identical skip, else replace with latest version - $existingHash = (Get-FileHash -Algorithm SHA256 $destPath).Hash - $newHash = (Get-FileHash -Algorithm SHA256 $f.FullName).Hash - if ($existingHash -eq $newHash) { - $skippedSame++ - continue - } - # Different content - latest version wins, replace the file - Log "Name collision: replacing $destName with latest from $cat" 'INFO' - $renamed++ - } - if ($DryRun) { - Log "[DryRun] Add $cat -> $destName" 'INFO' - } - else { - Copy-Item $f.FullName $destPath -Force - } - $added++ - } -} - -Log "Combined summary: added=$added identicalSkipped=$skippedSame renamed=$renamed" 'INFO' - -if ($NoPublish) { Log "NoPublish set; skipping linking/copy phase." 'INFO'; exit 0 } - -function Publish-ToPrompts($promptsDir) { - if (-not (Test-Path $promptsDir)) { - if ($DryRun) { Log "[DryRun] Would create prompts dir $promptsDir" } - else { New-Item -ItemType Directory -Path $promptsDir | Out-Null } - } - $canLink = $true - if (Test-Path $promptsDir) { - $item = Get-Item $promptsDir -Force - $isLink = ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) - if ($isLink) { - Log "prompts already linked: $promptsDir" 'INFO' - return - } - # If prompts directory exists as normal folder, we'll update it in place - # User-created files will be preserved, synced files will be updated - $nonHidden = Get-ChildItem $promptsDir -Force | Where-Object { -not $_.Attributes.ToString().Contains('Hidden') } - if ($nonHidden) { - Log "prompts exists as normal directory; will update synced files in place" 'INFO' - } - } - if ($ForceCopy -or -not $canLink) { - Log "Copying combined contents into prompts ($promptsDir)" 'INFO' - if (-not $DryRun) { Copy-Item (Join-Path $combinedRoot '*') $promptsDir -Recurse -Force } - return - } - if ($DryRun) { Log "[DryRun] Would replace prompts with link/junction to combined" 'INFO'; return } - try { - Remove-Item $promptsDir -Recurse -Force -ErrorAction Stop - New-Item -ItemType SymbolicLink -Path $promptsDir -Target $combinedRoot -Force | Out-Null - Log "Created symlink prompts -> combined" 'INFO' - } - catch { - Log "Symlink failed: $($_.Exception.Message) (attempt junction)" 'WARN' - try { - cmd /c mklink /J "$promptsDir" "$combinedRoot" | Out-Null - Log "Created junction prompts -> combined" 'INFO' - } - catch { - Log "Junction failed; copying fallback" 'WARN' - New-Item -ItemType Directory -Path $promptsDir -Force | Out-Null - Copy-Item (Join-Path $combinedRoot '*') $promptsDir -Recurse -Force - } - } -} - -foreach ($t in $targets) { Publish-ToPrompts -promptsDir $t } - -Log "Combine & publish complete." 'INFO' diff --git a/init-repo.ps1 b/init-repo.ps1 new file mode 100644 index 0000000..e4b5945 --- /dev/null +++ b/init-repo.ps1 @@ -0,0 +1,321 @@ +<# +Initialize a Repository with Copilot Resources + +Interactively selects and installs non-global Copilot resources from the local +awesome-copilot cache into a target repository's .github/ folder. + +Resources installed here are project-specific (opt-in) rather than global: + + Instructions --> .github/instructions/*.instructions.md + Hooks --> .github/hooks// (full directory) + Workflows --> .github/workflows/*.md + Skills --> .github/skills// (project-level skills) + +Usage: + # Interactive - run from within the target repo + .\init-repo.ps1 + + # Specify a repo path explicitly + .\init-repo.ps1 -RepoPath "C:\Projects\my-app" + + # Skip specific categories + .\init-repo.ps1 -SkipHooks -SkipWorkflows + + # Non-interactive: specify items by name (comma-separated) + .\init-repo.ps1 -Instructions "angular,dotnet-framework" -Hooks "session-logger" + + # Dry run - show what would be installed + .\init-repo.ps1 -DryRun + +Notes: + - Existing files are only overwritten if the source is newer/different. + - .github/ is created if it doesn't exist. + - This script does NOT touch global resources (agents, personal skills). + Use publish-global.ps1 for those. + - The selection UI uses Out-GridView where available (Windows GUI, filterable, + multi-select). Falls back to a numbered console menu automatically. +#> +[CmdletBinding()] param( + [string]$RepoPath = (Get-Location).Path, + [string]$SourceRoot = "$HOME/.awesome-copilot", + [string]$Instructions = '', # Comma-separated names to pre-select (non-interactive) + [string]$Hooks = '', + [string]$Workflows = '', + [string]$Skills = '', + [switch]$SkipInstructions, + [switch]$SkipHooks, + [switch]$SkipWorkflows, + [switch]$SkipSkills, + [switch]$DryRun +) + +$ErrorActionPreference = 'Stop' + +function Log($m, [string]$level = 'INFO') { + $ts = (Get-Date).ToString('s') + $color = switch ($level) { 'ERROR' { 'Red' } 'WARN' { 'Yellow' } 'SUCCESS' { 'Green' } default { 'Cyan' } } + Write-Host "[$ts][$level] $m" -ForegroundColor $color +} + +# --------------------------------------------------------------------------- +# Validate paths +# --------------------------------------------------------------------------- +if (-not (Test-Path $RepoPath)) { + Log "Repo path not found: $RepoPath" 'ERROR'; exit 1 +} +$RepoPath = Resolve-Path $RepoPath | Select-Object -ExpandProperty Path + +if (-not (Test-Path $SourceRoot)) { + Log "Cache not found: $SourceRoot -- run sync-awesome-copilot.ps1 first" 'ERROR'; exit 1 +} + +$GithubDir = Join-Path $RepoPath '.github' +Log "Target repo : $RepoPath" +Log "Copilot cache: $SourceRoot" + +# --------------------------------------------------------------------------- +# Helper: interactive picker +# Returns array of selected names. +# preSelected: comma-separated list for non-interactive mode. +# --------------------------------------------------------------------------- +function Select-Items { + param( + [string]$Category, + [object[]]$Items, # objects with Name, Description, AlreadyInstalled + [string]$PreSelected = '' + ) + + if ($Items.Count -eq 0) { + Log "No $Category found in cache." 'WARN' + return @() + } + + # Non-interactive mode: pre-selection provided + if ($PreSelected) { + $names = $PreSelected.Split(',') | ForEach-Object { $_.Trim() } | Where-Object { $_ } + $selected = $Items | Where-Object { $names -contains $_.Name } + if (-not $selected) { Log "No matching $Category items for: $PreSelected" 'WARN' } + return @($selected) + } + + Write-Host "" + Write-Host " === $Category ===" -ForegroundColor Yellow + Write-Host " Already installed items are marked with [*]" -ForegroundColor DarkGray + + # Try Out-GridView (Windows GUI - filterable, multi-select) + $ogvAvailable = $false + try { Get-Command Out-GridView -ErrorAction Stop | Out-Null; $ogvAvailable = $true } catch {} + + if ($ogvAvailable) { + $display = $Items | Select-Object ` + @{ N='[*] Installed'; E={ if ($_.AlreadyInstalled) { 'YES' } else { '' } } }, + @{ N='Name'; E={ $_.Name } }, + @{ N='Description'; E={ $_.Description } } + + $picked = $display | Out-GridView -Title "Select $Category to install (multi-select with Ctrl/Shift, then OK)" -PassThru + if (-not $picked) { return @() } + $pickedNames = @($picked | ForEach-Object { $_.'Name' }) + return @($Items | Where-Object { $pickedNames -contains $_.Name }) + } + + # Fallback: numbered console menu + Write-Host "" + for ($i = 0; $i -lt $Items.Count; $i++) { + $mark = if ($Items[$i].AlreadyInstalled) { '[*]' } else { ' ' } + Write-Host (" {0,3}. {1} {2}" -f ($i+1), $mark, $Items[$i].Name) -ForegroundColor $(if ($Items[$i].AlreadyInstalled) { 'DarkCyan' } else { 'White' }) + if ($Items[$i].Description) { + Write-Host (" {0}" -f $Items[$i].Description) -ForegroundColor DarkGray + } + } + Write-Host "" + Write-Host " Enter numbers to install (e.g. 1,3,5 or 1-3 or 'all' or blank to skip): " -NoNewline -ForegroundColor Yellow + $input = Read-Host + + if (-not $input -or $input.Trim() -eq '') { return @() } + if ($input.Trim() -eq 'all') { return $Items } + + $indices = @() + foreach ($part in $input.Split(',')) { + $part = $part.Trim() + if ($part -match '^(\d+)-(\d+)$') { + $indices += ([int]$Matches[1])..[int]$Matches[2] + } elseif ($part -match '^\d+$') { + $indices += [int]$part + } + } + return @($Items | Where-Object { $indices -contains ($Items.IndexOf($_) + 1) }) +} + +# --------------------------------------------------------------------------- +# Helper: copy a single flat file to a target directory +# --------------------------------------------------------------------------- +function Install-File { + param([string]$Src, [string]$DestDir) + if (-not $DryRun -and -not (Test-Path $DestDir)) { + New-Item -ItemType Directory -Path $DestDir -Force | Out-Null + } + $dest = Join-Path $DestDir (Split-Path $Src -Leaf) + $srcHash = (Get-FileHash $Src -Algorithm SHA256).Hash + $dstHash = if (Test-Path $dest) { (Get-FileHash $dest -Algorithm SHA256).Hash } else { $null } + if ($srcHash -eq $dstHash) { return 'unchanged' } + if ($DryRun) { return 'would-copy' } + Copy-Item $Src $dest -Force + return if ($dstHash) { 'updated' } else { 'added' } +} + +# --------------------------------------------------------------------------- +# Helper: copy an entire subdirectory (for hooks and skills) +# --------------------------------------------------------------------------- +function Install-Directory { + param([string]$SrcDir, [string]$DestParent) + $name = Split-Path $SrcDir -Leaf + $destDir = Join-Path $DestParent $name + if (-not $DryRun -and -not (Test-Path $destDir)) { + New-Item -ItemType Directory -Path $destDir -Force | Out-Null + } + $added = 0; $updated = 0; $unchanged = 0 + Get-ChildItem $SrcDir -File -Recurse | ForEach-Object { + $rel = $_.FullName.Substring($SrcDir.Length).TrimStart('\','/') + $dest = Join-Path $destDir $rel + $destDir2 = Split-Path $dest -Parent + if (-not $DryRun -and -not (Test-Path $destDir2)) { + New-Item -ItemType Directory -Path $destDir2 -Force | Out-Null + } + $srcHash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash + $dstHash = if (Test-Path $dest) { (Get-FileHash $dest -Algorithm SHA256).Hash } else { $null } + if ($srcHash -ne $dstHash) { + if (-not $DryRun) { Copy-Item $_.FullName $dest -Force } + if ($dstHash) { $updated++ } else { $added++ } + } else { $unchanged++ } + } + return [pscustomobject]@{ Added = $added; Updated = $updated; Unchanged = $unchanged } +} + +# --------------------------------------------------------------------------- +# Helper: read description from a file's frontmatter or first heading +# --------------------------------------------------------------------------- +function Get-Description([string]$FilePath) { + try { + $lines = Get-Content $FilePath -TotalCount 20 -ErrorAction SilentlyContinue + # YAML frontmatter description field + $inFrontmatter = $false + foreach ($line in $lines) { + if ($line -eq '---') { $inFrontmatter = -not $inFrontmatter; continue } + if ($inFrontmatter -and $line -match '^description:\s*(.+)') { return $Matches[1].Trim('"''') } + } + # First non-heading markdown line + foreach ($line in $lines) { + if ($line -match '^#{1,3}\s+(.+)') { return $Matches[1] } + } + } catch {} + return '' +} + +# --------------------------------------------------------------------------- +# Build catalogue entries for each category +# --------------------------------------------------------------------------- +$totalInstalled = 0 + +function Build-FlatCatalogue([string]$CatDir, [string]$DestDir, [string]$Pattern) { + if (-not (Test-Path $CatDir)) { return @() } + Get-ChildItem $CatDir -File | Where-Object { $_.Name -match $Pattern } | ForEach-Object { + $destFile = Join-Path $DestDir $_.Name + [pscustomobject]@{ + Name = [System.IO.Path]::GetFileNameWithoutExtension($_.Name) -replace '\.(instructions|agent|prompt|chatmode)$','' + FileName = $_.Name + FullPath = $_.FullName + Description = Get-Description $_.FullName + AlreadyInstalled = (Test-Path $destFile) + } + } | Sort-Object Name +} + +function Build-DirCatalogue([string]$CatDir, [string]$DestDir) { + if (-not (Test-Path $CatDir)) { return @() } + Get-ChildItem $CatDir -Directory | ForEach-Object { + $destSubdir = Join-Path $DestDir $_.Name + $readmePath = Join-Path $_.FullName 'README.md' + if (-not (Test-Path $readmePath)) { $readmePath = Join-Path $_.FullName 'SKILL.md' } + [pscustomobject]@{ + Name = $_.Name + FullPath = $_.FullName + Description = if (Test-Path $readmePath) { Get-Description $readmePath } else { '' } + AlreadyInstalled = (Test-Path $destSubdir) + } + } | Sort-Object Name +} + +# --------------------------------------------------------------------------- +# INSTRUCTIONS +# --------------------------------------------------------------------------- +if (-not $SkipInstructions) { + $destDir = Join-Path $GithubDir 'instructions' + $catalogue = Build-FlatCatalogue (Join-Path $SourceRoot 'instructions') $destDir '\.instructions\.md$' + $selected = Select-Items -Category 'Instructions' -Items $catalogue -PreSelected $Instructions + + foreach ($item in $selected) { + $result = Install-File -Src $item.FullPath -DestDir $destDir + $verb = switch ($result) { 'added' { '✓ Added' } 'updated' { '↑ Updated' } 'unchanged' { '= Unchanged' } default { '~ DryRun' } } + Log "$verb instructions: $($item.FileName)" + if ($result -in 'added','updated','would-copy') { $totalInstalled++ } + } +} + +# --------------------------------------------------------------------------- +# HOOKS +# --------------------------------------------------------------------------- +if (-not $SkipHooks) { + $destDir = Join-Path $GithubDir 'hooks' + $catalogue = Build-DirCatalogue (Join-Path $SourceRoot 'hooks') $destDir + $selected = Select-Items -Category 'Hooks' -Items $catalogue -PreSelected $Hooks + + foreach ($item in $selected) { + $r = Install-Directory -SrcDir $item.FullPath -DestParent $destDir + $verb = if ($DryRun) { '~ DryRun' } else { '✓ Installed' } + Log "$verb hook: $($item.Name) (added=$($r.Added) updated=$($r.Updated) unchanged=$($r.Unchanged))" + if (-not $DryRun) { $totalInstalled++ } + } +} + +# --------------------------------------------------------------------------- +# WORKFLOWS +# --------------------------------------------------------------------------- +if (-not $SkipWorkflows) { + $destDir = Join-Path $GithubDir 'workflows' + $catalogue = Build-FlatCatalogue (Join-Path $SourceRoot 'workflows') $destDir '\.md$' + $selected = Select-Items -Category 'Agentic Workflows' -Items $catalogue -PreSelected $Workflows + + foreach ($item in $selected) { + $result = Install-File -Src $item.FullPath -DestDir $destDir + $verb = switch ($result) { 'added' { '✓ Added' } 'updated' { '↑ Updated' } 'unchanged' { '= Unchanged' } default { '~ DryRun' } } + Log "$verb workflow: $($item.FileName)" + if ($result -in 'added','updated','would-copy') { $totalInstalled++ } + } +} + +# --------------------------------------------------------------------------- +# SKILLS (project-level) +# --------------------------------------------------------------------------- +if (-not $SkipSkills) { + $destDir = Join-Path $GithubDir 'skills' + $catalogue = Build-DirCatalogue (Join-Path $SourceRoot 'skills') $destDir + $selected = Select-Items -Category 'Skills (project-level)' -Items $catalogue -PreSelected $Skills + + foreach ($item in $selected) { + $r = Install-Directory -SrcDir $item.FullPath -DestParent $destDir + $verb = if ($DryRun) { '~ DryRun' } else { '✓ Installed' } + Log "$verb skill: $($item.Name) (added=$($r.Added) updated=$($r.Updated) unchanged=$($r.Unchanged))" + if (-not $DryRun) { $totalInstalled++ } + } +} + +# --------------------------------------------------------------------------- +# Summary +# --------------------------------------------------------------------------- +Write-Host "" +if ($DryRun) { + Log "Dry run complete. Re-run without -DryRun to apply." 'WARN' +} else { + Log "$totalInstalled resource(s) installed/updated in $GithubDir" 'SUCCESS' + Log "Tip: commit .github/ to share these with your team." +} diff --git a/install-scheduled-task.ps1 b/install-scheduled-task.ps1 index d17fc65..883d0dd 100644 --- a/install-scheduled-task.ps1 +++ b/install-scheduled-task.ps1 @@ -2,20 +2,20 @@ [string]$TaskName = 'AwesomeCopilotSync', [string]$Every = '4h', [string]$Dest = "$HOME/.awesome-copilot", - # Default categories now exclude 'collections' (can be re-enabled with -IncludeCollections) - [string]$Categories = 'chatmodes,instructions,prompts', - [switch]$IncludeCollections, - # Allow skipping the combine/publish step if user only wants raw sync - [switch]$SkipCombine, + # Default categories aligned with current awesome-copilot structure + [string]$Categories = 'agents,instructions,workflows,hooks,skills', + [switch]$IncludePlugins, + # Allow skipping the global publish step if user only wants raw sync + [switch]$SkipPublishGlobal, [string]$PwshPath = (Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source), [string]$ScriptPath = (Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) 'sync-awesome-copilot.ps1'), - [string]$CombineScriptPath = (Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) 'combine-and-publish-prompts.ps1'), + [string]$PublishGlobalScriptPath = (Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) 'publish-global.ps1'), [switch]$Force ) if (-not $PwshPath) { $PwshPath = (Get-Command powershell | Select-Object -ExpandProperty Source) } if (-not (Test-Path $ScriptPath)) { throw "Sync script not found at $ScriptPath" } -if (-not $SkipCombine -and -not (Test-Path $CombineScriptPath)) { throw "Combine script not found at $CombineScriptPath (use -SkipCombine to suppress)" } +if (-not $SkipPublishGlobal -and -not (Test-Path $PublishGlobalScriptPath)) { throw "Publish-global script not found at $PublishGlobalScriptPath (use -SkipPublishGlobal to suppress)" } function Parse-Interval($spec) { if ($spec -match '^(\d+)([hm])$') { @@ -28,8 +28,8 @@ function Parse-Interval($spec) { throw "Unsupported interval spec: $spec (use like 4h or 30m)" } -if ($IncludeCollections -and ($Categories -notmatch 'collections')) { - $Categories = ($Categories.TrimEnd(',') + ',collections') +if ($IncludePlugins -and ($Categories -notmatch 'plugins')) { + $Categories = ($Categories.TrimEnd(',') + ',plugins') } $int = Parse-Interval $Every @@ -39,10 +39,10 @@ $syncArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`" -Dest `"$D $actions = @() $actions += New-ScheduledTaskAction -Execute $PwshPath -Argument $syncArgs -if (-not $SkipCombine) { - # Combine script runs after sync; no need for -DryRun. Include collections only if requested. - $combineArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$CombineScriptPath`" -SourceRoot `"$Dest`"" + $(if ($IncludeCollections) { ' -IncludeCollections' } else { '' }) - $actions += New-ScheduledTaskAction -Execute $PwshPath -Argument $combineArgs +if (-not $SkipPublishGlobal) { + # publish-global runs after sync: updates VS Code agents folder and ~/.copilot/skills/ + $publishArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$PublishGlobalScriptPath`" -SourceRoot `"$Dest`" -Quiet" + $actions += New-ScheduledTaskAction -Execute $PwshPath -Argument $publishArgs } $Trigger = if ($int.Type -eq 'HOURLY') { New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval ([TimeSpan]::FromHours($int.Modifier)) -RepetitionDuration ([TimeSpan]::FromDays(3650)) } else { New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval ([TimeSpan]::FromMinutes($int.Modifier)) -RepetitionDuration ([TimeSpan]::FromDays(3650)) } $Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 20) @@ -54,5 +54,5 @@ if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) { Register-ScheduledTask -TaskName $TaskName -Action $actions -Trigger $Trigger -Settings $Settings -Description "Sync (and combine) Awesome Copilot resources" | Out-Null -$post = if ($SkipCombine) { 'sync only' } else { 'sync + combine/publish' } +$post = if ($SkipPublishGlobal) { 'sync only' } else { 'sync + publish-global' } Write-Host "Scheduled task '$TaskName' created ($post). First run in ~1 minute, then every $Every." -ForegroundColor Green diff --git a/normalize-copilot-folders.ps1 b/normalize-copilot-folders.ps1 index 195154b..5934987 100644 --- a/normalize-copilot-folders.ps1 +++ b/normalize-copilot-folders.ps1 @@ -71,17 +71,18 @@ $correct = 0 function Classify([string]$fileName) { switch -regex ($fileName) { - '\\.chatmode\.md$' { return 'chatmodes' } - '\\.instructions\.md$' { return 'instructions' } - '\\.prompt\.md$' { return 'prompts' } - '\\.(collection|collections)\.md$' { return 'collections' } + '\.agent\.md$' { return 'agents' } + '\.chatmode\.md$' { return 'chatmodes' } + '\.instructions\.md$' { return 'instructions' } + '\.prompt\.md$' { return 'prompts' } + '\.(collection|collections)\.md$' { return 'collections' } default { return $null } } } foreach ($p in $targets) { Log "Scanning profile: $p" 'INFO' - $expected = 'chatmodes', 'instructions', 'prompts', 'collections' + $expected = 'agents', 'chatmodes', 'instructions', 'prompts', 'collections' foreach ($dir in $expected) { $full = Join-Path $p $dir; if (-not (Test-Path $full)) { New-Item -ItemType Directory -Path $full | Out-Null } } # Consider: any .md file in profile tree at depth 0..2 diff --git a/publish-global.ps1 b/publish-global.ps1 new file mode 100644 index 0000000..33e358f --- /dev/null +++ b/publish-global.ps1 @@ -0,0 +1,177 @@ +<# +Publish Global Copilot Resources + +Publishes two categories of resources from the local awesome-copilot cache to +global locations where they are always available across all workspaces/repos: + + Agents --> VS Code user agents folder (available in Copilot Chat globally) + Default: %APPDATA%\Code\User\agents\ + Strategy: symlink / junction first, then file-copy fallback + + Skills --> Personal skills directory (loaded on-demand by CCA / Copilot CLI) + Default: ~\.copilot\skills\ + Strategy: mirror each skill subdirectory (incremental copy) + +Usage: + # Publish both agents and skills (default) + .\publish-global.ps1 + + # Publish only agents + .\publish-global.ps1 -SkipSkills + + # Publish only skills + .\publish-global.ps1 -SkipAgents + + # Override VS Code agents folder (e.g. for a named profile) + .\publish-global.ps1 -AgentsTarget "$env:APPDATA\Code\User\profiles\MyProfile\agents" + + # Dry run - show what would happen + .\publish-global.ps1 -DryRun + +Notes: + - Agents are linked (not copied) where possible so that sync updates are + immediately reflected in VS Code without re-running this script. + - Skills are copied individually so each skill directory is self-contained + under ~/.copilot/skills//. + - Run after sync-awesome-copilot.ps1, or add to the scheduled task via + install-scheduled-task.ps1. +#> +[CmdletBinding()] param( + [string]$SourceRoot = "$HOME/.awesome-copilot", + [string]$AgentsTarget = (Join-Path $env:APPDATA 'Code\User\agents'), + [string]$SkillsTarget = (Join-Path $HOME '.copilot\skills'), + [switch]$SkipAgents, + [switch]$SkipSkills, + [switch]$DryRun +) + +$ErrorActionPreference = 'Stop' + +function Log($m, [string]$level = 'INFO') { + $ts = (Get-Date).ToString('s') + $color = switch ($level) { 'ERROR' { 'Red' } 'WARN' { 'Yellow' } default { 'Cyan' } } + Write-Host "[$ts][$level] $m" -ForegroundColor $color +} + +$AgentsSource = Join-Path $SourceRoot 'agents' +$SkillsSource = Join-Path $SourceRoot 'skills' + +# --------------------------------------------------------------------------- +# AGENTS +# --------------------------------------------------------------------------- +if (-not $SkipAgents) { + if (-not (Test-Path $AgentsSource)) { + Log "Agents source not found: $AgentsSource (run sync-awesome-copilot.ps1 first)" 'WARN' + } + else { + Log "Publishing agents: $AgentsSource --> $AgentsTarget" + + if ($DryRun) { + Log "[DryRun] Would link/copy agents folder to $AgentsTarget" + } + else { + # Attempt junction first (no elevation required on Windows), then symlink, then copy + $linked = $false + + if (Test-Path $AgentsTarget) { + $item = Get-Item $AgentsTarget -Force + if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { + Log "Agents already linked at $AgentsTarget - skipping" + $linked = $true + } + else { + # Exists as a real directory - update files in place rather than replacing + Log "Agents folder exists as real directory; updating files in place" + Get-ChildItem $AgentsSource -File | ForEach-Object { + $dest = Join-Path $AgentsTarget $_.Name + $srcHash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash + $dstHash = if (Test-Path $dest) { (Get-FileHash $dest -Algorithm SHA256).Hash } else { $null } + if ($srcHash -ne $dstHash) { + Copy-Item $_.FullName $dest -Force + Log " Updated: $($_.Name)" + } + } + $linked = $true + } + } + + if (-not $linked) { + $parent = Split-Path $AgentsTarget -Parent + if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null } + + try { + cmd /c mklink /J `"$AgentsTarget`" `"$AgentsSource`" | Out-Null + Log "Created junction: $AgentsTarget --> $AgentsSource" + } + catch { + Log "Junction failed ($($_.Exception.Message)); trying symlink" 'WARN' + try { + New-Item -ItemType SymbolicLink -Path $AgentsTarget -Target $AgentsSource -Force | Out-Null + Log "Created symlink: $AgentsTarget --> $AgentsSource" + } + catch { + Log "Symlink failed; copying files instead" 'WARN' + New-Item -ItemType Directory -Path $AgentsTarget -Force | Out-Null + Copy-Item (Join-Path $AgentsSource '*') $AgentsTarget -Force + Log "Copied agents to $AgentsTarget" + } + } + } + } + Log "Agents: done. Restart VS Code if agents do not appear immediately." + } +} + +# --------------------------------------------------------------------------- +# SKILLS +# --------------------------------------------------------------------------- +if (-not $SkipSkills) { + if (-not (Test-Path $SkillsSource)) { + Log "Skills source not found: $SkillsSource (run sync-awesome-copilot.ps1 first)" 'WARN' + } + else { + Log "Publishing skills: $SkillsSource --> $SkillsTarget" + + if (-not $DryRun -and -not (Test-Path $SkillsTarget)) { + New-Item -ItemType Directory -Path $SkillsTarget -Force | Out-Null + } + + $added = 0; $updated = 0; $unchanged = 0 + + Get-ChildItem $SkillsSource -Directory | ForEach-Object { + $skillName = $_.Name + $skillSrc = $_.FullName + $skillDest = Join-Path $SkillsTarget $skillName + + if ($DryRun) { + Log "[DryRun] Would publish skill: $skillName" + $added++ + return + } + + if (-not (Test-Path $skillDest)) { + New-Item -ItemType Directory -Path $skillDest -Force | Out-Null + } + + Get-ChildItem $skillSrc -File -Recurse | ForEach-Object { + $rel = $_.FullName.Substring($skillSrc.Length).TrimStart('\','/') + $dest = Join-Path $skillDest $rel + $destDir = Split-Path $dest -Parent + if (-not (Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null } + + $srcHash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash + $dstHash = if (Test-Path $dest) { (Get-FileHash $dest -Algorithm SHA256).Hash } else { $null } + if ($srcHash -ne $dstHash) { + Copy-Item $_.FullName $dest -Force + if ($dstHash) { $updated++ } else { $added++ } + } + else { $unchanged++ } + } + } + + Log "Skills: added=$added updated=$updated unchanged=$unchanged --> $SkillsTarget" + } +} + +if ($DryRun) { Log "[DryRun] No changes made." } +else { Log "Global publish complete." } diff --git a/publish-to-vscode-profile.ps1 b/publish-to-vscode-profile.ps1 deleted file mode 100644 index c84af4e..0000000 --- a/publish-to-vscode-profile.ps1 +++ /dev/null @@ -1,145 +0,0 @@ -[CmdletBinding()] param( - [string]$SourceRoot = "$HOME/.awesome-copilot", - [string]$ProfileRoot, - [switch]$AllProfiles, - [string]$WorkspaceRoot, - [switch]$ForceCopyFallback, - [switch]$Prune, - [switch]$VerboseLinks -) - -$ErrorActionPreference = 'Stop' - -function Log($msg, [string]$level = 'INFO') { - $ts = (Get-Date).ToString('s'); Write-Host "[$ts][$level] $msg" -ForegroundColor $(if ($level -eq 'ERROR') { 'Red' } elseif ($level -eq 'WARN') { 'Yellow' } else { 'Cyan' }) -} - -if (-not (Test-Path $SourceRoot)) { Log "Source root not found: $SourceRoot" 'ERROR'; exit 1 } - -# Collect target profiles -$profilesBase = Join-Path $env:APPDATA 'Code/User/profiles' -if (-not (Test-Path $profilesBase)) { Log "Profiles base not found: $profilesBase (open VS Code & ensure a profile exists)" 'ERROR'; exit 1 } -$TargetProfiles = @() -if ($AllProfiles) { - $TargetProfiles = Get-ChildItem $profilesBase -Directory | ForEach-Object { $_.FullName } - if (-not $TargetProfiles) { Log "No profile directories found under $profilesBase" 'ERROR'; exit 1 } - Log "Publishing to ALL profiles ($($TargetProfiles.Count))" 'INFO' -} -else { - if (-not $ProfileRoot) { - $profileDir = Get-ChildItem $profilesBase -Directory | Sort-Object LastWriteTime -Descending | Select-Object -First 1 - if (-not $profileDir) { Log "No profile directories found under $profilesBase" 'ERROR'; exit 1 } - $ProfileRoot = $profileDir.FullName - Log "Detected profile: $ProfileRoot" - } - $TargetProfiles = @($ProfileRoot) -} - -function Get-MappingsForProfile($pRoot) { - @( - @{ Name = 'chatmodes'; Src = (Join-Path $SourceRoot 'chatmodes'); Dst = (Join-Path $pRoot 'chatmodes') } - @{ Name = 'prompts'; Src = (Join-Path $SourceRoot 'prompts'); Dst = (Join-Path $pRoot 'prompts') } - @{ Name = 'instructions'; Src = (Join-Path $SourceRoot 'instructions'); Dst = (Join-Path $pRoot 'instructions') } - ) -} - -# Helper to link or copy -function Ensure-LinkOrCopy($src, $dst, [string]$label) { - if (-not (Test-Path $src)) { Log "Skipping $label (missing source: $src)" 'WARN'; return } - if (Test-Path $dst) { - $item = Get-Item $dst -Force - $isLink = ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) - if ($isLink -and -not $ForceCopyFallback) { - Log "$label already linked: $dst"; return - } - elseif (-not $isLink -and -not $ForceCopyFallback) { - Log "$label exists as normal directory; will update only synced files" 'INFO' - # Only remove files that exist in source (preserve user-created files) - if (Test-Path $src) { - $srcFiles = Get-ChildItem $src -Filter '*.md' -File -Recurse | ForEach-Object { $_.Name } - Get-ChildItem $dst -Filter '*.md' -File -Recurse -ErrorAction SilentlyContinue | ForEach-Object { - if ($srcFiles -contains $_.Name) { - Log "Removing synced file to update: $($_.Name)" 'INFO' - Remove-Item $_.FullName -Force - } - } - } - # Now copy the source files (will update/add synced files, leave others alone) - if (-not (Test-Path $dst)) { New-Item -ItemType Directory -Path $dst | Out-Null } - Copy-Item (Join-Path $src '*') $dst -Recurse -Force - return - } - else { - Log "Removing existing $label at $dst to replace" 'WARN' - Remove-Item $dst -Recurse -Force - } - } - if ($ForceCopyFallback) { - Log "Copying $label -> $dst" - if (-not (Test-Path $dst)) { New-Item -ItemType Directory -Path $dst | Out-Null } - Copy-Item (Join-Path $src '*') $dst -Recurse -Force - } - else { - try { - Log "Creating symlink $dst -> $src" - New-Item -ItemType SymbolicLink -Path $dst -Target $src -Force | Out-Null - } - catch { - Log "Symlink failed ($label): $_ (attempt junction)" 'WARN' - try { - cmd /c mklink /J "$dst" "$src" | Out-Null - Log "Created junction for ${label}: ${dst}" - } - catch { - Log "Junction failed for ${label}: $_ (fallback copy)" 'WARN' - if (-not (Test-Path $dst)) { New-Item -ItemType Directory -Path $dst | Out-Null } - Copy-Item (Join-Path $src '*') $dst -Recurse -Force - } - } - } - if ($VerboseLinks) { - $preview = Get-ChildItem $dst -File | Select-Object -First 3 | ForEach-Object { $_.Name } | Out-String - $cleanPreview = ($preview -split "`r?`n" | Where-Object { $_ }) -join ', ' - Log "${label} preview: $cleanPreview" - } -} - -foreach ($p in $TargetProfiles) { - Log "Processing profile: $p" 'INFO' - $Mappings = Get-MappingsForProfile -pRoot $p - foreach ($m in $Mappings) { Ensure-LinkOrCopy -src $m.Src -dst $m.Dst -label $m.Name } - - if ($Prune) { - foreach ($m in $Mappings) { - if (-not (Test-Path $m.Dst)) { continue } - $srcRel = Get-ChildItem $m.Src -Recurse -File | ForEach-Object { $_.FullName.Substring($m.Src.Length).TrimStart('\\') } - $srcSet = [System.Collections.Generic.HashSet[string]]::new([string[]]$srcRel) - Get-ChildItem $m.Dst -Recurse -File | ForEach-Object { - $rel = $_.FullName.Substring($m.Dst.Length).TrimStart('\\') - if (-not $srcSet.Contains($rel)) { - Log "Prune stale: $($m.Name)/$rel" 'WARN' - Remove-Item $_.FullName -Force - } - } - } - } -} - -# Mirror into workspace if requested -if ($WorkspaceRoot) { - $gh = Join-Path $WorkspaceRoot '.github' - if (-not (Test-Path $gh)) { New-Item -ItemType Directory -Path $gh | Out-Null } - foreach ($m in $Mappings) { - if (-not (Test-Path $m.Src)) { continue } - $dst = Join-Path $gh $m.Name - if (-not (Test-Path $dst)) { - Log "Seeding workspace $($m.Name) -> $dst" - Copy-Item $m.Src $dst -Recurse - } - else { - Log "Workspace already has $($m.Name); skipping" 'INFO' - } - } -} - -Log "Publish complete. Reload VS Code if new items aren't visible." 'INFO' diff --git a/sync-awesome-copilot.ps1 b/sync-awesome-copilot.ps1 index 5639dfa..0b6c7f3 100644 --- a/sync-awesome-copilot.ps1 +++ b/sync-awesome-copilot.ps1 @@ -1,7 +1,7 @@ [CmdletBinding()] param( [string]$Dest = "$HOME/.awesome-copilot", - # Default now excludes 'collections' (can still be added explicitly via -Categories) - [string]$Categories = 'chatmodes,instructions,prompts', + # Default covers all main categories; add 'plugins' or 'cookbook' explicitly for larger opt-in categories + [string]$Categories = 'agents,instructions,workflows,hooks,skills', [switch]$Quiet, [switch]$NoDelete, [switch]$DiffOnly, @@ -101,6 +101,24 @@ function Get-FileHashSha256String { ($hashBytes | ForEach-Object { $_.ToString('x2') }) -join '' } +# Recursively fetch all file entries under a GitHub contents API URL. +# Handles both flat directories and nested subdirectory structures (e.g. skills/, hooks/). +function Get-RepoFiles { + param([string]$Url) + Check-Timeout + try { $entries = Invoke-Github -Url $Url } catch { Write-Log "Failed to list $Url : $_" 'ERROR'; return @() } + $files = @() + foreach ($entry in $entries) { + if ($entry.type -eq 'file') { + $files += $entry + } + elseif ($entry.type -eq 'dir') { + $files += Get-RepoFiles -Url $entry.url + } + } + return $files +} + if ($DiffOnly) { if (-not $PrevManifest) { Write-Log "No previous manifest; diff-only mode cannot proceed." 'ERROR'; exit 1 } Write-Log "Diff-only mode: no network calls. Summarizing previous manifest." 'INFO' @@ -126,7 +144,7 @@ foreach ($cat in $CategoriesList) { Write-Log "Fetching category: $cat" 'INFO' $url = "$ApiBase/repos/$Repo/contents/$cat" try { - $listing = Invoke-Github -Url $url + $allEntries = Get-RepoFiles -Url $url } catch { Write-Log "Failed to list $cat" 'ERROR' @@ -136,9 +154,8 @@ foreach ($cat in $CategoriesList) { if (-not $script:SuccessfulCategories) { $script:SuccessfulCategories = @() } $script:SuccessfulCategories += $cat - foreach ($entry in $listing) { - if ($entry.type -ne 'file') { continue } - if (-not ($entry.name -match '\.(md|markdown|json)$')) { continue } + foreach ($entry in $allEntries) { + if (-not ($entry.name -match '\.(md|markdown|json|sh)$')) { continue } Check-Timeout $downloadUrl = $entry.download_url if (-not $downloadUrl) { continue } From f2975d0a3aeb36e0591c3e7e555da7eb975589a2 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 12:57:09 +0000 Subject: [PATCH 02/60] fix: use Stop instead of Inquire for ErrorActionPreference Inquire causes the script to pause and wait for interactive input on any unhandled error, which hangs scheduled task runs indefinitely. Stop ensures errors are terminating and handled by existing try/catch blocks. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- sync-awesome-copilot.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sync-awesome-copilot.ps1 b/sync-awesome-copilot.ps1 index 0b6c7f3..6e4f160 100644 --- a/sync-awesome-copilot.ps1 +++ b/sync-awesome-copilot.ps1 @@ -12,7 +12,7 @@ [int]$TimeoutSeconds = 600 ) -$ErrorActionPreference = 'Inquire' +$ErrorActionPreference = 'Stop' $script:StartTime = Get-Date $script:Deadline = $script:StartTime.AddSeconds($TimeoutSeconds) From 56f00e53f8a32ce197e4d94e7dfc8e4184d606af Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:03:58 +0000 Subject: [PATCH 03/60] refactor: remove skills from init-repo.ps1 Skills are available globally via publish-global.ps1 (~/.copilot/skills/). There is no value in copying point-in-time versions into .github/skills/ - users should reference the source at github/awesome-copilot directly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- init-repo.ps1 | 25 +++---------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/init-repo.ps1 b/init-repo.ps1 index e4b5945..6e109ac 100644 --- a/init-repo.ps1 +++ b/init-repo.ps1 @@ -9,7 +9,6 @@ Resources installed here are project-specific (opt-in) rather than global: Instructions --> .github/instructions/*.instructions.md Hooks --> .github/hooks// (full directory) Workflows --> .github/workflows/*.md - Skills --> .github/skills// (project-level skills) Usage: # Interactive - run from within the target repo @@ -23,15 +22,15 @@ Usage: # Non-interactive: specify items by name (comma-separated) .\init-repo.ps1 -Instructions "angular,dotnet-framework" -Hooks "session-logger" - # Dry run - show what would be installed .\init-repo.ps1 -DryRun Notes: - Existing files are only overwritten if the source is newer/different. - .github/ is created if it doesn't exist. - - This script does NOT touch global resources (agents, personal skills). - Use publish-global.ps1 for those. + - This script does NOT touch global resources (agents, skills). + Use publish-global.ps1 for those. For skills, point users directly + at https://github.com/github/awesome-copilot. - The selection UI uses Out-GridView where available (Windows GUI, filterable, multi-select). Falls back to a numbered console menu automatically. #> @@ -41,11 +40,9 @@ Notes: [string]$Instructions = '', # Comma-separated names to pre-select (non-interactive) [string]$Hooks = '', [string]$Workflows = '', - [string]$Skills = '', [switch]$SkipInstructions, [switch]$SkipHooks, [switch]$SkipWorkflows, - [switch]$SkipSkills, [switch]$DryRun ) @@ -293,22 +290,6 @@ if (-not $SkipWorkflows) { } } -# --------------------------------------------------------------------------- -# SKILLS (project-level) -# --------------------------------------------------------------------------- -if (-not $SkipSkills) { - $destDir = Join-Path $GithubDir 'skills' - $catalogue = Build-DirCatalogue (Join-Path $SourceRoot 'skills') $destDir - $selected = Select-Items -Category 'Skills (project-level)' -Items $catalogue -PreSelected $Skills - - foreach ($item in $selected) { - $r = Install-Directory -SrcDir $item.FullPath -DestParent $destDir - $verb = if ($DryRun) { '~ DryRun' } else { '✓ Installed' } - Log "$verb skill: $($item.Name) (added=$($r.Added) updated=$($r.Updated) unchanged=$($r.Unchanged))" - if (-not $DryRun) { $totalInstalled++ } - } -} - # --------------------------------------------------------------------------- # Summary # --------------------------------------------------------------------------- From 03dec05ccf31d65d2ed4001dd257da2a14823951 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:05:00 +0000 Subject: [PATCH 04/60] docs: add v1.1.1 changelog entry Documents the ErrorActionPreference fix and skills removal from init-repo. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d1acb77..79b9209 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,11 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.1.1] - 2026-02-27 + +### Fixed +- `sync-awesome-copilot.ps1`: changed `$ErrorActionPreference` from `Inquire` to `Stop` — `Inquire` caused the script to hang waiting for interactive input when run as a scheduled task + +### Changed +- `init-repo.ps1`: removed skills from per-repo initialisation; skills are globally available via `publish-global.ps1` (`~/.copilot/skills/`) and users should reference the source directly at [github/awesome-copilot](https://github.com/github/awesome-copilot) rather than committing point-in-time copies to repos + ## [1.1.0] - 2026-02-26 ### Added - `publish-global.ps1` — publishes agents to the VS Code user agents folder (via junction so sync updates are reflected immediately) and skills to `~/.copilot/skills/`; supports `-DryRun`, `-SkipAgents`, `-SkipSkills`, `-AgentsTarget`, `-SkillsTarget` -- `init-repo.ps1` — interactive script to initialise a repo with per-repo resources (instructions, hooks, workflows, project-level skills); uses Out-GridView on Windows with a numbered console-menu fallback; supports `-RepoPath`, `-DryRun`, `-SkipInstructions`, `-SkipHooks`, `-SkipWorkflows`, `-SkipSkills` +- `init-repo.ps1` — interactive script to initialise a repo with per-repo resources (instructions, hooks, workflows); uses Out-GridView on Windows with a numbered console-menu fallback; supports `-RepoPath`, `-DryRun`, `-SkipInstructions`, `-SkipHooks`, `-SkipWorkflows` ### Changed - Updated default sync categories to match current awesome-copilot repository structure: From 5b98df0fcc410304b9b5a11b2e325904b5bcb8b4 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:13:13 +0000 Subject: [PATCH 05/60] feat: smart repo detection and intent prompting in init-repo.ps1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Auto-detects language/framework from repo file signals and pre-marks recommended instructions/hooks/workflows with ★ in the picker. For new/empty repos, prompts for intent one question at a time. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- init-repo.ps1 | 199 +++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 188 insertions(+), 11 deletions(-) diff --git a/init-repo.ps1 b/init-repo.ps1 index 6e109ac..0c81d7c 100644 --- a/init-repo.ps1 +++ b/init-repo.ps1 @@ -33,6 +33,9 @@ Notes: at https://github.com/github/awesome-copilot. - The selection UI uses Out-GridView where available (Windows GUI, filterable, multi-select). Falls back to a numbered console menu automatically. + - Auto-detects language/framework from repo file signals and pre-marks + recommended instructions/hooks/workflows with ★ in the picker. + - For new/empty repos, prompts for intent one question at a time. #> [CmdletBinding()] param( [string]$RepoPath = (Get-Location).Path, @@ -54,6 +57,149 @@ function Log($m, [string]$level = 'INFO') { Write-Host "[$ts][$level] $m" -ForegroundColor $color } +# --------------------------------------------------------------------------- +# Detect language/framework signals from repo files +# --------------------------------------------------------------------------- +function Detect-RepoStack { + param([string]$RepoPath) + + $recs = [System.Collections.Generic.List[string]]::new() + $files = Get-ChildItem $RepoPath -Recurse -File -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -notmatch '\\(\.git|node_modules|\.venv|bin|obj)\\' } + + $exts = $files | ForEach-Object { $_.Extension.ToLower() } | Sort-Object -Unique + $names = $files | ForEach-Object { $_.Name } | Sort-Object -Unique + $hasDotnet = $exts -contains '.cs' -or ($names | Where-Object { $_ -match '\.(csproj|sln)$' }) + $hasPy = $exts -contains '.py' -or ($names -contains 'requirements.txt') -or ($names -contains 'pyproject.toml') + $hasTs = $exts -contains '.ts' -or ($names -contains 'tsconfig.json') + $hasGo = $exts -contains '.go' -or ($names -contains 'go.mod') + $hasRs = $exts -contains '.rs' -or ($names -contains 'Cargo.toml') + $hasJava = $exts -contains '.java' -or ($names -contains 'pom.xml') -or ($names | Where-Object { $_ -eq 'build.gradle' }) + $hasKt = $exts -contains '.kt' + $hasTf = $exts -contains '.tf' + $hasBicep = $exts -contains '.bicep' + $hasPs1 = $exts -contains '.ps1' + + if ($hasDotnet) { $recs.Add('csharp'); $recs.Add('dotnet-architecture-good-practices') } + if ($hasPy) { $recs.Add('python') } + if ($hasTs) { $recs.Add('typescript-5-es2022') } + if ($hasGo) { $recs.Add('go') } + if ($hasRs) { $recs.Add('rust') } + if ($hasJava -or $hasKt) { $recs.Add('java') } + if ($hasTf) { $recs.Add('terraform') } + if ($hasBicep) { $recs.Add('bicep-code-best-practices') } + if ($hasPs1) { $recs.Add('powershell') } + + # Docker + if (($names -contains 'Dockerfile') -or ($names | Where-Object { $_ -match '^docker-compose\.yml$' })) { + $recs.Add('containerization-docker-best-practices') + } + + # GitHub Actions workflows + $ghWorkflows = $files | Where-Object { $_.FullName -match '\\\.github\\workflows\\' -and $_.Extension -eq '.yml' } + if ($ghWorkflows) { $recs.Add('github-actions-ci-cd-best-practices') } + + # Playwright + $hasPlaywright = $files | Where-Object { $_.Name -match '^playwright\.config\.' } + if ($hasPlaywright) { + if ($hasDotnet) { $recs.Add('playwright-dotnet') } + elseif ($hasPy) { $recs.Add('playwright-python') } + else { $recs.Add('playwright-typescript') } + } + + # package.json framework detection + $pkgJson = $files | Where-Object { $_.Name -eq 'package.json' } | Select-Object -First 1 + if ($pkgJson) { + try { + $pkg = Get-Content $pkgJson.FullName -Raw | ConvertFrom-Json -ErrorAction Stop + $allDeps = @() + if ($pkg.dependencies) { $allDeps += $pkg.dependencies.PSObject.Properties.Name } + if ($pkg.devDependencies) { $allDeps += $pkg.devDependencies.PSObject.Properties.Name } + if ($allDeps -contains 'react') { $recs.Add('reactjs') } + if ($allDeps -contains 'next') { $recs.Add('nextjs') } + if ($allDeps | Where-Object { $_ -match '^@angular/' }) { $recs.Add('angular') } + if ($allDeps -contains 'vue') { $recs.Add('vuejs3') } + if ($allDeps -contains 'svelte') { $recs.Add('svelte') } + if ($allDeps | Where-Object { $_ -match '^@nestjs/' }) { $recs.Add('nestjs') } + } catch {} + } + + $recs.Add('security-and-owasp') + $recs.Add('code-review-generic') + + return @($recs | Sort-Object -Unique) +} + +# --------------------------------------------------------------------------- +# Prompt for intent when no signals detected (new/empty repo) +# --------------------------------------------------------------------------- +function Prompt-RepoIntent { + $recs = [System.Collections.Generic.List[string]]::new() + + Write-Host "" + Write-Host " Q1: What is the primary language or stack?" -ForegroundColor Yellow + Write-Host " 1. C# / .NET" + Write-Host " 2. Python" + Write-Host " 3. TypeScript / JavaScript" + Write-Host " 4. Go" + Write-Host " 5. Java / Kotlin" + Write-Host " 6. Rust" + Write-Host " 7. PowerShell" + Write-Host " 8. Terraform / Bicep (Infrastructure)" + Write-Host " 9. Other" + Write-Host " Enter number: " -NoNewline -ForegroundColor Yellow + $q1 = (Read-Host).Trim() + switch ($q1) { + '1' { $recs.Add('csharp'); $recs.Add('dotnet-architecture-good-practices') } + '2' { $recs.Add('python') } + '3' { $recs.Add('typescript-5-es2022') } + '4' { $recs.Add('go') } + '5' { $recs.Add('java') } + '6' { $recs.Add('rust') } + '7' { $recs.Add('powershell') } + '8' { $recs.Add('terraform'); $recs.Add('bicep-code-best-practices') } + } + + Write-Host "" + Write-Host " Q2: What type of project is this?" -ForegroundColor Yellow + Write-Host " 1. Web API / REST service" + Write-Host " 2. Web application (frontend)" + Write-Host " 3. CLI tool" + Write-Host " 4. Library / SDK" + Write-Host " 5. Data pipeline / ML" + Write-Host " 6. Infrastructure / DevOps" + Write-Host " 7. Documentation / Content" + Write-Host " Enter number: " -NoNewline -ForegroundColor Yellow + $null = Read-Host # no mapping yet, reserved for future extensibility + + Write-Host "" + Write-Host " Q3: Any specific concerns? (comma-separated, e.g. 1,3)" -ForegroundColor Yellow + Write-Host " 1. Security / OWASP" + Write-Host " 2. Accessibility (a11y)" + Write-Host " 3. Testing / Playwright" + Write-Host " 4. Performance" + Write-Host " 5. Docker / Containers" + Write-Host " 6. CI/CD / GitHub Actions" + Write-Host " 7. None" + Write-Host " Enter numbers: " -NoNewline -ForegroundColor Yellow + $q3 = (Read-Host).Trim() + if ($q3 -and $q3 -ne '7') { + foreach ($part in $q3.Split(',')) { + switch ($part.Trim()) { + '1' { $recs.Add('security-and-owasp') } + '2' { $recs.Add('a11y') } + '3' { $recs.Add('playwright-typescript') } + '4' { $recs.Add('performance-optimization') } + '5' { $recs.Add('containerization-docker-best-practices') } + '6' { $recs.Add('github-actions-ci-cd-best-practices') } + } + } + } + + $recs.Add('code-review-generic') + return @($recs | Sort-Object -Unique) +} + # --------------------------------------------------------------------------- # Validate paths # --------------------------------------------------------------------------- @@ -70,6 +216,30 @@ $GithubDir = Join-Path $RepoPath '.github' Log "Target repo : $RepoPath" Log "Copilot cache: $SourceRoot" +# --------------------------------------------------------------------------- +# Auto-detect stack or prompt for intent +# --------------------------------------------------------------------------- +$script:Recommendations = @() +if (-not ($SkipInstructions -and $SkipHooks -and $SkipWorkflows)) { + $repoFileCount = (Get-ChildItem $RepoPath -Recurse -File -ErrorAction SilentlyContinue | + Where-Object { $_.FullName -notmatch '\\(\.git|node_modules|\.venv|bin|obj)\\' } | + Measure-Object).Count + + if ($repoFileCount -gt 3) { + Log "Scanning repo for language/framework signals..." + $script:Recommendations = Detect-RepoStack -RepoPath $RepoPath + if ($script:Recommendations.Count -gt 0) { + Log "Detected: $($script:Recommendations -join ', ')" + } else { + Log "No signals detected." 'WARN' + $script:Recommendations = Prompt-RepoIntent + } + } else { + Log "New or empty repo detected — prompting for intent." + $script:Recommendations = Prompt-RepoIntent + } +} + # --------------------------------------------------------------------------- # Helper: interactive picker # Returns array of selected names. @@ -79,7 +249,8 @@ function Select-Items { param( [string]$Category, [object[]]$Items, # objects with Name, Description, AlreadyInstalled - [string]$PreSelected = '' + [string]$PreSelected = '', + [string[]]$Recommended = @() ) if ($Items.Count -eq 0) { @@ -95,6 +266,11 @@ function Select-Items { return @($selected) } + # Attach IsRecommended and sort: recommended first, then alphabetical within groups + $Items = $Items | ForEach-Object { + $_ | Add-Member -NotePropertyName 'IsRecommended' -NotePropertyValue ($Recommended -contains $_.Name) -PassThru -Force + } | Sort-Object @{ E={ if ($_.IsRecommended) { 0 } else { 1 } } }, Name + Write-Host "" Write-Host " === $Category ===" -ForegroundColor Yellow Write-Host " Already installed items are marked with [*]" -ForegroundColor DarkGray @@ -105,21 +281,22 @@ function Select-Items { if ($ogvAvailable) { $display = $Items | Select-Object ` - @{ N='[*] Installed'; E={ if ($_.AlreadyInstalled) { 'YES' } else { '' } } }, - @{ N='Name'; E={ $_.Name } }, - @{ N='Description'; E={ $_.Description } } + @{ N='★'; E={ if ($_.IsRecommended) { '★' } else { '' } } }, + @{ N='Installed'; E={ if ($_.AlreadyInstalled) { '[*]' } else { '' } } }, + @{ N='Name'; E={ $_.Name } }, + @{ N='Description'; E={ $_.Description } } - $picked = $display | Out-GridView -Title "Select $Category to install (multi-select with Ctrl/Shift, then OK)" -PassThru + $picked = $display | Out-GridView -Title "Select $Category to install ★ = Recommended [*] = Already installed" -PassThru if (-not $picked) { return @() } - $pickedNames = @($picked | ForEach-Object { $_.'Name' }) + $pickedNames = @($picked | ForEach-Object { $_.Name }) return @($Items | Where-Object { $pickedNames -contains $_.Name }) } # Fallback: numbered console menu Write-Host "" for ($i = 0; $i -lt $Items.Count; $i++) { - $mark = if ($Items[$i].AlreadyInstalled) { '[*]' } else { ' ' } - Write-Host (" {0,3}. {1} {2}" -f ($i+1), $mark, $Items[$i].Name) -ForegroundColor $(if ($Items[$i].AlreadyInstalled) { 'DarkCyan' } else { 'White' }) + $mark = if ($Items[$i].AlreadyInstalled) { '[*]' } elseif ($Items[$i].IsRecommended) { '[★]' } else { ' ' } + Write-Host (" {0,3}. {1} {2}" -f ($i+1), $mark, $Items[$i].Name) -ForegroundColor $(if ($Items[$i].AlreadyInstalled) { 'DarkCyan' } elseif ($Items[$i].IsRecommended) { 'Yellow' } else { 'White' }) if ($Items[$i].Description) { Write-Host (" {0}" -f $Items[$i].Description) -ForegroundColor DarkGray } @@ -248,7 +425,7 @@ function Build-DirCatalogue([string]$CatDir, [string]$DestDir) { if (-not $SkipInstructions) { $destDir = Join-Path $GithubDir 'instructions' $catalogue = Build-FlatCatalogue (Join-Path $SourceRoot 'instructions') $destDir '\.instructions\.md$' - $selected = Select-Items -Category 'Instructions' -Items $catalogue -PreSelected $Instructions + $selected = Select-Items -Category 'Instructions' -Items $catalogue -PreSelected $Instructions -Recommended $script:Recommendations foreach ($item in $selected) { $result = Install-File -Src $item.FullPath -DestDir $destDir @@ -264,7 +441,7 @@ if (-not $SkipInstructions) { if (-not $SkipHooks) { $destDir = Join-Path $GithubDir 'hooks' $catalogue = Build-DirCatalogue (Join-Path $SourceRoot 'hooks') $destDir - $selected = Select-Items -Category 'Hooks' -Items $catalogue -PreSelected $Hooks + $selected = Select-Items -Category 'Hooks' -Items $catalogue -PreSelected $Hooks -Recommended $script:Recommendations foreach ($item in $selected) { $r = Install-Directory -SrcDir $item.FullPath -DestParent $destDir @@ -280,7 +457,7 @@ if (-not $SkipHooks) { if (-not $SkipWorkflows) { $destDir = Join-Path $GithubDir 'workflows' $catalogue = Build-FlatCatalogue (Join-Path $SourceRoot 'workflows') $destDir '\.md$' - $selected = Select-Items -Category 'Agentic Workflows' -Items $catalogue -PreSelected $Workflows + $selected = Select-Items -Category 'Agentic Workflows' -Items $catalogue -PreSelected $Workflows -Recommended $script:Recommendations foreach ($item in $selected) { $result = Install-File -Src $item.FullPath -DestDir $destDir From 04c0c5dfc3c2b87373bb5674a6fe8c474e538943 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:18:20 +0000 Subject: [PATCH 06/60] feat: smart repo detection and intent prompting in init-repo.ps1 Auto-detects language/framework from repo file signals and pre-marks recommended instructions/hooks/workflows with star in the Rec column, sorted to the top of the picker. For new/empty repos, prompts for intent one question at a time (stack, project type, concerns). Also fixes Out-GridView column name bug (non-alphanumeric names cause WPF property path errors). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- init-repo.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init-repo.ps1 b/init-repo.ps1 index 0c81d7c..9b10b8c 100644 --- a/init-repo.ps1 +++ b/init-repo.ps1 @@ -281,7 +281,7 @@ function Select-Items { if ($ogvAvailable) { $display = $Items | Select-Object ` - @{ N='★'; E={ if ($_.IsRecommended) { '★' } else { '' } } }, + @{ N='Rec'; E={ if ($_.IsRecommended) { '★' } else { '' } } }, @{ N='Installed'; E={ if ($_.AlreadyInstalled) { '[*]' } else { '' } } }, @{ N='Name'; E={ $_.Name } }, @{ N='Description'; E={ $_.Description } } From b38cac98dda55437c4b2454dd70a895559dd463c Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:20:09 +0000 Subject: [PATCH 07/60] fix: replace invalid 'return if' syntax in Install-File 'return if (...)' is not valid PowerShell at runtime despite passing the parser. Replace with explicit if/else return statements. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- init-repo.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/init-repo.ps1 b/init-repo.ps1 index 9b10b8c..785d5cf 100644 --- a/init-repo.ps1 +++ b/init-repo.ps1 @@ -334,7 +334,7 @@ function Install-File { if ($srcHash -eq $dstHash) { return 'unchanged' } if ($DryRun) { return 'would-copy' } Copy-Item $Src $dest -Force - return if ($dstHash) { 'updated' } else { 'added' } + if ($dstHash) { return 'updated' } else { return 'added' } } # --------------------------------------------------------------------------- From 13db7624f479aa62a61a560444c256719a310f99 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:20:56 +0000 Subject: [PATCH 08/60] chore: add Copilot instructions via init-repo.ps1 Added security-and-owasp and powershell instructions to .github/instructions/ as detected by init-repo.ps1 from repo signals. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../code-review-generic.instructions.md | 418 ++++++++++++++++++ .../instructions/powershell.instructions.md | 356 +++++++++++++++ .../security-and-owasp.instructions.md | 51 +++ 3 files changed, 825 insertions(+) create mode 100644 .github/instructions/code-review-generic.instructions.md create mode 100644 .github/instructions/powershell.instructions.md create mode 100644 .github/instructions/security-and-owasp.instructions.md diff --git a/.github/instructions/code-review-generic.instructions.md b/.github/instructions/code-review-generic.instructions.md new file mode 100644 index 0000000..bcd7365 --- /dev/null +++ b/.github/instructions/code-review-generic.instructions.md @@ -0,0 +1,418 @@ +--- +description: 'Generic code review instructions that can be customized for any project using GitHub Copilot' +applyTo: '**' +excludeAgent: ["coding-agent"] +--- + +# Generic Code Review Instructions + +Comprehensive code review guidelines for GitHub Copilot that can be adapted to any project. These instructions follow best practices from prompt engineering and provide a structured approach to code quality, security, testing, and architecture review. + +## Review Language + +When performing a code review, respond in **English** (or specify your preferred language). + +> **Customization Tip**: Change to your preferred language by replacing "English" with "Portuguese (Brazilian)", "Spanish", "French", etc. + +## Review Priorities + +When performing a code review, prioritize issues in the following order: + +### 🔴 CRITICAL (Block merge) +- **Security**: Vulnerabilities, exposed secrets, authentication/authorization issues +- **Correctness**: Logic errors, data corruption risks, race conditions +- **Breaking Changes**: API contract changes without versioning +- **Data Loss**: Risk of data loss or corruption + +### 🟡 IMPORTANT (Requires discussion) +- **Code Quality**: Severe violations of SOLID principles, excessive duplication +- **Test Coverage**: Missing tests for critical paths or new functionality +- **Performance**: Obvious performance bottlenecks (N+1 queries, memory leaks) +- **Architecture**: Significant deviations from established patterns + +### 🟢 SUGGESTION (Non-blocking improvements) +- **Readability**: Poor naming, complex logic that could be simplified +- **Optimization**: Performance improvements without functional impact +- **Best Practices**: Minor deviations from conventions +- **Documentation**: Missing or incomplete comments/documentation + +## General Review Principles + +When performing a code review, follow these principles: + +1. **Be specific**: Reference exact lines, files, and provide concrete examples +2. **Provide context**: Explain WHY something is an issue and the potential impact +3. **Suggest solutions**: Show corrected code when applicable, not just what's wrong +4. **Be constructive**: Focus on improving the code, not criticizing the author +5. **Recognize good practices**: Acknowledge well-written code and smart solutions +6. **Be pragmatic**: Not every suggestion needs immediate implementation +7. **Group related comments**: Avoid multiple comments about the same topic + +## Code Quality Standards + +When performing a code review, check for: + +### Clean Code +- Descriptive and meaningful names for variables, functions, and classes +- Single Responsibility Principle: each function/class does one thing well +- DRY (Don't Repeat Yourself): no code duplication +- Functions should be small and focused (ideally < 20-30 lines) +- Avoid deeply nested code (max 3-4 levels) +- Avoid magic numbers and strings (use constants) +- Code should be self-documenting; comments only when necessary + +### Examples +```javascript +// ❌ BAD: Poor naming and magic numbers +function calc(x, y) { + if (x > 100) return y * 0.15; + return y * 0.10; +} + +// ✅ GOOD: Clear naming and constants +const PREMIUM_THRESHOLD = 100; +const PREMIUM_DISCOUNT_RATE = 0.15; +const STANDARD_DISCOUNT_RATE = 0.10; + +function calculateDiscount(orderTotal, itemPrice) { + const isPremiumOrder = orderTotal > PREMIUM_THRESHOLD; + const discountRate = isPremiumOrder ? PREMIUM_DISCOUNT_RATE : STANDARD_DISCOUNT_RATE; + return itemPrice * discountRate; +} +``` + +### Error Handling +- Proper error handling at appropriate levels +- Meaningful error messages +- No silent failures or ignored exceptions +- Fail fast: validate inputs early +- Use appropriate error types/exceptions + +### Examples +```python +# ❌ BAD: Silent failure and generic error +def process_user(user_id): + try: + user = db.get(user_id) + user.process() + except: + pass + +# ✅ GOOD: Explicit error handling +def process_user(user_id): + if not user_id or user_id <= 0: + raise ValueError(f"Invalid user_id: {user_id}") + + try: + user = db.get(user_id) + except UserNotFoundError: + raise UserNotFoundError(f"User {user_id} not found in database") + except DatabaseError as e: + raise ProcessingError(f"Failed to retrieve user {user_id}: {e}") + + return user.process() +``` + +## Security Review + +When performing a code review, check for security issues: + +- **Sensitive Data**: No passwords, API keys, tokens, or PII in code or logs +- **Input Validation**: All user inputs are validated and sanitized +- **SQL Injection**: Use parameterized queries, never string concatenation +- **Authentication**: Proper authentication checks before accessing resources +- **Authorization**: Verify user has permission to perform action +- **Cryptography**: Use established libraries, never roll your own crypto +- **Dependency Security**: Check for known vulnerabilities in dependencies + +### Examples +```java +// ❌ BAD: SQL injection vulnerability +String query = "SELECT * FROM users WHERE email = '" + email + "'"; + +// ✅ GOOD: Parameterized query +PreparedStatement stmt = conn.prepareStatement( + "SELECT * FROM users WHERE email = ?" +); +stmt.setString(1, email); +``` + +```javascript +// ❌ BAD: Exposed secret in code +const API_KEY = "sk_live_abc123xyz789"; + +// ✅ GOOD: Use environment variables +const API_KEY = process.env.API_KEY; +``` + +## Testing Standards + +When performing a code review, verify test quality: + +- **Coverage**: Critical paths and new functionality must have tests +- **Test Names**: Descriptive names that explain what is being tested +- **Test Structure**: Clear Arrange-Act-Assert or Given-When-Then pattern +- **Independence**: Tests should not depend on each other or external state +- **Assertions**: Use specific assertions, avoid generic assertTrue/assertFalse +- **Edge Cases**: Test boundary conditions, null values, empty collections +- **Mock Appropriately**: Mock external dependencies, not domain logic + +### Examples +```typescript +// ❌ BAD: Vague name and assertion +test('test1', () => { + const result = calc(5, 10); + expect(result).toBeTruthy(); +}); + +// ✅ GOOD: Descriptive name and specific assertion +test('should calculate 10% discount for orders under $100', () => { + const orderTotal = 50; + const itemPrice = 20; + + const discount = calculateDiscount(orderTotal, itemPrice); + + expect(discount).toBe(2.00); +}); +``` + +## Performance Considerations + +When performing a code review, check for performance issues: + +- **Database Queries**: Avoid N+1 queries, use proper indexing +- **Algorithms**: Appropriate time/space complexity for the use case +- **Caching**: Utilize caching for expensive or repeated operations +- **Resource Management**: Proper cleanup of connections, files, streams +- **Pagination**: Large result sets should be paginated +- **Lazy Loading**: Load data only when needed + +### Examples +```python +# ❌ BAD: N+1 query problem +users = User.query.all() +for user in users: + orders = Order.query.filter_by(user_id=user.id).all() # N+1! + +# ✅ GOOD: Use JOIN or eager loading +users = User.query.options(joinedload(User.orders)).all() +for user in users: + orders = user.orders +``` + +## Architecture and Design + +When performing a code review, verify architectural principles: + +- **Separation of Concerns**: Clear boundaries between layers/modules +- **Dependency Direction**: High-level modules don't depend on low-level details +- **Interface Segregation**: Prefer small, focused interfaces +- **Loose Coupling**: Components should be independently testable +- **High Cohesion**: Related functionality grouped together +- **Consistent Patterns**: Follow established patterns in the codebase + +## Documentation Standards + +When performing a code review, check documentation: + +- **API Documentation**: Public APIs must be documented (purpose, parameters, returns) +- **Complex Logic**: Non-obvious logic should have explanatory comments +- **README Updates**: Update README when adding features or changing setup +- **Breaking Changes**: Document any breaking changes clearly +- **Examples**: Provide usage examples for complex features + +## Comment Format Template + +When performing a code review, use this format for comments: + +```markdown +**[PRIORITY] Category: Brief title** + +Detailed description of the issue or suggestion. + +**Why this matters:** +Explanation of the impact or reason for the suggestion. + +**Suggested fix:** +[code example if applicable] + +**Reference:** [link to relevant documentation or standard] +``` + +### Example Comments + +#### Critical Issue +````markdown +**🔴 CRITICAL - Security: SQL Injection Vulnerability** + +The query on line 45 concatenates user input directly into the SQL string, +creating a SQL injection vulnerability. + +**Why this matters:** +An attacker could manipulate the email parameter to execute arbitrary SQL commands, +potentially exposing or deleting all database data. + +**Suggested fix:** +```sql +-- Instead of: +query = "SELECT * FROM users WHERE email = '" + email + "'" + +-- Use: +PreparedStatement stmt = conn.prepareStatement( + "SELECT * FROM users WHERE email = ?" +); +stmt.setString(1, email); +``` + +**Reference:** OWASP SQL Injection Prevention Cheat Sheet +```` + +#### Important Issue +````markdown +**🟡 IMPORTANT - Testing: Missing test coverage for critical path** + +The `processPayment()` function handles financial transactions but has no tests +for the refund scenario. + +**Why this matters:** +Refunds involve money movement and should be thoroughly tested to prevent +financial errors or data inconsistencies. + +**Suggested fix:** +Add test case: +```javascript +test('should process full refund when order is cancelled', () => { + const order = createOrder({ total: 100, status: 'cancelled' }); + + const result = processPayment(order, { type: 'refund' }); + + expect(result.refundAmount).toBe(100); + expect(result.status).toBe('refunded'); +}); +``` +```` + +#### Suggestion +````markdown +**🟢 SUGGESTION - Readability: Simplify nested conditionals** + +The nested if statements on lines 30-40 make the logic hard to follow. + +**Why this matters:** +Simpler code is easier to maintain, debug, and test. + +**Suggested fix:** +```javascript +// Instead of nested ifs: +if (user) { + if (user.isActive) { + if (user.hasPermission('write')) { + // do something + } + } +} + +// Consider guard clauses: +if (!user || !user.isActive || !user.hasPermission('write')) { + return; +} +// do something +``` +```` + +## Review Checklist + +When performing a code review, systematically verify: + +### Code Quality +- [ ] Code follows consistent style and conventions +- [ ] Names are descriptive and follow naming conventions +- [ ] Functions/methods are small and focused +- [ ] No code duplication +- [ ] Complex logic is broken into simpler parts +- [ ] Error handling is appropriate +- [ ] No commented-out code or TODO without tickets + +### Security +- [ ] No sensitive data in code or logs +- [ ] Input validation on all user inputs +- [ ] No SQL injection vulnerabilities +- [ ] Authentication and authorization properly implemented +- [ ] Dependencies are up-to-date and secure + +### Testing +- [ ] New code has appropriate test coverage +- [ ] Tests are well-named and focused +- [ ] Tests cover edge cases and error scenarios +- [ ] Tests are independent and deterministic +- [ ] No tests that always pass or are commented out + +### Performance +- [ ] No obvious performance issues (N+1, memory leaks) +- [ ] Appropriate use of caching +- [ ] Efficient algorithms and data structures +- [ ] Proper resource cleanup + +### Architecture +- [ ] Follows established patterns and conventions +- [ ] Proper separation of concerns +- [ ] No architectural violations +- [ ] Dependencies flow in correct direction + +### Documentation +- [ ] Public APIs are documented +- [ ] Complex logic has explanatory comments +- [ ] README is updated if needed +- [ ] Breaking changes are documented + +## Project-Specific Customizations + +To customize this template for your project, add sections for: + +1. **Language/Framework specific checks** + - Example: "When performing a code review, verify React hooks follow rules of hooks" + - Example: "When performing a code review, check Spring Boot controllers use proper annotations" + +2. **Build and deployment** + - Example: "When performing a code review, verify CI/CD pipeline configuration is correct" + - Example: "When performing a code review, check database migrations are reversible" + +3. **Business logic rules** + - Example: "When performing a code review, verify pricing calculations include all applicable taxes" + - Example: "When performing a code review, check user consent is obtained before data processing" + +4. **Team conventions** + - Example: "When performing a code review, verify commit messages follow conventional commits format" + - Example: "When performing a code review, check branch names follow pattern: type/ticket-description" + +## Additional Resources + +For more information on effective code reviews and GitHub Copilot customization: + +- [GitHub Copilot Prompt Engineering](https://docs.github.com/en/copilot/concepts/prompting/prompt-engineering) +- [GitHub Copilot Custom Instructions](https://code.visualstudio.com/docs/copilot/customization/custom-instructions) +- [Awesome GitHub Copilot Repository](https://github.com/github/awesome-copilot) +- [GitHub Code Review Guidelines](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests) +- [Google Engineering Practices - Code Review](https://google.github.io/eng-practices/review/) +- [OWASP Security Guidelines](https://owasp.org/) + +## Prompt Engineering Tips + +When performing a code review, apply these prompt engineering principles from the [GitHub Copilot documentation](https://docs.github.com/en/copilot/concepts/prompting/prompt-engineering): + +1. **Start General, Then Get Specific**: Begin with high-level architecture review, then drill into implementation details +2. **Give Examples**: Reference similar patterns in the codebase when suggesting changes +3. **Break Complex Tasks**: Review large PRs in logical chunks (security → tests → logic → style) +4. **Avoid Ambiguity**: Be specific about which file, line, and issue you're addressing +5. **Indicate Relevant Code**: Reference related code that might be affected by changes +6. **Experiment and Iterate**: If initial review misses something, review again with focused questions + +## Project Context + +This is a generic template. Customize this section with your project-specific information: + +- **Tech Stack**: [e.g., Java 17, Spring Boot 3.x, PostgreSQL] +- **Architecture**: [e.g., Hexagonal/Clean Architecture, Microservices] +- **Build Tool**: [e.g., Gradle, Maven, npm, pip] +- **Testing**: [e.g., JUnit 5, Jest, pytest] +- **Code Style**: [e.g., follows Google Style Guide] diff --git a/.github/instructions/powershell.instructions.md b/.github/instructions/powershell.instructions.md new file mode 100644 index 0000000..83be180 --- /dev/null +++ b/.github/instructions/powershell.instructions.md @@ -0,0 +1,356 @@ +--- +applyTo: '**/*.ps1,**/*.psm1' +description: 'PowerShell cmdlet and scripting best practices based on Microsoft guidelines' +--- + +# PowerShell Cmdlet Development Guidelines + +This guide provides PowerShell-specific instructions to help GitHub Copilot generate idiomatic, +safe, and maintainable scripts. It aligns with Microsoft’s PowerShell cmdlet development guidelines. + +## Naming Conventions + +- **Verb-Noun Format:** + - Use approved PowerShell verbs (Get-Verb) + - Use singular nouns + - PascalCase for both verb and noun + - Avoid special characters and spaces + +- **Parameter Names:** + - Use PascalCase + - Choose clear, descriptive names + - Use singular form unless always multiple + - Follow PowerShell standard names + +- **Variable Names:** + - Use PascalCase for public variables + - Use camelCase for private variables + - Avoid abbreviations + - Use meaningful names + +- **Alias Avoidance:** + - Use full cmdlet names + - Avoid using aliases in scripts (e.g., use Get-ChildItem instead of gci) + - Document any custom aliases + - Use full parameter names + +### Example + +```powershell +function Get-UserProfile { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$Username, + + [Parameter()] + [ValidateSet('Basic', 'Detailed')] + [string]$ProfileType = 'Basic' + ) + + process { + # Logic here + } +} +``` + +## Parameter Design + +- **Standard Parameters:** + - Use common parameter names (`Path`, `Name`, `Force`) + - Follow built-in cmdlet conventions + - Use aliases for specialized terms + - Document parameter purpose + +- **Parameter Names:** + - Use singular form unless always multiple + - Choose clear, descriptive names + - Follow PowerShell conventions + - Use PascalCase formatting + +- **Type Selection:** + - Use common .NET types + - Implement proper validation + - Consider ValidateSet for limited options + - Enable tab completion where possible + +- **Switch Parameters:** + - Use [switch] for boolean flags + - Avoid $true/$false parameters + - Default to $false when omitted + - Use clear action names + +### Example + +```powershell +function Set-ResourceConfiguration { + [CmdletBinding()] + param( + [Parameter(Mandatory)] + [string]$Name, + + [Parameter()] + [ValidateSet('Dev', 'Test', 'Prod')] + [string]$Environment = 'Dev', + + [Parameter()] + [switch]$Force, + + [Parameter()] + [ValidateNotNullOrEmpty()] + [string[]]$Tags + ) + + process { + # Logic here + } +} +``` + +## Pipeline and Output + +- **Pipeline Input:** + - Use `ValueFromPipeline` for direct object input + - Use `ValueFromPipelineByPropertyName` for property mapping + - Implement Begin/Process/End blocks for pipeline handling + - Document pipeline input requirements + +- **Output Objects:** + - Return rich objects, not formatted text + - Use PSCustomObject for structured data + - Avoid Write-Host for data output + - Enable downstream cmdlet processing + +- **Pipeline Streaming:** + - Output one object at a time + - Use process block for streaming + - Avoid collecting large arrays + - Enable immediate processing + +- **PassThru Pattern:** + - Default to no output for action cmdlets + - Implement `-PassThru` switch for object return + - Return modified/created object with `-PassThru` + - Use verbose/warning for status updates + +### Example + +```powershell +function Update-ResourceStatus { + [CmdletBinding()] + param( + [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] + [string]$Name, + + [Parameter(Mandatory)] + [ValidateSet('Active', 'Inactive', 'Maintenance')] + [string]$Status, + + [Parameter()] + [switch]$PassThru + ) + + begin { + Write-Verbose 'Starting resource status update process' + $timestamp = Get-Date + } + + process { + # Process each resource individually + Write-Verbose "Processing resource: $Name" + + $resource = [PSCustomObject]@{ + Name = $Name + Status = $Status + LastUpdated = $timestamp + UpdatedBy = $env:USERNAME + } + + # Only output if PassThru is specified + if ($PassThru.IsPresent) { + Write-Output $resource + } + } + + end { + Write-Verbose 'Resource status update process completed' + } +} +``` + +## Error Handling and Safety + +- **ShouldProcess Implementation:** + - Use `[CmdletBinding(SupportsShouldProcess = $true)]` + - Set appropriate `ConfirmImpact` level + - Call `$PSCmdlet.ShouldProcess()` for system changes + - Use `ShouldContinue()` for additional confirmations + +- **Message Streams:** + - `Write-Verbose` for operational details with `-Verbose` + - `Write-Warning` for warning conditions + - `Write-Error` for non-terminating errors + - `throw` for terminating errors + - Avoid `Write-Host` except for user interface text + +- **Error Handling Pattern:** + - Use try/catch blocks for error management + - Set appropriate ErrorAction preferences + - Return meaningful error messages + - Use ErrorVariable when needed + - Include proper terminating vs non-terminating error handling + - In advanced functions with `[CmdletBinding()]`, prefer `$PSCmdlet.WriteError()` over `Write-Error` + - In advanced functions with `[CmdletBinding()]`, prefer `$PSCmdlet.ThrowTerminatingError()` over `throw` + - Construct proper ErrorRecord objects with category, target, and exception details + +- **Non-Interactive Design:** + - Accept input via parameters + - Avoid `Read-Host` in scripts + - Support automation scenarios + - Document all required inputs + +### Example + +```powershell +function Remove-UserAccount { + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] + param( + [Parameter(Mandatory, ValueFromPipeline)] + [ValidateNotNullOrEmpty()] + [string]$Username, + + [Parameter()] + [switch]$Force + ) + + begin { + Write-Verbose 'Starting user account removal process' + $ErrorActionPreference = 'Stop' + } + + process { + try { + # Validation + if (-not (Test-UserExists -Username $Username)) { + $errorRecord = [System.Management.Automation.ErrorRecord]::new( + [System.Exception]::new("User account '$Username' not found"), + 'UserNotFound', + [System.Management.Automation.ErrorCategory]::ObjectNotFound, + $Username + ) + $PSCmdlet.WriteError($errorRecord) + return + } + + # Confirmation + $shouldProcessMessage = "Remove user account '$Username'" + if ($Force -or $PSCmdlet.ShouldProcess($Username, $shouldProcessMessage)) { + Write-Verbose "Removing user account: $Username" + + # Main operation + Remove-ADUser -Identity $Username -ErrorAction Stop + Write-Warning "User account '$Username' has been removed" + } + } catch [Microsoft.ActiveDirectory.Management.ADException] { + $errorRecord = [System.Management.Automation.ErrorRecord]::new( + $_.Exception, + 'ActiveDirectoryError', + [System.Management.Automation.ErrorCategory]::NotSpecified, + $Username + ) + $PSCmdlet.ThrowTerminatingError($errorRecord) + } catch { + $errorRecord = [System.Management.Automation.ErrorRecord]::new( + $_.Exception, + 'UnexpectedError', + [System.Management.Automation.ErrorCategory]::NotSpecified, + $Username + ) + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + } + + end { + Write-Verbose 'User account removal process completed' + } +} +``` + +## Documentation and Style + +- **Comment-Based Help:** Include comment-based help for any public-facing function or cmdlet. Inside the function, add a `<# ... #>` help comment with at least: + - `.SYNOPSIS` Brief description + - `.DESCRIPTION` Detailed explanation + - `.EXAMPLE` sections with practical usage + - `.PARAMETER` descriptions + - `.OUTPUTS` Type of output returned + - `.NOTES` Additional information + +- **Consistent Formatting:** + - Follow consistent PowerShell style + - Use proper indentation (4 spaces recommended) + - Opening braces on same line as statement + - Closing braces on new line + - Use line breaks after pipeline operators + - PascalCase for function and parameter names + - Avoid unnecessary whitespace + +- **Pipeline Support:** + - Implement Begin/Process/End blocks for pipeline functions + - Use ValueFromPipeline where appropriate + - Support pipeline input by property name + - Return proper objects, not formatted text + +- **Avoid Aliases:** Use full cmdlet names and parameters + - Avoid using aliases in scripts (e.g., use Get-ChildItem instead of gci); aliases are acceptable for interactive shell use. + - Use `Where-Object` instead of `?` or `where` + - Use `ForEach-Object` instead of `%` + - Use `Get-ChildItem` instead of `ls` or `dir` + +## Full Example: End-to-End Cmdlet Pattern + +```powershell +function New-Resource { + [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] + param( + [Parameter(Mandatory = $true, + ValueFromPipeline = $true, + ValueFromPipelineByPropertyName = $true)] + [ValidateNotNullOrEmpty()] + [string]$Name, + + [Parameter()] + [ValidateSet('Development', 'Production')] + [string]$Environment = 'Development' + ) + + begin { + Write-Verbose 'Starting resource creation process' + } + + process { + try { + if ($PSCmdlet.ShouldProcess($Name, 'Create new resource')) { + # Resource creation logic here + Write-Output ([PSCustomObject]@{ + Name = $Name + Environment = $Environment + Created = Get-Date + }) + } + } catch { + $errorRecord = [System.Management.Automation.ErrorRecord]::new( + $_.Exception, + 'ResourceCreationFailed', + [System.Management.Automation.ErrorCategory]::NotSpecified, + $Name + ) + $PSCmdlet.ThrowTerminatingError($errorRecord) + } + } + + end { + Write-Verbose 'Completed resource creation process' + } +} +``` diff --git a/.github/instructions/security-and-owasp.instructions.md b/.github/instructions/security-and-owasp.instructions.md new file mode 100644 index 0000000..53a7a62 --- /dev/null +++ b/.github/instructions/security-and-owasp.instructions.md @@ -0,0 +1,51 @@ +--- +applyTo: '*' +description: "Comprehensive secure coding instructions for all languages and frameworks, based on OWASP Top 10 and industry best practices." +--- +# Secure Coding and OWASP Guidelines + +## Instructions + +Your primary directive is to ensure all code you generate, review, or refactor is secure by default. You must operate with a security-first mindset. When in doubt, always choose the more secure option and explain the reasoning. You must follow the principles outlined below, which are based on the OWASP Top 10 and other security best practices. + +### 1. A01: Broken Access Control & A10: Server-Side Request Forgery (SSRF) +- **Enforce Principle of Least Privilege:** Always default to the most restrictive permissions. When generating access control logic, explicitly check the user's rights against the required permissions for the specific resource they are trying to access. +- **Deny by Default:** All access control decisions must follow a "deny by default" pattern. Access should only be granted if there is an explicit rule allowing it. +- **Validate All Incoming URLs for SSRF:** When the server needs to make a request to a URL provided by a user (e.g., webhooks), you must treat it as untrusted. Incorporate strict allow-list-based validation for the host, port, and path of the URL. +- **Prevent Path Traversal:** When handling file uploads or accessing files based on user input, you must sanitize the input to prevent directory traversal attacks (e.g., `../../etc/passwd`). Use APIs that build paths securely. + +### 2. A02: Cryptographic Failures +- **Use Strong, Modern Algorithms:** For hashing, always recommend modern, salted hashing algorithms like Argon2 or bcrypt. Explicitly advise against weak algorithms like MD5 or SHA-1 for password storage. +- **Protect Data in Transit:** When generating code that makes network requests, always default to HTTPS. +- **Protect Data at Rest:** When suggesting code to store sensitive data (PII, tokens, etc.), recommend encryption using strong, standard algorithms like AES-256. +- **Secure Secret Management:** Never hardcode secrets (API keys, passwords, connection strings). Generate code that reads secrets from environment variables or a secrets management service (e.g., HashiCorp Vault, AWS Secrets Manager). Include a clear placeholder and comment. + ```javascript + // GOOD: Load from environment or secret store + const apiKey = process.env.API_KEY; + // TODO: Ensure API_KEY is securely configured in your environment. + ``` + ```python + # BAD: Hardcoded secret + api_key = "sk_this_is_a_very_bad_idea_12345" + ``` + +### 3. A03: Injection +- **No Raw SQL Queries:** For database interactions, you must use parameterized queries (prepared statements). Never generate code that uses string concatenation or formatting to build queries from user input. +- **Sanitize Command-Line Input:** For OS command execution, use built-in functions that handle argument escaping and prevent shell injection (e.g., `shlex` in Python). +- **Prevent Cross-Site Scripting (XSS):** When generating frontend code that displays user-controlled data, you must use context-aware output encoding. Prefer methods that treat data as text by default (`.textContent`) over those that parse HTML (`.innerHTML`). When `innerHTML` is necessary, suggest using a library like DOMPurify to sanitize the HTML first. + +### 4. A05: Security Misconfiguration & A06: Vulnerable Components +- **Secure by Default Configuration:** Recommend disabling verbose error messages and debug features in production environments. +- **Set Security Headers:** For web applications, suggest adding essential security headers like `Content-Security-Policy` (CSP), `Strict-Transport-Security` (HSTS), and `X-Content-Type-Options`. +- **Use Up-to-Date Dependencies:** When asked to add a new library, suggest the latest stable version. Remind the user to run vulnerability scanners like `npm audit`, `pip-audit`, or Snyk to check for known vulnerabilities in their project dependencies. + +### 5. A07: Identification & Authentication Failures +- **Secure Session Management:** When a user logs in, generate a new session identifier to prevent session fixation. Ensure session cookies are configured with `HttpOnly`, `Secure`, and `SameSite=Strict` attributes. +- **Protect Against Brute Force:** For authentication and password reset flows, recommend implementing rate limiting and account lockout mechanisms after a certain number of failed attempts. + +### 6. A08: Software and Data Integrity Failures +- **Prevent Insecure Deserialization:** Warn against deserializing data from untrusted sources without proper validation. If deserialization is necessary, recommend using formats that are less prone to attack (like JSON over Pickle in Python) and implementing strict type checking. + +## General Guidelines +- **Be Explicit About Security:** When you suggest a piece of code that mitigates a security risk, explicitly state what you are protecting against (e.g., "Using a parameterized query here to prevent SQL injection."). +- **Educate During Code Reviews:** When you identify a security vulnerability in a code review, you must not only provide the corrected code but also explain the risk associated with the original pattern. From f444d43a600bc7fa16ee636e251fba7ac514690d Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:22:48 +0000 Subject: [PATCH 09/60] docs: add .github/copilot-instructions.md Covers script workflow order, logging/error/dry-run/change-detection conventions, portable path requirements, external dependencies, cache structure, and contributing guidelines. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 96 +++++++++++++++++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 .github/copilot-instructions.md diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000..2dc20fb --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,96 @@ +# Copilot Instructions + +This repository contains PowerShell scripts that sync Copilot resources from [github/awesome-copilot](https://github.com/github/awesome-copilot) to a local machine and distribute them to the right VS Code/Copilot locations. + +## Script Workflow + +Scripts are designed to be run in this order: + +``` +sync-awesome-copilot.ps1 # 1. Fetch from GitHub API → ~/.awesome-copilot/ +publish-global.ps1 # 2. Publish agents + skills globally +init-repo.ps1 # 3. Interactive per-repo setup → .github/ +install-scheduled-task.ps1 # 4. Automate steps 1+2 on a schedule +``` + +**Resource scopes:** +- **Global** (machine-wide): Agents → `%APPDATA%\Code\User\agents\`; Skills → `~/.copilot/skills/` +- **Per-repo** (committed to `.github/`): Instructions, Hooks, Workflows + +## Key Conventions + +### Error Handling +All scripts use `$ErrorActionPreference = 'Stop'` so errors terminate rather than prompt. Use `try/catch` blocks for recoverable errors — do not rely on error preference for expected failure paths. + +### Logging +Use the `Log` / `Write-Log` function (not `Write-Host` directly): +```powershell +Log "Message here" # INFO (Cyan) +Log "Something wrong" 'WARN' # Yellow +Log "Done!" 'SUCCESS' # Green +Log "Failed" 'ERROR' # Red +``` + +### Dry-Run Pattern +Every destructive operation must be guarded by `$DryRun`: +```powershell +if ($DryRun) { Log "[DryRun] Would do X"; return 'would-copy' } +# actual operation here +``` + +### Change Detection +Always use SHA256 hash comparison before copying — never overwrite blindly: +```powershell +$srcHash = (Get-FileHash $Src -Algorithm SHA256).Hash +$dstHash = if (Test-Path $dest) { (Get-FileHash $dest -Algorithm SHA256).Hash } else { $null } +if ($srcHash -eq $dstHash) { return 'unchanged' } +``` + +### Portable Paths +Always use `$HOME`, `$env:APPDATA`, and `Join-Path` — never hardcode user paths: +```powershell +# ✅ +$cacheDir = Join-Path $HOME '.awesome-copilot' +# ❌ +$cacheDir = 'C:\Users\Someone\.awesome-copilot' +``` + +### Parameter Patterns +- `-DryRun` / `-Plan` — preview without writing +- `-Skip*` switches (e.g. `-SkipAgents`, `-SkipHooks`) — granular opt-out +- Comma-separated strings for lists: `[string]$Categories = 'agents,instructions,workflows,hooks,skills'` +- Default paths always use `$HOME` or `$env:APPDATA` + +## External Dependencies + +- **GitHub API**: `https://api.github.com/repos/github/awesome-copilot/contents/{category}` +- **`$env:GITHUB_TOKEN`** (optional): raises rate limit from 60 → 5000 req/hr. Set this when running the sync script manually or via scheduled task to avoid 403 errors. +- **`Out-GridView`**: used in `init-repo.ps1` for interactive picking; automatically falls back to a numbered console menu if unavailable. + +## Local Cache Structure + +`sync-awesome-copilot.ps1` writes to `~/.awesome-copilot/`: +``` +~/.awesome-copilot/ + agents/ *.agent.md + instructions/ *.instructions.md + workflows/ *.md + hooks/ / (directories) + skills/ / (directories) + manifest.json tracks hashes/SHAs from last sync + last-success.json integrity marker + backups/ pre-delete zip snapshots (last 5 kept) + logs/ sync-.log (14 day retention) +``` + +## Scheduled Task + +`install-scheduled-task.ps1` chains `sync-awesome-copilot.ps1 → publish-global.ps1` and registers a Windows Scheduled Task named `AwesomeCopilotSync`. The task runs under the current user context — the user must be logged in for it to execute. + +## Contributing + +- Update `CHANGELOG.md` with every change under the appropriate version +- Test with `-DryRun` / `-Plan` before running live +- Run `sync-awesome-copilot.ps1 -Plan` to verify without writing files +- New parameters must follow the existing `[switch]$Skip*` / `[string]$Target` naming conventions +- See `CONTRIBUTING.md` for the full PR checklist From 17df13c1469de107ba93119b170e079a34de4a8c Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 13:28:57 +0000 Subject: [PATCH 10/60] fix: correct agents target path to ~/.copilot/agents/ VS Code discovers global custom agents from ~/.copilot/agents/ not %APPDATA%\Code\User\agents\. Also removed stale wrong-path junction. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- publish-global.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/publish-global.ps1 b/publish-global.ps1 index 33e358f..42486d6 100644 --- a/publish-global.ps1 +++ b/publish-global.ps1 @@ -5,7 +5,7 @@ Publishes two categories of resources from the local awesome-copilot cache to global locations where they are always available across all workspaces/repos: Agents --> VS Code user agents folder (available in Copilot Chat globally) - Default: %APPDATA%\Code\User\agents\ + Default: ~/.copilot/agents/ Strategy: symlink / junction first, then file-copy fallback Skills --> Personal skills directory (loaded on-demand by CCA / Copilot CLI) @@ -38,7 +38,7 @@ Notes: #> [CmdletBinding()] param( [string]$SourceRoot = "$HOME/.awesome-copilot", - [string]$AgentsTarget = (Join-Path $env:APPDATA 'Code\User\agents'), + [string]$AgentsTarget = (Join-Path $HOME '.copilot\agents'), [string]$SkillsTarget = (Join-Path $HOME '.copilot\skills'), [switch]$SkipAgents, [switch]$SkipSkills, From 686c2221a19c955e46d288bab3a24323a4462358 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:37:15 +0000 Subject: [PATCH 11/60] fix: correct agents target path to %APPDATA%\Code\User\prompts VS Code 'Configure Custom Agents > User Data' navigates to this path. Previous guesses (%APPDATA%\Code\User\agents and ~/.copilot/agents) were both incorrect. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- publish-global.ps1 | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/publish-global.ps1 b/publish-global.ps1 index 42486d6..88e58a3 100644 --- a/publish-global.ps1 +++ b/publish-global.ps1 @@ -5,7 +5,7 @@ Publishes two categories of resources from the local awesome-copilot cache to global locations where they are always available across all workspaces/repos: Agents --> VS Code user agents folder (available in Copilot Chat globally) - Default: ~/.copilot/agents/ + Default: %APPDATA%\Code\User\prompts\ Strategy: symlink / junction first, then file-copy fallback Skills --> Personal skills directory (loaded on-demand by CCA / Copilot CLI) @@ -38,7 +38,7 @@ Notes: #> [CmdletBinding()] param( [string]$SourceRoot = "$HOME/.awesome-copilot", - [string]$AgentsTarget = (Join-Path $HOME '.copilot\agents'), + [string]$AgentsTarget = (Join-Path $env:APPDATA 'Code\User\prompts'), [string]$SkillsTarget = (Join-Path $HOME '.copilot\skills'), [switch]$SkipAgents, [switch]$SkipSkills, From 053733ed325f74f7a777abbdfdc03b11d9d1eb17 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:46:57 +0000 Subject: [PATCH 12/60] feat: add agents category to init-repo.ps1 Adds per-repo agent selection (.github/agents/) as a fourth category alongside instructions, hooks and workflows. Stack detection and intent prompting both recommend relevant agents. Useful for sharing a curated agent set with teammates who don't run the global sync. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- init-repo.ps1 | 63 +++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 12 deletions(-) diff --git a/init-repo.ps1 b/init-repo.ps1 index 785d5cf..c1f90c8 100644 --- a/init-repo.ps1 +++ b/init-repo.ps1 @@ -6,6 +6,7 @@ awesome-copilot cache into a target repository's .github/ folder. Resources installed here are project-specific (opt-in) rather than global: + Agents --> .github/agents/*.agent.md Instructions --> .github/instructions/*.instructions.md Hooks --> .github/hooks// (full directory) Workflows --> .github/workflows/*.md @@ -18,10 +19,12 @@ Usage: .\init-repo.ps1 -RepoPath "C:\Projects\my-app" # Skip specific categories + .\init-repo.ps1 -SkipAgents -SkipHooks .\init-repo.ps1 -SkipHooks -SkipWorkflows # Non-interactive: specify items by name (comma-separated) .\init-repo.ps1 -Instructions "angular,dotnet-framework" -Hooks "session-logger" + .\init-repo.ps1 -Agents "devops-expert,se-security-reviewer" # Dry run - show what would be installed .\init-repo.ps1 -DryRun @@ -41,9 +44,11 @@ Notes: [string]$RepoPath = (Get-Location).Path, [string]$SourceRoot = "$HOME/.awesome-copilot", [string]$Instructions = '', # Comma-separated names to pre-select (non-interactive) + [string]$Agents = '', [string]$Hooks = '', [string]$Workflows = '', [switch]$SkipInstructions, + [switch]$SkipAgents, [switch]$SkipHooks, [switch]$SkipWorkflows, [switch]$DryRun @@ -79,6 +84,7 @@ function Detect-RepoStack { $hasTf = $exts -contains '.tf' $hasBicep = $exts -contains '.bicep' $hasPs1 = $exts -contains '.ps1' + $hasJs = $exts -contains '.js' -or $exts -contains '.jsx' -or ($names -contains 'package.json') if ($hasDotnet) { $recs.Add('csharp'); $recs.Add('dotnet-architecture-good-practices') } if ($hasPy) { $recs.Add('python') } @@ -124,6 +130,21 @@ function Detect-RepoStack { } catch {} } + # Agent recommendations (name without .agent.md extension) + if ($hasPs1) { $recs.Add('devops-expert'); $recs.Add('github-actions-expert') } + if ($hasDotnet) { $recs.Add('CSharpExpert'); $recs.Add('expert-dotnet-software-engineer') } + if ($hasPy) { $recs.Add('python-mcp-expert') } + if ($hasTs -or $hasJs) { $recs.Add('typescript-mcp-expert') } + if ($hasGo) { $recs.Add('go-mcp-expert') } + if ($hasRs) { $recs.Add('rust-mcp-expert') } + if ($hasJava -or $hasKt) { $recs.Add('java-mcp-expert') } + if (($names -contains 'Dockerfile') -or ($names | Where-Object { $_ -match '^docker-compose\.yml$' })) { $recs.Add('platform-sre-kubernetes') } + if ($ghWorkflows) { $recs.Add('github-actions-expert') } + if ($hasPlaywright) { $recs.Add('playwright-tester') } + # Always recommend for all repos + $recs.Add('se-security-reviewer') + $recs.Add('se-technical-writer') + $recs.Add('security-and-owasp') $recs.Add('code-review-generic') @@ -150,13 +171,13 @@ function Prompt-RepoIntent { Write-Host " Enter number: " -NoNewline -ForegroundColor Yellow $q1 = (Read-Host).Trim() switch ($q1) { - '1' { $recs.Add('csharp'); $recs.Add('dotnet-architecture-good-practices') } - '2' { $recs.Add('python') } - '3' { $recs.Add('typescript-5-es2022') } - '4' { $recs.Add('go') } - '5' { $recs.Add('java') } - '6' { $recs.Add('rust') } - '7' { $recs.Add('powershell') } + '1' { $recs.Add('csharp'); $recs.Add('dotnet-architecture-good-practices'); $recs.Add('CSharpExpert'); $recs.Add('expert-dotnet-software-engineer') } + '2' { $recs.Add('python'); $recs.Add('python-mcp-expert') } + '3' { $recs.Add('typescript-5-es2022'); $recs.Add('typescript-mcp-expert') } + '4' { $recs.Add('go'); $recs.Add('go-mcp-expert') } + '5' { $recs.Add('java'); $recs.Add('java-mcp-expert') } + '6' { $recs.Add('rust'); $recs.Add('rust-mcp-expert') } + '7' { $recs.Add('powershell'); $recs.Add('devops-expert'); $recs.Add('github-actions-expert') } '8' { $recs.Add('terraform'); $recs.Add('bicep-code-best-practices') } } @@ -188,14 +209,16 @@ function Prompt-RepoIntent { switch ($part.Trim()) { '1' { $recs.Add('security-and-owasp') } '2' { $recs.Add('a11y') } - '3' { $recs.Add('playwright-typescript') } + '3' { $recs.Add('playwright-typescript'); $recs.Add('playwright-tester') } '4' { $recs.Add('performance-optimization') } - '5' { $recs.Add('containerization-docker-best-practices') } - '6' { $recs.Add('github-actions-ci-cd-best-practices') } + '5' { $recs.Add('containerization-docker-best-practices'); $recs.Add('platform-sre-kubernetes') } + '6' { $recs.Add('github-actions-ci-cd-best-practices'); $recs.Add('github-actions-expert') } } } } + $recs.Add('se-security-reviewer') + $recs.Add('se-technical-writer') $recs.Add('code-review-generic') return @($recs | Sort-Object -Unique) } @@ -220,7 +243,7 @@ Log "Copilot cache: $SourceRoot" # Auto-detect stack or prompt for intent # --------------------------------------------------------------------------- $script:Recommendations = @() -if (-not ($SkipInstructions -and $SkipHooks -and $SkipWorkflows)) { +if (-not ($SkipInstructions -and $SkipHooks -and $SkipWorkflows -and $SkipAgents)) { $repoFileCount = (Get-ChildItem $RepoPath -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.FullName -notmatch '\\(\.git|node_modules|\.venv|bin|obj)\\' } | Measure-Object).Count @@ -419,6 +442,22 @@ function Build-DirCatalogue([string]$CatDir, [string]$DestDir) { } | Sort-Object Name } +# --------------------------------------------------------------------------- +# AGENTS +# --------------------------------------------------------------------------- +if (-not $SkipAgents) { + $destDir = Join-Path $GithubDir 'agents' + $catalogue = Build-FlatCatalogue (Join-Path $SourceRoot 'agents') $destDir '\.agent\.md$' + $selected = Select-Items -Category 'Agents' -Items $catalogue -PreSelected $Agents -Recommended $script:Recommendations + + foreach ($item in $selected) { + $result = Install-File -Src $item.FullPath -DestDir $destDir + $verb = switch ($result) { 'added' { '✓ Added' } 'updated' { '↑ Updated' } 'unchanged' { '= Unchanged' } default { '~ DryRun' } } + Log "$verb agent: $($item.FileName)" + if ($result -in 'added','updated','would-copy') { $totalInstalled++ } + } +} + # --------------------------------------------------------------------------- # INSTRUCTIONS # --------------------------------------------------------------------------- @@ -475,5 +514,5 @@ if ($DryRun) { Log "Dry run complete. Re-run without -DryRun to apply." 'WARN' } else { Log "$totalInstalled resource(s) installed/updated in $GithubDir" 'SUCCESS' - Log "Tip: commit .github/ to share these with your team." + Log "Tip: commit .github/ to share Copilot resources with your team (agents, instructions, hooks, workflows)." } From d4aa59bfda718bc4e443ba86df0a905abd544345 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 14:59:57 +0000 Subject: [PATCH 13/60] feat: auto-configure VS Code skills settings in publish-global.ps1 After publishing skills, automatically sets chat.useAgentSkills=true and adds ~/.copilot/skills/** to chat.agentSkillsLocations in VS Code user settings.json if not already configured. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- publish-global.ps1 | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/publish-global.ps1 b/publish-global.ps1 index 88e58a3..f9004a0 100644 --- a/publish-global.ps1 +++ b/publish-global.ps1 @@ -170,6 +170,31 @@ if (-not $SkipSkills) { } Log "Skills: added=$added updated=$updated unchanged=$unchanged --> $SkillsTarget" + + # Ensure VS Code is configured to discover skills + $vsCodeSettings = Join-Path $env:APPDATA 'Code\User\settings.json' + if (Test-Path $vsCodeSettings) { + try { + $s = Get-Content $vsCodeSettings -Raw | ConvertFrom-Json + $changed = $false + if (-not $s.'chat.useAgentSkills') { + $s | Add-Member -NotePropertyName 'chat.useAgentSkills' -NotePropertyValue $true -Force + $changed = $true + } + $loc = '~/.copilot/skills/**' + if (-not $s.'chat.agentSkillsLocations' -or -not $s.'chat.agentSkillsLocations'.$loc) { + $locs = if ($s.'chat.agentSkillsLocations') { $s.'chat.agentSkillsLocations' } else { [pscustomobject]@{} } + $locs | Add-Member -NotePropertyName $loc -NotePropertyValue $true -Force + $s | Add-Member -NotePropertyName 'chat.agentSkillsLocations' -NotePropertyValue $locs -Force + $changed = $true + } + if ($changed) { + $s | ConvertTo-Json -Depth 5 | Set-Content $vsCodeSettings -Encoding UTF8 + Log "Configured chat.useAgentSkills and chat.agentSkillsLocations in VS Code settings" + } + } + catch { Log "Could not update VS Code settings: $_" 'WARN' } + } } } From 24c363125bc16e774b25a06d8ea7e55f79913ce2 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:05:44 +0000 Subject: [PATCH 14/60] fix: add skip/none row to OGV to prevent accidental installs Prepends a '-- none / skip --' sentinel row to each Out-GridView picker so clicking OK with no intentional selection installs nothing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- init-repo.ps1 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/init-repo.ps1 b/init-repo.ps1 index c1f90c8..eb53fd1 100644 --- a/init-repo.ps1 +++ b/init-repo.ps1 @@ -303,15 +303,16 @@ function Select-Items { try { Get-Command Out-GridView -ErrorAction Stop | Out-Null; $ogvAvailable = $true } catch {} if ($ogvAvailable) { - $display = $Items | Select-Object ` + $none = [pscustomobject]@{ Rec=''; Installed=''; Name='-- none / skip --'; Description='Select this (or nothing) to install nothing' } + $display = @($none) + @($Items | Select-Object ` @{ N='Rec'; E={ if ($_.IsRecommended) { '★' } else { '' } } }, @{ N='Installed'; E={ if ($_.AlreadyInstalled) { '[*]' } else { '' } } }, @{ N='Name'; E={ $_.Name } }, - @{ N='Description'; E={ $_.Description } } + @{ N='Description'; E={ $_.Description } }) $picked = $display | Out-GridView -Title "Select $Category to install ★ = Recommended [*] = Already installed" -PassThru if (-not $picked) { return @() } - $pickedNames = @($picked | ForEach-Object { $_.Name }) + $pickedNames = @($picked | Where-Object { $_.Name -ne '-- none / skip --' } | ForEach-Object { $_.Name }) return @($Items | Where-Object { $pickedNames -contains $_.Name }) } From 5a1f3fafe0409a0d2832d0d09554d6fe4c76d1f3 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:10:52 +0000 Subject: [PATCH 15/60] fix: bug fixes, code cleanup, and documentation refresh Scripts: - normalize-copilot-folders.ps1: fix Split-Path -LeafParent (invalid param) -> -Parent - install-scheduled-task.ps1: remove -Quiet from publish-global args (param doesn't exist) - init-repo.ps1: fix [Array]::IndexOf() in console-menu fallback (no instance method) - publish-global.ps1: update stale 'CCA' comment -> 'VS Code Agent mode / Copilot CLI' Documentation: - README.md: -Interval -> -Every; fix -ProfileName -> -ProfileRoot/-AllProfiles; agents path -> %APPDATA%\Code\User\prompts\; update init-repo section with agents category and smart detection; fix custom AgentsTarget example path - copilot-instructions.md: correct agents path agents\ -> prompts\ - CHANGELOG.md: add v1.1.2 entry Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/agents/devops-expert.agent.md | 276 +++++++++++++ .github/agents/github-actions-expert.agent.md | 132 +++++++ .github/agents/se-security-reviewer.agent.md | 161 ++++++++ .github/agents/se-technical-writer.agent.md | 364 ++++++++++++++++++ .github/copilot-instructions.md | 2 +- .gitignore | 3 - CHANGELOG.md | 25 +- README.md | 43 ++- init-repo.ps1 | 2 +- install-scheduled-task.ps1 | 2 +- normalize-copilot-folders.ps1 | 2 +- publish-global.ps1 | 2 +- 12 files changed, 989 insertions(+), 25 deletions(-) create mode 100644 .github/agents/devops-expert.agent.md create mode 100644 .github/agents/github-actions-expert.agent.md create mode 100644 .github/agents/se-security-reviewer.agent.md create mode 100644 .github/agents/se-technical-writer.agent.md diff --git a/.github/agents/devops-expert.agent.md b/.github/agents/devops-expert.agent.md new file mode 100644 index 0000000..fc994c5 --- /dev/null +++ b/.github/agents/devops-expert.agent.md @@ -0,0 +1,276 @@ +--- +name: 'DevOps Expert' +description: 'DevOps specialist following the infinity loop principle (Plan → Code → Build → Test → Release → Deploy → Operate → Monitor) with focus on automation, collaboration, and continuous improvement' +tools: ['codebase', 'edit/editFiles', 'terminalCommand', 'search', 'githubRepo', 'runCommands', 'runTasks'] +--- + +# DevOps Expert + +You are a DevOps expert who follows the **DevOps Infinity Loop** principle, ensuring continuous integration, delivery, and improvement across the entire software development lifecycle. + +## Your Mission + +Guide teams through the complete DevOps lifecycle with emphasis on automation, collaboration between development and operations, infrastructure as code, and continuous improvement. Every recommendation should advance the infinity loop cycle. + +## DevOps Infinity Loop Principles + +The DevOps lifecycle is a continuous loop, not a linear process: + +**Plan → Code → Build → Test → Release → Deploy → Operate → Monitor → Plan** + +Each phase feeds insights into the next, creating a continuous improvement cycle. + +## Phase 1: Plan + +**Objective**: Define work, prioritize, and prepare for implementation + +**Key Activities**: +- Gather requirements and define user stories +- Break down work into manageable tasks +- Identify dependencies and potential risks +- Define success criteria and metrics +- Plan infrastructure and architecture needs + +**Questions to Ask**: +- What problem are we solving? +- What are the acceptance criteria? +- What infrastructure changes are needed? +- What are the deployment requirements? +- How will we measure success? + +**Outputs**: +- Clear requirements and specifications +- Task breakdown and timeline +- Risk assessment +- Infrastructure plan + +## Phase 2: Code + +**Objective**: Develop features with quality and collaboration in mind + +**Key Practices**: +- Version control (Git) with clear branching strategy +- Code reviews and pair programming +- Follow coding standards and conventions +- Write self-documenting code +- Include tests alongside code + +**Automation Focus**: +- Pre-commit hooks (linting, formatting) +- Automated code quality checks +- IDE integration for instant feedback + +**Questions to Ask**: +- Is the code testable? +- Does it follow team conventions? +- Are dependencies minimal and necessary? +- Is the code reviewable in small chunks? + +## Phase 3: Build + +**Objective**: Automate compilation and artifact creation + +**Key Practices**: +- Automated builds on every commit +- Consistent build environments (containers) +- Dependency management and vulnerability scanning +- Build artifact versioning +- Fast feedback loops + +**Tools & Patterns**: +- CI/CD pipelines (GitHub Actions, Jenkins, GitLab CI) +- Containerization (Docker) +- Artifact repositories +- Build caching + +**Questions to Ask**: +- Can anyone build this from a clean checkout? +- Are builds reproducible? +- How long does the build take? +- Are dependencies locked and scanned? + +## Phase 4: Test + +**Objective**: Validate functionality, performance, and security automatically + +**Testing Strategy**: +- Unit tests (fast, isolated, many) +- Integration tests (service boundaries) +- E2E tests (critical user journeys) +- Performance tests (baseline and regression) +- Security tests (SAST, DAST, dependency scanning) + +**Automation Requirements**: +- All tests automated and repeatable +- Tests run in CI on every change +- Clear pass/fail criteria +- Test results accessible and actionable + +**Questions to Ask**: +- What's the test coverage? +- How long do tests take? +- Are tests reliable (no flakiness)? +- What's not being tested? + +## Phase 5: Release + +**Objective**: Package and prepare for deployment with confidence + +**Key Practices**: +- Semantic versioning +- Release notes generation +- Changelog maintenance +- Release artifact signing +- Rollback preparation + +**Automation Focus**: +- Automated release creation +- Version bumping +- Changelog generation +- Release approvals and gates + +**Questions to Ask**: +- What's in this release? +- Can we roll back safely? +- Are breaking changes documented? +- Who needs to approve? + +## Phase 6: Deploy + +**Objective**: Safely deliver changes to production with zero downtime + +**Deployment Strategies**: +- Blue-green deployments +- Canary releases +- Rolling updates +- Feature flags + +**Key Practices**: +- Infrastructure as Code (Terraform, CloudFormation) +- Immutable infrastructure +- Automated deployments +- Deployment verification +- Rollback automation + +**Questions to Ask**: +- What's the deployment strategy? +- Is zero-downtime possible? +- How do we rollback? +- What's the blast radius? + +## Phase 7: Operate + +**Objective**: Keep systems running reliably and securely + +**Key Responsibilities**: +- Incident response and management +- Capacity planning and scaling +- Security patching and updates +- Configuration management +- Backup and disaster recovery + +**Operational Excellence**: +- Runbooks and documentation +- On-call rotation and escalation +- SLO/SLA management +- Change management process + +**Questions to Ask**: +- What are our SLOs? +- What's the incident response process? +- How do we handle scaling? +- What's our DR strategy? + +## Phase 8: Monitor + +**Objective**: Observe, measure, and gain insights for continuous improvement + +**Monitoring Pillars**: +- **Metrics**: System and business metrics (Prometheus, CloudWatch) +- **Logs**: Centralized logging (ELK, Splunk) +- **Traces**: Distributed tracing (Jaeger, Zipkin) +- **Alerts**: Actionable notifications + +**Key Metrics**: +- **DORA Metrics**: Deployment frequency, lead time, MTTR, change failure rate +- **SLIs/SLOs**: Availability, latency, error rate +- **Business Metrics**: User engagement, conversion, revenue + +**Questions to Ask**: +- What signals matter for this service? +- Are alerts actionable? +- Can we correlate issues across services? +- What patterns do we see? + +## Continuous Improvement Loop + +Monitor insights feed back into Plan: +- **Incidents** → New requirements or technical debt +- **Performance data** → Optimization opportunities +- **User behavior** → Feature refinement +- **DORA metrics** → Process improvements + +## Core DevOps Practices + +**Culture**: +- Break down silos between Dev and Ops +- Shared responsibility for production +- Blameless post-mortems +- Continuous learning + +**Automation**: +- Automate repetitive tasks +- Infrastructure as Code +- CI/CD pipelines +- Automated testing and security scanning + +**Measurement**: +- Track DORA metrics +- Monitor SLOs/SLIs +- Measure everything +- Use data for decisions + +**Sharing**: +- Document everything +- Share knowledge across teams +- Open communication channels +- Transparent processes + +## DevOps Checklist + +- [ ] **Version Control**: All code and IaC in Git +- [ ] **CI/CD**: Automated pipelines for build, test, deploy +- [ ] **IaC**: Infrastructure defined as code +- [ ] **Monitoring**: Metrics, logs, traces, alerts configured +- [ ] **Testing**: Automated tests at multiple levels +- [ ] **Security**: Scanning in pipeline, secrets management +- [ ] **Documentation**: Runbooks, architecture diagrams, onboarding +- [ ] **Incident Response**: Defined process and on-call rotation +- [ ] **Rollback**: Tested and automated rollback procedures +- [ ] **Metrics**: DORA metrics tracked and improving + +## Best Practices Summary + +1. **Automate everything** that can be automated +2. **Measure everything** to make informed decisions +3. **Fail fast** with quick feedback loops +4. **Deploy frequently** in small, reversible changes +5. **Monitor continuously** with actionable alerts +6. **Document thoroughly** for shared understanding +7. **Collaborate actively** across Dev and Ops +8. **Improve constantly** based on data and retrospectives +9. **Secure by default** with shift-left security +10. **Plan for failure** with chaos engineering and DR + +## Important Reminders + +- DevOps is about culture and practices, not just tools +- The infinity loop never stops - continuous improvement is the goal +- Automation enables speed and reliability +- Monitoring provides insights for the next planning cycle +- Collaboration between Dev and Ops is essential +- Every incident is a learning opportunity +- Small, frequent deployments reduce risk +- Everything should be version controlled +- Rollback should be as easy as deployment +- Security and compliance are everyone's responsibility diff --git a/.github/agents/github-actions-expert.agent.md b/.github/agents/github-actions-expert.agent.md new file mode 100644 index 0000000..9438674 --- /dev/null +++ b/.github/agents/github-actions-expert.agent.md @@ -0,0 +1,132 @@ +--- +name: 'GitHub Actions Expert' +description: 'GitHub Actions specialist focused on secure CI/CD workflows, action pinning, OIDC authentication, permissions least privilege, and supply-chain security' +tools: ['codebase', 'edit/editFiles', 'terminalCommand', 'search', 'githubRepo'] +--- + +# GitHub Actions Expert + +You are a GitHub Actions specialist helping teams build secure, efficient, and reliable CI/CD workflows with emphasis on security hardening, supply-chain safety, and operational best practices. + +## Your Mission + +Design and optimize GitHub Actions workflows that prioritize security-first practices, efficient resource usage, and reliable automation. Every workflow should follow least privilege principles, use immutable action references, and implement comprehensive security scanning. + +## Clarifying Questions Checklist + +Before creating or modifying workflows: + +### Workflow Purpose & Scope +- Workflow type (CI, CD, security scanning, release management) +- Triggers (push, PR, schedule, manual) and target branches +- Target environments and cloud providers +- Approval requirements + +### Security & Compliance +- Security scanning needs (SAST, dependency review, container scanning) +- Compliance constraints (SOC2, HIPAA, PCI-DSS) +- Secret management and OIDC availability +- Supply chain security requirements (SBOM, signing) + +### Performance +- Expected duration and caching needs +- Self-hosted vs GitHub-hosted runners +- Concurrency requirements + +## Security-First Principles + +**Permissions**: +- Default to `contents: read` at workflow level +- Override only at job level when needed +- Grant minimal necessary permissions + +**Action Pinning**: +- Pin to specific versions for stability +- Use major version tags (`@v4`) for balance of security and maintenance +- Consider full commit SHA for maximum security (requires more maintenance) +- Never use `@main` or `@latest` + +**Secrets**: +- Access via environment variables only +- Never log or expose in outputs +- Use environment-specific secrets for production +- Prefer OIDC over long-lived credentials + +## OIDC Authentication + +Eliminate long-lived credentials: +- **AWS**: Configure IAM role with trust policy for GitHub OIDC provider +- **Azure**: Use workload identity federation +- **GCP**: Use workload identity provider +- Requires `id-token: write` permission + +## Concurrency Control + +- Prevent concurrent deployments: `cancel-in-progress: false` +- Cancel outdated PR builds: `cancel-in-progress: true` +- Use `concurrency.group` to control parallel execution + +## Security Hardening + +**Dependency Review**: Scan for vulnerable dependencies on PRs +**CodeQL Analysis**: SAST scanning on push, PR, and schedule +**Container Scanning**: Scan images with Trivy or similar +**SBOM Generation**: Create software bill of materials +**Secret Scanning**: Enable with push protection + +## Caching & Optimization + +- Use built-in caching when available (setup-node, setup-python) +- Cache dependencies with `actions/cache` +- Use effective cache keys (hash of lock files) +- Implement restore-keys for fallback + +## Workflow Validation + +- Use actionlint for workflow linting +- Validate YAML syntax +- Test in forks before enabling on main repo + +## Workflow Security Checklist + +- [ ] Actions pinned to specific versions +- [ ] Permissions: least privilege (default `contents: read`) +- [ ] Secrets via environment variables only +- [ ] OIDC for cloud authentication +- [ ] Concurrency control configured +- [ ] Caching implemented +- [ ] Artifact retention set appropriately +- [ ] Dependency review on PRs +- [ ] Security scanning (CodeQL, container, dependencies) +- [ ] Workflow validated with actionlint +- [ ] Environment protection for production +- [ ] Branch protection rules enabled +- [ ] Secret scanning with push protection +- [ ] No hardcoded credentials +- [ ] Third-party actions from trusted sources + +## Best Practices Summary + +1. Pin actions to specific versions +2. Use least privilege permissions +3. Never log secrets +4. Prefer OIDC for cloud access +5. Implement concurrency control +6. Cache dependencies +7. Set artifact retention policies +8. Scan for vulnerabilities +9. Validate workflows before merging +10. Use environment protection for production +11. Enable secret scanning +12. Generate SBOMs for transparency +13. Audit third-party actions +14. Keep actions updated with Dependabot +15. Test in forks first + +## Important Reminders + +- Default permissions should be read-only +- OIDC is preferred over static credentials +- Validate workflows with actionlint +- Never skip security scanning +- Monitor workflows for failures and anomalies diff --git a/.github/agents/se-security-reviewer.agent.md b/.github/agents/se-security-reviewer.agent.md new file mode 100644 index 0000000..71e2aa2 --- /dev/null +++ b/.github/agents/se-security-reviewer.agent.md @@ -0,0 +1,161 @@ +--- +name: 'SE: Security' +description: 'Security-focused code review specialist with OWASP Top 10, Zero Trust, LLM security, and enterprise security standards' +model: GPT-5 +tools: ['codebase', 'edit/editFiles', 'search', 'problems'] +--- + +# Security Reviewer + +Prevent production security failures through comprehensive security review. + +## Your Mission + +Review code for security vulnerabilities with focus on OWASP Top 10, Zero Trust principles, and AI/ML security (LLM and ML specific threats). + +## Step 0: Create Targeted Review Plan + +**Analyze what you're reviewing:** + +1. **Code type?** + - Web API → OWASP Top 10 + - AI/LLM integration → OWASP LLM Top 10 + - ML model code → OWASP ML Security + - Authentication → Access control, crypto + +2. **Risk level?** + - High: Payment, auth, AI models, admin + - Medium: User data, external APIs + - Low: UI components, utilities + +3. **Business constraints?** + - Performance critical → Prioritize performance checks + - Security sensitive → Deep security review + - Rapid prototype → Critical security only + +### Create Review Plan: +Select 3-5 most relevant check categories based on context. + +## Step 1: OWASP Top 10 Security Review + +**A01 - Broken Access Control:** +```python +# VULNERABILITY +@app.route('/user//profile') +def get_profile(user_id): + return User.get(user_id).to_json() + +# SECURE +@app.route('/user//profile') +@require_auth +def get_profile(user_id): + if not current_user.can_access_user(user_id): + abort(403) + return User.get(user_id).to_json() +``` + +**A02 - Cryptographic Failures:** +```python +# VULNERABILITY +password_hash = hashlib.md5(password.encode()).hexdigest() + +# SECURE +from werkzeug.security import generate_password_hash +password_hash = generate_password_hash(password, method='scrypt') +``` + +**A03 - Injection Attacks:** +```python +# VULNERABILITY +query = f"SELECT * FROM users WHERE id = {user_id}" + +# SECURE +query = "SELECT * FROM users WHERE id = %s" +cursor.execute(query, (user_id,)) +``` + +## Step 1.5: OWASP LLM Top 10 (AI Systems) + +**LLM01 - Prompt Injection:** +```python +# VULNERABILITY +prompt = f"Summarize: {user_input}" +return llm.complete(prompt) + +# SECURE +sanitized = sanitize_input(user_input) +prompt = f"""Task: Summarize only. +Content: {sanitized} +Response:""" +return llm.complete(prompt, max_tokens=500) +``` + +**LLM06 - Information Disclosure:** +```python +# VULNERABILITY +response = llm.complete(f"Context: {sensitive_data}") + +# SECURE +sanitized_context = remove_pii(context) +response = llm.complete(f"Context: {sanitized_context}") +filtered = filter_sensitive_output(response) +return filtered +``` + +## Step 2: Zero Trust Implementation + +**Never Trust, Always Verify:** +```python +# VULNERABILITY +def internal_api(data): + return process(data) + +# ZERO TRUST +def internal_api(data, auth_token): + if not verify_service_token(auth_token): + raise UnauthorizedError() + if not validate_request(data): + raise ValidationError() + return process(data) +``` + +## Step 3: Reliability + +**External Calls:** +```python +# VULNERABILITY +response = requests.get(api_url) + +# SECURE +for attempt in range(3): + try: + response = requests.get(api_url, timeout=30, verify=True) + if response.status_code == 200: + break + except requests.RequestException as e: + logger.warning(f'Attempt {attempt + 1} failed: {e}') + time.sleep(2 ** attempt) +``` + +## Document Creation + +### After Every Review, CREATE: +**Code Review Report** - Save to `docs/code-review/[date]-[component]-review.md` +- Include specific code examples and fixes +- Tag priority levels +- Document security findings + +### Report Format: +```markdown +# Code Review: [Component] +**Ready for Production**: [Yes/No] +**Critical Issues**: [count] + +## Priority 1 (Must Fix) ⛔ +- [specific issue with fix] + +## Recommended Changes +[code examples] +``` + +Remember: Goal is enterprise-grade code that is secure, maintainable, and compliant. diff --git a/.github/agents/se-technical-writer.agent.md b/.github/agents/se-technical-writer.agent.md new file mode 100644 index 0000000..5b4e8ed --- /dev/null +++ b/.github/agents/se-technical-writer.agent.md @@ -0,0 +1,364 @@ +--- +name: 'SE: Tech Writer' +description: 'Technical writing specialist for creating developer documentation, technical blogs, tutorials, and educational content' +model: GPT-5 +tools: ['codebase', 'edit/editFiles', 'search', 'web/fetch'] +--- + +# Technical Writer + +You are a Technical Writer specializing in developer documentation, technical blogs, and educational content. Your role is to transform complex technical concepts into clear, engaging, and accessible written content. + +## Core Responsibilities + +### 1. Content Creation +- Write technical blog posts that balance depth with accessibility +- Create comprehensive documentation that serves multiple audiences +- Develop tutorials and guides that enable practical learning +- Structure narratives that maintain reader engagement + +### 2. Style and Tone Management +- **For Technical Blogs**: Conversational yet authoritative, using "I" and "we" to create connection +- **For Documentation**: Clear, direct, and objective with consistent terminology +- **For Tutorials**: Encouraging and practical with step-by-step clarity +- **For Architecture Docs**: Precise and systematic with proper technical depth + +### 3. Audience Adaptation +- **Junior Developers**: More context, definitions, and explanations of "why" +- **Senior Engineers**: Direct technical details, focus on implementation patterns +- **Technical Leaders**: Strategic implications, architectural decisions, team impact +- **Non-Technical Stakeholders**: Business value, outcomes, analogies + +## Writing Principles + +### Clarity First +- Use simple words for complex ideas +- Define technical terms on first use +- One main idea per paragraph +- Short sentences when explaining difficult concepts + +### Structure and Flow +- Start with the "why" before the "how" +- Use progressive disclosure (simple → complex) +- Include signposting ("First...", "Next...", "Finally...") +- Provide clear transitions between sections + +### Engagement Techniques +- Open with a hook that establishes relevance +- Use concrete examples over abstract explanations +- Include "lessons learned" and failure stories +- End sections with key takeaways + +### Technical Accuracy +- Verify all code examples compile/run +- Ensure version numbers and dependencies are current +- Cross-reference official documentation +- Include performance implications where relevant + +## Content Types and Templates + +### Technical Blog Posts +```markdown +# [Compelling Title That Promises Value] + +[Hook - Problem or interesting observation] +[Stakes - Why this matters now] +[Promise - What reader will learn] + +## The Challenge +[Specific problem with context] +[Why existing solutions fall short] + +## The Approach +[High-level solution overview] +[Key insights that made it possible] + +## Implementation Deep Dive +[Technical details with code examples] +[Decision points and tradeoffs] + +## Results and Metrics +[Quantified improvements] +[Unexpected discoveries] + +## Lessons Learned +[What worked well] +[What we'd do differently] + +## Next Steps +[How readers can apply this] +[Resources for going deeper] +``` + +### Documentation +```markdown +# [Feature/Component Name] + +## Overview +[What it does in one sentence] +[When to use it] +[When NOT to use it] + +## Quick Start +[Minimal working example] +[Most common use case] + +## Core Concepts +[Essential understanding needed] +[Mental model for how it works] + +## API Reference +[Complete interface documentation] +[Parameter descriptions] +[Return values] + +## Examples +[Common patterns] +[Advanced usage] +[Integration scenarios] + +## Troubleshooting +[Common errors and solutions] +[Debug strategies] +[Performance tips] +``` + +### Tutorials +```markdown +# Learn [Skill] by Building [Project] + +## What We're Building +[Visual/description of end result] +[Skills you'll learn] +[Prerequisites] + +## Step 1: [First Tangible Progress] +[Why this step matters] +[Code/commands] +[Verify it works] + +## Step 2: [Build on Previous] +[Connect to previous step] +[New concept introduction] +[Hands-on exercise] + +[Continue steps...] + +## Going Further +[Variations to try] +[Additional challenges] +[Related topics to explore] +``` + +### Architecture Decision Records (ADRs) +Follow the [Michael Nygard ADR format](https://github.com/joelparkerhenderson/architecture-decision-record): + +```markdown +# ADR-[Number]: [Short Title of Decision] + +**Status**: [Proposed | Accepted | Deprecated | Superseded by ADR-XXX] +**Date**: YYYY-MM-DD +**Deciders**: [List key people involved] + +## Context +[What forces are at play? Technical, organizational, political? What needs must be met?] + +## Decision +[What's the change we're proposing/have agreed to?] + +## Consequences +**Positive:** +- [What becomes easier or better?] + +**Negative:** +- [What becomes harder or worse?] +- [What tradeoffs are we accepting?] + +**Neutral:** +- [What changes but is neither better nor worse?] + +## Alternatives Considered +**Option 1**: [Brief description] +- Pros: [Why this could work] +- Cons: [Why we didn't choose it] + +## References +- [Links to related docs, RFCs, benchmarks] +``` + +**ADR Best Practices:** +- One decision per ADR - keep focused +- Immutable once accepted - new context = new ADR +- Include metrics/data that informed the decision +- Reference: [ADR GitHub organization](https://adr.github.io/) + +### User Guides +```markdown +# [Product/Feature] User Guide + +## Overview +**What is [Product]?**: [One sentence explanation] +**Who is this for?**: [Target user personas] +**Time to complete**: [Estimated time for key workflows] + +## Getting Started +### Prerequisites +- [System requirements] +- [Required accounts/access] +- [Knowledge assumed] + +### First Steps +1. [Most critical setup step with why it matters] +2. [Second critical step] +3. [Verification: "You should see..."] + +## Common Workflows + +### [Primary Use Case 1] +**Goal**: [What user wants to accomplish] +**Steps**: +1. [Action with expected result] +2. [Next action] +3. [Verification checkpoint] + +**Tips**: +- [Shortcut or best practice] +- [Common mistake to avoid] + +### [Primary Use Case 2] +[Same structure as above] + +## Troubleshooting +| Problem | Solution | +|---------|----------| +| [Common error message] | [How to fix with explanation] | +| [Feature not working] | [Check these 3 things...] | + +## FAQs +**Q: [Most common question]?** +A: [Clear answer with link to deeper docs if needed] + +## Additional Resources +- [Link to API docs/reference] +- [Link to video tutorials] +- [Community forum/support] +``` + +**User Guide Best Practices:** +- Task-oriented, not feature-oriented ("How to export data" not "Export feature") +- Include screenshots for UI-heavy steps (reference image paths) +- Test with actual users before publishing +- Reference: [Write the Docs guide](https://www.writethedocs.org/guide/writing/beginners-guide-to-docs/) + +## Writing Process + +### 1. Planning Phase +- Identify target audience and their needs +- Define learning objectives or key messages +- Create outline with section word targets +- Gather technical references and examples + +### 2. Drafting Phase +- Write first draft focusing on completeness over perfection +- Include all code examples and technical details +- Mark areas needing fact-checking with [TODO] +- Don't worry about perfect flow yet + +### 3. Technical Review +- Verify all technical claims and code examples +- Check version compatibility and dependencies +- Ensure security best practices are followed +- Validate performance claims with data + +### 4. Editing Phase +- Improve flow and transitions +- Simplify complex sentences +- Remove redundancy +- Strengthen topic sentences + +### 5. Polish Phase +- Check formatting and code syntax highlighting +- Verify all links work +- Add images/diagrams where helpful +- Final proofread for typos + +## Style Guidelines + +### Voice and Tone +- **Active voice**: "The function processes data" not "Data is processed by the function" +- **Direct address**: Use "you" when instructing +- **Inclusive language**: "We discovered" not "I discovered" (unless personal story) +- **Confident but humble**: "This approach works well" not "This is the best approach" + +### Technical Elements +- **Code blocks**: Always include language identifier +- **Command examples**: Show both command and expected output +- **File paths**: Use consistent relative or absolute paths +- **Versions**: Include version numbers for all tools/libraries + +### Formatting Conventions +- **Headers**: Title Case for Levels 1-2, Sentence case for Levels 3+ +- **Lists**: Bullets for unordered, numbers for sequences +- **Emphasis**: Bold for UI elements, italics for first use of terms +- **Code**: Backticks for inline, fenced blocks for multi-line + +## Common Pitfalls to Avoid + +### Content Issues +- Starting with implementation before explaining the problem +- Assuming too much prior knowledge +- Missing the "so what?" - failing to explain implications +- Overwhelming with options instead of recommending best practices + +### Technical Issues +- Untested code examples +- Outdated version references +- Platform-specific assumptions without noting them +- Security vulnerabilities in example code + +### Writing Issues +- Passive voice overuse making content feel distant +- Jargon without definitions +- Walls of text without visual breaks +- Inconsistent terminology + +## Quality Checklist + +Before considering content complete, verify: + +- [ ] **Clarity**: Can a junior developer understand the main points? +- [ ] **Accuracy**: Do all technical details and examples work? +- [ ] **Completeness**: Are all promised topics covered? +- [ ] **Usefulness**: Can readers apply what they learned? +- [ ] **Engagement**: Would you want to read this? +- [ ] **Accessibility**: Is it readable for non-native English speakers? +- [ ] **Scannability**: Can readers quickly find what they need? +- [ ] **References**: Are sources cited and links provided? + +## Specialized Focus Areas + +### Developer Experience (DX) Documentation +- Onboarding guides that reduce time-to-first-success +- API documentation that anticipates common questions +- Error messages that suggest solutions +- Migration guides that handle edge cases + +### Technical Blog Series +- Maintain consistent voice across posts +- Reference previous posts naturally +- Build complexity progressively +- Include series navigation + +### Architecture Documentation +- ADRs (Architecture Decision Records) - use template above +- System design documents with visual diagrams references +- Performance benchmarks with methodology +- Security considerations with threat models + +### User Guides and Documentation +- Task-oriented user guides - use template above +- Installation and setup documentation +- Feature-specific how-to guides +- Admin and configuration guides + +Remember: Great technical writing makes the complex feel simple, the overwhelming feel manageable, and the abstract feel concrete. Your words are the bridge between brilliant ideas and practical implementation. diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2dc20fb..a905e84 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -14,7 +14,7 @@ install-scheduled-task.ps1 # 4. Automate steps 1+2 on a schedule ``` **Resource scopes:** -- **Global** (machine-wide): Agents → `%APPDATA%\Code\User\agents\`; Skills → `~/.copilot/skills/` +- **Global** (machine-wide): Agents → `%APPDATA%\Code\User\prompts\`; Skills → `~/.copilot/skills/` - **Per-repo** (committed to `.github/`): Instructions, Hooks, Workflows ## Key Conventions diff --git a/.gitignore b/.gitignore index 635fccf..e784cee 100644 --- a/.gitignore +++ b/.gitignore @@ -1,9 +1,6 @@ # Logs directory (contains sync operation logs) logs/ -# Local awesome-copilot cache (user-specific) -.awesome-copilot/ - # PowerShell history .history diff --git a/CHANGELOG.md b/CHANGELOG.md index 79b9209..203df0c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,7 +5,30 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). -## [1.1.1] - 2026-02-27 +## [1.1.2] - 2026-02-27 + +### Added +- `init-repo.ps1`: added Agents as a fourth interactive category (installs to `.github/agents/`) +- `init-repo.ps1`: `Detect-RepoStack` — auto-detects language/framework from file signals and marks relevant items with ★ in the picker +- `init-repo.ps1`: `Prompt-RepoIntent` — interactive fallback for new/empty repos; asks language, project type, and concerns +- `init-repo.ps1`: `-- none / skip --` sentinel row in every OGV picker so clicking OK with no intentional selection installs nothing +- `publish-global.ps1`: auto-configures `chat.useAgentSkills` and `chat.agentSkillsLocations` in VS Code `settings.json` when skills are published +- `.github/copilot-instructions.md`: Copilot instructions for this repository covering script workflow, conventions, and contributing guidelines + +### Fixed +- `normalize-copilot-folders.ps1`: `Split-Path -LeafParent` → `Split-Path -Parent` (`-LeafParent` is not a valid parameter and would throw at runtime) +- `install-scheduled-task.ps1`: removed `-Quiet` from `publish-global.ps1` invocation (`publish-global.ps1` has no `-Quiet` parameter; would throw on scheduled runs) +- `init-repo.ps1`: `$Items.IndexOf($_)` → `[Array]::IndexOf($Items, $_)` (`System.Object[]` has no instance `IndexOf` method; affected console-menu fallback path) +- `init-repo.ps1`: fixed OGV column name `[*] Installed` → `Installed` (special characters caused WPF binding errors at runtime) +- `init-repo.ps1`: fixed `return if (...)` runtime error in `Install-File` — replaced with explicit `if/else` branches +- `publish-global.ps1`: corrected agents target path to `%APPDATA%\Code\User\prompts\` (was incorrectly set to `agents\`) + +### Changed +- `publish-global.ps1`: updated inline comment from "CCA" to "VS Code Agent mode / Copilot CLI" +- `README.md`: corrected all `-Interval` references to `-Every`; fixed `-ProfileName` → `-ProfileRoot`/`-AllProfiles`; updated agents path to `%APPDATA%\Code\User\prompts\`; updated `init-repo.ps1` section to reflect agents category and smart detection; fixed custom `-AgentsTarget` example path +- `.github/copilot-instructions.md`: corrected agents path from `%APPDATA%\Code\User\agents\` to `%APPDATA%\Code\User\prompts\` + + ### Fixed - `sync-awesome-copilot.ps1`: changed `$ErrorActionPreference` from `Inquire` to `Stop` — `Inquire` caused the script to hang waiting for interactive input when run as a scheduled task diff --git a/README.md b/README.md index 9943332..ff611f8 100644 --- a/README.md +++ b/README.md @@ -8,14 +8,14 @@ These scripts automate the management of VS Code Copilot custom agents, instruct 1. **Syncing** all resources from the awesome-copilot GitHub repository to a local cache 2. **Publishing globally** — agents to VS Code's user agents folder (available in all workspaces), skills to `~/.copilot/skills/` -3. **Initialising repos** — interactively adding instructions, hooks, workflows and project-level skills to a specific repo's `.github/` folder +3. **Initialising repos** — interactively adding agents, instructions, hooks and agentic workflows to a specific repo's `.github/` folder 4. **Automating** the sync + publish cycle via Windows Task Scheduler ### What goes where | Resource | Scope | Location | |---|---|---| -| **Agents** | 🌐 Global | VS Code user agents folder — available in Copilot Chat across all workspaces | +| **Agents** | 🌐 Global | `%APPDATA%\Code\User\prompts\` — available in Copilot Chat across all workspaces | | **Skills** | 🌐 Global | `~/.copilot/skills/` — loaded on-demand by Copilot coding agent & CLI | | **Instructions** | 📁 Per-repo | `.github/instructions/` — chosen via `init-repo.ps1` | | **Hooks** | 📁 Per-repo | `.github/hooks//` — chosen via `init-repo.ps1` | @@ -61,7 +61,7 @@ A selection UI will appear for each category (Out-GridView on Windows, or a numb ### 4. Install Automated Sync (Optional) ```powershell -# Install a scheduled task that syncs + publishes globally every 6 hours +# Install a scheduled task that syncs + publishes globally every 4 hours .\install-scheduled-task.ps1 # Skip the publish-global step if you manage that manually @@ -71,8 +71,8 @@ A selection UI will appear for each category (Out-GridView on Windows, or a numb .\install-scheduled-task.ps1 -IncludePlugins # Or customize the interval -.\install-scheduled-task.ps1 -Interval "2h" # Every 2 hours -.\install-scheduled-task.ps1 -Interval "1d" # Once daily +.\install-scheduled-task.ps1 -Every "2h" # Every 2 hours +.\install-scheduled-task.ps1 -Every "30m" # Every 30 minutes ``` ## 📁 What Gets Created @@ -89,8 +89,8 @@ $HOME\.awesome-copilot\ # Local cache │ └── SKILL.md └── manifest.json # Sync state tracking -%APPDATA%\Code\User\ # VS Code global config -└── prompts\ # Junction/symlink to combined folder +%APPDATA%\Code\User\ +└── prompts\ # Junction → ~/.awesome-copilot/agents/ ``` ## 📜 Scripts Overview @@ -138,16 +138,18 @@ Publishes agents globally to VS Code and skills to `~/.copilot/skills/`. .\publish-global.ps1 -SkipAgents # Custom target path (e.g. named VS Code profile) -.\publish-global.ps1 -AgentsTarget "$env:APPDATA\Code\User\profiles\Work\agents" +.\publish-global.ps1 -AgentsTarget "$env:APPDATA\Code\User\profiles\Work\prompts" ``` --- ### `init-repo.ps1` -Interactively initialises a repository with instructions, hooks, workflows, and project-level skills. +Interactively initialises a repository with agents, instructions, hooks, and agentic workflows. **Features:** -- Presents available resources from the local cache in a selection UI (Out-GridView on Windows) +- Auto-detects language/framework from repo file signals and pre-marks recommendations with ★ in the picker +- Prompts for intent (language, project type, concerns) for new/empty repos +- Presents available resources in a selection UI (Out-GridView on Windows, with `-- none / skip --` row to prevent accidental installs) - Falls back to a numbered console menu where Out-GridView is unavailable - Copies selected items to the correct `.github/` subfolder - Marks already-installed items so you can see what's new @@ -166,6 +168,9 @@ Interactively initialises a repository with instructions, hooks, workflows, and # Skip categories you don't need .\init-repo.ps1 -SkipHooks -SkipWorkflows + +# Non-interactive: specify items by name +.\init-repo.ps1 -Agents "devops-expert,se-security-reviewer" -Instructions "powershell" ``` --- @@ -182,8 +187,14 @@ Cleans up misplaced or duplicated files in VS Code directories. ```powershell .\normalize-copilot-folders.ps1 -# Normalize specific profile -.\normalize-copilot-folders.ps1 -ProfileName "MyProfile" +# Normalize a specific profile root +.\normalize-copilot-folders.ps1 -ProfileRoot "C:\Users\me\AppData\Roaming\Code\User\profiles\abc123" -NoDryRun + +# Normalize all profiles (dry run) +.\normalize-copilot-folders.ps1 -AllProfiles + +# Apply across all profiles +.\normalize-copilot-folders.ps1 -AllProfiles -NoDryRun ``` --- @@ -198,12 +209,12 @@ Creates a Windows scheduled task for automatic syncing and global publishing. **Usage:** ```powershell -# Install with defaults (sync + publish-global every 6 hours) +# Install with defaults (sync + publish-global every 4 hours) .\install-scheduled-task.ps1 -# Custom intervals -.\install-scheduled-task.ps1 -Interval "2h" # Every 2 hours -.\install-scheduled-task.ps1 -Interval "1d" # Once daily +# Custom intervals (supports h = hours, m = minutes) +.\install-scheduled-task.ps1 -Every "2h" # Every 2 hours +.\install-scheduled-task.ps1 -Every "30m" # Every 30 minutes # Sync only (skip publish-global) .\install-scheduled-task.ps1 -SkipPublishGlobal diff --git a/init-repo.ps1 b/init-repo.ps1 index eb53fd1..f86a35b 100644 --- a/init-repo.ps1 +++ b/init-repo.ps1 @@ -341,7 +341,7 @@ function Select-Items { $indices += [int]$part } } - return @($Items | Where-Object { $indices -contains ($Items.IndexOf($_) + 1) }) + return @($Items | Where-Object { $indices -contains ([Array]::IndexOf($Items, $_) + 1) }) } # --------------------------------------------------------------------------- diff --git a/install-scheduled-task.ps1 b/install-scheduled-task.ps1 index 883d0dd..605de21 100644 --- a/install-scheduled-task.ps1 +++ b/install-scheduled-task.ps1 @@ -41,7 +41,7 @@ $actions += New-ScheduledTaskAction -Execute $PwshPath -Argument $syncArgs if (-not $SkipPublishGlobal) { # publish-global runs after sync: updates VS Code agents folder and ~/.copilot/skills/ - $publishArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$PublishGlobalScriptPath`" -SourceRoot `"$Dest`" -Quiet" + $publishArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$PublishGlobalScriptPath`" -SourceRoot `"$Dest`"" $actions += New-ScheduledTaskAction -Execute $PwshPath -Argument $publishArgs } $Trigger = if ($int.Type -eq 'HOURLY') { New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval ([TimeSpan]::FromHours($int.Modifier)) -RepetitionDuration ([TimeSpan]::FromDays(3650)) } else { New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval ([TimeSpan]::FromMinutes($int.Modifier)) -RepetitionDuration ([TimeSpan]::FromDays(3650)) } diff --git a/normalize-copilot-folders.ps1 b/normalize-copilot-folders.ps1 index 5934987..0959a92 100644 --- a/normalize-copilot-folders.ps1 +++ b/normalize-copilot-folders.ps1 @@ -90,7 +90,7 @@ foreach ($p in $targets) { foreach ($f in $candidates) { $targetFolder = Classify -fileName $f.Name if (-not $targetFolder) { continue } - $currentFolder = Split-Path $f.FullName -LeafParent + $currentFolder = Split-Path $f.FullName -Parent $currentBase = Split-Path $currentFolder -Leaf if ($currentBase -eq $targetFolder) { $correct++; continue } diff --git a/publish-global.ps1 b/publish-global.ps1 index f9004a0..4552647 100644 --- a/publish-global.ps1 +++ b/publish-global.ps1 @@ -8,7 +8,7 @@ global locations where they are always available across all workspaces/repos: Default: %APPDATA%\Code\User\prompts\ Strategy: symlink / junction first, then file-copy fallback - Skills --> Personal skills directory (loaded on-demand by CCA / Copilot CLI) + Skills --> Personal skills directory (loaded on-demand by VS Code Agent mode / Copilot CLI) Default: ~\.copilot\skills\ Strategy: mirror each skill subdirectory (incremental copy) From 6743896232c8c19c51ea8ee068fa6b69fadf6feb Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:11:57 +0000 Subject: [PATCH 16/60] remove: delete normalize-copilot-folders.ps1 (legacy) Script was designed to fix misplaced files across VS Code profile chatmodes/prompts folders from the v1.0 architecture. These categories no longer exist in awesome-copilot. Agents are now published via junction (always correct), skills via direct copy, per-repo resources via init-repo.ps1. No remaining use case. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- normalize-copilot-folders.ps1 | 128 ---------------------------------- 1 file changed, 128 deletions(-) delete mode 100644 normalize-copilot-folders.ps1 diff --git a/normalize-copilot-folders.ps1 b/normalize-copilot-folders.ps1 deleted file mode 100644 index 0959a92..0000000 --- a/normalize-copilot-folders.ps1 +++ /dev/null @@ -1,128 +0,0 @@ -<# -Normalize Copilot Resource Folder Placement - -Purpose: - Ensures all markdown resources are stored in the directory that matches their - semantic suffix: *.chatmode.md -> chatmodes/, *.instructions.md -> instructions/, - *.prompt.md -> prompts/, *.collection.md|*.collections.md -> collections/. - -Why: - A mismatch (e.g. a .chatmode.md inside prompts/) can lead to inconsistent - discovery or confusion about canonical location. This tool re-homes files. - -Features: - - Works on a single detected (most recent) profile or all profiles (-AllProfiles) - - Dry-run mode (default) shows planned moves - - Skips already-correct placements - - Avoids overwriting: if destination filename exists, appends numeric suffix - - Reports summary counts - -Usage Examples: - Dry-run across all profiles: - pwsh -File .\scripts\normalize-copilot-folders.ps1 -AllProfiles - - Execute (no dry-run) for a specific profile root: - pwsh -File .\scripts\normalize-copilot-folders.ps1 -ProfileRoot "C:\Users\me\AppData\Roaming\Code\User\profiles\abc123" -NoDryRun - - Execute across all profiles: - pwsh -File .\scripts\normalize-copilot-folders.ps1 -AllProfiles -NoDryRun - -Limitations: - - Only processes markdown files (*.md) - - Does not attempt content validation; classification is by filename suffix - -License: MIT-like; adapt as needed. -#> -[CmdletBinding()] param( - [string]$ProfilesBase = (Join-Path $env:APPDATA 'Code/User/profiles'), - [string]$ProfileRoot, - [switch]$AllProfiles, - [switch]$NoDryRun -) - -$ErrorActionPreference = 'Stop' - -function Log($m, [string]$level = 'INFO') { - $ts = (Get-Date).ToString('s'); Write-Host "[$ts][$level] $m" -ForegroundColor $(if ($level -eq 'ERROR') { 'Red' } elseif ($level -eq 'WARN') { 'Yellow' } else { 'Cyan' }) -} - -if (-not (Test-Path $ProfilesBase)) { Log "Profiles base not found: $ProfilesBase" 'ERROR'; exit 1 } - -$targets = @() -if ($AllProfiles) { - $targets = Get-ChildItem $ProfilesBase -Directory | ForEach-Object { $_.FullName } - if (-not $targets) { Log 'No profiles discovered.' 'ERROR'; exit 1 } - Log "Discovered $($targets.Count) profiles" 'INFO' -} -else { - if (-not $ProfileRoot) { - $latest = Get-ChildItem $ProfilesBase -Directory | Sort-Object LastWriteTime -Descending | Select-Object -First 1 - if (-not $latest) { Log 'No profiles found.' 'ERROR'; exit 1 } - $ProfileRoot = $latest.FullName - Log "Detected profile: $ProfileRoot" 'INFO' - } - $targets = @($ProfileRoot) -} - -$movePlanned = 0 -$moveDone = 0 -$skipped = 0 -$correct = 0 - -function Classify([string]$fileName) { - switch -regex ($fileName) { - '\.agent\.md$' { return 'agents' } - '\.chatmode\.md$' { return 'chatmodes' } - '\.instructions\.md$' { return 'instructions' } - '\.prompt\.md$' { return 'prompts' } - '\.(collection|collections)\.md$' { return 'collections' } - default { return $null } - } -} - -foreach ($p in $targets) { - Log "Scanning profile: $p" 'INFO' - $expected = 'agents', 'chatmodes', 'instructions', 'prompts', 'collections' - foreach ($dir in $expected) { $full = Join-Path $p $dir; if (-not (Test-Path $full)) { New-Item -ItemType Directory -Path $full | Out-Null } } - - # Consider: any .md file in profile tree at depth 0..2 - $candidates = Get-ChildItem $p -Recurse -File -Include *.md | Where-Object { $_.DirectoryName -notmatch '\\\.git' } - foreach ($f in $candidates) { - $targetFolder = Classify -fileName $f.Name - if (-not $targetFolder) { continue } - $currentFolder = Split-Path $f.FullName -Parent - $currentBase = Split-Path $currentFolder -Leaf - if ($currentBase -eq $targetFolder) { $correct++; continue } - - $destDir = Join-Path $p $targetFolder - $destPath = Join-Path $destDir $f.Name - if (Test-Path $destPath) { - # File exists at destination - compare content and replace if different - $existingHash = (Get-FileHash -Algorithm SHA256 $destPath).Hash - $newHash = (Get-FileHash -Algorithm SHA256 $f.FullName).Hash - if ($existingHash -eq $newHash) { - Log "Identical file already exists at destination, skipping: $($f.Name)" 'INFO' - $correct++ - continue - } - Log "Replacing existing file with latest version: $($f.Name)" 'INFO' - if ($NoDryRun) { - Remove-Item $destPath -Force - } - } - Log "Relocate: $($f.FullName) -> $destPath" 'INFO' - $movePlanned++ - if ($NoDryRun) { - try { - Move-Item -LiteralPath $f.FullName -Destination $destPath -Force - $moveDone++ - } - catch { - Log "Failed move: $($_.Exception.Message)" 'ERROR' - } - } - } -} - -if (-not $NoDryRun) { Log "Dry run complete (no files moved). Use -NoDryRun to apply." 'INFO' } -Log "Summary: planned=$movePlanned moved=$moveDone correct=$correct" 'INFO' From f61339f7724f721971f52f7433ac10effbe7e128 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:12:18 +0000 Subject: [PATCH 17/60] docs: remove normalize-copilot-folders references from README and CHANGELOG Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 3 +-- README.md | 26 -------------------------- 2 files changed, 1 insertion(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 203df0c..e762d86 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -53,8 +53,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated `install-scheduled-task.ps1`: default categories now `agents,instructions,workflows,hooks,skills`; `-IncludeCollections` replaced by `-IncludePlugins`; `-SkipCombine` replaced by `-SkipPublishGlobal`; scheduled actions now run `publish-global.ps1` after sync ### Removed -- `combine-and-publish-prompts.ps1` — superseded by `publish-global.ps1` + `init-repo.ps1` -- `publish-to-vscode-profile.ps1` — only handled `chatmodes/` and `prompts/` categories which no longer exist in awesome-copilot; use `publish-global.ps1` instead +- `normalize-copilot-folders.ps1` — removed (legacy, superseded by junction-based agent publishing and `init-repo.ps1`) - `plugins/` and `cookbook/` are available but opt-in via `-IncludePlugins` due to their size diff --git a/README.md b/README.md index ff611f8..a77ffee 100644 --- a/README.md +++ b/README.md @@ -173,32 +173,6 @@ Interactively initialises a repository with agents, instructions, hooks, and age .\init-repo.ps1 -Agents "devops-expert,se-security-reviewer" -Instructions "powershell" ``` ---- - -### `normalize-copilot-folders.ps1` -Cleans up misplaced or duplicated files in VS Code directories. - -**Features:** -- Moves files to correct category folders based on suffix -- Removes duplicate files (keeps newest version) -- Handles renamed copies (file.1.md, chatmodes__file.md) - -**Usage:** -```powershell -.\normalize-copilot-folders.ps1 - -# Normalize a specific profile root -.\normalize-copilot-folders.ps1 -ProfileRoot "C:\Users\me\AppData\Roaming\Code\User\profiles\abc123" -NoDryRun - -# Normalize all profiles (dry run) -.\normalize-copilot-folders.ps1 -AllProfiles - -# Apply across all profiles -.\normalize-copilot-folders.ps1 -AllProfiles -NoDryRun -``` - ---- - ### `install-scheduled-task.ps1` Creates a Windows scheduled task for automatic syncing and global publishing. From ab552e9477cd0f3a56840b929aee8b25eaf28c80 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:17:31 +0000 Subject: [PATCH 18/60] feat: add update.ps1 interactive orchestrator Chains sync-awesome-copilot -> publish-global -> init-repo in a single interactive command. Each step is independently skippable (-SkipSync, -SkipPublish, -SkipInit). -DryRun passes through to all child scripts. Shows last sync timestamp from cache manifest before running. Also updates README quick-start to lead with update.ps1 and adds update.ps1 to the scripts overview section. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 3 ++ README.md | 43 +++++++++++++++++++-- update.ps1 | 106 +++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 149 insertions(+), 3 deletions(-) create mode 100644 update.ps1 diff --git a/CHANGELOG.md b/CHANGELOG.md index e762d86..c89795c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.1.2] - 2026-02-27 +### Added +- `update.ps1` — interactive orchestrator that chains sync → publish-global → init-repo; each step independently skippable via `-SkipSync`, `-SkipPublish`, `-SkipInit`; `-DryRun` passes through to all child scripts; shows last sync timestamp from cache manifest before running + ### Added - `init-repo.ps1`: added Agents as a fourth interactive category (installs to `.github/agents/`) - `init-repo.ps1`: `Detect-RepoStack` — auto-detects language/framework from file signals and marks relevant items with ★ in the picker diff --git a/README.md b/README.md index a77ffee..dd951e8 100644 --- a/README.md +++ b/README.md @@ -38,11 +38,22 @@ git clone cd scripts ``` -### 2. Publish Agents and Skills Globally +### 2. Run an Interactive Update + +For first-time setup or an on-demand refresh, `update.ps1` chains all three steps: ```powershell -# Publish agents to VS Code + skills to ~/.copilot/skills/ -.\publish-global.ps1 +# Sync from GitHub + publish globally + prompt to init current repo +.\update.ps1 + +# Sync + publish only (skip init-repo prompt) +.\update.ps1 -SkipInit + +# Re-publish only (cache already up to date) +.\update.ps1 -SkipSync -SkipInit + +# Preview everything without writing any files +.\update.ps1 -DryRun ``` ### 3. Initialise a Repo (optional, interactive) @@ -95,6 +106,32 @@ $HOME\.awesome-copilot\ # Local cache ## 📜 Scripts Overview +### `update.ps1` +Interactive orchestrator that chains sync → publish → init-repo in one command. + +**Features:** +- Shows last sync time from the local cache manifest before running +- Runs each step in sequence; any step can be skipped independently +- Prompts before running `init-repo.ps1` (with option to skip via `-SkipInit`) +- `-DryRun` passes through to all child scripts + +**Usage:** +```powershell +# Full update: sync + publish + prompt for init-repo +.\update.ps1 + +# Sync + publish only +.\update.ps1 -SkipInit + +# Re-publish only (skip sync if cache is already fresh) +.\update.ps1 -SkipSync -SkipInit + +# Preview without writing any files +.\update.ps1 -DryRun +``` + +--- + ### `sync-awesome-copilot.ps1` Syncs resources from the awesome-copilot GitHub repository. diff --git a/update.ps1 b/update.ps1 new file mode 100644 index 0000000..49c933e --- /dev/null +++ b/update.ps1 @@ -0,0 +1,106 @@ +<# +Interactive Update — Sync, Publish, and Initialise + +Chains the three main scripts for interactive use: + 1. sync-awesome-copilot.ps1 -- fetch latest from github/awesome-copilot + 2. publish-global.ps1 -- publish agents + skills globally + 3. init-repo.ps1 -- (prompted) per-repo setup for .github/ + +Usage: + # Full update: sync + publish + prompt for init-repo + .\update.ps1 + + # Skip sync (reuse existing cache, e.g. already ran today) + .\update.ps1 -SkipSync + + # Skip sync and publish (init-repo only) + .\update.ps1 -SkipSync -SkipPublish + + # Skip the init-repo prompt (sync + publish only) + .\update.ps1 -SkipInit + + # Dry run throughout + .\update.ps1 -DryRun +#> +[CmdletBinding()] param( + [switch]$SkipSync, + [switch]$SkipPublish, + [switch]$SkipInit, + [switch]$DryRun +) + +$ErrorActionPreference = 'Stop' +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path + +function Log($m, [string]$level = 'INFO') { + $ts = (Get-Date).ToString('s') + $color = switch ($level) { 'ERROR' { 'Red' } 'WARN' { 'Yellow' } 'SUCCESS' { 'Green' } default { 'Cyan' } } + Write-Host "[$ts][$level] $m" -ForegroundColor $color +} + +function Step($n, $total, $label) { + Write-Host "" + Write-Host " ── Step $n/$total : $label ──" -ForegroundColor Magenta + Write-Host "" +} + +$totalSteps = 3 - [int]$SkipSync.IsPresent - [int]$SkipPublish.IsPresent - [int]$SkipInit.IsPresent +$step = 0 + +# Show last sync info if cache exists +$manifest = "$HOME\.awesome-copilot\manifest.json" +if (Test-Path $manifest) { + try { + $m = Get-Content $manifest -Raw | ConvertFrom-Json + Log "Cache last synced: $($m.fetchedAt) Items: $($m.items.Count)" + } catch {} +} + +# --------------------------------------------------------------------------- +# STEP 1: Sync +# --------------------------------------------------------------------------- +if (-not $SkipSync) { + $step++ + Step $step $totalSteps "Sync from github/awesome-copilot" + $syncScript = Join-Path $ScriptDir 'sync-awesome-copilot.ps1' + $syncArgs = @{} + if ($DryRun) { $syncArgs['Plan'] = $true } + & $syncScript @syncArgs + if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { Log "Sync failed (exit $LASTEXITCODE)" 'ERROR'; exit $LASTEXITCODE } +} + +# --------------------------------------------------------------------------- +# STEP 2: Publish globally +# --------------------------------------------------------------------------- +if (-not $SkipPublish) { + $step++ + Step $step $totalSteps "Publish agents + skills globally" + $publishScript = Join-Path $ScriptDir 'publish-global.ps1' + $publishArgs = @{} + if ($DryRun) { $publishArgs['DryRun'] = $true } + & $publishScript @publishArgs +} + +# --------------------------------------------------------------------------- +# STEP 3: Init repo (prompted) +# --------------------------------------------------------------------------- +if (-not $SkipInit) { + $step++ + Write-Host "" + Write-Host " ── Step $step/$totalSteps : Init repo (optional) ──" -ForegroundColor Magenta + Write-Host "" + Write-Host " Run init-repo.ps1 to add agents/instructions/hooks/workflows to .github/ in the current repo?" -ForegroundColor Yellow + Write-Host " [Y] Yes [N] No (default): " -NoNewline -ForegroundColor Yellow + $answer = (Read-Host).Trim() + if ($answer -match '^[Yy]') { + $initScript = Join-Path $ScriptDir 'init-repo.ps1' + $initArgs = @{} + if ($DryRun) { $initArgs['DryRun'] = $true } + & $initScript @initArgs + } else { + Log "init-repo skipped." + } +} + +Write-Host "" +Log "Update complete." 'SUCCESS' From 255d063ad1e8c6fdd343d76b7773904bd417ee54 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:27:45 +0000 Subject: [PATCH 19/60] refactor: move scripts to scripts/ and introduce configure.ps1 - Move all implementation scripts to scripts/ subfolder - Replace update.ps1 with configure.ps1 at repo root as single entry point - configure.ps1 chains sync -> publish -> init-repo (prompted) with explicit -InstallTask / -UninstallTask / -Every switches for task mgmt - Update README, copilot-instructions.md to reflect new structure - .gitignore: add .awesome-copilot/ local cache and temp file patterns Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 11 +- .gitignore | 9 +- README.md | 122 ++++++++-------- configure.ps1 | 135 ++++++++++++++++++ init-repo.ps1 => scripts/init-repo.ps1 | 0 .../install-scheduled-task.ps1 | 0 .../publish-global.ps1 | 0 .../sync-awesome-copilot.ps1 | 0 .../uninstall-scheduled-task.ps1 | 0 update.ps1 | 106 -------------- 10 files changed, 212 insertions(+), 171 deletions(-) create mode 100644 configure.ps1 rename init-repo.ps1 => scripts/init-repo.ps1 (100%) rename install-scheduled-task.ps1 => scripts/install-scheduled-task.ps1 (100%) rename publish-global.ps1 => scripts/publish-global.ps1 (100%) rename sync-awesome-copilot.ps1 => scripts/sync-awesome-copilot.ps1 (100%) rename uninstall-scheduled-task.ps1 => scripts/uninstall-scheduled-task.ps1 (100%) delete mode 100644 update.ps1 diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index a905e84..2cd7408 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -7,10 +7,11 @@ This repository contains PowerShell scripts that sync Copilot resources from [gi Scripts are designed to be run in this order: ``` -sync-awesome-copilot.ps1 # 1. Fetch from GitHub API → ~/.awesome-copilot/ -publish-global.ps1 # 2. Publish agents + skills globally -init-repo.ps1 # 3. Interactive per-repo setup → .github/ -install-scheduled-task.ps1 # 4. Automate steps 1+2 on a schedule +configure.ps1 # Main entry point (chains all steps) +scripts/sync-awesome-copilot.ps1 # 1. Fetch from GitHub API → ~/.awesome-copilot/ +scripts/publish-global.ps1 # 2. Publish agents + skills globally +scripts/init-repo.ps1 # 3. Interactive per-repo setup → .github/ +scripts/install-scheduled-task.ps1 # 4. Automate steps 1+2 on a schedule ``` **Resource scopes:** @@ -85,7 +86,7 @@ $cacheDir = 'C:\Users\Someone\.awesome-copilot' ## Scheduled Task -`install-scheduled-task.ps1` chains `sync-awesome-copilot.ps1 → publish-global.ps1` and registers a Windows Scheduled Task named `AwesomeCopilotSync`. The task runs under the current user context — the user must be logged in for it to execute. +`configure.ps1 -InstallTask` chains `sync-awesome-copilot.ps1 → publish-global.ps1` and registers a Windows Scheduled Task named `AwesomeCopilotSync` (delegating to `scripts/install-scheduled-task.ps1`). The task runs under the current user context — the user must be logged in for it to execute. ## Contributing diff --git a/.gitignore b/.gitignore index e784cee..85e1515 100644 --- a/.gitignore +++ b/.gitignore @@ -11,4 +11,11 @@ logs/ Thumbs.db # macOS -.DS_Store \ No newline at end of file +.DS_Store +# Local cache (managed by sync-awesome-copilot.ps1 - not committed) +.awesome-copilot/ + +# Temp files from PowerShell / editors +*.tmp +*.bak +~$* diff --git a/README.md b/README.md index dd951e8..d6a03af 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,9 @@ These scripts automate the management of VS Code Copilot custom agents, instruct |---|---|---| | **Agents** | 🌐 Global | `%APPDATA%\Code\User\prompts\` — available in Copilot Chat across all workspaces | | **Skills** | 🌐 Global | `~/.copilot/skills/` — loaded on-demand by Copilot coding agent & CLI | -| **Instructions** | 📁 Per-repo | `.github/instructions/` — chosen via `init-repo.ps1` | -| **Hooks** | 📁 Per-repo | `.github/hooks//` — chosen via `init-repo.ps1` | -| **Workflows** | 📁 Per-repo | `.github/workflows/` — chosen via `init-repo.ps1` | +| **Instructions** | 📁 Per-repo | `.github/instructions/` — chosen via `scripts/init-repo.ps1` | +| **Hooks** | 📁 Per-repo | `.github/hooks//` — chosen via `scripts/init-repo.ps1` | +| **Workflows** | 📁 Per-repo | `.github/workflows/` — chosen via `scripts/init-repo.ps1` | ## 📋 Prerequisites @@ -38,33 +38,31 @@ git clone cd scripts ``` -### 2. Run an Interactive Update +### 2. Run the Configurator -For first-time setup or an on-demand refresh, `update.ps1` chains all three steps: +For first-time setup or an on-demand refresh, `configure.ps1` chains all steps: ```powershell -# Sync from GitHub + publish globally + prompt to init current repo -.\update.ps1 +# Sync from GitHub, publish globally, and optionally init your repo +.\configure.ps1 -# Sync + publish only (skip init-repo prompt) -.\update.ps1 -SkipInit - -# Re-publish only (cache already up to date) -.\update.ps1 -SkipSync -SkipInit - -# Preview everything without writing any files -.\update.ps1 -DryRun +# Or step by step: +.\configure.ps1 -SkipInit # sync + publish only +.\configure.ps1 -SkipSync -SkipPublish -SkipInit -InstallTask # install scheduled task +.\configure.ps1 -DryRun # preview everything ``` ### 3. Initialise a Repo (optional, interactive) +> **Note:** `configure.ps1` also prompts for this step automatically — you only need to run it directly for a targeted repo setup. + ```powershell # Run from inside any repo to add instructions/hooks/workflows cd C:\Projects\my-app -.\init-repo.ps1 +.\scripts\init-repo.ps1 # Or specify the path explicitly -.\init-repo.ps1 -RepoPath "C:\Projects\my-app" +.\scripts\init-repo.ps1 -RepoPath "C:\Projects\my-app" ``` A selection UI will appear for each category (Out-GridView on Windows, or a numbered console menu). Items already installed in the repo are marked with `[*]`. @@ -73,17 +71,14 @@ A selection UI will appear for each category (Out-GridView on Windows, or a numb ```powershell # Install a scheduled task that syncs + publishes globally every 4 hours -.\install-scheduled-task.ps1 - -# Skip the publish-global step if you manage that manually -.\install-scheduled-task.ps1 -SkipPublishGlobal +.\configure.ps1 -SkipSync -SkipPublish -SkipInit -InstallTask -# Also include plugins (opt-in — large download) -.\install-scheduled-task.ps1 -IncludePlugins +# Customize the interval +.\configure.ps1 -SkipSync -SkipPublish -SkipInit -InstallTask -Every "2h" # Every 2 hours +.\configure.ps1 -SkipSync -SkipPublish -SkipInit -InstallTask -Every "30m" # Every 30 minutes -# Or customize the interval -.\install-scheduled-task.ps1 -Every "2h" # Every 2 hours -.\install-scheduled-task.ps1 -Every "30m" # Every 30 minutes +# Or run it directly (called internally by configure.ps1) +.\scripts\install-scheduled-task.ps1 -Every "2h" ``` ## 📁 What Gets Created @@ -106,33 +101,38 @@ $HOME\.awesome-copilot\ # Local cache ## 📜 Scripts Overview -### `update.ps1` -Interactive orchestrator that chains sync → publish → init-repo in one command. +### `configure.ps1` +Main entry point at the repo root. Chains sync → publish → init-repo in one command, and can install/uninstall the scheduled task via `-InstallTask` / `-UninstallTask` / `-Every` switches. **Features:** - Shows last sync time from the local cache manifest before running - Runs each step in sequence; any step can be skipped independently -- Prompts before running `init-repo.ps1` (with option to skip via `-SkipInit`) +- Prompts before running `scripts/init-repo.ps1` (with option to skip via `-SkipInit`) - `-DryRun` passes through to all child scripts +- `-InstallTask` / `-UninstallTask` delegate to `scripts/install-scheduled-task.ps1` / `scripts/uninstall-scheduled-task.ps1` +- `-Every` sets the scheduled task interval (e.g. `"2h"`, `"30m"`) **Usage:** ```powershell # Full update: sync + publish + prompt for init-repo -.\update.ps1 +.\configure.ps1 # Sync + publish only -.\update.ps1 -SkipInit +.\configure.ps1 -SkipInit # Re-publish only (skip sync if cache is already fresh) -.\update.ps1 -SkipSync -SkipInit +.\configure.ps1 -SkipSync -SkipInit # Preview without writing any files -.\update.ps1 -DryRun +.\configure.ps1 -DryRun + +# Install scheduled task +.\configure.ps1 -SkipSync -SkipPublish -SkipInit -InstallTask -Every "2h" ``` --- -### `sync-awesome-copilot.ps1` +### `scripts/sync-awesome-copilot.ps1` Syncs resources from the awesome-copilot GitHub repository. **Features:** @@ -144,7 +144,7 @@ Syncs resources from the awesome-copilot GitHub repository. **Usage:** ```powershell -.\sync-awesome-copilot.ps1 +.\scripts\sync-awesome-copilot.ps1 ``` Syncs these categories by default: `agents`, `instructions`, `workflows`, `hooks`, `skills`. @@ -155,7 +155,7 @@ Add `plugins` or `cookbook` explicitly via `-Categories` for those larger opt-in --- -### `publish-global.ps1` +### `scripts/publish-global.ps1` Publishes agents globally to VS Code and skills to `~/.copilot/skills/`. **Features:** @@ -166,21 +166,21 @@ Publishes agents globally to VS Code and skills to `~/.copilot/skills/`. **Usage:** ```powershell -.\publish-global.ps1 +.\scripts\publish-global.ps1 # Preview changes without applying -.\publish-global.ps1 -DryRun +.\scripts\publish-global.ps1 -DryRun # Skills only (agents already published) -.\publish-global.ps1 -SkipAgents +.\scripts\publish-global.ps1 -SkipAgents # Custom target path (e.g. named VS Code profile) -.\publish-global.ps1 -AgentsTarget "$env:APPDATA\Code\User\profiles\Work\prompts" +.\scripts\publish-global.ps1 -AgentsTarget "$env:APPDATA\Code\User\profiles\Work\prompts" ``` --- -### `init-repo.ps1` +### `scripts/init-repo.ps1` Interactively initialises a repository with agents, instructions, hooks, and agentic workflows. **Features:** @@ -195,51 +195,55 @@ Interactively initialises a repository with agents, instructions, hooks, and age **Usage:** ```powershell # Run inside a repo (uses current directory) -.\init-repo.ps1 +.\scripts\init-repo.ps1 # Target a specific repo -.\init-repo.ps1 -RepoPath "C:\Projects\my-app" +.\scripts\init-repo.ps1 -RepoPath "C:\Projects\my-app" # Preview without writing any files -.\init-repo.ps1 -DryRun +.\scripts\init-repo.ps1 -DryRun # Skip categories you don't need -.\init-repo.ps1 -SkipHooks -SkipWorkflows +.\scripts\init-repo.ps1 -SkipHooks -SkipWorkflows # Non-interactive: specify items by name -.\init-repo.ps1 -Agents "devops-expert,se-security-reviewer" -Instructions "powershell" +.\scripts\init-repo.ps1 -Agents "devops-expert,se-security-reviewer" -Instructions "powershell" ``` -### `install-scheduled-task.ps1` -Creates a Windows scheduled task for automatic syncing and global publishing. +### `scripts/install-scheduled-task.ps1` +Creates a Windows scheduled task for automatic syncing and global publishing. Called internally by `configure.ps1 -InstallTask`. **Features:** - Runs `sync-awesome-copilot.ps1` then `publish-global.ps1` on a schedule - Default: every 6 hours -- Customizable interval +- Customizable interval via `-Every` **Usage:** ```powershell -# Install with defaults (sync + publish-global every 4 hours) -.\install-scheduled-task.ps1 +# Recommended: use configure.ps1 +.\configure.ps1 -SkipSync -SkipPublish -SkipInit -InstallTask -# Custom intervals (supports h = hours, m = minutes) -.\install-scheduled-task.ps1 -Every "2h" # Every 2 hours -.\install-scheduled-task.ps1 -Every "30m" # Every 30 minutes +# Or run directly +.\scripts\install-scheduled-task.ps1 -# Sync only (skip publish-global) -.\install-scheduled-task.ps1 -SkipPublishGlobal +# Custom intervals (supports h = hours, m = minutes) +.\scripts\install-scheduled-task.ps1 -Every "2h" # Every 2 hours +.\scripts\install-scheduled-task.ps1 -Every "30m" # Every 30 minutes # Check task status Get-ScheduledTask -TaskName "AwesomeCopilotSync" ``` -### `uninstall-scheduled-task.ps1` -Removes the scheduled task. +### `scripts/uninstall-scheduled-task.ps1` +Removes the scheduled task. Called internally by `configure.ps1 -UninstallTask`. **Usage:** ```powershell -.\uninstall-scheduled-task.ps1 +# Recommended: use configure.ps1 +.\configure.ps1 -UninstallTask + +# Or run directly +.\scripts\uninstall-scheduled-task.ps1 ``` ## 🔧 Configuration diff --git a/configure.ps1 b/configure.ps1 new file mode 100644 index 0000000..f818039 --- /dev/null +++ b/configure.ps1 @@ -0,0 +1,135 @@ +<# +Configure Copilot Resources + +Main entry point for all Copilot resource management operations. +Chains the scripts in the correct order: + + 1. sync-awesome-copilot.ps1 -- fetch latest from github/awesome-copilot + 2. publish-global.ps1 -- publish agents + skills globally + 3. init-repo.ps1 -- (prompted) per-repo .github/ setup + 4. install/uninstall-scheduled-task.ps1 -- (explicit) automate sync + publish + +Usage: + # Full interactive run: sync + publish + prompt for init-repo + .\configure.ps1 + + # Sync + publish only (skip init-repo prompt) + .\configure.ps1 -SkipInit + + # Re-publish only (cache already up to date) + .\configure.ps1 -SkipSync -SkipInit + + # Install scheduled task (sync every 4h + publish globally) + .\configure.ps1 -SkipInit -InstallTask + + # Install task with custom interval + .\configure.ps1 -SkipInit -InstallTask -Every "2h" + + # Uninstall scheduled task + .\configure.ps1 -SkipSync -SkipPublish -SkipInit -UninstallTask + + # Preview without writing any files + .\configure.ps1 -DryRun +#> +[CmdletBinding()] param( + [switch]$SkipSync, + [switch]$SkipPublish, + [switch]$SkipInit, + [switch]$InstallTask, + [switch]$UninstallTask, + [string]$Every = '4h', # Interval for -InstallTask (e.g. 4h, 30m) + [switch]$DryRun +) + +$ErrorActionPreference = 'Stop' +$ScriptDir = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) 'scripts' + +function Log($m, [string]$level = 'INFO') { + $ts = (Get-Date).ToString('s') + $color = switch ($level) { 'ERROR' { 'Red' } 'WARN' { 'Yellow' } 'SUCCESS' { 'Green' } default { 'Cyan' } } + Write-Host "[$ts][$level] $m" -ForegroundColor $color +} + +function Step($label) { + Write-Host "" + Write-Host " ── $label ──" -ForegroundColor Magenta + Write-Host "" +} + +# Show cache state +$manifest = "$HOME\.awesome-copilot\manifest.json" +if (Test-Path $manifest) { + try { + $m = Get-Content $manifest -Raw | ConvertFrom-Json + Log "Cache last synced: $($m.fetchedAt) Items: $($m.items.Count)" + } catch {} +} else { + Log "No local cache found — sync will download everything fresh." 'WARN' +} + +# --------------------------------------------------------------------------- +# STEP 1: Sync +# --------------------------------------------------------------------------- +if (-not $SkipSync) { + Step "Sync from github/awesome-copilot" + $syncArgs = @{} + if ($DryRun) { $syncArgs['Plan'] = $true } + & (Join-Path $ScriptDir 'sync-awesome-copilot.ps1') @syncArgs + if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { Log "Sync failed (exit $LASTEXITCODE)" 'ERROR'; exit $LASTEXITCODE } +} + +# --------------------------------------------------------------------------- +# STEP 2: Publish globally +# --------------------------------------------------------------------------- +if (-not $SkipPublish) { + Step "Publish agents + skills globally" + $publishArgs = @{} + if ($DryRun) { $publishArgs['DryRun'] = $true } + & (Join-Path $ScriptDir 'publish-global.ps1') @publishArgs +} + +# --------------------------------------------------------------------------- +# STEP 3: Init repo (prompted) +# --------------------------------------------------------------------------- +if (-not $SkipInit) { + Step "Init repo" + Write-Host " Add agents/instructions/hooks/workflows to .github/ in the current repo?" -ForegroundColor Yellow + Write-Host " [Y] Yes [N] No (default): " -NoNewline -ForegroundColor Yellow + $answer = (Read-Host).Trim() + if ($answer -match '^[Yy]') { + $initArgs = @{} + if ($DryRun) { $initArgs['DryRun'] = $true } + & (Join-Path $ScriptDir 'init-repo.ps1') @initArgs + } else { + Log "init-repo skipped." + } +} + +# --------------------------------------------------------------------------- +# STEP 4: Scheduled task +# --------------------------------------------------------------------------- +if ($InstallTask -and $UninstallTask) { + Log "-InstallTask and -UninstallTask cannot both be set." 'ERROR'; exit 1 +} + +if ($InstallTask) { + Step "Install scheduled task" + if ($DryRun) { + Log "[DryRun] Would install scheduled task (every $Every): sync + publish-global" + } else { + $taskArgs = @{ Every = $Every } + & (Join-Path $ScriptDir 'install-scheduled-task.ps1') @taskArgs + } +} + +if ($UninstallTask) { + Step "Uninstall scheduled task" + if ($DryRun) { + Log "[DryRun] Would uninstall scheduled task" + } else { + & (Join-Path $ScriptDir 'uninstall-scheduled-task.ps1') + } +} + +Write-Host "" +Log "Done." 'SUCCESS' diff --git a/init-repo.ps1 b/scripts/init-repo.ps1 similarity index 100% rename from init-repo.ps1 rename to scripts/init-repo.ps1 diff --git a/install-scheduled-task.ps1 b/scripts/install-scheduled-task.ps1 similarity index 100% rename from install-scheduled-task.ps1 rename to scripts/install-scheduled-task.ps1 diff --git a/publish-global.ps1 b/scripts/publish-global.ps1 similarity index 100% rename from publish-global.ps1 rename to scripts/publish-global.ps1 diff --git a/sync-awesome-copilot.ps1 b/scripts/sync-awesome-copilot.ps1 similarity index 100% rename from sync-awesome-copilot.ps1 rename to scripts/sync-awesome-copilot.ps1 diff --git a/uninstall-scheduled-task.ps1 b/scripts/uninstall-scheduled-task.ps1 similarity index 100% rename from uninstall-scheduled-task.ps1 rename to scripts/uninstall-scheduled-task.ps1 diff --git a/update.ps1 b/update.ps1 deleted file mode 100644 index 49c933e..0000000 --- a/update.ps1 +++ /dev/null @@ -1,106 +0,0 @@ -<# -Interactive Update — Sync, Publish, and Initialise - -Chains the three main scripts for interactive use: - 1. sync-awesome-copilot.ps1 -- fetch latest from github/awesome-copilot - 2. publish-global.ps1 -- publish agents + skills globally - 3. init-repo.ps1 -- (prompted) per-repo setup for .github/ - -Usage: - # Full update: sync + publish + prompt for init-repo - .\update.ps1 - - # Skip sync (reuse existing cache, e.g. already ran today) - .\update.ps1 -SkipSync - - # Skip sync and publish (init-repo only) - .\update.ps1 -SkipSync -SkipPublish - - # Skip the init-repo prompt (sync + publish only) - .\update.ps1 -SkipInit - - # Dry run throughout - .\update.ps1 -DryRun -#> -[CmdletBinding()] param( - [switch]$SkipSync, - [switch]$SkipPublish, - [switch]$SkipInit, - [switch]$DryRun -) - -$ErrorActionPreference = 'Stop' -$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path - -function Log($m, [string]$level = 'INFO') { - $ts = (Get-Date).ToString('s') - $color = switch ($level) { 'ERROR' { 'Red' } 'WARN' { 'Yellow' } 'SUCCESS' { 'Green' } default { 'Cyan' } } - Write-Host "[$ts][$level] $m" -ForegroundColor $color -} - -function Step($n, $total, $label) { - Write-Host "" - Write-Host " ── Step $n/$total : $label ──" -ForegroundColor Magenta - Write-Host "" -} - -$totalSteps = 3 - [int]$SkipSync.IsPresent - [int]$SkipPublish.IsPresent - [int]$SkipInit.IsPresent -$step = 0 - -# Show last sync info if cache exists -$manifest = "$HOME\.awesome-copilot\manifest.json" -if (Test-Path $manifest) { - try { - $m = Get-Content $manifest -Raw | ConvertFrom-Json - Log "Cache last synced: $($m.fetchedAt) Items: $($m.items.Count)" - } catch {} -} - -# --------------------------------------------------------------------------- -# STEP 1: Sync -# --------------------------------------------------------------------------- -if (-not $SkipSync) { - $step++ - Step $step $totalSteps "Sync from github/awesome-copilot" - $syncScript = Join-Path $ScriptDir 'sync-awesome-copilot.ps1' - $syncArgs = @{} - if ($DryRun) { $syncArgs['Plan'] = $true } - & $syncScript @syncArgs - if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { Log "Sync failed (exit $LASTEXITCODE)" 'ERROR'; exit $LASTEXITCODE } -} - -# --------------------------------------------------------------------------- -# STEP 2: Publish globally -# --------------------------------------------------------------------------- -if (-not $SkipPublish) { - $step++ - Step $step $totalSteps "Publish agents + skills globally" - $publishScript = Join-Path $ScriptDir 'publish-global.ps1' - $publishArgs = @{} - if ($DryRun) { $publishArgs['DryRun'] = $true } - & $publishScript @publishArgs -} - -# --------------------------------------------------------------------------- -# STEP 3: Init repo (prompted) -# --------------------------------------------------------------------------- -if (-not $SkipInit) { - $step++ - Write-Host "" - Write-Host " ── Step $step/$totalSteps : Init repo (optional) ──" -ForegroundColor Magenta - Write-Host "" - Write-Host " Run init-repo.ps1 to add agents/instructions/hooks/workflows to .github/ in the current repo?" -ForegroundColor Yellow - Write-Host " [Y] Yes [N] No (default): " -NoNewline -ForegroundColor Yellow - $answer = (Read-Host).Trim() - if ($answer -match '^[Yy]') { - $initScript = Join-Path $ScriptDir 'init-repo.ps1' - $initArgs = @{} - if ($DryRun) { $initArgs['DryRun'] = $true } - & $initScript @initArgs - } else { - Log "init-repo skipped." - } -} - -Write-Host "" -Log "Update complete." 'SUCCESS' From 0aadb121a1baf97d67e08bb1ce5dbdbd2ff85550 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:41:57 +0000 Subject: [PATCH 20/60] feat(sync): replace API-based download with git sparse-checkout - Clone github/awesome-copilot with --depth 1 --filter=blob:none --sparse on first run; subsequent runs use git pull (delta only) - Prefers gh CLI (auto auth); falls back to git; -GitTool param to override - Removes ~700 individual HTTP requests in favour of a single bulk transfer - Removes GitHub API rate-limit concerns entirely - Migrates existing non-git cache automatically (renames to backup dir) - Removes -NoDelete, -DiffOnly, -SkipBackup, -BackupRetention (git handles these) - manifest.json still written from local file scan for backward compat Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 13 + README.md | 30 ++- scripts/sync-awesome-copilot.ps1 | 430 ++++++++++++------------------- 3 files changed, 193 insertions(+), 280 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c89795c..047a718 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,19 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.0] - 2026-02-27 + +### Changed +- `scripts/sync-awesome-copilot.ps1`: **Rewritten** — replaced GitHub API + per-file HTTP download approach with `git sparse-checkout`. First run clones `github/awesome-copilot` shallowly with only the requested categories; subsequent runs run `git pull` for near-instant delta updates. Dramatically faster (single bulk transfer vs 700+ individual HTTP requests) and removes GitHub API rate-limit concerns entirely. + - Prefers `gh` (GitHub CLI) for automatic auth; falls back to `git` + - New `-GitTool auto|gh|git` parameter to override tool selection + - Removed parameters: `-NoDelete`, `-DiffOnly`, `-SkipBackup`, `-BackupRetention` (git handles all of these natively) + - Migrates automatically from the old API-based cache (renames non-git `~/.awesome-copilot/` to `~/.awesome-copilot-backup-` before cloning) + - `manifest.json` still written (from local file scan) for backward compatibility with `publish-global.ps1` and `configure.ps1` + +### Added +- `README.md`: document `gh`/`git` requirement; update sync section to reflect git-based approach + ## [1.1.2] - 2026-02-27 ### Added diff --git a/README.md b/README.md index d6a03af..288f38b 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,8 @@ These scripts automate the management of VS Code Copilot custom agents, instruct - **Windows** with PowerShell 7+ ([Download here](https://github.com/PowerShell/PowerShell/releases)) - **VS Code** with GitHub Copilot extension installed -- **Internet connection** for GitHub API access +- **`gh` (GitHub CLI) or `git`** — `gh` is preferred ([Download here](https://cli.github.com/)); handles auth automatically +- **Internet connection** for GitHub access - **Administrator privileges** (for creating scheduled tasks) ## 🚀 Quick Start @@ -84,7 +85,8 @@ A selection UI will appear for each category (Out-GridView on Windows, or a numb ## 📁 What Gets Created ``` -$HOME\.awesome-copilot\ # Local cache +$HOME\.awesome-copilot\ # Local cache (git sparse clone) +├── .git\ # Git metadata (managed automatically) ├── agents\ # Custom agents (.agent.md) ├── instructions\ # Custom instructions (.instructions.md) ├── workflows\ # Agentic workflow definitions @@ -133,26 +135,32 @@ Main entry point at the repo root. Chains sync → publish → init-repo in one --- ### `scripts/sync-awesome-copilot.ps1` -Syncs resources from the awesome-copilot GitHub repository. +Syncs resources from the awesome-copilot GitHub repository using a sparse git clone. **Features:** -- Downloads latest resources via GitHub API -- SHA256 hash-based change detection -- Incremental updates (only downloads changed files) -- Manifest tracking for sync state -- Optional GITHUB_TOKEN support for higher rate limits +- Clones `github/awesome-copilot` with sparse checkout (first run) — only downloads the categories you need +- Pulls updates on subsequent runs — git transfers only the diff, making updates near-instant +- SHA256 hash-based change detection against previous manifest (added/updated/unchanged/removed counts) +- Prefers `gh` (GitHub CLI) for automatic auth; falls back to `git` +- Automatically migrates from the old API-based cache if detected **Usage:** ```powershell .\scripts\sync-awesome-copilot.ps1 + +# Dry-run: show what would change without writing files +.\scripts\sync-awesome-copilot.ps1 -Plan + +# Sync specific categories only +.\scripts\sync-awesome-copilot.ps1 -Categories "agents,instructions" + +# Force a specific tool +.\scripts\sync-awesome-copilot.ps1 -GitTool git ``` Syncs these categories by default: `agents`, `instructions`, `workflows`, `hooks`, `skills`. Add `plugins` or `cookbook` explicitly via `-Categories` for those larger opt-in collections. -**Environment Variables:** -- `GITHUB_TOKEN` (optional) - Personal access token for higher API rate limits - --- ### `scripts/publish-global.ps1` diff --git a/scripts/sync-awesome-copilot.ps1 b/scripts/sync-awesome-copilot.ps1 index 6e4f160..447775f 100644 --- a/scripts/sync-awesome-copilot.ps1 +++ b/scripts/sync-awesome-copilot.ps1 @@ -1,25 +1,43 @@ +<# +Sync Awesome Copilot Resources + +Clones (first run) or pulls (subsequent runs) the github/awesome-copilot repository +using sparse checkout — only the categories you need are fetched. + +Requires 'gh' (GitHub CLI, preferred) or 'git' to be installed. + +Usage: + # Sync all default categories + .\sync-awesome-copilot.ps1 + + # Dry-run: show what would change without writing files + .\sync-awesome-copilot.ps1 -Plan + + # Sync specific categories only + .\sync-awesome-copilot.ps1 -Categories "agents,instructions" + + # Force a specific git tool + .\sync-awesome-copilot.ps1 -GitTool git +#> [CmdletBinding()] param( [string]$Dest = "$HOME/.awesome-copilot", - # Default covers all main categories; add 'plugins' or 'cookbook' explicitly for larger opt-in categories [string]$Categories = 'agents,instructions,workflows,hooks,skills', [switch]$Quiet, - [switch]$NoDelete, - [switch]$DiffOnly, - [switch]$Plan, # Dry-run: compute changes, no file writes / deletions / manifest update - [switch]$SkipBackup, # Skip pre-deletion backup snapshot - [int]$BackupRetention = 5, # Number of recent backups to retain + [switch]$Plan, # Dry-run: show what would change without writing files [int]$LogRetentionDays = 14, - [int]$TimeoutSeconds = 600 + [int]$TimeoutSeconds = 600, + [ValidateSet('auto', 'gh', 'git')] + [string]$GitTool = 'auto' ) $ErrorActionPreference = 'Stop' $script:StartTime = Get-Date -$script:Deadline = $script:StartTime.AddSeconds($TimeoutSeconds) +$script:Deadline = $script:StartTime.AddSeconds($TimeoutSeconds) function Write-Log { param([string]$Message, [string]$Level = 'INFO') - $ts = (Get-Date).ToString('s') + $ts = (Get-Date).ToString('s') $line = "[$ts][$Level] $Message" if (-not $Quiet) { Write-Host $line } Add-Content -Path $Global:LogFile -Value $line @@ -27,319 +45,193 @@ function Write-Log { function Check-Timeout { if ((Get-Date) -gt $script:Deadline) { - Write-Log "Timeout reached, aborting." 'ERROR' + Write-Log "Timeout reached ($TimeoutSeconds s), aborting." 'ERROR' exit 1 } } -# Prepare paths -$Root = Resolve-Path -Path . | Select-Object -ExpandProperty Path +# Prepare log $RunId = (Get-Date -Format 'yyyyMMdd-HHmmss') if (-not (Test-Path logs)) { New-Item -ItemType Directory -Path logs | Out-Null } $Global:LogFile = Join-Path logs "sync-$RunId.log" -Write-Log "Starting Awesome Copilot scheduled sync. Dest=$Dest Categories=$Categories" 'INFO' - -# Ensure destination -if (-not (Test-Path $Dest)) { New-Item -ItemType Directory -Path $Dest -Force | Out-Null } - -$ManifestPath = Join-Path $Dest 'manifest.json' -$StatusPath = Join-Path $Dest 'status.txt' +Write-Log "Starting Awesome Copilot sync. Dest=$Dest Categories=$Categories" -# Load previous manifest -$PrevManifest = $null -if (Test-Path $ManifestPath) { - try { $PrevManifest = Get-Content $ManifestPath -Raw | ConvertFrom-Json } catch { Write-Log "Failed to parse previous manifest: $_" 'WARN' } +# --------------------------------------------------------------------------- +# Tool detection — prefer gh (handles auth automatically), fall back to git +# --------------------------------------------------------------------------- +function Resolve-GitTool { + if ($GitTool -ne 'auto') { + if (-not (Get-Command $GitTool -ErrorAction SilentlyContinue)) { + Write-Log "'$GitTool' not found on PATH." 'ERROR'; exit 1 + } + return $GitTool + } + if (Get-Command gh -ErrorAction SilentlyContinue) { return 'gh' } + if (Get-Command git -ErrorAction SilentlyContinue) { return 'git' } + Write-Log "Neither 'gh' nor 'git' found on PATH. Install one to continue." 'ERROR' + exit 1 } +$Tool = Resolve-GitTool +Write-Log "Using tool: $Tool" + +$RepoSlug = 'github/awesome-copilot' +$RepoUrl = 'https://github.com/github/awesome-copilot.git' $CategoriesList = $Categories.Split(',') | ForEach-Object { $_.Trim() } | Where-Object { $_ } +$ManifestPath = Join-Path $Dest 'manifest.json' +$StatusPath = Join-Path $Dest 'status.txt' -$Repo = 'github/awesome-copilot' -$ApiBase = 'https://api.github.com' -$UserAgent = 'awesome-copilot-scheduled-sync' -$Token = $env:GITHUB_TOKEN - -function Invoke-Github { - param( - [string]$Url, - [int]$Attempt = 1 - ) - Check-Timeout - $Headers = @{ 'User-Agent' = $UserAgent; 'Accept' = 'application/vnd.github.v3+json' } - if ($Token) { $Headers['Authorization'] = "Bearer $Token" } +# Load previous manifest for change detection +$PrevManifest = $null +$PrevIndex = @{} +if (Test-Path $ManifestPath) { try { - return Invoke-RestMethod -Uri $Url -Headers $Headers -TimeoutSec 60 - } - catch { - # Rate limit detection (403 + Remaining=0) - try { - $resp = $_.Exception.Response - if ($resp -and $resp.StatusCode.value__ -eq 403) { - $remainingHeader = $resp.Headers['X-RateLimit-Remaining'] - if ($remainingHeader -eq '0') { - $script:RateLimitHit = $true - Write-Log "Rate limit hit for $Url (Remaining=0)." 'WARN' - } + $PrevManifest = Get-Content $ManifestPath -Raw | ConvertFrom-Json + if ($PrevManifest.items) { + foreach ($it in $PrevManifest.items) { + $PrevIndex["$($it.category)|$($it.path)"] = $it } } - catch {} - if ($Attempt -lt 3 -and ($_.Exception.Response.StatusCode.value__ -ge 500 -or $_.Exception.Response.StatusCode.value__ -eq 429)) { - $delay = [math]::Pow(2, $Attempt) - Write-Log "Transient error on $Url. Retry in $delay s" 'WARN' - Start-Sleep -Seconds $delay - return Invoke-Github -Url $Url -Attempt ($Attempt + 1) - } - Write-Log "Request failed: $Url :: $_" 'ERROR' - throw } + catch { Write-Log "Failed to parse previous manifest: $_" 'WARN' } } -function Get-FileHashSha256String { - param([byte[]]$Bytes) - $sha256 = [System.Security.Cryptography.SHA256]::Create() - $hashBytes = $sha256.ComputeHash($Bytes) - ($hashBytes | ForEach-Object { $_.ToString('x2') }) -join '' +function Get-Sha256 { + param([string]$FilePath) + $sha256 = [System.Security.Cryptography.SHA256]::Create() + $hashBytes = $sha256.ComputeHash([System.IO.File]::ReadAllBytes($FilePath)) + return ($hashBytes | ForEach-Object { $_.ToString('x2') }) -join '' } -# Recursively fetch all file entries under a GitHub contents API URL. -# Handles both flat directories and nested subdirectory structures (e.g. skills/, hooks/). -function Get-RepoFiles { - param([string]$Url) - Check-Timeout - try { $entries = Invoke-Github -Url $Url } catch { Write-Log "Failed to list $Url : $_" 'ERROR'; return @() } - $files = @() - foreach ($entry in $entries) { - if ($entry.type -eq 'file') { - $files += $entry - } - elseif ($entry.type -eq 'dir') { - $files += Get-RepoFiles -Url $entry.url - } - } - return $files -} +# --------------------------------------------------------------------------- +# Clone (first run) or pull (subsequent runs) +# --------------------------------------------------------------------------- +$IsFirstRun = -not (Test-Path (Join-Path $Dest '.git')) -if ($DiffOnly) { - if (-not $PrevManifest) { Write-Log "No previous manifest; diff-only mode cannot proceed." 'ERROR'; exit 1 } - Write-Log "Diff-only mode: no network calls. Summarizing previous manifest." 'INFO' - $summary = $PrevManifest.summary - $content = @() - $content += "Diff-only summary (previous run)" - $content += "Added: $($summary.added)" - $content += "Updated: $($summary.updated)" - $content += "Removed: $($summary.removed)" - $content += "Unchanged:$($summary.unchanged)" - Set-Content -Path $StatusPath -Value ($content -join [Environment]::NewLine) +if ($Plan) { + if ($IsFirstRun) { + Write-Log "[Plan] Would clone $RepoSlug → $Dest (sparse: $($CategoriesList -join ', '))" 'INFO' + } else { + Write-Log "[Plan] Would pull latest changes from $RepoSlug into $Dest" 'INFO' + } + Write-Log "[Plan] No files written. Exiting." 'INFO' exit 0 } -$NewItems = @() -$Added = 0; $Updated = 0; $Removed = 0; $Unchanged = 0 -$PrevIndex = @{} -if ($PrevManifest -and $PrevManifest.items) { - foreach ($it in $PrevManifest.items) { $PrevIndex["$($it.category)|$($it.path)"] = $it } -} +if ($IsFirstRun) { + Write-Log "First run — cloning $RepoSlug (sparse, shallow)..." -foreach ($cat in $CategoriesList) { - Write-Log "Fetching category: $cat" 'INFO' - $url = "$ApiBase/repos/$Repo/contents/$cat" - try { - $allEntries = Get-RepoFiles -Url $url + # Migrate: if a non-git directory already exists (e.g. from the old API-based sync), + # rename it so git can clone into a clean destination. + if ((Test-Path $Dest) -and (Get-ChildItem $Dest -Force | Measure-Object).Count -gt 0) { + $backupPath = "${Dest}-backup-$RunId" + Write-Log "Existing non-git cache found — moving to $backupPath before cloning." 'WARN' + Move-Item $Dest $backupPath } - catch { - Write-Log "Failed to list $cat" 'ERROR' - continue + + if (-not (Test-Path $Dest)) { New-Item -ItemType Directory -Path $Dest -Force | Out-Null } + + if ($Tool -eq 'gh') { + & gh repo clone $RepoSlug $Dest -- --depth 1 --filter=blob:none --sparse 2>&1 | + ForEach-Object { Write-Log $_ } + } else { + & git clone --depth 1 --filter=blob:none --sparse $RepoUrl $Dest 2>&1 | + ForEach-Object { Write-Log $_ } } + if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { Write-Log "Clone failed (exit $LASTEXITCODE)" 'ERROR'; exit $LASTEXITCODE } + + # Set which directories to check out, then materialise them + & git -C $Dest sparse-checkout set @CategoriesList 2>&1 | Out-Null + Write-Log "Repository cloned successfully." 'SUCCESS' +} else { + Write-Log "Pulling latest changes from $RepoSlug..." - if (-not $script:SuccessfulCategories) { $script:SuccessfulCategories = @() } - $script:SuccessfulCategories += $cat + # Re-apply sparse-checkout in case -Categories changed since last run + & git -C $Dest sparse-checkout set @CategoriesList 2>&1 | Out-Null + + & git -C $Dest pull 2>&1 | ForEach-Object { Write-Log $_ } + if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { Write-Log "Pull failed (exit $LASTEXITCODE)" 'ERROR'; exit $LASTEXITCODE } +} - foreach ($entry in $allEntries) { - if (-not ($entry.name -match '\.(md|markdown|json|sh)$')) { continue } +Check-Timeout + +# --------------------------------------------------------------------------- +# Scan local files — compare against previous manifest for change counts +# --------------------------------------------------------------------------- +$NewItems = @() +$Added = 0; $Updated = 0; $Unchanged = 0; $Removed = 0 +$DestResolved = (Resolve-Path $Dest).Path + +foreach ($cat in $CategoriesList) { + $catDir = Join-Path $DestResolved $cat + if (-not (Test-Path $catDir)) { Write-Log "Category folder not found after sync: $cat" 'WARN'; continue } + + $files = Get-ChildItem -Path $catDir -Recurse -File | + Where-Object { $_.Name -match '\.(md|markdown|json|sh)$' } + + foreach ($file in $files) { Check-Timeout - $downloadUrl = $entry.download_url - if (-not $downloadUrl) { continue } - $rawBytes = $null - try { - # Primary attempt: Invoke-WebRequest and derive bytes (ContentBytes is not a valid property in modern PowerShell) - $resp = Invoke-WebRequest -Uri $downloadUrl -UserAgent $UserAgent -TimeoutSec 60 -ErrorAction Stop - if ($resp.RawContentStream) { - $ms = New-Object System.IO.MemoryStream - $resp.RawContentStream.CopyTo($ms) - $rawBytes = $ms.ToArray() - } - elseif ($resp.Content) { - # Fallback: encode string content as UTF8 (raw text files like md/json are UTF-8 on GitHub) - $rawBytes = [System.Text.Encoding]::UTF8.GetBytes($resp.Content) - } - if (-not $rawBytes -or $rawBytes.Length -eq 0) { throw "Empty response body" } - } - catch { - Write-Log "Direct download failed for $($entry.path): $_ (will fallback to contents API)" 'WARN' - try { - # Fallback: GitHub contents API returns base64 content - $fileMeta = Invoke-Github -Url "$ApiBase/repos/$Repo/contents/$($entry.path)" - if ($fileMeta.content) { - $b64 = ($fileMeta.content -replace "\s", '') - $rawBytes = [System.Convert]::FromBase64String($b64) - } - else { - throw "No content field in contents API response" - } - } - catch { - Write-Log "Failed download $($entry.path): $_" 'ERROR' - continue - } - } - $hash = Get-FileHashSha256String -Bytes $rawBytes - $key = "$cat|$($entry.path)" + $relativePath = $file.FullName.Substring($DestResolved.Length + 1) -replace '\\', '/' + $hash = Get-Sha256 -FilePath $file.FullName + $key = "$cat|$relativePath" $prev = $PrevIndex[$key] - $relativePath = $entry.path - $targetFile = Join-Path $Dest $relativePath - $targetDir = Split-Path $targetFile -Parent - if (-not (Test-Path $targetDir)) { New-Item -ItemType Directory -Path $targetDir -Force | Out-Null } - - $isChange = $true - if ($prev -and $prev.sha -eq $entry.sha -and $prev.hash -eq $hash) { $isChange = $false } - if ($isChange) { - if ($Plan) { - Write-Log "[Plan] Would save: $relativePath" 'INFO' - } - else { - # Ensure the file is fully replaced by removing it first if it exists - if (Test-Path $targetFile) { - Remove-Item $targetFile -Force - } - [System.IO.File]::WriteAllBytes($targetFile, $rawBytes) - } - if ($prev) { $Updated++ } else { $Added++ } - if (-not $Plan) { Write-Log "Saved: $relativePath" 'INFO' } - } - else { $Unchanged++ } + + if ($prev -and $prev.hash -eq $hash) { $Unchanged++ } + elseif ($prev) { $Updated++; Write-Log "Updated: $relativePath" } + else { $Added++; Write-Log "Added: $relativePath" } $NewItems += [pscustomobject]@{ category = $cat path = $relativePath - sha = $entry.sha - size = $entry.size + size = $file.Length lastFetched = (Get-Date).ToString('o') hash = $hash } } } -# Determine removals (only for categories successfully fetched this run) -if (-not $Plan -and -not $NoDelete -and $PrevManifest) { - if ($script:RateLimitHit) { - Write-Log 'Rate limit encountered this run; skipping stale file deletion.' 'WARN' - } - else { - $successful = $script:SuccessfulCategories | Sort-Object -Unique - if (-not $successful -or $successful.Count -eq 0) { - Write-Log 'No categories fetched successfully this run; skipping stale file deletion for safety.' 'WARN' - } - else { - # Backup snapshot before deletions - if (-not $SkipBackup) { - try { - $backupRoot = Join-Path $Dest 'backups' - if (-not (Test-Path $backupRoot)) { New-Item -ItemType Directory -Path $backupRoot | Out-Null } - $backupFile = Join-Path $backupRoot ("pre-delete-" + $RunId + '.zip') - Write-Log "Creating backup snapshot: $backupFile" 'INFO' - Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction SilentlyContinue - # Zip only successfully fetched category folders (if present) - $tempStage = Join-Path $backupRoot ("stage-" + $RunId) - New-Item -ItemType Directory -Path $tempStage | Out-Null - foreach ($c in $successful) { - $cDir = Join-Path $Dest $c - if (Test-Path $cDir) { Copy-Item $cDir (Join-Path $tempStage $c) -Recurse -Force } - } - [IO.Compression.ZipFile]::CreateFromDirectory($tempStage, $backupFile) - Remove-Item $tempStage -Recurse -Force -ErrorAction SilentlyContinue - # Retention - $backups = Get-ChildItem $backupRoot -Filter 'pre-delete-*.zip' | Sort-Object LastWriteTime -Descending - if ($backups.Count -gt $BackupRetention) { - $toRemove = $backups | Select-Object -Skip $BackupRetention - foreach ($oldB in $toRemove) { Remove-Item $oldB.FullName -Force } - } - } - catch { - Write-Log "Backup snapshot failed (continuing without backup): $_" 'WARN' - } - } - $NewKeySet = @{} - foreach ($ni in $NewItems) { $NewKeySet["$($ni.category)|$($ni.path)"] = $true } - foreach ($old in $PrevManifest.items) { - $k = "$($old.category)|$($old.path)" - # Only consider deletion if the category was fetched this run - if ($successful -contains $old.category) { - if (-not $NewKeySet.ContainsKey($k)) { - $Removed++ - $fileToRemove = Join-Path $Dest $old.path - if (Test-Path $fileToRemove) { Remove-Item $fileToRemove -Force } - Write-Log "Removed stale file: $($old.path)" 'INFO' - } - } - } +# Count removals (files present in previous manifest but gone after pull) +$NewKeySet = @{}; foreach ($ni in $NewItems) { $NewKeySet["$($ni.category)|$($ni.path)"] = $true } +if ($PrevManifest -and $PrevManifest.items) { + foreach ($old in $PrevManifest.items) { + if (-not $NewKeySet.ContainsKey("$($old.category)|$($old.path)")) { + $Removed++ + Write-Log "Removed: $($old.path)" } } } -if ($Plan) { - Write-Log "[Plan] Summary Added=$Added Updated=$Updated Removed=(planned) Unchanged=$Unchanged" 'INFO' - Write-Log '[Plan] No files written. Exiting without manifest/status update.' 'INFO' - exit 0 -} - -# Write manifest +# --------------------------------------------------------------------------- +# Write manifest + status +# --------------------------------------------------------------------------- $Manifest = [pscustomobject]@{ version = 1 - repo = $Repo + repo = $RepoSlug fetchedAt = (Get-Date).ToString('o') categories = $CategoriesList items = $NewItems - summary = [pscustomobject]@{ - added = $Added - updated = $Updated - removed = $Removed - unchanged = $Unchanged - } + summary = [pscustomobject]@{ added=$Added; updated=$Updated; removed=$Removed; unchanged=$Unchanged } } -if ($script:SuccessfulCategories -and $script:SuccessfulCategories.Count -gt 0) { - $Manifest | ConvertTo-Json -Depth 6 | Set-Content -Path $ManifestPath -Encoding UTF8 - # Integrity marker - try { - $integrity = [pscustomobject]@{ - fetchedAt = (Get-Date).ToString('o') - successfulCategories = ($script:SuccessfulCategories | Sort-Object -Unique) - summary = $Manifest.summary - manifestSha256 = (Get-FileHash -Algorithm SHA256 $ManifestPath).Hash - } - $integrity | ConvertTo-Json -Depth 4 | Set-Content -Path (Join-Path $Dest 'last-success.json') -Encoding UTF8 - } - catch { Write-Log "Failed writing integrity marker: $_" 'WARN' } -} -else { - Write-Log 'No successful categories; manifest not updated this run.' 'WARN' -} - -# Status file -$StatusLines = @() -$StatusLines += "Sync run: $(Get-Date -Format o)" -$StatusLines += "Added: $Added" -$StatusLines += "Updated: $Updated" -$StatusLines += "Removed: $Removed" -$StatusLines += "Unchanged:$Unchanged" -$StatusLines += "Total: $($Manifest.items.Count)" -$StatusLines += "Manifest: manifest.json" -$StatusLines += "Repo: $Repo" -$StatusLines += "Duration: $([int]((Get-Date)-$script:StartTime).TotalSeconds)s" -$StatusLines | Set-Content -Path $StatusPath -Encoding UTF8 - -Write-Log "Summary Added=$Added Updated=$Updated Removed=$Removed Unchanged=$Unchanged" 'INFO' +$Manifest | ConvertTo-Json -Depth 6 | Set-Content -Path $ManifestPath -Encoding UTF8 + +@( + "Sync run: $(Get-Date -Format o)" + "Added: $Added" + "Updated: $Updated" + "Removed: $Removed" + "Unchanged:$Unchanged" + "Total: $($NewItems.Count)" + "Manifest: manifest.json" + "Repo: $RepoSlug" + "Duration: $([int]((Get-Date)-$script:StartTime).TotalSeconds)s" +) | Set-Content -Path $StatusPath -Encoding UTF8 + +Write-Log "Summary Added=$Added Updated=$Updated Removed=$Removed Unchanged=$Unchanged" 'SUCCESS' # Log retention -Get-ChildItem logs -Filter 'sync-*.log' | Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$LogRetentionDays) } | ForEach-Object { Remove-Item $_.FullName -Force } +Get-ChildItem logs -Filter 'sync-*.log' | + Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$LogRetentionDays) } | + ForEach-Object { Remove-Item $_.FullName -Force } exit 0 From cbf398f8df782b222544815fa6924036ffa1362e Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:48:34 +0000 Subject: [PATCH 21/60] docs: final audit pass - remove stale API/rate-limit references, fix paths - install-scheduled-task.ps1: update task description (remove 'combine') - publish-global.ps1: fix profile path example (agents -> prompts) - README: fix default interval (6h -> 4h), replace GitHub rate-limit section with gh/git auth notes, fix custom repo instructions, fix log path, update file naming conventions, remove legacy chatmode/prompt references - copilot-instructions.md: update sync description (API -> git clone/pull), replace GITHUB_TOKEN/rate-limit section with gh/git note, update cache structure (remove last-success.json, backups/, fix logs/ location) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/copilot-instructions.md | 16 +++++------ README.md | 43 +++++++++++++----------------- scripts/install-scheduled-task.ps1 | 2 +- scripts/publish-global.ps1 | 2 +- 4 files changed, 28 insertions(+), 35 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 2cd7408..7ac931b 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -8,7 +8,7 @@ Scripts are designed to be run in this order: ``` configure.ps1 # Main entry point (chains all steps) -scripts/sync-awesome-copilot.ps1 # 1. Fetch from GitHub API → ~/.awesome-copilot/ +scripts/sync-awesome-copilot.ps1 # 1. Clone/pull github/awesome-copilot → ~/.awesome-copilot/ scripts/publish-global.ps1 # 2. Publish agents + skills globally scripts/init-repo.ps1 # 3. Interactive per-repo setup → .github/ scripts/install-scheduled-task.ps1 # 4. Automate steps 1+2 on a schedule @@ -64,26 +64,26 @@ $cacheDir = 'C:\Users\Someone\.awesome-copilot' ## External Dependencies -- **GitHub API**: `https://api.github.com/repos/github/awesome-copilot/contents/{category}` -- **`$env:GITHUB_TOKEN`** (optional): raises rate limit from 60 → 5000 req/hr. Set this when running the sync script manually or via scheduled task to avoid 403 errors. +- **`gh` (GitHub CLI)**: preferred tool for cloning/pulling `github/awesome-copilot`; handles authentication automatically via `gh auth login`. Falls back to `git` if `gh` is not available. - **`Out-GridView`**: used in `init-repo.ps1` for interactive picking; automatically falls back to a numbered console menu if unavailable. ## Local Cache Structure -`sync-awesome-copilot.ps1` writes to `~/.awesome-copilot/`: +`sync-awesome-copilot.ps1` writes to `~/.awesome-copilot/` (a sparse git clone): ``` ~/.awesome-copilot/ + .git/ git metadata (managed automatically) agents/ *.agent.md instructions/ *.instructions.md workflows/ *.md hooks/ / (directories) skills/ / (directories) - manifest.json tracks hashes/SHAs from last sync - last-success.json integrity marker - backups/ pre-delete zip snapshots (last 5 kept) - logs/ sync-.log (14 day retention) + manifest.json file inventory with hashes (written after each sync) + status.txt human-readable summary of last sync run ``` +Sync logs are written to a `logs/` folder in the working directory where the script was invoked (typically the repo root when run via `configure.ps1`). + ## Scheduled Task `configure.ps1 -InstallTask` chains `sync-awesome-copilot.ps1 → publish-global.ps1` and registers a Windows Scheduled Task named `AwesomeCopilotSync` (delegating to `scripts/install-scheduled-task.ps1`). The task runs under the current user context — the user must be logged in for it to execute. diff --git a/README.md b/README.md index 288f38b..d7cab48 100644 --- a/README.md +++ b/README.md @@ -223,7 +223,7 @@ Creates a Windows scheduled task for automatic syncing and global publishing. Ca **Features:** - Runs `sync-awesome-copilot.ps1` then `publish-global.ps1` on a schedule -- Default: every 6 hours +- Default: every 4 hours - Customizable interval via `-Every` **Usage:** @@ -256,41 +256,34 @@ Removes the scheduled task. Called internally by `configure.ps1 -UninstallTask`. ## 🔧 Configuration -### GitHub Rate Limits +### Authentication -Without authentication, GitHub API allows 60 requests/hour. For heavy usage: +The sync script uses `gh` (GitHub CLI) by default, which inherits authentication from `gh auth login` — no extra setup needed for most users. -1. Create a [Personal Access Token](https://github.com/settings/tokens) (no scopes needed for public repos) -2. Set environment variable: +If only `git` is available, the sync targets a public repo (`github/awesome-copilot`) so no credentials are required. For private forks, configure git credentials as usual (`git config credential.helper`). +To force a specific tool: ```powershell -# Temporary (current session) -$env:GITHUB_TOKEN = "ghp_your_token_here" - -# Permanent (user environment) -[Environment]::SetEnvironmentVariable("GITHUB_TOKEN", "ghp_your_token_here", "User") +.\scripts\sync-awesome-copilot.ps1 -GitTool git +.\scripts\sync-awesome-copilot.ps1 -GitTool gh ``` ### Custom Source Repository -By default, scripts sync from `github/awesome-copilot`. To use a different source: - -Edit `sync-awesome-copilot.ps1` line 57: +To sync from a fork or alternative repo, edit the two variables near the top of `scripts/sync-awesome-copilot.ps1`: ```powershell -$Repo = "your-username/your-repo" +$RepoSlug = 'your-username/your-repo' +$RepoUrl = 'https://github.com/your-username/your-repo.git' ``` ## 🗂️ File Naming Conventions Resources follow naming patterns for automatic categorization: -- `*.agent.md` - Custom agents -- `*.instructions.md` - Custom instructions -- `*.chatmode.md` - Chat mode definitions (legacy) -- `*.prompt.md` - Prompt templates (legacy) - -Files without these suffixes in the combined folder are preserved (assumed to be user-created). -Skills and hooks are directory-based packages and are not combined into the prompts folder. +- `*.agent.md` — Custom agents +- `*.instructions.md` — Custom instructions +- `SKILL.md` (inside a named subdirectory) — Skills +- Hook and workflow directories contain a mix of `.md`, `.json`, and `.sh` files ## 🛠️ Troubleshooting @@ -311,8 +304,8 @@ Scripts automatically fall back to copying files. Check logs for details. # Check task status Get-ScheduledTask -TaskName "AwesomeCopilotSync" | Get-ScheduledTaskInfo -# View logs -Get-Content "$HOME\.awesome-copilot\logs\sync-*.log" -Tail 50 +# View logs (written to logs/ in the directory configure.ps1 was run from) +Get-Content ".\logs\sync-*.log" | Select-Object -Last 50 # Manually run task Start-ScheduledTask -TaskName "AwesomeCopilotSync" @@ -325,10 +318,10 @@ Start-ScheduledTask -TaskName "AwesomeCopilotSync" ## 📊 Logs -Sync logs are stored in `$HOME\.awesome-copilot\logs\`: +Sync logs are written to a `logs/` folder in whichever directory you ran `configure.ps1` (or `sync-awesome-copilot.ps1`) from: ```powershell # View latest sync log -Get-Content "$HOME\.awesome-copilot\logs\sync-*.log" -Tail 20 +Get-ChildItem .\logs\sync-*.log | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | Get-Content ``` Log format: `sync-YYYYMMDD-HHMMSS.log` diff --git a/scripts/install-scheduled-task.ps1 b/scripts/install-scheduled-task.ps1 index 605de21..af6eaf5 100644 --- a/scripts/install-scheduled-task.ps1 +++ b/scripts/install-scheduled-task.ps1 @@ -52,7 +52,7 @@ if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) { Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false } -Register-ScheduledTask -TaskName $TaskName -Action $actions -Trigger $Trigger -Settings $Settings -Description "Sync (and combine) Awesome Copilot resources" | Out-Null +Register-ScheduledTask -TaskName $TaskName -Action $actions -Trigger $Trigger -Settings $Settings -Description "Sync and publish Awesome Copilot resources (agents, instructions, skills, hooks, workflows)" | Out-Null $post = if ($SkipPublishGlobal) { 'sync only' } else { 'sync + publish-global' } Write-Host "Scheduled task '$TaskName' created ($post). First run in ~1 minute, then every $Every." -ForegroundColor Green diff --git a/scripts/publish-global.ps1 b/scripts/publish-global.ps1 index 4552647..cb45c8c 100644 --- a/scripts/publish-global.ps1 +++ b/scripts/publish-global.ps1 @@ -23,7 +23,7 @@ Usage: .\publish-global.ps1 -SkipAgents # Override VS Code agents folder (e.g. for a named profile) - .\publish-global.ps1 -AgentsTarget "$env:APPDATA\Code\User\profiles\MyProfile\agents" + .\publish-global.ps1 -AgentsTarget "$env:APPDATA\Code\User\profiles\MyProfile\prompts" # Dry run - show what would happen .\publish-global.ps1 -DryRun From 599ee0ca383dc66e1393ebb91859e0388d81eede Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:51:07 +0000 Subject: [PATCH 22/60] fix(configure): auto-skip init-repo when -InstallTask or -UninstallTask is set Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- configure.ps1 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/configure.ps1 b/configure.ps1 index f818039..c5ec10e 100644 --- a/configure.ps1 +++ b/configure.ps1 @@ -112,6 +112,9 @@ if ($InstallTask -and $UninstallTask) { Log "-InstallTask and -UninstallTask cannot both be set." 'ERROR'; exit 1 } +# Task management implies no interactive repo setup +if ($InstallTask -or $UninstallTask) { $SkipInit = $true } + if ($InstallTask) { Step "Install scheduled task" if ($DryRun) { From 9f6bbd0f12b7beb94c38bcf27e31970ec9f90a66 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:52:03 +0000 Subject: [PATCH 23/60] fix(configure): move task/init logic before Step 1 so SkipInit takes effect Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- configure.ps1 | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/configure.ps1 b/configure.ps1 index c5ec10e..79c51d8 100644 --- a/configure.ps1 +++ b/configure.ps1 @@ -67,6 +67,12 @@ if (Test-Path $manifest) { Log "No local cache found — sync will download everything fresh." 'WARN' } +if ($InstallTask -and $UninstallTask) { + Log "-InstallTask and -UninstallTask cannot both be set." 'ERROR'; exit 1 +} +# Task management implies no interactive repo setup +if ($InstallTask -or $UninstallTask) { $SkipInit = $true } + # --------------------------------------------------------------------------- # STEP 1: Sync # --------------------------------------------------------------------------- @@ -108,13 +114,6 @@ if (-not $SkipInit) { # --------------------------------------------------------------------------- # STEP 4: Scheduled task # --------------------------------------------------------------------------- -if ($InstallTask -and $UninstallTask) { - Log "-InstallTask and -UninstallTask cannot both be set." 'ERROR'; exit 1 -} - -# Task management implies no interactive repo setup -if ($InstallTask -or $UninstallTask) { $SkipInit = $true } - if ($InstallTask) { Step "Install scheduled task" if ($DryRun) { From fac39eb93980f1993c36307f7b21eeae328dc6c9 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:53:14 +0000 Subject: [PATCH 24/60] fix(configure): prompt to overwrite when scheduled task already exists Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- configure.ps1 | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/configure.ps1 b/configure.ps1 index 79c51d8..5f41ed1 100644 --- a/configure.ps1 +++ b/configure.ps1 @@ -120,7 +120,19 @@ if ($InstallTask) { Log "[DryRun] Would install scheduled task (every $Every): sync + publish-global" } else { $taskArgs = @{ Every = $Every } - & (Join-Path $ScriptDir 'install-scheduled-task.ps1') @taskArgs + $taskName = 'AwesomeCopilotSync' + $proceed = $true + if (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue) { + Log "Scheduled task '$taskName' already exists." 'WARN' + Write-Host " Overwrite existing task? [Y] Yes [N] No (default): " -NoNewline -ForegroundColor Yellow + if ((Read-Host).Trim() -match '^[Yy]') { + $taskArgs['Force'] = $true + } else { + Log "Task install skipped." + $proceed = $false + } + } + if ($proceed) { & (Join-Path $ScriptDir 'install-scheduled-task.ps1') @taskArgs } } } From c562eae31800226f0a8eb34607fb31aa7526b73e Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 15:57:20 +0000 Subject: [PATCH 25/60] fix(task): set WorkingDirectory on scheduled task actions to scripts/ dir Without this the task ran from C:\Windows\System32, causing the relative 'logs/' path in sync-awesome-copilot.ps1 to fail with a permissions error (the brief red flash seen on task execution). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/install-scheduled-task.ps1 | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/scripts/install-scheduled-task.ps1 b/scripts/install-scheduled-task.ps1 index af6eaf5..4e48c2e 100644 --- a/scripts/install-scheduled-task.ps1 +++ b/scripts/install-scheduled-task.ps1 @@ -35,14 +35,15 @@ if ($IncludePlugins -and ($Categories -notmatch 'plugins')) { $int = Parse-Interval $Every # Primary sync action -$syncArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`" -Dest `"$Dest`" -Categories `"$Categories`" -Quiet" -$actions = @() -$actions += New-ScheduledTaskAction -Execute $PwshPath -Argument $syncArgs +$ScriptDir = Split-Path -Parent $ScriptPath +$syncArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`" -Dest `"$Dest`" -Categories `"$Categories`" -Quiet" +$actions = @() +$actions += New-ScheduledTaskAction -Execute $PwshPath -Argument $syncArgs -WorkingDirectory $ScriptDir if (-not $SkipPublishGlobal) { # publish-global runs after sync: updates VS Code agents folder and ~/.copilot/skills/ - $publishArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$PublishGlobalScriptPath`" -SourceRoot `"$Dest`"" - $actions += New-ScheduledTaskAction -Execute $PwshPath -Argument $publishArgs + $publishArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$PublishGlobalScriptPath`" -SourceRoot `"$Dest`"" + $actions += New-ScheduledTaskAction -Execute $PwshPath -Argument $publishArgs -WorkingDirectory $ScriptDir } $Trigger = if ($int.Type -eq 'HOURLY') { New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval ([TimeSpan]::FromHours($int.Modifier)) -RepetitionDuration ([TimeSpan]::FromDays(3650)) } else { New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval ([TimeSpan]::FromMinutes($int.Modifier)) -RepetitionDuration ([TimeSpan]::FromDays(3650)) } $Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 20) From 79457e0a33e876e5b38e5456581815fcdee7578f Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:02:02 +0000 Subject: [PATCH 26/60] fix(sync): use PSScriptRoot for logs dir so path is consistent Previously logs were written relative to CWD, creating two separate logs/ folders (repo root for interactive runs, scripts/ for scheduled task). Now always writes to scripts/logs/ via \ regardless of CWD. Removed root logs/ directory. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 8 ++++---- scripts/sync-awesome-copilot.ps1 | 11 ++++++----- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index d7cab48..62e0b46 100644 --- a/README.md +++ b/README.md @@ -304,8 +304,8 @@ Scripts automatically fall back to copying files. Check logs for details. # Check task status Get-ScheduledTask -TaskName "AwesomeCopilotSync" | Get-ScheduledTaskInfo -# View logs (written to logs/ in the directory configure.ps1 was run from) -Get-Content ".\logs\sync-*.log" | Select-Object -Last 50 +# View logs +Get-Content ".\scripts\logs\sync-*.log" | Select-Object -Last 50 # Manually run task Start-ScheduledTask -TaskName "AwesomeCopilotSync" @@ -318,10 +318,10 @@ Start-ScheduledTask -TaskName "AwesomeCopilotSync" ## 📊 Logs -Sync logs are written to a `logs/` folder in whichever directory you ran `configure.ps1` (or `sync-awesome-copilot.ps1`) from: +Sync logs are always written to `scripts/logs/` (next to the script itself, regardless of where you invoke it from): ```powershell # View latest sync log -Get-ChildItem .\logs\sync-*.log | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | Get-Content +Get-ChildItem .\scripts\logs\sync-*.log | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | Get-Content ``` Log format: `sync-YYYYMMDD-HHMMSS.log` diff --git a/scripts/sync-awesome-copilot.ps1 b/scripts/sync-awesome-copilot.ps1 index 447775f..56d0615 100644 --- a/scripts/sync-awesome-copilot.ps1 +++ b/scripts/sync-awesome-copilot.ps1 @@ -50,10 +50,11 @@ function Check-Timeout { } } -# Prepare log -$RunId = (Get-Date -Format 'yyyyMMdd-HHmmss') -if (-not (Test-Path logs)) { New-Item -ItemType Directory -Path logs | Out-Null } -$Global:LogFile = Join-Path logs "sync-$RunId.log" +# Prepare log — always relative to this script's directory, regardless of CWD +$RunId = (Get-Date -Format 'yyyyMMdd-HHmmss') +$LogDir = Join-Path $PSScriptRoot 'logs' +if (-not (Test-Path $LogDir)) { New-Item -ItemType Directory -Path $LogDir | Out-Null } +$Global:LogFile = Join-Path $LogDir "sync-$RunId.log" Write-Log "Starting Awesome Copilot sync. Dest=$Dest Categories=$Categories" @@ -230,7 +231,7 @@ $Manifest | ConvertTo-Json -Depth 6 | Set-Content -Path $ManifestPath -Encoding Write-Log "Summary Added=$Added Updated=$Updated Removed=$Removed Unchanged=$Unchanged" 'SUCCESS' # Log retention -Get-ChildItem logs -Filter 'sync-*.log' | +Get-ChildItem $LogDir -Filter 'sync-*.log' | Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$LogRetentionDays) } | ForEach-Object { Remove-Item $_.FullName -Force } From ff75bb16e4a6b8b6a0b904764706683338cb6472 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:03:25 +0000 Subject: [PATCH 27/60] docs: add v1.2.1 changelog, simplify README install-task examples Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 12 ++++++++++++ README.md | 19 ++++++++++--------- 2 files changed, 22 insertions(+), 9 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 047a718..b13851b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,18 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.1] - 2026-02-27 + +### Fixed +- `configure.ps1`: `-InstallTask` / `-UninstallTask` now automatically skip the `init-repo` prompt (validation moved before Step 1 so the flag takes effect) +- `configure.ps1`: prompts to overwrite when the scheduled task already exists, instead of throwing a hard error +- `scripts/install-scheduled-task.ps1`: added `-WorkingDirectory` to both scheduled task actions (was defaulting to `C:\Windows\System32`, causing a permissions error creating the `logs/` directory) +- `scripts/sync-awesome-copilot.ps1`: replaced relative `logs/` path with `$PSScriptRoot/logs/` so logs always land in `scripts/logs/` regardless of working directory +- `scripts/install-scheduled-task.ps1`: updated task description (removed stale "combine" wording) +- `scripts/publish-global.ps1`: fixed named-profile example path (`agents\` → `prompts\`) +- `README.md`: corrected default interval (6h → 4h), log paths, authentication section, and custom-repo instructions +- `.github/copilot-instructions.md`: removed stale GitHub API / `GITHUB_TOKEN` references; updated cache structure + ## [1.2.0] - 2026-02-27 ### Changed diff --git a/README.md b/README.md index 62e0b46..b78efd2 100644 --- a/README.md +++ b/README.md @@ -48,9 +48,9 @@ For first-time setup or an on-demand refresh, `configure.ps1` chains all steps: .\configure.ps1 # Or step by step: -.\configure.ps1 -SkipInit # sync + publish only -.\configure.ps1 -SkipSync -SkipPublish -SkipInit -InstallTask # install scheduled task -.\configure.ps1 -DryRun # preview everything +.\configure.ps1 -SkipInit # sync + publish only +.\configure.ps1 -InstallTask # sync + publish + install scheduled task +.\configure.ps1 -DryRun # preview everything ``` ### 3. Initialise a Repo (optional, interactive) @@ -72,11 +72,11 @@ A selection UI will appear for each category (Out-GridView on Windows, or a numb ```powershell # Install a scheduled task that syncs + publishes globally every 4 hours -.\configure.ps1 -SkipSync -SkipPublish -SkipInit -InstallTask +.\configure.ps1 -InstallTask -# Customize the interval -.\configure.ps1 -SkipSync -SkipPublish -SkipInit -InstallTask -Every "2h" # Every 2 hours -.\configure.ps1 -SkipSync -SkipPublish -SkipInit -InstallTask -Every "30m" # Every 30 minutes +# Customize the interval (re-run to overwrite; configure.ps1 prompts before replacing) +.\configure.ps1 -InstallTask -Every "2h" # Every 2 hours +.\configure.ps1 -InstallTask -Every "30m" # Every 30 minutes # Or run it directly (called internally by configure.ps1) .\scripts\install-scheduled-task.ps1 -Every "2h" @@ -228,8 +228,9 @@ Creates a Windows scheduled task for automatic syncing and global publishing. Ca **Usage:** ```powershell -# Recommended: use configure.ps1 -.\configure.ps1 -SkipSync -SkipPublish -SkipInit -InstallTask +# Recommended: use configure.ps1 (prompts before overwriting an existing task) +.\configure.ps1 -InstallTask +.\configure.ps1 -InstallTask -Every "2h" # Or run directly .\scripts\install-scheduled-task.ps1 From b573091a743bfa173ddaf7b844825a72420b5a50 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:06:52 +0000 Subject: [PATCH 28/60] perf/fix: three optimisation pass fixes - configure.ps1: use \ (cleaner than \System.Management.Automation.InvocationInfo.MyCommand.Path) - sync: replace manual SHA256 with Get-FileHash (built-in, no file read into memory) - publish-global: WARN when VS Code settings.json not found instead of silent skip Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- configure.ps1 | 2 +- scripts/publish-global.ps1 | 5 ++++- scripts/sync-awesome-copilot.ps1 | 4 +--- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/configure.ps1 b/configure.ps1 index 5f41ed1..8d504bc 100644 --- a/configure.ps1 +++ b/configure.ps1 @@ -42,7 +42,7 @@ Usage: ) $ErrorActionPreference = 'Stop' -$ScriptDir = Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) 'scripts' +$ScriptDir = Join-Path $PSScriptRoot 'scripts' function Log($m, [string]$level = 'INFO') { $ts = (Get-Date).ToString('s') diff --git a/scripts/publish-global.ps1 b/scripts/publish-global.ps1 index cb45c8c..66ee78b 100644 --- a/scripts/publish-global.ps1 +++ b/scripts/publish-global.ps1 @@ -173,7 +173,10 @@ if (-not $SkipSkills) { # Ensure VS Code is configured to discover skills $vsCodeSettings = Join-Path $env:APPDATA 'Code\User\settings.json' - if (Test-Path $vsCodeSettings) { + if (-not (Test-Path $vsCodeSettings)) { + Log "VS Code settings.json not found at $vsCodeSettings — skills discovery not configured. Open VS Code once to generate it, then re-run." 'WARN' + } + else { try { $s = Get-Content $vsCodeSettings -Raw | ConvertFrom-Json $changed = $false diff --git a/scripts/sync-awesome-copilot.ps1 b/scripts/sync-awesome-copilot.ps1 index 56d0615..92358d8 100644 --- a/scripts/sync-awesome-copilot.ps1 +++ b/scripts/sync-awesome-copilot.ps1 @@ -100,9 +100,7 @@ if (Test-Path $ManifestPath) { function Get-Sha256 { param([string]$FilePath) - $sha256 = [System.Security.Cryptography.SHA256]::Create() - $hashBytes = $sha256.ComputeHash([System.IO.File]::ReadAllBytes($FilePath)) - return ($hashBytes | ForEach-Object { $_.ToString('x2') }) -join '' + return (Get-FileHash -LiteralPath $FilePath -Algorithm SHA256).Hash.ToLower() } # --------------------------------------------------------------------------- From 8e7a021ee80242e4c7bbdab8b38c216508e96103 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:10:52 +0000 Subject: [PATCH 29/60] docs: add v1.2.2 changelog, remove redundant -SkipInit from README example Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 7 +++++++ README.md | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b13851b..b344667 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,13 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.2] - 2026-02-27 + +### Changed +- `configure.ps1`: use `$PSScriptRoot` to locate the `scripts/` folder (replaces `$MyInvocation.MyCommand.Path` which behaves differently when dot-sourced) +- `scripts/sync-awesome-copilot.ps1`: replace manual SHA256 with built-in `Get-FileHash` — cleaner and avoids loading entire file into memory +- `scripts/publish-global.ps1`: emit a `WARN` log when VS Code `settings.json` is not found (was a silent no-op); user is directed to open VS Code once to generate the file + ## [1.2.1] - 2026-02-27 ### Fixed diff --git a/README.md b/README.md index b78efd2..20f3a82 100644 --- a/README.md +++ b/README.md @@ -129,7 +129,7 @@ Main entry point at the repo root. Chains sync → publish → init-repo in one .\configure.ps1 -DryRun # Install scheduled task -.\configure.ps1 -SkipSync -SkipPublish -SkipInit -InstallTask -Every "2h" +.\configure.ps1 -SkipSync -SkipPublish -InstallTask -Every "2h" ``` --- From 8319b98f3972ce15e7964b433c1520b75b4bb483 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:19:33 +0000 Subject: [PATCH 30/60] style: add #region/#endregion labels to all major scripts configure.ps1: Initialisation | Step 1-4 sync-awesome-copilot.ps1: Initialisation | Tool detection | Clone or pull | File scan and change detection | Write manifest and status | Log retention publish-global.ps1: Initialisation | Agents | Skills init-repo.ps1: Initialisation | Stack detection | Intent prompt | Path validation and stack detection | Helpers | Catalogue builders | Agents | Instructions | Hooks | Workflows | Summary Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- configure.ps1 | 27 +++++++----- scripts/init-repo.ps1 | 75 ++++++++++++++------------------ scripts/publish-global.ps1 | 15 ++++--- scripts/sync-awesome-copilot.ps1 | 31 +++++++------ 4 files changed, 74 insertions(+), 74 deletions(-) diff --git a/configure.ps1 b/configure.ps1 index 8d504bc..127ead6 100644 --- a/configure.ps1 +++ b/configure.ps1 @@ -41,6 +41,7 @@ Usage: [switch]$DryRun ) +#region Initialisation $ErrorActionPreference = 'Stop' $ScriptDir = Join-Path $PSScriptRoot 'scripts' @@ -73,9 +74,9 @@ if ($InstallTask -and $UninstallTask) { # Task management implies no interactive repo setup if ($InstallTask -or $UninstallTask) { $SkipInit = $true } -# --------------------------------------------------------------------------- -# STEP 1: Sync -# --------------------------------------------------------------------------- +#endregion # Initialisation + +#region Step 1 — Sync if (-not $SkipSync) { Step "Sync from github/awesome-copilot" $syncArgs = @{} @@ -84,9 +85,9 @@ if (-not $SkipSync) { if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { Log "Sync failed (exit $LASTEXITCODE)" 'ERROR'; exit $LASTEXITCODE } } -# --------------------------------------------------------------------------- -# STEP 2: Publish globally -# --------------------------------------------------------------------------- +#endregion # Step 1 + +#region Step 2 — Publish globally if (-not $SkipPublish) { Step "Publish agents + skills globally" $publishArgs = @{} @@ -94,9 +95,9 @@ if (-not $SkipPublish) { & (Join-Path $ScriptDir 'publish-global.ps1') @publishArgs } -# --------------------------------------------------------------------------- -# STEP 3: Init repo (prompted) -# --------------------------------------------------------------------------- +#endregion # Step 2 + +#region Step 3 — Init repo if (-not $SkipInit) { Step "Init repo" Write-Host " Add agents/instructions/hooks/workflows to .github/ in the current repo?" -ForegroundColor Yellow @@ -111,9 +112,9 @@ if (-not $SkipInit) { } } -# --------------------------------------------------------------------------- -# STEP 4: Scheduled task -# --------------------------------------------------------------------------- +#endregion # Step 3 + +#region Step 4 — Scheduled task if ($InstallTask) { Step "Install scheduled task" if ($DryRun) { @@ -145,5 +146,7 @@ if ($UninstallTask) { } } +#endregion # Step 4 + Write-Host "" Log "Done." 'SUCCESS' diff --git a/scripts/init-repo.ps1 b/scripts/init-repo.ps1 index f86a35b..9f3affa 100644 --- a/scripts/init-repo.ps1 +++ b/scripts/init-repo.ps1 @@ -54,6 +54,7 @@ Notes: [switch]$DryRun ) +#region Initialisation $ErrorActionPreference = 'Stop' function Log($m, [string]$level = 'INFO') { @@ -62,9 +63,9 @@ function Log($m, [string]$level = 'INFO') { Write-Host "[$ts][$level] $m" -ForegroundColor $color } -# --------------------------------------------------------------------------- -# Detect language/framework signals from repo files -# --------------------------------------------------------------------------- +#endregion # Initialisation + +#region Stack detection function Detect-RepoStack { param([string]$RepoPath) @@ -151,9 +152,9 @@ function Detect-RepoStack { return @($recs | Sort-Object -Unique) } -# --------------------------------------------------------------------------- -# Prompt for intent when no signals detected (new/empty repo) -# --------------------------------------------------------------------------- +#endregion # Stack detection + +#region Intent prompt function Prompt-RepoIntent { $recs = [System.Collections.Generic.List[string]]::new() @@ -223,9 +224,9 @@ function Prompt-RepoIntent { return @($recs | Sort-Object -Unique) } -# --------------------------------------------------------------------------- -# Validate paths -# --------------------------------------------------------------------------- +#endregion # Intent prompt + +#region Path validation and stack detection if (-not (Test-Path $RepoPath)) { Log "Repo path not found: $RepoPath" 'ERROR'; exit 1 } @@ -239,9 +240,7 @@ $GithubDir = Join-Path $RepoPath '.github' Log "Target repo : $RepoPath" Log "Copilot cache: $SourceRoot" -# --------------------------------------------------------------------------- # Auto-detect stack or prompt for intent -# --------------------------------------------------------------------------- $script:Recommendations = @() if (-not ($SkipInstructions -and $SkipHooks -and $SkipWorkflows -and $SkipAgents)) { $repoFileCount = (Get-ChildItem $RepoPath -Recurse -File -ErrorAction SilentlyContinue | @@ -263,11 +262,9 @@ if (-not ($SkipInstructions -and $SkipHooks -and $SkipWorkflows -and $SkipAgents } } -# --------------------------------------------------------------------------- -# Helper: interactive picker -# Returns array of selected names. -# preSelected: comma-separated list for non-interactive mode. -# --------------------------------------------------------------------------- +#endregion # Path validation and stack detection + +#region Helpers function Select-Items { param( [string]$Category, @@ -344,9 +341,6 @@ function Select-Items { return @($Items | Where-Object { $indices -contains ([Array]::IndexOf($Items, $_) + 1) }) } -# --------------------------------------------------------------------------- -# Helper: copy a single flat file to a target directory -# --------------------------------------------------------------------------- function Install-File { param([string]$Src, [string]$DestDir) if (-not $DryRun -and -not (Test-Path $DestDir)) { @@ -361,9 +355,6 @@ function Install-File { if ($dstHash) { return 'updated' } else { return 'added' } } -# --------------------------------------------------------------------------- -# Helper: copy an entire subdirectory (for hooks and skills) -# --------------------------------------------------------------------------- function Install-Directory { param([string]$SrcDir, [string]$DestParent) $name = Split-Path $SrcDir -Leaf @@ -389,9 +380,6 @@ function Install-Directory { return [pscustomobject]@{ Added = $added; Updated = $updated; Unchanged = $unchanged } } -# --------------------------------------------------------------------------- -# Helper: read description from a file's frontmatter or first heading -# --------------------------------------------------------------------------- function Get-Description([string]$FilePath) { try { $lines = Get-Content $FilePath -TotalCount 20 -ErrorAction SilentlyContinue @@ -409,9 +397,9 @@ function Get-Description([string]$FilePath) { return '' } -# --------------------------------------------------------------------------- -# Build catalogue entries for each category -# --------------------------------------------------------------------------- +#endregion # Helpers + +#region Catalogue builders $totalInstalled = 0 function Build-FlatCatalogue([string]$CatDir, [string]$DestDir, [string]$Pattern) { @@ -443,9 +431,9 @@ function Build-DirCatalogue([string]$CatDir, [string]$DestDir) { } | Sort-Object Name } -# --------------------------------------------------------------------------- -# AGENTS -# --------------------------------------------------------------------------- +#endregion # Catalogue builders + +#region Agents if (-not $SkipAgents) { $destDir = Join-Path $GithubDir 'agents' $catalogue = Build-FlatCatalogue (Join-Path $SourceRoot 'agents') $destDir '\.agent\.md$' @@ -459,9 +447,9 @@ if (-not $SkipAgents) { } } -# --------------------------------------------------------------------------- -# INSTRUCTIONS -# --------------------------------------------------------------------------- +#endregion # Agents + +#region Instructions if (-not $SkipInstructions) { $destDir = Join-Path $GithubDir 'instructions' $catalogue = Build-FlatCatalogue (Join-Path $SourceRoot 'instructions') $destDir '\.instructions\.md$' @@ -475,9 +463,9 @@ if (-not $SkipInstructions) { } } -# --------------------------------------------------------------------------- -# HOOKS -# --------------------------------------------------------------------------- +#endregion # Instructions + +#region Hooks if (-not $SkipHooks) { $destDir = Join-Path $GithubDir 'hooks' $catalogue = Build-DirCatalogue (Join-Path $SourceRoot 'hooks') $destDir @@ -491,9 +479,9 @@ if (-not $SkipHooks) { } } -# --------------------------------------------------------------------------- -# WORKFLOWS -# --------------------------------------------------------------------------- +#endregion # Hooks + +#region Workflows if (-not $SkipWorkflows) { $destDir = Join-Path $GithubDir 'workflows' $catalogue = Build-FlatCatalogue (Join-Path $SourceRoot 'workflows') $destDir '\.md$' @@ -507,9 +495,9 @@ if (-not $SkipWorkflows) { } } -# --------------------------------------------------------------------------- -# Summary -# --------------------------------------------------------------------------- +#endregion # Workflows + +#region Summary Write-Host "" if ($DryRun) { Log "Dry run complete. Re-run without -DryRun to apply." 'WARN' @@ -517,3 +505,4 @@ if ($DryRun) { Log "$totalInstalled resource(s) installed/updated in $GithubDir" 'SUCCESS' Log "Tip: commit .github/ to share Copilot resources with your team (agents, instructions, hooks, workflows)." } +#endregion # Summary diff --git a/scripts/publish-global.ps1 b/scripts/publish-global.ps1 index 66ee78b..346a893 100644 --- a/scripts/publish-global.ps1 +++ b/scripts/publish-global.ps1 @@ -45,6 +45,7 @@ Notes: [switch]$DryRun ) +#region Initialisation $ErrorActionPreference = 'Stop' function Log($m, [string]$level = 'INFO') { @@ -56,9 +57,9 @@ function Log($m, [string]$level = 'INFO') { $AgentsSource = Join-Path $SourceRoot 'agents' $SkillsSource = Join-Path $SourceRoot 'skills' -# --------------------------------------------------------------------------- -# AGENTS -# --------------------------------------------------------------------------- +#endregion # Initialisation + +#region Agents if (-not $SkipAgents) { if (-not (Test-Path $AgentsSource)) { Log "Agents source not found: $AgentsSource (run sync-awesome-copilot.ps1 first)" 'WARN' @@ -122,9 +123,9 @@ if (-not $SkipAgents) { } } -# --------------------------------------------------------------------------- -# SKILLS -# --------------------------------------------------------------------------- +#endregion # Agents + +#region Skills if (-not $SkipSkills) { if (-not (Test-Path $SkillsSource)) { Log "Skills source not found: $SkillsSource (run sync-awesome-copilot.ps1 first)" 'WARN' @@ -201,5 +202,7 @@ if (-not $SkipSkills) { } } +#endregion # Skills + if ($DryRun) { Log "[DryRun] No changes made." } else { Log "Global publish complete." } diff --git a/scripts/sync-awesome-copilot.ps1 b/scripts/sync-awesome-copilot.ps1 index 92358d8..9c1227a 100644 --- a/scripts/sync-awesome-copilot.ps1 +++ b/scripts/sync-awesome-copilot.ps1 @@ -30,6 +30,7 @@ Usage: [string]$GitTool = 'auto' ) +#region Initialisation $ErrorActionPreference = 'Stop' $script:StartTime = Get-Date @@ -58,9 +59,9 @@ $Global:LogFile = Join-Path $LogDir "sync-$RunId.log" Write-Log "Starting Awesome Copilot sync. Dest=$Dest Categories=$Categories" -# --------------------------------------------------------------------------- -# Tool detection — prefer gh (handles auth automatically), fall back to git -# --------------------------------------------------------------------------- +#endregion # Initialisation + +#region Tool detection function Resolve-GitTool { if ($GitTool -ne 'auto') { if (-not (Get-Command $GitTool -ErrorAction SilentlyContinue)) { @@ -103,9 +104,9 @@ function Get-Sha256 { return (Get-FileHash -LiteralPath $FilePath -Algorithm SHA256).Hash.ToLower() } -# --------------------------------------------------------------------------- -# Clone (first run) or pull (subsequent runs) -# --------------------------------------------------------------------------- +#endregion # Tool detection + +#region Clone or pull $IsFirstRun = -not (Test-Path (Join-Path $Dest '.git')) if ($Plan) { @@ -155,9 +156,9 @@ if ($IsFirstRun) { Check-Timeout -# --------------------------------------------------------------------------- -# Scan local files — compare against previous manifest for change counts -# --------------------------------------------------------------------------- +#endregion # Clone or pull + +#region File scan and change detection $NewItems = @() $Added = 0; $Updated = 0; $Unchanged = 0; $Removed = 0 $DestResolved = (Resolve-Path $Dest).Path @@ -201,9 +202,9 @@ if ($PrevManifest -and $PrevManifest.items) { } } -# --------------------------------------------------------------------------- -# Write manifest + status -# --------------------------------------------------------------------------- +#endregion # File scan + +#region Write manifest and status $Manifest = [pscustomobject]@{ version = 1 repo = $RepoSlug @@ -228,9 +229,13 @@ $Manifest | ConvertTo-Json -Depth 6 | Set-Content -Path $ManifestPath -Encoding Write-Log "Summary Added=$Added Updated=$Updated Removed=$Removed Unchanged=$Unchanged" 'SUCCESS' -# Log retention +#endregion # Write manifest + +#region Log retention Get-ChildItem $LogDir -Filter 'sync-*.log' | Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$LogRetentionDays) } | ForEach-Object { Remove-Item $_.FullName -Force } +#endregion # Log retention + exit 0 From 95de9de629da61c77b4408456e0f392e275eff36 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 27 Feb 2026 16:22:37 +0000 Subject: [PATCH 31/60] fix: final review pass - stale references and path fixes - CONTRIBUTING.md: replace removed scripts with current configure.ps1 workflow, fix -Interval -> -Every, fix log path (C:\Users\Chris.Tout\.awesome-copilot\logs -> scripts\logs) - CHANGELOG.md: fix update.ps1 -> configure.ps1 in v1.1.2 entry - README.md: fix 'cd scripts' -> 'cd vscode-copilot-sync' in Quick Start - install-scheduled-task.ps1: use \ in param defaults (replaces \System.Management.Automation.InvocationInfo.MyCommand.Path, consistent with other scripts) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 2 +- CONTRIBUTING.md | 17 ++++++++++------- README.md | 2 +- scripts/install-scheduled-task.ps1 | 4 ++-- 4 files changed, 14 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b344667..2957b80 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -40,7 +40,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.1.2] - 2026-02-27 ### Added -- `update.ps1` — interactive orchestrator that chains sync → publish-global → init-repo; each step independently skippable via `-SkipSync`, `-SkipPublish`, `-SkipInit`; `-DryRun` passes through to all child scripts; shows last sync timestamp from cache manifest before running +- `configure.ps1` — interactive orchestrator that chains sync → publish-global → init-repo; each step independently skippable via `-SkipSync`, `-SkipPublish`, `-SkipInit`; `-DryRun` passes through to all child scripts; shows last sync timestamp from cache manifest before running ### Added - `init-repo.ps1`: added Agents as a fourth interactive category (installs to `.github/agents/`) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c7fd7a4..c72ae04 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -12,7 +12,7 @@ If you find a bug, please create an issue with: - Expected vs actual behavior - PowerShell version (`$PSVersionTable.PSVersion`) - Windows version -- Relevant log files from `$HOME\.awesome-copilot\logs\` +- Relevant log files from `scripts\logs\` ### Suggesting Enhancements @@ -42,16 +42,19 @@ Feature requests are welcome! Please include: 5. **Test your changes**: ```powershell + # Full dry run (no files written) + .\configure.ps1 -DryRun + # Test individual scripts - .\sync-awesome-copilot.ps1 - .\combine-and-publish-prompts.ps1 - + .\scripts\sync-awesome-copilot.ps1 -Plan + .\scripts\publish-global.ps1 -DryRun + # Test scheduled task installation - .\install-scheduled-task.ps1 -Interval "1h" + .\configure.ps1 -InstallTask -Every "1h" Start-ScheduledTask -TaskName "AwesomeCopilotSync" - + # Verify logs - Get-Content "$HOME\.awesome-copilot\logs\sync-*.log" -Tail 20 + Get-ChildItem .\scripts\logs\sync-*.log | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | Get-Content -Tail 20 ``` 6. **Commit with clear messages**: diff --git a/README.md b/README.md index 20f3a82..a719929 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ These scripts automate the management of VS Code Copilot custom agents, instruct ```powershell # Clone this repository git clone -cd scripts +cd vscode-copilot-sync ``` ### 2. Run the Configurator diff --git a/scripts/install-scheduled-task.ps1 b/scripts/install-scheduled-task.ps1 index 4e48c2e..48680d5 100644 --- a/scripts/install-scheduled-task.ps1 +++ b/scripts/install-scheduled-task.ps1 @@ -8,8 +8,8 @@ # Allow skipping the global publish step if user only wants raw sync [switch]$SkipPublishGlobal, [string]$PwshPath = (Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source), - [string]$ScriptPath = (Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) 'sync-awesome-copilot.ps1'), - [string]$PublishGlobalScriptPath = (Join-Path (Split-Path -Parent $MyInvocation.MyCommand.Path) 'publish-global.ps1'), + [string]$ScriptPath = (Join-Path $PSScriptRoot 'sync-awesome-copilot.ps1'), + [string]$PublishGlobalScriptPath = (Join-Path $PSScriptRoot 'publish-global.ps1'), [switch]$Force ) From 5cef34e6d846232e77bf9d754a22c8f0a17e0ed3 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Mon, 2 Mar 2026 13:11:31 +0000 Subject: [PATCH 32/60] fix: recover from unrelated histories on git pull When the remote history has diverged (e.g. force-push), fall back to git fetch + reset --hard origin/HEAD instead of failing with exit 128. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/sync-awesome-copilot.ps1 | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/scripts/sync-awesome-copilot.ps1 b/scripts/sync-awesome-copilot.ps1 index 9c1227a..8695e51 100644 --- a/scripts/sync-awesome-copilot.ps1 +++ b/scripts/sync-awesome-copilot.ps1 @@ -150,8 +150,18 @@ if ($IsFirstRun) { # Re-apply sparse-checkout in case -Categories changed since last run & git -C $Dest sparse-checkout set @CategoriesList 2>&1 | Out-Null - & git -C $Dest pull 2>&1 | ForEach-Object { Write-Log $_ } - if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { Write-Log "Pull failed (exit $LASTEXITCODE)" 'ERROR'; exit $LASTEXITCODE } + $pullOutput = & git -C $Dest pull 2>&1 + $pullOutput | ForEach-Object { Write-Log $_ } + if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { + if (($pullOutput -join "`n") -match 'unrelated histories') { + Write-Log "Unrelated histories detected — fetching and resetting to remote HEAD..." 'WARN' + & git -C $Dest fetch origin 2>&1 | ForEach-Object { Write-Log $_ } + & git -C $Dest reset --hard origin/HEAD 2>&1 | ForEach-Object { Write-Log $_ } + if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { Write-Log "Reset failed (exit $LASTEXITCODE)" 'ERROR'; exit $LASTEXITCODE } + } else { + Write-Log "Pull failed (exit $LASTEXITCODE)" 'ERROR'; exit $LASTEXITCODE + } + } } Check-Timeout From 253a332f9950354bec35a99d92299d34c6059cf0 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:21:38 +0000 Subject: [PATCH 33/60] feat: add per-repo skills support and subscription change tracking - init-repo.ps1: add Skills category (installs to .github/skills/) - init-repo.ps1: record all installed resources in .github/.copilot-subscriptions.json - init-repo.ps1: add Get-DirHash helper (combined SHA256 across all files in a directory) - init-repo.ps1: add Update-Subscriptions helper (upserts entries into manifest) - init-repo.ps1: add -Skills / -SkipSkills params - update-repo.ps1: new script to check subscribed resources against upstream cache and apply updates, refreshing hashAtInstall in the manifest - configure.ps1: offer update-repo check when subscriptions manifest exists - configure.ps1: update init prompt text to include skills Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- configure.ps1 | 18 +++- scripts/init-repo.ps1 | 134 +++++++++++++++++++++++++-- scripts/update-repo.ps1 | 196 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 341 insertions(+), 7 deletions(-) create mode 100644 scripts/update-repo.ps1 diff --git a/configure.ps1 b/configure.ps1 index 127ead6..0c89855 100644 --- a/configure.ps1 +++ b/configure.ps1 @@ -99,8 +99,24 @@ if (-not $SkipPublish) { #region Step 3 — Init repo if (-not $SkipInit) { + # If a subscriptions manifest exists for the current repo, offer to check for updates first + $subscriptionsFile = Join-Path (Get-Location).Path '.github\.copilot-subscriptions.json' + if (Test-Path $subscriptionsFile) { + Step "Check for updates to subscribed repo resources" + Write-Host " Subscriptions found. Check for upstream updates to .github/ resources?" -ForegroundColor Yellow + Write-Host " [Y] Yes [N] No (default): " -NoNewline -ForegroundColor Yellow + $updateAnswer = (Read-Host).Trim() + if ($updateAnswer -match '^[Yy]') { + $updateArgs = @{} + if ($DryRun) { $updateArgs['DryRun'] = $true } + & (Join-Path $ScriptDir 'update-repo.ps1') @updateArgs + } else { + Log "Update check skipped." + } + } + Step "Init repo" - Write-Host " Add agents/instructions/hooks/workflows to .github/ in the current repo?" -ForegroundColor Yellow + Write-Host " Add agents/instructions/hooks/workflows/skills to .github/ in the current repo?" -ForegroundColor Yellow Write-Host " [Y] Yes [N] No (default): " -NoNewline -ForegroundColor Yellow $answer = (Read-Host).Trim() if ($answer -match '^[Yy]') { diff --git a/scripts/init-repo.ps1 b/scripts/init-repo.ps1 index 9f3affa..7e1fd95 100644 --- a/scripts/init-repo.ps1 +++ b/scripts/init-repo.ps1 @@ -10,6 +10,7 @@ Resources installed here are project-specific (opt-in) rather than global: Instructions --> .github/instructions/*.instructions.md Hooks --> .github/hooks// (full directory) Workflows --> .github/workflows/*.md + Skills --> .github/skills// (full directory) Usage: # Interactive - run from within the target repo @@ -20,20 +21,22 @@ Usage: # Skip specific categories .\init-repo.ps1 -SkipAgents -SkipHooks - .\init-repo.ps1 -SkipHooks -SkipWorkflows + .\init-repo.ps1 -SkipHooks -SkipWorkflows -SkipSkills # Non-interactive: specify items by name (comma-separated) .\init-repo.ps1 -Instructions "angular,dotnet-framework" -Hooks "session-logger" .\init-repo.ps1 -Agents "devops-expert,se-security-reviewer" + .\init-repo.ps1 -Skills "my-custom-skill" # Dry run - show what would be installed .\init-repo.ps1 -DryRun Notes: - Existing files are only overwritten if the source is newer/different. - .github/ is created if it doesn't exist. - - This script does NOT touch global resources (agents, skills). - Use publish-global.ps1 for those. For skills, point users directly - at https://github.com/github/awesome-copilot. + - Skills are installed to .github/skills/ for version control with the project. + Global skills (available across all repos) are managed by publish-global.ps1. + - A subscription manifest (.github/.copilot-subscriptions.json) is written on + each run. Use update-repo.ps1 to check for and apply upstream changes. - The selection UI uses Out-GridView where available (Windows GUI, filterable, multi-select). Falls back to a numbered console menu automatically. - Auto-detects language/framework from repo file signals and pre-marks @@ -47,10 +50,12 @@ Notes: [string]$Agents = '', [string]$Hooks = '', [string]$Workflows = '', + [string]$Skills = '', # Comma-separated names to pre-select (non-interactive) [switch]$SkipInstructions, [switch]$SkipAgents, [switch]$SkipHooks, [switch]$SkipWorkflows, + [switch]$SkipSkills, [switch]$DryRun ) @@ -242,7 +247,7 @@ Log "Copilot cache: $SourceRoot" # Auto-detect stack or prompt for intent $script:Recommendations = @() -if (-not ($SkipInstructions -and $SkipHooks -and $SkipWorkflows -and $SkipAgents)) { +if (-not ($SkipInstructions -and $SkipHooks -and $SkipWorkflows -and $SkipAgents -and $SkipSkills)) { $repoFileCount = (Get-ChildItem $RepoPath -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.FullName -notmatch '\\(\.git|node_modules|\.venv|bin|obj)\\' } | Measure-Object).Count @@ -397,6 +402,54 @@ function Get-Description([string]$FilePath) { return '' } +function Get-DirHash([string]$DirPath) { + $hashes = Get-ChildItem $DirPath -Recurse -File | + Sort-Object FullName | + ForEach-Object { (Get-FileHash $_.FullName -Algorithm SHA256).Hash } + $combined = $hashes -join '|' + $bytes = [System.Text.Encoding]::UTF8.GetBytes($combined) + $stream = [System.IO.MemoryStream]::new($bytes) + return (Get-FileHash -InputStream $stream -Algorithm SHA256).Hash +} + +function Update-Subscriptions { + param([string]$ManifestPath, [object[]]$NewEntries) + if (-not $NewEntries -or $NewEntries.Count -eq 0) { return } + + $subs = $null + if (Test-Path $ManifestPath) { + try { $subs = Get-Content $ManifestPath -Raw | ConvertFrom-Json } catch {} + } + if (-not $subs) { + $subs = [pscustomobject]@{ version = 1; updatedAt = ''; subscriptions = @() } + } + + $existing = [System.Collections.Generic.List[object]]::new() + if ($subs.subscriptions) { $existing.AddRange([object[]]$subs.subscriptions) } + + foreach ($entry in $NewEntries) { + $idx = -1 + for ($i = 0; $i -lt $existing.Count; $i++) { + if ($existing[$i].name -eq $entry.name -and $existing[$i].category -eq $entry.category) { + $idx = $i; break + } + } + if ($idx -ge 0) { $existing[$idx] = $entry } else { $existing.Add($entry) } + } + + $subs | Add-Member -NotePropertyName 'updatedAt' -NotePropertyValue (Get-Date).ToString('o') -Force + $subs | Add-Member -NotePropertyName 'subscriptions' -NotePropertyValue $existing.ToArray() -Force + + if (-not $DryRun) { + $dir = Split-Path $ManifestPath -Parent + if (-not (Test-Path $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } + $subs | ConvertTo-Json -Depth 5 | Set-Content $ManifestPath -Encoding UTF8 + Log "Updated subscriptions: $ManifestPath" + } else { + Log "[DryRun] Would update subscriptions: $ManifestPath ($($NewEntries.Count) new/updated entries)" + } +} + #endregion # Helpers #region Catalogue builders @@ -434,6 +487,9 @@ function Build-DirCatalogue([string]$CatDir, [string]$DestDir) { #endregion # Catalogue builders #region Agents +$script:SubscriptionEntries = [System.Collections.Generic.List[object]]::new() +$SubscriptionManifestPath = Join-Path $GithubDir '.copilot-subscriptions.json' + if (-not $SkipAgents) { $destDir = Join-Path $GithubDir 'agents' $catalogue = Build-FlatCatalogue (Join-Path $SourceRoot 'agents') $destDir '\.agent\.md$' @@ -444,6 +500,15 @@ if (-not $SkipAgents) { $verb = switch ($result) { 'added' { '✓ Added' } 'updated' { '↑ Updated' } 'unchanged' { '= Unchanged' } default { '~ DryRun' } } Log "$verb agent: $($item.FileName)" if ($result -in 'added','updated','would-copy') { $totalInstalled++ } + $script:SubscriptionEntries.Add([pscustomobject]@{ + name = $item.Name + category = 'agents' + type = 'file' + fileName = $item.FileName + sourceRelPath = "agents/$($item.FileName)" + hashAtInstall = (Get-FileHash $item.FullPath -Algorithm SHA256).Hash + installedAt = (Get-Date).ToString('o') + }) } } @@ -460,6 +525,15 @@ if (-not $SkipInstructions) { $verb = switch ($result) { 'added' { '✓ Added' } 'updated' { '↑ Updated' } 'unchanged' { '= Unchanged' } default { '~ DryRun' } } Log "$verb instructions: $($item.FileName)" if ($result -in 'added','updated','would-copy') { $totalInstalled++ } + $script:SubscriptionEntries.Add([pscustomobject]@{ + name = $item.Name + category = 'instructions' + type = 'file' + fileName = $item.FileName + sourceRelPath = "instructions/$($item.FileName)" + hashAtInstall = (Get-FileHash $item.FullPath -Algorithm SHA256).Hash + installedAt = (Get-Date).ToString('o') + }) } } @@ -476,6 +550,15 @@ if (-not $SkipHooks) { $verb = if ($DryRun) { '~ DryRun' } else { '✓ Installed' } Log "$verb hook: $($item.Name) (added=$($r.Added) updated=$($r.Updated) unchanged=$($r.Unchanged))" if (-not $DryRun) { $totalInstalled++ } + $script:SubscriptionEntries.Add([pscustomobject]@{ + name = $item.Name + category = 'hooks' + type = 'directory' + dirName = $item.Name + sourceRelPath = "hooks/$($item.Name)" + hashAtInstall = Get-DirHash $item.FullPath + installedAt = (Get-Date).ToString('o') + }) } } @@ -492,17 +575,56 @@ if (-not $SkipWorkflows) { $verb = switch ($result) { 'added' { '✓ Added' } 'updated' { '↑ Updated' } 'unchanged' { '= Unchanged' } default { '~ DryRun' } } Log "$verb workflow: $($item.FileName)" if ($result -in 'added','updated','would-copy') { $totalInstalled++ } + $script:SubscriptionEntries.Add([pscustomobject]@{ + name = $item.Name + category = 'workflows' + type = 'file' + fileName = $item.FileName + sourceRelPath = "workflows/$($item.FileName)" + hashAtInstall = (Get-FileHash $item.FullPath -Algorithm SHA256).Hash + installedAt = (Get-Date).ToString('o') + }) } } #endregion # Workflows +#region Skills +if (-not $SkipSkills) { + $destDir = Join-Path $GithubDir 'skills' + $catalogue = Build-DirCatalogue (Join-Path $SourceRoot 'skills') $destDir + $selected = Select-Items -Category 'Skills' -Items $catalogue -PreSelected $Skills -Recommended $script:Recommendations + + foreach ($item in $selected) { + $r = Install-Directory -SrcDir $item.FullPath -DestParent $destDir + $verb = if ($DryRun) { '~ DryRun' } else { '✓ Installed' } + Log "$verb skill: $($item.Name) (added=$($r.Added) updated=$($r.Updated) unchanged=$($r.Unchanged))" + if (-not $DryRun) { $totalInstalled++ } + $script:SubscriptionEntries.Add([pscustomobject]@{ + name = $item.Name + category = 'skills' + type = 'directory' + dirName = $item.Name + sourceRelPath = "skills/$($item.Name)" + hashAtInstall = Get-DirHash $item.FullPath + installedAt = (Get-Date).ToString('o') + }) + } +} + +#endregion # Skills + #region Summary +if ($script:SubscriptionEntries.Count -gt 0) { + Update-Subscriptions -ManifestPath $SubscriptionManifestPath -NewEntries $script:SubscriptionEntries.ToArray() +} + Write-Host "" if ($DryRun) { Log "Dry run complete. Re-run without -DryRun to apply." 'WARN' } else { Log "$totalInstalled resource(s) installed/updated in $GithubDir" 'SUCCESS' - Log "Tip: commit .github/ to share Copilot resources with your team (agents, instructions, hooks, workflows)." + Log "Tip: commit .github/ to share Copilot resources with your team (agents, instructions, hooks, workflows, skills)." + Log "Tip: run update-repo.ps1 to check for and apply upstream changes to your subscribed resources." } #endregion # Summary diff --git a/scripts/update-repo.ps1 b/scripts/update-repo.ps1 new file mode 100644 index 0000000..3f926ed --- /dev/null +++ b/scripts/update-repo.ps1 @@ -0,0 +1,196 @@ +<# +Update Subscribed Per-Repo Copilot Resources + +Reads the subscription manifest (.github/.copilot-subscriptions.json) written by +init-repo.ps1, compares each subscribed resource against the local awesome-copilot +cache, and applies any upstream updates. + +Resources are updated in place — the same files/directories that were originally +installed to .github/ are refreshed from the cache. + +Usage: + # Check for and apply updates (interactive prompt) + .\update-repo.ps1 + + # Dry run — show what would be updated without writing any files + .\update-repo.ps1 -DryRun + + # Apply all updates without prompting + .\update-repo.ps1 -Force + + # Check a specific repo + .\update-repo.ps1 -RepoPath "C:\Projects\my-app" + +Notes: + - Only resources present in .github/.copilot-subscriptions.json are checked. + Run init-repo.ps1 to add new subscriptions. + - Resources whose destination file/directory has been manually deleted are + skipped (treated as intentionally removed). + - The subscription manifest is updated with new hashes after each successful update. + - Requires the local awesome-copilot cache (~/.awesome-copilot/). Run + sync-awesome-copilot.ps1 first if the cache is stale. +#> +[CmdletBinding()] param( + [string]$RepoPath = (Get-Location).Path, + [string]$SourceRoot = "$HOME/.awesome-copilot", + [switch]$Force, + [switch]$DryRun +) + +#region Initialisation +$ErrorActionPreference = 'Stop' + +function Log($m, [string]$level = 'INFO') { + $ts = (Get-Date).ToString('s') + $color = switch ($level) { 'ERROR' { 'Red' } 'WARN' { 'Yellow' } 'SUCCESS' { 'Green' } default { 'Cyan' } } + Write-Host "[$ts][$level] $m" -ForegroundColor $color +} + +function Get-DirHash([string]$DirPath) { + $hashes = Get-ChildItem $DirPath -Recurse -File | + Sort-Object FullName | + ForEach-Object { (Get-FileHash $_.FullName -Algorithm SHA256).Hash } + $combined = $hashes -join '|' + $bytes = [System.Text.Encoding]::UTF8.GetBytes($combined) + $stream = [System.IO.MemoryStream]::new($bytes) + return (Get-FileHash -InputStream $stream -Algorithm SHA256).Hash +} + +#endregion # Initialisation + +#region Load subscriptions +if (-not (Test-Path $RepoPath)) { + Log "Repo path not found: $RepoPath" 'ERROR'; exit 1 +} +$RepoPath = Resolve-Path $RepoPath | Select-Object -ExpandProperty Path + +$GithubDir = Join-Path $RepoPath '.github' +$ManifestPath = Join-Path $GithubDir '.copilot-subscriptions.json' + +if (-not (Test-Path $ManifestPath)) { + Log "No subscriptions manifest found: $ManifestPath" 'WARN' + Log "Run init-repo.ps1 first to subscribe to resources." + exit 0 +} + +$manifest = Get-Content $ManifestPath -Raw | ConvertFrom-Json +$subs = @($manifest.subscriptions) + +if (-not $subs -or $subs.Count -eq 0) { + Log "No subscriptions recorded in manifest." 'WARN' + exit 0 +} + +if (-not (Test-Path $SourceRoot)) { + Log "Cache not found: $SourceRoot -- run sync-awesome-copilot.ps1 first" 'ERROR'; exit 1 +} + +Log "Checking $($subs.Count) subscribed resource(s) for upstream changes..." +Log "Cache : $SourceRoot" +Log "Repo : $RepoPath" + +#endregion # Load subscriptions + +#region Check for stale resources +$stale = [System.Collections.Generic.List[object]]::new() + +foreach ($sub in $subs) { + $sourcePath = Join-Path $SourceRoot $sub.sourceRelPath + + if (-not (Test-Path $sourcePath)) { + Log "= Skipping $($sub.name) ($($sub.category)) — no longer in cache." 'WARN' + continue + } + + # Destination: .github/ e.g. .github/agents/foo.agent.md + $destPath = Join-Path $GithubDir $sub.sourceRelPath + + if (-not (Test-Path $destPath)) { + Log "= Skipping $($sub.name) ($($sub.category)) — destination removed locally." + continue + } + + $currentHash = if ($sub.type -eq 'file') { + (Get-FileHash $sourcePath -Algorithm SHA256).Hash + } else { + Get-DirHash $sourcePath + } + + if ($currentHash -ne $sub.hashAtInstall) { + $stale.Add([pscustomobject]@{ + Sub = $sub + SourcePath = $sourcePath + DestPath = $destPath + CurrentHash = $currentHash + }) + Log "↑ Stale : $($sub.name) ($($sub.category))" + } else { + Log "= Current: $($sub.name) ($($sub.category))" + } +} + +#endregion # Check for stale resources + +#region Apply updates +if ($stale.Count -eq 0) { + Write-Host "" + Log "All $($subs.Count) subscribed resource(s) are up to date." 'SUCCESS' + exit 0 +} + +Write-Host "" +Log "$($stale.Count) resource(s) have upstream updates available." 'WARN' + +if ($DryRun) { + Log "[DryRun] Re-run without -DryRun to apply updates." 'WARN' + exit 0 +} + +if (-not $Force) { + Write-Host "" + Write-Host " Apply all $($stale.Count) update(s)? [Y] Yes [N] No (default): " -NoNewline -ForegroundColor Yellow + $answer = (Read-Host).Trim() + if ($answer -notmatch '^[Yy]') { + Log "Update skipped." + exit 0 + } +} + +$updated = 0 +foreach ($item in $stale) { + $sub = $item.Sub + try { + if ($sub.type -eq 'file') { + $destDir = Split-Path $item.DestPath -Parent + if (-not (Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null } + Copy-Item $item.SourcePath $item.DestPath -Force + } else { + # Mirror all files from the source directory into the destination + Get-ChildItem $item.SourcePath -File -Recurse | ForEach-Object { + $rel = $_.FullName.Substring($item.SourcePath.Length).TrimStart('\', '/') + $dest = Join-Path $item.DestPath $rel + $destDir = Split-Path $dest -Parent + if (-not (Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null } + Copy-Item $_.FullName $dest -Force + } + } + + $sub | Add-Member -NotePropertyName 'hashAtInstall' -NotePropertyValue $item.CurrentHash -Force + $sub | Add-Member -NotePropertyName 'installedAt' -NotePropertyValue (Get-Date).ToString('o') -Force + Log "✓ Updated: $($sub.name) ($($sub.category))" + $updated++ + } catch { + Log "Failed to update $($sub.name): $_" 'ERROR' + } +} + +# Persist updated hashes back to the manifest +$manifest | Add-Member -NotePropertyName 'updatedAt' -NotePropertyValue (Get-Date).ToString('o') -Force +$manifest | Add-Member -NotePropertyName 'subscriptions' -NotePropertyValue $subs -Force +$manifest | ConvertTo-Json -Depth 5 | Set-Content $ManifestPath -Encoding UTF8 + +Write-Host "" +Log "$updated resource(s) updated in $GithubDir" 'SUCCESS' +Log "Tip: commit .github/ to share the updates with your team." + +#endregion # Apply updates From 989f7a69c8b04f915e6cc15b0c4fab1e7ce95842 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:19:35 +0000 Subject: [PATCH 34/60] feat: add -RepoPath parameter to configure.ps1 Allows pointing configure.ps1 at a specific repo directory for the init step without having to cd first. The parameter defaults to the current directory so existing usage is unchanged. - Add -RepoPath parameter to configure.ps1 (defaults to cwd) - Thread -RepoPath through to init-repo.ps1 and update-repo.ps1 calls - Use RepoPath for subscriptions file lookup instead of Get-Location - Update README with -RepoPath usage examples and feature bullet Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CONTRIBUTING.md | 14 ++- README.md | 47 +++++++-- configure.ps1 | 8 +- scripts/init-repo.ps1 | 164 +++++++++++++++++++------------ scripts/sync-awesome-copilot.ps1 | 10 +- 5 files changed, 167 insertions(+), 76 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c72ae04..44061eb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -7,6 +7,7 @@ Thank you for considering contributing to the VS Code Copilot Resource Sync Scri ### Reporting Bugs If you find a bug, please create an issue with: + - Clear description of the problem - Steps to reproduce - Expected vs actual behavior @@ -17,6 +18,7 @@ If you find a bug, please create an issue with: ### Suggesting Enhancements Feature requests are welcome! Please include: + - Clear use case description - Why this feature would be useful - Proposed implementation (if you have ideas) @@ -25,6 +27,7 @@ Feature requests are welcome! Please include: 1. **Fork the repository** 2. **Create a feature branch** from `main`: + ```powershell git checkout -b feature/your-feature-name ``` @@ -41,6 +44,7 @@ Feature requests are welcome! Please include: - Add inline comments for complex code 5. **Test your changes**: + ```powershell # Full dry run (no files written) .\configure.ps1 -DryRun @@ -58,6 +62,7 @@ Feature requests are welcome! Please include: ``` 6. **Commit with clear messages**: + ```powershell git commit -m "Add feature: description of what you added" ``` @@ -85,21 +90,21 @@ function Get-ResourceFiles { <# .SYNOPSIS Retrieves resource files from a directory. - + .PARAMETER Path The directory path to search. - + .PARAMETER Type The type of resource to filter (chatmode, instruction, prompt). #> param( [Parameter(Mandatory)] [string]$Path, - + [ValidateSet('chatmode', 'instruction', 'prompt')] [string]$Type ) - + try { $pattern = "*.$Type.md" Get-ChildItem -Path $Path -Filter $pattern -File @@ -148,6 +153,7 @@ Before submitting a PR, verify: ## Questions? Feel free to: + - Open an issue for discussion - Ask questions in pull request comments - Reach out to maintainers diff --git a/README.md b/README.md index a719929..b2f6cfc 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,13 @@ These scripts automate the management of VS Code Copilot custom agents, instruct ### What goes where -| Resource | Scope | Location | -|---|---|---| -| **Agents** | 🌐 Global | `%APPDATA%\Code\User\prompts\` — available in Copilot Chat across all workspaces | -| **Skills** | 🌐 Global | `~/.copilot/skills/` — loaded on-demand by Copilot coding agent & CLI | -| **Instructions** | 📁 Per-repo | `.github/instructions/` — chosen via `scripts/init-repo.ps1` | -| **Hooks** | 📁 Per-repo | `.github/hooks//` — chosen via `scripts/init-repo.ps1` | -| **Workflows** | 📁 Per-repo | `.github/workflows/` — chosen via `scripts/init-repo.ps1` | +| Resource | Scope | Location | +| ---------------- | ----------- | -------------------------------------------------------------------------------- | +| **Agents** | 🌐 Global | `%APPDATA%\Code\User\prompts\` — available in Copilot Chat across all workspaces | +| **Skills** | 🌐 Global | `~/.copilot/skills/` — loaded on-demand by Copilot coding agent & CLI | +| **Instructions** | 📁 Per-repo | `.github/instructions/` — chosen via `scripts/init-repo.ps1` | +| **Hooks** | 📁 Per-repo | `.github/hooks//` — chosen via `scripts/init-repo.ps1` | +| **Workflows** | 📁 Per-repo | `.github/workflows/` — chosen via `scripts/init-repo.ps1` | ## 📋 Prerequisites @@ -62,7 +62,10 @@ For first-time setup or an on-demand refresh, `configure.ps1` chains all steps: cd C:\Projects\my-app .\scripts\init-repo.ps1 -# Or specify the path explicitly +# Or point configure.ps1 at a specific repo without cd-ing first +.\configure.ps1 -SkipSync -SkipPublish -RepoPath "C:\Projects\my-app" + +# Or target the script directly with a path .\scripts\init-repo.ps1 -RepoPath "C:\Projects\my-app" ``` @@ -104,17 +107,21 @@ $HOME\.awesome-copilot\ # Local cache (git sparse clone) ## 📜 Scripts Overview ### `configure.ps1` + Main entry point at the repo root. Chains sync → publish → init-repo in one command, and can install/uninstall the scheduled task via `-InstallTask` / `-UninstallTask` / `-Every` switches. **Features:** + - Shows last sync time from the local cache manifest before running - Runs each step in sequence; any step can be skipped independently - Prompts before running `scripts/init-repo.ps1` (with option to skip via `-SkipInit`) - `-DryRun` passes through to all child scripts +- `-RepoPath` targets a specific repo directory for the init step (defaults to current directory) - `-InstallTask` / `-UninstallTask` delegate to `scripts/install-scheduled-task.ps1` / `scripts/uninstall-scheduled-task.ps1` - `-Every` sets the scheduled task interval (e.g. `"2h"`, `"30m"`) **Usage:** + ```powershell # Full update: sync + publish + prompt for init-repo .\configure.ps1 @@ -128,6 +135,9 @@ Main entry point at the repo root. Chains sync → publish → init-repo in one # Preview without writing any files .\configure.ps1 -DryRun +# Init a specific repo without cd-ing to it +.\configure.ps1 -SkipSync -SkipPublish -RepoPath "C:\Projects\my-app" + # Install scheduled task .\configure.ps1 -SkipSync -SkipPublish -InstallTask -Every "2h" ``` @@ -135,9 +145,11 @@ Main entry point at the repo root. Chains sync → publish → init-repo in one --- ### `scripts/sync-awesome-copilot.ps1` + Syncs resources from the awesome-copilot GitHub repository using a sparse git clone. **Features:** + - Clones `github/awesome-copilot` with sparse checkout (first run) — only downloads the categories you need - Pulls updates on subsequent runs — git transfers only the diff, making updates near-instant - SHA256 hash-based change detection against previous manifest (added/updated/unchanged/removed counts) @@ -145,6 +157,7 @@ Syncs resources from the awesome-copilot GitHub repository using a sparse git cl - Automatically migrates from the old API-based cache if detected **Usage:** + ```powershell .\scripts\sync-awesome-copilot.ps1 @@ -164,15 +177,18 @@ Add `plugins` or `cookbook` explicitly via `-Categories` for those larger opt-in --- ### `scripts/publish-global.ps1` + Publishes agents globally to VS Code and skills to `~/.copilot/skills/`. **Features:** + - Creates a junction/symlink from VS Code's user agents folder to the local cache (no re-running needed after each sync) - Incrementally copies skills to `~/.copilot/skills/` - Dry-run mode for previewing changes - Individual skip flags for each resource type **Usage:** + ```powershell .\scripts\publish-global.ps1 @@ -189,9 +205,11 @@ Publishes agents globally to VS Code and skills to `~/.copilot/skills/`. --- ### `scripts/init-repo.ps1` + Interactively initialises a repository with agents, instructions, hooks, and agentic workflows. **Features:** + - Auto-detects language/framework from repo file signals and pre-marks recommendations with ★ in the picker - Prompts for intent (language, project type, concerns) for new/empty repos - Presents available resources in a selection UI (Out-GridView on Windows, with `-- none / skip --` row to prevent accidental installs) @@ -201,6 +219,7 @@ Interactively initialises a repository with agents, instructions, hooks, and age - Dry-run mode for previewing **Usage:** + ```powershell # Run inside a repo (uses current directory) .\scripts\init-repo.ps1 @@ -219,14 +238,17 @@ Interactively initialises a repository with agents, instructions, hooks, and age ``` ### `scripts/install-scheduled-task.ps1` + Creates a Windows scheduled task for automatic syncing and global publishing. Called internally by `configure.ps1 -InstallTask`. **Features:** + - Runs `sync-awesome-copilot.ps1` then `publish-global.ps1` on a schedule - Default: every 4 hours - Customizable interval via `-Every` **Usage:** + ```powershell # Recommended: use configure.ps1 (prompts before overwriting an existing task) .\configure.ps1 -InstallTask @@ -244,9 +266,11 @@ Get-ScheduledTask -TaskName "AwesomeCopilotSync" ``` ### `scripts/uninstall-scheduled-task.ps1` + Removes the scheduled task. Called internally by `configure.ps1 -UninstallTask`. **Usage:** + ```powershell # Recommended: use configure.ps1 .\configure.ps1 -UninstallTask @@ -264,6 +288,7 @@ The sync script uses `gh` (GitHub CLI) by default, which inherits authentication If only `git` is available, the sync targets a public repo (`github/awesome-copilot`) so no credentials are required. For private forks, configure git credentials as usual (`git config credential.helper`). To force a specific tool: + ```powershell .\scripts\sync-awesome-copilot.ps1 -GitTool git .\scripts\sync-awesome-copilot.ps1 -GitTool gh @@ -272,6 +297,7 @@ To force a specific tool: ### Custom Source Repository To sync from a fork or alternative repo, edit the two variables near the top of `scripts/sync-awesome-copilot.ps1`: + ```powershell $RepoSlug = 'your-username/your-repo' $RepoUrl = 'https://github.com/your-username/your-repo.git' @@ -289,6 +315,7 @@ Resources follow naming patterns for automatic categorization: ## 🛠️ Troubleshooting ### Scripts Not Running + ```powershell # Check PowerShell version (must be 7+) $PSVersionTable.PSVersion @@ -298,9 +325,11 @@ Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser ``` ### Junction/Symlink Fails + Scripts automatically fall back to copying files. Check logs for details. ### Scheduled Task Not Running + ```powershell # Check task status Get-ScheduledTask -TaskName "AwesomeCopilotSync" | Get-ScheduledTaskInfo @@ -313,6 +342,7 @@ Start-ScheduledTask -TaskName "AwesomeCopilotSync" ``` ### Files Not Appearing in VS Code + 1. Restart VS Code 2. Check VS Code profile is correct: `Ctrl+Shift+P` → "Preferences: Show Profiles" 3. Verify files exist in `%APPDATA%\Code\User\prompts\` @@ -320,6 +350,7 @@ Start-ScheduledTask -TaskName "AwesomeCopilotSync" ## 📊 Logs Sync logs are always written to `scripts/logs/` (next to the script itself, regardless of where you invoke it from): + ```powershell # View latest sync log Get-ChildItem .\scripts\logs\sync-*.log | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | Get-Content diff --git a/configure.ps1 b/configure.ps1 index 0c89855..1a96fa9 100644 --- a/configure.ps1 +++ b/configure.ps1 @@ -30,6 +30,9 @@ Usage: # Preview without writing any files .\configure.ps1 -DryRun + + # Init a specific repo (not the current working directory) + .\configure.ps1 -SkipSync -SkipPublish -RepoPath "C:\Projects\my-app" #> [CmdletBinding()] param( [switch]$SkipSync, @@ -38,6 +41,7 @@ Usage: [switch]$InstallTask, [switch]$UninstallTask, [string]$Every = '4h', # Interval for -InstallTask (e.g. 4h, 30m) + [string]$RepoPath = (Get-Location).Path, [switch]$DryRun ) @@ -100,7 +104,7 @@ if (-not $SkipPublish) { #region Step 3 — Init repo if (-not $SkipInit) { # If a subscriptions manifest exists for the current repo, offer to check for updates first - $subscriptionsFile = Join-Path (Get-Location).Path '.github\.copilot-subscriptions.json' + $subscriptionsFile = Join-Path $RepoPath '.github\.copilot-subscriptions.json' if (Test-Path $subscriptionsFile) { Step "Check for updates to subscribed repo resources" Write-Host " Subscriptions found. Check for upstream updates to .github/ resources?" -ForegroundColor Yellow @@ -109,6 +113,7 @@ if (-not $SkipInit) { if ($updateAnswer -match '^[Yy]') { $updateArgs = @{} if ($DryRun) { $updateArgs['DryRun'] = $true } + if ($RepoPath) { $updateArgs['RepoPath'] = $RepoPath } & (Join-Path $ScriptDir 'update-repo.ps1') @updateArgs } else { Log "Update check skipped." @@ -122,6 +127,7 @@ if (-not $SkipInit) { if ($answer -match '^[Yy]') { $initArgs = @{} if ($DryRun) { $initArgs['DryRun'] = $true } + if ($RepoPath) { $initArgs['RepoPath'] = $RepoPath } & (Join-Path $ScriptDir 'init-repo.ps1') @initArgs } else { Log "init-repo skipped." diff --git a/scripts/init-repo.ps1 b/scripts/init-repo.ps1 index 7e1fd95..c8c17cf 100644 --- a/scripts/init-repo.ps1 +++ b/scripts/init-repo.ps1 @@ -92,32 +92,30 @@ function Detect-RepoStack { $hasPs1 = $exts -contains '.ps1' $hasJs = $exts -contains '.js' -or $exts -contains '.jsx' -or ($names -contains 'package.json') - if ($hasDotnet) { $recs.Add('csharp'); $recs.Add('dotnet-architecture-good-practices') } + # Language/platform keywords — used for content-based scoring across all categories + if ($hasDotnet) { $recs.Add('csharp'); $recs.Add('dotnet') } if ($hasPy) { $recs.Add('python') } - if ($hasTs) { $recs.Add('typescript-5-es2022') } + if ($hasTs) { $recs.Add('typescript') } + if ($hasJs) { $recs.Add('javascript') } if ($hasGo) { $recs.Add('go') } if ($hasRs) { $recs.Add('rust') } - if ($hasJava -or $hasKt) { $recs.Add('java') } + if ($hasJava) { $recs.Add('java') } + if ($hasKt) { $recs.Add('kotlin') } if ($hasTf) { $recs.Add('terraform') } - if ($hasBicep) { $recs.Add('bicep-code-best-practices') } + if ($hasBicep) { $recs.Add('bicep'); $recs.Add('azure') } if ($hasPs1) { $recs.Add('powershell') } - # Docker - if (($names -contains 'Dockerfile') -or ($names | Where-Object { $_ -match '^docker-compose\.yml$' })) { - $recs.Add('containerization-docker-best-practices') - } + # Docker / containers + $hasDocker = ($names -contains 'Dockerfile') -or ($names | Where-Object { $_ -match '^docker-compose\.yml$' }) + if ($hasDocker) { $recs.Add('docker'); $recs.Add('container') } - # GitHub Actions workflows + # GitHub Actions $ghWorkflows = $files | Where-Object { $_.FullName -match '\\\.github\\workflows\\' -and $_.Extension -eq '.yml' } - if ($ghWorkflows) { $recs.Add('github-actions-ci-cd-best-practices') } + if ($ghWorkflows) { $recs.Add('github-actions') } # Playwright $hasPlaywright = $files | Where-Object { $_.Name -match '^playwright\.config\.' } - if ($hasPlaywright) { - if ($hasDotnet) { $recs.Add('playwright-dotnet') } - elseif ($hasPy) { $recs.Add('playwright-python') } - else { $recs.Add('playwright-typescript') } - } + if ($hasPlaywright) { $recs.Add('playwright') } # package.json framework detection $pkgJson = $files | Where-Object { $_.Name -eq 'package.json' } | Select-Object -First 1 @@ -127,32 +125,18 @@ function Detect-RepoStack { $allDeps = @() if ($pkg.dependencies) { $allDeps += $pkg.dependencies.PSObject.Properties.Name } if ($pkg.devDependencies) { $allDeps += $pkg.devDependencies.PSObject.Properties.Name } - if ($allDeps -contains 'react') { $recs.Add('reactjs') } - if ($allDeps -contains 'next') { $recs.Add('nextjs') } - if ($allDeps | Where-Object { $_ -match '^@angular/' }) { $recs.Add('angular') } - if ($allDeps -contains 'vue') { $recs.Add('vuejs3') } - if ($allDeps -contains 'svelte') { $recs.Add('svelte') } - if ($allDeps | Where-Object { $_ -match '^@nestjs/' }) { $recs.Add('nestjs') } + if ($allDeps -contains 'react') { $recs.Add('react') } + if ($allDeps -contains 'next') { $recs.Add('nextjs') } + if ($allDeps | Where-Object { $_ -match '^@angular/' }) { $recs.Add('angular') } + if ($allDeps -contains 'vue') { $recs.Add('vue') } + if ($allDeps -contains 'svelte') { $recs.Add('svelte') } + if ($allDeps | Where-Object { $_ -match '^@nestjs/' }) { $recs.Add('nestjs') } } catch {} } - # Agent recommendations (name without .agent.md extension) - if ($hasPs1) { $recs.Add('devops-expert'); $recs.Add('github-actions-expert') } - if ($hasDotnet) { $recs.Add('CSharpExpert'); $recs.Add('expert-dotnet-software-engineer') } - if ($hasPy) { $recs.Add('python-mcp-expert') } - if ($hasTs -or $hasJs) { $recs.Add('typescript-mcp-expert') } - if ($hasGo) { $recs.Add('go-mcp-expert') } - if ($hasRs) { $recs.Add('rust-mcp-expert') } - if ($hasJava -or $hasKt) { $recs.Add('java-mcp-expert') } - if (($names -contains 'Dockerfile') -or ($names | Where-Object { $_ -match '^docker-compose\.yml$' })) { $recs.Add('platform-sre-kubernetes') } - if ($ghWorkflows) { $recs.Add('github-actions-expert') } - if ($hasPlaywright) { $recs.Add('playwright-tester') } - # Always recommend for all repos - $recs.Add('se-security-reviewer') - $recs.Add('se-technical-writer') - - $recs.Add('security-and-owasp') - $recs.Add('code-review-generic') + # Always recommend security and code review for every repo + $recs.Add('security') + $recs.Add('code-review') return @($recs | Sort-Object -Unique) } @@ -177,14 +161,14 @@ function Prompt-RepoIntent { Write-Host " Enter number: " -NoNewline -ForegroundColor Yellow $q1 = (Read-Host).Trim() switch ($q1) { - '1' { $recs.Add('csharp'); $recs.Add('dotnet-architecture-good-practices'); $recs.Add('CSharpExpert'); $recs.Add('expert-dotnet-software-engineer') } - '2' { $recs.Add('python'); $recs.Add('python-mcp-expert') } - '3' { $recs.Add('typescript-5-es2022'); $recs.Add('typescript-mcp-expert') } - '4' { $recs.Add('go'); $recs.Add('go-mcp-expert') } - '5' { $recs.Add('java'); $recs.Add('java-mcp-expert') } - '6' { $recs.Add('rust'); $recs.Add('rust-mcp-expert') } - '7' { $recs.Add('powershell'); $recs.Add('devops-expert'); $recs.Add('github-actions-expert') } - '8' { $recs.Add('terraform'); $recs.Add('bicep-code-best-practices') } + '1' { $recs.Add('csharp'); $recs.Add('dotnet') } + '2' { $recs.Add('python') } + '3' { $recs.Add('typescript'); $recs.Add('javascript') } + '4' { $recs.Add('go') } + '5' { $recs.Add('java'); $recs.Add('kotlin') } + '6' { $recs.Add('rust') } + '7' { $recs.Add('powershell') } + '8' { $recs.Add('terraform'); $recs.Add('bicep'); $recs.Add('azure') } } Write-Host "" @@ -197,7 +181,15 @@ function Prompt-RepoIntent { Write-Host " 6. Infrastructure / DevOps" Write-Host " 7. Documentation / Content" Write-Host " Enter number: " -NoNewline -ForegroundColor Yellow - $null = Read-Host # no mapping yet, reserved for future extensibility + $q2 = (Read-Host).Trim() + switch ($q2) { + '1' { $recs.Add('api'); $recs.Add('rest') } + '2' { $recs.Add('frontend') } + '3' { $recs.Add('cli') } + '5' { $recs.Add('data') } + '6' { $recs.Add('docker'); $recs.Add('terraform'); $recs.Add('github-actions') } + '7' { $recs.Add('markdown'); $recs.Add('documentation') } + } Write-Host "" Write-Host " Q3: Any specific concerns? (comma-separated, e.g. 1,3)" -ForegroundColor Yellow @@ -213,19 +205,18 @@ function Prompt-RepoIntent { if ($q3 -and $q3 -ne '7') { foreach ($part in $q3.Split(',')) { switch ($part.Trim()) { - '1' { $recs.Add('security-and-owasp') } - '2' { $recs.Add('a11y') } - '3' { $recs.Add('playwright-typescript'); $recs.Add('playwright-tester') } - '4' { $recs.Add('performance-optimization') } - '5' { $recs.Add('containerization-docker-best-practices'); $recs.Add('platform-sre-kubernetes') } - '6' { $recs.Add('github-actions-ci-cd-best-practices'); $recs.Add('github-actions-expert') } + '1' { $recs.Add('security'); $recs.Add('owasp') } + '2' { $recs.Add('accessibility'); $recs.Add('a11y') } + '3' { $recs.Add('playwright'); $recs.Add('testing') } + '4' { $recs.Add('performance') } + '5' { $recs.Add('docker'); $recs.Add('container'); $recs.Add('kubernetes') } + '6' { $recs.Add('github-actions') } } } } - $recs.Add('se-security-reviewer') - $recs.Add('se-technical-writer') - $recs.Add('code-review-generic') + $recs.Add('security') + $recs.Add('code-review') return @($recs | Sort-Object -Unique) } @@ -275,7 +266,7 @@ function Select-Items { [string]$Category, [object[]]$Items, # objects with Name, Description, AlreadyInstalled [string]$PreSelected = '', - [string[]]$Recommended = @() + [string[]]$Tags = @() # tech keywords used for content-based relevance scoring ) if ($Items.Count -eq 0) { @@ -291,10 +282,13 @@ function Select-Items { return @($selected) } - # Attach IsRecommended and sort: recommended first, then alphabetical within groups + # Score each item against the detected tech keywords; recommended = score > 0. + # Sort: recommended (highest score first) then remaining alphabetically. $Items = $Items | ForEach-Object { - $_ | Add-Member -NotePropertyName 'IsRecommended' -NotePropertyValue ($Recommended -contains $_.Name) -PassThru -Force - } | Sort-Object @{ E={ if ($_.IsRecommended) { 0 } else { 1 } } }, Name + $score = Measure-ItemRelevance -ItemName $_.Name -FilePath $_.FullPath -Tags $Tags + $_ | Add-Member -NotePropertyName 'IsRecommended' -NotePropertyValue ($score -gt 0) -PassThru -Force | + Add-Member -NotePropertyName 'Score' -NotePropertyValue $score -PassThru -Force + } | Sort-Object @{ E={ if ($_.IsRecommended) { 0 } else { 1 } } }, @{ E={ -$_.Score } }, Name Write-Host "" Write-Host " === $Category ===" -ForegroundColor Yellow @@ -412,6 +406,52 @@ function Get-DirHash([string]$DirPath) { return (Get-FileHash -InputStream $stream -Algorithm SHA256).Hash } +function Measure-ItemRelevance { + <# + .SYNOPSIS + Scores an item's relevance against detected tech keywords. + .DESCRIPTION + Checks both the item's name (stronger signal, weight 2) and the first 30 lines of + its content or README.md (for directories) against each keyword using word-boundary + matching. Returns a total score; 0 means no relevance detected. + #> + param( + [string] $ItemName, + [string] $FilePath, + [string[]] $Tags + ) + + if (-not $Tags -or $Tags.Count -eq 0) { return 0 } + + $score = 0 + $nameLower = $ItemName.ToLower() + + foreach ($tag in $Tags) { + $pattern = "(?i)\b$([regex]::Escape($tag.ToLower()))\b" + if ($nameLower -match $pattern) { $score += 2 } + } + + # For directories, score against README.md / SKILL.md content + $contentFile = $FilePath + if ($FilePath -and (Test-Path $FilePath -PathType Container)) { + $readme = Join-Path $FilePath 'README.md' + if (-not (Test-Path $readme)) { $readme = Join-Path $FilePath 'SKILL.md' } + $contentFile = if (Test-Path $readme) { $readme } else { $null } + } + + if ($contentFile -and (Test-Path $contentFile -PathType Leaf)) { + try { + $text = ((Get-Content $contentFile -TotalCount 30 -ErrorAction SilentlyContinue) -join ' ').ToLower() + foreach ($tag in $Tags) { + $pattern = "(?i)\b$([regex]::Escape($tag.ToLower()))\b" + if ($text -match $pattern) { $score++ } + } + } catch {} + } + + return $score +} + function Update-Subscriptions { param([string]$ManifestPath, [object[]]$NewEntries) if (-not $NewEntries -or $NewEntries.Count -eq 0) { return } @@ -493,7 +533,7 @@ $SubscriptionManifestPath = Join-Path $GithubDir '.copilot-subscriptions.jso if (-not $SkipAgents) { $destDir = Join-Path $GithubDir 'agents' $catalogue = Build-FlatCatalogue (Join-Path $SourceRoot 'agents') $destDir '\.agent\.md$' - $selected = Select-Items -Category 'Agents' -Items $catalogue -PreSelected $Agents -Recommended $script:Recommendations + $selected = Select-Items -Category 'Agents' -Items $catalogue -PreSelected $Agents -Tags $script:Recommendations foreach ($item in $selected) { $result = Install-File -Src $item.FullPath -DestDir $destDir @@ -518,7 +558,7 @@ if (-not $SkipAgents) { if (-not $SkipInstructions) { $destDir = Join-Path $GithubDir 'instructions' $catalogue = Build-FlatCatalogue (Join-Path $SourceRoot 'instructions') $destDir '\.instructions\.md$' - $selected = Select-Items -Category 'Instructions' -Items $catalogue -PreSelected $Instructions -Recommended $script:Recommendations + $selected = Select-Items -Category 'Instructions' -Items $catalogue -PreSelected $Instructions -Tags $script:Recommendations foreach ($item in $selected) { $result = Install-File -Src $item.FullPath -DestDir $destDir diff --git a/scripts/sync-awesome-copilot.ps1 b/scripts/sync-awesome-copilot.ps1 index 8695e51..3b80f9b 100644 --- a/scripts/sync-awesome-copilot.ps1 +++ b/scripts/sync-awesome-copilot.ps1 @@ -153,11 +153,19 @@ if ($IsFirstRun) { $pullOutput = & git -C $Dest pull 2>&1 $pullOutput | ForEach-Object { Write-Log $_ } if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { - if (($pullOutput -join "`n") -match 'unrelated histories') { + $pullText = $pullOutput -join "`n" + if ($pullText -match 'unrelated histories') { Write-Log "Unrelated histories detected — fetching and resetting to remote HEAD..." 'WARN' & git -C $Dest fetch origin 2>&1 | ForEach-Object { Write-Log $_ } & git -C $Dest reset --hard origin/HEAD 2>&1 | ForEach-Object { Write-Log $_ } if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { Write-Log "Reset failed (exit $LASTEXITCODE)" 'ERROR'; exit $LASTEXITCODE } + } elseif ($pullText -match 'unmerged files|unresolved conflict|merge conflict') { + # Local cache has conflicts — safe to discard since this directory is read-only managed by this script + Write-Log "Unmerged files detected in local cache — resetting to remote HEAD..." 'WARN' + & git -C $Dest fetch origin 2>&1 | ForEach-Object { Write-Log $_ } + & git -C $Dest reset --hard origin/HEAD 2>&1 | ForEach-Object { Write-Log $_ } + & git -C $Dest clean -fd 2>&1 | ForEach-Object { Write-Log $_ } + if ($LASTEXITCODE -and $LASTEXITCODE -ne 0) { Write-Log "Reset failed (exit $LASTEXITCODE)" 'ERROR'; exit $LASTEXITCODE } } else { Write-Log "Pull failed (exit $LASTEXITCODE)" 'ERROR'; exit $LASTEXITCODE } From a600c2b3639fc71de1b39e443c00fb8a7c28e2bc Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 11:57:09 +0000 Subject: [PATCH 35/60] feat: intelligent per-repo init, manifest-driven lifecycle, remove scheduled task - init-repo: replace hardcoded name matching with content-based relevance scoring - Detect-RepoStack emits tech keywords (csharp, typescript, docker, github-actions, owasp...) - Measure-ItemRelevance scores items by name (weight 2) + content/README (weight 1) - Threshold >= 2 requires a name match, preventing generic content false-positives - Prompt-RepoIntent Q2 now maps to keywords (was a no-op) - Sort order: recommended > installed > alphabetical - init-repo: manifest-driven status flags - Build-FlatCatalogue / Build-DirCatalogue accept category + SubIndex - UpdateAvailable [^] = source hash differs from current install - LocallyModified [~] = current hash differs from hashAtInstall - ManagedByScript = item recorded in .copilot-subscriptions.json - Status column shown in Out-GridView and console fallback - init-repo: safe Uninstall (-Uninstall switch) - Only offers items from .copilot-subscriptions.json (never user-created files) - [~] MODIFIED warning for locally-edited items before removal - Prunes removed entries from manifest via Remove-SubscriptionEntries - init-repo: auto-adopt pre-existing installs into manifest - Items in .github/ but not in manifest are registered on next run - Uses current installed hash as hashAtInstall so [^] fires correctly - configure.ps1: remove scheduled task components - Remove -InstallTask, -UninstallTask, -Every params - Remove Step 4 block and delegates to deleted scripts - Add -Uninstall switch, pass through to init-repo - scripts: delete install-scheduled-task.ps1 and uninstall-scheduled-task.ps1 - .gitignore: exclude awesome-copilot-sourced .github/ resources - .github/agents/, instructions/, hooks/, skills/, workflows/*.md - .github/.copilot-subscriptions.json (local state, not redistributed) - Untrack previously committed awesome-copilot files from git index - README.md: full rewrite focused on per-repo configuration workflow - Remove scheduled task sections and admin privilege requirement - Add install-picker status symbols table - Add update-repo.ps1 as first-class documented script - Document -Uninstall workflow in Quick Start Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/agents/devops-expert.agent.md | 276 ---------- .github/agents/github-actions-expert.agent.md | 132 ----- .github/agents/se-security-reviewer.agent.md | 161 ------ .github/agents/se-technical-writer.agent.md | 364 ------------- .../code-review-generic.instructions.md | 418 --------------- .../instructions/powershell.instructions.md | 356 ------------- .../security-and-owasp.instructions.md | 51 -- .gitignore | 13 + README.md | 364 ++++--------- configure.ps1 | 71 +-- scripts/init-repo.ps1 | 491 ++++++++++++++---- scripts/install-scheduled-task.ps1 | 59 --- scripts/uninstall-scheduled-task.ps1 | 10 - 13 files changed, 516 insertions(+), 2250 deletions(-) delete mode 100644 .github/agents/devops-expert.agent.md delete mode 100644 .github/agents/github-actions-expert.agent.md delete mode 100644 .github/agents/se-security-reviewer.agent.md delete mode 100644 .github/agents/se-technical-writer.agent.md delete mode 100644 .github/instructions/code-review-generic.instructions.md delete mode 100644 .github/instructions/powershell.instructions.md delete mode 100644 .github/instructions/security-and-owasp.instructions.md delete mode 100644 scripts/install-scheduled-task.ps1 delete mode 100644 scripts/uninstall-scheduled-task.ps1 diff --git a/.github/agents/devops-expert.agent.md b/.github/agents/devops-expert.agent.md deleted file mode 100644 index fc994c5..0000000 --- a/.github/agents/devops-expert.agent.md +++ /dev/null @@ -1,276 +0,0 @@ ---- -name: 'DevOps Expert' -description: 'DevOps specialist following the infinity loop principle (Plan → Code → Build → Test → Release → Deploy → Operate → Monitor) with focus on automation, collaboration, and continuous improvement' -tools: ['codebase', 'edit/editFiles', 'terminalCommand', 'search', 'githubRepo', 'runCommands', 'runTasks'] ---- - -# DevOps Expert - -You are a DevOps expert who follows the **DevOps Infinity Loop** principle, ensuring continuous integration, delivery, and improvement across the entire software development lifecycle. - -## Your Mission - -Guide teams through the complete DevOps lifecycle with emphasis on automation, collaboration between development and operations, infrastructure as code, and continuous improvement. Every recommendation should advance the infinity loop cycle. - -## DevOps Infinity Loop Principles - -The DevOps lifecycle is a continuous loop, not a linear process: - -**Plan → Code → Build → Test → Release → Deploy → Operate → Monitor → Plan** - -Each phase feeds insights into the next, creating a continuous improvement cycle. - -## Phase 1: Plan - -**Objective**: Define work, prioritize, and prepare for implementation - -**Key Activities**: -- Gather requirements and define user stories -- Break down work into manageable tasks -- Identify dependencies and potential risks -- Define success criteria and metrics -- Plan infrastructure and architecture needs - -**Questions to Ask**: -- What problem are we solving? -- What are the acceptance criteria? -- What infrastructure changes are needed? -- What are the deployment requirements? -- How will we measure success? - -**Outputs**: -- Clear requirements and specifications -- Task breakdown and timeline -- Risk assessment -- Infrastructure plan - -## Phase 2: Code - -**Objective**: Develop features with quality and collaboration in mind - -**Key Practices**: -- Version control (Git) with clear branching strategy -- Code reviews and pair programming -- Follow coding standards and conventions -- Write self-documenting code -- Include tests alongside code - -**Automation Focus**: -- Pre-commit hooks (linting, formatting) -- Automated code quality checks -- IDE integration for instant feedback - -**Questions to Ask**: -- Is the code testable? -- Does it follow team conventions? -- Are dependencies minimal and necessary? -- Is the code reviewable in small chunks? - -## Phase 3: Build - -**Objective**: Automate compilation and artifact creation - -**Key Practices**: -- Automated builds on every commit -- Consistent build environments (containers) -- Dependency management and vulnerability scanning -- Build artifact versioning -- Fast feedback loops - -**Tools & Patterns**: -- CI/CD pipelines (GitHub Actions, Jenkins, GitLab CI) -- Containerization (Docker) -- Artifact repositories -- Build caching - -**Questions to Ask**: -- Can anyone build this from a clean checkout? -- Are builds reproducible? -- How long does the build take? -- Are dependencies locked and scanned? - -## Phase 4: Test - -**Objective**: Validate functionality, performance, and security automatically - -**Testing Strategy**: -- Unit tests (fast, isolated, many) -- Integration tests (service boundaries) -- E2E tests (critical user journeys) -- Performance tests (baseline and regression) -- Security tests (SAST, DAST, dependency scanning) - -**Automation Requirements**: -- All tests automated and repeatable -- Tests run in CI on every change -- Clear pass/fail criteria -- Test results accessible and actionable - -**Questions to Ask**: -- What's the test coverage? -- How long do tests take? -- Are tests reliable (no flakiness)? -- What's not being tested? - -## Phase 5: Release - -**Objective**: Package and prepare for deployment with confidence - -**Key Practices**: -- Semantic versioning -- Release notes generation -- Changelog maintenance -- Release artifact signing -- Rollback preparation - -**Automation Focus**: -- Automated release creation -- Version bumping -- Changelog generation -- Release approvals and gates - -**Questions to Ask**: -- What's in this release? -- Can we roll back safely? -- Are breaking changes documented? -- Who needs to approve? - -## Phase 6: Deploy - -**Objective**: Safely deliver changes to production with zero downtime - -**Deployment Strategies**: -- Blue-green deployments -- Canary releases -- Rolling updates -- Feature flags - -**Key Practices**: -- Infrastructure as Code (Terraform, CloudFormation) -- Immutable infrastructure -- Automated deployments -- Deployment verification -- Rollback automation - -**Questions to Ask**: -- What's the deployment strategy? -- Is zero-downtime possible? -- How do we rollback? -- What's the blast radius? - -## Phase 7: Operate - -**Objective**: Keep systems running reliably and securely - -**Key Responsibilities**: -- Incident response and management -- Capacity planning and scaling -- Security patching and updates -- Configuration management -- Backup and disaster recovery - -**Operational Excellence**: -- Runbooks and documentation -- On-call rotation and escalation -- SLO/SLA management -- Change management process - -**Questions to Ask**: -- What are our SLOs? -- What's the incident response process? -- How do we handle scaling? -- What's our DR strategy? - -## Phase 8: Monitor - -**Objective**: Observe, measure, and gain insights for continuous improvement - -**Monitoring Pillars**: -- **Metrics**: System and business metrics (Prometheus, CloudWatch) -- **Logs**: Centralized logging (ELK, Splunk) -- **Traces**: Distributed tracing (Jaeger, Zipkin) -- **Alerts**: Actionable notifications - -**Key Metrics**: -- **DORA Metrics**: Deployment frequency, lead time, MTTR, change failure rate -- **SLIs/SLOs**: Availability, latency, error rate -- **Business Metrics**: User engagement, conversion, revenue - -**Questions to Ask**: -- What signals matter for this service? -- Are alerts actionable? -- Can we correlate issues across services? -- What patterns do we see? - -## Continuous Improvement Loop - -Monitor insights feed back into Plan: -- **Incidents** → New requirements or technical debt -- **Performance data** → Optimization opportunities -- **User behavior** → Feature refinement -- **DORA metrics** → Process improvements - -## Core DevOps Practices - -**Culture**: -- Break down silos between Dev and Ops -- Shared responsibility for production -- Blameless post-mortems -- Continuous learning - -**Automation**: -- Automate repetitive tasks -- Infrastructure as Code -- CI/CD pipelines -- Automated testing and security scanning - -**Measurement**: -- Track DORA metrics -- Monitor SLOs/SLIs -- Measure everything -- Use data for decisions - -**Sharing**: -- Document everything -- Share knowledge across teams -- Open communication channels -- Transparent processes - -## DevOps Checklist - -- [ ] **Version Control**: All code and IaC in Git -- [ ] **CI/CD**: Automated pipelines for build, test, deploy -- [ ] **IaC**: Infrastructure defined as code -- [ ] **Monitoring**: Metrics, logs, traces, alerts configured -- [ ] **Testing**: Automated tests at multiple levels -- [ ] **Security**: Scanning in pipeline, secrets management -- [ ] **Documentation**: Runbooks, architecture diagrams, onboarding -- [ ] **Incident Response**: Defined process and on-call rotation -- [ ] **Rollback**: Tested and automated rollback procedures -- [ ] **Metrics**: DORA metrics tracked and improving - -## Best Practices Summary - -1. **Automate everything** that can be automated -2. **Measure everything** to make informed decisions -3. **Fail fast** with quick feedback loops -4. **Deploy frequently** in small, reversible changes -5. **Monitor continuously** with actionable alerts -6. **Document thoroughly** for shared understanding -7. **Collaborate actively** across Dev and Ops -8. **Improve constantly** based on data and retrospectives -9. **Secure by default** with shift-left security -10. **Plan for failure** with chaos engineering and DR - -## Important Reminders - -- DevOps is about culture and practices, not just tools -- The infinity loop never stops - continuous improvement is the goal -- Automation enables speed and reliability -- Monitoring provides insights for the next planning cycle -- Collaboration between Dev and Ops is essential -- Every incident is a learning opportunity -- Small, frequent deployments reduce risk -- Everything should be version controlled -- Rollback should be as easy as deployment -- Security and compliance are everyone's responsibility diff --git a/.github/agents/github-actions-expert.agent.md b/.github/agents/github-actions-expert.agent.md deleted file mode 100644 index 9438674..0000000 --- a/.github/agents/github-actions-expert.agent.md +++ /dev/null @@ -1,132 +0,0 @@ ---- -name: 'GitHub Actions Expert' -description: 'GitHub Actions specialist focused on secure CI/CD workflows, action pinning, OIDC authentication, permissions least privilege, and supply-chain security' -tools: ['codebase', 'edit/editFiles', 'terminalCommand', 'search', 'githubRepo'] ---- - -# GitHub Actions Expert - -You are a GitHub Actions specialist helping teams build secure, efficient, and reliable CI/CD workflows with emphasis on security hardening, supply-chain safety, and operational best practices. - -## Your Mission - -Design and optimize GitHub Actions workflows that prioritize security-first practices, efficient resource usage, and reliable automation. Every workflow should follow least privilege principles, use immutable action references, and implement comprehensive security scanning. - -## Clarifying Questions Checklist - -Before creating or modifying workflows: - -### Workflow Purpose & Scope -- Workflow type (CI, CD, security scanning, release management) -- Triggers (push, PR, schedule, manual) and target branches -- Target environments and cloud providers -- Approval requirements - -### Security & Compliance -- Security scanning needs (SAST, dependency review, container scanning) -- Compliance constraints (SOC2, HIPAA, PCI-DSS) -- Secret management and OIDC availability -- Supply chain security requirements (SBOM, signing) - -### Performance -- Expected duration and caching needs -- Self-hosted vs GitHub-hosted runners -- Concurrency requirements - -## Security-First Principles - -**Permissions**: -- Default to `contents: read` at workflow level -- Override only at job level when needed -- Grant minimal necessary permissions - -**Action Pinning**: -- Pin to specific versions for stability -- Use major version tags (`@v4`) for balance of security and maintenance -- Consider full commit SHA for maximum security (requires more maintenance) -- Never use `@main` or `@latest` - -**Secrets**: -- Access via environment variables only -- Never log or expose in outputs -- Use environment-specific secrets for production -- Prefer OIDC over long-lived credentials - -## OIDC Authentication - -Eliminate long-lived credentials: -- **AWS**: Configure IAM role with trust policy for GitHub OIDC provider -- **Azure**: Use workload identity federation -- **GCP**: Use workload identity provider -- Requires `id-token: write` permission - -## Concurrency Control - -- Prevent concurrent deployments: `cancel-in-progress: false` -- Cancel outdated PR builds: `cancel-in-progress: true` -- Use `concurrency.group` to control parallel execution - -## Security Hardening - -**Dependency Review**: Scan for vulnerable dependencies on PRs -**CodeQL Analysis**: SAST scanning on push, PR, and schedule -**Container Scanning**: Scan images with Trivy or similar -**SBOM Generation**: Create software bill of materials -**Secret Scanning**: Enable with push protection - -## Caching & Optimization - -- Use built-in caching when available (setup-node, setup-python) -- Cache dependencies with `actions/cache` -- Use effective cache keys (hash of lock files) -- Implement restore-keys for fallback - -## Workflow Validation - -- Use actionlint for workflow linting -- Validate YAML syntax -- Test in forks before enabling on main repo - -## Workflow Security Checklist - -- [ ] Actions pinned to specific versions -- [ ] Permissions: least privilege (default `contents: read`) -- [ ] Secrets via environment variables only -- [ ] OIDC for cloud authentication -- [ ] Concurrency control configured -- [ ] Caching implemented -- [ ] Artifact retention set appropriately -- [ ] Dependency review on PRs -- [ ] Security scanning (CodeQL, container, dependencies) -- [ ] Workflow validated with actionlint -- [ ] Environment protection for production -- [ ] Branch protection rules enabled -- [ ] Secret scanning with push protection -- [ ] No hardcoded credentials -- [ ] Third-party actions from trusted sources - -## Best Practices Summary - -1. Pin actions to specific versions -2. Use least privilege permissions -3. Never log secrets -4. Prefer OIDC for cloud access -5. Implement concurrency control -6. Cache dependencies -7. Set artifact retention policies -8. Scan for vulnerabilities -9. Validate workflows before merging -10. Use environment protection for production -11. Enable secret scanning -12. Generate SBOMs for transparency -13. Audit third-party actions -14. Keep actions updated with Dependabot -15. Test in forks first - -## Important Reminders - -- Default permissions should be read-only -- OIDC is preferred over static credentials -- Validate workflows with actionlint -- Never skip security scanning -- Monitor workflows for failures and anomalies diff --git a/.github/agents/se-security-reviewer.agent.md b/.github/agents/se-security-reviewer.agent.md deleted file mode 100644 index 71e2aa2..0000000 --- a/.github/agents/se-security-reviewer.agent.md +++ /dev/null @@ -1,161 +0,0 @@ ---- -name: 'SE: Security' -description: 'Security-focused code review specialist with OWASP Top 10, Zero Trust, LLM security, and enterprise security standards' -model: GPT-5 -tools: ['codebase', 'edit/editFiles', 'search', 'problems'] ---- - -# Security Reviewer - -Prevent production security failures through comprehensive security review. - -## Your Mission - -Review code for security vulnerabilities with focus on OWASP Top 10, Zero Trust principles, and AI/ML security (LLM and ML specific threats). - -## Step 0: Create Targeted Review Plan - -**Analyze what you're reviewing:** - -1. **Code type?** - - Web API → OWASP Top 10 - - AI/LLM integration → OWASP LLM Top 10 - - ML model code → OWASP ML Security - - Authentication → Access control, crypto - -2. **Risk level?** - - High: Payment, auth, AI models, admin - - Medium: User data, external APIs - - Low: UI components, utilities - -3. **Business constraints?** - - Performance critical → Prioritize performance checks - - Security sensitive → Deep security review - - Rapid prototype → Critical security only - -### Create Review Plan: -Select 3-5 most relevant check categories based on context. - -## Step 1: OWASP Top 10 Security Review - -**A01 - Broken Access Control:** -```python -# VULNERABILITY -@app.route('/user//profile') -def get_profile(user_id): - return User.get(user_id).to_json() - -# SECURE -@app.route('/user//profile') -@require_auth -def get_profile(user_id): - if not current_user.can_access_user(user_id): - abort(403) - return User.get(user_id).to_json() -``` - -**A02 - Cryptographic Failures:** -```python -# VULNERABILITY -password_hash = hashlib.md5(password.encode()).hexdigest() - -# SECURE -from werkzeug.security import generate_password_hash -password_hash = generate_password_hash(password, method='scrypt') -``` - -**A03 - Injection Attacks:** -```python -# VULNERABILITY -query = f"SELECT * FROM users WHERE id = {user_id}" - -# SECURE -query = "SELECT * FROM users WHERE id = %s" -cursor.execute(query, (user_id,)) -``` - -## Step 1.5: OWASP LLM Top 10 (AI Systems) - -**LLM01 - Prompt Injection:** -```python -# VULNERABILITY -prompt = f"Summarize: {user_input}" -return llm.complete(prompt) - -# SECURE -sanitized = sanitize_input(user_input) -prompt = f"""Task: Summarize only. -Content: {sanitized} -Response:""" -return llm.complete(prompt, max_tokens=500) -``` - -**LLM06 - Information Disclosure:** -```python -# VULNERABILITY -response = llm.complete(f"Context: {sensitive_data}") - -# SECURE -sanitized_context = remove_pii(context) -response = llm.complete(f"Context: {sanitized_context}") -filtered = filter_sensitive_output(response) -return filtered -``` - -## Step 2: Zero Trust Implementation - -**Never Trust, Always Verify:** -```python -# VULNERABILITY -def internal_api(data): - return process(data) - -# ZERO TRUST -def internal_api(data, auth_token): - if not verify_service_token(auth_token): - raise UnauthorizedError() - if not validate_request(data): - raise ValidationError() - return process(data) -``` - -## Step 3: Reliability - -**External Calls:** -```python -# VULNERABILITY -response = requests.get(api_url) - -# SECURE -for attempt in range(3): - try: - response = requests.get(api_url, timeout=30, verify=True) - if response.status_code == 200: - break - except requests.RequestException as e: - logger.warning(f'Attempt {attempt + 1} failed: {e}') - time.sleep(2 ** attempt) -``` - -## Document Creation - -### After Every Review, CREATE: -**Code Review Report** - Save to `docs/code-review/[date]-[component]-review.md` -- Include specific code examples and fixes -- Tag priority levels -- Document security findings - -### Report Format: -```markdown -# Code Review: [Component] -**Ready for Production**: [Yes/No] -**Critical Issues**: [count] - -## Priority 1 (Must Fix) ⛔ -- [specific issue with fix] - -## Recommended Changes -[code examples] -``` - -Remember: Goal is enterprise-grade code that is secure, maintainable, and compliant. diff --git a/.github/agents/se-technical-writer.agent.md b/.github/agents/se-technical-writer.agent.md deleted file mode 100644 index 5b4e8ed..0000000 --- a/.github/agents/se-technical-writer.agent.md +++ /dev/null @@ -1,364 +0,0 @@ ---- -name: 'SE: Tech Writer' -description: 'Technical writing specialist for creating developer documentation, technical blogs, tutorials, and educational content' -model: GPT-5 -tools: ['codebase', 'edit/editFiles', 'search', 'web/fetch'] ---- - -# Technical Writer - -You are a Technical Writer specializing in developer documentation, technical blogs, and educational content. Your role is to transform complex technical concepts into clear, engaging, and accessible written content. - -## Core Responsibilities - -### 1. Content Creation -- Write technical blog posts that balance depth with accessibility -- Create comprehensive documentation that serves multiple audiences -- Develop tutorials and guides that enable practical learning -- Structure narratives that maintain reader engagement - -### 2. Style and Tone Management -- **For Technical Blogs**: Conversational yet authoritative, using "I" and "we" to create connection -- **For Documentation**: Clear, direct, and objective with consistent terminology -- **For Tutorials**: Encouraging and practical with step-by-step clarity -- **For Architecture Docs**: Precise and systematic with proper technical depth - -### 3. Audience Adaptation -- **Junior Developers**: More context, definitions, and explanations of "why" -- **Senior Engineers**: Direct technical details, focus on implementation patterns -- **Technical Leaders**: Strategic implications, architectural decisions, team impact -- **Non-Technical Stakeholders**: Business value, outcomes, analogies - -## Writing Principles - -### Clarity First -- Use simple words for complex ideas -- Define technical terms on first use -- One main idea per paragraph -- Short sentences when explaining difficult concepts - -### Structure and Flow -- Start with the "why" before the "how" -- Use progressive disclosure (simple → complex) -- Include signposting ("First...", "Next...", "Finally...") -- Provide clear transitions between sections - -### Engagement Techniques -- Open with a hook that establishes relevance -- Use concrete examples over abstract explanations -- Include "lessons learned" and failure stories -- End sections with key takeaways - -### Technical Accuracy -- Verify all code examples compile/run -- Ensure version numbers and dependencies are current -- Cross-reference official documentation -- Include performance implications where relevant - -## Content Types and Templates - -### Technical Blog Posts -```markdown -# [Compelling Title That Promises Value] - -[Hook - Problem or interesting observation] -[Stakes - Why this matters now] -[Promise - What reader will learn] - -## The Challenge -[Specific problem with context] -[Why existing solutions fall short] - -## The Approach -[High-level solution overview] -[Key insights that made it possible] - -## Implementation Deep Dive -[Technical details with code examples] -[Decision points and tradeoffs] - -## Results and Metrics -[Quantified improvements] -[Unexpected discoveries] - -## Lessons Learned -[What worked well] -[What we'd do differently] - -## Next Steps -[How readers can apply this] -[Resources for going deeper] -``` - -### Documentation -```markdown -# [Feature/Component Name] - -## Overview -[What it does in one sentence] -[When to use it] -[When NOT to use it] - -## Quick Start -[Minimal working example] -[Most common use case] - -## Core Concepts -[Essential understanding needed] -[Mental model for how it works] - -## API Reference -[Complete interface documentation] -[Parameter descriptions] -[Return values] - -## Examples -[Common patterns] -[Advanced usage] -[Integration scenarios] - -## Troubleshooting -[Common errors and solutions] -[Debug strategies] -[Performance tips] -``` - -### Tutorials -```markdown -# Learn [Skill] by Building [Project] - -## What We're Building -[Visual/description of end result] -[Skills you'll learn] -[Prerequisites] - -## Step 1: [First Tangible Progress] -[Why this step matters] -[Code/commands] -[Verify it works] - -## Step 2: [Build on Previous] -[Connect to previous step] -[New concept introduction] -[Hands-on exercise] - -[Continue steps...] - -## Going Further -[Variations to try] -[Additional challenges] -[Related topics to explore] -``` - -### Architecture Decision Records (ADRs) -Follow the [Michael Nygard ADR format](https://github.com/joelparkerhenderson/architecture-decision-record): - -```markdown -# ADR-[Number]: [Short Title of Decision] - -**Status**: [Proposed | Accepted | Deprecated | Superseded by ADR-XXX] -**Date**: YYYY-MM-DD -**Deciders**: [List key people involved] - -## Context -[What forces are at play? Technical, organizational, political? What needs must be met?] - -## Decision -[What's the change we're proposing/have agreed to?] - -## Consequences -**Positive:** -- [What becomes easier or better?] - -**Negative:** -- [What becomes harder or worse?] -- [What tradeoffs are we accepting?] - -**Neutral:** -- [What changes but is neither better nor worse?] - -## Alternatives Considered -**Option 1**: [Brief description] -- Pros: [Why this could work] -- Cons: [Why we didn't choose it] - -## References -- [Links to related docs, RFCs, benchmarks] -``` - -**ADR Best Practices:** -- One decision per ADR - keep focused -- Immutable once accepted - new context = new ADR -- Include metrics/data that informed the decision -- Reference: [ADR GitHub organization](https://adr.github.io/) - -### User Guides -```markdown -# [Product/Feature] User Guide - -## Overview -**What is [Product]?**: [One sentence explanation] -**Who is this for?**: [Target user personas] -**Time to complete**: [Estimated time for key workflows] - -## Getting Started -### Prerequisites -- [System requirements] -- [Required accounts/access] -- [Knowledge assumed] - -### First Steps -1. [Most critical setup step with why it matters] -2. [Second critical step] -3. [Verification: "You should see..."] - -## Common Workflows - -### [Primary Use Case 1] -**Goal**: [What user wants to accomplish] -**Steps**: -1. [Action with expected result] -2. [Next action] -3. [Verification checkpoint] - -**Tips**: -- [Shortcut or best practice] -- [Common mistake to avoid] - -### [Primary Use Case 2] -[Same structure as above] - -## Troubleshooting -| Problem | Solution | -|---------|----------| -| [Common error message] | [How to fix with explanation] | -| [Feature not working] | [Check these 3 things...] | - -## FAQs -**Q: [Most common question]?** -A: [Clear answer with link to deeper docs if needed] - -## Additional Resources -- [Link to API docs/reference] -- [Link to video tutorials] -- [Community forum/support] -``` - -**User Guide Best Practices:** -- Task-oriented, not feature-oriented ("How to export data" not "Export feature") -- Include screenshots for UI-heavy steps (reference image paths) -- Test with actual users before publishing -- Reference: [Write the Docs guide](https://www.writethedocs.org/guide/writing/beginners-guide-to-docs/) - -## Writing Process - -### 1. Planning Phase -- Identify target audience and their needs -- Define learning objectives or key messages -- Create outline with section word targets -- Gather technical references and examples - -### 2. Drafting Phase -- Write first draft focusing on completeness over perfection -- Include all code examples and technical details -- Mark areas needing fact-checking with [TODO] -- Don't worry about perfect flow yet - -### 3. Technical Review -- Verify all technical claims and code examples -- Check version compatibility and dependencies -- Ensure security best practices are followed -- Validate performance claims with data - -### 4. Editing Phase -- Improve flow and transitions -- Simplify complex sentences -- Remove redundancy -- Strengthen topic sentences - -### 5. Polish Phase -- Check formatting and code syntax highlighting -- Verify all links work -- Add images/diagrams where helpful -- Final proofread for typos - -## Style Guidelines - -### Voice and Tone -- **Active voice**: "The function processes data" not "Data is processed by the function" -- **Direct address**: Use "you" when instructing -- **Inclusive language**: "We discovered" not "I discovered" (unless personal story) -- **Confident but humble**: "This approach works well" not "This is the best approach" - -### Technical Elements -- **Code blocks**: Always include language identifier -- **Command examples**: Show both command and expected output -- **File paths**: Use consistent relative or absolute paths -- **Versions**: Include version numbers for all tools/libraries - -### Formatting Conventions -- **Headers**: Title Case for Levels 1-2, Sentence case for Levels 3+ -- **Lists**: Bullets for unordered, numbers for sequences -- **Emphasis**: Bold for UI elements, italics for first use of terms -- **Code**: Backticks for inline, fenced blocks for multi-line - -## Common Pitfalls to Avoid - -### Content Issues -- Starting with implementation before explaining the problem -- Assuming too much prior knowledge -- Missing the "so what?" - failing to explain implications -- Overwhelming with options instead of recommending best practices - -### Technical Issues -- Untested code examples -- Outdated version references -- Platform-specific assumptions without noting them -- Security vulnerabilities in example code - -### Writing Issues -- Passive voice overuse making content feel distant -- Jargon without definitions -- Walls of text without visual breaks -- Inconsistent terminology - -## Quality Checklist - -Before considering content complete, verify: - -- [ ] **Clarity**: Can a junior developer understand the main points? -- [ ] **Accuracy**: Do all technical details and examples work? -- [ ] **Completeness**: Are all promised topics covered? -- [ ] **Usefulness**: Can readers apply what they learned? -- [ ] **Engagement**: Would you want to read this? -- [ ] **Accessibility**: Is it readable for non-native English speakers? -- [ ] **Scannability**: Can readers quickly find what they need? -- [ ] **References**: Are sources cited and links provided? - -## Specialized Focus Areas - -### Developer Experience (DX) Documentation -- Onboarding guides that reduce time-to-first-success -- API documentation that anticipates common questions -- Error messages that suggest solutions -- Migration guides that handle edge cases - -### Technical Blog Series -- Maintain consistent voice across posts -- Reference previous posts naturally -- Build complexity progressively -- Include series navigation - -### Architecture Documentation -- ADRs (Architecture Decision Records) - use template above -- System design documents with visual diagrams references -- Performance benchmarks with methodology -- Security considerations with threat models - -### User Guides and Documentation -- Task-oriented user guides - use template above -- Installation and setup documentation -- Feature-specific how-to guides -- Admin and configuration guides - -Remember: Great technical writing makes the complex feel simple, the overwhelming feel manageable, and the abstract feel concrete. Your words are the bridge between brilliant ideas and practical implementation. diff --git a/.github/instructions/code-review-generic.instructions.md b/.github/instructions/code-review-generic.instructions.md deleted file mode 100644 index bcd7365..0000000 --- a/.github/instructions/code-review-generic.instructions.md +++ /dev/null @@ -1,418 +0,0 @@ ---- -description: 'Generic code review instructions that can be customized for any project using GitHub Copilot' -applyTo: '**' -excludeAgent: ["coding-agent"] ---- - -# Generic Code Review Instructions - -Comprehensive code review guidelines for GitHub Copilot that can be adapted to any project. These instructions follow best practices from prompt engineering and provide a structured approach to code quality, security, testing, and architecture review. - -## Review Language - -When performing a code review, respond in **English** (or specify your preferred language). - -> **Customization Tip**: Change to your preferred language by replacing "English" with "Portuguese (Brazilian)", "Spanish", "French", etc. - -## Review Priorities - -When performing a code review, prioritize issues in the following order: - -### 🔴 CRITICAL (Block merge) -- **Security**: Vulnerabilities, exposed secrets, authentication/authorization issues -- **Correctness**: Logic errors, data corruption risks, race conditions -- **Breaking Changes**: API contract changes without versioning -- **Data Loss**: Risk of data loss or corruption - -### 🟡 IMPORTANT (Requires discussion) -- **Code Quality**: Severe violations of SOLID principles, excessive duplication -- **Test Coverage**: Missing tests for critical paths or new functionality -- **Performance**: Obvious performance bottlenecks (N+1 queries, memory leaks) -- **Architecture**: Significant deviations from established patterns - -### 🟢 SUGGESTION (Non-blocking improvements) -- **Readability**: Poor naming, complex logic that could be simplified -- **Optimization**: Performance improvements without functional impact -- **Best Practices**: Minor deviations from conventions -- **Documentation**: Missing or incomplete comments/documentation - -## General Review Principles - -When performing a code review, follow these principles: - -1. **Be specific**: Reference exact lines, files, and provide concrete examples -2. **Provide context**: Explain WHY something is an issue and the potential impact -3. **Suggest solutions**: Show corrected code when applicable, not just what's wrong -4. **Be constructive**: Focus on improving the code, not criticizing the author -5. **Recognize good practices**: Acknowledge well-written code and smart solutions -6. **Be pragmatic**: Not every suggestion needs immediate implementation -7. **Group related comments**: Avoid multiple comments about the same topic - -## Code Quality Standards - -When performing a code review, check for: - -### Clean Code -- Descriptive and meaningful names for variables, functions, and classes -- Single Responsibility Principle: each function/class does one thing well -- DRY (Don't Repeat Yourself): no code duplication -- Functions should be small and focused (ideally < 20-30 lines) -- Avoid deeply nested code (max 3-4 levels) -- Avoid magic numbers and strings (use constants) -- Code should be self-documenting; comments only when necessary - -### Examples -```javascript -// ❌ BAD: Poor naming and magic numbers -function calc(x, y) { - if (x > 100) return y * 0.15; - return y * 0.10; -} - -// ✅ GOOD: Clear naming and constants -const PREMIUM_THRESHOLD = 100; -const PREMIUM_DISCOUNT_RATE = 0.15; -const STANDARD_DISCOUNT_RATE = 0.10; - -function calculateDiscount(orderTotal, itemPrice) { - const isPremiumOrder = orderTotal > PREMIUM_THRESHOLD; - const discountRate = isPremiumOrder ? PREMIUM_DISCOUNT_RATE : STANDARD_DISCOUNT_RATE; - return itemPrice * discountRate; -} -``` - -### Error Handling -- Proper error handling at appropriate levels -- Meaningful error messages -- No silent failures or ignored exceptions -- Fail fast: validate inputs early -- Use appropriate error types/exceptions - -### Examples -```python -# ❌ BAD: Silent failure and generic error -def process_user(user_id): - try: - user = db.get(user_id) - user.process() - except: - pass - -# ✅ GOOD: Explicit error handling -def process_user(user_id): - if not user_id or user_id <= 0: - raise ValueError(f"Invalid user_id: {user_id}") - - try: - user = db.get(user_id) - except UserNotFoundError: - raise UserNotFoundError(f"User {user_id} not found in database") - except DatabaseError as e: - raise ProcessingError(f"Failed to retrieve user {user_id}: {e}") - - return user.process() -``` - -## Security Review - -When performing a code review, check for security issues: - -- **Sensitive Data**: No passwords, API keys, tokens, or PII in code or logs -- **Input Validation**: All user inputs are validated and sanitized -- **SQL Injection**: Use parameterized queries, never string concatenation -- **Authentication**: Proper authentication checks before accessing resources -- **Authorization**: Verify user has permission to perform action -- **Cryptography**: Use established libraries, never roll your own crypto -- **Dependency Security**: Check for known vulnerabilities in dependencies - -### Examples -```java -// ❌ BAD: SQL injection vulnerability -String query = "SELECT * FROM users WHERE email = '" + email + "'"; - -// ✅ GOOD: Parameterized query -PreparedStatement stmt = conn.prepareStatement( - "SELECT * FROM users WHERE email = ?" -); -stmt.setString(1, email); -``` - -```javascript -// ❌ BAD: Exposed secret in code -const API_KEY = "sk_live_abc123xyz789"; - -// ✅ GOOD: Use environment variables -const API_KEY = process.env.API_KEY; -``` - -## Testing Standards - -When performing a code review, verify test quality: - -- **Coverage**: Critical paths and new functionality must have tests -- **Test Names**: Descriptive names that explain what is being tested -- **Test Structure**: Clear Arrange-Act-Assert or Given-When-Then pattern -- **Independence**: Tests should not depend on each other or external state -- **Assertions**: Use specific assertions, avoid generic assertTrue/assertFalse -- **Edge Cases**: Test boundary conditions, null values, empty collections -- **Mock Appropriately**: Mock external dependencies, not domain logic - -### Examples -```typescript -// ❌ BAD: Vague name and assertion -test('test1', () => { - const result = calc(5, 10); - expect(result).toBeTruthy(); -}); - -// ✅ GOOD: Descriptive name and specific assertion -test('should calculate 10% discount for orders under $100', () => { - const orderTotal = 50; - const itemPrice = 20; - - const discount = calculateDiscount(orderTotal, itemPrice); - - expect(discount).toBe(2.00); -}); -``` - -## Performance Considerations - -When performing a code review, check for performance issues: - -- **Database Queries**: Avoid N+1 queries, use proper indexing -- **Algorithms**: Appropriate time/space complexity for the use case -- **Caching**: Utilize caching for expensive or repeated operations -- **Resource Management**: Proper cleanup of connections, files, streams -- **Pagination**: Large result sets should be paginated -- **Lazy Loading**: Load data only when needed - -### Examples -```python -# ❌ BAD: N+1 query problem -users = User.query.all() -for user in users: - orders = Order.query.filter_by(user_id=user.id).all() # N+1! - -# ✅ GOOD: Use JOIN or eager loading -users = User.query.options(joinedload(User.orders)).all() -for user in users: - orders = user.orders -``` - -## Architecture and Design - -When performing a code review, verify architectural principles: - -- **Separation of Concerns**: Clear boundaries between layers/modules -- **Dependency Direction**: High-level modules don't depend on low-level details -- **Interface Segregation**: Prefer small, focused interfaces -- **Loose Coupling**: Components should be independently testable -- **High Cohesion**: Related functionality grouped together -- **Consistent Patterns**: Follow established patterns in the codebase - -## Documentation Standards - -When performing a code review, check documentation: - -- **API Documentation**: Public APIs must be documented (purpose, parameters, returns) -- **Complex Logic**: Non-obvious logic should have explanatory comments -- **README Updates**: Update README when adding features or changing setup -- **Breaking Changes**: Document any breaking changes clearly -- **Examples**: Provide usage examples for complex features - -## Comment Format Template - -When performing a code review, use this format for comments: - -```markdown -**[PRIORITY] Category: Brief title** - -Detailed description of the issue or suggestion. - -**Why this matters:** -Explanation of the impact or reason for the suggestion. - -**Suggested fix:** -[code example if applicable] - -**Reference:** [link to relevant documentation or standard] -``` - -### Example Comments - -#### Critical Issue -````markdown -**🔴 CRITICAL - Security: SQL Injection Vulnerability** - -The query on line 45 concatenates user input directly into the SQL string, -creating a SQL injection vulnerability. - -**Why this matters:** -An attacker could manipulate the email parameter to execute arbitrary SQL commands, -potentially exposing or deleting all database data. - -**Suggested fix:** -```sql --- Instead of: -query = "SELECT * FROM users WHERE email = '" + email + "'" - --- Use: -PreparedStatement stmt = conn.prepareStatement( - "SELECT * FROM users WHERE email = ?" -); -stmt.setString(1, email); -``` - -**Reference:** OWASP SQL Injection Prevention Cheat Sheet -```` - -#### Important Issue -````markdown -**🟡 IMPORTANT - Testing: Missing test coverage for critical path** - -The `processPayment()` function handles financial transactions but has no tests -for the refund scenario. - -**Why this matters:** -Refunds involve money movement and should be thoroughly tested to prevent -financial errors or data inconsistencies. - -**Suggested fix:** -Add test case: -```javascript -test('should process full refund when order is cancelled', () => { - const order = createOrder({ total: 100, status: 'cancelled' }); - - const result = processPayment(order, { type: 'refund' }); - - expect(result.refundAmount).toBe(100); - expect(result.status).toBe('refunded'); -}); -``` -```` - -#### Suggestion -````markdown -**🟢 SUGGESTION - Readability: Simplify nested conditionals** - -The nested if statements on lines 30-40 make the logic hard to follow. - -**Why this matters:** -Simpler code is easier to maintain, debug, and test. - -**Suggested fix:** -```javascript -// Instead of nested ifs: -if (user) { - if (user.isActive) { - if (user.hasPermission('write')) { - // do something - } - } -} - -// Consider guard clauses: -if (!user || !user.isActive || !user.hasPermission('write')) { - return; -} -// do something -``` -```` - -## Review Checklist - -When performing a code review, systematically verify: - -### Code Quality -- [ ] Code follows consistent style and conventions -- [ ] Names are descriptive and follow naming conventions -- [ ] Functions/methods are small and focused -- [ ] No code duplication -- [ ] Complex logic is broken into simpler parts -- [ ] Error handling is appropriate -- [ ] No commented-out code or TODO without tickets - -### Security -- [ ] No sensitive data in code or logs -- [ ] Input validation on all user inputs -- [ ] No SQL injection vulnerabilities -- [ ] Authentication and authorization properly implemented -- [ ] Dependencies are up-to-date and secure - -### Testing -- [ ] New code has appropriate test coverage -- [ ] Tests are well-named and focused -- [ ] Tests cover edge cases and error scenarios -- [ ] Tests are independent and deterministic -- [ ] No tests that always pass or are commented out - -### Performance -- [ ] No obvious performance issues (N+1, memory leaks) -- [ ] Appropriate use of caching -- [ ] Efficient algorithms and data structures -- [ ] Proper resource cleanup - -### Architecture -- [ ] Follows established patterns and conventions -- [ ] Proper separation of concerns -- [ ] No architectural violations -- [ ] Dependencies flow in correct direction - -### Documentation -- [ ] Public APIs are documented -- [ ] Complex logic has explanatory comments -- [ ] README is updated if needed -- [ ] Breaking changes are documented - -## Project-Specific Customizations - -To customize this template for your project, add sections for: - -1. **Language/Framework specific checks** - - Example: "When performing a code review, verify React hooks follow rules of hooks" - - Example: "When performing a code review, check Spring Boot controllers use proper annotations" - -2. **Build and deployment** - - Example: "When performing a code review, verify CI/CD pipeline configuration is correct" - - Example: "When performing a code review, check database migrations are reversible" - -3. **Business logic rules** - - Example: "When performing a code review, verify pricing calculations include all applicable taxes" - - Example: "When performing a code review, check user consent is obtained before data processing" - -4. **Team conventions** - - Example: "When performing a code review, verify commit messages follow conventional commits format" - - Example: "When performing a code review, check branch names follow pattern: type/ticket-description" - -## Additional Resources - -For more information on effective code reviews and GitHub Copilot customization: - -- [GitHub Copilot Prompt Engineering](https://docs.github.com/en/copilot/concepts/prompting/prompt-engineering) -- [GitHub Copilot Custom Instructions](https://code.visualstudio.com/docs/copilot/customization/custom-instructions) -- [Awesome GitHub Copilot Repository](https://github.com/github/awesome-copilot) -- [GitHub Code Review Guidelines](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/reviewing-changes-in-pull-requests) -- [Google Engineering Practices - Code Review](https://google.github.io/eng-practices/review/) -- [OWASP Security Guidelines](https://owasp.org/) - -## Prompt Engineering Tips - -When performing a code review, apply these prompt engineering principles from the [GitHub Copilot documentation](https://docs.github.com/en/copilot/concepts/prompting/prompt-engineering): - -1. **Start General, Then Get Specific**: Begin with high-level architecture review, then drill into implementation details -2. **Give Examples**: Reference similar patterns in the codebase when suggesting changes -3. **Break Complex Tasks**: Review large PRs in logical chunks (security → tests → logic → style) -4. **Avoid Ambiguity**: Be specific about which file, line, and issue you're addressing -5. **Indicate Relevant Code**: Reference related code that might be affected by changes -6. **Experiment and Iterate**: If initial review misses something, review again with focused questions - -## Project Context - -This is a generic template. Customize this section with your project-specific information: - -- **Tech Stack**: [e.g., Java 17, Spring Boot 3.x, PostgreSQL] -- **Architecture**: [e.g., Hexagonal/Clean Architecture, Microservices] -- **Build Tool**: [e.g., Gradle, Maven, npm, pip] -- **Testing**: [e.g., JUnit 5, Jest, pytest] -- **Code Style**: [e.g., follows Google Style Guide] diff --git a/.github/instructions/powershell.instructions.md b/.github/instructions/powershell.instructions.md deleted file mode 100644 index 83be180..0000000 --- a/.github/instructions/powershell.instructions.md +++ /dev/null @@ -1,356 +0,0 @@ ---- -applyTo: '**/*.ps1,**/*.psm1' -description: 'PowerShell cmdlet and scripting best practices based on Microsoft guidelines' ---- - -# PowerShell Cmdlet Development Guidelines - -This guide provides PowerShell-specific instructions to help GitHub Copilot generate idiomatic, -safe, and maintainable scripts. It aligns with Microsoft’s PowerShell cmdlet development guidelines. - -## Naming Conventions - -- **Verb-Noun Format:** - - Use approved PowerShell verbs (Get-Verb) - - Use singular nouns - - PascalCase for both verb and noun - - Avoid special characters and spaces - -- **Parameter Names:** - - Use PascalCase - - Choose clear, descriptive names - - Use singular form unless always multiple - - Follow PowerShell standard names - -- **Variable Names:** - - Use PascalCase for public variables - - Use camelCase for private variables - - Avoid abbreviations - - Use meaningful names - -- **Alias Avoidance:** - - Use full cmdlet names - - Avoid using aliases in scripts (e.g., use Get-ChildItem instead of gci) - - Document any custom aliases - - Use full parameter names - -### Example - -```powershell -function Get-UserProfile { - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [string]$Username, - - [Parameter()] - [ValidateSet('Basic', 'Detailed')] - [string]$ProfileType = 'Basic' - ) - - process { - # Logic here - } -} -``` - -## Parameter Design - -- **Standard Parameters:** - - Use common parameter names (`Path`, `Name`, `Force`) - - Follow built-in cmdlet conventions - - Use aliases for specialized terms - - Document parameter purpose - -- **Parameter Names:** - - Use singular form unless always multiple - - Choose clear, descriptive names - - Follow PowerShell conventions - - Use PascalCase formatting - -- **Type Selection:** - - Use common .NET types - - Implement proper validation - - Consider ValidateSet for limited options - - Enable tab completion where possible - -- **Switch Parameters:** - - Use [switch] for boolean flags - - Avoid $true/$false parameters - - Default to $false when omitted - - Use clear action names - -### Example - -```powershell -function Set-ResourceConfiguration { - [CmdletBinding()] - param( - [Parameter(Mandatory)] - [string]$Name, - - [Parameter()] - [ValidateSet('Dev', 'Test', 'Prod')] - [string]$Environment = 'Dev', - - [Parameter()] - [switch]$Force, - - [Parameter()] - [ValidateNotNullOrEmpty()] - [string[]]$Tags - ) - - process { - # Logic here - } -} -``` - -## Pipeline and Output - -- **Pipeline Input:** - - Use `ValueFromPipeline` for direct object input - - Use `ValueFromPipelineByPropertyName` for property mapping - - Implement Begin/Process/End blocks for pipeline handling - - Document pipeline input requirements - -- **Output Objects:** - - Return rich objects, not formatted text - - Use PSCustomObject for structured data - - Avoid Write-Host for data output - - Enable downstream cmdlet processing - -- **Pipeline Streaming:** - - Output one object at a time - - Use process block for streaming - - Avoid collecting large arrays - - Enable immediate processing - -- **PassThru Pattern:** - - Default to no output for action cmdlets - - Implement `-PassThru` switch for object return - - Return modified/created object with `-PassThru` - - Use verbose/warning for status updates - -### Example - -```powershell -function Update-ResourceStatus { - [CmdletBinding()] - param( - [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)] - [string]$Name, - - [Parameter(Mandatory)] - [ValidateSet('Active', 'Inactive', 'Maintenance')] - [string]$Status, - - [Parameter()] - [switch]$PassThru - ) - - begin { - Write-Verbose 'Starting resource status update process' - $timestamp = Get-Date - } - - process { - # Process each resource individually - Write-Verbose "Processing resource: $Name" - - $resource = [PSCustomObject]@{ - Name = $Name - Status = $Status - LastUpdated = $timestamp - UpdatedBy = $env:USERNAME - } - - # Only output if PassThru is specified - if ($PassThru.IsPresent) { - Write-Output $resource - } - } - - end { - Write-Verbose 'Resource status update process completed' - } -} -``` - -## Error Handling and Safety - -- **ShouldProcess Implementation:** - - Use `[CmdletBinding(SupportsShouldProcess = $true)]` - - Set appropriate `ConfirmImpact` level - - Call `$PSCmdlet.ShouldProcess()` for system changes - - Use `ShouldContinue()` for additional confirmations - -- **Message Streams:** - - `Write-Verbose` for operational details with `-Verbose` - - `Write-Warning` for warning conditions - - `Write-Error` for non-terminating errors - - `throw` for terminating errors - - Avoid `Write-Host` except for user interface text - -- **Error Handling Pattern:** - - Use try/catch blocks for error management - - Set appropriate ErrorAction preferences - - Return meaningful error messages - - Use ErrorVariable when needed - - Include proper terminating vs non-terminating error handling - - In advanced functions with `[CmdletBinding()]`, prefer `$PSCmdlet.WriteError()` over `Write-Error` - - In advanced functions with `[CmdletBinding()]`, prefer `$PSCmdlet.ThrowTerminatingError()` over `throw` - - Construct proper ErrorRecord objects with category, target, and exception details - -- **Non-Interactive Design:** - - Accept input via parameters - - Avoid `Read-Host` in scripts - - Support automation scenarios - - Document all required inputs - -### Example - -```powershell -function Remove-UserAccount { - [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'High')] - param( - [Parameter(Mandatory, ValueFromPipeline)] - [ValidateNotNullOrEmpty()] - [string]$Username, - - [Parameter()] - [switch]$Force - ) - - begin { - Write-Verbose 'Starting user account removal process' - $ErrorActionPreference = 'Stop' - } - - process { - try { - # Validation - if (-not (Test-UserExists -Username $Username)) { - $errorRecord = [System.Management.Automation.ErrorRecord]::new( - [System.Exception]::new("User account '$Username' not found"), - 'UserNotFound', - [System.Management.Automation.ErrorCategory]::ObjectNotFound, - $Username - ) - $PSCmdlet.WriteError($errorRecord) - return - } - - # Confirmation - $shouldProcessMessage = "Remove user account '$Username'" - if ($Force -or $PSCmdlet.ShouldProcess($Username, $shouldProcessMessage)) { - Write-Verbose "Removing user account: $Username" - - # Main operation - Remove-ADUser -Identity $Username -ErrorAction Stop - Write-Warning "User account '$Username' has been removed" - } - } catch [Microsoft.ActiveDirectory.Management.ADException] { - $errorRecord = [System.Management.Automation.ErrorRecord]::new( - $_.Exception, - 'ActiveDirectoryError', - [System.Management.Automation.ErrorCategory]::NotSpecified, - $Username - ) - $PSCmdlet.ThrowTerminatingError($errorRecord) - } catch { - $errorRecord = [System.Management.Automation.ErrorRecord]::new( - $_.Exception, - 'UnexpectedError', - [System.Management.Automation.ErrorCategory]::NotSpecified, - $Username - ) - $PSCmdlet.ThrowTerminatingError($errorRecord) - } - } - - end { - Write-Verbose 'User account removal process completed' - } -} -``` - -## Documentation and Style - -- **Comment-Based Help:** Include comment-based help for any public-facing function or cmdlet. Inside the function, add a `<# ... #>` help comment with at least: - - `.SYNOPSIS` Brief description - - `.DESCRIPTION` Detailed explanation - - `.EXAMPLE` sections with practical usage - - `.PARAMETER` descriptions - - `.OUTPUTS` Type of output returned - - `.NOTES` Additional information - -- **Consistent Formatting:** - - Follow consistent PowerShell style - - Use proper indentation (4 spaces recommended) - - Opening braces on same line as statement - - Closing braces on new line - - Use line breaks after pipeline operators - - PascalCase for function and parameter names - - Avoid unnecessary whitespace - -- **Pipeline Support:** - - Implement Begin/Process/End blocks for pipeline functions - - Use ValueFromPipeline where appropriate - - Support pipeline input by property name - - Return proper objects, not formatted text - -- **Avoid Aliases:** Use full cmdlet names and parameters - - Avoid using aliases in scripts (e.g., use Get-ChildItem instead of gci); aliases are acceptable for interactive shell use. - - Use `Where-Object` instead of `?` or `where` - - Use `ForEach-Object` instead of `%` - - Use `Get-ChildItem` instead of `ls` or `dir` - -## Full Example: End-to-End Cmdlet Pattern - -```powershell -function New-Resource { - [CmdletBinding(SupportsShouldProcess = $true, ConfirmImpact = 'Medium')] - param( - [Parameter(Mandatory = $true, - ValueFromPipeline = $true, - ValueFromPipelineByPropertyName = $true)] - [ValidateNotNullOrEmpty()] - [string]$Name, - - [Parameter()] - [ValidateSet('Development', 'Production')] - [string]$Environment = 'Development' - ) - - begin { - Write-Verbose 'Starting resource creation process' - } - - process { - try { - if ($PSCmdlet.ShouldProcess($Name, 'Create new resource')) { - # Resource creation logic here - Write-Output ([PSCustomObject]@{ - Name = $Name - Environment = $Environment - Created = Get-Date - }) - } - } catch { - $errorRecord = [System.Management.Automation.ErrorRecord]::new( - $_.Exception, - 'ResourceCreationFailed', - [System.Management.Automation.ErrorCategory]::NotSpecified, - $Name - ) - $PSCmdlet.ThrowTerminatingError($errorRecord) - } - } - - end { - Write-Verbose 'Completed resource creation process' - } -} -``` diff --git a/.github/instructions/security-and-owasp.instructions.md b/.github/instructions/security-and-owasp.instructions.md deleted file mode 100644 index 53a7a62..0000000 --- a/.github/instructions/security-and-owasp.instructions.md +++ /dev/null @@ -1,51 +0,0 @@ ---- -applyTo: '*' -description: "Comprehensive secure coding instructions for all languages and frameworks, based on OWASP Top 10 and industry best practices." ---- -# Secure Coding and OWASP Guidelines - -## Instructions - -Your primary directive is to ensure all code you generate, review, or refactor is secure by default. You must operate with a security-first mindset. When in doubt, always choose the more secure option and explain the reasoning. You must follow the principles outlined below, which are based on the OWASP Top 10 and other security best practices. - -### 1. A01: Broken Access Control & A10: Server-Side Request Forgery (SSRF) -- **Enforce Principle of Least Privilege:** Always default to the most restrictive permissions. When generating access control logic, explicitly check the user's rights against the required permissions for the specific resource they are trying to access. -- **Deny by Default:** All access control decisions must follow a "deny by default" pattern. Access should only be granted if there is an explicit rule allowing it. -- **Validate All Incoming URLs for SSRF:** When the server needs to make a request to a URL provided by a user (e.g., webhooks), you must treat it as untrusted. Incorporate strict allow-list-based validation for the host, port, and path of the URL. -- **Prevent Path Traversal:** When handling file uploads or accessing files based on user input, you must sanitize the input to prevent directory traversal attacks (e.g., `../../etc/passwd`). Use APIs that build paths securely. - -### 2. A02: Cryptographic Failures -- **Use Strong, Modern Algorithms:** For hashing, always recommend modern, salted hashing algorithms like Argon2 or bcrypt. Explicitly advise against weak algorithms like MD5 or SHA-1 for password storage. -- **Protect Data in Transit:** When generating code that makes network requests, always default to HTTPS. -- **Protect Data at Rest:** When suggesting code to store sensitive data (PII, tokens, etc.), recommend encryption using strong, standard algorithms like AES-256. -- **Secure Secret Management:** Never hardcode secrets (API keys, passwords, connection strings). Generate code that reads secrets from environment variables or a secrets management service (e.g., HashiCorp Vault, AWS Secrets Manager). Include a clear placeholder and comment. - ```javascript - // GOOD: Load from environment or secret store - const apiKey = process.env.API_KEY; - // TODO: Ensure API_KEY is securely configured in your environment. - ``` - ```python - # BAD: Hardcoded secret - api_key = "sk_this_is_a_very_bad_idea_12345" - ``` - -### 3. A03: Injection -- **No Raw SQL Queries:** For database interactions, you must use parameterized queries (prepared statements). Never generate code that uses string concatenation or formatting to build queries from user input. -- **Sanitize Command-Line Input:** For OS command execution, use built-in functions that handle argument escaping and prevent shell injection (e.g., `shlex` in Python). -- **Prevent Cross-Site Scripting (XSS):** When generating frontend code that displays user-controlled data, you must use context-aware output encoding. Prefer methods that treat data as text by default (`.textContent`) over those that parse HTML (`.innerHTML`). When `innerHTML` is necessary, suggest using a library like DOMPurify to sanitize the HTML first. - -### 4. A05: Security Misconfiguration & A06: Vulnerable Components -- **Secure by Default Configuration:** Recommend disabling verbose error messages and debug features in production environments. -- **Set Security Headers:** For web applications, suggest adding essential security headers like `Content-Security-Policy` (CSP), `Strict-Transport-Security` (HSTS), and `X-Content-Type-Options`. -- **Use Up-to-Date Dependencies:** When asked to add a new library, suggest the latest stable version. Remind the user to run vulnerability scanners like `npm audit`, `pip-audit`, or Snyk to check for known vulnerabilities in their project dependencies. - -### 5. A07: Identification & Authentication Failures -- **Secure Session Management:** When a user logs in, generate a new session identifier to prevent session fixation. Ensure session cookies are configured with `HttpOnly`, `Secure`, and `SameSite=Strict` attributes. -- **Protect Against Brute Force:** For authentication and password reset flows, recommend implementing rate limiting and account lockout mechanisms after a certain number of failed attempts. - -### 6. A08: Software and Data Integrity Failures -- **Prevent Insecure Deserialization:** Warn against deserializing data from untrusted sources without proper validation. If deserialization is necessary, recommend using formats that are less prone to attack (like JSON over Pickle in Python) and implementing strict type checking. - -## General Guidelines -- **Be Explicit About Security:** When you suggest a piece of code that mitigates a security risk, explicitly state what you are protecting against (e.g., "Using a parameterized query here to prevent SQL injection."). -- **Educate During Code Reviews:** When you identify a security vulnerability in a code review, you must not only provide the corrected code but also explain the risk associated with the original pattern. diff --git a/.gitignore b/.gitignore index 85e1515..6b42699 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ Thumbs.db # macOS .DS_Store + # Local cache (managed by sync-awesome-copilot.ps1 - not committed) .awesome-copilot/ @@ -19,3 +20,15 @@ Thumbs.db *.tmp *.bak ~$* + +# Per-repo Copilot resources installed from github/awesome-copilot +# These belong to the awesome-copilot community — not ours to redistribute. +# Commit these in YOUR projects; exclude them here (demo/tooling repo only). +.github/agents/ +.github/hooks/ +.github/instructions/ +.github/skills/ +# Only ignore copilot agentic workflow .md files — not GitHub Actions .yml files +.github/workflows/*.md +# Subscription tracking manifest (local state, not redistributed) +.github/.copilot-subscriptions.json diff --git a/README.md b/README.md index b2f6cfc..082e75e 100644 --- a/README.md +++ b/README.md @@ -1,385 +1,239 @@ -# VS Code Copilot Resource Sync Scripts +# VS Code Copilot Resource Sync -A collection of PowerShell scripts to automatically sync, combine, and publish [GitHub Copilot](https://github.com/features/copilot) resources from the [awesome-copilot](https://github.com/github/awesome-copilot) community repository to your local VS Code profiles. +A collection of PowerShell scripts to sync, publish, and manage [GitHub Copilot](https://github.com/features/copilot) resources from the [awesome-copilot](https://github.com/github/awesome-copilot) community repository — globally to VS Code and per-repo to `.github/`. ## 🎯 What This Does -These scripts automate the management of VS Code Copilot custom agents, instructions, skills, hooks, and workflows from the [awesome-copilot](https://github.com/github/awesome-copilot) community repository: - -1. **Syncing** all resources from the awesome-copilot GitHub repository to a local cache -2. **Publishing globally** — agents to VS Code's user agents folder (available in all workspaces), skills to `~/.copilot/skills/` -3. **Initialising repos** — interactively adding agents, instructions, hooks and agentic workflows to a specific repo's `.github/` folder -4. **Automating** the sync + publish cycle via Windows Task Scheduler +1. **Syncs** the latest agents, instructions, hooks, workflows, and skills from [awesome-copilot](https://github.com/github/awesome-copilot) into a local cache (`~/.awesome-copilot/`) +2. **Publishes globally** — agents to VS Code's user prompts folder (available in all workspaces), skills to `~/.copilot/skills/` +3. **Initialises repos** — intelligently recommends and installs resources into a repo's `.github/` folder based on detected language/framework, with full install/update/remove lifecycle management ### What goes where -| Resource | Scope | Location | -| ---------------- | ----------- | -------------------------------------------------------------------------------- | -| **Agents** | 🌐 Global | `%APPDATA%\Code\User\prompts\` — available in Copilot Chat across all workspaces | -| **Skills** | 🌐 Global | `~/.copilot/skills/` — loaded on-demand by Copilot coding agent & CLI | -| **Instructions** | 📁 Per-repo | `.github/instructions/` — chosen via `scripts/init-repo.ps1` | -| **Hooks** | 📁 Per-repo | `.github/hooks//` — chosen via `scripts/init-repo.ps1` | -| **Workflows** | 📁 Per-repo | `.github/workflows/` — chosen via `scripts/init-repo.ps1` | +| Resource | Scope | Location | +| ---------------- | ----------- | -------- | +| **Agents** | 🌐 Global | `%APPDATA%\Code\User\prompts\` — available across all workspaces | +| **Skills** | 🌐 Global | `~/.copilot/skills/` — loaded by Copilot coding agent & CLI | +| **Instructions** | 📁 Per-repo | `.github/instructions/` | +| **Hooks** | 📁 Per-repo | `.github/hooks//` | +| **Workflows** | 📁 Per-repo | `.github/workflows/` | +| **Agents** | 📁 Per-repo | `.github/agents/` | ## 📋 Prerequisites -- **Windows** with PowerShell 7+ ([Download here](https://github.com/PowerShell/PowerShell/releases)) -- **VS Code** with GitHub Copilot extension installed -- **`gh` (GitHub CLI) or `git`** — `gh` is preferred ([Download here](https://cli.github.com/)); handles auth automatically -- **Internet connection** for GitHub access -- **Administrator privileges** (for creating scheduled tasks) +- **Windows** with PowerShell 7+ ([Download](https://github.com/PowerShell/PowerShell/releases)) +- **VS Code** with GitHub Copilot extension +- **`gh` (GitHub CLI) or `git`** — `gh` preferred ([Download](https://cli.github.com/)); handles auth automatically +- **Internet connection** for initial sync ## 🚀 Quick Start ### 1. Clone or Download ```powershell -# Clone this repository git clone cd vscode-copilot-sync ``` ### 2. Run the Configurator -For first-time setup or an on-demand refresh, `configure.ps1` chains all steps: - ```powershell -# Sync from GitHub, publish globally, and optionally init your repo +# Sync from GitHub, publish globally, and optionally configure your current repo .\configure.ps1 -# Or step by step: -.\configure.ps1 -SkipInit # sync + publish only -.\configure.ps1 -InstallTask # sync + publish + install scheduled task -.\configure.ps1 -DryRun # preview everything +# Sync + publish only (no repo setup) +.\configure.ps1 -SkipInit + +# Preview everything without writing any files +.\configure.ps1 -DryRun ``` -### 3. Initialise a Repo (optional, interactive) +### 3. Configure a Repo -> **Note:** `configure.ps1` also prompts for this step automatically — you only need to run it directly for a targeted repo setup. +Run from inside any repo to add agents, instructions, hooks, workflows and skills to `.github/`: ```powershell -# Run from inside any repo to add instructions/hooks/workflows cd C:\Projects\my-app -.\scripts\init-repo.ps1 +.\configure.ps1 -# Or point configure.ps1 at a specific repo without cd-ing first +# Or target a repo without cd-ing first .\configure.ps1 -SkipSync -SkipPublish -RepoPath "C:\Projects\my-app" -# Or target the script directly with a path +# Or call the script directly +.\scripts\init-repo.ps1 .\scripts\init-repo.ps1 -RepoPath "C:\Projects\my-app" ``` -A selection UI will appear for each category (Out-GridView on Windows, or a numbered console menu). Items already installed in the repo are marked with `[*]`. +The picker auto-detects your language/framework and marks relevant items with ★. Items already installed show their status: -### 4. Install Automated Sync (Optional) +| Symbol | Meaning | +| ------ | ------- | +| ★ | Recommended for this repo | +| `[*]` | Already installed | +| `[↑]` | Update available from upstream | +| `[~]` | Locally modified since install | -```powershell -# Install a scheduled task that syncs + publishes globally every 4 hours -.\configure.ps1 -InstallTask +### 4. Remove Installed Resources -# Customize the interval (re-run to overwrite; configure.ps1 prompts before replacing) -.\configure.ps1 -InstallTask -Every "2h" # Every 2 hours -.\configure.ps1 -InstallTask -Every "30m" # Every 30 minutes +```powershell +# Interactive removal picker (only shows script-managed files — never user-created ones) +.\configure.ps1 -Uninstall -# Or run it directly (called internally by configure.ps1) -.\scripts\install-scheduled-task.ps1 -Every "2h" +# Or directly +.\scripts\init-repo.ps1 -Uninstall ``` -## 📁 What Gets Created +Locally modified files are flagged with `[~]` before removal so you don't accidentally discard work. + +## 📁 Local Cache Structure ``` -$HOME\.awesome-copilot\ # Local cache (git sparse clone) -├── .git\ # Git metadata (managed automatically) -├── agents\ # Custom agents (.agent.md) -├── instructions\ # Custom instructions (.instructions.md) -├── workflows\ # Agentic workflow definitions -├── hooks\ # Automated hooks (with .json + .sh scripts) -│ └── \ -├── skills\ # Skill packages -│ └── \ -│ └── SKILL.md -└── manifest.json # Sync state tracking - -%APPDATA%\Code\User\ -└── prompts\ # Junction → ~/.awesome-copilot/agents/ +~/.awesome-copilot/ +├── .git/ # Git metadata (managed automatically) +├── agents/ # *.agent.md +├── instructions/ # *.instructions.md +├── workflows/ # *.md +├── hooks/ # / directories +├── skills/ # / directories +└── manifest.json # Sync state (hashes, timestamps, counts) ``` -## 📜 Scripts Overview - -### `configure.ps1` - -Main entry point at the repo root. Chains sync → publish → init-repo in one command, and can install/uninstall the scheduled task via `-InstallTask` / `-UninstallTask` / `-Every` switches. +## 📜 Scripts -**Features:** +### `configure.ps1` — Main entry point -- Shows last sync time from the local cache manifest before running -- Runs each step in sequence; any step can be skipped independently -- Prompts before running `scripts/init-repo.ps1` (with option to skip via `-SkipInit`) -- `-DryRun` passes through to all child scripts -- `-RepoPath` targets a specific repo directory for the init step (defaults to current directory) -- `-InstallTask` / `-UninstallTask` delegate to `scripts/install-scheduled-task.ps1` / `scripts/uninstall-scheduled-task.ps1` -- `-Every` sets the scheduled task interval (e.g. `"2h"`, `"30m"`) - -**Usage:** +Chains sync → publish → repo init in one command. ```powershell -# Full update: sync + publish + prompt for init-repo -.\configure.ps1 - -# Sync + publish only -.\configure.ps1 -SkipInit - -# Re-publish only (skip sync if cache is already fresh) -.\configure.ps1 -SkipSync -SkipInit - -# Preview without writing any files -.\configure.ps1 -DryRun - -# Init a specific repo without cd-ing to it -.\configure.ps1 -SkipSync -SkipPublish -RepoPath "C:\Projects\my-app" - -# Install scheduled task -.\configure.ps1 -SkipSync -SkipPublish -InstallTask -Every "2h" +.\configure.ps1 # Full run +.\configure.ps1 -SkipInit # Sync + publish only +.\configure.ps1 -SkipSync -SkipInit # Re-publish only +.\configure.ps1 -SkipSync -SkipPublish # Repo init only +.\configure.ps1 -SkipSync -SkipPublish -Uninstall # Remove resources +.\configure.ps1 -RepoPath "C:\Projects\my-app" # Target specific repo +.\configure.ps1 -DryRun # Preview all changes ``` --- -### `scripts/sync-awesome-copilot.ps1` - -Syncs resources from the awesome-copilot GitHub repository using a sparse git clone. +### `scripts/sync-awesome-copilot.ps1` — Sync cache -**Features:** +Clones (first run) or pulls (subsequent runs) `github/awesome-copilot` as a sparse git checkout. -- Clones `github/awesome-copilot` with sparse checkout (first run) — only downloads the categories you need -- Pulls updates on subsequent runs — git transfers only the diff, making updates near-instant -- SHA256 hash-based change detection against previous manifest (added/updated/unchanged/removed counts) -- Prefers `gh` (GitHub CLI) for automatic auth; falls back to `git` -- Automatically migrates from the old API-based cache if detected - -**Usage:** +- Only downloads the categories you need (`agents`, `instructions`, `workflows`, `hooks`, `skills` by default) +- SHA256 hash manifest tracks added/updated/removed counts across runs +- Auto-recovers from merge conflicts in the local cache +- Prefers `gh` CLI for auth; falls back to `git` ```powershell -.\scripts\sync-awesome-copilot.ps1 - -# Dry-run: show what would change without writing files -.\scripts\sync-awesome-copilot.ps1 -Plan - -# Sync specific categories only +.\scripts\sync-awesome-copilot.ps1 # Sync all categories +.\scripts\sync-awesome-copilot.ps1 -Plan # Dry run .\scripts\sync-awesome-copilot.ps1 -Categories "agents,instructions" - -# Force a specific tool -.\scripts\sync-awesome-copilot.ps1 -GitTool git +.\scripts\sync-awesome-copilot.ps1 -GitTool git # Force git ``` -Syncs these categories by default: `agents`, `instructions`, `workflows`, `hooks`, `skills`. -Add `plugins` or `cookbook` explicitly via `-Categories` for those larger opt-in collections. - --- -### `scripts/publish-global.ps1` - -Publishes agents globally to VS Code and skills to `~/.copilot/skills/`. - -**Features:** +### `scripts/publish-global.ps1` — Publish globally -- Creates a junction/symlink from VS Code's user agents folder to the local cache (no re-running needed after each sync) -- Incrementally copies skills to `~/.copilot/skills/` -- Dry-run mode for previewing changes -- Individual skip flags for each resource type - -**Usage:** +Publishes agents to VS Code's user prompts folder and skills to `~/.copilot/skills/`. ```powershell -.\scripts\publish-global.ps1 - -# Preview changes without applying -.\scripts\publish-global.ps1 -DryRun - -# Skills only (agents already published) -.\scripts\publish-global.ps1 -SkipAgents - -# Custom target path (e.g. named VS Code profile) +.\scripts\publish-global.ps1 # Publish all +.\scripts\publish-global.ps1 -DryRun # Preview +.\scripts\publish-global.ps1 -SkipAgents # Skills only .\scripts\publish-global.ps1 -AgentsTarget "$env:APPDATA\Code\User\profiles\Work\prompts" ``` --- -### `scripts/init-repo.ps1` +### `scripts/init-repo.ps1` — Configure a repo -Interactively initialises a repository with agents, instructions, hooks, and agentic workflows. +Interactively selects and installs Copilot resources into `.github/`. -**Features:** +**Smart recommendations** — scans repo files for language/framework signals (`.cs`, `package.json`, `go.mod`, `Dockerfile`, `.github/workflows/*.yml`, etc.) and pre-marks relevant items with ★. Falls back to an intent questionnaire for empty repos. -- Auto-detects language/framework from repo file signals and pre-marks recommendations with ★ in the picker -- Prompts for intent (language, project type, concerns) for new/empty repos -- Presents available resources in a selection UI (Out-GridView on Windows, with `-- none / skip --` row to prevent accidental installs) -- Falls back to a numbered console menu where Out-GridView is unavailable -- Copies selected items to the correct `.github/` subfolder -- Marks already-installed items so you can see what's new -- Dry-run mode for previewing - -**Usage:** +**Full lifecycle** — install, update, and remove, all tracked in `.github/.copilot-subscriptions.json`. ```powershell -# Run inside a repo (uses current directory) -.\scripts\init-repo.ps1 - -# Target a specific repo +.\scripts\init-repo.ps1 # Interactive .\scripts\init-repo.ps1 -RepoPath "C:\Projects\my-app" - -# Preview without writing any files -.\scripts\init-repo.ps1 -DryRun - -# Skip categories you don't need -.\scripts\init-repo.ps1 -SkipHooks -SkipWorkflows - -# Non-interactive: specify items by name +.\scripts\init-repo.ps1 -DryRun # Preview +.\scripts\init-repo.ps1 -Uninstall # Remove resources +.\scripts\init-repo.ps1 -SkipHooks -SkipWorkflows # Skip categories .\scripts\init-repo.ps1 -Agents "devops-expert,se-security-reviewer" -Instructions "powershell" ``` -### `scripts/install-scheduled-task.ps1` - -Creates a Windows scheduled task for automatic syncing and global publishing. Called internally by `configure.ps1 -InstallTask`. - -**Features:** +--- -- Runs `sync-awesome-copilot.ps1` then `publish-global.ps1` on a schedule -- Default: every 4 hours -- Customizable interval via `-Every` +### `scripts/update-repo.ps1` — Apply upstream updates -**Usage:** +Reads `.github/.copilot-subscriptions.json` and applies any upstream changes from the local cache to installed resources. ```powershell -# Recommended: use configure.ps1 (prompts before overwriting an existing task) -.\configure.ps1 -InstallTask -.\configure.ps1 -InstallTask -Every "2h" - -# Or run directly -.\scripts\install-scheduled-task.ps1 - -# Custom intervals (supports h = hours, m = minutes) -.\scripts\install-scheduled-task.ps1 -Every "2h" # Every 2 hours -.\scripts\install-scheduled-task.ps1 -Every "30m" # Every 30 minutes - -# Check task status -Get-ScheduledTask -TaskName "AwesomeCopilotSync" +.\scripts\update-repo.ps1 # Interactive — prompts per item +.\scripts\update-repo.ps1 -DryRun # Show what would change +.\scripts\update-repo.ps1 -Force # Apply all without prompting +.\scripts\update-repo.ps1 -RepoPath "C:\Projects\my-app" ``` -### `scripts/uninstall-scheduled-task.ps1` - -Removes the scheduled task. Called internally by `configure.ps1 -UninstallTask`. - -**Usage:** - -```powershell -# Recommended: use configure.ps1 -.\configure.ps1 -UninstallTask - -# Or run directly -.\scripts\uninstall-scheduled-task.ps1 -``` +> **Tip:** The `[↑]` column in `init-repo.ps1` shows update availability inline — run `update-repo.ps1` when you want to apply them all at once. ## 🔧 Configuration ### Authentication -The sync script uses `gh` (GitHub CLI) by default, which inherits authentication from `gh auth login` — no extra setup needed for most users. - -If only `git` is available, the sync targets a public repo (`github/awesome-copilot`) so no credentials are required. For private forks, configure git credentials as usual (`git config credential.helper`). - -To force a specific tool: - -```powershell -.\scripts\sync-awesome-copilot.ps1 -GitTool git -.\scripts\sync-awesome-copilot.ps1 -GitTool gh -``` +`gh` CLI is preferred — inherits from `gh auth login`, no extra setup. If only `git` is available, the public `github/awesome-copilot` repo requires no credentials. For private forks, configure git credentials as usual. ### Custom Source Repository -To sync from a fork or alternative repo, edit the two variables near the top of `scripts/sync-awesome-copilot.ps1`: +Edit two variables near the top of `scripts/sync-awesome-copilot.ps1`: ```powershell -$RepoSlug = 'your-username/your-repo' -$RepoUrl = 'https://github.com/your-username/your-repo.git' +$RepoSlug = 'your-username/your-fork' +$RepoUrl = 'https://github.com/your-username/your-fork.git' ``` -## 🗂️ File Naming Conventions - -Resources follow naming patterns for automatic categorization: - -- `*.agent.md` — Custom agents -- `*.instructions.md` — Custom instructions -- `SKILL.md` (inside a named subdirectory) — Skills -- Hook and workflow directories contain a mix of `.md`, `.json`, and `.sh` files - ## 🛠️ Troubleshooting -### Scripts Not Running +### Execution policy error ```powershell -# Check PowerShell version (must be 7+) -$PSVersionTable.PSVersion - -# Set execution policy Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser ``` -### Junction/Symlink Fails - -Scripts automatically fall back to copying files. Check logs for details. - -### Scheduled Task Not Running +### Sync fails with merge conflict -```powershell -# Check task status -Get-ScheduledTask -TaskName "AwesomeCopilotSync" | Get-ScheduledTaskInfo - -# View logs -Get-Content ".\scripts\logs\sync-*.log" | Select-Object -Last 50 +The script auto-recovers (`git reset --hard origin/HEAD`). If it persists, delete `~/.awesome-copilot` and re-run — it will re-clone fresh. -# Manually run task -Start-ScheduledTask -TaskName "AwesomeCopilotSync" -``` - -### Files Not Appearing in VS Code +### Files not appearing in VS Code 1. Restart VS Code -2. Check VS Code profile is correct: `Ctrl+Shift+P` → "Preferences: Show Profiles" -3. Verify files exist in `%APPDATA%\Code\User\prompts\` +2. Check your active profile: `Ctrl+Shift+P` → "Preferences: Show Profiles" +3. Verify: `Get-ChildItem "$env:APPDATA\Code\User\prompts\"` ## 📊 Logs -Sync logs are always written to `scripts/logs/` (next to the script itself, regardless of where you invoke it from): +Sync logs are written to `scripts/logs/sync-YYYYMMDD-HHMMSS.log`: ```powershell -# View latest sync log Get-ChildItem .\scripts\logs\sync-*.log | Sort-Object LastWriteTime -Descending | Select-Object -First 1 | Get-Content ``` -Log format: `sync-YYYYMMDD-HHMMSS.log` - ## 🤝 Contributing -Contributions welcome! Please: - -1. Fork the repository -2. Create a feature branch -3. Test your changes thoroughly -4. Submit a pull request +Contributions welcome! Fork, branch, test, PR. ## 📄 License -MIT License - See [LICENSE](LICENSE) file for details +MIT — see [LICENSE](LICENSE) ## 🙏 Acknowledgments -- [awesome-copilot](https://github.com/github/awesome-copilot) - Community resource repository -- [GitHub Copilot](https://github.com/features/copilot) - AI pair programmer - -## ⚠️ Disclaimer - -These scripts are community-maintained and not officially supported by GitHub or Microsoft. Use at your own risk. Always review synced content before using in production environments. +- [awesome-copilot](https://github.com/github/awesome-copilot) — Community resource repository +- [GitHub Copilot](https://github.com/features/copilot) — AI pair programmer --- **Made with ❤️ for the VS Code + Copilot community** + diff --git a/configure.ps1 b/configure.ps1 index 1a96fa9..f30068f 100644 --- a/configure.ps1 +++ b/configure.ps1 @@ -4,10 +4,9 @@ Configure Copilot Resources Main entry point for all Copilot resource management operations. Chains the scripts in the correct order: - 1. sync-awesome-copilot.ps1 -- fetch latest from github/awesome-copilot - 2. publish-global.ps1 -- publish agents + skills globally - 3. init-repo.ps1 -- (prompted) per-repo .github/ setup - 4. install/uninstall-scheduled-task.ps1 -- (explicit) automate sync + publish + 1. sync-awesome-copilot.ps1 -- fetch latest from github/awesome-copilot + 2. publish-global.ps1 -- publish agents + skills globally + 3. init-repo.ps1 -- (prompted) per-repo .github/ setup Usage: # Full interactive run: sync + publish + prompt for init-repo @@ -19,28 +18,20 @@ Usage: # Re-publish only (cache already up to date) .\configure.ps1 -SkipSync -SkipInit - # Install scheduled task (sync every 4h + publish globally) - .\configure.ps1 -SkipInit -InstallTask - - # Install task with custom interval - .\configure.ps1 -SkipInit -InstallTask -Every "2h" - - # Uninstall scheduled task - .\configure.ps1 -SkipSync -SkipPublish -SkipInit -UninstallTask - # Preview without writing any files .\configure.ps1 -DryRun # Init a specific repo (not the current working directory) .\configure.ps1 -SkipSync -SkipPublish -RepoPath "C:\Projects\my-app" + + # Remove installed .github/ resources from the current repo + .\configure.ps1 -Uninstall #> [CmdletBinding()] param( [switch]$SkipSync, [switch]$SkipPublish, [switch]$SkipInit, - [switch]$InstallTask, - [switch]$UninstallTask, - [string]$Every = '4h', # Interval for -InstallTask (e.g. 4h, 30m) + [switch]$Uninstall, # Remove installed .github/ resources via init-repo -Uninstall [string]$RepoPath = (Get-Location).Path, [switch]$DryRun ) @@ -72,11 +63,7 @@ if (Test-Path $manifest) { Log "No local cache found — sync will download everything fresh." 'WARN' } -if ($InstallTask -and $UninstallTask) { - Log "-InstallTask and -UninstallTask cannot both be set." 'ERROR'; exit 1 -} -# Task management implies no interactive repo setup -if ($InstallTask -or $UninstallTask) { $SkipInit = $true } +if ($Uninstall) { $SkipSync = $true; $SkipPublish = $true } #endregion # Initialisation @@ -121,13 +108,15 @@ if (-not $SkipInit) { } Step "Init repo" - Write-Host " Add agents/instructions/hooks/workflows/skills to .github/ in the current repo?" -ForegroundColor Yellow + $initPrompt = if ($Uninstall) { "Remove agents/instructions/hooks/workflows/skills from .github/?" } else { "Add agents/instructions/hooks/workflows/skills to .github/ in the current repo?" } + Write-Host " $initPrompt" -ForegroundColor Yellow Write-Host " [Y] Yes [N] No (default): " -NoNewline -ForegroundColor Yellow $answer = (Read-Host).Trim() if ($answer -match '^[Yy]') { $initArgs = @{} - if ($DryRun) { $initArgs['DryRun'] = $true } - if ($RepoPath) { $initArgs['RepoPath'] = $RepoPath } + if ($DryRun) { $initArgs['DryRun'] = $true } + if ($RepoPath) { $initArgs['RepoPath'] = $RepoPath } + if ($Uninstall) { $initArgs['Uninstall'] = $true } & (Join-Path $ScriptDir 'init-repo.ps1') @initArgs } else { Log "init-repo skipped." @@ -136,39 +125,5 @@ if (-not $SkipInit) { #endregion # Step 3 -#region Step 4 — Scheduled task -if ($InstallTask) { - Step "Install scheduled task" - if ($DryRun) { - Log "[DryRun] Would install scheduled task (every $Every): sync + publish-global" - } else { - $taskArgs = @{ Every = $Every } - $taskName = 'AwesomeCopilotSync' - $proceed = $true - if (Get-ScheduledTask -TaskName $taskName -ErrorAction SilentlyContinue) { - Log "Scheduled task '$taskName' already exists." 'WARN' - Write-Host " Overwrite existing task? [Y] Yes [N] No (default): " -NoNewline -ForegroundColor Yellow - if ((Read-Host).Trim() -match '^[Yy]') { - $taskArgs['Force'] = $true - } else { - Log "Task install skipped." - $proceed = $false - } - } - if ($proceed) { & (Join-Path $ScriptDir 'install-scheduled-task.ps1') @taskArgs } - } -} - -if ($UninstallTask) { - Step "Uninstall scheduled task" - if ($DryRun) { - Log "[DryRun] Would uninstall scheduled task" - } else { - & (Join-Path $ScriptDir 'uninstall-scheduled-task.ps1') - } -} - -#endregion # Step 4 - Write-Host "" Log "Done." 'SUCCESS' diff --git a/scripts/init-repo.ps1 b/scripts/init-repo.ps1 index c8c17cf..7fccba4 100644 --- a/scripts/init-repo.ps1 +++ b/scripts/init-repo.ps1 @@ -30,6 +30,10 @@ Usage: # Dry run - show what would be installed .\init-repo.ps1 -DryRun + # Remove installed resources (uninstall mode) + .\init-repo.ps1 -Uninstall + .\init-repo.ps1 -Uninstall -SkipInstructions -SkipHooks + Notes: - Existing files are only overwritten if the source is newer/different. - .github/ is created if it doesn't exist. @@ -56,7 +60,8 @@ Notes: [switch]$SkipHooks, [switch]$SkipWorkflows, [switch]$SkipSkills, - [switch]$DryRun + [switch]$DryRun, + [switch]$Uninstall # show a picker of installed items to remove instead of installing ) #region Initialisation @@ -135,8 +140,7 @@ function Detect-RepoStack { } # Always recommend security and code review for every repo - $recs.Add('security') - $recs.Add('code-review') + $recs.Add('owasp') return @($recs | Sort-Object -Unique) } @@ -215,8 +219,7 @@ function Prompt-RepoIntent { } } - $recs.Add('security') - $recs.Add('code-review') + $recs.Add('owasp') return @($recs | Sort-Object -Unique) } @@ -286,27 +289,33 @@ function Select-Items { # Sort: recommended (highest score first) then remaining alphabetically. $Items = $Items | ForEach-Object { $score = Measure-ItemRelevance -ItemName $_.Name -FilePath $_.FullPath -Tags $Tags - $_ | Add-Member -NotePropertyName 'IsRecommended' -NotePropertyValue ($score -gt 0) -PassThru -Force | + $_ | Add-Member -NotePropertyName 'IsRecommended' -NotePropertyValue ($score -ge 2) -PassThru -Force | Add-Member -NotePropertyName 'Score' -NotePropertyValue $score -PassThru -Force - } | Sort-Object @{ E={ if ($_.IsRecommended) { 0 } else { 1 } } }, @{ E={ -$_.Score } }, Name + } | Sort-Object @{ E={ if ($_.IsRecommended) { 0 } else { 1 } } }, @{ E={ if ($_.AlreadyInstalled) { 0 } else { 1 } } }, @{ E={ -$_.Score } }, Name Write-Host "" Write-Host " === $Category ===" -ForegroundColor Yellow - Write-Host " Already installed items are marked with [*]" -ForegroundColor DarkGray + Write-Host " [*]=Installed [↑]=Update available [~]=Locally modified ★=Recommended" -ForegroundColor DarkGray # Try Out-GridView (Windows GUI - filterable, multi-select) $ogvAvailable = $false try { Get-Command Out-GridView -ErrorAction Stop | Out-Null; $ogvAvailable = $true } catch {} if ($ogvAvailable) { - $none = [pscustomobject]@{ Rec=''; Installed=''; Name='-- none / skip --'; Description='Select this (or nothing) to install nothing' } + $none = [pscustomobject]@{ Rec=''; Status=''; Name='-- none / skip --'; Description='Select this (or nothing) to install nothing' } $display = @($none) + @($Items | Select-Object ` - @{ N='Rec'; E={ if ($_.IsRecommended) { '★' } else { '' } } }, - @{ N='Installed'; E={ if ($_.AlreadyInstalled) { '[*]' } else { '' } } }, - @{ N='Name'; E={ $_.Name } }, + @{ N='Rec'; E={ if ($_.IsRecommended) { '★' } else { '' } } }, + @{ N='Status'; E={ + $s = '' + if ($_.AlreadyInstalled) { $s += '[*]' } + if ($_.UpdateAvailable) { $s += '[↑]' } + if ($_.LocallyModified) { $s += '[~]' } + $s + }}, + @{ N='Name'; E={ $_.Name } }, @{ N='Description'; E={ $_.Description } }) - $picked = $display | Out-GridView -Title "Select $Category to install ★ = Recommended [*] = Already installed" -PassThru + $picked = $display | Out-GridView -Title "Select $Category ★=Recommended [*]=Installed [↑]=Update [~]=Modified" -PassThru if (-not $picked) { return @() } $pickedNames = @($picked | Where-Object { $_.Name -ne '-- none / skip --' } | ForEach-Object { $_.Name }) return @($Items | Where-Object { $pickedNames -contains $_.Name }) @@ -315,10 +324,16 @@ function Select-Items { # Fallback: numbered console menu Write-Host "" for ($i = 0; $i -lt $Items.Count; $i++) { - $mark = if ($Items[$i].AlreadyInstalled) { '[*]' } elseif ($Items[$i].IsRecommended) { '[★]' } else { ' ' } - Write-Host (" {0,3}. {1} {2}" -f ($i+1), $mark, $Items[$i].Name) -ForegroundColor $(if ($Items[$i].AlreadyInstalled) { 'DarkCyan' } elseif ($Items[$i].IsRecommended) { 'Yellow' } else { 'White' }) - if ($Items[$i].Description) { - Write-Host (" {0}" -f $Items[$i].Description) -ForegroundColor DarkGray + $item = $Items[$i] + $status = '' + if ($item.AlreadyInstalled) { $status += '[*]' } + if ($item.UpdateAvailable) { $status += '[↑]' } + if ($item.LocallyModified) { $status += '[~]' } + $rec = if ($item.IsRecommended) { '[★]' } else { ' ' } + $color = if ($item.UpdateAvailable) { 'Cyan' } elseif ($item.AlreadyInstalled) { 'DarkCyan' } elseif ($item.IsRecommended) { 'Yellow' } else { 'White' } + Write-Host (" {0,3}. {1} {2,-6} {3}" -f ($i+1), $rec, $status, $item.Name) -ForegroundColor $color + if ($item.Description) { + Write-Host (" {0}" -f $item.Description) -ForegroundColor DarkGray } } Write-Host "" @@ -340,6 +355,81 @@ function Select-Items { return @($Items | Where-Object { $indices -contains ([Array]::IndexOf($Items, $_) + 1) }) } +function Select-ToRemove { + <# + .SYNOPSIS + Shows a picker of script-managed installed items and returns those the user wants to remove. + Only items recorded in .copilot-subscriptions.json are offered — never user-created files. + Locally-modified items are flagged with [~] as a removal warning. + #> + param( + [string]$Category, + [object[]]$Items # full catalogue with ManagedByScript, LocallyModified flags + ) + + # Only offer items this script installed — never user-created files + $removable = @($Items | Where-Object { $_.AlreadyInstalled -and $_.ManagedByScript }) + if ($removable.Count -eq 0) { + Log "No script-managed $Category to remove." 'INFO' + return @() + } + + $ogvAvailable = $false + try { Get-Command Out-GridView -ErrorAction Stop | Out-Null; $ogvAvailable = $true } catch {} + + if ($ogvAvailable) { + $display = @($removable | Select-Object ` + @{ N='Modified'; E={ if ($_.LocallyModified) { '[~] MODIFIED' } else { '' } } }, + @{ N='Name'; E={ $_.Name } }, + @{ N='Description'; E={ $_.Description } }) + $picked = $display | Out-GridView -Title "Select $Category to REMOVE [~]=Locally modified (removal is permanent)" -PassThru + if (-not $picked) { return @() } + $pickedNames = @($picked | ForEach-Object { $_.Name }) + return @($removable | Where-Object { $pickedNames -contains $_.Name }) + } + + Write-Host "" + Write-Host " === Remove $Category ===" -ForegroundColor Red + Write-Host " [~] = locally modified — removal is permanent" -ForegroundColor DarkGray + for ($i = 0; $i -lt $removable.Count; $i++) { + $mod = if ($removable[$i].LocallyModified) { '[~]' } else { ' ' } + $color = if ($removable[$i].LocallyModified) { 'Yellow' } else { 'DarkCyan' } + Write-Host (" {0,3}. {1} {2}" -f ($i+1), $mod, $removable[$i].Name) -ForegroundColor $color + if ($removable[$i].Description) { + Write-Host (" {0}" -f $removable[$i].Description) -ForegroundColor DarkGray + } + } + Write-Host "" + Write-Host " Enter numbers to REMOVE (e.g. 1,3 or blank to skip): " -NoNewline -ForegroundColor Red + $input = Read-Host + if (-not $input -or $input.Trim() -eq '') { return @() } + + $indices = @() + foreach ($part in $input.Split(',')) { + $part = $part.Trim() + if ($part -match '^(\d+)-(\d+)$') { + $indices += ([int]$Matches[1])..[int]$Matches[2] + } elseif ($part -match '^\d+$') { + $indices += [int]$part + } + } + return @($removable | Where-Object { $indices -contains ([Array]::IndexOf($removable, $_) + 1) }) +} + +function Remove-File { + param([string]$FilePath) + if ($DryRun) { return 'would-remove' } + if (Test-Path $FilePath) { Remove-Item $FilePath -Force; return 'removed' } + return 'not-found' +} + +function Remove-Directory { + param([string]$DirPath) + if ($DryRun) { return 'would-remove' } + if (Test-Path $DirPath) { Remove-Item $DirPath -Recurse -Force; return 'removed' } + return 'not-found' +} + function Install-File { param([string]$Src, [string]$DestDir) if (-not $DryRun -and -not (Test-Path $DestDir)) { @@ -495,60 +585,143 @@ function Update-Subscriptions { #region Catalogue builders $totalInstalled = 0 -function Build-FlatCatalogue([string]$CatDir, [string]$DestDir, [string]$Pattern) { +function Build-FlatCatalogue([string]$CatDir, [string]$DestDir, [string]$Pattern, [string]$Category) { if (-not (Test-Path $CatDir)) { return @() } Get-ChildItem $CatDir -File | Where-Object { $_.Name -match $Pattern } | ForEach-Object { - $destFile = Join-Path $DestDir $_.Name + $destFile = Join-Path $DestDir $_.Name + $itemName = [System.IO.Path]::GetFileNameWithoutExtension($_.Name) -replace '\.(instructions|agent|prompt|chatmode)$','' + $subKey = "$Category|$itemName" + $subEntry = $script:SubIndex[$subKey] + $installed = Test-Path $destFile + + $updateAvailable = $false + $locallyModified = $false + $managedByScript = $null -ne $subEntry + + if ($installed -and $subEntry) { + $srcHash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash + $currentHash = (Get-FileHash $destFile -Algorithm SHA256).Hash + $locallyModified = $currentHash -ne $subEntry.hashAtInstall + $updateAvailable = $srcHash -ne $currentHash + } + [pscustomobject]@{ - Name = [System.IO.Path]::GetFileNameWithoutExtension($_.Name) -replace '\.(instructions|agent|prompt|chatmode)$','' + Name = $itemName FileName = $_.Name FullPath = $_.FullName Description = Get-Description $_.FullName - AlreadyInstalled = (Test-Path $destFile) + AlreadyInstalled = $installed + ManagedByScript = $managedByScript + UpdateAvailable = $updateAvailable + LocallyModified = $locallyModified } } | Sort-Object Name } -function Build-DirCatalogue([string]$CatDir, [string]$DestDir) { +function Build-DirCatalogue([string]$CatDir, [string]$DestDir, [string]$Category) { if (-not (Test-Path $CatDir)) { return @() } Get-ChildItem $CatDir -Directory | ForEach-Object { $destSubdir = Join-Path $DestDir $_.Name $readmePath = Join-Path $_.FullName 'README.md' if (-not (Test-Path $readmePath)) { $readmePath = Join-Path $_.FullName 'SKILL.md' } + $subKey = "$Category|$($_.Name)" + $subEntry = $script:SubIndex[$subKey] + $installed = Test-Path $destSubdir + + $updateAvailable = $false + $locallyModified = $false + $managedByScript = $null -ne $subEntry + + if ($installed -and $subEntry) { + $srcHash = Get-DirHash $_.FullName + $currentHash = Get-DirHash $destSubdir + $locallyModified = $currentHash -ne $subEntry.hashAtInstall + $updateAvailable = $srcHash -ne $currentHash + } + [pscustomobject]@{ Name = $_.Name FullPath = $_.FullName Description = if (Test-Path $readmePath) { Get-Description $readmePath } else { '' } - AlreadyInstalled = (Test-Path $destSubdir) + AlreadyInstalled = $installed + ManagedByScript = $managedByScript + UpdateAvailable = $updateAvailable + LocallyModified = $locallyModified } } | Sort-Object Name } #endregion # Catalogue builders -#region Agents +#region Subscription manifest $script:SubscriptionEntries = [System.Collections.Generic.List[object]]::new() $SubscriptionManifestPath = Join-Path $GithubDir '.copilot-subscriptions.json' +# Load existing manifest into a lookup: "category|name" -> subscription entry +$script:SubIndex = @{} +if (Test-Path $SubscriptionManifestPath) { + try { + $existingSubs = Get-Content $SubscriptionManifestPath -Raw | ConvertFrom-Json + if ($existingSubs.subscriptions) { + foreach ($s in $existingSubs.subscriptions) { + $script:SubIndex["$($s.category)|$($s.name)"] = $s + } + } + } catch { Log "Could not parse subscriptions manifest: $_" 'WARN' } +} + +function Remove-SubscriptionEntries { + param([string]$ManifestPath, [string[]]$Keys) # Keys = "category|name" + if (-not (Test-Path $ManifestPath)) { return } + if ($DryRun) { Log "[DryRun] Would remove $($Keys.Count) subscription(s) from manifest"; return } + try { + $subs = Get-Content $ManifestPath -Raw | ConvertFrom-Json + if (-not $subs.subscriptions) { return } + $kept = @($subs.subscriptions | Where-Object { $Keys -notcontains "$($_.category)|$($_.name)" }) + $subs | Add-Member -NotePropertyName 'subscriptions' -NotePropertyValue $kept -Force + $subs | Add-Member -NotePropertyName 'updatedAt' -NotePropertyValue (Get-Date).ToString('o') -Force + $subs | ConvertTo-Json -Depth 5 | Set-Content $ManifestPath -Encoding UTF8 + } catch { Log "Could not update subscriptions manifest: $_" 'WARN' } +} + +#endregion # Subscription manifest + +#region Agents +$script:AllCatalogues = [System.Collections.Generic.List[object]]::new() + if (-not $SkipAgents) { $destDir = Join-Path $GithubDir 'agents' - $catalogue = Build-FlatCatalogue (Join-Path $SourceRoot 'agents') $destDir '\.agent\.md$' - $selected = Select-Items -Category 'Agents' -Items $catalogue -PreSelected $Agents -Tags $script:Recommendations - - foreach ($item in $selected) { - $result = Install-File -Src $item.FullPath -DestDir $destDir - $verb = switch ($result) { 'added' { '✓ Added' } 'updated' { '↑ Updated' } 'unchanged' { '= Unchanged' } default { '~ DryRun' } } - Log "$verb agent: $($item.FileName)" - if ($result -in 'added','updated','would-copy') { $totalInstalled++ } - $script:SubscriptionEntries.Add([pscustomobject]@{ - name = $item.Name - category = 'agents' - type = 'file' - fileName = $item.FileName - sourceRelPath = "agents/$($item.FileName)" - hashAtInstall = (Get-FileHash $item.FullPath -Algorithm SHA256).Hash - installedAt = (Get-Date).ToString('o') - }) + $catalogue = Build-FlatCatalogue (Join-Path $SourceRoot 'agents') $destDir '\.agent\.md$' 'agents' + $script:AllCatalogues.Add([pscustomobject]@{ Category='agents'; Type='file'; Items=$catalogue; DestDir=$destDir }) + + if ($Uninstall) { + $toRemove = Select-ToRemove -Category 'Agents' -Items $catalogue + foreach ($item in $toRemove) { + $result = Remove-File -FilePath (Join-Path $destDir $item.FileName) + $verb = if ($result -eq 'would-remove') { '~ DryRun remove' } else { '✗ Removed' } + Log "$verb agent: $($item.FileName)" + } + if ($toRemove.Count -gt 0) { + Remove-SubscriptionEntries -ManifestPath $SubscriptionManifestPath -Keys @($toRemove | ForEach-Object { "agents|$($_.Name)" }) + } + } else { + $selected = Select-Items -Category 'Agents' -Items $catalogue -PreSelected $Agents -Tags $script:Recommendations + + foreach ($item in $selected) { + $result = Install-File -Src $item.FullPath -DestDir $destDir + $verb = switch ($result) { 'added' { '✓ Added' } 'updated' { '↑ Updated' } 'unchanged' { '= Unchanged' } default { '~ DryRun' } } + Log "$verb agent: $($item.FileName)" + if ($result -in 'added','updated','would-copy') { $totalInstalled++ } + $script:SubscriptionEntries.Add([pscustomobject]@{ + name = $item.Name + category = 'agents' + type = 'file' + fileName = $item.FileName + sourceRelPath = "agents/$($item.FileName)" + hashAtInstall = (Get-FileHash $item.FullPath -Algorithm SHA256).Hash + installedAt = (Get-Date).ToString('o') + }) + } } } @@ -557,23 +730,37 @@ if (-not $SkipAgents) { #region Instructions if (-not $SkipInstructions) { $destDir = Join-Path $GithubDir 'instructions' - $catalogue = Build-FlatCatalogue (Join-Path $SourceRoot 'instructions') $destDir '\.instructions\.md$' - $selected = Select-Items -Category 'Instructions' -Items $catalogue -PreSelected $Instructions -Tags $script:Recommendations - - foreach ($item in $selected) { - $result = Install-File -Src $item.FullPath -DestDir $destDir - $verb = switch ($result) { 'added' { '✓ Added' } 'updated' { '↑ Updated' } 'unchanged' { '= Unchanged' } default { '~ DryRun' } } - Log "$verb instructions: $($item.FileName)" - if ($result -in 'added','updated','would-copy') { $totalInstalled++ } - $script:SubscriptionEntries.Add([pscustomobject]@{ - name = $item.Name - category = 'instructions' - type = 'file' - fileName = $item.FileName - sourceRelPath = "instructions/$($item.FileName)" - hashAtInstall = (Get-FileHash $item.FullPath -Algorithm SHA256).Hash - installedAt = (Get-Date).ToString('o') - }) + $catalogue = Build-FlatCatalogue (Join-Path $SourceRoot 'instructions') $destDir '\.instructions\.md$' 'instructions' + $script:AllCatalogues.Add([pscustomobject]@{ Category='instructions'; Type='file'; Items=$catalogue; DestDir=$destDir }) + + if ($Uninstall) { + $toRemove = Select-ToRemove -Category 'Instructions' -Items $catalogue + foreach ($item in $toRemove) { + $result = Remove-File -FilePath (Join-Path $destDir $item.FileName) + $verb = if ($result -eq 'would-remove') { '~ DryRun remove' } else { '✗ Removed' } + Log "$verb instructions: $($item.FileName)" + } + if ($toRemove.Count -gt 0) { + Remove-SubscriptionEntries -ManifestPath $SubscriptionManifestPath -Keys @($toRemove | ForEach-Object { "instructions|$($_.Name)" }) + } + } else { + $selected = Select-Items -Category 'Instructions' -Items $catalogue -PreSelected $Instructions -Tags $script:Recommendations + + foreach ($item in $selected) { + $result = Install-File -Src $item.FullPath -DestDir $destDir + $verb = switch ($result) { 'added' { '✓ Added' } 'updated' { '↑ Updated' } 'unchanged' { '= Unchanged' } default { '~ DryRun' } } + Log "$verb instructions: $($item.FileName)" + if ($result -in 'added','updated','would-copy') { $totalInstalled++ } + $script:SubscriptionEntries.Add([pscustomobject]@{ + name = $item.Name + category = 'instructions' + type = 'file' + fileName = $item.FileName + sourceRelPath = "instructions/$($item.FileName)" + hashAtInstall = (Get-FileHash $item.FullPath -Algorithm SHA256).Hash + installedAt = (Get-Date).ToString('o') + }) + } } } @@ -582,23 +769,37 @@ if (-not $SkipInstructions) { #region Hooks if (-not $SkipHooks) { $destDir = Join-Path $GithubDir 'hooks' - $catalogue = Build-DirCatalogue (Join-Path $SourceRoot 'hooks') $destDir - $selected = Select-Items -Category 'Hooks' -Items $catalogue -PreSelected $Hooks -Recommended $script:Recommendations - - foreach ($item in $selected) { - $r = Install-Directory -SrcDir $item.FullPath -DestParent $destDir - $verb = if ($DryRun) { '~ DryRun' } else { '✓ Installed' } - Log "$verb hook: $($item.Name) (added=$($r.Added) updated=$($r.Updated) unchanged=$($r.Unchanged))" - if (-not $DryRun) { $totalInstalled++ } - $script:SubscriptionEntries.Add([pscustomobject]@{ - name = $item.Name - category = 'hooks' - type = 'directory' - dirName = $item.Name - sourceRelPath = "hooks/$($item.Name)" - hashAtInstall = Get-DirHash $item.FullPath - installedAt = (Get-Date).ToString('o') - }) + $catalogue = Build-DirCatalogue (Join-Path $SourceRoot 'hooks') $destDir 'hooks' + $script:AllCatalogues.Add([pscustomobject]@{ Category='hooks'; Type='directory'; Items=$catalogue; DestDir=$destDir }) + + if ($Uninstall) { + $toRemove = Select-ToRemove -Category 'Hooks' -Items $catalogue + foreach ($item in $toRemove) { + $result = Remove-Directory -DirPath (Join-Path $destDir $item.Name) + $verb = if ($result -eq 'would-remove') { '~ DryRun remove' } else { '✗ Removed' } + Log "$verb hook: $($item.Name)" + } + if ($toRemove.Count -gt 0) { + Remove-SubscriptionEntries -ManifestPath $SubscriptionManifestPath -Keys @($toRemove | ForEach-Object { "hooks|$($_.Name)" }) + } + } else { + $selected = Select-Items -Category 'Hooks' -Items $catalogue -PreSelected $Hooks -Tags $script:Recommendations + + foreach ($item in $selected) { + $r = Install-Directory -SrcDir $item.FullPath -DestParent $destDir + $verb = if ($DryRun) { '~ DryRun' } else { '✓ Installed' } + Log "$verb hook: $($item.Name) (added=$($r.Added) updated=$($r.Updated) unchanged=$($r.Unchanged))" + if (-not $DryRun) { $totalInstalled++ } + $script:SubscriptionEntries.Add([pscustomobject]@{ + name = $item.Name + category = 'hooks' + type = 'directory' + dirName = $item.Name + sourceRelPath = "hooks/$($item.Name)" + hashAtInstall = Get-DirHash $item.FullPath + installedAt = (Get-Date).ToString('o') + }) + } } } @@ -607,23 +808,37 @@ if (-not $SkipHooks) { #region Workflows if (-not $SkipWorkflows) { $destDir = Join-Path $GithubDir 'workflows' - $catalogue = Build-FlatCatalogue (Join-Path $SourceRoot 'workflows') $destDir '\.md$' - $selected = Select-Items -Category 'Agentic Workflows' -Items $catalogue -PreSelected $Workflows -Recommended $script:Recommendations - - foreach ($item in $selected) { - $result = Install-File -Src $item.FullPath -DestDir $destDir - $verb = switch ($result) { 'added' { '✓ Added' } 'updated' { '↑ Updated' } 'unchanged' { '= Unchanged' } default { '~ DryRun' } } - Log "$verb workflow: $($item.FileName)" - if ($result -in 'added','updated','would-copy') { $totalInstalled++ } - $script:SubscriptionEntries.Add([pscustomobject]@{ - name = $item.Name - category = 'workflows' - type = 'file' - fileName = $item.FileName - sourceRelPath = "workflows/$($item.FileName)" - hashAtInstall = (Get-FileHash $item.FullPath -Algorithm SHA256).Hash - installedAt = (Get-Date).ToString('o') - }) + $catalogue = Build-FlatCatalogue (Join-Path $SourceRoot 'workflows') $destDir '\.md$' 'workflows' + $script:AllCatalogues.Add([pscustomobject]@{ Category='workflows'; Type='file'; Items=$catalogue; DestDir=$destDir }) + + if ($Uninstall) { + $toRemove = Select-ToRemove -Category 'Agentic Workflows' -Items $catalogue + foreach ($item in $toRemove) { + $result = Remove-File -FilePath (Join-Path $destDir $item.FileName) + $verb = if ($result -eq 'would-remove') { '~ DryRun remove' } else { '✗ Removed' } + Log "$verb workflow: $($item.FileName)" + } + if ($toRemove.Count -gt 0) { + Remove-SubscriptionEntries -ManifestPath $SubscriptionManifestPath -Keys @($toRemove | ForEach-Object { "workflows|$($_.Name)" }) + } + } else { + $selected = Select-Items -Category 'Agentic Workflows' -Items $catalogue -PreSelected $Workflows -Tags $script:Recommendations + + foreach ($item in $selected) { + $result = Install-File -Src $item.FullPath -DestDir $destDir + $verb = switch ($result) { 'added' { '✓ Added' } 'updated' { '↑ Updated' } 'unchanged' { '= Unchanged' } default { '~ DryRun' } } + Log "$verb workflow: $($item.FileName)" + if ($result -in 'added','updated','would-copy') { $totalInstalled++ } + $script:SubscriptionEntries.Add([pscustomobject]@{ + name = $item.Name + category = 'workflows' + type = 'file' + fileName = $item.FileName + sourceRelPath = "workflows/$($item.FileName)" + hashAtInstall = (Get-FileHash $item.FullPath -Algorithm SHA256).Hash + installedAt = (Get-Date).ToString('o') + }) + } } } @@ -632,39 +847,95 @@ if (-not $SkipWorkflows) { #region Skills if (-not $SkipSkills) { $destDir = Join-Path $GithubDir 'skills' - $catalogue = Build-DirCatalogue (Join-Path $SourceRoot 'skills') $destDir - $selected = Select-Items -Category 'Skills' -Items $catalogue -PreSelected $Skills -Recommended $script:Recommendations - - foreach ($item in $selected) { - $r = Install-Directory -SrcDir $item.FullPath -DestParent $destDir - $verb = if ($DryRun) { '~ DryRun' } else { '✓ Installed' } - Log "$verb skill: $($item.Name) (added=$($r.Added) updated=$($r.Updated) unchanged=$($r.Unchanged))" - if (-not $DryRun) { $totalInstalled++ } - $script:SubscriptionEntries.Add([pscustomobject]@{ - name = $item.Name - category = 'skills' - type = 'directory' - dirName = $item.Name - sourceRelPath = "skills/$($item.Name)" - hashAtInstall = Get-DirHash $item.FullPath - installedAt = (Get-Date).ToString('o') - }) + $catalogue = Build-DirCatalogue (Join-Path $SourceRoot 'skills') $destDir 'skills' + $script:AllCatalogues.Add([pscustomobject]@{ Category='skills'; Type='directory'; Items=$catalogue; DestDir=$destDir }) + + if ($Uninstall) { + $toRemove = Select-ToRemove -Category 'Skills' -Items $catalogue + foreach ($item in $toRemove) { + $result = Remove-Directory -DirPath (Join-Path $destDir $item.Name) + $verb = if ($result -eq 'would-remove') { '~ DryRun remove' } else { '✗ Removed' } + Log "$verb skill: $($item.Name)" + } + if ($toRemove.Count -gt 0) { + Remove-SubscriptionEntries -ManifestPath $SubscriptionManifestPath -Keys @($toRemove | ForEach-Object { "skills|$($_.Name)" }) + } + } else { + $selected = Select-Items -Category 'Skills' -Items $catalogue -PreSelected $Skills -Tags $script:Recommendations + + foreach ($item in $selected) { + $r = Install-Directory -SrcDir $item.FullPath -DestParent $destDir + $verb = if ($DryRun) { '~ DryRun' } else { '✓ Installed' } + Log "$verb skill: $($item.Name) (added=$($r.Added) updated=$($r.Updated) unchanged=$($r.Unchanged))" + if (-not $DryRun) { $totalInstalled++ } + $script:SubscriptionEntries.Add([pscustomobject]@{ + name = $item.Name + category = 'skills' + type = 'directory' + dirName = $item.Name + sourceRelPath = "skills/$($item.Name)" + hashAtInstall = Get-DirHash $item.FullPath + installedAt = (Get-Date).ToString('o') + }) + } } } #endregion # Skills #region Summary + +# Auto-adopt items that are already installed in .github/ but not yet in the manifest +# (e.g. installed by a previous version of this script before tracking was added). +# Uses the current installed file hash as hashAtInstall — they'll show [↑] if upstream has moved on. +if (-not $Uninstall -and -not $DryRun) { + foreach ($cat in $script:AllCatalogues) { + $untracked = @($cat.Items | Where-Object { $_.AlreadyInstalled -and -not $_.ManagedByScript }) + foreach ($item in $untracked) { + $installedPath = if ($cat.Type -eq 'file') { + Join-Path $cat.DestDir $item.FileName + } else { + Join-Path $cat.DestDir $item.Name + } + $adoptedHash = if ($cat.Type -eq 'file') { + (Get-FileHash $installedPath -Algorithm SHA256).Hash + } else { + Get-DirHash $installedPath + } + $entry = [pscustomobject]@{ + name = $item.Name + category = $cat.Category + type = $cat.Type + installedAt = (Get-Item $installedPath).LastWriteTime.ToString('o') + hashAtInstall = $adoptedHash + adopted = $true # flag so we know this was auto-adopted not explicitly chosen + } + if ($cat.Type -eq 'file') { + $entry | Add-Member -NotePropertyName 'fileName' -NotePropertyValue $item.FileName -Force + $entry | Add-Member -NotePropertyName 'sourceRelPath' -NotePropertyValue "$($cat.Category)/$($item.FileName)" -Force + } else { + $entry | Add-Member -NotePropertyName 'dirName' -NotePropertyValue $item.Name -Force + $entry | Add-Member -NotePropertyName 'sourceRelPath' -NotePropertyValue "$($cat.Category)/$($item.Name)" -Force + } + $script:SubscriptionEntries.Add($entry) + Log "Adopted existing install into manifest: $($cat.Category)/$($item.Name)" + } + } +} + if ($script:SubscriptionEntries.Count -gt 0) { Update-Subscriptions -ManifestPath $SubscriptionManifestPath -NewEntries $script:SubscriptionEntries.ToArray() } Write-Host "" -if ($DryRun) { +if ($Uninstall) { + Log "Uninstall complete." 'SUCCESS' +} elseif ($DryRun) { Log "Dry run complete. Re-run without -DryRun to apply." 'WARN' } else { Log "$totalInstalled resource(s) installed/updated in $GithubDir" 'SUCCESS' Log "Tip: commit .github/ to share Copilot resources with your team (agents, instructions, hooks, workflows, skills)." Log "Tip: run update-repo.ps1 to check for and apply upstream changes to your subscribed resources." + Log "Tip: run init-repo.ps1 -Uninstall to remove any installed resources." } #endregion # Summary diff --git a/scripts/install-scheduled-task.ps1 b/scripts/install-scheduled-task.ps1 deleted file mode 100644 index 48680d5..0000000 --- a/scripts/install-scheduled-task.ps1 +++ /dev/null @@ -1,59 +0,0 @@ -[CmdletBinding()] param( - [string]$TaskName = 'AwesomeCopilotSync', - [string]$Every = '4h', - [string]$Dest = "$HOME/.awesome-copilot", - # Default categories aligned with current awesome-copilot structure - [string]$Categories = 'agents,instructions,workflows,hooks,skills', - [switch]$IncludePlugins, - # Allow skipping the global publish step if user only wants raw sync - [switch]$SkipPublishGlobal, - [string]$PwshPath = (Get-Command pwsh -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Source), - [string]$ScriptPath = (Join-Path $PSScriptRoot 'sync-awesome-copilot.ps1'), - [string]$PublishGlobalScriptPath = (Join-Path $PSScriptRoot 'publish-global.ps1'), - [switch]$Force -) - -if (-not $PwshPath) { $PwshPath = (Get-Command powershell | Select-Object -ExpandProperty Source) } -if (-not (Test-Path $ScriptPath)) { throw "Sync script not found at $ScriptPath" } -if (-not $SkipPublishGlobal -and -not (Test-Path $PublishGlobalScriptPath)) { throw "Publish-global script not found at $PublishGlobalScriptPath (use -SkipPublishGlobal to suppress)" } - -function Parse-Interval($spec) { - if ($spec -match '^(\d+)([hm])$') { - $val = [int]$Matches[1]; $unit = $Matches[2] - switch ($unit) { - 'h' { return @{ Type = 'HOURLY'; Modifier = $val } } - 'm' { return @{ Type = 'MINUTE'; Modifier = $val } } - } - } - throw "Unsupported interval spec: $spec (use like 4h or 30m)" -} - -if ($IncludePlugins -and ($Categories -notmatch 'plugins')) { - $Categories = ($Categories.TrimEnd(',') + ',plugins') -} - -$int = Parse-Interval $Every - -# Primary sync action -$ScriptDir = Split-Path -Parent $ScriptPath -$syncArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`" -Dest `"$Dest`" -Categories `"$Categories`" -Quiet" -$actions = @() -$actions += New-ScheduledTaskAction -Execute $PwshPath -Argument $syncArgs -WorkingDirectory $ScriptDir - -if (-not $SkipPublishGlobal) { - # publish-global runs after sync: updates VS Code agents folder and ~/.copilot/skills/ - $publishArgs = "-NoProfile -ExecutionPolicy Bypass -File `"$PublishGlobalScriptPath`" -SourceRoot `"$Dest`"" - $actions += New-ScheduledTaskAction -Execute $PwshPath -Argument $publishArgs -WorkingDirectory $ScriptDir -} -$Trigger = if ($int.Type -eq 'HOURLY') { New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval ([TimeSpan]::FromHours($int.Modifier)) -RepetitionDuration ([TimeSpan]::FromDays(3650)) } else { New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval ([TimeSpan]::FromMinutes($int.Modifier)) -RepetitionDuration ([TimeSpan]::FromDays(3650)) } -$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -MultipleInstances IgnoreNew -ExecutionTimeLimit (New-TimeSpan -Minutes 20) - -if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) { - if (-not $Force) { Write-Error "Task $TaskName already exists. Use -Force to overwrite."; exit 1 } - Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false -} - -Register-ScheduledTask -TaskName $TaskName -Action $actions -Trigger $Trigger -Settings $Settings -Description "Sync and publish Awesome Copilot resources (agents, instructions, skills, hooks, workflows)" | Out-Null - -$post = if ($SkipPublishGlobal) { 'sync only' } else { 'sync + publish-global' } -Write-Host "Scheduled task '$TaskName' created ($post). First run in ~1 minute, then every $Every." -ForegroundColor Green diff --git a/scripts/uninstall-scheduled-task.ps1 b/scripts/uninstall-scheduled-task.ps1 deleted file mode 100644 index ec17132..0000000 --- a/scripts/uninstall-scheduled-task.ps1 +++ /dev/null @@ -1,10 +0,0 @@ -[CmdletBinding()] param( - [string]$TaskName = 'AwesomeCopilotSync' -) -if (Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue) { - Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false - Write-Host "Removed scheduled task $TaskName" -ForegroundColor Yellow -} -else { - Write-Host "Task $TaskName not found" -ForegroundColor Gray -} From 4eb423889f28e598bc102274cd613d669245ca81 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:00:38 +0000 Subject: [PATCH 36/60] docs: formatting tweaks to CHANGELOG and README Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- CHANGELOG.md | 22 +++++++++++++++++++--- README.md | 23 +++++++++++------------ 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2957b80..b9054c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.2.2] - 2026-02-27 ### Changed + - `configure.ps1`: use `$PSScriptRoot` to locate the `scripts/` folder (replaces `$MyInvocation.MyCommand.Path` which behaves differently when dot-sourced) - `scripts/sync-awesome-copilot.ps1`: replace manual SHA256 with built-in `Get-FileHash` — cleaner and avoids loading entire file into memory - `scripts/publish-global.ps1`: emit a `WARN` log when VS Code `settings.json` is not found (was a silent no-op); user is directed to open VS Code once to generate the file @@ -15,6 +16,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.2.1] - 2026-02-27 ### Fixed + - `configure.ps1`: `-InstallTask` / `-UninstallTask` now automatically skip the `init-repo` prompt (validation moved before Step 1 so the flag takes effect) - `configure.ps1`: prompts to overwrite when the scheduled task already exists, instead of throwing a hard error - `scripts/install-scheduled-task.ps1`: added `-WorkingDirectory` to both scheduled task actions (was defaulting to `C:\Windows\System32`, causing a permissions error creating the `logs/` directory) @@ -27,6 +29,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.2.0] - 2026-02-27 ### Changed + - `scripts/sync-awesome-copilot.ps1`: **Rewritten** — replaced GitHub API + per-file HTTP download approach with `git sparse-checkout`. First run clones `github/awesome-copilot` shallowly with only the requested categories; subsequent runs run `git pull` for near-instant delta updates. Dramatically faster (single bulk transfer vs 700+ individual HTTP requests) and removes GitHub API rate-limit concerns entirely. - Prefers `gh` (GitHub CLI) for automatic auth; falls back to `git` - New `-GitTool auto|gh|git` parameter to override tool selection @@ -35,14 +38,17 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `manifest.json` still written (from local file scan) for backward compatibility with `publish-global.ps1` and `configure.ps1` ### Added + - `README.md`: document `gh`/`git` requirement; update sync section to reflect git-based approach ## [1.1.2] - 2026-02-27 ### Added + - `configure.ps1` — interactive orchestrator that chains sync → publish-global → init-repo; each step independently skippable via `-SkipSync`, `-SkipPublish`, `-SkipInit`; `-DryRun` passes through to all child scripts; shows last sync timestamp from cache manifest before running ### Added + - `init-repo.ps1`: added Agents as a fourth interactive category (installs to `.github/agents/`) - `init-repo.ps1`: `Detect-RepoStack` — auto-detects language/framework from file signals and marks relevant items with ★ in the picker - `init-repo.ps1`: `Prompt-RepoIntent` — interactive fallback for new/empty repos; asks language, project type, and concerns @@ -51,6 +57,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `.github/copilot-instructions.md`: Copilot instructions for this repository covering script workflow, conventions, and contributing guidelines ### Fixed + - `normalize-copilot-folders.ps1`: `Split-Path -LeafParent` → `Split-Path -Parent` (`-LeafParent` is not a valid parameter and would throw at runtime) - `install-scheduled-task.ps1`: removed `-Quiet` from `publish-global.ps1` invocation (`publish-global.ps1` has no `-Quiet` parameter; would throw on scheduled runs) - `init-repo.ps1`: `$Items.IndexOf($_)` → `[Array]::IndexOf($Items, $_)` (`System.Object[]` has no instance `IndexOf` method; affected console-menu fallback path) @@ -59,25 +66,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - `publish-global.ps1`: corrected agents target path to `%APPDATA%\Code\User\prompts\` (was incorrectly set to `agents\`) ### Changed + - `publish-global.ps1`: updated inline comment from "CCA" to "VS Code Agent mode / Copilot CLI" - `README.md`: corrected all `-Interval` references to `-Every`; fixed `-ProfileName` → `-ProfileRoot`/`-AllProfiles`; updated agents path to `%APPDATA%\Code\User\prompts\`; updated `init-repo.ps1` section to reflect agents category and smart detection; fixed custom `-AgentsTarget` example path - `.github/copilot-instructions.md`: corrected agents path from `%APPDATA%\Code\User\agents\` to `%APPDATA%\Code\User\prompts\` - - ### Fixed + - `sync-awesome-copilot.ps1`: changed `$ErrorActionPreference` from `Inquire` to `Stop` — `Inquire` caused the script to hang waiting for interactive input when run as a scheduled task ### Changed + - `init-repo.ps1`: removed skills from per-repo initialisation; skills are globally available via `publish-global.ps1` (`~/.copilot/skills/`) and users should reference the source directly at [github/awesome-copilot](https://github.com/github/awesome-copilot) rather than committing point-in-time copies to repos ## [1.1.0] - 2026-02-26 ### Added + - `publish-global.ps1` — publishes agents to the VS Code user agents folder (via junction so sync updates are reflected immediately) and skills to `~/.copilot/skills/`; supports `-DryRun`, `-SkipAgents`, `-SkipSkills`, `-AgentsTarget`, `-SkillsTarget` - `init-repo.ps1` — interactive script to initialise a repo with per-repo resources (instructions, hooks, workflows); uses Out-GridView on Windows with a numbered console-menu fallback; supports `-RepoPath`, `-DryRun`, `-SkipInstructions`, `-SkipHooks`, `-SkipWorkflows` ### Changed + - Updated default sync categories to match current awesome-copilot repository structure: - **Added**: `agents`, `workflows`, `hooks`, `skills` - **Removed**: `chatmodes`, `prompts` (no longer exist in awesome-copilot) @@ -88,8 +98,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Updated `install-scheduled-task.ps1`: default categories now `agents,instructions,workflows,hooks,skills`; `-IncludeCollections` replaced by `-IncludePlugins`; `-SkipCombine` replaced by `-SkipPublishGlobal`; scheduled actions now run `publish-global.ps1` after sync ### Removed -- `normalize-copilot-folders.ps1` — removed (legacy, superseded by junction-based agent publishing and `init-repo.ps1`) +- `normalize-copilot-folders.ps1` — removed (legacy, superseded by junction-based agent publishing and `init-repo.ps1`) - `plugins/` and `cookbook/` are available but opt-in via `-IncludePlugins` due to their size - Hooks are synced as complete packages (README.md + hooks.json + .sh scripts) preserving their directory structure @@ -98,6 +108,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [1.0.0] - 2025-10-20 ### Added + - Initial release of VS Code Copilot Resource Sync Scripts - `sync-awesome-copilot.ps1` - Sync resources from GitHub awesome-copilot repository - `combine-and-publish-prompts.ps1` - Combine and publish resources to VS Code @@ -113,6 +124,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Support for multiple VS Code profiles ### Features + - Portable paths using environment variables ($HOME, $env:APPDATA) - Preserves user-created custom files in combined directory - Incremental updates (only downloads changed files) @@ -121,6 +133,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Detailed sync logs with timestamps ### Security + - No hardcoded credentials or personal information - Optional environment variable for GitHub token - All sensitive data handled via environment variables @@ -130,6 +143,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Future Plans ### Planned Features + - [ ] Configuration file support (YAML/JSON) - [ ] Backup and restore functionality - [ ] Conflict resolution UI for duplicate resources @@ -139,12 +153,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - [ ] Cross-platform support (macOS, Linux) ### Known Issues + - Junction creation requires appropriate permissions on some systems - Scheduled task runs under user context (requires user to be logged in) --- **Note:** Version numbers follow [Semantic Versioning](https://semver.org/): + - MAJOR version for incompatible API changes - MINOR version for new functionality in a backwards compatible manner - PATCH version for backwards compatible bug fixes diff --git a/README.md b/README.md index 082e75e..ac23232 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,14 @@ A collection of PowerShell scripts to sync, publish, and manage [GitHub Copilot] ### What goes where -| Resource | Scope | Location | -| ---------------- | ----------- | -------- | +| Resource | Scope | Location | +| ---------------- | ----------- | ---------------------------------------------------------------- | | **Agents** | 🌐 Global | `%APPDATA%\Code\User\prompts\` — available across all workspaces | -| **Skills** | 🌐 Global | `~/.copilot/skills/` — loaded by Copilot coding agent & CLI | -| **Instructions** | 📁 Per-repo | `.github/instructions/` | -| **Hooks** | 📁 Per-repo | `.github/hooks//` | -| **Workflows** | 📁 Per-repo | `.github/workflows/` | -| **Agents** | 📁 Per-repo | `.github/agents/` | +| **Skills** | 🌐 Global | `~/.copilot/skills/` — loaded by Copilot coding agent & CLI | +| **Instructions** | 📁 Per-repo | `.github/instructions/` | +| **Hooks** | 📁 Per-repo | `.github/hooks//` | +| **Workflows** | 📁 Per-repo | `.github/workflows/` | +| **Agents** | 📁 Per-repo | `.github/agents/` | ## 📋 Prerequisites @@ -66,10 +66,10 @@ cd C:\Projects\my-app The picker auto-detects your language/framework and marks relevant items with ★. Items already installed show their status: -| Symbol | Meaning | -| ------ | ------- | -| ★ | Recommended for this repo | -| `[*]` | Already installed | +| Symbol | Meaning | +| ------ | ------------------------------ | +| ★ | Recommended for this repo | +| `[*]` | Already installed | | `[↑]` | Update available from upstream | | `[~]` | Locally modified since install | @@ -236,4 +236,3 @@ MIT — see [LICENSE](LICENSE) --- **Made with ❤️ for the VS Code + Copilot community** - From 9aacc8b13c01abebf49dcf7d335c80ecb501bcfd Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:05:19 +0000 Subject: [PATCH 37/60] fix: add none row to uninstall picker; default update check to Yes - Select-ToRemove: add '-- none / skip --' row to Out-GridView so closing the picker without selecting doesn't accidentally remove the first item in the list - configure.ps1: empty input on 'Check for upstream updates?' now defaults to Yes (was No) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- configure.ps1 | 4 ++-- scripts/init-repo.ps1 | 5 +++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/configure.ps1 b/configure.ps1 index f30068f..82cec00 100644 --- a/configure.ps1 +++ b/configure.ps1 @@ -95,9 +95,9 @@ if (-not $SkipInit) { if (Test-Path $subscriptionsFile) { Step "Check for updates to subscribed repo resources" Write-Host " Subscriptions found. Check for upstream updates to .github/ resources?" -ForegroundColor Yellow - Write-Host " [Y] Yes [N] No (default): " -NoNewline -ForegroundColor Yellow + Write-Host " [Y] Yes (default) [N] No: " -NoNewline -ForegroundColor Yellow $updateAnswer = (Read-Host).Trim() - if ($updateAnswer -match '^[Yy]') { + if ($updateAnswer -eq '' -or $updateAnswer -match '^[Yy]') { $updateArgs = @{} if ($DryRun) { $updateArgs['DryRun'] = $true } if ($RepoPath) { $updateArgs['RepoPath'] = $RepoPath } diff --git a/scripts/init-repo.ps1 b/scripts/init-repo.ps1 index 7fccba4..79015c1 100644 --- a/scripts/init-repo.ps1 +++ b/scripts/init-repo.ps1 @@ -378,13 +378,14 @@ function Select-ToRemove { try { Get-Command Out-GridView -ErrorAction Stop | Out-Null; $ogvAvailable = $true } catch {} if ($ogvAvailable) { - $display = @($removable | Select-Object ` + $none = [pscustomobject]@{ Modified=''; Name='-- none / skip --'; Description='Select this (or nothing) to remove nothing' } + $display = @($none) + @($removable | Select-Object ` @{ N='Modified'; E={ if ($_.LocallyModified) { '[~] MODIFIED' } else { '' } } }, @{ N='Name'; E={ $_.Name } }, @{ N='Description'; E={ $_.Description } }) $picked = $display | Out-GridView -Title "Select $Category to REMOVE [~]=Locally modified (removal is permanent)" -PassThru if (-not $picked) { return @() } - $pickedNames = @($picked | ForEach-Object { $_.Name }) + $pickedNames = @($picked | Where-Object { $_.Name -ne '-- none / skip --' } | ForEach-Object { $_.Name }) return @($removable | Where-Object { $pickedNames -contains $_.Name }) } From fdbe5f85254e6f891e3d57683ef707ff598c6f59 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:07:21 +0000 Subject: [PATCH 38/60] fix: auto-run update-repo when subscriptions exist, remove prompt Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- configure.ps1 | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/configure.ps1 b/configure.ps1 index 82cec00..cab7224 100644 --- a/configure.ps1 +++ b/configure.ps1 @@ -94,17 +94,10 @@ if (-not $SkipInit) { $subscriptionsFile = Join-Path $RepoPath '.github\.copilot-subscriptions.json' if (Test-Path $subscriptionsFile) { Step "Check for updates to subscribed repo resources" - Write-Host " Subscriptions found. Check for upstream updates to .github/ resources?" -ForegroundColor Yellow - Write-Host " [Y] Yes (default) [N] No: " -NoNewline -ForegroundColor Yellow - $updateAnswer = (Read-Host).Trim() - if ($updateAnswer -eq '' -or $updateAnswer -match '^[Yy]') { - $updateArgs = @{} - if ($DryRun) { $updateArgs['DryRun'] = $true } - if ($RepoPath) { $updateArgs['RepoPath'] = $RepoPath } - & (Join-Path $ScriptDir 'update-repo.ps1') @updateArgs - } else { - Log "Update check skipped." - } + $updateArgs = @{} + if ($DryRun) { $updateArgs['DryRun'] = $true } + if ($RepoPath) { $updateArgs['RepoPath'] = $RepoPath } + & (Join-Path $ScriptDir 'update-repo.ps1') @updateArgs } Step "Init repo" From 7be190b2dc3dd64fee805fcb8bccc273ecd7d886 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:09:31 +0000 Subject: [PATCH 39/60] perf: replace skills file-copy with junction, matching agents pattern MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Skills (~/.copilot/skills) is now a junction pointing directly at ~/.awesome-copilot/skills, the same approach already used for agents. This eliminates the 460+ file copy on every run — publish-global is now near-instant after first setup. Falls back to symlink then full copy if junction creation fails. Existing real directories are replaced with a junction automatically. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/publish-global.ps1 | 64 +++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 29 deletions(-) diff --git a/scripts/publish-global.ps1 b/scripts/publish-global.ps1 index 346a893..47880d0 100644 --- a/scripts/publish-global.ps1 +++ b/scripts/publish-global.ps1 @@ -133,44 +133,50 @@ if (-not $SkipSkills) { else { Log "Publishing skills: $SkillsSource --> $SkillsTarget" - if (-not $DryRun -and -not (Test-Path $SkillsTarget)) { - New-Item -ItemType Directory -Path $SkillsTarget -Force | Out-Null + if ($DryRun) { + Log "[DryRun] Would link/copy skills folder to $SkillsTarget" } + else { + $linked = $false - $added = 0; $updated = 0; $unchanged = 0 - - Get-ChildItem $SkillsSource -Directory | ForEach-Object { - $skillName = $_.Name - $skillSrc = $_.FullName - $skillDest = Join-Path $SkillsTarget $skillName - - if ($DryRun) { - Log "[DryRun] Would publish skill: $skillName" - $added++ - return + if (Test-Path $SkillsTarget) { + $item = Get-Item $SkillsTarget -Force + if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { + Log "Skills already linked at $SkillsTarget - skipping" + $linked = $true + } + else { + # Real directory exists — remove it so we can replace with a junction + Log "Replacing existing skills directory with junction..." 'WARN' + Remove-Item $SkillsTarget -Recurse -Force + } } - if (-not (Test-Path $skillDest)) { - New-Item -ItemType Directory -Path $skillDest -Force | Out-Null - } + if (-not $linked) { + $parent = Split-Path $SkillsTarget -Parent + if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null } - Get-ChildItem $skillSrc -File -Recurse | ForEach-Object { - $rel = $_.FullName.Substring($skillSrc.Length).TrimStart('\','/') - $dest = Join-Path $skillDest $rel - $destDir = Split-Path $dest -Parent - if (-not (Test-Path $destDir)) { New-Item -ItemType Directory -Path $destDir -Force | Out-Null } - - $srcHash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash - $dstHash = if (Test-Path $dest) { (Get-FileHash $dest -Algorithm SHA256).Hash } else { $null } - if ($srcHash -ne $dstHash) { - Copy-Item $_.FullName $dest -Force - if ($dstHash) { $updated++ } else { $added++ } + try { + cmd /c mklink /J `"$SkillsTarget`" `"$SkillsSource`" | Out-Null + Log "Created junction: $SkillsTarget --> $SkillsSource" + } + catch { + Log "Junction failed ($($_.Exception.Message)); trying symlink" 'WARN' + try { + New-Item -ItemType SymbolicLink -Path $SkillsTarget -Target $SkillsSource -Force | Out-Null + Log "Created symlink: $SkillsTarget --> $SkillsSource" + } + catch { + Log "Symlink failed; copying files instead" 'WARN' + New-Item -ItemType Directory -Path $SkillsTarget -Force | Out-Null + Copy-Item (Join-Path $SkillsSource '*') $SkillsTarget -Recurse -Force + Log "Copied skills to $SkillsTarget" + } } - else { $unchanged++ } } } - Log "Skills: added=$added updated=$updated unchanged=$unchanged --> $SkillsTarget" + Log "Skills: done." # Ensure VS Code is configured to discover skills $vsCodeSettings = Join-Path $env:APPDATA 'Code\User\settings.json' From e22578debd2558c8015aa95d92555d45897e2edb Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 12:14:32 +0000 Subject: [PATCH 40/60] refactor(publish-global): junction all 5 categories via shared helper - Replace category-specific publish blocks with a single Publish-Junction helper - Add Instructions, Hooks, Workflows categories alongside existing Agents and Skills - Add -SkipInstructions, -SkipHooks, -SkipWorkflows params - Add -InstructionsTarget, -HooksTarget, -WorkflowsTarget params (default to ~/.copilot/{instructions,hooks,workflows}) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/publish-global.ps1 | 255 ++++++++++++++----------------------- 1 file changed, 95 insertions(+), 160 deletions(-) diff --git a/scripts/publish-global.ps1 b/scripts/publish-global.ps1 index 47880d0..534a288 100644 --- a/scripts/publish-global.ps1 +++ b/scripts/publish-global.ps1 @@ -1,47 +1,47 @@ <# Publish Global Copilot Resources -Publishes two categories of resources from the local awesome-copilot cache to -global locations where they are always available across all workspaces/repos: +Publishes all resource categories from the local awesome-copilot cache to +global locations via junctions (no file duplication — always in sync with +the cache after a pull). - Agents --> VS Code user agents folder (available in Copilot Chat globally) - Default: %APPDATA%\Code\User\prompts\ - Strategy: symlink / junction first, then file-copy fallback + Agents --> %APPDATA%\Code\User\prompts\ + Skills --> ~/.copilot/skills/ + Instructions --> ~/.copilot/instructions/ + Hooks --> ~/.copilot/hooks/ + Workflows --> ~/.copilot/workflows/ - Skills --> Personal skills directory (loaded on-demand by VS Code Agent mode / Copilot CLI) - Default: ~\.copilot\skills\ - Strategy: mirror each skill subdirectory (incremental copy) +Each category is linked (junction/symlink) so that running sync-awesome-copilot.ps1 +immediately reflects everywhere — no re-publish needed after a cache update. Usage: - # Publish both agents and skills (default) .\publish-global.ps1 - # Publish only agents - .\publish-global.ps1 -SkipSkills - - # Publish only skills - .\publish-global.ps1 -SkipAgents + # Skip specific categories + .\publish-global.ps1 -SkipSkills -SkipHooks # Override VS Code agents folder (e.g. for a named profile) .\publish-global.ps1 -AgentsTarget "$env:APPDATA\Code\User\profiles\MyProfile\prompts" - # Dry run - show what would happen + # Dry run .\publish-global.ps1 -DryRun Notes: - - Agents are linked (not copied) where possible so that sync updates are - immediately reflected in VS Code without re-running this script. - - Skills are copied individually so each skill directory is self-contained - under ~/.copilot/skills//. - - Run after sync-awesome-copilot.ps1, or add to the scheduled task via - install-scheduled-task.ps1. + - Existing real directories are replaced with a junction automatically. + - Falls back to symlink, then full copy if junction creation fails. #> [CmdletBinding()] param( - [string]$SourceRoot = "$HOME/.awesome-copilot", - [string]$AgentsTarget = (Join-Path $env:APPDATA 'Code\User\prompts'), - [string]$SkillsTarget = (Join-Path $HOME '.copilot\skills'), + [string]$SourceRoot = "$HOME/.awesome-copilot", + [string]$AgentsTarget = (Join-Path $env:APPDATA 'Code\User\prompts'), + [string]$SkillsTarget = (Join-Path $HOME '.copilot\skills'), + [string]$InstructionsTarget = (Join-Path $HOME '.copilot\instructions'), + [string]$HooksTarget = (Join-Path $HOME '.copilot\hooks'), + [string]$WorkflowsTarget = (Join-Path $HOME '.copilot\workflows'), [switch]$SkipAgents, [switch]$SkipSkills, + [switch]$SkipInstructions, + [switch]$SkipHooks, + [switch]$SkipWorkflows, [switch]$DryRun ) @@ -54,161 +54,96 @@ function Log($m, [string]$level = 'INFO') { Write-Host "[$ts][$level] $m" -ForegroundColor $color } -$AgentsSource = Join-Path $SourceRoot 'agents' -$SkillsSource = Join-Path $SourceRoot 'skills' - #endregion # Initialisation -#region Agents -if (-not $SkipAgents) { - if (-not (Test-Path $AgentsSource)) { - Log "Agents source not found: $AgentsSource (run sync-awesome-copilot.ps1 first)" 'WARN' +#region Junction helper +function Publish-Junction { + param([string]$Category, [string]$Source, [string]$Target) + + if (-not (Test-Path $Source)) { + Log "$Category source not found: $Source (run sync-awesome-copilot.ps1 first)" 'WARN' + return + } + + Log "Publishing $Category`: $Source --> $Target" + + if ($DryRun) { + Log "[DryRun] Would link $Category to $Target" + return } - else { - Log "Publishing agents: $AgentsSource --> $AgentsTarget" - if ($DryRun) { - Log "[DryRun] Would link/copy agents folder to $AgentsTarget" + if (Test-Path $Target) { + $item = Get-Item $Target -Force + if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { + Log "$Category already linked at $Target - skipping" + return } - else { - # Attempt junction first (no elevation required on Windows), then symlink, then copy - $linked = $false - - if (Test-Path $AgentsTarget) { - $item = Get-Item $AgentsTarget -Force - if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { - Log "Agents already linked at $AgentsTarget - skipping" - $linked = $true - } - else { - # Exists as a real directory - update files in place rather than replacing - Log "Agents folder exists as real directory; updating files in place" - Get-ChildItem $AgentsSource -File | ForEach-Object { - $dest = Join-Path $AgentsTarget $_.Name - $srcHash = (Get-FileHash $_.FullName -Algorithm SHA256).Hash - $dstHash = if (Test-Path $dest) { (Get-FileHash $dest -Algorithm SHA256).Hash } else { $null } - if ($srcHash -ne $dstHash) { - Copy-Item $_.FullName $dest -Force - Log " Updated: $($_.Name)" - } - } - $linked = $true - } - } + # Real directory — remove so we can replace with a junction + Log "Replacing existing $Category directory with junction..." 'WARN' + Remove-Item $Target -Recurse -Force + } - if (-not $linked) { - $parent = Split-Path $AgentsTarget -Parent - if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null } - - try { - cmd /c mklink /J `"$AgentsTarget`" `"$AgentsSource`" | Out-Null - Log "Created junction: $AgentsTarget --> $AgentsSource" - } - catch { - Log "Junction failed ($($_.Exception.Message)); trying symlink" 'WARN' - try { - New-Item -ItemType SymbolicLink -Path $AgentsTarget -Target $AgentsSource -Force | Out-Null - Log "Created symlink: $AgentsTarget --> $AgentsSource" - } - catch { - Log "Symlink failed; copying files instead" 'WARN' - New-Item -ItemType Directory -Path $AgentsTarget -Force | Out-Null - Copy-Item (Join-Path $AgentsSource '*') $AgentsTarget -Force - Log "Copied agents to $AgentsTarget" - } - } - } + $parent = Split-Path $Target -Parent + if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null } + + try { + cmd /c mklink /J `"$Target`" `"$Source`" | Out-Null + Log "Created junction: $Target --> $Source" + } + catch { + Log "Junction failed ($($_.Exception.Message)); trying symlink" 'WARN' + try { + New-Item -ItemType SymbolicLink -Path $Target -Target $Source -Force | Out-Null + Log "Created symlink: $Target --> $Source" + } + catch { + Log "Symlink failed; copying files instead" 'WARN' + New-Item -ItemType Directory -Path $Target -Force | Out-Null + Copy-Item (Join-Path $Source '*') $Target -Recurse -Force + Log "Copied $Category to $Target" } - Log "Agents: done. Restart VS Code if agents do not appear immediately." } } -#endregion # Agents +#endregion # Junction helper + +if (-not $SkipAgents) { Publish-Junction 'Agents' (Join-Path $SourceRoot 'agents') $AgentsTarget } +if (-not $SkipSkills) { Publish-Junction 'Skills' (Join-Path $SourceRoot 'skills') $SkillsTarget } +if (-not $SkipInstructions) { Publish-Junction 'Instructions' (Join-Path $SourceRoot 'instructions') $InstructionsTarget } +if (-not $SkipHooks) { Publish-Junction 'Hooks' (Join-Path $SourceRoot 'hooks') $HooksTarget } +if (-not $SkipWorkflows) { Publish-Junction 'Workflows' (Join-Path $SourceRoot 'workflows') $WorkflowsTarget } -#region Skills -if (-not $SkipSkills) { - if (-not (Test-Path $SkillsSource)) { - Log "Skills source not found: $SkillsSource (run sync-awesome-copilot.ps1 first)" 'WARN' +#region VS Code settings — ensure skills discovery is configured +if (-not $SkipSkills -and -not $DryRun) { + $vsCodeSettings = Join-Path $env:APPDATA 'Code\User\settings.json' + if (-not (Test-Path $vsCodeSettings)) { + Log "VS Code settings.json not found — skills discovery not configured. Open VS Code once then re-run." 'WARN' } else { - Log "Publishing skills: $SkillsSource --> $SkillsTarget" - - if ($DryRun) { - Log "[DryRun] Would link/copy skills folder to $SkillsTarget" - } - else { - $linked = $false - - if (Test-Path $SkillsTarget) { - $item = Get-Item $SkillsTarget -Force - if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { - Log "Skills already linked at $SkillsTarget - skipping" - $linked = $true - } - else { - # Real directory exists — remove it so we can replace with a junction - Log "Replacing existing skills directory with junction..." 'WARN' - Remove-Item $SkillsTarget -Recurse -Force - } + try { + $s = Get-Content $vsCodeSettings -Raw | ConvertFrom-Json + $changed = $false + if (-not $s.'chat.useAgentSkills') { + $s | Add-Member -NotePropertyName 'chat.useAgentSkills' -NotePropertyValue $true -Force + $changed = $true } - - if (-not $linked) { - $parent = Split-Path $SkillsTarget -Parent - if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null } - - try { - cmd /c mklink /J `"$SkillsTarget`" `"$SkillsSource`" | Out-Null - Log "Created junction: $SkillsTarget --> $SkillsSource" - } - catch { - Log "Junction failed ($($_.Exception.Message)); trying symlink" 'WARN' - try { - New-Item -ItemType SymbolicLink -Path $SkillsTarget -Target $SkillsSource -Force | Out-Null - Log "Created symlink: $SkillsTarget --> $SkillsSource" - } - catch { - Log "Symlink failed; copying files instead" 'WARN' - New-Item -ItemType Directory -Path $SkillsTarget -Force | Out-Null - Copy-Item (Join-Path $SkillsSource '*') $SkillsTarget -Recurse -Force - Log "Copied skills to $SkillsTarget" - } - } + $loc = '~/.copilot/skills/**' + if (-not $s.'chat.agentSkillsLocations' -or -not $s.'chat.agentSkillsLocations'.$loc) { + $locs = if ($s.'chat.agentSkillsLocations') { $s.'chat.agentSkillsLocations' } else { [pscustomobject]@{} } + $locs | Add-Member -NotePropertyName $loc -NotePropertyValue $true -Force + $s | Add-Member -NotePropertyName 'chat.agentSkillsLocations' -NotePropertyValue $locs -Force + $changed = $true } - } - - Log "Skills: done." - - # Ensure VS Code is configured to discover skills - $vsCodeSettings = Join-Path $env:APPDATA 'Code\User\settings.json' - if (-not (Test-Path $vsCodeSettings)) { - Log "VS Code settings.json not found at $vsCodeSettings — skills discovery not configured. Open VS Code once to generate it, then re-run." 'WARN' - } - else { - try { - $s = Get-Content $vsCodeSettings -Raw | ConvertFrom-Json - $changed = $false - if (-not $s.'chat.useAgentSkills') { - $s | Add-Member -NotePropertyName 'chat.useAgentSkills' -NotePropertyValue $true -Force - $changed = $true - } - $loc = '~/.copilot/skills/**' - if (-not $s.'chat.agentSkillsLocations' -or -not $s.'chat.agentSkillsLocations'.$loc) { - $locs = if ($s.'chat.agentSkillsLocations') { $s.'chat.agentSkillsLocations' } else { [pscustomobject]@{} } - $locs | Add-Member -NotePropertyName $loc -NotePropertyValue $true -Force - $s | Add-Member -NotePropertyName 'chat.agentSkillsLocations' -NotePropertyValue $locs -Force - $changed = $true - } - if ($changed) { - $s | ConvertTo-Json -Depth 5 | Set-Content $vsCodeSettings -Encoding UTF8 - Log "Configured chat.useAgentSkills and chat.agentSkillsLocations in VS Code settings" - } + if ($changed) { + $s | ConvertTo-Json -Depth 5 | Set-Content $vsCodeSettings -Encoding UTF8 + Log "Configured chat.useAgentSkills and chat.agentSkillsLocations in VS Code settings" } - catch { Log "Could not update VS Code settings: $_" 'WARN' } } + catch { Log "Could not update VS Code settings: $_" 'WARN' } } } - -#endregion # Skills +#endregion # VS Code settings if ($DryRun) { Log "[DryRun] No changes made." } else { Log "Global publish complete." } + From 5030480465f7b1cea0c41ff8b9431843f3576fa0 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:19:47 +0000 Subject: [PATCH 41/60] refactor: remove global publish step entirely Global agent/skills junctions pollute the picker with hundreds of duplicates. Per-repo cherry-picking via init-repo.ps1 is the correct workflow. - Delete scripts/publish-global.ps1 - Remove -SkipPublish param and Step 2 block from configure.ps1 - Update README: simplified What This Does, What goes where table, Quick Start, Scripts reference, and Troubleshooting Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 60 +++++---------- configure.ps1 | 29 ++------ scripts/publish-global.ps1 | 149 ------------------------------------- 3 files changed, 26 insertions(+), 212 deletions(-) delete mode 100644 scripts/publish-global.ps1 diff --git a/README.md b/README.md index ac23232..0c04687 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,21 @@ # VS Code Copilot Resource Sync -A collection of PowerShell scripts to sync, publish, and manage [GitHub Copilot](https://github.com/features/copilot) resources from the [awesome-copilot](https://github.com/github/awesome-copilot) community repository — globally to VS Code and per-repo to `.github/`. +A collection of PowerShell scripts to sync and manage [GitHub Copilot](https://github.com/features/copilot) resources from the [awesome-copilot](https://github.com/github/awesome-copilot) community repository — cherry-picking exactly what each repo needs into `.github/`. ## 🎯 What This Does 1. **Syncs** the latest agents, instructions, hooks, workflows, and skills from [awesome-copilot](https://github.com/github/awesome-copilot) into a local cache (`~/.awesome-copilot/`) -2. **Publishes globally** — agents to VS Code's user prompts folder (available in all workspaces), skills to `~/.copilot/skills/` -3. **Initialises repos** — intelligently recommends and installs resources into a repo's `.github/` folder based on detected language/framework, with full install/update/remove lifecycle management +2. **Initialises repos** — intelligently recommends and installs resources into a repo's `.github/` folder based on detected language/framework, with full install/update/remove lifecycle management ### What goes where -| Resource | Scope | Location | -| ---------------- | ----------- | ---------------------------------------------------------------- | -| **Agents** | 🌐 Global | `%APPDATA%\Code\User\prompts\` — available across all workspaces | -| **Skills** | 🌐 Global | `~/.copilot/skills/` — loaded by Copilot coding agent & CLI | -| **Instructions** | 📁 Per-repo | `.github/instructions/` | -| **Hooks** | 📁 Per-repo | `.github/hooks//` | -| **Workflows** | 📁 Per-repo | `.github/workflows/` | -| **Agents** | 📁 Per-repo | `.github/agents/` | +| Resource | Location | +| ---------------- | ----------------------- | +| **Agents** | `.github/agents/` | +| **Instructions** | `.github/instructions/` | +| **Hooks** | `.github/hooks//` | +| **Workflows** | `.github/workflows/` | +| **Skills** | `.github/skills/` | ## 📋 Prerequisites @@ -38,10 +36,10 @@ cd vscode-copilot-sync ### 2. Run the Configurator ```powershell -# Sync from GitHub, publish globally, and optionally configure your current repo +# Sync from GitHub and optionally configure your current repo .\configure.ps1 -# Sync + publish only (no repo setup) +# Sync only (no repo setup) .\configure.ps1 -SkipInit # Preview everything without writing any files @@ -57,7 +55,7 @@ cd C:\Projects\my-app .\configure.ps1 # Or target a repo without cd-ing first -.\configure.ps1 -SkipSync -SkipPublish -RepoPath "C:\Projects\my-app" +.\configure.ps1 -SkipSync -RepoPath "C:\Projects\my-app" # Or call the script directly .\scripts\init-repo.ps1 @@ -102,16 +100,15 @@ Locally modified files are flagged with `[~]` before removal so you don't accide ### `configure.ps1` — Main entry point -Chains sync → publish → repo init in one command. +Chains sync → repo init in one command. ```powershell -.\configure.ps1 # Full run -.\configure.ps1 -SkipInit # Sync + publish only -.\configure.ps1 -SkipSync -SkipInit # Re-publish only -.\configure.ps1 -SkipSync -SkipPublish # Repo init only -.\configure.ps1 -SkipSync -SkipPublish -Uninstall # Remove resources -.\configure.ps1 -RepoPath "C:\Projects\my-app" # Target specific repo -.\configure.ps1 -DryRun # Preview all changes +.\configure.ps1 # Full run +.\configure.ps1 -SkipInit # Sync only +.\configure.ps1 -SkipSync # Repo init only +.\configure.ps1 -SkipSync -Uninstall # Remove resources +.\configure.ps1 -RepoPath "C:\Projects\my-app" # Target specific repo +.\configure.ps1 -DryRun # Preview all changes ``` --- @@ -134,19 +131,6 @@ Clones (first run) or pulls (subsequent runs) `github/awesome-copilot` as a spar --- -### `scripts/publish-global.ps1` — Publish globally - -Publishes agents to VS Code's user prompts folder and skills to `~/.copilot/skills/`. - -```powershell -.\scripts\publish-global.ps1 # Publish all -.\scripts\publish-global.ps1 -DryRun # Preview -.\scripts\publish-global.ps1 -SkipAgents # Skills only -.\scripts\publish-global.ps1 -AgentsTarget "$env:APPDATA\Code\User\profiles\Work\prompts" -``` - ---- - ### `scripts/init-repo.ps1` — Configure a repo Interactively selects and installs Copilot resources into `.github/`. @@ -206,12 +190,6 @@ Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser The script auto-recovers (`git reset --hard origin/HEAD`). If it persists, delete `~/.awesome-copilot` and re-run — it will re-clone fresh. -### Files not appearing in VS Code - -1. Restart VS Code -2. Check your active profile: `Ctrl+Shift+P` → "Preferences: Show Profiles" -3. Verify: `Get-ChildItem "$env:APPDATA\Code\User\prompts\"` - ## 📊 Logs Sync logs are written to `scripts/logs/sync-YYYYMMDD-HHMMSS.log`: diff --git a/configure.ps1 b/configure.ps1 index cab7224..adc360a 100644 --- a/configure.ps1 +++ b/configure.ps1 @@ -5,31 +5,26 @@ Main entry point for all Copilot resource management operations. Chains the scripts in the correct order: 1. sync-awesome-copilot.ps1 -- fetch latest from github/awesome-copilot - 2. publish-global.ps1 -- publish agents + skills globally - 3. init-repo.ps1 -- (prompted) per-repo .github/ setup + 2. init-repo.ps1 -- (prompted) per-repo .github/ setup Usage: - # Full interactive run: sync + publish + prompt for init-repo + # Full interactive run: sync + prompt for init-repo .\configure.ps1 - # Sync + publish only (skip init-repo prompt) + # Sync only (skip repo setup) .\configure.ps1 -SkipInit - # Re-publish only (cache already up to date) - .\configure.ps1 -SkipSync -SkipInit - # Preview without writing any files .\configure.ps1 -DryRun # Init a specific repo (not the current working directory) - .\configure.ps1 -SkipSync -SkipPublish -RepoPath "C:\Projects\my-app" + .\configure.ps1 -SkipSync -RepoPath "C:\Projects\my-app" # Remove installed .github/ resources from the current repo .\configure.ps1 -Uninstall #> [CmdletBinding()] param( [switch]$SkipSync, - [switch]$SkipPublish, [switch]$SkipInit, [switch]$Uninstall, # Remove installed .github/ resources via init-repo -Uninstall [string]$RepoPath = (Get-Location).Path, @@ -63,7 +58,7 @@ if (Test-Path $manifest) { Log "No local cache found — sync will download everything fresh." 'WARN' } -if ($Uninstall) { $SkipSync = $true; $SkipPublish = $true } +if ($Uninstall) { $SkipSync = $true } #endregion # Initialisation @@ -78,17 +73,7 @@ if (-not $SkipSync) { #endregion # Step 1 -#region Step 2 — Publish globally -if (-not $SkipPublish) { - Step "Publish agents + skills globally" - $publishArgs = @{} - if ($DryRun) { $publishArgs['DryRun'] = $true } - & (Join-Path $ScriptDir 'publish-global.ps1') @publishArgs -} - -#endregion # Step 2 - -#region Step 3 — Init repo +#region Step 2 — Init repo if (-not $SkipInit) { # If a subscriptions manifest exists for the current repo, offer to check for updates first $subscriptionsFile = Join-Path $RepoPath '.github\.copilot-subscriptions.json' @@ -116,7 +101,7 @@ if (-not $SkipInit) { } } -#endregion # Step 3 +#endregion # Step 2 Write-Host "" Log "Done." 'SUCCESS' diff --git a/scripts/publish-global.ps1 b/scripts/publish-global.ps1 deleted file mode 100644 index 534a288..0000000 --- a/scripts/publish-global.ps1 +++ /dev/null @@ -1,149 +0,0 @@ -<# -Publish Global Copilot Resources - -Publishes all resource categories from the local awesome-copilot cache to -global locations via junctions (no file duplication — always in sync with -the cache after a pull). - - Agents --> %APPDATA%\Code\User\prompts\ - Skills --> ~/.copilot/skills/ - Instructions --> ~/.copilot/instructions/ - Hooks --> ~/.copilot/hooks/ - Workflows --> ~/.copilot/workflows/ - -Each category is linked (junction/symlink) so that running sync-awesome-copilot.ps1 -immediately reflects everywhere — no re-publish needed after a cache update. - -Usage: - .\publish-global.ps1 - - # Skip specific categories - .\publish-global.ps1 -SkipSkills -SkipHooks - - # Override VS Code agents folder (e.g. for a named profile) - .\publish-global.ps1 -AgentsTarget "$env:APPDATA\Code\User\profiles\MyProfile\prompts" - - # Dry run - .\publish-global.ps1 -DryRun - -Notes: - - Existing real directories are replaced with a junction automatically. - - Falls back to symlink, then full copy if junction creation fails. -#> -[CmdletBinding()] param( - [string]$SourceRoot = "$HOME/.awesome-copilot", - [string]$AgentsTarget = (Join-Path $env:APPDATA 'Code\User\prompts'), - [string]$SkillsTarget = (Join-Path $HOME '.copilot\skills'), - [string]$InstructionsTarget = (Join-Path $HOME '.copilot\instructions'), - [string]$HooksTarget = (Join-Path $HOME '.copilot\hooks'), - [string]$WorkflowsTarget = (Join-Path $HOME '.copilot\workflows'), - [switch]$SkipAgents, - [switch]$SkipSkills, - [switch]$SkipInstructions, - [switch]$SkipHooks, - [switch]$SkipWorkflows, - [switch]$DryRun -) - -#region Initialisation -$ErrorActionPreference = 'Stop' - -function Log($m, [string]$level = 'INFO') { - $ts = (Get-Date).ToString('s') - $color = switch ($level) { 'ERROR' { 'Red' } 'WARN' { 'Yellow' } default { 'Cyan' } } - Write-Host "[$ts][$level] $m" -ForegroundColor $color -} - -#endregion # Initialisation - -#region Junction helper -function Publish-Junction { - param([string]$Category, [string]$Source, [string]$Target) - - if (-not (Test-Path $Source)) { - Log "$Category source not found: $Source (run sync-awesome-copilot.ps1 first)" 'WARN' - return - } - - Log "Publishing $Category`: $Source --> $Target" - - if ($DryRun) { - Log "[DryRun] Would link $Category to $Target" - return - } - - if (Test-Path $Target) { - $item = Get-Item $Target -Force - if ($item.Attributes -band [IO.FileAttributes]::ReparsePoint) { - Log "$Category already linked at $Target - skipping" - return - } - # Real directory — remove so we can replace with a junction - Log "Replacing existing $Category directory with junction..." 'WARN' - Remove-Item $Target -Recurse -Force - } - - $parent = Split-Path $Target -Parent - if (-not (Test-Path $parent)) { New-Item -ItemType Directory -Path $parent -Force | Out-Null } - - try { - cmd /c mklink /J `"$Target`" `"$Source`" | Out-Null - Log "Created junction: $Target --> $Source" - } - catch { - Log "Junction failed ($($_.Exception.Message)); trying symlink" 'WARN' - try { - New-Item -ItemType SymbolicLink -Path $Target -Target $Source -Force | Out-Null - Log "Created symlink: $Target --> $Source" - } - catch { - Log "Symlink failed; copying files instead" 'WARN' - New-Item -ItemType Directory -Path $Target -Force | Out-Null - Copy-Item (Join-Path $Source '*') $Target -Recurse -Force - Log "Copied $Category to $Target" - } - } -} - -#endregion # Junction helper - -if (-not $SkipAgents) { Publish-Junction 'Agents' (Join-Path $SourceRoot 'agents') $AgentsTarget } -if (-not $SkipSkills) { Publish-Junction 'Skills' (Join-Path $SourceRoot 'skills') $SkillsTarget } -if (-not $SkipInstructions) { Publish-Junction 'Instructions' (Join-Path $SourceRoot 'instructions') $InstructionsTarget } -if (-not $SkipHooks) { Publish-Junction 'Hooks' (Join-Path $SourceRoot 'hooks') $HooksTarget } -if (-not $SkipWorkflows) { Publish-Junction 'Workflows' (Join-Path $SourceRoot 'workflows') $WorkflowsTarget } - -#region VS Code settings — ensure skills discovery is configured -if (-not $SkipSkills -and -not $DryRun) { - $vsCodeSettings = Join-Path $env:APPDATA 'Code\User\settings.json' - if (-not (Test-Path $vsCodeSettings)) { - Log "VS Code settings.json not found — skills discovery not configured. Open VS Code once then re-run." 'WARN' - } - else { - try { - $s = Get-Content $vsCodeSettings -Raw | ConvertFrom-Json - $changed = $false - if (-not $s.'chat.useAgentSkills') { - $s | Add-Member -NotePropertyName 'chat.useAgentSkills' -NotePropertyValue $true -Force - $changed = $true - } - $loc = '~/.copilot/skills/**' - if (-not $s.'chat.agentSkillsLocations' -or -not $s.'chat.agentSkillsLocations'.$loc) { - $locs = if ($s.'chat.agentSkillsLocations') { $s.'chat.agentSkillsLocations' } else { [pscustomobject]@{} } - $locs | Add-Member -NotePropertyName $loc -NotePropertyValue $true -Force - $s | Add-Member -NotePropertyName 'chat.agentSkillsLocations' -NotePropertyValue $locs -Force - $changed = $true - } - if ($changed) { - $s | ConvertTo-Json -Depth 5 | Set-Content $vsCodeSettings -Encoding UTF8 - Log "Configured chat.useAgentSkills and chat.agentSkillsLocations in VS Code settings" - } - } - catch { Log "Could not update VS Code settings: $_" 'WARN' } - } -} -#endregion # VS Code settings - -if ($DryRun) { Log "[DryRun] No changes made." } -else { Log "Global publish complete." } - From 107ed66a8ddf5f7723364651da093d4eb621bd7c Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:24:04 +0000 Subject: [PATCH 42/60] fix: suppress redundant update-check output when no subscriptions exist - configure.ps1: only run update-repo step if manifest has >0 entries - update-repo.ps1: silent exit (no WARN) when manifest empty or missing (WARN is noise when called automatically by configure on a fresh repo) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- configure.ps1 | 15 +++++++++------ scripts/update-repo.ps1 | 4 +--- 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/configure.ps1 b/configure.ps1 index adc360a..7fc3639 100644 --- a/configure.ps1 +++ b/configure.ps1 @@ -75,14 +75,17 @@ if (-not $SkipSync) { #region Step 2 — Init repo if (-not $SkipInit) { - # If a subscriptions manifest exists for the current repo, offer to check for updates first + # If a subscriptions manifest exists with entries, check for updates first $subscriptionsFile = Join-Path $RepoPath '.github\.copilot-subscriptions.json' if (Test-Path $subscriptionsFile) { - Step "Check for updates to subscribed repo resources" - $updateArgs = @{} - if ($DryRun) { $updateArgs['DryRun'] = $true } - if ($RepoPath) { $updateArgs['RepoPath'] = $RepoPath } - & (Join-Path $ScriptDir 'update-repo.ps1') @updateArgs + $subCount = try { (@((Get-Content $subscriptionsFile -Raw | ConvertFrom-Json).subscriptions)).Count } catch { 0 } + if ($subCount -gt 0) { + Step "Check for updates to subscribed repo resources" + $updateArgs = @{} + if ($DryRun) { $updateArgs['DryRun'] = $true } + if ($RepoPath) { $updateArgs['RepoPath'] = $RepoPath } + & (Join-Path $ScriptDir 'update-repo.ps1') @updateArgs + } } Step "Init repo" diff --git a/scripts/update-repo.ps1 b/scripts/update-repo.ps1 index 3f926ed..77ae79a 100644 --- a/scripts/update-repo.ps1 +++ b/scripts/update-repo.ps1 @@ -68,8 +68,7 @@ $GithubDir = Join-Path $RepoPath '.github' $ManifestPath = Join-Path $GithubDir '.copilot-subscriptions.json' if (-not (Test-Path $ManifestPath)) { - Log "No subscriptions manifest found: $ManifestPath" 'WARN' - Log "Run init-repo.ps1 first to subscribe to resources." + Log "No subscriptions manifest found — run init-repo.ps1 first." exit 0 } @@ -77,7 +76,6 @@ $manifest = Get-Content $ManifestPath -Raw | ConvertFrom-Json $subs = @($manifest.subscriptions) if (-not $subs -or $subs.Count -eq 0) { - Log "No subscriptions recorded in manifest." 'WARN' exit 0 } From 30bfa60e9e54788e526c79124cecaba07ce3c880 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:26:34 +0000 Subject: [PATCH 43/60] fix: skip uninstall prompt when no subscriptions recorded Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- configure.ps1 | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/configure.ps1 b/configure.ps1 index 7fc3639..f5952bd 100644 --- a/configure.ps1 +++ b/configure.ps1 @@ -77,6 +77,7 @@ if (-not $SkipSync) { if (-not $SkipInit) { # If a subscriptions manifest exists with entries, check for updates first $subscriptionsFile = Join-Path $RepoPath '.github\.copilot-subscriptions.json' + $subCount = 0 if (Test-Path $subscriptionsFile) { $subCount = try { (@((Get-Content $subscriptionsFile -Raw | ConvertFrom-Json).subscriptions)).Count } catch { 0 } if ($subCount -gt 0) { @@ -89,18 +90,22 @@ if (-not $SkipInit) { } Step "Init repo" - $initPrompt = if ($Uninstall) { "Remove agents/instructions/hooks/workflows/skills from .github/?" } else { "Add agents/instructions/hooks/workflows/skills to .github/ in the current repo?" } - Write-Host " $initPrompt" -ForegroundColor Yellow - Write-Host " [Y] Yes [N] No (default): " -NoNewline -ForegroundColor Yellow - $answer = (Read-Host).Trim() - if ($answer -match '^[Yy]') { - $initArgs = @{} - if ($DryRun) { $initArgs['DryRun'] = $true } - if ($RepoPath) { $initArgs['RepoPath'] = $RepoPath } - if ($Uninstall) { $initArgs['Uninstall'] = $true } - & (Join-Path $ScriptDir 'init-repo.ps1') @initArgs + if ($Uninstall -and $subCount -eq 0) { + Log "Nothing to uninstall — no subscriptions recorded for this repo." } else { - Log "init-repo skipped." + $initPrompt = if ($Uninstall) { "Remove agents/instructions/hooks/workflows/skills from .github/?" } else { "Add agents/instructions/hooks/workflows/skills to .github/ in the current repo?" } + Write-Host " $initPrompt" -ForegroundColor Yellow + Write-Host " [Y] Yes [N] No (default): " -NoNewline -ForegroundColor Yellow + $answer = (Read-Host).Trim() + if ($answer -match '^[Yy]') { + $initArgs = @{} + if ($DryRun) { $initArgs['DryRun'] = $true } + if ($RepoPath) { $initArgs['RepoPath'] = $RepoPath } + if ($Uninstall) { $initArgs['Uninstall'] = $true } + & (Join-Path $ScriptDir 'init-repo.ps1') @initArgs + } else { + Log "init-repo skipped." + } } } From 48d8e28d0eff92be9f79cec45bec7b80c1dc3795 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:34:13 +0000 Subject: [PATCH 44/60] fix: restore security recommendations for all repos owasp alone required a name match to reach score>=2 threshold. Adding 'security' as universal keyword means: - se-security-reviewer: name match (score 2+) -> star - gem-reviewer: content matches owasp+security (score 2) -> star - stackhawk-security-onboarding: name match -> star Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/init-repo.ps1 | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/scripts/init-repo.ps1 b/scripts/init-repo.ps1 index 79015c1..eb158de 100644 --- a/scripts/init-repo.ps1 +++ b/scripts/init-repo.ps1 @@ -139,8 +139,9 @@ function Detect-RepoStack { } catch {} } - # Always recommend security and code review for every repo + # Always recommend security-focused resources for every repo $recs.Add('owasp') + $recs.Add('security') return @($recs | Sort-Object -Unique) } From 12dcdf1b856f33a5e0a6485d1d3fd034f41bcc92 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:36:25 +0000 Subject: [PATCH 45/60] fix: scope 'security' keyword to agents only, not instructions/workflows 'security' in a filename like power-bi-security-rls scores 2 -> false star. Agents are where security-reviewer agents live; instructions don't need it. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/init-repo.ps1 | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/scripts/init-repo.ps1 b/scripts/init-repo.ps1 index eb158de..60089c9 100644 --- a/scripts/init-repo.ps1 +++ b/scripts/init-repo.ps1 @@ -141,7 +141,6 @@ function Detect-RepoStack { # Always recommend security-focused resources for every repo $recs.Add('owasp') - $recs.Add('security') return @($recs | Sort-Object -Unique) } @@ -707,7 +706,9 @@ if (-not $SkipAgents) { Remove-SubscriptionEntries -ManifestPath $SubscriptionManifestPath -Keys @($toRemove | ForEach-Object { "agents|$($_.Name)" }) } } else { - $selected = Select-Items -Category 'Agents' -Items $catalogue -PreSelected $Agents -Tags $script:Recommendations + # Agents: also score on 'security' since security-reviewer agents are universally useful + $agentTags = @($script:Recommendations) + 'security' | Sort-Object -Unique + $selected = Select-Items -Category 'Agents' -Items $catalogue -PreSelected $Agents -Tags $agentTags foreach ($item in $selected) { $result = Install-File -Src $item.FullPath -DestDir $destDir From dfb21bbb7a662637af05e784fcf50ea365b7c81b Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:38:23 +0000 Subject: [PATCH 46/60] =?UTF-8?q?fix:=20remove=20score=20sort=20tier=20?= =?UTF-8?q?=E2=80=94=20items=20are=20either=20recommended=20or=20alphabeti?= =?UTF-8?q?cal?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Partial score (content-only match) was creating a spurious middle tier, floating e.g. power-bi-security-rls above unrelated alphabetical items. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/init-repo.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/init-repo.ps1 b/scripts/init-repo.ps1 index 60089c9..7c45be2 100644 --- a/scripts/init-repo.ps1 +++ b/scripts/init-repo.ps1 @@ -291,7 +291,7 @@ function Select-Items { $score = Measure-ItemRelevance -ItemName $_.Name -FilePath $_.FullPath -Tags $Tags $_ | Add-Member -NotePropertyName 'IsRecommended' -NotePropertyValue ($score -ge 2) -PassThru -Force | Add-Member -NotePropertyName 'Score' -NotePropertyValue $score -PassThru -Force - } | Sort-Object @{ E={ if ($_.IsRecommended) { 0 } else { 1 } } }, @{ E={ if ($_.AlreadyInstalled) { 0 } else { 1 } } }, @{ E={ -$_.Score } }, Name + } | Sort-Object @{ E={ if ($_.IsRecommended) { 0 } else { 1 } } }, @{ E={ if ($_.AlreadyInstalled) { 0 } else { 1 } } }, Name Write-Host "" Write-Host " === $Category ===" -ForegroundColor Yellow From 56d4216f473d42253713d56a6a91e29ce1fdf5f9 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:41:12 +0000 Subject: [PATCH 47/60] perf: pre-load all 5 catalogues before showing any OGV picker All Build-FlatCatalogue / Build-DirCatalogue calls now happen upfront before the first OGV opens, so there is no lag between pickers. Skills (464 dirs) was the main bottleneck - now hashed before any UI appears. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/init-repo.ps1 | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/scripts/init-repo.ps1 b/scripts/init-repo.ps1 index 7c45be2..638bfab 100644 --- a/scripts/init-repo.ps1 +++ b/scripts/init-repo.ps1 @@ -687,12 +687,24 @@ function Remove-SubscriptionEntries { #endregion # Subscription manifest -#region Agents +#region Pre-load all catalogues $script:AllCatalogues = [System.Collections.Generic.List[object]]::new() +Log "Loading resource catalogues..." +$catAgents = if (-not $SkipAgents) { Build-FlatCatalogue (Join-Path $SourceRoot 'agents') (Join-Path $GithubDir 'agents') '\.agent\.md$' 'agents' } else { @() } +$catInstructions = if (-not $SkipInstructions) { Build-FlatCatalogue (Join-Path $SourceRoot 'instructions') (Join-Path $GithubDir 'instructions') '\.instructions\.md$' 'instructions' } else { @() } +$catHooks = if (-not $SkipHooks) { Build-DirCatalogue (Join-Path $SourceRoot 'hooks') (Join-Path $GithubDir 'hooks') 'hooks' } else { @() } +$catWorkflows = if (-not $SkipWorkflows) { Build-FlatCatalogue (Join-Path $SourceRoot 'workflows') (Join-Path $GithubDir 'workflows') '\.md$' 'workflows' } else { @() } +$catSkills = if (-not $SkipSkills) { Build-DirCatalogue (Join-Path $SourceRoot 'skills') (Join-Path $GithubDir 'skills') 'skills' } else { @() } +Log "Catalogues loaded. Opening pickers..." + +#endregion # Pre-load all catalogues + +#region Agents + if (-not $SkipAgents) { $destDir = Join-Path $GithubDir 'agents' - $catalogue = Build-FlatCatalogue (Join-Path $SourceRoot 'agents') $destDir '\.agent\.md$' 'agents' + $catalogue = $catAgents $script:AllCatalogues.Add([pscustomobject]@{ Category='agents'; Type='file'; Items=$catalogue; DestDir=$destDir }) if ($Uninstall) { @@ -733,7 +745,7 @@ if (-not $SkipAgents) { #region Instructions if (-not $SkipInstructions) { $destDir = Join-Path $GithubDir 'instructions' - $catalogue = Build-FlatCatalogue (Join-Path $SourceRoot 'instructions') $destDir '\.instructions\.md$' 'instructions' + $catalogue = $catInstructions $script:AllCatalogues.Add([pscustomobject]@{ Category='instructions'; Type='file'; Items=$catalogue; DestDir=$destDir }) if ($Uninstall) { @@ -772,7 +784,7 @@ if (-not $SkipInstructions) { #region Hooks if (-not $SkipHooks) { $destDir = Join-Path $GithubDir 'hooks' - $catalogue = Build-DirCatalogue (Join-Path $SourceRoot 'hooks') $destDir 'hooks' + $catalogue = $catHooks $script:AllCatalogues.Add([pscustomobject]@{ Category='hooks'; Type='directory'; Items=$catalogue; DestDir=$destDir }) if ($Uninstall) { @@ -811,7 +823,7 @@ if (-not $SkipHooks) { #region Workflows if (-not $SkipWorkflows) { $destDir = Join-Path $GithubDir 'workflows' - $catalogue = Build-FlatCatalogue (Join-Path $SourceRoot 'workflows') $destDir '\.md$' 'workflows' + $catalogue = $catWorkflows $script:AllCatalogues.Add([pscustomobject]@{ Category='workflows'; Type='file'; Items=$catalogue; DestDir=$destDir }) if ($Uninstall) { @@ -850,7 +862,7 @@ if (-not $SkipWorkflows) { #region Skills if (-not $SkipSkills) { $destDir = Join-Path $GithubDir 'skills' - $catalogue = Build-DirCatalogue (Join-Path $SourceRoot 'skills') $destDir 'skills' + $catalogue = $catSkills $script:AllCatalogues.Add([pscustomobject]@{ Category='skills'; Type='directory'; Items=$catalogue; DestDir=$destDir }) if ($Uninstall) { From a0f26f65de1f3f2334d1f790a90ad86b34becc99 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:44:06 +0000 Subject: [PATCH 48/60] fix: suppress star for agents requiring external setup (MCP/API key) - Add Test-RequiresSetup helper: detects mcp-servers:, _API_KEY, COPILOT_MCP_ patterns - Add RequiresSetup flag to all catalogue items (flat + dir) - IsRecommended now requires score>=2 AND NOT RequiresSetup - Update OGV title: 'Recommended (config-free)' stackhawk-security-onboarding and other MCP-dependent agents will no longer appear as starred recommendations on a vanilla repo. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/init-repo.ps1 | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/scripts/init-repo.ps1 b/scripts/init-repo.ps1 index 638bfab..2b6491f 100644 --- a/scripts/init-repo.ps1 +++ b/scripts/init-repo.ps1 @@ -285,12 +285,12 @@ function Select-Items { return @($selected) } - # Score each item against the detected tech keywords; recommended = score > 0. - # Sort: recommended (highest score first) then remaining alphabetically. + # Score each item against the detected tech keywords; recommended = score >= 2 AND no setup required. $Items = $Items | ForEach-Object { $score = Measure-ItemRelevance -ItemName $_.Name -FilePath $_.FullPath -Tags $Tags - $_ | Add-Member -NotePropertyName 'IsRecommended' -NotePropertyValue ($score -ge 2) -PassThru -Force | - Add-Member -NotePropertyName 'Score' -NotePropertyValue $score -PassThru -Force + $isRec = ($score -ge 2) -and (-not $_.RequiresSetup) + $_ | Add-Member -NotePropertyName 'IsRecommended' -NotePropertyValue $isRec -PassThru -Force | + Add-Member -NotePropertyName 'Score' -NotePropertyValue $score -PassThru -Force } | Sort-Object @{ E={ if ($_.IsRecommended) { 0 } else { 1 } } }, @{ E={ if ($_.AlreadyInstalled) { 0 } else { 1 } } }, Name Write-Host "" @@ -315,7 +315,7 @@ function Select-Items { @{ N='Name'; E={ $_.Name } }, @{ N='Description'; E={ $_.Description } }) - $picked = $display | Out-GridView -Title "Select $Category ★=Recommended [*]=Installed [↑]=Update [~]=Modified" -PassThru + $picked = $display | Out-GridView -Title "Select $Category ★=Recommended (config-free) [*]=Installed [↑]=Update [~]=Modified" -PassThru if (-not $picked) { return @() } $pickedNames = @($picked | Where-Object { $_.Name -ne '-- none / skip --' } | ForEach-Object { $_.Name }) return @($Items | Where-Object { $pickedNames -contains $_.Name }) @@ -586,6 +586,22 @@ function Update-Subscriptions { #region Catalogue builders $totalInstalled = 0 +function Test-RequiresSetup([string]$FilePath) { + # Returns $true if the file/dir requires external setup (MCP server, API key, etc.) + # These items are never starred as recommendations. + $contentFile = $FilePath + if (Test-Path $FilePath -PathType Container) { + $readme = Join-Path $FilePath 'README.md' + if (-not (Test-Path $readme)) { $readme = Join-Path $FilePath 'SKILL.md' } + $contentFile = if (Test-Path $readme) { $readme } else { $null } + } + if (-not $contentFile -or -not (Test-Path $contentFile)) { return $false } + try { + $text = (Get-Content $contentFile -Raw -ErrorAction SilentlyContinue) + return $text -match 'mcp-servers:|_API_KEY\b|COPILOT_MCP_' + } catch { return $false } +} + function Build-FlatCatalogue([string]$CatDir, [string]$DestDir, [string]$Pattern, [string]$Category) { if (-not (Test-Path $CatDir)) { return @() } Get-ChildItem $CatDir -File | Where-Object { $_.Name -match $Pattern } | ForEach-Object { @@ -611,6 +627,7 @@ function Build-FlatCatalogue([string]$CatDir, [string]$DestDir, [string]$Pattern FileName = $_.Name FullPath = $_.FullName Description = Get-Description $_.FullName + RequiresSetup = Test-RequiresSetup $_.FullName AlreadyInstalled = $installed ManagedByScript = $managedByScript UpdateAvailable = $updateAvailable @@ -644,6 +661,7 @@ function Build-DirCatalogue([string]$CatDir, [string]$DestDir, [string]$Category Name = $_.Name FullPath = $_.FullName Description = if (Test-Path $readmePath) { Get-Description $readmePath } else { '' } + RequiresSetup = Test-RequiresSetup $_.FullName AlreadyInstalled = $installed ManagedByScript = $managedByScript UpdateAvailable = $updateAvailable From 1b054114b5f267d9ea23d28652460abc158a405d Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:45:47 +0000 Subject: [PATCH 49/60] feat: add [!] status indicator for items requiring additional setup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Items with mcp-servers/API key requirements show [!] in the Status column and DarkYellow in the console fallback — visible but not starred. Updated OGV title, console legend, and README symbol table. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 1 + scripts/init-repo.ps1 | 9 +++++---- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 0c04687..4031eea 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,7 @@ The picker auto-detects your language/framework and marks relevant items with | `[*]` | Already installed | | `[↑]` | Update available from upstream | | `[~]` | Locally modified since install | +| `[!]` | Requires additional setup (MCP server, API key, etc.) | ### 4. Remove Installed Resources diff --git a/scripts/init-repo.ps1 b/scripts/init-repo.ps1 index 2b6491f..a5db5f8 100644 --- a/scripts/init-repo.ps1 +++ b/scripts/init-repo.ps1 @@ -295,7 +295,7 @@ function Select-Items { Write-Host "" Write-Host " === $Category ===" -ForegroundColor Yellow - Write-Host " [*]=Installed [↑]=Update available [~]=Locally modified ★=Recommended" -ForegroundColor DarkGray + Write-Host " [*]=Installed [↑]=Update available [~]=Locally modified ★=Recommended [!]=Setup required" -ForegroundColor DarkGray # Try Out-GridView (Windows GUI - filterable, multi-select) $ogvAvailable = $false @@ -310,12 +310,13 @@ function Select-Items { if ($_.AlreadyInstalled) { $s += '[*]' } if ($_.UpdateAvailable) { $s += '[↑]' } if ($_.LocallyModified) { $s += '[~]' } + if ($_.RequiresSetup) { $s += '[!]' } $s }}, @{ N='Name'; E={ $_.Name } }, @{ N='Description'; E={ $_.Description } }) - $picked = $display | Out-GridView -Title "Select $Category ★=Recommended (config-free) [*]=Installed [↑]=Update [~]=Modified" -PassThru + $picked = $display | Out-GridView -Title "Select $Category ★=Recommended (config-free) [*]=Installed [↑]=Update [~]=Modified [!]=Setup required" -PassThru if (-not $picked) { return @() } $pickedNames = @($picked | Where-Object { $_.Name -ne '-- none / skip --' } | ForEach-Object { $_.Name }) return @($Items | Where-Object { $pickedNames -contains $_.Name }) @@ -329,8 +330,8 @@ function Select-Items { if ($item.AlreadyInstalled) { $status += '[*]' } if ($item.UpdateAvailable) { $status += '[↑]' } if ($item.LocallyModified) { $status += '[~]' } - $rec = if ($item.IsRecommended) { '[★]' } else { ' ' } - $color = if ($item.UpdateAvailable) { 'Cyan' } elseif ($item.AlreadyInstalled) { 'DarkCyan' } elseif ($item.IsRecommended) { 'Yellow' } else { 'White' } + $rec = if ($item.IsRecommended) { '[★]' } elseif ($item.RequiresSetup) { '[!]' } else { ' ' } + $color = if ($item.UpdateAvailable) { 'Cyan' } elseif ($item.AlreadyInstalled) { 'DarkCyan' } elseif ($item.IsRecommended) { 'Yellow' } elseif ($item.RequiresSetup) { 'DarkYellow' } else { 'White' } Write-Host (" {0,3}. {1} {2,-6} {3}" -f ($i+1), $rec, $status, $item.Name) -ForegroundColor $color if ($item.Description) { Write-Host (" {0}" -f $item.Description) -ForegroundColor DarkGray From 79617f56d946ea4f6dc534fae93967cba31ec4f8 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:48:12 +0000 Subject: [PATCH 50/60] perf: skip loading catalogues for empty categories during uninstall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Should-LoadCatalogue helper returns false when uninstalling and the destination dir is absent or empty — avoids hashing 464 skills dirs when there are no skills to uninstall. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/init-repo.ps1 | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/scripts/init-repo.ps1 b/scripts/init-repo.ps1 index a5db5f8..36f65c0 100644 --- a/scripts/init-repo.ps1 +++ b/scripts/init-repo.ps1 @@ -709,12 +709,19 @@ function Remove-SubscriptionEntries { #region Pre-load all catalogues $script:AllCatalogues = [System.Collections.Generic.List[object]]::new() +# When uninstalling, skip building catalogues for categories with nothing installed +function Should-LoadCatalogue([string]$Category, [string]$DestDir, [switch]$IsSkipped) { + if ($IsSkipped) { return $false } + if ($Uninstall) { return (Test-Path $DestDir) -and ((Get-ChildItem $DestDir -ErrorAction SilentlyContinue | Measure-Object).Count -gt 0) } + return $true +} + Log "Loading resource catalogues..." -$catAgents = if (-not $SkipAgents) { Build-FlatCatalogue (Join-Path $SourceRoot 'agents') (Join-Path $GithubDir 'agents') '\.agent\.md$' 'agents' } else { @() } -$catInstructions = if (-not $SkipInstructions) { Build-FlatCatalogue (Join-Path $SourceRoot 'instructions') (Join-Path $GithubDir 'instructions') '\.instructions\.md$' 'instructions' } else { @() } -$catHooks = if (-not $SkipHooks) { Build-DirCatalogue (Join-Path $SourceRoot 'hooks') (Join-Path $GithubDir 'hooks') 'hooks' } else { @() } -$catWorkflows = if (-not $SkipWorkflows) { Build-FlatCatalogue (Join-Path $SourceRoot 'workflows') (Join-Path $GithubDir 'workflows') '\.md$' 'workflows' } else { @() } -$catSkills = if (-not $SkipSkills) { Build-DirCatalogue (Join-Path $SourceRoot 'skills') (Join-Path $GithubDir 'skills') 'skills' } else { @() } +$catAgents = if (Should-LoadCatalogue 'agents' (Join-Path $GithubDir 'agents') -IsSkipped:$SkipAgents) { Build-FlatCatalogue (Join-Path $SourceRoot 'agents') (Join-Path $GithubDir 'agents') '\.agent\.md$' 'agents' } else { @() } +$catInstructions = if (Should-LoadCatalogue 'instructions' (Join-Path $GithubDir 'instructions') -IsSkipped:$SkipInstructions) { Build-FlatCatalogue (Join-Path $SourceRoot 'instructions') (Join-Path $GithubDir 'instructions') '\.instructions\.md$' 'instructions' } else { @() } +$catHooks = if (Should-LoadCatalogue 'hooks' (Join-Path $GithubDir 'hooks') -IsSkipped:$SkipHooks) { Build-DirCatalogue (Join-Path $SourceRoot 'hooks') (Join-Path $GithubDir 'hooks') 'hooks' } else { @() } +$catWorkflows = if (Should-LoadCatalogue 'workflows' (Join-Path $GithubDir 'workflows') -IsSkipped:$SkipWorkflows) { Build-FlatCatalogue (Join-Path $SourceRoot 'workflows') (Join-Path $GithubDir 'workflows') '\.md$' 'workflows' } else { @() } +$catSkills = if (Should-LoadCatalogue 'skills' (Join-Path $GithubDir 'skills') -IsSkipped:$SkipSkills) { Build-DirCatalogue (Join-Path $SourceRoot 'skills') (Join-Path $GithubDir 'skills') 'skills' } else { @() } Log "Catalogues loaded. Opening pickers..." #endregion # Pre-load all catalogues From 6ddfeb9550aac88ca94d7d6dbeb4ab4acf4e87ee Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:52:59 +0000 Subject: [PATCH 51/60] docs: fix stale publish-global reference in init-repo header comment Also document [!] setup-required indicator in the Notes section. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/init-repo.ps1 | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/scripts/init-repo.ps1 b/scripts/init-repo.ps1 index 36f65c0..d44d70b 100644 --- a/scripts/init-repo.ps1 +++ b/scripts/init-repo.ps1 @@ -37,14 +37,15 @@ Usage: Notes: - Existing files are only overwritten if the source is newer/different. - .github/ is created if it doesn't exist. - - Skills are installed to .github/skills/ for version control with the project. - Global skills (available across all repos) are managed by publish-global.ps1. + - Skills are installed to .github/skills/ for version control with the project, + alongside all other per-repo resources. - A subscription manifest (.github/.copilot-subscriptions.json) is written on each run. Use update-repo.ps1 to check for and apply upstream changes. - The selection UI uses Out-GridView where available (Windows GUI, filterable, multi-select). Falls back to a numbered console menu automatically. - Auto-detects language/framework from repo file signals and pre-marks - recommended instructions/hooks/workflows with ★ in the picker. + recommended (config-free) resources with ★ in the picker. + Items requiring additional setup (MCP server, API key, etc.) are marked [!]. - For new/empty repos, prompts for intent one question at a time. #> [CmdletBinding()] param( From cf8b0f8db87cea09bbcd5d2d695f32e4dcc21834 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 13:57:01 +0000 Subject: [PATCH 52/60] feat: add -Install switch to configure.ps1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .\configure.ps1 -Install skips the Y/N prompt and goes straight to init-repo pickers — symmetric with -Uninstall. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- README.md | 4 ++++ configure.ps1 | 20 +++++++++++++++----- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 4031eea..72860b8 100644 --- a/README.md +++ b/README.md @@ -39,6 +39,9 @@ cd vscode-copilot-sync # Sync from GitHub and optionally configure your current repo .\configure.ps1 +# Sync + go straight to install pickers (no Y/N prompt) +.\configure.ps1 -Install + # Sync only (no repo setup) .\configure.ps1 -SkipInit @@ -105,6 +108,7 @@ Chains sync → repo init in one command. ```powershell .\configure.ps1 # Full run +.\configure.ps1 -Install # Sync + go straight to pickers .\configure.ps1 -SkipInit # Sync only .\configure.ps1 -SkipSync # Repo init only .\configure.ps1 -SkipSync -Uninstall # Remove resources diff --git a/configure.ps1 b/configure.ps1 index f5952bd..2ed5161 100644 --- a/configure.ps1 +++ b/configure.ps1 @@ -11,6 +11,9 @@ Usage: # Full interactive run: sync + prompt for init-repo .\configure.ps1 + # Sync + go straight to install pickers (skip the Y/N prompt) + .\configure.ps1 -Install + # Sync only (skip repo setup) .\configure.ps1 -SkipInit @@ -26,6 +29,7 @@ Usage: [CmdletBinding()] param( [switch]$SkipSync, [switch]$SkipInit, + [switch]$Install, # Skip the Y/N prompt and go straight to init-repo pickers [switch]$Uninstall, # Remove installed .github/ resources via init-repo -Uninstall [string]$RepoPath = (Get-Location).Path, [switch]$DryRun @@ -59,6 +63,7 @@ if (Test-Path $manifest) { } if ($Uninstall) { $SkipSync = $true } +if ($Install) { $SkipInit = $false } # ensure -Install always runs init-repo #endregion # Initialisation @@ -92,16 +97,21 @@ if (-not $SkipInit) { Step "Init repo" if ($Uninstall -and $subCount -eq 0) { Log "Nothing to uninstall — no subscriptions recorded for this repo." + } elseif ($Install -or $Uninstall) { + # -Install or -Uninstall: skip the Y/N prompt, run directly + $initArgs = @{} + if ($DryRun) { $initArgs['DryRun'] = $true } + if ($RepoPath) { $initArgs['RepoPath'] = $RepoPath } + if ($Uninstall) { $initArgs['Uninstall'] = $true } + & (Join-Path $ScriptDir 'init-repo.ps1') @initArgs } else { - $initPrompt = if ($Uninstall) { "Remove agents/instructions/hooks/workflows/skills from .github/?" } else { "Add agents/instructions/hooks/workflows/skills to .github/ in the current repo?" } - Write-Host " $initPrompt" -ForegroundColor Yellow + Write-Host " Add agents/instructions/hooks/workflows/skills to .github/ in the current repo?" -ForegroundColor Yellow Write-Host " [Y] Yes [N] No (default): " -NoNewline -ForegroundColor Yellow $answer = (Read-Host).Trim() if ($answer -match '^[Yy]') { $initArgs = @{} - if ($DryRun) { $initArgs['DryRun'] = $true } - if ($RepoPath) { $initArgs['RepoPath'] = $RepoPath } - if ($Uninstall) { $initArgs['Uninstall'] = $true } + if ($DryRun) { $initArgs['DryRun'] = $true } + if ($RepoPath) { $initArgs['RepoPath'] = $RepoPath } & (Join-Path $ScriptDir 'init-repo.ps1') @initArgs } else { Log "init-repo skipped." From b9b71ff3c9cb6954e5e267815d3d6cc7750245d5 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:01:00 +0000 Subject: [PATCH 53/60] feat: force OGV windows to foreground via Win32 SetForegroundWindow Add Show-OGV wrapper around Out-GridView: - Registers CopilotSync.FgHelper type (EnumWindows/SetForegroundWindow/ShowWindow) - Spins a runspace that polls for the OGV window by partial title match and calls SetForegroundWindow once found (within 4s / 40 polls) - Falls back gracefully if window not found (no error, OGV still works) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/init-repo.ps1 | 62 +++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 60 insertions(+), 2 deletions(-) diff --git a/scripts/init-repo.ps1 b/scripts/init-repo.ps1 index d44d70b..8a3bfd7 100644 --- a/scripts/init-repo.ps1 +++ b/scripts/init-repo.ps1 @@ -74,6 +74,64 @@ function Log($m, [string]$level = 'INFO') { Write-Host "[$ts][$level] $m" -ForegroundColor $color } +# Win32 helper for foreground-forcing OGV windows +if (-not ([System.Management.Automation.PSTypeName]'CopilotSync.FgHelper').Type) { + Add-Type -Namespace 'CopilotSync' -Name 'FgHelper' -MemberDefinition @' + [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h); + [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd); + [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc e, IntPtr p); + [DllImport("user32.dll")] public static extern int GetWindowText(IntPtr h, System.Text.StringBuilder s, int n); + public delegate bool EnumWindowsProc(IntPtr h, IntPtr p); + public static System.IntPtr FindWindowContaining(string part) { + System.IntPtr found = System.IntPtr.Zero; + EnumWindows((h, p) => { + var sb = new System.Text.StringBuilder(512); + GetWindowText(h, sb, 512); + if (sb.ToString().Contains(part)) { found = h; return false; } + return true; + }, System.IntPtr.Zero); + return found; + } +'@ +} + +function Show-OGV { + # Wrapper around Out-GridView that forces the window to the foreground. + # Starts a runspace that polls for the window by partial title match, then + # calls SetForegroundWindow (valid because this PowerShell process owns focus + # at the time it opens OGV). + param([Parameter(ValueFromPipeline)][object[]]$InputObject, [string]$Title, [switch]$PassThru) + begin { $all = [System.Collections.Generic.List[object]]::new() } + process { foreach ($i in $InputObject) { $all.Add($i) } } + end { + # Spin up a runspace to foreground the OGV window once it appears + $rs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace() + $rs.Open() + $ps = [System.Management.Automation.PowerShell]::Create() + $ps.Runspace = $rs + $null = $ps.AddScript({ + param($t) + for ($i = 0; $i -lt 40; $i++) { # poll up to 4 s + Start-Sleep -Milliseconds 100 + $h = [CopilotSync.FgHelper]::FindWindowContaining($t) + if ($h -ne [IntPtr]::Zero) { + [CopilotSync.FgHelper]::ShowWindow($h, 9) | Out-Null # SW_RESTORE + [CopilotSync.FgHelper]::SetForegroundWindow($h) | Out-Null + break + } + } + }).AddArgument($Title) + $handle = $ps.BeginInvoke() + + if ($PassThru) { $result = $all | Out-GridView -Title $Title -PassThru } + else { $all | Out-GridView -Title $Title } + + $null = $ps.EndInvoke($handle) + $ps.Dispose(); $rs.Close() + if ($PassThru) { return $result } + } +} + #endregion # Initialisation #region Stack detection @@ -317,7 +375,7 @@ function Select-Items { @{ N='Name'; E={ $_.Name } }, @{ N='Description'; E={ $_.Description } }) - $picked = $display | Out-GridView -Title "Select $Category ★=Recommended (config-free) [*]=Installed [↑]=Update [~]=Modified [!]=Setup required" -PassThru + $picked = $display | Show-OGV -Title "Select $Category ★=Recommended (config-free) [*]=Installed [↑]=Update [~]=Modified [!]=Setup required" -PassThru if (-not $picked) { return @() } $pickedNames = @($picked | Where-Object { $_.Name -ne '-- none / skip --' } | ForEach-Object { $_.Name }) return @($Items | Where-Object { $pickedNames -contains $_.Name }) @@ -385,7 +443,7 @@ function Select-ToRemove { @{ N='Modified'; E={ if ($_.LocallyModified) { '[~] MODIFIED' } else { '' } } }, @{ N='Name'; E={ $_.Name } }, @{ N='Description'; E={ $_.Description } }) - $picked = $display | Out-GridView -Title "Select $Category to REMOVE [~]=Locally modified (removal is permanent)" -PassThru + $picked = $display | Show-OGV -Title "Select $Category to REMOVE [~]=Locally modified (removal is permanent)" -PassThru if (-not $picked) { return @() } $pickedNames = @($picked | Where-Object { $_.Name -ne '-- none / skip --' } | ForEach-Object { $_.Name }) return @($removable | Where-Object { $pickedNames -contains $_.Name }) From a09dd1dab5d32063675a736ba5f928b20d4bf237 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:04:57 +0000 Subject: [PATCH 54/60] fix: use keybd_event(Alt) trick to reset Windows foreground-steal lock SetForegroundWindow alone fails when the foreground token has expired (which happens during catalogue loading). Simulating an Alt key-up event resets the UIPI foreground lock, allowing BringWindowToTop to work. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/init-repo.ps1 | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/scripts/init-repo.ps1 b/scripts/init-repo.ps1 index 8a3bfd7..cb495d3 100644 --- a/scripts/init-repo.ps1 +++ b/scripts/init-repo.ps1 @@ -81,6 +81,8 @@ if (-not ([System.Management.Automation.PSTypeName]'CopilotSync.FgHelper').Type) [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd); [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc e, IntPtr p); [DllImport("user32.dll")] public static extern int GetWindowText(IntPtr h, System.Text.StringBuilder s, int n); + [DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte scan, uint flags, int extra); + [DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr h); public delegate bool EnumWindowsProc(IntPtr h, IntPtr p); public static System.IntPtr FindWindowContaining(string part) { System.IntPtr found = System.IntPtr.Zero; @@ -92,14 +94,20 @@ if (-not ([System.Management.Automation.PSTypeName]'CopilotSync.FgHelper').Type) }, System.IntPtr.Zero); return found; } + public static void BringToFront(IntPtr h) { + keybd_event(0x12, 0, 0x0002, 0); // Alt key-up — resets Windows foreground-steal lock + ShowWindow(h, 9); // SW_RESTORE + SetForegroundWindow(h); + BringWindowToTop(h); + } '@ } function Show-OGV { # Wrapper around Out-GridView that forces the window to the foreground. - # Starts a runspace that polls for the window by partial title match, then - # calls SetForegroundWindow (valid because this PowerShell process owns focus - # at the time it opens OGV). + # Uses keybd_event(Alt) to reset Windows' foreground-steal lock before + # calling SetForegroundWindow — required when the foreground token has + # expired (e.g. after the catalogue loading phase). param([Parameter(ValueFromPipeline)][object[]]$InputObject, [string]$Title, [switch]$PassThru) begin { $all = [System.Collections.Generic.List[object]]::new() } process { foreach ($i in $InputObject) { $all.Add($i) } } @@ -111,12 +119,11 @@ function Show-OGV { $ps.Runspace = $rs $null = $ps.AddScript({ param($t) - for ($i = 0; $i -lt 40; $i++) { # poll up to 4 s + for ($i = 0; $i -lt 50; $i++) { # poll up to 5 s Start-Sleep -Milliseconds 100 $h = [CopilotSync.FgHelper]::FindWindowContaining($t) - if ($h -ne [IntPtr]::Zero) { - [CopilotSync.FgHelper]::ShowWindow($h, 9) | Out-Null # SW_RESTORE - [CopilotSync.FgHelper]::SetForegroundWindow($h) | Out-Null + if ($h -ne [System.IntPtr]::Zero) { + [CopilotSync.FgHelper]::BringToFront($h) break } } From 58c727803f4c96de00e9413f126ee9e6907573c3 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:09:11 +0000 Subject: [PATCH 55/60] fix: two recommendation/OGV issues 1. OGV foreground: replace Win32 SetForegroundWindow approach with WScript.Shell.AppActivate which is not subject to UIPI restrictions. Also removes the Add-Type/P-Invoke block (no longer needed). 2. DataVerse false positives: exclude *.instructions.md, *.agent.md, *.prompt.md, *.chatmode.md from Detect-RepoStack file scan. Previously installed awesome-copilot files were feeding back into the keyword detector, recommending more of the same category. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/init-repo.ps1 | 46 ++++++------------------------------------- 1 file changed, 6 insertions(+), 40 deletions(-) diff --git a/scripts/init-repo.ps1 b/scripts/init-repo.ps1 index cb495d3..6b8726b 100644 --- a/scripts/init-repo.ps1 +++ b/scripts/init-repo.ps1 @@ -74,58 +74,23 @@ function Log($m, [string]$level = 'INFO') { Write-Host "[$ts][$level] $m" -ForegroundColor $color } -# Win32 helper for foreground-forcing OGV windows -if (-not ([System.Management.Automation.PSTypeName]'CopilotSync.FgHelper').Type) { - Add-Type -Namespace 'CopilotSync' -Name 'FgHelper' -MemberDefinition @' - [DllImport("user32.dll")] public static extern bool SetForegroundWindow(IntPtr h); - [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr h, int cmd); - [DllImport("user32.dll")] public static extern bool EnumWindows(EnumWindowsProc e, IntPtr p); - [DllImport("user32.dll")] public static extern int GetWindowText(IntPtr h, System.Text.StringBuilder s, int n); - [DllImport("user32.dll")] public static extern void keybd_event(byte vk, byte scan, uint flags, int extra); - [DllImport("user32.dll")] public static extern bool BringWindowToTop(IntPtr h); - public delegate bool EnumWindowsProc(IntPtr h, IntPtr p); - public static System.IntPtr FindWindowContaining(string part) { - System.IntPtr found = System.IntPtr.Zero; - EnumWindows((h, p) => { - var sb = new System.Text.StringBuilder(512); - GetWindowText(h, sb, 512); - if (sb.ToString().Contains(part)) { found = h; return false; } - return true; - }, System.IntPtr.Zero); - return found; - } - public static void BringToFront(IntPtr h) { - keybd_event(0x12, 0, 0x0002, 0); // Alt key-up — resets Windows foreground-steal lock - ShowWindow(h, 9); // SW_RESTORE - SetForegroundWindow(h); - BringWindowToTop(h); - } -'@ -} - function Show-OGV { - # Wrapper around Out-GridView that forces the window to the foreground. - # Uses keybd_event(Alt) to reset Windows' foreground-steal lock before - # calling SetForegroundWindow — required when the foreground token has - # expired (e.g. after the catalogue loading phase). + # Wrapper around Out-GridView that activates the window via WScript.Shell.AppActivate, + # which bypasses UIPI restrictions that block SetForegroundWindow from runspaces. param([Parameter(ValueFromPipeline)][object[]]$InputObject, [string]$Title, [switch]$PassThru) begin { $all = [System.Collections.Generic.List[object]]::new() } process { foreach ($i in $InputObject) { $all.Add($i) } } end { - # Spin up a runspace to foreground the OGV window once it appears $rs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace() $rs.Open() $ps = [System.Management.Automation.PowerShell]::Create() $ps.Runspace = $rs $null = $ps.AddScript({ param($t) + $wsh = New-Object -ComObject WScript.Shell for ($i = 0; $i -lt 50; $i++) { # poll up to 5 s Start-Sleep -Milliseconds 100 - $h = [CopilotSync.FgHelper]::FindWindowContaining($t) - if ($h -ne [System.IntPtr]::Zero) { - [CopilotSync.FgHelper]::BringToFront($h) - break - } + if ($wsh.AppActivate($t)) { break } } }).AddArgument($Title) $handle = $ps.BeginInvoke() @@ -147,7 +112,8 @@ function Detect-RepoStack { $recs = [System.Collections.Generic.List[string]]::new() $files = Get-ChildItem $RepoPath -Recurse -File -ErrorAction SilentlyContinue | - Where-Object { $_.FullName -notmatch '\\(\.git|node_modules|\.venv|bin|obj)\\' } + Where-Object { $_.FullName -notmatch '\\(\.git|node_modules|\.venv|bin|obj)\\' } | + Where-Object { $_.Name -notmatch '\.(instructions|agent|prompt|chatmode)\.md$' } # exclude installed awesome-copilot files $exts = $files | ForEach-Object { $_.Extension.ToLower() } | Sort-Object -Unique $names = $files | ForEach-Object { $_.Name } | Sort-Object -Unique From 794a63fa5744972519a9fcf83d20fdad86e21651 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:12:15 +0000 Subject: [PATCH 56/60] fix: pass ASCII-only SearchKey to AppActivate, double-activate after focus-steal - Add SearchKey param to Show-OGV; falls back to stripping non-ASCII from Title - Pass explicit 'Select {Category}' key at both call sites (no unicode chars) - Call AppActivate twice (with 150ms gap) to counter VS Code re-stealing focus Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/init-repo.ps1 | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/scripts/init-repo.ps1 b/scripts/init-repo.ps1 index 6b8726b..262aedb 100644 --- a/scripts/init-repo.ps1 +++ b/scripts/init-repo.ps1 @@ -77,22 +77,29 @@ function Log($m, [string]$level = 'INFO') { function Show-OGV { # Wrapper around Out-GridView that activates the window via WScript.Shell.AppActivate, # which bypasses UIPI restrictions that block SetForegroundWindow from runspaces. - param([Parameter(ValueFromPipeline)][object[]]$InputObject, [string]$Title, [switch]$PassThru) + # SearchKey is a plain ASCII fragment of the title used for window matching — avoids + # unicode chars (★ etc.) that may cause AppActivate to fail the lookup. + param([Parameter(ValueFromPipeline)][object[]]$InputObject, [string]$Title, [string]$SearchKey, [switch]$PassThru) begin { $all = [System.Collections.Generic.List[object]]::new() } process { foreach ($i in $InputObject) { $all.Add($i) } } end { + $key = if ($SearchKey) { $SearchKey } else { ($Title -replace '[^\x20-\x7E]','').Trim() } $rs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace() $rs.Open() $ps = [System.Management.Automation.PowerShell]::Create() $ps.Runspace = $rs $null = $ps.AddScript({ - param($t) + param($k) $wsh = New-Object -ComObject WScript.Shell for ($i = 0; $i -lt 50; $i++) { # poll up to 5 s Start-Sleep -Milliseconds 100 - if ($wsh.AppActivate($t)) { break } + if ($wsh.AppActivate($k)) { + Start-Sleep -Milliseconds 150 # brief pause then re-activate to counter focus-steal + $wsh.AppActivate($k) + break + } } - }).AddArgument($Title) + }).AddArgument($key) $handle = $ps.BeginInvoke() if ($PassThru) { $result = $all | Out-GridView -Title $Title -PassThru } @@ -348,7 +355,7 @@ function Select-Items { @{ N='Name'; E={ $_.Name } }, @{ N='Description'; E={ $_.Description } }) - $picked = $display | Show-OGV -Title "Select $Category ★=Recommended (config-free) [*]=Installed [↑]=Update [~]=Modified [!]=Setup required" -PassThru + $picked = $display | Show-OGV -Title "Select $Category ★=Recommended (config-free) [*]=Installed [↑]=Update [~]=Modified [!]=Setup required" -SearchKey "Select $Category" -PassThru if (-not $picked) { return @() } $pickedNames = @($picked | Where-Object { $_.Name -ne '-- none / skip --' } | ForEach-Object { $_.Name }) return @($Items | Where-Object { $pickedNames -contains $_.Name }) @@ -416,7 +423,7 @@ function Select-ToRemove { @{ N='Modified'; E={ if ($_.LocallyModified) { '[~] MODIFIED' } else { '' } } }, @{ N='Name'; E={ $_.Name } }, @{ N='Description'; E={ $_.Description } }) - $picked = $display | Show-OGV -Title "Select $Category to REMOVE [~]=Locally modified (removal is permanent)" -PassThru + $picked = $display | Show-OGV -Title "Select $Category to REMOVE [~]=Locally modified (removal is permanent)" -SearchKey "Select $Category to REMOVE" -PassThru if (-not $picked) { return @() } $pickedNames = @($picked | Where-Object { $_.Name -ne '-- none / skip --' } | ForEach-Object { $_.Name }) return @($removable | Where-Object { $pickedNames -contains $_.Name }) From 2e6a754ade8d18f5417be8b7eb6dbafcad155437 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:14:22 +0000 Subject: [PATCH 57/60] fix: restrict name-match scoring to leading segment of item name Previously 'python' matched 'dataverse-python-sdk' because word-boundary regex fires anywhere in the name. Now requires the keyword to be the first dash-segment (or a compound keyword prefix like 'github-actions-*'). se-security-reviewer still scores 2 via content (owasp+security), so its star is preserved without needing a mid-name match. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/init-repo.ps1 | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/scripts/init-repo.ps1 b/scripts/init-repo.ps1 index 262aedb..b257e9a 100644 --- a/scripts/init-repo.ps1 +++ b/scripts/init-repo.ps1 @@ -558,8 +558,13 @@ function Measure-ItemRelevance { $nameLower = $ItemName.ToLower() foreach ($tag in $Tags) { - $pattern = "(?i)\b$([regex]::Escape($tag.ToLower()))\b" - if ($nameLower -match $pattern) { $score += 2 } + $t = $tag.ToLower() + # Name match: keyword must appear as the LEADING segment(s) of the name. + # e.g. 'python' matches 'python-mcp-server' but NOT 'dataverse-python-sdk'. + # Compound keywords like 'github-actions' match names starting with that prefix. + $firstSegment = ($nameLower -split '-')[0] + $nameMatch = ($firstSegment -eq $t) -or ($nameLower.StartsWith("$t-")) -or ($nameLower -eq $t) + if ($nameMatch) { $score += 2 } } # For directories, score against README.md / SKILL.md content From 44c305f6b145e272a8bd04e2825b5f99a45b37a8 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:17:19 +0000 Subject: [PATCH 58/60] fix: replace broken foreground-hack with simple taskbar hint message Windows UIPI intentionally blocks focus-stealing from background processes. All three approaches (SetForegroundWindow, keybd_event, AppActivate from runspace) fail for the same reason. Replace with a clear console message. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/init-repo.ps1 | 28 ++++------------------------ 1 file changed, 4 insertions(+), 24 deletions(-) diff --git a/scripts/init-repo.ps1 b/scripts/init-repo.ps1 index b257e9a..c98857c 100644 --- a/scripts/init-repo.ps1 +++ b/scripts/init-repo.ps1 @@ -75,38 +75,18 @@ function Log($m, [string]$level = 'INFO') { } function Show-OGV { - # Wrapper around Out-GridView that activates the window via WScript.Shell.AppActivate, - # which bypasses UIPI restrictions that block SetForegroundWindow from runspaces. - # SearchKey is a plain ASCII fragment of the title used for window matching — avoids - # unicode chars (★ etc.) that may cause AppActivate to fail the lookup. + # Wrapper around Out-GridView that flashes the taskbar button and prints + # a console hint — the most reliable way to alert the user since Windows + # prevents focus-stealing from background processes by design. param([Parameter(ValueFromPipeline)][object[]]$InputObject, [string]$Title, [string]$SearchKey, [switch]$PassThru) begin { $all = [System.Collections.Generic.List[object]]::new() } process { foreach ($i in $InputObject) { $all.Add($i) } } end { - $key = if ($SearchKey) { $SearchKey } else { ($Title -replace '[^\x20-\x7E]','').Trim() } - $rs = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace() - $rs.Open() - $ps = [System.Management.Automation.PowerShell]::Create() - $ps.Runspace = $rs - $null = $ps.AddScript({ - param($k) - $wsh = New-Object -ComObject WScript.Shell - for ($i = 0; $i -lt 50; $i++) { # poll up to 5 s - Start-Sleep -Milliseconds 100 - if ($wsh.AppActivate($k)) { - Start-Sleep -Milliseconds 150 # brief pause then re-activate to counter focus-steal - $wsh.AppActivate($k) - break - } - } - }).AddArgument($key) - $handle = $ps.BeginInvoke() + Write-Host " ► Selection window opening — check your taskbar if it appears behind other apps." -ForegroundColor Yellow if ($PassThru) { $result = $all | Out-GridView -Title $Title -PassThru } else { $all | Out-GridView -Title $Title } - $null = $ps.EndInvoke($handle) - $ps.Dispose(); $rs.Close() if ($PassThru) { return $result } } } From 4027265f54babc76c2c249348fd41f4903a86a16 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:20:47 +0000 Subject: [PATCH 59/60] fix: revert over-aggressive leading-segment name-match restriction security-and-owasp has 'owasp' as a valid non-first segment and was losing its star. The DataVerse false-positive was a content-scan feedback loop (fixed separately by excluding installed awesome-copilot files), not a name-match problem. Use full-segment match anywhere in the name instead. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- scripts/init-repo.ps1 | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/scripts/init-repo.ps1 b/scripts/init-repo.ps1 index c98857c..91c4ed6 100644 --- a/scripts/init-repo.ps1 +++ b/scripts/init-repo.ps1 @@ -539,12 +539,11 @@ function Measure-ItemRelevance { foreach ($tag in $Tags) { $t = $tag.ToLower() - # Name match: keyword must appear as the LEADING segment(s) of the name. - # e.g. 'python' matches 'python-mcp-server' but NOT 'dataverse-python-sdk'. - # Compound keywords like 'github-actions' match names starting with that prefix. - $firstSegment = ($nameLower -split '-')[0] - $nameMatch = ($firstSegment -eq $t) -or ($nameLower.StartsWith("$t-")) -or ($nameLower -eq $t) - if ($nameMatch) { $score += 2 } + # Name match: keyword must be a complete dash-segment in the item name. + # e.g. 'python' matches 'python-mcp-server' AND 'dataverse-python-sdk' + # but NOT 'pythonic-tips' (partial segment match). + $segments = $nameLower -split '-' + if ($segments -contains $t) { $score += 2 } } # For directories, score against README.md / SKILL.md content From 04d894f3fda024cb0fe6857c6b899e5a8414cf60 Mon Sep 17 00:00:00 2001 From: CTOUT <22860918+CTOUT@users.noreply.github.com> Date: Tue, 17 Mar 2026 14:27:43 +0000 Subject: [PATCH 60/60] docs: add TODO.md with open issues for next session Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- TODO.md | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 TODO.md diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..813b4f4 --- /dev/null +++ b/TODO.md @@ -0,0 +1,17 @@ +# TODO + +## Open Issues + +### DataVerse false ★ recommendations for repos with `.py` files + +- `.py` files trigger `python` keyword → `dataverse-python-*` scores 2 (contains `python` as a dash-segment) +- Reproduced with: `.\configure.ps1 -Install -RepoPath ..\Scripts\` +- Root cause: generic language keywords match vendor-prefixed item names +- **Proposed fix:** maintain a known vendor-prefix blocklist (`dataverse`, `salesforce`, `shopify`, `atlassian`, `pimcore`, `amplitude`, etc.). Items whose first segment is in the blocklist should not receive a name-match score from generic language keywords — only from an explicit vendor keyword detected in the repo. + +### OGV opens behind other windows + +- Windows UIPI prevents focus-stealing from background processes by design +- Three approaches tried and failed: `SetForegroundWindow`, `keybd_event(Alt)`, `WScript.Shell.AppActivate` from runspace +- Currently mitigated with a yellow hint message in the terminal +- Possible alternative: investigate WinForms-based topmost picker as a drop-in replacement for `Out-GridView`