@ijfw/install 1.1.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/CHANGELOG.md +107 -0
- package/LICENSE +21 -0
- package/README.md +53 -0
- package/dist/ijfw.js +1316 -0
- package/dist/install.js +279 -0
- package/dist/uninstall.js +259 -0
- package/package.json +58 -0
- package/src/install.ps1 +277 -0
package/src/install.ps1
ADDED
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# IJFW Windows-native installer (F4).
|
|
2
|
+
# PowerShell 5.1+ / PowerShell Core on Windows. No WSL required.
|
|
3
|
+
#
|
|
4
|
+
# Mirrors installer/src/install.js flow:
|
|
5
|
+
# preflight -> resolve target -> clone/pull -> run scripts/install.sh via Git Bash
|
|
6
|
+
# -> merge marketplace into %USERPROFILE%\.claude\settings.json -> summary.
|
|
7
|
+
#
|
|
8
|
+
# Usage:
|
|
9
|
+
# Invoke-Expression (iwr https://raw.githubusercontent.com/TheRealSeanDonahoe/ijfw/main/installer/src/install.ps1).Content
|
|
10
|
+
# or:
|
|
11
|
+
# .\install.ps1 -Dir C:\Users\me\.ijfw -Branch main
|
|
12
|
+
|
|
13
|
+
[CmdletBinding()]
|
|
14
|
+
param(
|
|
15
|
+
[string]$Dir = "",
|
|
16
|
+
[string]$Branch = "main",
|
|
17
|
+
[switch]$NoMarketplace,
|
|
18
|
+
[switch]$Yes,
|
|
19
|
+
[switch]$Purge
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
$ErrorActionPreference = "Stop"
|
|
23
|
+
$DEFAULT_REPO = "https://github.com/TheRealSeanDonahoe/ijfw.git"
|
|
24
|
+
|
|
25
|
+
function Write-Ok($msg) { Write-Host " [ok] $msg" -ForegroundColor Green }
|
|
26
|
+
function Write-Info($msg) { Write-Host " ... $msg" -ForegroundColor Gray }
|
|
27
|
+
|
|
28
|
+
function Test-Command($cmd) {
|
|
29
|
+
try { Get-Command $cmd -ErrorAction Stop | Out-Null; return $true } catch { return $false }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function Get-Target {
|
|
33
|
+
if ($Dir) {
|
|
34
|
+
$resolved = Resolve-Path -LiteralPath $Dir -ErrorAction SilentlyContinue
|
|
35
|
+
if ($resolved) { return $resolved.Path } else { return $Dir }
|
|
36
|
+
}
|
|
37
|
+
if ($env:IJFW_HOME) { return $env:IJFW_HOME }
|
|
38
|
+
return Join-Path $env:USERPROFILE ".ijfw"
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function Invoke-Preflight {
|
|
42
|
+
$issues = @()
|
|
43
|
+
$node = if (Test-Command node) { (node --version) } else { $null }
|
|
44
|
+
if (-not $node -or ([int]($node -replace 'v(\d+)\..*','$1') -lt 18)) {
|
|
45
|
+
$issues += "Node 18+ unlocks IJFW (found $node). Grab it from https://nodejs.org and we'll pick up where you left off."
|
|
46
|
+
}
|
|
47
|
+
if (-not (Test-Command git)) { $issues += "Install Git for Windows (https://git-scm.com) and rerun -- it bundles everything we need." }
|
|
48
|
+
if (-not (Resolve-GitBash)) { $issues += "IJFW needs Git Bash (ships with Git for Windows). Install Git for Windows and rerun -- takes 60 seconds." }
|
|
49
|
+
return $issues
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
# Locate Git Bash explicitly. On Windows the plain `bash` command often
|
|
53
|
+
# resolves to WSL's bash, which fails with 'No such file or directory' when
|
|
54
|
+
# no Linux distro is installed. Git for Windows ships bash.exe alongside
|
|
55
|
+
# git.exe under <git-root>\bin\ (or \usr\bin\), so derive it from git's path.
|
|
56
|
+
function Resolve-GitBash {
|
|
57
|
+
$gitCmd = Get-Command git -ErrorAction SilentlyContinue
|
|
58
|
+
if ($gitCmd) {
|
|
59
|
+
$gitDir = Split-Path -Parent $gitCmd.Source
|
|
60
|
+
$candidates = @(
|
|
61
|
+
(Join-Path $gitDir 'bash.exe'),
|
|
62
|
+
(Join-Path (Split-Path -Parent $gitDir) 'bin\bash.exe'),
|
|
63
|
+
(Join-Path (Split-Path -Parent $gitDir) 'usr\bin\bash.exe')
|
|
64
|
+
)
|
|
65
|
+
foreach ($c in $candidates) { if (Test-Path $c) { return $c } }
|
|
66
|
+
}
|
|
67
|
+
foreach ($c in @(
|
|
68
|
+
'C:\Program Files\Git\bin\bash.exe',
|
|
69
|
+
'C:\Program Files\Git\usr\bin\bash.exe',
|
|
70
|
+
'C:\Program Files (x86)\Git\bin\bash.exe'
|
|
71
|
+
)) { if (Test-Path $c) { return $c } }
|
|
72
|
+
return $null
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function Invoke-CloneOrPull($target, $branch) {
|
|
76
|
+
if (-not (Test-Path $target)) {
|
|
77
|
+
# Fresh install.
|
|
78
|
+
New-Item -ItemType Directory -Force -Path $target | Out-Null
|
|
79
|
+
& git clone --depth 1 --branch $branch $DEFAULT_REPO $target
|
|
80
|
+
if ($LASTEXITCODE -ne 0) { throw "Could not reach the IJFW repo (exit $LASTEXITCODE). Check your network connection and retry." }
|
|
81
|
+
return "cloned"
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
# Upgrade path.
|
|
85
|
+
$hasGit = Test-Path (Join-Path $target ".git")
|
|
86
|
+
if ($hasGit) {
|
|
87
|
+
& git -C $target remote get-url origin 2>$null | Out-Null
|
|
88
|
+
if ($LASTEXITCODE -eq 0) {
|
|
89
|
+
# fetch + hard checkout avoids ff-only failures from local divergence.
|
|
90
|
+
& git -C $target fetch --depth 1 origin $branch
|
|
91
|
+
if ($LASTEXITCODE -ne 0) { throw "Could not reach the IJFW repo (exit $LASTEXITCODE). Check your network connection and retry." }
|
|
92
|
+
& git -C $target checkout -f FETCH_HEAD
|
|
93
|
+
if ($LASTEXITCODE -ne 0) { throw "Could not reach the IJFW repo (exit $LASTEXITCODE). Check your network connection and retry." }
|
|
94
|
+
return "updated"
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
# Broken repo or no origin: backup user data, re-clone, restore.
|
|
99
|
+
$ts = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
|
|
100
|
+
$backupDir = "$target.bak.$ts"
|
|
101
|
+
Rename-Item -LiteralPath $target -NewName $backupDir
|
|
102
|
+
try {
|
|
103
|
+
& git clone --depth 1 --branch $branch $DEFAULT_REPO $target
|
|
104
|
+
if ($LASTEXITCODE -ne 0) { throw "Could not reach the IJFW repo (exit $LASTEXITCODE). Check your network connection and retry." }
|
|
105
|
+
foreach ($item in @('memory', 'sessions', 'install.log', '.session-counter')) {
|
|
106
|
+
$src = Join-Path $backupDir $item
|
|
107
|
+
if (Test-Path $src) {
|
|
108
|
+
$dst = Join-Path $target $item
|
|
109
|
+
if (Test-Path $dst) { Remove-Item -Recurse -Force -LiteralPath $dst }
|
|
110
|
+
Move-Item -LiteralPath $src -Destination $dst
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
Remove-Item -Recurse -Force -LiteralPath $backupDir
|
|
114
|
+
return "updated"
|
|
115
|
+
} catch {
|
|
116
|
+
if (Test-Path $target) { Remove-Item -Recurse -Force -LiteralPath $target }
|
|
117
|
+
Rename-Item -LiteralPath $backupDir -NewName $target
|
|
118
|
+
throw
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function Invoke-InstallScript($target) {
|
|
123
|
+
$script = Join-Path $target "scripts\install.sh"
|
|
124
|
+
if (-not (Test-Path $script)) { throw "The installer script is not at $script yet. Run the full install from a fresh clone." }
|
|
125
|
+
$gitBash = Resolve-GitBash
|
|
126
|
+
if (-not $gitBash) { throw "IJFW needs Git Bash to complete setup. Install Git for Windows (includes bash.exe) and rerun." }
|
|
127
|
+
Push-Location $target
|
|
128
|
+
try {
|
|
129
|
+
$env:IJFW_NONINTERACTIVE = if ($env:CI -or $Yes) { "1" } else { "" }
|
|
130
|
+
# Let the PS wrapper own the final closer so Merge-Marketplace output
|
|
131
|
+
# lands above it. Bash skips its "Full log" line when this is set.
|
|
132
|
+
$env:IJFW_SKIP_CLOSER = "1"
|
|
133
|
+
& $gitBash "./scripts/install.sh"
|
|
134
|
+
if ($LASTEXITCODE -ne 0) { throw "scripts/install.sh exited $LASTEXITCODE." }
|
|
135
|
+
} finally {
|
|
136
|
+
Pop-Location
|
|
137
|
+
Remove-Item Env:\IJFW_SKIP_CLOSER -ErrorAction SilentlyContinue
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function ConvertTo-Hashtable($obj) {
|
|
142
|
+
# PS 5.1 compatibility: ConvertFrom-Json's -AsHashtable is PS 7+ only.
|
|
143
|
+
# Walk the PSCustomObject tree manually into hashtables + arrays.
|
|
144
|
+
if ($null -eq $obj) { return $null }
|
|
145
|
+
if ($obj -is [System.Collections.IDictionary]) {
|
|
146
|
+
$h = @{}
|
|
147
|
+
foreach ($k in $obj.Keys) { $h[$k] = ConvertTo-Hashtable $obj[$k] }
|
|
148
|
+
return $h
|
|
149
|
+
}
|
|
150
|
+
if ($obj -is [System.Management.Automation.PSCustomObject]) {
|
|
151
|
+
$h = @{}
|
|
152
|
+
foreach ($p in $obj.PSObject.Properties) { $h[$p.Name] = ConvertTo-Hashtable $p.Value }
|
|
153
|
+
return $h
|
|
154
|
+
}
|
|
155
|
+
if ($obj -is [System.Collections.IEnumerable] -and -not ($obj -is [string])) {
|
|
156
|
+
$out = @()
|
|
157
|
+
foreach ($item in $obj) { $out += ,(ConvertTo-Hashtable $item) }
|
|
158
|
+
return ,$out
|
|
159
|
+
}
|
|
160
|
+
return $obj
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function ConvertFrom-Jsonc($raw) {
|
|
164
|
+
# State-machine JSONC cleaner: strips // line comments, /* block comments */,
|
|
165
|
+
# and trailing commas before } or ], but only when NOT inside a string.
|
|
166
|
+
# The regex version we shipped earlier mangled files whose string values
|
|
167
|
+
# contained // or /* patterns. This implementation walks the text char by
|
|
168
|
+
# char with a tiny state machine -- no regex false-positives.
|
|
169
|
+
if (-not $raw) { return $raw }
|
|
170
|
+
if ($raw.Length -gt 0 -and [int][char]$raw[0] -eq 0xFEFF) { $raw = $raw.Substring(1) }
|
|
171
|
+
|
|
172
|
+
$sb = New-Object System.Text.StringBuilder
|
|
173
|
+
$len = $raw.Length
|
|
174
|
+
$i = 0
|
|
175
|
+
$inString = $false
|
|
176
|
+
$escape = $false
|
|
177
|
+
|
|
178
|
+
while ($i -lt $len) {
|
|
179
|
+
$ch = $raw[$i]
|
|
180
|
+
if ($inString) {
|
|
181
|
+
[void]$sb.Append($ch)
|
|
182
|
+
if ($escape) { $escape = $false }
|
|
183
|
+
elseif ($ch -eq '\') { $escape = $true }
|
|
184
|
+
elseif ($ch -eq '"') { $inString = $false }
|
|
185
|
+
$i++
|
|
186
|
+
continue
|
|
187
|
+
}
|
|
188
|
+
if ($ch -eq '"') { $inString = $true; [void]$sb.Append($ch); $i++; continue }
|
|
189
|
+
if ($ch -eq '/' -and $i + 1 -lt $len) {
|
|
190
|
+
$next = $raw[$i + 1]
|
|
191
|
+
if ($next -eq '/') {
|
|
192
|
+
while ($i -lt $len -and $raw[$i] -ne "`n") { $i++ }
|
|
193
|
+
continue
|
|
194
|
+
}
|
|
195
|
+
if ($next -eq '*') {
|
|
196
|
+
$i += 2
|
|
197
|
+
while ($i + 1 -lt $len -and -not ($raw[$i] -eq '*' -and $raw[$i + 1] -eq '/')) { $i++ }
|
|
198
|
+
$i += 2
|
|
199
|
+
continue
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
[void]$sb.Append($ch)
|
|
203
|
+
$i++
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
# Strip trailing commas. Safe to do as a second regex pass now that strings
|
|
207
|
+
# and comments are out of the way.
|
|
208
|
+
$intermediate = $sb.ToString()
|
|
209
|
+
return ($intermediate -replace ',(\s*[}\]])','$1')
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function Merge-Marketplace {
|
|
213
|
+
$settingsPath = Join-Path $env:USERPROFILE ".claude\settings.json"
|
|
214
|
+
$settingsDir = Split-Path -Parent $settingsPath
|
|
215
|
+
if (-not (Test-Path $settingsDir)) { New-Item -ItemType Directory -Force -Path $settingsDir | Out-Null }
|
|
216
|
+
|
|
217
|
+
$settings = @{}
|
|
218
|
+
if (Test-Path $settingsPath) {
|
|
219
|
+
$raw = Get-Content -Raw -LiteralPath $settingsPath
|
|
220
|
+
$cleaned = ConvertFrom-Jsonc $raw
|
|
221
|
+
try {
|
|
222
|
+
$parsed = ConvertFrom-Json $cleaned -ErrorAction Stop
|
|
223
|
+
$settings = ConvertTo-Hashtable $parsed
|
|
224
|
+
if ($null -eq $settings) { $settings = @{} }
|
|
225
|
+
} catch {
|
|
226
|
+
# Graceful fallback: back up the unparseable file, surface the manual
|
|
227
|
+
# next step, return without throwing so the rest of the install stands.
|
|
228
|
+
$ts = Get-Date -Format 'yyyyMMdd-HHmmss'
|
|
229
|
+
$backup = "$settingsPath.bak.marketplace.$ts"
|
|
230
|
+
Copy-Item -LiteralPath $settingsPath -Destination $backup -Force
|
|
231
|
+
Write-Host " ==> HEADS UP" -ForegroundColor Yellow -NoNewline
|
|
232
|
+
Write-Host " your Claude settings.json is not valid JSON/JSONC" -ForegroundColor DarkGray
|
|
233
|
+
Write-Host " Backed up to $backup" -ForegroundColor DarkGray
|
|
234
|
+
Write-Host " The two /plugin commands above still complete the install." -ForegroundColor DarkGray
|
|
235
|
+
Write-Host ""
|
|
236
|
+
return $false
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
if (-not $settings.ContainsKey('extraKnownMarketplaces')) { $settings['extraKnownMarketplaces'] = @{} }
|
|
240
|
+
$settings.extraKnownMarketplaces['ijfw'] = @{ source = @{ source = 'github'; repo = 'TheRealSeanDonahoe/ijfw' } }
|
|
241
|
+
if (-not $settings.ContainsKey('enabledPlugins')) { $settings['enabledPlugins'] = @{} }
|
|
242
|
+
# Opportunistically clean up the legacy key written by v1.0.0-1.0.2.
|
|
243
|
+
if ($settings.enabledPlugins.ContainsKey('ijfw-core@ijfw')) {
|
|
244
|
+
$settings.enabledPlugins.Remove('ijfw-core@ijfw')
|
|
245
|
+
}
|
|
246
|
+
$settings.enabledPlugins['ijfw@ijfw'] = $true
|
|
247
|
+
|
|
248
|
+
$tmp = "$settingsPath.tmp"
|
|
249
|
+
$settings | ConvertTo-Json -Depth 20 | Set-Content -LiteralPath $tmp -Encoding UTF8
|
|
250
|
+
Move-Item -Force -LiteralPath $tmp -Destination $settingsPath
|
|
251
|
+
return $true
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
# --- main ---
|
|
255
|
+
$issues = Invoke-Preflight
|
|
256
|
+
if ($issues.Count -gt 0) {
|
|
257
|
+
Write-Host "Preflight:" -ForegroundColor Yellow
|
|
258
|
+
foreach ($i in $issues) { Write-Host " - $i" }
|
|
259
|
+
exit 1
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
$target = Get-Target
|
|
263
|
+
|
|
264
|
+
# scripts/install.sh owns the summary (Live now / Standing by / next step).
|
|
265
|
+
# Keep clone/pull output suppressed so the final banner reads clean.
|
|
266
|
+
$action = Invoke-CloneOrPull $target $Branch | Out-Null
|
|
267
|
+
|
|
268
|
+
Invoke-InstallScript $target
|
|
269
|
+
|
|
270
|
+
if (-not $NoMarketplace) {
|
|
271
|
+
# Best-effort: returns $true on success, prints its own message on fallback.
|
|
272
|
+
[void](Merge-Marketplace)
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
$log = Join-Path $env:USERPROFILE ".ijfw\install.log"
|
|
276
|
+
Write-Host " Full log $log" -ForegroundColor DarkGray
|
|
277
|
+
Write-Host ""
|