its-magic 0.1.2-10

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.
Files changed (97) hide show
  1. package/.cursor/agents/curator.mdc +21 -0
  2. package/.cursor/agents/dev.mdc +20 -0
  3. package/.cursor/agents/po.mdc +19 -0
  4. package/.cursor/agents/qa.mdc +19 -0
  5. package/.cursor/agents/release.mdc +19 -0
  6. package/.cursor/agents/tech-lead.mdc +21 -0
  7. package/.cursor/commands/architecture.md +29 -0
  8. package/.cursor/commands/auto.md +27 -0
  9. package/.cursor/commands/discovery.md +27 -0
  10. package/.cursor/commands/execute.md +32 -0
  11. package/.cursor/commands/intake.md +28 -0
  12. package/.cursor/commands/map-codebase.md +25 -0
  13. package/.cursor/commands/milestone-complete.md +24 -0
  14. package/.cursor/commands/milestone-start.md +26 -0
  15. package/.cursor/commands/pause.md +25 -0
  16. package/.cursor/commands/phase-context.md +25 -0
  17. package/.cursor/commands/plan-verify.md +26 -0
  18. package/.cursor/commands/qa.md +28 -0
  19. package/.cursor/commands/quick.md +24 -0
  20. package/.cursor/commands/refresh-context.md +26 -0
  21. package/.cursor/commands/release.md +29 -0
  22. package/.cursor/commands/research.md +28 -0
  23. package/.cursor/commands/resume.md +26 -0
  24. package/.cursor/commands/sprint-plan.md +30 -0
  25. package/.cursor/commands/verify-work.md +25 -0
  26. package/.cursor/hooks/README.md +13 -0
  27. package/.cursor/hooks/hook.py +197 -0
  28. package/.cursor/hooks.json +26 -0
  29. package/.cursor/plans/cursor-gsd-team-kit_8cfee9b8.plan.md +57 -0
  30. package/.cursor/remote.json +18 -0
  31. package/.cursor/rules/core.mdc +18 -0
  32. package/.cursor/rules/escalation.mdc +11 -0
  33. package/.cursor/rules/handoffs.mdc +10 -0
  34. package/.cursor/rules/quality.mdc +15 -0
  35. package/.cursor/scratchpad.md +34 -0
  36. package/.cursor/skills/its-magic/SKILL.md +39 -0
  37. package/.cursor/skills/its-magic/templates/acceptance.json +10 -0
  38. package/.cursor/skills/its-magic/templates/acceptance.md +7 -0
  39. package/.cursor/skills/its-magic/templates/architecture.json +11 -0
  40. package/.cursor/skills/its-magic/templates/architecture.md +14 -0
  41. package/.cursor/skills/its-magic/templates/decision.json +14 -0
  42. package/.cursor/skills/its-magic/templates/decision.md +19 -0
  43. package/.cursor/skills/its-magic/templates/handoff.json +6 -0
  44. package/.cursor/skills/its-magic/templates/handoff.md +12 -0
  45. package/.cursor/skills/its-magic/templates/milestone.json +7 -0
  46. package/.cursor/skills/its-magic/templates/phase-context.json +6 -0
  47. package/.cursor/skills/its-magic/templates/plan-verify.json +11 -0
  48. package/.cursor/skills/its-magic/templates/sprint.json +6 -0
  49. package/.cursor/skills/its-magic/templates/sprint.md +11 -0
  50. package/.cursor/skills/its-magic/templates/story.json +9 -0
  51. package/.cursor/skills/its-magic/templates/story.md +15 -0
  52. package/.cursor/skills/its-magic/templates/uat.json +15 -0
  53. package/.github/workflows/ci.yml +49 -0
  54. package/.github/workflows/deploy.yml +56 -0
  55. package/README.md +755 -0
  56. package/bin/its-magic.js +86 -0
  57. package/decisions/DEC-0001.md +21 -0
  58. package/decisions/DEC-0002.md +21 -0
  59. package/docs/engineering/architecture.md +354 -0
  60. package/docs/engineering/codebase-map.md +14 -0
  61. package/docs/engineering/context/phase-template.json +6 -0
  62. package/docs/engineering/decisions.md +6 -0
  63. package/docs/engineering/dependencies.json +5 -0
  64. package/docs/engineering/research.md +11 -0
  65. package/docs/engineering/runbook.md +32 -0
  66. package/docs/engineering/state.md +33 -0
  67. package/docs/product/acceptance.md +6 -0
  68. package/docs/product/backlog.md +7 -0
  69. package/docs/product/vision.md +46 -0
  70. package/handoffs/dev_to_qa.md +8 -0
  71. package/handoffs/po_to_tl.md +8 -0
  72. package/handoffs/qa_to_dev.md +6 -0
  73. package/handoffs/release_notes.md +14 -0
  74. package/handoffs/resume_brief.md +8 -0
  75. package/handoffs/tl_to_dev.md +7 -0
  76. package/installer.ps1 +189 -0
  77. package/installer.py +195 -0
  78. package/installer.sh +201 -0
  79. package/milestones/M0001/milestone.json +7 -0
  80. package/milestones/M0001/phases.json +9 -0
  81. package/milestones/M0001/progress.md +3 -0
  82. package/milestones/M0001/summary.md +3 -0
  83. package/package.json +38 -0
  84. package/scripts/generate-release-notes.ps1 +74 -0
  85. package/scripts/generate-release-notes.sh +63 -0
  86. package/scripts/release-all.ps1 +423 -0
  87. package/scripts/release-all.sh +226 -0
  88. package/sprints/S0001/plan-verify.json +5 -0
  89. package/sprints/S0001/progress.md +4 -0
  90. package/sprints/S0001/qa-findings.md +113 -0
  91. package/sprints/S0001/sprint.md +70 -0
  92. package/sprints/S0001/summary.md +46 -0
  93. package/sprints/S0001/tasks.md +35 -0
  94. package/sprints/S0001/uat.json +8 -0
  95. package/sprints/S0001/uat.md +8 -0
  96. package/sprints/quick/Q0001/summary.md +3 -0
  97. package/sprints/quick/Q0001/task.json +6 -0
