@mriekki/project-forge 4.13.4 → 4.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,13 +8,13 @@ The Builder brings the idea and makes the material decisions. Project Forge supp
8
8
 
9
9
  ## What it feels like
10
10
 
11
- Start with an idea:
11
+ Start with the one Project-creation prompt:
12
12
 
13
13
  ```text
14
- New Project: I want to build a hiking trail tracker.
14
+ New Project
15
15
  ```
16
16
 
17
- Project Forge guides the Builder through:
17
+ Project Forge presents the currently supported ways to begin, explains each option, and then guides the Builder through the path they select. A greenfield path looks like:
18
18
 
19
19
  ```text
20
20
  idea
@@ -72,7 +72,8 @@ open personal invitation
72
72
  → accept invitation
73
73
  → automatic readiness verification
74
74
  → configure one ChatGPT coding Project
75
- → New Project: <idea>
75
+ → New Project
76
+ → choose a supported start mode
76
77
  ```
77
78
 
78
79
  **Project Forge Builder** is the GitHub App used for Project Forge authorization and project orchestration.
@@ -93,6 +94,8 @@ clone repository
93
94
 
94
95
  Project Forge supports independent workspace instances on multiple developer computers. Git/GitHub is the shared source boundary; local databases, credentials, SSH keys/trust, Docker state, and readiness stay workstation-local.
95
96
 
97
+ On Windows, Project Forge defaults compatible development to **WSL2 with Ubuntu**: keep repositories in the WSL Linux filesystem (normally `~/projects`), run shells/toolchains from WSL, keep desktop UI applications on Windows, and use Docker Desktop with its WSL2 backend/integration rather than installing a second Docker Engine inside the distro. Native Windows remains valid for Projects with a real Windows-specific requirement. Existing Windows checkouts can be migrated non-destructively with [`tools/windows/project-forge-wsl2.ps1`](tools/windows/project-forge-wsl2.ps1); see [`docs/WINDOWS_WSL2.md`](docs/WINDOWS_WSL2.md).
98
+
96
99
  ## Public ChatGPT contract
97
100
 
98
101
  Project Forge's public ChatGPT bootstrap entrypoint is [`chatgpt/START_HERE.md`](chatgpt/START_HERE.md).
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@mriekki/project-forge",
4
- "version": "4.13.4",
4
+ "version": "4.14.0",
5
5
  "defaultProfile": "greenfield-project",
