|
1 | 1 | #Requires -Version 7.0 |
2 | | -# setup/new-project.ps1 -- Kit 3: New project scaffolder (Steps 1-2) |
| 2 | +# setup/new-project.ps1 -- Kit 3: New project scaffolder (Steps 1-4) |
3 | 3 | # |
4 | | -# Collects project concept, creates directory structure, and initializes |
5 | | -# the git repo and GitHub remote. |
6 | | -# |
7 | | -# Steps 3-4 (CLAUDE.md generation and workspace open) will be added in issue #20. |
| 4 | +# Collects project concept, creates directory structure, initializes |
| 5 | +# the git repo and GitHub remote, generates CLAUDE.md, and opens VS Code. |
8 | 6 | # |
9 | 7 | # Usage: |
10 | 8 | # .\setup\new-project.ps1 # Fully interactive |
@@ -851,32 +849,359 @@ logger: |
851 | 849 | } |
852 | 850 |
|
853 | 851 | # --------------------------------------------------------------------------- |
854 | | -# Main entry point |
| 852 | +# STEP 3: CLAUDE.md Generation |
855 | 853 | # --------------------------------------------------------------------------- |
856 | 854 |
|
857 | | -Write-Section 'Kit 3: New Project' |
858 | | -Write-Host ' Collects project concept and creates directory structure.' |
859 | | -Write-Host '' |
| 855 | +function Invoke-ClaudeMdGeneration { |
| 856 | + <# |
| 857 | + .SYNOPSIS |
| 858 | + Generates a project CLAUDE.md using Claude Code or a template fallback. |
| 859 | + .PARAMETER Data |
| 860 | + Hashtable returned by Invoke-ConceptCollection. |
| 861 | + .PARAMETER ScaffoldResult |
| 862 | + Hashtable returned by Invoke-Scaffolding. |
| 863 | + #> |
| 864 | + [CmdletBinding()] |
| 865 | + param( |
| 866 | + [Parameter(Mandatory)] |
| 867 | + [hashtable]$Data, |
860 | 868 |
|
861 | | -$conceptData = Invoke-ConceptCollection |
862 | | -$scaffoldResult = Invoke-Scaffolding -Data $conceptData |
| 869 | + [Parameter(Mandatory)] |
| 870 | + [hashtable]$ScaffoldResult |
| 871 | + ) |
| 872 | + |
| 873 | + Write-Section 'Step 3: CLAUDE.md Generation' |
| 874 | + |
| 875 | + $projectName = $Data.Name |
| 876 | + $githubUser = $Data.GitHubUser |
| 877 | + $targetDir = $ScaffoldResult.TargetDir |
| 878 | + $concept = $Data.Concept |
| 879 | + $profiles = @($Data.Profiles) |
| 880 | + $primaryProfile = if ($profiles.Count -gt 0) { $profiles[0] } else { '' } |
| 881 | + |
| 882 | + $claudeMdPath = Join-Path $targetDir 'CLAUDE.md' |
| 883 | + $claudeMdWritten = $false |
| 884 | + $issueCreated = $false |
| 885 | + $issueUrl = '' |
| 886 | + |
| 887 | + # ------------------------------------------------------------------ |
| 888 | + # 3.1 Check Claude auth and choose generation path |
| 889 | + # ------------------------------------------------------------------ |
| 890 | + |
| 891 | + $authResult = Test-ClaudeAuth |
| 892 | + $useClaudeAi = $authResult.Met |
| 893 | + |
| 894 | + if ($useClaudeAi) { |
| 895 | + Write-Step 'Claude Code authenticated -- generating CLAUDE.md with AI' |
| 896 | + |
| 897 | + # Build profile body text for the prompt |
| 898 | + $profileBody = '' |
| 899 | + if ($primaryProfile -ne '') { |
| 900 | + $profileObj = Get-Profile -Name $primaryProfile |
| 901 | + if ($null -ne $profileObj -and $profileObj.Body) { |
| 902 | + $profileBody = $profileObj.Body |
| 903 | + } |
| 904 | + } |
| 905 | + |
| 906 | + # Read the global CLAUDE.md as format reference |
| 907 | + $globalClaudeMd = '' |
| 908 | + $globalClaudeMdPath = Join-Path $script:RepoRoot 'claude' 'CLAUDE.md' |
| 909 | + if (Test-Path $globalClaudeMdPath) { |
| 910 | + $globalClaudeMd = Get-Content $globalClaudeMdPath -Raw |
| 911 | + } |
| 912 | + |
| 913 | + # Build features and non-goals as text |
| 914 | + $featuresText = if ($concept.Features.Count -gt 0) { |
| 915 | + ($concept.Features | ForEach-Object { "- $_" }) -join "`n" |
| 916 | + } else { |
| 917 | + '(not specified)' |
| 918 | + } |
| 919 | + $nonGoalsText = if ($concept.NonGoals.Count -gt 0) { |
| 920 | + ($concept.NonGoals | ForEach-Object { "- $_" }) -join "`n" |
| 921 | + } else { |
| 922 | + '(not specified)' |
| 923 | + } |
| 924 | + |
| 925 | + $prompt = @" |
| 926 | +Generate a project CLAUDE.md for a new software project. Include: project context, goals and non-goals, tech stack and conventions, build commands, key constraints, and suggested first steps. |
| 927 | +
|
| 928 | +PROJECT CONCEPT BRIEF: |
| 929 | +Name: $projectName |
| 930 | +Description: $($concept.Description) |
| 931 | +Problem: $($concept.Problem) |
| 932 | +Features: |
| 933 | +$featuresText |
| 934 | +Non-goals: |
| 935 | +$nonGoalsText |
| 936 | +Profile: $primaryProfile |
| 937 | +GitHub: https://github.com/$githubUser/$projectName |
| 938 | +
|
| 939 | +PROFILE DETAILS: |
| 940 | +$profileBody |
| 941 | +
|
| 942 | +FORMAT REFERENCE (follow the structure and style of this global CLAUDE.md): |
| 943 | +$globalClaudeMd |
| 944 | +
|
| 945 | +Generate only the CLAUDE.md content. Do not include any preamble or explanation. |
| 946 | +"@ |
| 947 | + |
| 948 | + $generatedContent = $null |
| 949 | + $accepted = $false |
| 950 | + |
| 951 | + while (-not $accepted) { |
| 952 | + Write-Step 'Calling Claude Code to generate CLAUDE.md...' |
| 953 | + try { |
| 954 | + $rawOutput = & claude --print -p $prompt 2>&1 | Out-String |
| 955 | + if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($rawOutput)) { |
| 956 | + Write-Warn "Claude returned exit code $LASTEXITCODE or empty output -- falling back to template" |
| 957 | + $useClaudeAi = $false |
| 958 | + break |
| 959 | + } |
| 960 | + $generatedContent = $rawOutput.Trim() |
| 961 | + } catch { |
| 962 | + Write-Warn "Claude Code call failed: $_ -- falling back to template" |
| 963 | + $useClaudeAi = $false |
| 964 | + break |
| 965 | + } |
| 966 | + |
| 967 | + # Display result |
| 968 | + Write-Host '' |
| 969 | + Write-Host "${script:Bold}--- Generated CLAUDE.md ---${script:Reset}" |
| 970 | + Write-Host $generatedContent |
| 971 | + Write-Host "${script:Bold}--- End of CLAUDE.md ---${script:Reset}" |
| 972 | + Write-Host '' |
| 973 | + |
| 974 | + $choice = Read-Host -Prompt ' Accept this CLAUDE.md? [Y/n/edit]' |
| 975 | + $choice = $choice.Trim().ToLower() |
| 976 | + |
| 977 | + if ($choice -eq '' -or $choice -eq 'y') { |
| 978 | + $accepted = $true |
| 979 | + } elseif ($choice -eq 'n') { |
| 980 | + Write-Step 'Regenerating...' |
| 981 | + # Loop again |
| 982 | + } elseif ($choice -eq 'edit') { |
| 983 | + $tempFile = Join-Path ([System.IO.Path]::GetTempPath()) "claude-md-$projectName.md" |
| 984 | + Set-Content -Path $tempFile -Value $generatedContent -Encoding utf8NoBOM |
| 985 | + Write-Step "Opening in VS Code: $tempFile" |
| 986 | + & code --wait $tempFile |
| 987 | + if (Test-Path $tempFile) { |
| 988 | + $generatedContent = Get-Content $tempFile -Raw |
| 989 | + } |
| 990 | + $accepted = $true |
| 991 | + } else { |
| 992 | + Write-Warn "Unrecognized choice '$choice'. Press Enter or type y to accept, n to regenerate, edit to open in VS Code." |
| 993 | + } |
| 994 | + } |
| 995 | + |
| 996 | + if ($useClaudeAi -and $accepted -and $null -ne $generatedContent) { |
| 997 | + try { |
| 998 | + Set-Content -Path $claudeMdPath -Value $generatedContent -Encoding utf8NoBOM |
| 999 | + Write-OK 'CLAUDE.md written from AI generation' |
| 1000 | + $claudeMdWritten = $true |
| 1001 | + } catch { |
| 1002 | + Write-Warn "Could not write CLAUDE.md: $_" |
| 1003 | + } |
| 1004 | + } |
| 1005 | + } |
| 1006 | + |
| 1007 | + # ------------------------------------------------------------------ |
| 1008 | + # 3.2 Fallback: template substitution |
| 1009 | + # ------------------------------------------------------------------ |
| 1010 | + |
| 1011 | + if (-not $claudeMdWritten) { |
| 1012 | + Write-Warn 'Claude Code not authenticated -- using template fallback' |
| 1013 | + |
| 1014 | + $templatePath = Join-Path $script:RepoRoot 'project-templates' 'claude-md-template.md' |
| 1015 | + if (-not (Test-Path $templatePath)) { |
| 1016 | + Write-Warn "Template not found: $templatePath -- skipping CLAUDE.md creation" |
| 1017 | + } else { |
| 1018 | + # Map profile to file extension |
| 1019 | + $profileExt = switch ($primaryProfile) { |
| 1020 | + 'go-cli' { 'go' } |
| 1021 | + 'go-web' { 'go' } |
| 1022 | + 'iot-embedded' { 'yaml' } |
| 1023 | + default { 'ts' } |
| 1024 | + } |
| 1025 | + |
| 1026 | + $machine = if ($env:COMPUTERNAME) { $env:COMPUTERNAME } else { hostname } |
| 1027 | + |
| 1028 | + $templateContent = Get-Content $templatePath -Raw |
| 1029 | + $templateContent = $templateContent -replace '\{\{PROJECT_NAME\}\}', $projectName |
| 1030 | + $templateContent = $templateContent -replace '\{\{PROJECT_DESCRIPTION\}\}', $concept.Description |
| 1031 | + $templateContent = $templateContent -replace '\{\{PROFILE\}\}', $primaryProfile |
| 1032 | + $templateContent = $templateContent -replace '\{\{DEVSPACE\}\}', $Data.DevSpace |
| 1033 | + $templateContent = $templateContent -replace '\{\{AUTHOR\}\}', $githubUser |
| 1034 | + $templateContent = $templateContent -replace '\{\{MACHINE\}\}', $machine |
| 1035 | + $templateContent = $templateContent -replace '\{\{PROFILE_EXT\}\}', $profileExt |
| 1036 | + |
| 1037 | + try { |
| 1038 | + Set-Content -Path $claudeMdPath -Value $templateContent -Encoding utf8NoBOM |
| 1039 | + Write-OK 'CLAUDE.md written from template' |
| 1040 | + $claudeMdWritten = $true |
| 1041 | + } catch { |
| 1042 | + Write-Warn "Could not write CLAUDE.md: $_" |
| 1043 | + } |
| 1044 | + } |
| 1045 | + } |
| 1046 | + |
| 1047 | + # ------------------------------------------------------------------ |
| 1048 | + # 3.3 Create Phase 0 GitHub issue |
| 1049 | + # ------------------------------------------------------------------ |
| 1050 | + |
| 1051 | + if ($ScaffoldResult.GitHubCreated) { |
| 1052 | + Write-Step 'Creating Phase 0 GitHub issue...' |
| 1053 | + |
| 1054 | + $featuresSection = if ($concept.Features.Count -gt 0) { |
| 1055 | + ($concept.Features | ForEach-Object { "- $_" }) -join "`n" |
| 1056 | + } else { |
| 1057 | + '- (not specified)' |
| 1058 | + } |
| 1059 | + $nonGoalsSection = if ($concept.NonGoals.Count -gt 0) { |
| 1060 | + ($concept.NonGoals | ForEach-Object { "- $_" }) -join "`n" |
| 1061 | + } else { |
| 1062 | + '- (not specified)' |
| 1063 | + } |
| 1064 | + |
| 1065 | + $issueBody = @" |
| 1066 | +## Concept |
| 1067 | +
|
| 1068 | +$($concept.Description) |
| 1069 | +
|
| 1070 | +## Problem statement |
| 1071 | +
|
| 1072 | +$($concept.Problem) |
| 1073 | +
|
| 1074 | +## Core features |
| 1075 | +
|
| 1076 | +$featuresSection |
| 1077 | +
|
| 1078 | +## Non-goals |
| 1079 | +
|
| 1080 | +$nonGoalsSection |
| 1081 | +
|
| 1082 | +## Acceptance criteria |
| 1083 | +
|
| 1084 | +- [ ] Concept validated with at least one potential user |
| 1085 | +- [ ] Tech stack confirmed appropriate for the problem |
| 1086 | +- [ ] Non-goals agreed upon by stakeholders |
| 1087 | +- [ ] CLAUDE.md reviewed and updated with any corrections |
| 1088 | +
|
| 1089 | +## Next steps |
| 1090 | +
|
| 1091 | +1. Review and refine this concept brief |
| 1092 | +2. Set up the development environment (`.\setup\stack.ps1`) |
| 1093 | +3. Create architecture doc in `docs/ARCHITECTURE.md` |
| 1094 | +4. Break the concept into implementable issues |
| 1095 | +"@ |
| 1096 | + |
| 1097 | + try { |
| 1098 | + $ghIssueOutput = & gh issue create ` |
| 1099 | + -R "$githubUser/$projectName" ` |
| 1100 | + --title "Phase 0 -- Concept validation: $projectName" ` |
| 1101 | + --body $issueBody ` |
| 1102 | + --label 'chore' ` |
| 1103 | + --label 'phase-1' 2>&1 | Out-String |
| 1104 | + $ghIssueExit = $LASTEXITCODE |
| 1105 | + |
| 1106 | + if ($ghIssueExit -eq 0) { |
| 1107 | + $issueUrl = $ghIssueOutput.Trim() |
| 1108 | + Write-OK "Phase 0 issue created: $issueUrl" |
| 1109 | + $issueCreated = $true |
| 1110 | + } else { |
| 1111 | + Write-Warn "Could not create GitHub issue (exit $ghIssueExit): $ghIssueOutput" |
| 1112 | + } |
| 1113 | + } catch { |
| 1114 | + Write-Warn "gh issue create threw exception: $_" |
| 1115 | + } |
| 1116 | + } |
| 1117 | + |
| 1118 | + return @{ |
| 1119 | + ClaudeMdWritten = $claudeMdWritten |
| 1120 | + IssueCreated = $issueCreated |
| 1121 | + IssueUrl = $issueUrl |
| 1122 | + } |
| 1123 | +} |
863 | 1124 |
|
864 | 1125 | # --------------------------------------------------------------------------- |
865 | | -# Final summary |
| 1126 | +# STEP 4: Workspace Open |
866 | 1127 | # --------------------------------------------------------------------------- |
867 | 1128 |
|
868 | | -Write-Section 'Done' |
869 | | -Write-OK "Project scaffolded: $($scaffoldResult.TargetDir)" |
| 1129 | +function Invoke-WorkspaceOpen { |
| 1130 | + <# |
| 1131 | + .SYNOPSIS |
| 1132 | + Displays the project summary and opens VS Code. |
| 1133 | + .PARAMETER Data |
| 1134 | + Hashtable returned by Invoke-ConceptCollection. |
| 1135 | + .PARAMETER ScaffoldResult |
| 1136 | + Hashtable returned by Invoke-Scaffolding. |
| 1137 | + .PARAMETER ClaudeMdResult |
| 1138 | + Hashtable returned by Invoke-ClaudeMdGeneration. |
| 1139 | + #> |
| 1140 | + [CmdletBinding()] |
| 1141 | + param( |
| 1142 | + [Parameter(Mandatory)] |
| 1143 | + [hashtable]$Data, |
| 1144 | + |
| 1145 | + [Parameter(Mandatory)] |
| 1146 | + [hashtable]$ScaffoldResult, |
| 1147 | + |
| 1148 | + [Parameter(Mandatory)] |
| 1149 | + [hashtable]$ClaudeMdResult |
| 1150 | + ) |
| 1151 | + |
| 1152 | + $projectName = $Data.Name |
| 1153 | + $targetDir = $ScaffoldResult.TargetDir |
| 1154 | + |
| 1155 | + Write-Section "Project ready: $projectName" |
| 1156 | + |
| 1157 | + Write-Host " Location: $targetDir" |
| 1158 | + if ($ScaffoldResult.GitHubCreated) { |
| 1159 | + Write-Host " GitHub: $($ScaffoldResult.RepoUrl)" |
| 1160 | + } |
| 1161 | + if ($ClaudeMdResult.IssueCreated) { |
| 1162 | + Write-Host " Issue #1: $($ClaudeMdResult.IssueUrl)" |
| 1163 | + } |
| 1164 | + Write-Host '' |
| 1165 | + |
| 1166 | + # Open VS Code workspace |
| 1167 | + $workspacePath = Join-Path $targetDir "$projectName.code-workspace" |
| 1168 | + if (Test-Path $workspacePath) { |
| 1169 | + Write-Step "Opening VS Code: $workspacePath" |
| 1170 | + try { |
| 1171 | + & code $workspacePath |
| 1172 | + } catch { |
| 1173 | + Write-Warn "Could not open VS Code: $_" |
| 1174 | + Write-Host " Manual: code `"$workspacePath`"" |
| 1175 | + } |
| 1176 | + } else { |
| 1177 | + Write-Step "Opening VS Code: $targetDir" |
| 1178 | + try { |
| 1179 | + & code $targetDir |
| 1180 | + } catch { |
| 1181 | + Write-Warn "Could not open VS Code: $_" |
| 1182 | + Write-Host " Manual: code `"$targetDir`"" |
| 1183 | + } |
| 1184 | + } |
870 | 1185 |
|
871 | | -if ($scaffoldResult.GitHubCreated) { |
872 | | - Write-OK "GitHub repo: $($scaffoldResult.RepoUrl)" |
| 1186 | + Write-Host '' |
| 1187 | + Write-Host ' Next steps:' |
| 1188 | + Write-Host " 1. Review CLAUDE.md in $targetDir" |
| 1189 | + if ($ClaudeMdResult.IssueCreated) { |
| 1190 | + Write-Host " 2. Open GitHub issue #1 to review the concept: $($ClaudeMdResult.IssueUrl)" |
| 1191 | + } |
| 1192 | + Write-Host " 3. Run \`claude\` in the project directory to begin planning" |
| 1193 | + Write-Host '' |
873 | 1194 | } |
874 | 1195 |
|
875 | | -Write-Host '' |
876 | | -Write-Host ' Next steps:' |
877 | | -Write-Host " 1. cd `"$($scaffoldResult.TargetDir)`"" |
878 | | -Write-Host ' 2. Run .\setup\stack.ps1 to install toolchain for your chosen profiles' |
879 | | -Write-Host ' 3. Steps 3-4 (CLAUDE.md generation and workspace open) coming in issue #20' |
| 1196 | +# --------------------------------------------------------------------------- |
| 1197 | +# Main entry point |
| 1198 | +# --------------------------------------------------------------------------- |
| 1199 | + |
| 1200 | +Write-Section 'Kit 3: New Project' |
| 1201 | +Write-Host ' Collects project concept and creates directory structure.' |
880 | 1202 | Write-Host '' |
881 | 1203 |
|
882 | | -# Steps 3-4 (CLAUDE.md generation and workspace open) will be added in issue #20 |
| 1204 | +$conceptData = Invoke-ConceptCollection |
| 1205 | +$scaffoldResult = Invoke-Scaffolding -Data $conceptData |
| 1206 | +$claudeMdResult = Invoke-ClaudeMdGeneration -Data $conceptData -ScaffoldResult $scaffoldResult |
| 1207 | +Invoke-WorkspaceOpen -Data $conceptData -ScaffoldResult $scaffoldResult -ClaudeMdResult $claudeMdResult |
0 commit comments