@@ -0,0 +1,74 @@
1
+ Param(
2
+ [string]$RepoRoot,
3
+ [string]$Version = "v0.0.0",
4
+ [string]$Sprint = "S0001"
5
+ )
6
+
7
+ $ErrorActionPreference = "Stop"
8
+
9
+ function Resolve-RepoRoot {
10
+ if ($RepoRoot) { return (Resolve-Path $RepoRoot).Path }
11
+ return (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
12
+ }
13
+
14
+ function Read-IfExists($Path) {
15
+ if (Test-Path $Path -PathType Leaf) {
16
+ return Get-Content -Path $Path -Raw
17
+ }
18
+ return ""
19
+ }
20
+
21
+ $root = Resolve-RepoRoot
22
+ $summaryPath = Join-Path $root "sprints\$Sprint\summary.md"
23
+ $qaPath = Join-Path $root "sprints\$Sprint\qa-findings.md"
24
+ $runbookPath = Join-Path $root "docs\engineering\runbook.md"
25
+ $outPath = Join-Path $root "handoffs\release_notes.md"
26
+
27
+ $summary = Read-IfExists $summaryPath
28
+ $qa = Read-IfExists $qaPath
29
+ $runbook = Read-IfExists $runbookPath
30
+
31
+ $gitChanges = ""
32
+ if (Get-Command git -ErrorAction SilentlyContinue) {
33
+ try {
34
+ $isRepo = git -C $root rev-parse --is-inside-work-tree 2>$null
35
+ if ($isRepo -eq "true") {
36
+ $gitChanges = git -C $root log -n 20 --pretty=format:"- %s"
37
+ }
38
+ } catch {}
39
+ }
40
+
41
+ $timestamp = (Get-Date).ToString("yyyy-MM-dd")
42
+
43
+ @"
44
+ # Release Notes — $Version
45
+
46
+ **Sprint:** $Sprint
47
+ **Date:** $timestamp
48
+
49
+ ---
50
+
51
+ ## Summary
52
+
53
+ ${summary.Trim()}
54
+
55
+ ---
56
+
57
+ ## Changes (last 20 commits)
58
+
59
+ ${gitChanges.Trim()}
60
+
61
+ ---
62
+
63
+ ## QA Findings (from sprint)
64
+
65
+ ${qa.Trim()}
66
+
67
+ ---
68
+
69
+ ## Runbook Notes
70
+
71
+ ${runbook.Trim()}
72
+ "@ | Set-Content -Path $outPath
73
+
74
+ Write-Host "Release notes written to: $outPath"
@@ -0,0 +1,63 @@
1
+ #!/usr/bin/env sh
2
+ set -e
3
+
4
+ ROOT="${1:-$(cd "$(dirname "$0")/.." && pwd)}"
5
+ VERSION="${2:-v0.0.0}"
6
+ SPRINT="${3:-S0001}"
7
+
8
+ SUMMARY_PATH="$ROOT/sprints/$SPRINT/summary.md"
9
+ QA_PATH="$ROOT/sprints/$SPRINT/qa-findings.md"
10
+ RUNBOOK_PATH="$ROOT/docs/engineering/runbook.md"
11
+ OUT_PATH="$ROOT/handoffs/release_notes.md"
12
+
13
+ read_if_exists() {
14
+ if [ -f "$1" ]; then
15
+ cat "$1"
16
+ fi
17
+ }
18
+
19
+ summary="$(read_if_exists "$SUMMARY_PATH")"
20
+ qa="$(read_if_exists "$QA_PATH")"
21
+ runbook="$(read_if_exists "$RUNBOOK_PATH")"
22
+
23
+ git_changes=""
24
+ if command -v git >/dev/null 2>&1; then
25
+ if git -C "$ROOT" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
26
+ git_changes="$(git -C "$ROOT" log -n 20 --pretty=format:"- %s")"
27
+ fi
28
+ fi
29
+
30
+ timestamp="$(date +"%Y-%m-%d")"
31
+
32
+ cat > "$OUT_PATH" <<EOF
33
+ # Release Notes — $VERSION
34
+
35
+ **Sprint:** $SPRINT
36
+ **Date:** $timestamp
37
+
38
+ ---
39
+
40
+ ## Summary
41
+
42
+ $summary
43
+
44
+ ---
45
+
46
+ ## Changes (last 20 commits)
47
+
48
+ $git_changes
49
+
50
+ ---
51
+
52
+ ## QA Findings (from sprint)
53
+
54
+ $qa
55
+
56
+ ---
57
+
58
+ ## Runbook Notes
59
+
60
+ $runbook
61
+ EOF
62
+
63
+ echo "Release notes written to: $OUT_PATH"
@@ -0,0 +1,423 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Unified release: npm + Chocolatey + Homebrew (all three at once).
4
+ .DESCRIPTION
5
+ 1. Bumps version in package.json (patch|minor|major|explicit)
6
+ 2. Publishes to npm
7
+ 3. Creates a GitHub release with a source zip (for Homebrew/Chocolatey URLs)
8
+ 4. Updates Chocolatey nuspec + pushes to chocolatey.org
9
+ 5. Updates Homebrew formula with new URL + sha256
10
+ .PARAMETER Bump
11
+ Version bump type: patch, minor, major, or an explicit semver (e.g. 1.2.3).
12
+ Default: patch
13
+ .PARAMETER NpmTag
14
+ npm dist-tag (e.g. latest, beta, rc). Default: latest
15
+ .PARAMETER SkipNpm
16
+ Skip npm publish.
17
+ .PARAMETER SkipChoco
18
+ Skip Chocolatey push.
19
+ .PARAMETER SkipBrew
20
+ Skip Homebrew formula update.
21
+ .PARAMETER DryRun
22
+ Print what would happen without executing.
23
+ #>
24
+ param(
25
+ [string]$Bump = "patch",
26
+ [string]$NpmTag = "latest",
27
+ [switch]$SkipNpm,
28
+ [switch]$SkipChoco,
29
+ [switch]$SkipBrew,
30
+ [switch]$DryRun,
31
+ [switch]$SkipBrewPush,
32
+ [string]$BrewTapRepo = "",
33
+ [string]$BrewTapBranch = "main",
34
+ [string]$BrewTapDir = "",
35
+ [bool]$CreateBrewTapIfMissing = $true
36
+ )
37
+
38
+ $ErrorActionPreference = 'Stop'
39
+ $repoRoot = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
40
+ if (-not (Test-Path (Join-Path $repoRoot 'package.json'))) {
41
+ $repoRoot = Split-Path -Parent $PSScriptRoot
42
+ }
43
+ Push-Location $repoRoot
44
+
45
+ # ── Helpers ──────────────────────────────────────────────────────────
46
+ function Log($msg) { Write-Host "[release] $msg" -ForegroundColor Cyan }
47
+ function Warn($msg){ Write-Host "[release] $msg" -ForegroundColor Yellow }
48
+ function Err($msg) { Write-Host "[release] $msg" -ForegroundColor Red; Pop-Location; exit 1 }
49
+
50
+ # Quote char for building regex patterns (avoids PS 5.1 parser issues)
51
+ $DQ = [char]34
52
+ $chocoFailed = $false
53
+ $brewTapFailed = $false
54
+ $brewTapPushed = $false
55
+
56
+ # ── 1. Version bump ─────────────────────────────────────────────────
57
+ Log "Bumping version ($Bump) ..."
58
+ if ($DryRun) {
59
+ Log "(dry-run) would run: npm version $Bump --no-git-tag-version"
60
+ $newVersion = "0.0.0-dryrun"
61
+ } else {
62
+ npm version $Bump --no-git-tag-version | Out-Null
63
+ $pkg = Get-Content package.json -Raw | ConvertFrom-Json
64
+ $newVersion = $pkg.version
65
+ }
66
+ Log "New version: $newVersion"
67
+
68
+ # ── 2. npm publish ───────────────────────────────────────────────────
69
+ if (-not $SkipNpm) {
70
+ Log "Publishing to npm (tag=$NpmTag) ..."
71
+ if ($DryRun) {
72
+ Log "(dry-run) would run: npm publish --tag $NpmTag"
73
+ } else {
74
+ npm publish --tag $NpmTag
75
+ if ($LASTEXITCODE -ne 0) { Err "npm publish failed" }
76
+ Log "npm publish OK"
77
+ }
78
+ } else {
79
+ Warn "Skipping npm"
80
+ }
81
+
82
+ # ── 3. GitHub release (needed by Homebrew + Chocolatey) ──────────────
83
+ $ghAvailable = $null -ne (Get-Command gh -ErrorAction SilentlyContinue)
84
+ $tagName = "v$newVersion"
85
+ $zipUrl = ""
86
+ $tarUrl = ""
87
+
88
+ $isPrerelease = $newVersion -match '-'
89
+ $chocoVersion = $newVersion
90
+ if ($newVersion -match '^(\d+\.\d+\.\d+)-(\d+)$') {
91
+ # Old Chocolatey/NuGet rejects numeric-only prerelease labels (e.g. 0.1.1-1).
92
+ # Convert to a compatible label for nuspec only.
93
+ $chocoVersion = "$($Matches[1])-beta$($Matches[2])"
94
+ }
95
+ if ($chocoVersion -ne $newVersion) {
96
+ Log "Chocolatey version normalized: $newVersion -> $chocoVersion"
97
+ }
98
+
99
+ if ($ghAvailable) {
100
+ Log "Creating GitHub release $tagName ..."
101
+ if ($DryRun) {
102
+ Log "(dry-run) would run: gh release create $tagName --generate-notes"
103
+ $zipUrl = "https://github.com/USER/its-magic/archive/refs/tags/$tagName.zip"
104
+ $tarUrl = "https://github.com/USER/its-magic/archive/refs/tags/$tagName.tar.gz"
105
+ } else {
106
+ $ghArgs = @("release", "create", $tagName, "--generate-notes", "--title", $tagName)
107
+ if ($isPrerelease) { $ghArgs += "--prerelease" }
108
+ & gh @ghArgs
109
+ if ($LASTEXITCODE -ne 0) { Warn "gh release create failed - continuing anyway" }
110
+ # Derive archive URL from current remote
111
+ $remoteUrl = git remote get-url origin 2>$null
112
+ if ($remoteUrl -match 'github\.com[:/](.+?)(\.git)?$') {
113
+ $repoSlug = $Matches[1]
114
+ $zipUrl = "https://github.com/$repoSlug/archive/refs/tags/$tagName.zip"
115
+ $tarUrl = "https://github.com/$repoSlug/archive/refs/tags/$tagName.tar.gz"
116
+ }
117
+ Log "GitHub release created: $tagName"
118
+ }
119
+ } else {
120
+ Warn "gh CLI not found - skipping GitHub release"
121
+ $zipUrl = "https://github.com/USER/its-magic/archive/refs/tags/$tagName.zip"
122
+ $tarUrl = "https://github.com/USER/its-magic/archive/refs/tags/$tagName.tar.gz"
123
+ }
124
+
125
+ # ── 4. Chocolatey ───────────────────────────────────────────────────
126
+ if (-not $SkipChoco) {
127
+ $chocoDir = Join-Path $repoRoot 'packaging\chocolatey'
128
+ $nuspec = Join-Path $chocoDir 'its-magic.nuspec'
129
+ $chocoInstall = Join-Path $chocoDir 'tools\chocolateyInstall.ps1'
130
+
131
+ if (Test-Path $nuspec) {
132
+ Log "Updating Chocolatey nuspec to $chocoVersion ..."
133
+ if (-not $DryRun) {
134
+ # Update version in nuspec
135
+ $xml = [xml](Get-Content $nuspec -Raw)
136
+ $xml.package.metadata.version = $chocoVersion
137
+ $xml.Save($nuspec)
138
+
139
+ # Update URL in install script
140
+ $installContent = Get-Content $chocoInstall -Raw
141
+ if ($zipUrl) {
142
+ $installContent = $installContent -replace "\`$url\s*=\s*'[^']+'", "`$url = '$zipUrl'"
143
+ }
144
+ Set-Content -Path $chocoInstall -Value $installContent
145
+ }
146
+
147
+ # Compute checksum if we have the zip
148
+ if ($zipUrl -and -not $DryRun) {
149
+ Log "Downloading zip for checksum ..."
150
+ $tmpZip = Join-Path $env:TEMP "its-magic-$tagName.zip"
151
+ try {
152
+ Invoke-WebRequest -Uri $zipUrl -OutFile $tmpZip -UseBasicParsing
153
+ $sha = (Get-FileHash -Path $tmpZip -Algorithm SHA256).Hash.ToLower()
154
+ $installContent = Get-Content $chocoInstall -Raw
155
+ $installContent = $installContent -replace "\`$checksum\s*=\s*'[^']+'", "`$checksum = '$sha'"
156
+ Set-Content -Path $chocoInstall -Value $installContent
157
+ Remove-Item $tmpZip -Force
158
+ Log "Checksum: $sha"
159
+ } catch {
160
+ Warn "Could not download zip for checksum - set PLACEHOLDER manually"
161
+ }
162
+ }
163
+
164
+ $chocoAvailable = $null -ne (Get-Command choco -ErrorAction SilentlyContinue)
165
+ if ($chocoAvailable) {
166
+ Log "Packing + pushing Chocolatey package ..."
167
+ if ($DryRun) {
168
+ Log "(dry-run) would run: choco pack + choco push"
169
+ } else {
170
+ Push-Location $chocoDir
171
+ choco pack
172
+ if ($LASTEXITCODE -ne 0) {
173
+ $chocoFailed = $true
174
+ Warn "choco pack failed"
175
+ } else {
176
+ $nupkg = Get-ChildItem "*.nupkg" | Sort-Object LastWriteTime -Descending | Select-Object -First 1
177
+ if ($nupkg) {
178
+ choco push $nupkg.Name --source https://push.chocolatey.org/
179
+ if ($LASTEXITCODE -ne 0) {
180
+ $chocoFailed = $true
181
+ Warn "choco push failed"
182
+ }
183
+ else { Log "Chocolatey push OK" }
184
+ } else {
185
+ $chocoFailed = $true
186
+ Warn "No .nupkg created by choco pack"
187
+ }
188
+ }
189
+ Pop-Location
190
+ }
191
+ } else {
192
+ $chocoFailed = $true
193
+ Warn "choco not found - nuspec updated but not pushed. Run choco pack and choco push manually."
194
+ }
195
+ } else {
196
+ $chocoFailed = $true
197
+ Warn "nuspec not found at $nuspec - skipping Chocolatey"
198
+ }
199
+ } else {
200
+ Warn "Skipping Chocolatey"
201
+ }
202
+
203
+ # ── 5. Homebrew ─────────────────────────────────────────────────────
204
+ if (-not $SkipBrew) {
205
+ # Detect pre-release: anything with a hyphen
206
+ if ($isPrerelease) {
207
+ $formulaPath = Join-Path $repoRoot 'packaging\homebrew\its-magic-beta.rb'
208
+ Log "Pre-release detected - using beta formula"
209
+ } else {
210
+ $formulaPath = Join-Path $repoRoot 'packaging\homebrew\its-magic.rb'
211
+ }
212
+
213
+ if (Test-Path $formulaPath) {
214
+ $formulaName = Split-Path -Leaf $formulaPath
215
+ Log "Updating Homebrew formula ($formulaName) to $newVersion ..."
216
+ if (-not $DryRun) {
217
+ $formula = Get-Content $formulaPath -Raw
218
+
219
+ # Update version in URL
220
+ if ($tarUrl) {
221
+ $urlPattern = 'url ' + $DQ + 'https://github\.com/[^' + $DQ + ']+\.tar\.gz' + $DQ
222
+ $urlReplace = 'url ' + $DQ + $tarUrl + $DQ
223
+ $formula = $formula -replace $urlPattern, $urlReplace
224
+ } else {
225
+ $formula = $formula -replace 'vVERSION', $tagName
226
+ }
227
+
228
+ # Update explicit version line for beta formula
229
+ if ($isPrerelease) {
230
+ $verPattern = 'version ' + $DQ + '[^' + $DQ + ']*' + $DQ
231
+ $verReplace = 'version ' + $DQ + $newVersion + $DQ
232
+ $formula = $formula -replace $verPattern, $verReplace
233
+ }
234
+
235
+ # Compute tar.gz sha256 if possible
236
+ if ($tarUrl) {
237
+ $tmpTar = Join-Path $env:TEMP "its-magic-$tagName.tar.gz"
238
+ try {
239
+ Invoke-WebRequest -Uri $tarUrl -OutFile $tmpTar -UseBasicParsing
240
+ $sha = (Get-FileHash -Path $tmpTar -Algorithm SHA256).Hash.ToLower()
241
+ $shaPattern = 'sha256 ' + $DQ + '[^' + $DQ + ']*' + $DQ
242
+ $shaReplace = 'sha256 ' + $DQ + $sha + $DQ
243
+ $formula = $formula -replace $shaPattern, $shaReplace
244
+ Remove-Item $tmpTar -Force
245
+ Log "Homebrew sha256: $sha"
246
+ } catch {
247
+ Warn "Could not download tar.gz - set sha256 manually in the formula"
248
+ }
249
+ }
250
+
251
+ Set-Content -Path $formulaPath -Value $formula
252
+ Log "Homebrew formula updated: $formulaName"
253
+ if ($isPrerelease) {
254
+ Log "Users install beta with: brew install USER/tap/its-magic-beta"
255
+ } else {
256
+ Log "Users install stable with: brew install USER/tap/its-magic"
257
+ }
258
+ } else {
259
+ $formulaName = Split-Path -Leaf $formulaPath
260
+ Log "(dry-run) would update $formulaName"
261
+ }
262
+ } else {
263
+ Warn "Formula not found at $formulaPath - skipping Homebrew"
264
+ }
265
+
266
+ # Optional: publish formulas to Homebrew tap repository
267
+ if (-not $SkipBrewPush) {
268
+ if ($DryRun) {
269
+ Log "(dry-run) would publish formulas to Homebrew tap"
270
+ } else {
271
+ $gitAvailable = $null -ne (Get-Command git -ErrorAction SilentlyContinue)
272
+ $ghAvailableForTap = $null -ne (Get-Command gh -ErrorAction SilentlyContinue)
273
+ if (-not $gitAvailable) {
274
+ $brewTapFailed = $true
275
+ Warn "git not found - cannot push Homebrew formulas to tap"
276
+ } else {
277
+ if (-not $BrewTapRepo) {
278
+ $originUrl = git remote get-url origin 2>$null
279
+ if ($originUrl -match 'github\.com[:/](.+?)/(.+?)(\.git)?$') {
280
+ $owner = $Matches[1]
281
+ $BrewTapRepo = "$owner/homebrew-tap"
282
+ }
283
+ }
284
+
285
+ if (-not $BrewTapRepo) {
286
+ $brewTapFailed = $true
287
+ Warn "Could not infer Homebrew tap repo. Pass -BrewTapRepo owner/homebrew-tap"
288
+ } else {
289
+ if (-not $BrewTapDir) {
290
+ $safeTap = ($BrewTapRepo -replace '[^a-zA-Z0-9_-]', '-')
291
+ $BrewTapDir = Join-Path $env:TEMP "its-magic-$safeTap"
292
+ }
293
+
294
+ Log "Publishing formulas to tap $BrewTapRepo (branch: $BrewTapBranch) ..."
295
+
296
+ # Use a child scope with relaxed error handling for git commands
297
+ # that write to stderr even on success (e.g. empty repo warnings).
298
+ $tapReady = $true
299
+ $prevEAP = $ErrorActionPreference
300
+ $ErrorActionPreference = 'Continue'
301
+
302
+ # ── Step A: Ensure we have a local clone ──
303
+ $tapGitDir = Join-Path $BrewTapDir ".git"
304
+
305
+ # Clean up non-git directory leftovers
306
+ if ((Test-Path $BrewTapDir) -and -not (Test-Path $tapGitDir)) {
307
+ Warn "Tap directory exists but is not a git repo. Removing: $BrewTapDir"
308
+ Remove-Item -Recurse -Force $BrewTapDir
309
+ }
310
+
311
+ if (Test-Path $tapGitDir) {
312
+ # Already cloned - fetch latest
313
+ git -C $BrewTapDir fetch origin 2>&1 | Out-Null
314
+ } else {
315
+ # Clone (try gh first, then git)
316
+ $cloneOk = $false
317
+ if ($ghAvailableForTap) {
318
+ & gh repo clone $BrewTapRepo $BrewTapDir 2>&1 | Out-Null
319
+ if ($LASTEXITCODE -eq 0) { $cloneOk = $true }
320
+ }
321
+ if (-not $cloneOk) {
322
+ git clone "https://github.com/$BrewTapRepo.git" $BrewTapDir 2>&1 | Out-Null
323
+ if ($LASTEXITCODE -eq 0) { $cloneOk = $true }
324
+ }
325
+ if (-not $cloneOk -and $CreateBrewTapIfMissing -and $ghAvailableForTap) {
326
+ Warn "Tap repo not found. Creating $BrewTapRepo on GitHub ..."
327
+ & gh repo create $BrewTapRepo --public --description "Homebrew tap for its-magic" 2>&1 | Out-Null
328
+ if ($LASTEXITCODE -eq 0) {
329
+ Log "Tap repo created: https://github.com/$BrewTapRepo"
330
+ if (Test-Path $BrewTapDir) { Remove-Item -Recurse -Force $BrewTapDir }
331
+ & gh repo clone $BrewTapRepo $BrewTapDir 2>&1 | Out-Null
332
+ if ($LASTEXITCODE -eq 0) { $cloneOk = $true }
333
+ }
334
+ }
335
+ if (-not $cloneOk) {
336
+ $tapReady = $false; $brewTapFailed = $true
337
+ Warn "Failed to clone/create tap repo $BrewTapRepo"
338
+ }
339
+ }
340
+
341
+ # ── Step B: Ensure branch exists ──
342
+ if ($tapReady) {
343
+ # Check if repo has any commits at all
344
+ $hasCommits = $false
345
+ git -C $BrewTapDir log --oneline -1 2>&1 | Out-Null
346
+ if ($LASTEXITCODE -eq 0) { $hasCommits = $true }
347
+
348
+ if ($hasCommits) {
349
+ git -C $BrewTapDir checkout $BrewTapBranch 2>&1 | Out-Null
350
+ if ($LASTEXITCODE -ne 0) {
351
+ git -C $BrewTapDir checkout -b $BrewTapBranch 2>&1 | Out-Null
352
+ }
353
+ # Try to pull if remote branch exists
354
+ git -C $BrewTapDir pull origin $BrewTapBranch 2>&1 | Out-Null
355
+ } else {
356
+ Log "Empty tap repo detected - creating initial branch $BrewTapBranch"
357
+ git -C $BrewTapDir checkout --orphan $BrewTapBranch 2>&1 | Out-Null
358
+ }
359
+ }
360
+
361
+ # ── Step C: Copy formulas, commit, push ──
362
+ if ($tapReady) {
363
+ $tapFormulaDir = Join-Path $BrewTapDir "Formula"
364
+ if (-not (Test-Path $tapFormulaDir)) {
365
+ New-Item -ItemType Directory -Path $tapFormulaDir -Force | Out-Null
366
+ }
367
+
368
+ Copy-Item (Join-Path $repoRoot "packaging\homebrew\its-magic.rb") (Join-Path $tapFormulaDir "its-magic.rb") -Force
369
+ Copy-Item (Join-Path $repoRoot "packaging\homebrew\its-magic-beta.rb") (Join-Path $tapFormulaDir "its-magic-beta.rb") -Force
370
+
371
+ git -C $BrewTapDir add Formula/its-magic.rb Formula/its-magic-beta.rb
372
+
373
+ $tapChanges = git -C $BrewTapDir status --porcelain
374
+ if ($tapChanges) {
375
+ git -C $BrewTapDir commit -m "chore: update its-magic formulas for $newVersion" 2>&1 | Out-Null
376
+ if ($LASTEXITCODE -ne 0) {
377
+ $brewTapFailed = $true
378
+ Warn "Failed to commit Homebrew tap changes"
379
+ } else {
380
+ git -C $BrewTapDir push -u origin $BrewTapBranch 2>&1 | Out-Null
381
+ if ($LASTEXITCODE -ne 0) {
382
+ $brewTapFailed = $true
383
+ Warn "Failed to push Homebrew tap changes"
384
+ } else {
385
+ $brewTapPushed = $true
386
+ Log "Homebrew tap updated: https://github.com/$BrewTapRepo"
387
+ }
388
+ }
389
+ } else {
390
+ Log "No Homebrew formula changes to push"
391
+ }
392
+ }
393
+
394
+ $ErrorActionPreference = $prevEAP
395
+ }
396
+ }
397
+ }
398
+ } else {
399
+ Warn "Skipping Homebrew tap push"
400
+ }
401
+ } else {
402
+ Warn "Skipping Homebrew"
403
+ }
404
+
405
+ # ── Done ─────────────────────────────────────────────────────────────
406
+ Pop-Location
407
+ $npmStatus = "OK"
408
+ if ($SkipNpm) { $npmStatus = "SKIPPED" }
409
+ $chocoStatus = "OK"
410
+ if ($SkipChoco) { $chocoStatus = "SKIPPED" }
411
+ elseif ($DryRun) { $chocoStatus = "DRY-RUN" }
412
+ elseif ($chocoFailed) { $chocoStatus = "FAILED - check log" }
413
+ $brewStatus = "OK - formula updated"
414
+ if ($SkipBrew) { $brewStatus = "SKIPPED" }
415
+ elseif ($brewTapFailed) { $brewStatus = "FORMULA OK - TAP PUSH FAILED" }
416
+ elseif ($brewTapPushed) { $brewStatus = "OK - formula updated + tap pushed" }
417
+
418
+ Log "=========================================="
419
+ Log "Release $newVersion complete!"
420
+ Log " npm: $npmStatus"
421
+ Log " Chocolatey: $chocoStatus"
422
+ Log " Homebrew: $brewStatus"
423
+ Log "=========================================="