public/New-WayforgeProject.ps1
|
function New-WayforgeProject { <# .SYNOPSIS Scaffolds a new Wayforge workspace. .DESCRIPTION Creates a project directory and populates it with the standard Wayforge workspace structure: AGENTS.md, .agents/skills/, .workflow/definitions/, .workflow/hooks/, .workflow/schemas/, and a .gitignore. If the target directory already exists, use -InitializeExisting to add missing files and directories only; existing files are never overwritten. .PARAMETER Name The name of the project. This becomes the directory name and is used in generated artifacts such as AGENTS.md. .PARAMETER Path The parent path where the project directory will be created. Relative paths are resolved from the current location. .PARAMETER InitializeExisting When set, scaffolding is applied to an existing directory. Any file that already exists is skipped with a warning instead of being overwritten. .PARAMETER Harness Which coding-agent harnesses to wire enforcement config for. Defaults to 'claude'. Ignored when -SkipEnforcement is set or git is unavailable. .PARAMETER SkipEnforcement Skip wiring the enforcement layer (git hooks + harness config). The workspace is still scaffolded; run Register-WayforgeHooks and Sync-WayforgeHarness later to enable enforcement. .PARAMETER WithCI Also generate the CI gate workflow (.github/workflows/wayforge-gate.yml) so the same gates enforce at merge time. Requires branch protection to become a required check. .PARAMETER DetectHarness Ignore -Harness and instead wire every harness detected as installed on this machine (via Get-WayforgeHarness). Falls back to claude if none. .EXAMPLE New-WayforgeProject -Name MyProject -Path C:\Projects Creates C:\Projects\MyProject, scaffolds a fresh workspace, and wires the git-hook floor plus Claude enforcement config. .EXAMPLE New-WayforgeProject -Name MyProject -Path . -InitializeExisting Scaffolds a workspace into the existing ./MyProject directory without overwriting any files. #> [CmdletBinding(SupportsShouldProcess)] param( [Parameter(Mandatory, Position = 0)] [string]$Name, [Parameter(Mandatory, Position = 1)] [string]$Path, [switch]$InitializeExisting, [ValidateSet('claude', 'codex', 'grok', 'copilot', 'cursor', 'opencode', 'pi', 'kimi')] [string[]]$Harness = @('claude'), [switch]$SkipEnforcement, [switch]$WithCI, [switch]$DetectHarness ) begin { $ErrorActionPreference = 'Stop' } process { $resolvedParent = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path) $projectRoot = Join-Path $resolvedParent $Name if (Test-Path $projectRoot) { if (-not $InitializeExisting) { throw "Project directory already exists: $projectRoot. Use -InitializeExisting to scaffold additively." } if (-not (Test-Path $projectRoot -PathType Container)) { throw "A non-directory file exists at the project path: $projectRoot" } Write-Verbose "Scaffolding into existing directory: $projectRoot" } else { if ($PSCmdlet.ShouldProcess($projectRoot, 'Create project directory')) { New-Item -ItemType Directory -Path $projectRoot | Out-Null } } $directories = @( (Join-Path $projectRoot '.agents' 'skills') (Join-Path $projectRoot '.workflow' 'definitions') (Join-Path $projectRoot '.workflow' 'hooks') (Join-Path $projectRoot '.workflow' 'schemas') (Join-Path $projectRoot '.workflow' 'artifacts') ) foreach ($dir in $directories) { if (-not (Test-Path $dir)) { if ($PSCmdlet.ShouldProcess($dir, 'Create directory')) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } } } $moduleRoot = Resolve-WayforgeModuleRoot Install-WayforgeFile ` -ProjectRoot $projectRoot ` -RelativePath 'AGENTS.md' ` -TemplatePath (Join-Path $moduleRoot 'templates' 'AGENTS.md.template') ` -Tokens @{ '{{PROJECT_NAME}}' = $Name } Install-WayforgeFile ` -ProjectRoot $projectRoot ` -RelativePath '.gitignore' ` -TemplatePath (Join-Path $moduleRoot 'templates' 'gitignore.template') Install-WayforgeFile ` -ProjectRoot $projectRoot ` -RelativePath (Join-Path '.agents' 'skills' 'example' 'SKILL.md') ` -TemplatePath (Join-Path $moduleRoot 'templates' 'skill.example' 'SKILL.md') Install-WayforgeFile ` -ProjectRoot $projectRoot ` -RelativePath (Join-Path '.workflow' 'definitions' 'default.yaml') ` -TemplatePath (Join-Path $moduleRoot 'templates' 'workflow.default.yaml') Install-WayforgeFile ` -ProjectRoot $projectRoot ` -RelativePath (Join-Path '.workflow' 'schemas' 'example.json') ` -TemplatePath (Join-Path $moduleRoot 'templates' 'schema.example.json') Install-WayforgeFile ` -ProjectRoot $projectRoot ` -RelativePath (Join-Path '.workflow' 'schemas' 'plan.json') ` -TemplatePath (Join-Path $moduleRoot 'templates' 'schema.plan.json') Initialize-WayforgeGitRepository -ProjectRoot $projectRoot if ($DetectHarness) { $installed = @(Get-WayforgeHarness -ProjectPath $projectRoot | Where-Object { $_.Installed } | ForEach-Object { $_.Name }) if ($installed) { $Harness = $installed } else { Write-Warning 'No harnesses detected as installed; using claude.' $Harness = @('claude') } } # Wire the enforcement layer. A failure here never fails the scaffold — # the workspace is already usable; enforcement can be added later. $enforced = $false if (-not $SkipEnforcement) { $gitAvailable = [bool](Get-Command git -ErrorAction SilentlyContinue) if ($gitAvailable -and (Test-Path (Join-Path $projectRoot '.git'))) { if ($PSCmdlet.ShouldProcess($projectRoot, 'Wire enforcement (git hooks + harness config)')) { try { Register-WayforgeHooks -ProjectPath $projectRoot | Out-Null Sync-WayforgeHarness -Harness $Harness -ProjectPath $projectRoot | Out-Null if ($WithCI) { Register-WayforgeCI -ProjectPath $projectRoot | Out-Null } $enforced = $true } catch { Write-Warning "Enforcement wiring failed: $($_.Exception.Message). Scaffold is complete; run Register-WayforgeHooks and Sync-WayforgeHarness manually." } } } else { Write-Warning 'git unavailable or repository not initialized; skipping enforcement wiring. Run Register-WayforgeHooks and Sync-WayforgeHarness later.' } } [PSCustomObject]@{ PSTypeName = 'PSWayforge.Project' Name = $Name Path = $projectRoot InitializedExisting = $InitializeExisting.IsPresent Enforced = $enforced Harness = if ($enforced) { $Harness } else { @() } } } } |