6
6
  "profiles": {
7
7
  "greenfield-project": {
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "schemaVersion": 2,
3
3
  "package": "@mriekki/project-forge",
4
- "version": "4.13.4",
4
+ "version": "4.14.0",
5
5
  "components": {
6
6
  "core": {
7
7
  "revision": "b51cb62bcc9630748168c9fd1900c6109cafb587",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mriekki/project-forge",
3
- "version": "4.13.4",
3
+ "version": "4.14.0",
4
4
  "description": "Public consumer distribution and updater for Project Forge engineering assets.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,6 +10,7 @@
10
10
  "bin/",
11
11
  "src/",
12
12
  "assets/",
13
+ "tools/",
13
14
  "README.md"
14
15
  ],
15
16
  "scripts": {
@@ -0,0 +1,253 @@
1
+ [CmdletBinding(SupportsShouldProcess = $true)]
2
+ param(
3
+ [string]$Distro = "Ubuntu",
4
+ [string]$SourceRoot = (Get-Location).Path,
5
+ [string]$DestinationRoot = "~/projects",
6
+ [switch]$InstallMissing,
7
+ [switch]$MigrateRepositories,
8
+ [switch]$IncludeNestedRepositories,
9
+ [switch]$SkipDockerCheck
10
+ )
11
+
12
+ $ErrorActionPreference = "Stop"
13
+
14
+ function Write-Step([string]$Message) {
15
+ Write-Host "==> $Message"
16
+ }
17
+
18
+ function Invoke-Wsl {
19
+ param([Parameter(ValueFromRemainingArguments = $true)][string[]]$Arguments)
20
+ $output = & wsl.exe -d $Distro -- @Arguments 2>&1
21
+ if ($LASTEXITCODE -ne 0) {
22
+ throw "WSL command failed ($LASTEXITCODE): $($Arguments -join ' ')`n$output"
23
+ }
24
+ return $output
25
+ }
26
+
27
+ function Test-Command([string]$Name) {
28
+ return $null -ne (Get-Command $Name -ErrorAction SilentlyContinue)
29
+ }
30
+
31
+ function Get-GitValue([string]$Repository, [string[]]$Arguments) {
32
+ $result = & git -C $Repository @Arguments 2>$null
33
+ if ($LASTEXITCODE -ne 0) { return $null }
34
+ return (($result -join "`n").Trim())
35
+ }
36
+
37
+ function Convert-ToWslPath([string]$WindowsPath) {
38
+ return ((Invoke-Wsl wslpath -a $WindowsPath) -join "`n").Trim()
39
+ }
40
+
41
+ function Resolve-WslDestinationRoot {
42
+ $home = ((Invoke-Wsl sh -lc 'printf %s "$HOME"') -join "`n").Trim()
43
+ if ($DestinationRoot -eq "~") { return $home }
44
+ if ($DestinationRoot.StartsWith("~/")) {
45
+ return "$home/$($DestinationRoot.Substring(2))"
46
+ }
47
+ if ($DestinationRoot.StartsWith("/")) { return $DestinationRoot }
48
+ throw "DestinationRoot must be an absolute Linux path or start with ~/ (received '$DestinationRoot')."
49
+ }
50
+
51
+ function Get-RepositoryCandidates([string]$Root) {
52
+ $resolved = (Resolve-Path $Root).Path
53
+ $items = New-Object System.Collections.Generic.List[string]
54
+
55
+ if (Test-Path (Join-Path $resolved ".git")) {
56
+ $items.Add($resolved)
57
+ }
58
+
59
+ $children = Get-ChildItem -Path $resolved -Directory -Force
60
+ foreach ($child in $children) {
61
+ if (Test-Path (Join-Path $child.FullName ".git")) {
62
+ $items.Add($child.FullName)
63
+ }
64
+ }
65
+
66
+ if ($IncludeNestedRepositories) {
67
+ Get-ChildItem -Path $resolved -Directory -Force -Recurse -ErrorAction SilentlyContinue |
68
+ Where-Object { Test-Path (Join-Path $_.FullName ".git") } |
69
+ ForEach-Object { $items.Add($_.FullName) }
70
+ }
71
+
72
+ return $items | Sort-Object -Unique
73
+ }
74
+
75
+ if (-not [System.Environment]::OSVersion.Platform.ToString().StartsWith("Win")) {
76
+ throw "This tool must be run from Windows PowerShell or PowerShell on Windows."
77
+ }
78
+
79
+ Write-Step "Checking WSL2"
80
+ if (-not (Test-Command "wsl.exe")) {
81
+ if (-not $InstallMissing) {
82
+ throw "WSL is not available. Re-run with -InstallMissing from an elevated PowerShell session."
83
+ }
84
+ & wsl.exe --install -d $Distro
85
+ throw "WSL installation was requested. If Windows asks for a restart, restart first, complete the $Distro first-run setup, then run this command again."
86
+ }
87
+
88
+ & wsl.exe --set-default-version 2 | Out-Null
89
+ $distroNames = (& wsl.exe -l -q 2>$null) -replace "`0", "" | ForEach-Object { $_.Trim() } | Where-Object { $_ }
90
+ if ($distroNames -notcontains $Distro) {
91
+ if (-not $InstallMissing) {
92
+ throw "WSL distro '$Distro' is not installed. Re-run with -InstallMissing."
93
+ }
94
+ & wsl.exe --install -d $Distro
95
+ throw "Installation of '$Distro' was requested. Complete its first-run setup, then run this command again."
96
+ }
97
+
98
+ $versionLines = (& wsl.exe -l -v 2>$null) -replace "`0", ""
99
+ $distroLine = $versionLines | Where-Object { $_ -match "\b$([regex]::Escape($Distro))\b" } | Select-Object -First 1
100
+ if ($distroLine -and $distroLine -match "\s1\s*$") {
101
+ if (-not $InstallMissing) {
102
+ throw "'$Distro' is using WSL1. Re-run with -InstallMissing to convert it to WSL2."
103
+ }
104
+ & wsl.exe --set-version $Distro 2
105
+ if ($LASTEXITCODE -ne 0) { throw "Failed to convert '$Distro' to WSL2." }
106
+ }
107
+
108
+ Write-Step "Checking baseline Linux development tools"
109
+ $missingLinuxTools = @()
110
+ foreach ($tool in @("git", "curl", "tar")) {
111
+ & wsl.exe -d $Distro -- sh -lc "command -v $tool >/dev/null 2>&1"
112
+ if ($LASTEXITCODE -ne 0) { $missingLinuxTools += $tool }
113
+ }
114
+ if ($missingLinuxTools.Count -gt 0) {
115
+ if (-not $InstallMissing) {
116
+ throw "Missing WSL tools: $($missingLinuxTools -join ', '). Re-run with -InstallMissing."
117
+ }
118
+ Invoke-Wsl sudo apt-get update | Out-Null
119
+ Invoke-Wsl sudo apt-get install -y git curl ca-certificates tar build-essential | Out-Null
120
+ }
121
+
122
+ # Use Linux line endings for new checkouts unless the Builder already chose another global policy.
123
+ $wslAutocrlf = (& wsl.exe -d $Distro -- git config --global --get core.autocrlf 2>$null | Out-String).Trim()
124
+ if (-not $wslAutocrlf) {
125
+ Invoke-Wsl git config --global core.autocrlf input | Out-Null
126
+ }
127
+
128
+ # Carry Git identity into WSL only when WSL does not already have one.
129
+ if (Test-Command "git") {
130
+ foreach ($key in @("user.name", "user.email")) {
131
+ $existing = (& wsl.exe -d $Distro -- git config --global --get $key 2>$null | Out-String).Trim()
132
+ if (-not $existing) {
133
+ $hostValue = (& git config --global --get $key 2>$null | Out-String).Trim()
134
+ if ($hostValue) { Invoke-Wsl git config --global $key $hostValue | Out-Null }
135
+ }
136
+ }
137
+ }
138
+
139
+ Write-Step "Checking VS Code WSL integration"
140
+ if (Test-Command "code") {
141
+ & code --install-extension ms-vscode-remote.remote-wsl --force | Out-Null
142
+ } else {
143
+ Write-Warning "VS Code 'code' command was not found on Windows. Install VS Code if you want the recommended Windows UI + WSL workspace flow."
144
+ }
145
+
146
+ Write-Step "Checking Docker Desktop / WSL integration"
147
+ if (-not $SkipDockerCheck) {
148
+ $dockerDesktop = Get-Command "docker" -ErrorAction SilentlyContinue
149
+ if (-not $dockerDesktop -and $InstallMissing -and (Test-Command "winget")) {
150
+ & winget install --id Docker.DockerDesktop -e --accept-package-agreements --accept-source-agreements
151
+ Write-Warning "Docker Desktop installation was requested. Start Docker Desktop, enable WSL integration for '$Distro', then rerun this command to verify Docker from WSL."
152
+ }
153
+
154
+ & wsl.exe -d $Distro -- docker info 1>$null 2>$null
155
+ if ($LASTEXITCODE -ne 0) {
156
+ Write-Warning "Docker is not operational inside '$Distro'. Start Docker Desktop and enable Settings > Resources > WSL Integration for '$Distro'. Do not install a second Docker Engine inside WSL just to satisfy this check."
157
+ } else {
158
+ Write-Host "Docker from WSL: PASS"
159
+ }
160
+ }
161
+
162
+ if (-not $MigrateRepositories) {
163
+ Write-Host ""
164
+ Write-Host "WSL2 baseline setup: PASS"
165
+ Write-Host "No repositories were migrated. Re-run with -MigrateRepositories -SourceRoot <windows-folder> when ready."
166
+ exit 0
167
+ }
168
+
169
+ if (-not (Test-Command "git")) {
170
+ throw "Windows Git is required for migration preflight so source repositories can be verified before copying state."
171
+ }
172
+
173
+ $destinationRootLinux = Resolve-WslDestinationRoot
174
+ Invoke-Wsl mkdir -p $destinationRootLinux | Out-Null
175
+ $repositories = @(Get-RepositoryCandidates $SourceRoot)
176
+ if ($repositories.Count -eq 0) {
177
+ throw "No Git repositories found at '$SourceRoot'."
178
+ }
179
+
180
+ Write-Step "Migrating $($repositories.Count) repository/repositories to $Distro:$destinationRootLinux"
181
+ $migrated = New-Object System.Collections.Generic.List[string]
182
+ $skipped = New-Object System.Collections.Generic.List[string]
183
+
184
+ foreach ($repo in $repositories) {
185
+ $name = Split-Path $repo -Leaf
186
+ $dirty = (& git -C $repo status --porcelain=v1 2>$null | Out-String).Trim()
187
+ if ($LASTEXITCODE -ne 0) {
188
+ Write-Warning "Skipping '$repo': Git status failed."
189
+ $skipped.Add($repo)
190
+ continue
191
+ }
192
+ if ($dirty) {
193
+ Write-Warning "Skipping '$repo': working tree is not clean. Commit/stash/reconcile it first so migration cannot lose local work."
194
+ $skipped.Add($repo)
195
+ continue
196
+ }
197
+
198
+ $head = Get-GitValue $repo @("rev-parse", "HEAD")
199
+ $branch = Get-GitValue $repo @("branch", "--show-current")
200
+ $origin = Get-GitValue $repo @("config", "--get", "remote.origin.url")
201
+ $sourceLinux = Convert-ToWslPath $repo
202
+ $destinationLinux = "$destinationRootLinux/$name"
203
+
204
+ & wsl.exe -d $Distro -- test -e $destinationLinux
205
+ if ($LASTEXITCODE -eq 0) {
206
+ Write-Warning "Skipping '$repo': destination '$destinationLinux' already exists."
207
+ $skipped.Add($repo)
208
+ continue
209
+ }
210
+
211
+ if (-not $PSCmdlet.ShouldProcess($repo, "Create validated WSL2 checkout at $destinationLinux")) {
212
+ continue
213
+ }
214
+
215
+ # Clone from the local clean repository so committed local-only refs remain available during migration.
216
+ Invoke-Wsl git clone --no-hardlinks $sourceLinux $destinationLinux | Out-Null
217
+ if ($origin) {
218
+ Invoke-Wsl git -C $destinationLinux remote set-url origin $origin | Out-Null
219
+ }
220
+
221
+ $destHead = ((Invoke-Wsl git -C $destinationLinux rev-parse HEAD) -join "`n").Trim()
222
+ $destDirty = ((Invoke-Wsl git -C $destinationLinux status --porcelain=v1) -join "`n").Trim()
223
+ if ($destHead -ne $head) {
224
+ throw "Migration verification failed for '$name': source HEAD $head != destination HEAD $destHead. Windows source was left untouched."
225
+ }
226
+ if ($destDirty) {
227
+ throw "Migration verification failed for '$name': destination checkout is not clean. Windows source was left untouched."
228
+ }
229
+
230
+ if ($branch) {
231
+ $destBranch = ((Invoke-Wsl git -C $destinationLinux branch --show-current) -join "`n").Trim()
232
+ if ($destBranch -ne $branch) {
233
+ Write-Warning "'$name' migrated at the correct HEAD, but checked-out branch differs (source '$branch', destination '$destBranch')."
234
+ }
235
+ }
236
+
237
+ Write-Host "Migrated: $repo -> $Distro:$destinationLinux"
238
+ $migrated.Add($destinationLinux)
239
+ }
240
+
241
+ Write-Host ""
242
+ Write-Host "Migration complete."
243
+ Write-Host "Migrated: $($migrated.Count)"
244
+ Write-Host "Skipped: $($skipped.Count)"
245
+ Write-Host "Windows source checkouts were NOT deleted."
246
+ Write-Host ""
247
+ Write-Host "Next: open a migrated repository from WSL and run its normal Project Forge/project readiness checks."
248
+ if ($migrated.Count -gt 0) {
249
+ Write-Host "Example:"
250
+ Write-Host " wsl -d $Distro"
251
+ Write-Host " cd $($migrated[0])"
252
+ Write-Host " code ."
253
+ }