-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathinstall.ps1
More file actions
326 lines (290 loc) · 15.1 KB
/
Copy pathinstall.ps1
File metadata and controls
326 lines (290 loc) · 15.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
<#
.SYNOPSIS
Install Grad on Windows: a virtual environment, the dependencies, and a
shortcut that opens the workspace without a console window.
.DESCRIPTION
This does not produce a single self-contained .exe, and the reason is worth
stating rather than discovering. `claude-agent-sdk` does not call the API
directly -- it spawns the `claude` CLI and speaks stream-JSON over its
stdio. So the agent's brain is a separate native binary holding its own
subscription auth, and no amount of PyInstaller bundling can absorb it. The
same is true of JupyterLab, which is a Python environment with kernels and
is the point of the app rather than an implementation detail of it.
What is achievable, and what this does, is an install with one visible
entry point: a Start Menu shortcut that launches `pythonw.exe` (no console),
holds the single-instance lock, and lives in the notification area until you
quit it.
.PARAMETER InstallExtras
Extras to install. Defaults to the set the app actually needs at runtime,
which is not the same as the set it needs to start.
This defaulted to "ui,notebook,agent,lab" and produced an install that
launched and then refused the first three things anyone does with it.
`remote` is where `keyring` lives, so every credential path raised -- and
storing a credential is the step immediately after installing. `retrieval`
is `httpx` and `sqlite-vec`, so the paper funnel could not run. `math` is
SymPy, which the system prompt tells the agent to reach for by name.
None of it failed at install time. It failed later, one feature at a time,
on a machine where the developer already had all of it.
.PARAMETER NoShortcut
Skip creating the Start Menu and Desktop shortcuts.
.PARAMETER Python
Python to build the environment with. Defaults to whatever `py -3` or
`python` resolves to.
.PARAMETER Workspace
Folder for your research -- the ledger, notebooks, notes and figures.
Defaults to %USERPROFILE%\Grad, and keeping it out of the installation is
what lets `grad update` be a fast-forward rather than a merge with your own
work. Pass the installation folder itself to keep the old single-folder
layout.
.EXAMPLE
powershell -ExecutionPolicy Bypass -File .\install.ps1
.EXAMPLE
irm https://raw.githubusercontent.com/view321/Grad/main/install.ps1 | iex
# No checkout needed: the script clones the repository itself and then
# runs the clone's own copy of this installer.
#>
[CmdletBinding()]
param(
[string] $InstallExtras = "ui,notebook,agent,lab,retrieval,remote,math",
[switch] $NoShortcut,
[string] $Python = "",
[string] $Workspace = ""
)
$ErrorActionPreference = "Stop"
Set-StrictMode -Version Latest
function Write-Step($message) { Write-Host "==> $message" -ForegroundColor Cyan }
function Write-Warn($message) { Write-Host " ! $message" -ForegroundColor Yellow }
function Write-Ok($message) { Write-Host " + $message" -ForegroundColor Green }
# --------------------------------------------------------------------------
# 0. The repository, when this script arrived without one
# --------------------------------------------------------------------------
# `irm .../install.ps1 | iex` hands PowerShell this text with no file behind
# it: `$MyInvocation.MyCommand` is a script block, it has no `.Path`, and under
# StrictMode even asking for one throws -- so the property is read through
# PSObject, which answers null instead. No path (or a path with no
# `pyproject.toml` beside it -- a copy saved alone into Downloads) means there
# is no repository to install from, and the repository *is* the install: the
# prompts, the skills and the workspace are read from it at runtime. So it is
# cloned, and then the clone's own copy of this script runs -- the one-line
# bootstrap must not drift from the installer it bootstraps.
$RepoUrl = "https://github.com/view321/Grad.git"
$PathProperty = $MyInvocation.MyCommand.PSObject.Properties['Path']
$ScriptPath = if ($PathProperty) { $PathProperty.Value } else { $null }
$Root = if ($ScriptPath) { Split-Path -Parent $ScriptPath } else { $null }
if (-not $Root -or -not (Test-Path (Join-Path $Root "pyproject.toml"))) {
Write-Step "No repository behind this script; cloning Grad"
if (-not (Get-Command git -ErrorAction SilentlyContinue)) {
throw ("git was not found on PATH, and this script needs to clone $RepoUrl. " +
"Install it from https://git-scm.com/download/win and re-run.")
}
# Beside the app's own state, not %USERPROFILE%\Grad -- that is the default
# *workspace*, and the whole point of the split is that an update can
# replace this folder without touching your research.
$CloneDest = Join-Path (Join-Path $env:LOCALAPPDATA "Grad") "app"
if (Test-Path (Join-Path $CloneDest "pyproject.toml")) {
Write-Ok "reusing the existing clone at $CloneDest"
# Through cmd, because git reports progress on stderr and PowerShell 5.1
# turns redirected stderr into a terminating error under
# $ErrorActionPreference = "Stop".
cmd /c "git -C `"$CloneDest`" pull --ff-only >nul 2>&1"
if ($LASTEXITCODE -ne 0) { Write-Warn "could not fast-forward it; installing what is there" }
} else {
New-Item -ItemType Directory -Force -Path (Split-Path -Parent $CloneDest) | Out-Null
git clone $RepoUrl $CloneDest
if ($LASTEXITCODE -ne 0) { throw "git clone $RepoUrl failed." }
Write-Ok "cloned into $CloneDest"
}
# A child process rather than dot-sourcing: `& file.ps1` in this session
# would be refused by a Restricted execution policy, and the pipe-to-iex
# audience is exactly the audience that has one.
$Forward = @("-InstallExtras", $InstallExtras)
if ($NoShortcut) { $Forward += "-NoShortcut" }
if ($Python) { $Forward += @("-Python", $Python) }
if ($Workspace) { $Forward += @("-Workspace", $Workspace) }
& powershell -NoProfile -ExecutionPolicy Bypass -File (Join-Path $CloneDest "install.ps1") @Forward
exit $LASTEXITCODE
}
$VenvDir = Join-Path $Root ".venv"
$AppData = Join-Path $env:LOCALAPPDATA "Grad"
# --------------------------------------------------------------------------
# 1. Python
# --------------------------------------------------------------------------
Write-Step "Locating Python 3.11 or newer"
function Resolve-Python {
param([string] $Explicit)
if ($Explicit) { return $Explicit }
# `py -3` first: the launcher knows about every install, where `python` on
# PATH may be the Microsoft Store stub that only opens the Store page.
if (Get-Command py -ErrorAction SilentlyContinue) {
$probe = & py -3 -c "import sys; print(sys.executable)" 2>$null
if ($LASTEXITCODE -eq 0 -and $probe) { return $probe.Trim() }
}
if (Get-Command python -ErrorAction SilentlyContinue) { return "python" }
return ""
}
$PythonExe = Resolve-Python -Explicit $Python
if (-not $PythonExe) {
throw "No Python found. Install 3.11+ from https://www.python.org/downloads/ and re-run."
}
$Version = & $PythonExe -c "import sys; print('%d.%d' % sys.version_info[:2])"
$Parts = $Version.Split('.')
if ([int]$Parts[0] -lt 3 -or ([int]$Parts[0] -eq 3 -and [int]$Parts[1] -lt 11)) {
throw "Python $Version is too old; Grad needs 3.11 or newer."
}
Write-Ok "Python $Version at $PythonExe"
# --------------------------------------------------------------------------
# 2. The environment
# --------------------------------------------------------------------------
Write-Step "Creating the virtual environment"
if (-not (Test-Path $VenvDir)) {
& $PythonExe -m venv $VenvDir
Write-Ok "created $VenvDir"
} else {
Write-Ok "reusing $VenvDir"
}
$VenvPython = Join-Path $VenvDir "Scripts\python.exe"
$VenvPythonW = Join-Path $VenvDir "Scripts\pythonw.exe"
if (-not (Test-Path $VenvPython)) { throw "The virtual environment has no python.exe: $VenvPython" }
Write-Step "Installing Grad and its dependencies (this takes a few minutes)"
& $VenvPython -m pip install --upgrade pip --quiet
# Editable, because this repository *is* the install: the workspace, the
# prompts and the skills are read from here at runtime.
& $VenvPython -m pip install -e "$($Root)[$($InstallExtras)]"
if ($LASTEXITCODE -ne 0) { throw "pip install failed." }
Write-Ok "installed extras: $InstallExtras"
if (($InstallExtras -split ',') -contains 'ui') {
# Import what the desktop app needs, now, in the venv that will run it.
# `pystray` spent months absent from the `ui` extra and nothing failed at
# install time -- just a tray icon that never appeared, which is the way
# back to a hidden window. A declared-but-broken dependency should fail
# here, where the fix is obvious, not at first launch.
& $VenvPython -c "import pystray, PIL, nicegui, webview"
if ($LASTEXITCODE -ne 0) {
Write-Warn "the ui dependencies did not import cleanly; the desktop app may"
Write-Warn "open without a notification-area icon. Re-run this script after:"
Write-Warn " $VenvPython -m pip install -e `"$Root[ui]`" --force-reinstall"
} else {
Write-Ok "desktop dependencies import (pystray, PIL, nicegui, webview)"
}
}
# --------------------------------------------------------------------------
# 3. The things this installer cannot install
# --------------------------------------------------------------------------
Write-Step "Checking what has to be present but cannot be vendored"
$Claude = Get-Command claude -ErrorAction SilentlyContinue
if ($Claude) {
Write-Ok "claude CLI at $($Claude.Source)"
} else {
Write-Warn 'The "claude" CLI was not found on PATH.'
Write-Warn "The agent spawns it as a subprocess -- without it, the workspace"
Write-Warn "opens but no turn can run. Install it, then re-run this script:"
Write-Warn " npm install -g @anthropic-ai/claude-code"
}
# WebView2 is what pywebview renders into. It ships with Windows 11 and recent
# 10, so this is a check rather than a step -- but a missing runtime shows up as
# a window that opens blank, which is not a self-explaining failure.
$WebView2 = @(
"HKLM:\SOFTWARE\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}",
"HKLM:\SOFTWARE\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}"
) | Where-Object { Test-Path $_ } | Select-Object -First 1
if ($WebView2) {
Write-Ok "WebView2 runtime present"
} else {
Write-Warn "WebView2 runtime not detected. The desktop window needs it:"
Write-Warn " https://developer.microsoft.com/microsoft-edge/webview2/"
Write-Warn "Without it, run the browser fallback: python agent.py --ui (then open the port)."
}
# --------------------------------------------------------------------------
# 3b. Where the research goes
# --------------------------------------------------------------------------
# See install.sh for the reasoning: research committed into the same repository
# as the code puts your notebooks on the same branch as upstream's releases, and
# every update becomes a merge. Keeping them apart costs nothing.
Write-Step "Choosing where your research will live"
$RunsPath = Join-Path $Root "ledger\runs.jsonl"
if (-not $Workspace) {
if ((Test-Path $RunsPath) -and (Get-Item $RunsPath).Length -gt 0) {
# Research is already here. Moving it is `tools.workspace move`, which
# copies and verifies before deleting anything -- not something an
# installer should do quietly.
$Workspace = $Root
Write-Warn "this folder already holds a ledger, so it stays the workspace"
Write-Warn "to separate them later: .venv\Scripts\python -m tools.workspace move $env:USERPROFILE\Grad"
} else {
$Default = Join-Path $env:USERPROFILE "Grad"
$Reply = Read-Host " Workspace folder [$Default]"
$Workspace = if ($Reply) { $Reply } else { $Default }
}
}
if ($Workspace -eq $Root) {
Write-Ok "workspace: $Root (inside the installation)"
} else {
& $VenvPython -m tools.workspace use $Workspace --create | Out-Null
if ($LASTEXITCODE -ne 0) { throw "could not use $Workspace as the workspace" }
Write-Ok "workspace: $Workspace"
}
# What the extras were, so `grad update` reinstalls with the same set rather
# than guessing. Dropping `ui` on an update would take the desktop app off a
# machine whose owner asked only for an update.
& $VenvPython -c @"
import sys
from core import update
update.write_install_record(
extras=[x.strip() for x in '$InstallExtras'.split(',') if x.strip()],
installer='install.ps1',
)
"@ 2>$null | Out-Null
# --------------------------------------------------------------------------
# 4. The shortcut
# --------------------------------------------------------------------------
if (-not $NoShortcut) {
Write-Step "Creating shortcuts"
New-Item -ItemType Directory -Force -Path $AppData | Out-Null
$IconPath = Join-Path $AppData "grad.ico"
# Drawn by the app itself, so the shortcut, the notification area and the
# taskbar button cannot show three different marks. See
# ui/desktop.py:write_icon; the app renders this same path on demand when it
# is missing, which is what makes a dev checkout and a failed icon step here
# recoverable rather than permanent.
& $VenvPython -c "from ui import desktop; desktop.write_icon(r'$IconPath')" 2>$null
if ($LASTEXITCODE -ne 0 -or -not (Test-Path $IconPath)) {
Write-Warn "could not render the icon; the shortcut will use Python's"
$IconPath = $VenvPythonW
}
$Shell = New-Object -ComObject WScript.Shell
$Targets = @(
(Join-Path ([Environment]::GetFolderPath('Programs')) "Grad.lnk"),
(Join-Path ([Environment]::GetFolderPath('Desktop')) "Grad.lnk")
)
foreach ($LinkPath in $Targets) {
$Link = $Shell.CreateShortcut($LinkPath)
# pythonw.exe, not python.exe: the console-subsystem interpreter would
# put a black window behind the app for its whole lifetime. This is the
# same reasoning core/spawn.py applies to every child process.
$Link.TargetPath = $VenvPythonW
$Link.Arguments = "`"$(Join-Path $Root 'agent.py')`" --ui"
$Link.WorkingDirectory = $Root
$Link.IconLocation = $IconPath
$Link.Description = "Grad - a personal research agent for mathematics and machine learning"
$Link.Save()
Write-Ok $LinkPath
}
}
# --------------------------------------------------------------------------
# 5. Where things live
# --------------------------------------------------------------------------
Write-Host ""
Write-Step "Done"
Write-Host " Installation (the code, replaced by updates): $Root"
Write-Host " Workspace (your research, never touched): $Workspace"
Write-Host " App state (layouts, logs, Lab token): $AppData"
Write-Host ""
Write-Host " Start it from the Start Menu, or:"
Write-Host " $VenvPythonW `"$(Join-Path $Root 'agent.py')`" --ui"
Write-Host ""
Write-Host " Update to the newest release:"
Write-Host " $VenvPython `"$(Join-Path $Root 'agent.py')`" --update"
Write-Host ""
Write-Host " It opens on port 8080, or the next free port above it, and stays in"
Write-Host " the notification area when you close the window. Quit from there."
Write-Host ""