@castlekit/castle 0.4.0 → 0.4.1

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 (87) hide show
  1. package/.next/standalone/.next/BUILD_ID +1 -1
  2. package/.next/standalone/.next/build-manifest.json +2 -2
  3. package/.next/standalone/.next/prerender-manifest.json +3 -3
  4. package/.next/standalone/.next/server/app/_global-error.html +2 -2
  5. package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
  6. package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
  7. package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
  8. package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  9. package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
  10. package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  11. package/.next/standalone/.next/server/app/_not-found.html +1 -1
  12. package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
  13. package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
  14. package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  15. package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
  16. package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
  17. package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  18. package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
  19. package/.next/standalone/.next/server/app/api/avatars/[id]/route.js.nft.json +1 -1
  20. package/.next/standalone/.next/server/app/api/openclaw/agents/[id]/avatar/route.js.nft.json +1 -1
  21. package/.next/standalone/.next/server/app/api/openclaw/agents/route.js.nft.json +1 -1
  22. package/.next/standalone/.next/server/app/api/openclaw/agents/status/route.js.nft.json +1 -1
  23. package/.next/standalone/.next/server/app/api/openclaw/chat/attachments/route.js.nft.json +1 -1
  24. package/.next/standalone/.next/server/app/api/openclaw/chat/channels/route.js.nft.json +1 -1
  25. package/.next/standalone/.next/server/app/api/openclaw/chat/route.js.nft.json +1 -1
  26. package/.next/standalone/.next/server/app/api/openclaw/chat/search/route.js.nft.json +1 -1
  27. package/.next/standalone/.next/server/app/api/openclaw/chat/storage/route.js.nft.json +1 -1
  28. package/.next/standalone/.next/server/app/api/openclaw/config/route.js.nft.json +1 -1
  29. package/.next/standalone/.next/server/app/api/openclaw/events/route.js.nft.json +1 -1
  30. package/.next/standalone/.next/server/app/api/openclaw/logs/route.js.nft.json +1 -1
  31. package/.next/standalone/.next/server/app/api/openclaw/ping/route.js.nft.json +1 -1
  32. package/.next/standalone/.next/server/app/api/openclaw/session/context/route.js.nft.json +1 -1
  33. package/.next/standalone/.next/server/app/api/openclaw/session/status/route.js.nft.json +1 -1
  34. package/.next/standalone/.next/server/app/api/openclaw/sessions/route.js.nft.json +1 -1
  35. package/.next/standalone/.next/server/app/api/settings/avatar/route.js.nft.json +1 -1
  36. package/.next/standalone/.next/server/app/api/settings/route.js.nft.json +1 -1
  37. package/.next/standalone/.next/server/app/chat.html +1 -1
  38. package/.next/standalone/.next/server/app/chat.rsc +1 -1
  39. package/.next/standalone/.next/server/app/chat.segments/_full.segment.rsc +1 -1
  40. package/.next/standalone/.next/server/app/chat.segments/_head.segment.rsc +1 -1
  41. package/.next/standalone/.next/server/app/chat.segments/_index.segment.rsc +1 -1
  42. package/.next/standalone/.next/server/app/chat.segments/_tree.segment.rsc +1 -1
  43. package/.next/standalone/.next/server/app/chat.segments/chat/__PAGE__.segment.rsc +1 -1
  44. package/.next/standalone/.next/server/app/chat.segments/chat.segment.rsc +1 -1
  45. package/.next/standalone/.next/server/app/index.html +1 -1
  46. package/.next/standalone/.next/server/app/index.rsc +1 -1
  47. package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +1 -1
  48. package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +1 -1
  49. package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
  50. package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
  51. package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
  52. package/.next/standalone/.next/server/app/settings.html +1 -1
  53. package/.next/standalone/.next/server/app/settings.rsc +1 -1
  54. package/.next/standalone/.next/server/app/settings.segments/_full.segment.rsc +1 -1
  55. package/.next/standalone/.next/server/app/settings.segments/_head.segment.rsc +1 -1
  56. package/.next/standalone/.next/server/app/settings.segments/_index.segment.rsc +1 -1
  57. package/.next/standalone/.next/server/app/settings.segments/_tree.segment.rsc +1 -1
  58. package/.next/standalone/.next/server/app/settings.segments/settings/__PAGE__.segment.rsc +1 -1
  59. package/.next/standalone/.next/server/app/settings.segments/settings.segment.rsc +1 -1
  60. package/.next/standalone/.next/server/app/ui-kit.html +1 -1
  61. package/.next/standalone/.next/server/app/ui-kit.rsc +1 -1
  62. package/.next/standalone/.next/server/app/ui-kit.segments/_full.segment.rsc +1 -1
  63. package/.next/standalone/.next/server/app/ui-kit.segments/_head.segment.rsc +1 -1
  64. package/.next/standalone/.next/server/app/ui-kit.segments/_index.segment.rsc +1 -1
  65. package/.next/standalone/.next/server/app/ui-kit.segments/_tree.segment.rsc +1 -1
  66. package/.next/standalone/.next/server/app/ui-kit.segments/ui-kit/__PAGE__.segment.rsc +1 -1
  67. package/.next/standalone/.next/server/app/ui-kit.segments/ui-kit.segment.rsc +1 -1
  68. package/.next/standalone/.next/server/middleware-manifest.json +5 -5
  69. package/.next/standalone/.next/server/pages/404.html +1 -1
  70. package/.next/standalone/.next/server/pages/500.html +2 -2
  71. package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
  72. package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
  73. package/.next/standalone/CHANGELOG.md +12 -0
  74. package/.next/standalone/install.ps1 +437 -0
  75. package/.next/standalone/install.sh +4 -0
  76. package/.next/standalone/package.json +2 -1
  77. package/.next/standalone/src/cli/onboarding.ts +95 -22
  78. package/install.ps1 +437 -0
  79. package/install.sh +4 -0
  80. package/package.json +2 -1
  81. package/src/cli/onboarding.ts +95 -22
  82. /package/.next/standalone/.next/static/{R01Vc6rRTS_XwAg0Y8ERm → IDGX3MGCjqUE6NQVlug5A}/_buildManifest.js +0 -0
  83. /package/.next/standalone/.next/static/{R01Vc6rRTS_XwAg0Y8ERm → IDGX3MGCjqUE6NQVlug5A}/_clientMiddlewareManifest.json +0 -0
  84. /package/.next/standalone/.next/static/{R01Vc6rRTS_XwAg0Y8ERm → IDGX3MGCjqUE6NQVlug5A}/_ssgManifest.js +0 -0
  85. /package/.next/static/{R01Vc6rRTS_XwAg0Y8ERm → IDGX3MGCjqUE6NQVlug5A}/_buildManifest.js +0 -0
  86. /package/.next/static/{R01Vc6rRTS_XwAg0Y8ERm → IDGX3MGCjqUE6NQVlug5A}/_clientMiddlewareManifest.json +0 -0
  87. /package/.next/static/{R01Vc6rRTS_XwAg0Y8ERm → IDGX3MGCjqUE6NQVlug5A}/_ssgManifest.js +0 -0
@@ -0,0 +1,437 @@
1
+ # Castle Installer for Windows
2
+ # Usage: iwr -useb https://castlekit.com/install.ps1 | iex
3
+ # & ([scriptblock]::Create((iwr -useb https://castlekit.com/install.ps1))) -Version 0.4.0 -NoOnboard -DryRun
4
+
5
+ param(
6
+ [string]$Version = "latest",
7
+ [switch]$NoOnboard,
8
+ [switch]$DryRun,
9
+ [switch]$Verbose,
10
+ [switch]$Help
11
+ )
12
+
13
+ $ErrorActionPreference = "Stop"
14
+
15
+ # ─── Taglines ────────────────────────────────────────────────────────────────
16
+
17
+ $Taglines = @(
18
+ "Your kingdom awaits, sire."
19
+ "The throne room is ready."
20
+ "A fortress for your AI agents."
21
+ "All hail the command center."
22
+ "Knights of the round terminal."
23
+ "Raise the drawbridge, lower the latency."
24
+ "By royal decree, your agents are assembled."
25
+ "The court is now in session."
26
+ "From castle walls to API calls."
27
+ "Forged in code, ruled by you."
28
+ "Every king needs a castle."
29
+ "Where agents serve and dragons compile."
30
+ "The siege of busywork ends here."
31
+ "Hear ye, hear ye — your agents await."
32
+ "A castle built on open source bedrock."
33
+ "One does not simply walk in without a CLI."
34
+ "The moat is deep but the docs are deeper."
35
+ "Fear not the dark mode, for it is default."
36
+ "In the land of AI, the castlekeeper wears a hoodie."
37
+ "Excalibur was a sword. This is better."
38
+ "npm install --save-the-kingdom."
39
+ "The Round Table, but make it a dashboard."
40
+ "Dragons? Handled. Bugs? Working on it."
41
+ "A quest to automate the mundane."
42
+ )
43
+
44
+ $Tagline = $Taglines | Get-Random
45
+
46
+ # ─── Banner ──────────────────────────────────────────────────────────────────
47
+
48
+ function Print-Banner {
49
+ $banner = @(
50
+ ' |>>>'
51
+ ' |'
52
+ ' |>>> _ _|_ _ |>>>'
53
+ ' | |;| |;| |;| |'
54
+ ' _ _|_ _ \. . / _ _|_ _'
55
+ ' |;|_|;|_|;| \:. , / |;|_|;|_|;|'
56
+ ' \.. / ||; . | \. . /'
57
+ ' \. , / ||: . | \: . /'
58
+ ' ||: |_ _ ||_ . _ | _ _||: |'
59
+ ' ||: .|||_|;|_|;|_|;|_|;|_|;||:. |'
60
+ ' ||: ||. . . . ||: .|'
61
+ ' ||: . || . . . . , ||: | \,/'
62
+ ' ||: ||: , _______ . ||: , | /`\\'
63
+ ' ||: || . /+++++++\ . ||: |'
64
+ ' ||: ||. |+++++++| . ||: . |'
65
+ ' __ ||: . ||: , |+++++++|. . _||_ |'
66
+ " ____--``~ '--~~__|. |+++++__|----~ ~``---, ___"
67
+ "-~--~ ~---__|,--~' ~~----_____-~' ``~----~~"
68
+ )
69
+ # Blue-to-purple gradient using ANSI escape codes
70
+ $gradient = @(27, 27, 33, 33, 63, 63, 99, 99, 135, 135, 141, 141, 177, 177, 177, 176, 176, 176)
71
+ Write-Host ""
72
+ for ($i = 0; $i -lt $banner.Length; $i++) {
73
+ $color = $gradient[$i]
74
+ Write-Host "`e[38;5;${color}m$($banner[$i])`e[0m"
75
+ }
76
+ Write-Host ""
77
+ Write-Host " " -NoNewline
78
+ Write-Host "Castle" -ForegroundColor Blue -NoNewline
79
+ Write-Host " — The multi-agent workspace" -ForegroundColor DarkGray
80
+ Write-Host " $Tagline" -ForegroundColor DarkGray
81
+ Write-Host ""
82
+ }
83
+
84
+ # ─── Help ────────────────────────────────────────────────────────────────────
85
+
86
+ function Print-Usage {
87
+ Write-Host "Castle installer (Windows)"
88
+ Write-Host ""
89
+ Write-Host "Usage:"
90
+ Write-Host " iwr -useb https://castlekit.com/install.ps1 | iex"
91
+ Write-Host " & ([scriptblock]::Create((iwr -useb https://castlekit.com/install.ps1))) [options]"
92
+ Write-Host ""
93
+ Write-Host "Options:"
94
+ Write-Host " -Version <version> npm version to install (default: latest)"
95
+ Write-Host " -NoOnboard Skip setup wizard after install"
96
+ Write-Host " -DryRun Print what would happen (no changes)"
97
+ Write-Host " -Verbose Print debug output"
98
+ Write-Host " -Help Show this help"
99
+ Write-Host ""
100
+ Write-Host "Environment variables:"
101
+ Write-Host " CASTLE_VERSION=latest|<semver>"
102
+ Write-Host " CASTLE_NO_ONBOARD=1"
103
+ Write-Host " CASTLE_DRY_RUN=1"
104
+ Write-Host ""
105
+ Write-Host "Examples:"
106
+ Write-Host " iwr -useb https://castlekit.com/install.ps1 | iex"
107
+ Write-Host ' & ([scriptblock]::Create((iwr -useb https://castlekit.com/install.ps1))) -NoOnboard'
108
+ }
109
+
110
+ # ─── Environment variable overrides ─────────────────────────────────────────
111
+
112
+ if (-not $PSBoundParameters.ContainsKey("Version")) {
113
+ if (-not [string]::IsNullOrWhiteSpace($env:CASTLE_VERSION)) {
114
+ $Version = $env:CASTLE_VERSION
115
+ }
116
+ }
117
+ if (-not $PSBoundParameters.ContainsKey("NoOnboard")) {
118
+ if ($env:CASTLE_NO_ONBOARD -eq "1") {
119
+ $NoOnboard = $true
120
+ }
121
+ }
122
+ if (-not $PSBoundParameters.ContainsKey("DryRun")) {
123
+ if ($env:CASTLE_DRY_RUN -eq "1") {
124
+ $DryRun = $true
125
+ }
126
+ }
127
+
128
+ # ─── Helpers ─────────────────────────────────────────────────────────────────
129
+
130
+ function Refresh-Path {
131
+ $env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
132
+ }
133
+
134
+ # ─── Node.js ─────────────────────────────────────────────────────────────────
135
+
136
+ function Check-Node {
137
+ try {
138
+ $nodeVersion = (node -v 2>$null)
139
+ if ($nodeVersion) {
140
+ $major = [int]($nodeVersion -replace 'v(\d+)\..*', '$1')
141
+ if ($major -ge 22) {
142
+ Write-Host "[OK] Node.js $nodeVersion found" -ForegroundColor Green
143
+ return $true
144
+ } else {
145
+ Write-Host "[!] Node.js $nodeVersion found, but v22+ required" -ForegroundColor Yellow
146
+ return $false
147
+ }
148
+ }
149
+ } catch {
150
+ Write-Host "[!] Node.js not found" -ForegroundColor Yellow
151
+ return $false
152
+ }
153
+ return $false
154
+ }
155
+
156
+ function Install-Node {
157
+ Write-Host "[*] Installing Node.js 22..." -ForegroundColor Yellow
158
+
159
+ # Try winget first (Windows 11 / Windows 10 with App Installer)
160
+ if (Get-Command winget -ErrorAction SilentlyContinue) {
161
+ Write-Host " Using winget..." -ForegroundColor Gray
162
+ winget install OpenJS.NodeJS.LTS --accept-package-agreements --accept-source-agreements
163
+ Refresh-Path
164
+ Write-Host "[OK] Node.js installed via winget" -ForegroundColor Green
165
+ return
166
+ }
167
+
168
+ # Try Chocolatey
169
+ if (Get-Command choco -ErrorAction SilentlyContinue) {
170
+ Write-Host " Using Chocolatey..." -ForegroundColor Gray
171
+ choco install nodejs-lts -y
172
+ Refresh-Path
173
+ Write-Host "[OK] Node.js installed via Chocolatey" -ForegroundColor Green
174
+ return
175
+ }
176
+
177
+ # Try Scoop
178
+ if (Get-Command scoop -ErrorAction SilentlyContinue) {
179
+ Write-Host " Using Scoop..." -ForegroundColor Gray
180
+ scoop install nodejs-lts
181
+ Write-Host "[OK] Node.js installed via Scoop" -ForegroundColor Green
182
+ return
183
+ }
184
+
185
+ # No package manager available
186
+ Write-Host ""
187
+ Write-Host "Error: Could not find a package manager (winget, choco, or scoop)" -ForegroundColor Red
188
+ Write-Host ""
189
+ Write-Host "Please install Node.js 22+ manually:" -ForegroundColor Yellow
190
+ Write-Host " https://nodejs.org/en/download/" -ForegroundColor Cyan
191
+ Write-Host ""
192
+ Write-Host "Or install winget (App Installer) from the Microsoft Store." -ForegroundColor Gray
193
+ exit 1
194
+ }
195
+
196
+ # ─── Git ─────────────────────────────────────────────────────────────────────
197
+
198
+ function Check-Git {
199
+ if (Get-Command git -ErrorAction SilentlyContinue) {
200
+ Write-Host "[OK] Git found" -ForegroundColor Green
201
+ return $true
202
+ }
203
+ Write-Host "[!] Git not found (optional — needed for some npm packages)" -ForegroundColor Yellow
204
+ return $false
205
+ }
206
+
207
+ # ─── npm PATH ────────────────────────────────────────────────────────────────
208
+
209
+ function Ensure-NpmGlobalOnPath {
210
+ $npmPrefix = $null
211
+ try {
212
+ $npmPrefix = (npm config get prefix 2>$null).Trim()
213
+ } catch {
214
+ return
215
+ }
216
+
217
+ if ([string]::IsNullOrWhiteSpace($npmPrefix)) { return }
218
+
219
+ # On Windows, npm global bin is directly in the prefix (not prefix/bin)
220
+ $npmBin = $npmPrefix
221
+ $userPath = [Environment]::GetEnvironmentVariable("Path", "User")
222
+ if (-not ($userPath -split ";" | Where-Object { $_ -ieq $npmBin })) {
223
+ [Environment]::SetEnvironmentVariable("Path", "$userPath;$npmBin", "User")
224
+ Refresh-Path
225
+ Write-Host "[!] Added $npmBin to user PATH" -ForegroundColor Yellow
226
+ }
227
+ }
228
+
229
+ function Resolve-CastleBin {
230
+ # Check if castle is on PATH
231
+ if (Get-Command castle -ErrorAction SilentlyContinue) {
232
+ return (Get-Command castle).Source
233
+ }
234
+
235
+ Refresh-Path
236
+
237
+ if (Get-Command castle -ErrorAction SilentlyContinue) {
238
+ return (Get-Command castle).Source
239
+ }
240
+
241
+ # Check common npm global locations
242
+ $npmPrefix = $null
243
+ try { $npmPrefix = (npm config get prefix 2>$null).Trim() } catch {}
244
+ if ($npmPrefix -and (Test-Path (Join-Path $npmPrefix "castle.cmd"))) {
245
+ return (Join-Path $npmPrefix "castle.cmd")
246
+ }
247
+
248
+ # Check AppData roaming npm
249
+ $roamingNpm = Join-Path $env:APPDATA "npm"
250
+ if (Test-Path (Join-Path $roamingNpm "castle.cmd")) {
251
+ return (Join-Path $roamingNpm "castle.cmd")
252
+ }
253
+
254
+ return $null
255
+ }
256
+
257
+ # ─── Existing installation ───────────────────────────────────────────────────
258
+
259
+ function Check-ExistingCastle {
260
+ try {
261
+ $null = Get-Command castle -ErrorAction Stop
262
+ Write-Host "[*] Existing Castle installation detected" -ForegroundColor Yellow
263
+ return $true
264
+ } catch {
265
+ return $false
266
+ }
267
+ }
268
+
269
+ # ─── Install Castle ──────────────────────────────────────────────────────────
270
+
271
+ function Install-Castle {
272
+ $installSpec = "@castlekit/castle@$Version"
273
+
274
+ # Check if already installed with matching version
275
+ $resolvedVersion = $null
276
+ try {
277
+ $resolvedVersion = (npm view $installSpec version 2>$null).Trim()
278
+ } catch {}
279
+
280
+ $installedVersion = $null
281
+ try {
282
+ $npmList = npm list -g @castlekit/castle --depth=0 2>$null
283
+ if ($npmList -match '@castlekit/castle@(\S+)') {
284
+ $installedVersion = $Matches[1]
285
+ }
286
+ } catch {}
287
+
288
+ if ($resolvedVersion -and ($installedVersion -eq $resolvedVersion)) {
289
+ Write-Host "[OK] Castle $resolvedVersion already installed" -ForegroundColor Green
290
+ return
291
+ }
292
+
293
+ if ($resolvedVersion) {
294
+ Write-Host "[*] Installing Castle $resolvedVersion..." -ForegroundColor Yellow
295
+ } else {
296
+ Write-Host "[*] Installing Castle ($Version)..." -ForegroundColor Yellow
297
+ }
298
+
299
+ # Suppress npm noise
300
+ $prevLogLevel = $env:NPM_CONFIG_LOGLEVEL
301
+ $prevUpdateNotifier = $env:NPM_CONFIG_UPDATE_NOTIFIER
302
+ $prevFund = $env:NPM_CONFIG_FUND
303
+ $prevAudit = $env:NPM_CONFIG_AUDIT
304
+ $env:NPM_CONFIG_LOGLEVEL = "error"
305
+ $env:NPM_CONFIG_UPDATE_NOTIFIER = "false"
306
+ $env:NPM_CONFIG_FUND = "false"
307
+ $env:NPM_CONFIG_AUDIT = "false"
308
+ try {
309
+ $npmOutput = npm install -g $installSpec 2>&1
310
+ if ($LASTEXITCODE -ne 0) {
311
+ Write-Host "[!] npm install failed" -ForegroundColor Red
312
+ $npmOutput | ForEach-Object { Write-Host $_ }
313
+ Write-Host ""
314
+ Write-Host "Try: npm install -g --force $installSpec" -ForegroundColor Cyan
315
+ exit 1
316
+ }
317
+ } finally {
318
+ $env:NPM_CONFIG_LOGLEVEL = $prevLogLevel
319
+ $env:NPM_CONFIG_UPDATE_NOTIFIER = $prevUpdateNotifier
320
+ $env:NPM_CONFIG_FUND = $prevFund
321
+ $env:NPM_CONFIG_AUDIT = $prevAudit
322
+ }
323
+
324
+ Write-Host "[OK] Castle installed" -ForegroundColor Green
325
+ }
326
+
327
+ # ─── Main ────────────────────────────────────────────────────────────────────
328
+
329
+ function Main {
330
+ if ($Help) {
331
+ Print-Usage
332
+ return
333
+ }
334
+
335
+ if ($DryRun) {
336
+ Write-Host "[OK] Dry run" -ForegroundColor Green
337
+ Write-Host "[OK] Version: $Version" -ForegroundColor Green
338
+ if ($NoOnboard) {
339
+ Write-Host "[OK] Onboard: skipped" -ForegroundColor Green
340
+ }
341
+ Write-Host "Dry run complete (no changes made)." -ForegroundColor DarkGray
342
+ return
343
+ }
344
+
345
+ # Check PowerShell version
346
+ if ($PSVersionTable.PSVersion.Major -lt 5) {
347
+ Write-Host "Error: PowerShell 5+ required" -ForegroundColor Red
348
+ exit 1
349
+ }
350
+
351
+ Write-Host "[OK] Windows detected" -ForegroundColor Green
352
+
353
+ # Check for existing installation
354
+ $isUpgrade = Check-ExistingCastle
355
+
356
+ # Step 1: Node.js
357
+ if (-not (Check-Node)) {
358
+ Install-Node
359
+
360
+ # Verify after install
361
+ if (-not (Check-Node)) {
362
+ Write-Host ""
363
+ Write-Host "Error: Node.js installation may require a terminal restart" -ForegroundColor Red
364
+ Write-Host "Please close this terminal, open a new one, and run this installer again." -ForegroundColor Yellow
365
+ exit 1
366
+ }
367
+ }
368
+
369
+ # Step 2: Git check (non-blocking)
370
+ Check-Git | Out-Null
371
+
372
+ # Step 3: Ensure npm global bin is on PATH
373
+ Ensure-NpmGlobalOnPath
374
+
375
+ # Step 4: Install Castle
376
+ Install-Castle
377
+
378
+ # Resolve castle binary
379
+ Refresh-Path
380
+ $castleBin = Resolve-CastleBin
381
+
382
+ Write-Host ""
383
+ if ($isUpgrade) {
384
+ $updateMessages = @(
385
+ "The castle walls have been reinforced, my liege."
386
+ "New fortifications in place. The kingdom grows stronger."
387
+ "The royal engineers have been busy. Upgrade complete."
388
+ "Fresh stonework, same castle. Miss me?"
389
+ "The drawbridge has been upgraded. Smoother entry guaranteed."
390
+ )
391
+ Write-Host "Castle upgraded successfully!" -ForegroundColor Green
392
+ Write-Host ($updateMessages | Get-Random) -ForegroundColor DarkGray
393
+ } else {
394
+ $completionMessages = @(
395
+ "The castle has been erected. Long may it stand!"
396
+ "Your fortress is ready, sire. What are your orders?"
397
+ "The court is assembled. Your agents await."
398
+ "A fine castle indeed. Time to rule."
399
+ "Stone by stone, the kingdom begins."
400
+ )
401
+ Write-Host "Castle installed successfully!" -ForegroundColor Green
402
+ Write-Host ($completionMessages | Get-Random) -ForegroundColor DarkGray
403
+ }
404
+ Write-Host ""
405
+
406
+ if (-not $castleBin) {
407
+ Write-Host "[!] Castle is not on PATH yet." -ForegroundColor Yellow
408
+ Write-Host "Restart PowerShell, then run: castle setup" -ForegroundColor Cyan
409
+ $npmPrefix = $null
410
+ try { $npmPrefix = (npm config get prefix 2>$null).Trim() } catch {}
411
+ if ($npmPrefix) {
412
+ Write-Host "Expected path: $npmPrefix" -ForegroundColor DarkGray
413
+ }
414
+ return
415
+ }
416
+
417
+ # Step 5: Run setup
418
+ if ($NoOnboard) {
419
+ Write-Host "Skipping setup (requested). Run " -NoNewline
420
+ Write-Host "castle setup" -ForegroundColor Cyan -NoNewline
421
+ Write-Host " later."
422
+ } elseif (Test-Path (Join-Path $env:USERPROFILE ".castle\castle.json")) {
423
+ Write-Host "[OK] Castle is already configured" -ForegroundColor Green
424
+ Write-Host "Run " -NoNewline -ForegroundColor DarkGray
425
+ Write-Host "castle setup" -ForegroundColor Cyan -NoNewline
426
+ Write-Host " to reconfigure." -ForegroundColor DarkGray
427
+ } else {
428
+ Write-Host "Starting setup..." -ForegroundColor Cyan
429
+ Write-Host ""
430
+ & $castleBin setup
431
+ }
432
+ }
433
+
434
+ # ─── Entry ───────────────────────────────────────────────────────────────────
435
+
436
+ Print-Banner
437
+ Main
@@ -765,7 +765,11 @@ fi
765
765
 
766
766
  if [[ "$OS" == "unknown" ]]; then
767
767
  echo -e "${ERROR}Error: Unsupported operating system${NC}"
768
+ echo ""
768
769
  echo "This installer supports macOS and Linux (including WSL)."
770
+ echo ""
771
+ echo -e "For ${INFO}Windows${NC} (PowerShell):"
772
+ echo -e " ${ACCENT}iwr -useb https://castlekit.com/install.ps1 | iex${NC}"
769
773
  exit 1
770
774
  fi
771
775
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@castlekit/castle",
3
- "version": "0.4.0",
3
+ "version": "0.4.1",
4
4
  "description": "The multi-agent workspace",
5
5
  "type": "module",
6
6
  "bin": {
@@ -14,6 +14,7 @@
14
14
  "public/",
15
15
  "index.js",
16
16
  "install.sh",
17
+ "install.ps1",
17
18
  "next.config.ts",
18
19
  "postcss.config.mjs",
19
20
  "tsconfig.json",
@@ -355,28 +355,31 @@ export async function runOnboarding(): Promise<void> {
355
355
  const installSpinner = p.spinner();
356
356
  installSpinner.start("Installing OpenClaw...");
357
357
 
358
+ const installCmd = process.platform === "win32"
359
+ ? 'powershell -NoProfile -ExecutionPolicy Bypass -Command "& { iwr -useb https://openclaw.ai/install.ps1 | iex } -NoOnboard"'
360
+ : 'curl -fsSL --proto "=https" --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --no-onboard --no-prompt';
361
+
358
362
  try {
359
- execSync(
360
- 'curl -fsSL --proto "=https" --tlsv1.2 https://openclaw.ai/install.sh | bash -s -- --no-onboard --no-prompt',
361
- { stdio: "pipe", timeout: 120000 }
362
- );
363
+ execSync(installCmd, { stdio: "pipe", timeout: 120000 });
363
364
  installSpinner.stop(BLUE("✔ OpenClaw installed"));
364
365
  } catch (error) {
365
366
  installSpinner.stop(pc.red("OpenClaw installation failed"));
367
+ const manualCmd = process.platform === "win32"
368
+ ? "iwr -useb https://openclaw.ai/install.ps1 | iex"
369
+ : "curl -fsSL https://openclaw.ai/install.sh | bash";
366
370
  p.note(
367
- `Install OpenClaw manually:\n${BLUE_LIGHT(
368
- "curl -fsSL https://openclaw.ai/install.sh | bash"
369
- )}\n\nThen run: ${BLUE_LIGHT("castle setup")}`,
371
+ `Install OpenClaw manually:\n${BLUE_LIGHT(manualCmd)}\n\nThen run: ${BLUE_LIGHT("castle setup")}`,
370
372
  BLUE_BOLD("Manual Install")
371
373
  );
372
374
  p.outro("Come back when OpenClaw is installed!");
373
375
  process.exit(1);
374
376
  }
375
377
  } else {
378
+ const manualCmd = process.platform === "win32"
379
+ ? "iwr -useb https://openclaw.ai/install.ps1 | iex"
380
+ : "curl -fsSL https://openclaw.ai/install.sh | bash";
376
381
  p.note(
377
- `Install OpenClaw:\n${BLUE_LIGHT(
378
- "curl -fsSL https://openclaw.ai/install.sh | bash"
379
- )}\n\nThen come back and run:\n${BLUE_LIGHT("castle setup")}`,
382
+ `Install OpenClaw:\n${BLUE_LIGHT(manualCmd)}\n\nThen come back and run:\n${BLUE_LIGHT("castle setup")}`,
380
383
  BLUE_BOLD("Install OpenClaw First")
381
384
  );
382
385
  p.outro("See you soon!");
@@ -619,12 +622,37 @@ export async function runOnboarding(): Promise<void> {
619
622
 
620
623
  // Write PID file helper
621
624
  const pidFile = join(castleDir, "server.pid");
625
+ const isWin = process.platform === "win32";
626
+
627
+ // Stop existing service FIRST — otherwise the service manager respawns
628
+ // the old server immediately after we kill it, stealing the port.
629
+ if (process.platform === "darwin") {
630
+ const plistPath = join(home(), "Library", "LaunchAgents", "com.castlekit.castle.plist");
631
+ try {
632
+ execSyncChild(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: "ignore", timeout: 10000 });
633
+ } catch { /* no existing service */ }
634
+ } else if (process.platform === "linux") {
635
+ try {
636
+ execSyncChild("systemctl --user stop castle.service 2>/dev/null", { stdio: "ignore", timeout: 10000 });
637
+ } catch { /* no existing service */ }
638
+ } else if (isWin) {
639
+ try {
640
+ execSyncChild('schtasks /End /TN "CastleServer" 2>nul', { stdio: "ignore", timeout: 10000 });
641
+ } catch { /* no existing task */ }
642
+ try {
643
+ execSyncChild('schtasks /Delete /TN "CastleServer" /F 2>nul', { stdio: "ignore", timeout: 10000 });
644
+ } catch { /* no existing task */ }
645
+ }
622
646
 
623
647
  // Kill any existing Castle server (by PID file)
624
648
  try {
625
649
  const existingPid = parseInt(readF(pidFile, "utf-8").trim(), 10);
626
650
  if (Number.isInteger(existingPid) && existingPid > 0) {
627
- process.kill(existingPid);
651
+ if (isWin) {
652
+ try { execSyncChild(`taskkill /PID ${existingPid} /F 2>nul`, { stdio: "ignore", timeout: 5000 }); } catch { /* ignore */ }
653
+ } else {
654
+ process.kill(existingPid);
655
+ }
628
656
  for (let i = 0; i < 30; i++) {
629
657
  try {
630
658
  process.kill(existingPid, 0);
@@ -639,14 +667,35 @@ export async function runOnboarding(): Promise<void> {
639
667
  }
640
668
 
641
669
  // Kill anything else on the target port
642
- try {
643
- execSyncChild(`lsof -ti:${castlePort} | xargs kill -9 2>/dev/null`, {
644
- stdio: "ignore",
645
- timeout: 5000,
646
- });
647
- await new Promise((r) => setTimeout(r, 500));
648
- } catch {
649
- // Nothing on port or lsof not available
670
+ if (isWin) {
671
+ try {
672
+ const netstatOut = execSyncChild(
673
+ `netstat -ano | findstr ":${castlePort} " | findstr "LISTENING"`,
674
+ { encoding: "utf-8", timeout: 5000 }
675
+ ).toString();
676
+ const pids = new Set<string>();
677
+ for (const line of netstatOut.split("\n")) {
678
+ const parts = line.trim().split(/\s+/);
679
+ const pid = parts[parts.length - 1];
680
+ if (pid && /^\d+$/.test(pid) && pid !== "0") pids.add(pid);
681
+ }
682
+ for (const pid of pids) {
683
+ try { execSyncChild(`taskkill /PID ${pid} /F 2>nul`, { stdio: "ignore", timeout: 5000 }); } catch { /* ignore */ }
684
+ }
685
+ if (pids.size > 0) await new Promise((r) => setTimeout(r, 500));
686
+ } catch {
687
+ // Nothing on port or netstat not available
688
+ }
689
+ } else {
690
+ try {
691
+ execSyncChild(`lsof -ti:${castlePort} | xargs kill -9 2>/dev/null`, {
692
+ stdio: "ignore",
693
+ timeout: 5000,
694
+ });
695
+ await new Promise((r) => setTimeout(r, 500));
696
+ } catch {
697
+ // Nothing on port or lsof not available
698
+ }
650
699
  }
651
700
 
652
701
  // Escape XML special characters for plist values
@@ -693,9 +742,7 @@ ${plistEnvEntries}
693
742
  </dict>
694
743
  </dict>
695
744
  </plist>`;
696
- try {
697
- execSyncChild(`launchctl unload "${plistPath}" 2>/dev/null`, { stdio: "ignore", timeout: 10000 });
698
- } catch { /* ignore */ }
745
+ // Service was already unloaded above — just write new plist and load
699
746
  writeFile(plistPath, plist);
700
747
  try {
701
748
  execSyncChild(`launchctl load "${plistPath}"`, { stdio: "ignore", timeout: 10000 });
@@ -730,6 +777,32 @@ WantedBy=default.target
730
777
  } catch {
731
778
  // Non-fatal
732
779
  }
780
+ } else if (isWin) {
781
+ // Windows: use Task Scheduler to run Castle at logon
782
+ // Build a batch wrapper that sets environment variables and starts the server
783
+ const batPath = join(castleDir, "start-server.bat");
784
+ const envLines = Object.entries(serverEnv)
785
+ .map(([k, v]) => `set "${k}=${v}"`)
786
+ .join("\r\n");
787
+ const batContent = `@echo off\r\n${envLines}\r\ncd /d "${PROJECT_ROOT}"\r\n"${nodePath}" ${serverArgs.map((a) => `"${a}"`).join(" ")}\r\n`;
788
+ writeFile(batPath, batContent);
789
+
790
+ // Task was already deleted above — create fresh
791
+ try {
792
+ execSyncChild(
793
+ `schtasks /Create /TN "CastleServer" /TR "\\"${batPath}\\"" /SC ONLOGON /RL HIGHEST /F 2>nul`,
794
+ { stdio: "ignore", timeout: 10000 }
795
+ );
796
+ } catch {
797
+ // Non-fatal — fall back to spawning directly
798
+ }
799
+
800
+ // Also start the task now
801
+ try {
802
+ execSyncChild('schtasks /Run /TN "CastleServer" 2>nul', { stdio: "ignore", timeout: 10000 });
803
+ } catch {
804
+ // Non-fatal
805
+ }
733
806
  }
734
807
 
735
808
  // If no service manager started it, spawn directly