freecode-sh 0.1.0-alpha.15 → 0.1.0-alpha.19
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 +6 -6
- package/check-platform.mjs +20 -0
- package/freecode.mjs +56 -21
- package/install.ps1 +653 -0
- package/install.sh +2 -1
- package/npm-bootstrap-environment.mjs +7 -0
- package/package.json +10 -7
package/README.md
CHANGED
|
@@ -3,10 +3,10 @@
|
|
|
3
3
|
Free Code is a free coding agent for your terminal. Ask it to build, fix,
|
|
4
4
|
explain, and test code in your own projects.
|
|
5
5
|
|
|
6
|
-
This npm package is a small macOS bootstrapper. On first launch it
|
|
7
|
-
matching
|
|
8
|
-
checksum, and then runs the native executable. Application code and prompts
|
|
9
|
-
not shipped in the npm package.
|
|
6
|
+
This npm package is a small macOS and Windows bootstrapper. On first launch it
|
|
7
|
+
installs the matching native Free Code release, verifies its publisher signature
|
|
8
|
+
and checksum, and then runs the native executable. Application code and prompts
|
|
9
|
+
are not shipped in the npm package.
|
|
10
10
|
|
|
11
11
|
Both `freecode` and `freecode-sh` launch the same Free Code CLI.
|
|
12
12
|
|
|
@@ -22,7 +22,7 @@ freecode
|
|
|
22
22
|
```bash
|
|
23
23
|
freecode logout
|
|
24
24
|
npm uninstall -g freecode-sh
|
|
25
|
-
rm -rf ~/.local/share/freecode #
|
|
25
|
+
rm -rf ~/.local/share/freecode # macOS: optionally remove downloaded native versions
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
Free Code
|
|
28
|
+
Free Code supports Apple Silicon and Intel Macs, plus Windows x64.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const requestedPlatform = process.env.FREECODE_INSTALL_OS
|
|
4
|
+
const platform = requestedPlatform === 'Darwin'
|
|
5
|
+
? 'darwin'
|
|
6
|
+
: requestedPlatform === 'Windows_NT'
|
|
7
|
+
? 'win32'
|
|
8
|
+
: process.platform
|
|
9
|
+
const architecture = process.env.FREECODE_INSTALL_ARCH || process.arch
|
|
10
|
+
const supportedHost = (
|
|
11
|
+
(platform === 'darwin' && ['arm64', 'x64'].includes(architecture))
|
|
12
|
+
|| (platform === 'win32' && architecture === 'x64')
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
if (!supportedHost) {
|
|
16
|
+
process.stderr.write(
|
|
17
|
+
`freecode: Free Code's native npm installer supports macOS arm64/x64 and Windows x64 (detected ${platform}/${architecture}).\n`,
|
|
18
|
+
)
|
|
19
|
+
process.exit(1)
|
|
20
|
+
}
|
package/freecode.mjs
CHANGED
|
@@ -6,10 +6,10 @@ import { access, readFile, realpath } from 'node:fs/promises'
|
|
|
6
6
|
import { homedir } from 'node:os'
|
|
7
7
|
import { dirname, isAbsolute, join, relative, resolve, sep } from 'node:path'
|
|
8
8
|
import { fileURLToPath } from 'node:url'
|
|
9
|
+
import { withoutPowerShellModulePath } from './npm-bootstrap-environment.mjs'
|
|
9
10
|
|
|
10
11
|
const packageDirectory = dirname(fileURLToPath(import.meta.url))
|
|
11
12
|
const bootstrapPath = fileURLToPath(import.meta.url)
|
|
12
|
-
const installerPath = join(packageDirectory, 'install.sh')
|
|
13
13
|
const packageMetadata = JSON.parse(await readFile(join(packageDirectory, 'package.json'), 'utf8'))
|
|
14
14
|
|
|
15
15
|
if (packageMetadata.name !== 'freecode-sh' || !/^\d+\.\d+\.\d+-alpha\.\d+$/.test(packageMetadata.version)) {
|
|
@@ -17,49 +17,79 @@ if (packageMetadata.name !== 'freecode-sh' || !/^\d+\.\d+\.\d+-alpha\.\d+$/.test
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const homeDirectory = homedir()
|
|
20
|
-
if (!homeDirectory) fail('
|
|
20
|
+
if (!homeDirectory) fail('A home directory is required to install Free Code.')
|
|
21
21
|
|
|
22
|
-
const
|
|
22
|
+
const requestedPlatform = process.env.FREECODE_INSTALL_OS
|
|
23
|
+
const platform = requestedPlatform === 'Darwin'
|
|
24
|
+
? 'darwin'
|
|
25
|
+
: requestedPlatform === 'Windows_NT'
|
|
26
|
+
? 'win32'
|
|
27
|
+
: process.platform
|
|
28
|
+
const architecture = process.env.FREECODE_INSTALL_ARCH || process.arch
|
|
29
|
+
const supportedHost = (
|
|
30
|
+
(platform === 'darwin' && ['arm64', 'x64'].includes(architecture))
|
|
31
|
+
|| (platform === 'win32' && architecture === 'x64')
|
|
32
|
+
)
|
|
33
|
+
if (!supportedHost) {
|
|
34
|
+
fail(`Free Code's native npm installer supports macOS arm64/x64 and Windows x64 (detected ${platform}/${architecture}).`)
|
|
35
|
+
}
|
|
36
|
+
const windows = platform === 'win32'
|
|
37
|
+
const defaultInstallRoot = windows
|
|
38
|
+
? join(process.env.LOCALAPPDATA || join(homeDirectory, 'AppData', 'Local'), 'Freecode')
|
|
39
|
+
: join(homeDirectory, '.local', 'share', 'freecode')
|
|
40
|
+
const installRoot = resolve(process.env.FREECODE_INSTALL_ROOT || defaultInstallRoot)
|
|
23
41
|
const recordedBinDirectory = await readFirstLine(join(installRoot, 'bin-dir'))
|
|
24
42
|
let binDirectory = resolve(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
43
|
+
windows
|
|
44
|
+
? join(installRoot, 'bin')
|
|
45
|
+
: process.env.FREECODE_BIN_DIR || recordedBinDirectory || join(installRoot, 'npm-bin'),
|
|
28
46
|
)
|
|
29
|
-
let nativeExecutable = join(binDirectory, 'freecode')
|
|
30
|
-
let nativeVersion = await managedNativeVersion(nativeExecutable, installRoot)
|
|
47
|
+
let nativeExecutable = join(binDirectory, windows ? 'freecode.cmd' : 'freecode')
|
|
48
|
+
let nativeVersion = await managedNativeVersion(nativeExecutable, installRoot, platform)
|
|
31
49
|
|
|
32
50
|
if (nativeVersion && compareAlphaVersions(nativeVersion, packageMetadata.version) >= 0) {
|
|
33
|
-
process.exitCode = await
|
|
51
|
+
process.exitCode = await runManaged(nativeExecutable, nativeVersion, process.argv.slice(2))
|
|
34
52
|
} else {
|
|
35
|
-
if (await sameFile(nativeExecutable, bootstrapPath)) {
|
|
53
|
+
if (!windows && await sameFile(nativeExecutable, bootstrapPath)) {
|
|
36
54
|
binDirectory = join(installRoot, 'npm-bin')
|
|
37
55
|
nativeExecutable = join(binDirectory, 'freecode')
|
|
38
56
|
}
|
|
39
57
|
|
|
40
|
-
const platform = process.env.FREECODE_INSTALL_OS === 'Darwin' ? 'darwin' : process.platform
|
|
41
|
-
if (platform !== 'darwin') {
|
|
42
|
-
fail(`Free Code's native npm installer currently supports macOS only (detected ${process.platform}).`)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
58
|
process.stderr.write(`Installing Free Code ${packageMetadata.version}…\n`)
|
|
46
|
-
const
|
|
59
|
+
const installer = windows
|
|
60
|
+
? {
|
|
61
|
+
command: 'powershell.exe',
|
|
62
|
+
args: ['-NoLogo', '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File', join(packageDirectory, 'install.ps1'), '-Version', packageMetadata.version],
|
|
63
|
+
}
|
|
64
|
+
: { command: '/bin/sh', args: [join(packageDirectory, 'install.sh'), '--version', packageMetadata.version] }
|
|
65
|
+
const installerEnvironment = {
|
|
47
66
|
...process.env,
|
|
48
67
|
FREECODE_INSTALL_ROOT: installRoot,
|
|
49
68
|
FREECODE_BIN_DIR: binDirectory,
|
|
50
69
|
FREECODE_INSTALL_EMBEDDED: 'true',
|
|
51
|
-
}
|
|
70
|
+
}
|
|
71
|
+
const installStatus = await run(
|
|
72
|
+
installer.command,
|
|
73
|
+
installer.args,
|
|
74
|
+
windows ? withoutPowerShellModulePath(installerEnvironment) : installerEnvironment,
|
|
75
|
+
)
|
|
52
76
|
if (installStatus !== 0) process.exit(installStatus)
|
|
53
|
-
nativeVersion = await managedNativeVersion(nativeExecutable, installRoot)
|
|
77
|
+
nativeVersion = await managedNativeVersion(nativeExecutable, installRoot, platform)
|
|
54
78
|
if (!nativeVersion || compareAlphaVersions(nativeVersion, packageMetadata.version) < 0) {
|
|
55
79
|
fail('The signed installer completed without creating a managed Free Code executable.')
|
|
56
80
|
}
|
|
57
|
-
process.exitCode = await
|
|
81
|
+
process.exitCode = await runManaged(nativeExecutable, nativeVersion, process.argv.slice(2))
|
|
58
82
|
}
|
|
59
83
|
|
|
60
|
-
async function managedNativeVersion(path, root) {
|
|
84
|
+
async function managedNativeVersion(path, root, currentPlatform) {
|
|
61
85
|
try {
|
|
62
|
-
await access(path, constants.X_OK)
|
|
86
|
+
await access(path, currentPlatform === 'win32' ? constants.F_OK : constants.X_OK)
|
|
87
|
+
if (currentPlatform === 'win32') {
|
|
88
|
+
const version = await readFirstLine(join(root, 'current-version'))
|
|
89
|
+
if (!/^\d+\.\d+\.\d+-alpha\.\d+$/.test(version || '')) return undefined
|
|
90
|
+
await access(join(root, 'versions', version, 'bin', 'freecode.exe'), constants.F_OK)
|
|
91
|
+
return version
|
|
92
|
+
}
|
|
63
93
|
const target = await realpath(path)
|
|
64
94
|
const versionsDirectory = await realpath(resolve(root, 'versions'))
|
|
65
95
|
const relativeTarget = relative(versionsDirectory, target)
|
|
@@ -81,6 +111,11 @@ async function managedNativeVersion(path, root) {
|
|
|
81
111
|
}
|
|
82
112
|
}
|
|
83
113
|
|
|
114
|
+
function runManaged(path, version, args) {
|
|
115
|
+
const command = windows ? join(installRoot, 'versions', version, 'bin', 'freecode.exe') : path
|
|
116
|
+
return run(command, args)
|
|
117
|
+
}
|
|
118
|
+
|
|
84
119
|
function compareAlphaVersions(left, right) {
|
|
85
120
|
const parse = (version) => {
|
|
86
121
|
const match = version.match(/^(\d+)\.(\d+)\.(\d+)-alpha\.(\d+)$/)
|
package/install.ps1
ADDED
|
@@ -0,0 +1,653 @@
|
|
|
1
|
+
#Requires -Version 5.1
|
|
2
|
+
|
|
3
|
+
<#
|
|
4
|
+
.SYNOPSIS
|
|
5
|
+
Installs the Free Code native Windows alpha without administrator access.
|
|
6
|
+
|
|
7
|
+
.EXAMPLE
|
|
8
|
+
irm https://freecode.sh/install.ps1 | iex
|
|
9
|
+
|
|
10
|
+
.EXAMPLE
|
|
11
|
+
.\install.ps1 -Version 0.1.0-alpha.1 -NoModifyPath
|
|
12
|
+
#>
|
|
13
|
+
|
|
14
|
+
[CmdletBinding()]
|
|
15
|
+
param(
|
|
16
|
+
[string]$Version,
|
|
17
|
+
[string]$Channel,
|
|
18
|
+
[string]$BaseUrl,
|
|
19
|
+
[string]$ChannelBaseUrl,
|
|
20
|
+
[string]$ArchivePath,
|
|
21
|
+
[string]$ChecksumPath,
|
|
22
|
+
[string]$ManifestPath,
|
|
23
|
+
[Alias('InstallRoot')]
|
|
24
|
+
[string]$InstallDir,
|
|
25
|
+
[switch]$AllowUnsigned,
|
|
26
|
+
[switch]$NoModifyPath,
|
|
27
|
+
[switch]$Rollback,
|
|
28
|
+
[switch]$Uninstall
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
Set-StrictMode -Version 2.0
|
|
32
|
+
$ErrorActionPreference = 'Stop'
|
|
33
|
+
$ProgressPreference = 'SilentlyContinue'
|
|
34
|
+
|
|
35
|
+
$Product = 'freecode-sh'
|
|
36
|
+
$Target = 'windows-x64'
|
|
37
|
+
$AssetName = 'freecode-windows-x64.zip'
|
|
38
|
+
$LauncherRelativePath = 'bin/freecode.exe'
|
|
39
|
+
$InterfaceRelativePath = 'libexec/freecode/freecode-tui.exe'
|
|
40
|
+
$EngineRelativePath = 'libexec/freecode/freecode-engine.exe'
|
|
41
|
+
$DefaultBaseUrl = 'https://releases.freecode.sh/releases'
|
|
42
|
+
$DefaultChannelBaseUrl = 'https://releases.freecode.sh/channels'
|
|
43
|
+
$ReleaseSigningModulusBase64 = '8so4sWArOV0GXihYvoSoxDLI1yMoILnVfCjYQwe4CKQ9EC10JV30cntMJZWXvXZS6STqQGIjyWisa6Op4nIdOuXKDjaEcQIeftgZBzzyGrMmAMC3W57VAi3d+w/F2XnkXBaH3NOphfftF6Fxt3Q02CxN3PjuuFaOGytKsYF5uDLCvEb+MpzJlD3HZoBzhhMCAoY6wmCdlgaDxD20UFdyEigF5wwpcUWpYmbYMFp+6SLe/Fi8IrYirwiCv5eLur6PzFFcwFrJgUeZAdxmGuM8eJzZlc7mK0+gzi8KpC2FZ9eOx1SafPE4An4+u8rZ7WA1Qk5eCAhNU9lm+JAeeKcCXzhQpPUafybJg75dIbIayOC3tf5mn+X+Vl5+uY19nT2qcJsjgUeIcqbMs9FGXiIA2G292N5WppyiDcx8yAIG6rOrAUGzHMYCrhvxQLAx2KP5QfC53VZ3La7yZOATY5icF78qSSBrKDrT42vinbosbX++Wr34YB6pXhiZF6jHPHrx'
|
|
44
|
+
$ReleaseSigningExponentBase64 = 'AQAB'
|
|
45
|
+
|
|
46
|
+
function Fail([string]$Message) {
|
|
47
|
+
throw "freecode installer: $Message"
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function Get-EnvironmentValue([string]$Name) {
|
|
51
|
+
return [Environment]::GetEnvironmentVariable($Name, 'Process')
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (-not $Version) { $Version = Get-EnvironmentValue 'FREECODE_INSTALL_VERSION' }
|
|
55
|
+
if (-not $Channel) {
|
|
56
|
+
$Channel = Get-EnvironmentValue 'FREECODE_INSTALL_CHANNEL'
|
|
57
|
+
if (-not $Channel) { $Channel = 'alpha' }
|
|
58
|
+
}
|
|
59
|
+
if (-not $BaseUrl) {
|
|
60
|
+
$BaseUrl = Get-EnvironmentValue 'FREECODE_INSTALL_BASE_URL'
|
|
61
|
+
if (-not $BaseUrl) { $BaseUrl = $DefaultBaseUrl }
|
|
62
|
+
}
|
|
63
|
+
if (-not $ChannelBaseUrl) {
|
|
64
|
+
$ChannelBaseUrl = Get-EnvironmentValue 'FREECODE_CHANNEL_BASE_URL'
|
|
65
|
+
if (-not $ChannelBaseUrl) { $ChannelBaseUrl = $DefaultChannelBaseUrl }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
$testReleaseModulus = Get-EnvironmentValue 'FREECODE_TEST_RELEASE_PUBLIC_KEY_MODULUS'
|
|
69
|
+
$testReleaseExponent = Get-EnvironmentValue 'FREECODE_TEST_RELEASE_PUBLIC_KEY_EXPONENT'
|
|
70
|
+
if ($testReleaseModulus -or $testReleaseExponent) {
|
|
71
|
+
$testBaseUri = $null
|
|
72
|
+
if (
|
|
73
|
+
-not $testReleaseModulus -or
|
|
74
|
+
-not $testReleaseExponent -or
|
|
75
|
+
-not [Uri]::TryCreate($BaseUrl, [UriKind]::Absolute, [ref]$testBaseUri) -or
|
|
76
|
+
$testBaseUri.Scheme -ne 'http' -or
|
|
77
|
+
-not $testBaseUri.IsLoopback
|
|
78
|
+
) {
|
|
79
|
+
Fail 'the test release key override is permitted only with a loopback HTTP release URL'
|
|
80
|
+
}
|
|
81
|
+
$ReleaseSigningModulusBase64 = $testReleaseModulus
|
|
82
|
+
$ReleaseSigningExponentBase64 = $testReleaseExponent
|
|
83
|
+
}
|
|
84
|
+
if (-not $InstallDir) {
|
|
85
|
+
$InstallDir = Get-EnvironmentValue 'FREECODE_INSTALL_ROOT'
|
|
86
|
+
if (-not $InstallDir) {
|
|
87
|
+
if (-not $env:LOCALAPPDATA) { Fail 'LOCALAPPDATA is required' }
|
|
88
|
+
$InstallDir = Join-Path $env:LOCALAPPDATA 'Freecode'
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if ((Get-EnvironmentValue 'FREECODE_ALLOW_UNSIGNED_INSTALL') -eq 'true') {
|
|
92
|
+
$AllowUnsigned = $true
|
|
93
|
+
}
|
|
94
|
+
if ((Get-EnvironmentValue 'FREECODE_INSTALL_EMBEDDED') -eq 'true') {
|
|
95
|
+
$NoModifyPath = $true
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
$InstallDir = [IO.Path]::GetFullPath($InstallDir)
|
|
99
|
+
$VersionsDir = Join-Path $InstallDir 'versions'
|
|
100
|
+
$BinDir = Join-Path $InstallDir 'bin'
|
|
101
|
+
$CurrentVersionPath = Join-Path $InstallDir 'current-version'
|
|
102
|
+
$PreviousVersionPath = Join-Path $InstallDir 'previous-version'
|
|
103
|
+
$MarkerPath = Join-Path $InstallDir '.freecode-install.json'
|
|
104
|
+
$ShimPath = Join-Path $BinDir 'freecode.cmd'
|
|
105
|
+
|
|
106
|
+
function Assert-SafeInstallRoot {
|
|
107
|
+
if ($InstallDir.IndexOfAny([char[]]@('"', '%', '!', "`r", "`n", ';')) -ge 0) {
|
|
108
|
+
Fail 'the install directory contains characters that cannot be represented safely in the command shim'
|
|
109
|
+
}
|
|
110
|
+
$comparison = [StringComparison]::OrdinalIgnoreCase
|
|
111
|
+
$driveRoot = [IO.Path]::GetPathRoot($InstallDir)
|
|
112
|
+
$unsafeRoots = @($driveRoot, $env:USERPROFILE, $env:LOCALAPPDATA) | Where-Object { $_ }
|
|
113
|
+
foreach ($unsafe in $unsafeRoots) {
|
|
114
|
+
if ([string]::Equals($InstallDir.TrimEnd('\'), ([IO.Path]::GetFullPath($unsafe)).TrimEnd('\'), $comparison)) {
|
|
115
|
+
Fail "refusing to use a broad install directory: $InstallDir"
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function Assert-Version([string]$Value) {
|
|
121
|
+
if ($Value -notmatch '^\d+\.\d+\.\d+(-[0-9A-Za-z][0-9A-Za-z.-]*)?$') {
|
|
122
|
+
Fail "invalid release version: $Value"
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function Assert-Channel([string]$Value) {
|
|
127
|
+
if ($Value -notmatch '^[0-9A-Za-z][0-9A-Za-z._-]*$') {
|
|
128
|
+
Fail "invalid release channel: $Value"
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
function Receive-File([string]$Uri, [string]$Destination) {
|
|
133
|
+
try {
|
|
134
|
+
Invoke-WebRequest -UseBasicParsing -Uri $Uri -OutFile $Destination
|
|
135
|
+
} catch {
|
|
136
|
+
Fail "could not download $Uri ($($_.Exception.Message))"
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function Read-Json([string]$Path, [string]$Description) {
|
|
141
|
+
try {
|
|
142
|
+
return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json
|
|
143
|
+
} catch {
|
|
144
|
+
Fail "$Description is not valid JSON"
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function Get-Sha256Hex([string]$Path) {
|
|
149
|
+
$stream = [IO.File]::OpenRead($Path)
|
|
150
|
+
$sha256 = [Security.Cryptography.SHA256]::Create()
|
|
151
|
+
try {
|
|
152
|
+
return ([BitConverter]::ToString($sha256.ComputeHash($stream))).Replace('-', '').ToLowerInvariant()
|
|
153
|
+
} finally {
|
|
154
|
+
$sha256.Dispose()
|
|
155
|
+
$stream.Dispose()
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function Enter-InstallLock {
|
|
160
|
+
$normalizedInstallDir = $InstallDir.TrimEnd('\').ToUpperInvariant()
|
|
161
|
+
$sha256 = [Security.Cryptography.SHA256]::Create()
|
|
162
|
+
try {
|
|
163
|
+
$digest = $sha256.ComputeHash([Text.Encoding]::UTF8.GetBytes($normalizedInstallDir))
|
|
164
|
+
} finally {
|
|
165
|
+
$sha256.Dispose()
|
|
166
|
+
}
|
|
167
|
+
$mutexName = "Local\FreeCodeInstall-$(([BitConverter]::ToString($digest)).Replace('-', ''))"
|
|
168
|
+
$mutex = [Threading.Mutex]::new($false, $mutexName)
|
|
169
|
+
$acquired = $false
|
|
170
|
+
try {
|
|
171
|
+
try {
|
|
172
|
+
$acquired = $mutex.WaitOne(0)
|
|
173
|
+
if (-not $acquired) {
|
|
174
|
+
Write-Host 'Waiting for another Free Code setup to finish...'
|
|
175
|
+
$acquired = $mutex.WaitOne([TimeSpan]::FromMinutes(5))
|
|
176
|
+
}
|
|
177
|
+
} catch [Threading.AbandonedMutexException] {
|
|
178
|
+
$acquired = $true
|
|
179
|
+
}
|
|
180
|
+
if (-not $acquired) {
|
|
181
|
+
Fail 'timed out waiting for another Free Code setup to finish'
|
|
182
|
+
}
|
|
183
|
+
return $mutex
|
|
184
|
+
} catch {
|
|
185
|
+
$mutex.Dispose()
|
|
186
|
+
throw
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function Exit-InstallLock([Threading.Mutex]$Mutex) {
|
|
191
|
+
try {
|
|
192
|
+
$Mutex.ReleaseMutex()
|
|
193
|
+
} finally {
|
|
194
|
+
$Mutex.Dispose()
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function Assert-ReleaseManifestSignature([string]$ManifestPath, [string]$SignaturePath, [string]$Description) {
|
|
199
|
+
if (-not (Test-Path -LiteralPath $SignaturePath -PathType Leaf)) {
|
|
200
|
+
Fail "$Description does not have a valid Free Code release signature"
|
|
201
|
+
}
|
|
202
|
+
$rsa = [Security.Cryptography.RSA]::Create()
|
|
203
|
+
try {
|
|
204
|
+
$parameters = New-Object Security.Cryptography.RSAParameters
|
|
205
|
+
$parameters.Modulus = [Convert]::FromBase64String($ReleaseSigningModulusBase64)
|
|
206
|
+
$parameters.Exponent = [Convert]::FromBase64String($ReleaseSigningExponentBase64)
|
|
207
|
+
$rsa.ImportParameters($parameters)
|
|
208
|
+
$valid = $rsa.VerifyData(
|
|
209
|
+
[IO.File]::ReadAllBytes($ManifestPath),
|
|
210
|
+
[IO.File]::ReadAllBytes($SignaturePath),
|
|
211
|
+
[Security.Cryptography.HashAlgorithmName]::SHA256,
|
|
212
|
+
[Security.Cryptography.RSASignaturePadding]::Pkcs1
|
|
213
|
+
)
|
|
214
|
+
} catch {
|
|
215
|
+
$valid = $false
|
|
216
|
+
} finally {
|
|
217
|
+
$rsa.Dispose()
|
|
218
|
+
}
|
|
219
|
+
if (-not $valid) {
|
|
220
|
+
Fail "$Description does not have a valid Free Code release signature"
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
function Get-Asset($Manifest, [string]$Description) {
|
|
225
|
+
if (-not $Manifest.version) { Fail "$Description does not contain a version" }
|
|
226
|
+
if (-not $Manifest.assets) { Fail "$Description does not contain release assets" }
|
|
227
|
+
$property = $Manifest.assets.PSObject.Properties[$Target]
|
|
228
|
+
if (-not $property) { Fail "$Description does not contain a $Target asset" }
|
|
229
|
+
return $property.Value
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function Assert-PayloadContract($Manifest, [string]$Description) {
|
|
233
|
+
$payload = $null
|
|
234
|
+
$payloads = Get-OptionalProperty $Manifest 'payloads'
|
|
235
|
+
if ($payloads) {
|
|
236
|
+
$targetPayload = $payloads.PSObject.Properties[$Target]
|
|
237
|
+
if ($targetPayload) { $payload = $targetPayload.Value }
|
|
238
|
+
}
|
|
239
|
+
if (-not $payload) { $payload = Get-OptionalProperty $Manifest 'payload' }
|
|
240
|
+
if (
|
|
241
|
+
-not $payload -or
|
|
242
|
+
[string](Get-OptionalProperty $payload 'launcher') -ne $LauncherRelativePath -or
|
|
243
|
+
[string](Get-OptionalProperty $payload 'interface') -ne $InterfaceRelativePath -or
|
|
244
|
+
[string](Get-OptionalProperty $payload 'engine') -ne $EngineRelativePath
|
|
245
|
+
) {
|
|
246
|
+
Fail "$Description does not declare the expected $Target payload paths"
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function Get-OptionalProperty($Object, [string]$Name) {
|
|
251
|
+
if (-not $Object) { return $null }
|
|
252
|
+
$property = $Object.PSObject.Properties[$Name]
|
|
253
|
+
if (-not $property) { return $null }
|
|
254
|
+
return $property.Value
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function Test-UnsignedAsset($Security) {
|
|
258
|
+
return (Get-OptionalProperty $Security 'testUnsigned') -eq $true
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function Assert-WindowsAssetSecurity($Asset, [string]$Description) {
|
|
262
|
+
$security = Get-OptionalProperty $Asset 'security'
|
|
263
|
+
if ($security -and (Test-UnsignedAsset $security)) {
|
|
264
|
+
if (-not $AllowUnsigned) {
|
|
265
|
+
Fail "$Description is an unsigned test artifact; rerun with -AllowUnsigned only on a trusted test PC"
|
|
266
|
+
}
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
if (
|
|
270
|
+
-not $security -or
|
|
271
|
+
$security.windowsAuthenticodeSigned -ne $true -or
|
|
272
|
+
$security.timestamped -ne $true -or
|
|
273
|
+
-not $security.publisher
|
|
274
|
+
) {
|
|
275
|
+
Fail "$Description is not marked as Authenticode signed and timestamped"
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function Assert-ZipPaths([string]$Archive, [string]$Destination) {
|
|
280
|
+
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
281
|
+
$root = [IO.Path]::GetFullPath($Destination).TrimEnd('\') + '\'
|
|
282
|
+
$zip = [IO.Compression.ZipFile]::OpenRead($Archive)
|
|
283
|
+
try {
|
|
284
|
+
foreach ($entry in $zip.Entries) {
|
|
285
|
+
$name = $entry.FullName.Replace('\', '/')
|
|
286
|
+
if (
|
|
287
|
+
[string]::IsNullOrWhiteSpace($name) -or
|
|
288
|
+
$name.StartsWith('/') -or
|
|
289
|
+
$name -match '^[A-Za-z]:' -or
|
|
290
|
+
($name.Split('/') -contains '..')
|
|
291
|
+
) {
|
|
292
|
+
Fail "release archive contains an unsafe path: $name"
|
|
293
|
+
}
|
|
294
|
+
$destinationPath = [IO.Path]::GetFullPath((Join-Path $Destination $name))
|
|
295
|
+
if (-not $destinationPath.StartsWith($root, [StringComparison]::OrdinalIgnoreCase)) {
|
|
296
|
+
Fail "release archive escapes the staging directory: $name"
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
} finally {
|
|
300
|
+
$zip.Dispose()
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
function Get-RequiredExecutables([string]$Root) {
|
|
305
|
+
return @(
|
|
306
|
+
(Join-Path $Root $LauncherRelativePath),
|
|
307
|
+
(Join-Path $Root $InterfaceRelativePath),
|
|
308
|
+
(Join-Path $Root $EngineRelativePath)
|
|
309
|
+
)
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function Assert-CompleteInstall([string]$Root) {
|
|
313
|
+
foreach ($executable in Get-RequiredExecutables $Root) {
|
|
314
|
+
if (-not (Test-Path -LiteralPath $executable -PathType Leaf)) {
|
|
315
|
+
Fail "release is missing required executable: $executable"
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function Assert-BundleIntegrity([string]$Root, [string]$CandidateVersion) {
|
|
321
|
+
$bundlePath = Join-Path $Root 'libexec\freecode\bundle-manifest.json'
|
|
322
|
+
$bundle = Read-Json $bundlePath 'bundle manifest'
|
|
323
|
+
if ([string]$bundle.version -ne $CandidateVersion) {
|
|
324
|
+
Fail "bundle manifest version $($bundle.version) does not match $CandidateVersion"
|
|
325
|
+
}
|
|
326
|
+
if (-not $bundle.payload) { Fail 'bundle manifest does not contain payload checksums' }
|
|
327
|
+
$rootPrefix = [IO.Path]::GetFullPath($Root).TrimEnd('\') + '\'
|
|
328
|
+
foreach ($property in $bundle.payload.PSObject.Properties) {
|
|
329
|
+
$name = $property.Name.Replace('\', '/')
|
|
330
|
+
if ($name.StartsWith('/') -or $name -match '^[A-Za-z]:' -or ($name.Split('/') -contains '..')) {
|
|
331
|
+
Fail "bundle manifest contains an unsafe path: $name"
|
|
332
|
+
}
|
|
333
|
+
$path = [IO.Path]::GetFullPath((Join-Path $Root $name))
|
|
334
|
+
if (-not $path.StartsWith($rootPrefix, [StringComparison]::OrdinalIgnoreCase)) {
|
|
335
|
+
Fail "bundle manifest path escapes the install root: $name"
|
|
336
|
+
}
|
|
337
|
+
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { Fail "bundle payload is missing $name" }
|
|
338
|
+
$actual = Get-Sha256Hex $path
|
|
339
|
+
if ($actual -ne [string]$property.Value) { Fail "bundle payload checksum failed for $name" }
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
function Assert-Signatures([string]$Root, $Security) {
|
|
344
|
+
if ((Test-UnsignedAsset $Security) -and $AllowUnsigned) {
|
|
345
|
+
Write-Warning 'Installing an unsigned draft Windows artifact for testing. Do not redistribute this build.'
|
|
346
|
+
return
|
|
347
|
+
}
|
|
348
|
+
foreach ($executable in Get-RequiredExecutables $Root) {
|
|
349
|
+
$signature = Get-AuthenticodeSignature -LiteralPath $executable
|
|
350
|
+
if ([string]$signature.Status -ne 'Valid') {
|
|
351
|
+
Fail "Authenticode verification failed for $executable ($($signature.Status))"
|
|
352
|
+
}
|
|
353
|
+
if (-not $signature.SignerCertificate -or $signature.SignerCertificate.Subject -ne $Security.publisher) {
|
|
354
|
+
$actual = if ($signature.SignerCertificate) { $signature.SignerCertificate.Subject } else { 'missing' }
|
|
355
|
+
Fail "unexpected Authenticode publisher for $executable (expected $($Security.publisher), received $actual)"
|
|
356
|
+
}
|
|
357
|
+
if (-not $signature.TimeStamperCertificate) {
|
|
358
|
+
Fail "trusted timestamp is missing from $executable"
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function Write-TextAtomically([string]$Path, [string]$Text) {
|
|
364
|
+
$temporary = "$Path.$([Guid]::NewGuid().ToString('N')).tmp"
|
|
365
|
+
[IO.File]::WriteAllText($temporary, $Text, [Text.UTF8Encoding]::new($false))
|
|
366
|
+
if (Test-Path -LiteralPath $Path) {
|
|
367
|
+
$backup = "$Path.$([Guid]::NewGuid().ToString('N')).bak"
|
|
368
|
+
try {
|
|
369
|
+
[IO.File]::Replace($temporary, $Path, $backup, $true)
|
|
370
|
+
} finally {
|
|
371
|
+
Remove-Item -LiteralPath $backup -Force -ErrorAction SilentlyContinue
|
|
372
|
+
Remove-Item -LiteralPath $temporary -Force -ErrorAction SilentlyContinue
|
|
373
|
+
}
|
|
374
|
+
} else {
|
|
375
|
+
[IO.File]::Move($temporary, $Path)
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
function Write-CommandShim {
|
|
380
|
+
New-Item -ItemType Directory -Path $BinDir -Force | Out-Null
|
|
381
|
+
if ((Test-Path -LiteralPath $ShimPath) -and (Get-Item -LiteralPath $ShimPath).PSIsContainer) {
|
|
382
|
+
Fail "refusing to replace directory at $ShimPath"
|
|
383
|
+
}
|
|
384
|
+
$shim = @"
|
|
385
|
+
@echo off
|
|
386
|
+
setlocal
|
|
387
|
+
set "FREECODE_VERSION="
|
|
388
|
+
set /p FREECODE_VERSION=<"$CurrentVersionPath"
|
|
389
|
+
if not defined FREECODE_VERSION (
|
|
390
|
+
echo Free Code is not activated. Re-run install.ps1. 1>&2
|
|
391
|
+
exit /b 1
|
|
392
|
+
)
|
|
393
|
+
"$VersionsDir\%FREECODE_VERSION%\bin\freecode.exe" %*
|
|
394
|
+
"@
|
|
395
|
+
Write-TextAtomically $ShimPath ($shim -replace "`n", "`r`n")
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function Set-UserPath([bool]$Add) {
|
|
399
|
+
$userPath = [Environment]::GetEnvironmentVariable('Path', 'User')
|
|
400
|
+
$entries = @($userPath -split ';' | Where-Object { $_ })
|
|
401
|
+
$comparison = [StringComparer]::OrdinalIgnoreCase
|
|
402
|
+
$matches = @($entries | Where-Object { $comparison.Equals($_.TrimEnd('\'), $BinDir.TrimEnd('\')) })
|
|
403
|
+
if ($Add) {
|
|
404
|
+
if ($matches.Count -eq 0) {
|
|
405
|
+
$updated = (@($entries) + $BinDir) -join ';'
|
|
406
|
+
[Environment]::SetEnvironmentVariable('Path', $updated, 'User')
|
|
407
|
+
Write-Host "Added $BinDir to your user PATH."
|
|
408
|
+
}
|
|
409
|
+
if (-not (@($env:Path -split ';') | Where-Object { $comparison.Equals($_.TrimEnd('\'), $BinDir.TrimEnd('\')) })) {
|
|
410
|
+
$env:Path = "$BinDir;$env:Path"
|
|
411
|
+
}
|
|
412
|
+
} elseif ($matches.Count -gt 0) {
|
|
413
|
+
$updated = @($entries | Where-Object { -not $comparison.Equals($_.TrimEnd('\'), $BinDir.TrimEnd('\')) }) -join ';'
|
|
414
|
+
[Environment]::SetEnvironmentVariable('Path', $updated, 'User')
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
function Test-VersionHealth([string]$CandidateVersion) {
|
|
419
|
+
$launcher = Join-Path (Join-Path $VersionsDir $CandidateVersion) $LauncherRelativePath
|
|
420
|
+
$versionOutput = @(& $launcher --version 2>&1)
|
|
421
|
+
$versionExit = $LASTEXITCODE
|
|
422
|
+
$versionOutput | ForEach-Object { Write-Host $_ }
|
|
423
|
+
if ($versionExit -ne 0 -or $versionOutput.Count -eq 0 -or [string]$versionOutput[0] -ne "freecode-sh $CandidateVersion") {
|
|
424
|
+
return $false
|
|
425
|
+
}
|
|
426
|
+
$doctorOutput = @(& $launcher doctor 2>&1)
|
|
427
|
+
$doctorExit = $LASTEXITCODE
|
|
428
|
+
$doctorOutput | ForEach-Object { Write-Host $_ }
|
|
429
|
+
return $doctorExit -eq 0
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function Invoke-Uninstall {
|
|
433
|
+
if (-not (Test-Path -LiteralPath $MarkerPath -PathType Leaf)) {
|
|
434
|
+
Fail "refusing to remove an unrecognized directory without $MarkerPath"
|
|
435
|
+
}
|
|
436
|
+
$marker = Read-Json $MarkerPath 'install marker'
|
|
437
|
+
if ($marker.product -ne $Product -or $marker.installDir -ne $InstallDir) {
|
|
438
|
+
Fail 'install marker does not match this Free Code installation'
|
|
439
|
+
}
|
|
440
|
+
if (-not $NoModifyPath) { Set-UserPath $false }
|
|
441
|
+
Remove-Item -LiteralPath $InstallDir -Recurse -Force
|
|
442
|
+
Write-Host "Free Code was removed from $InstallDir."
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
function Invoke-Rollback {
|
|
446
|
+
if (-not (Test-Path -LiteralPath $PreviousVersionPath -PathType Leaf)) {
|
|
447
|
+
Fail 'there is no previous Free Code version to restore'
|
|
448
|
+
}
|
|
449
|
+
$previous = (Get-Content -LiteralPath $PreviousVersionPath -Raw).Trim()
|
|
450
|
+
Assert-Version $previous
|
|
451
|
+
$previousRoot = Join-Path $VersionsDir $previous
|
|
452
|
+
Assert-CompleteInstall $previousRoot
|
|
453
|
+
Assert-BundleIntegrity $previousRoot $previous
|
|
454
|
+
$metadataPath = Join-Path $previousRoot 'release-metadata.json'
|
|
455
|
+
$metadata = Read-Json $metadataPath 'stored release metadata'
|
|
456
|
+
Assert-WindowsAssetSecurity $metadata.asset 'stored release metadata'
|
|
457
|
+
Assert-Signatures $previousRoot $metadata.asset.security
|
|
458
|
+
$current = if (Test-Path -LiteralPath $CurrentVersionPath) { (Get-Content -LiteralPath $CurrentVersionPath -Raw).Trim() } else { '' }
|
|
459
|
+
Write-TextAtomically $CurrentVersionPath "$previous`n"
|
|
460
|
+
if (-not (Test-VersionHealth $previous)) {
|
|
461
|
+
if ($current) { Write-TextAtomically $CurrentVersionPath "$current`n" }
|
|
462
|
+
Fail 'the previous version failed its health check; activation was restored'
|
|
463
|
+
}
|
|
464
|
+
if ($current) { Write-TextAtomically $PreviousVersionPath "$current`n" }
|
|
465
|
+
Write-CommandShim
|
|
466
|
+
Write-Host "Rolled back Free Code to $previous."
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
Assert-SafeInstallRoot
|
|
470
|
+
Assert-Channel $Channel
|
|
471
|
+
|
|
472
|
+
$localInputs = @(@($ArchivePath, $ChecksumPath, $ManifestPath) | Where-Object { $_ })
|
|
473
|
+
if ($localInputs.Count -ne 0 -and $localInputs.Count -ne 3) {
|
|
474
|
+
Fail '-ArchivePath, -ChecksumPath, and -ManifestPath must be provided together'
|
|
475
|
+
}
|
|
476
|
+
if ($localInputs.Count -eq 3 -and -not $Version) {
|
|
477
|
+
Fail '-Version is required when installing local candidate files'
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
if ($Uninstall -and $Rollback) { Fail '-Uninstall and -Rollback cannot be used together' }
|
|
481
|
+
if ($Uninstall) {
|
|
482
|
+
$operationMutex = Enter-InstallLock
|
|
483
|
+
try {
|
|
484
|
+
Invoke-Uninstall
|
|
485
|
+
} finally {
|
|
486
|
+
Exit-InstallLock $operationMutex
|
|
487
|
+
}
|
|
488
|
+
exit 0
|
|
489
|
+
}
|
|
490
|
+
if ($Rollback) {
|
|
491
|
+
$operationMutex = Enter-InstallLock
|
|
492
|
+
try {
|
|
493
|
+
Invoke-Rollback
|
|
494
|
+
} finally {
|
|
495
|
+
Exit-InstallLock $operationMutex
|
|
496
|
+
}
|
|
497
|
+
exit 0
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
$architecture = if ($env:PROCESSOR_ARCHITEW6432) { $env:PROCESSOR_ARCHITEW6432 } else { $env:PROCESSOR_ARCHITECTURE }
|
|
501
|
+
if ($architecture -ne 'AMD64') {
|
|
502
|
+
Fail "this beta supports only Windows x64 (detected $architecture)"
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
# Windows PowerShell 5.1 may otherwise negotiate an obsolete TLS version.
|
|
506
|
+
if ([Net.ServicePointManager]::SecurityProtocol -band [Net.SecurityProtocolType]::Tls12) {
|
|
507
|
+
[Net.ServicePointManager]::SecurityProtocol = [Net.ServicePointManager]::SecurityProtocol -bor [Net.SecurityProtocolType]::Tls12
|
|
508
|
+
} else {
|
|
509
|
+
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
$BaseUrl = $BaseUrl.TrimEnd('/')
|
|
513
|
+
$ChannelBaseUrl = $ChannelBaseUrl.TrimEnd('/')
|
|
514
|
+
$TemporaryDir = Join-Path ([IO.Path]::GetTempPath()) "freecode-install-$([Guid]::NewGuid().ToString('N'))"
|
|
515
|
+
$StageDir = $null
|
|
516
|
+
$InstallMutex = $null
|
|
517
|
+
New-Item -ItemType Directory -Path $TemporaryDir -Force | Out-Null
|
|
518
|
+
|
|
519
|
+
try {
|
|
520
|
+
if (-not $Version) {
|
|
521
|
+
$channelPath = Join-Path $TemporaryDir 'channel.json'
|
|
522
|
+
$channelSignaturePath = Join-Path $TemporaryDir 'channel.json.sig'
|
|
523
|
+
Write-Host "Resolving Free Code channel $Channel..."
|
|
524
|
+
Receive-File "$ChannelBaseUrl/$Channel.json" $channelPath
|
|
525
|
+
Receive-File "$ChannelBaseUrl/$Channel.json.sig" $channelSignaturePath
|
|
526
|
+
Assert-ReleaseManifestSignature $channelPath $channelSignaturePath "channel $Channel"
|
|
527
|
+
$channelManifest = Read-Json $channelPath "channel $Channel"
|
|
528
|
+
if ($channelManifest.channel -ne $Channel) { Fail "channel manifest identifies $($channelManifest.channel), expected $Channel" }
|
|
529
|
+
Assert-PayloadContract $channelManifest "channel $Channel"
|
|
530
|
+
$channelAsset = Get-Asset $channelManifest "channel $Channel"
|
|
531
|
+
Assert-WindowsAssetSecurity $channelAsset "channel $Channel"
|
|
532
|
+
$Version = [string]$channelManifest.version
|
|
533
|
+
}
|
|
534
|
+
Assert-Version $Version
|
|
535
|
+
|
|
536
|
+
$downloadedArchivePath = Join-Path $TemporaryDir $AssetName
|
|
537
|
+
$downloadedChecksumPath = "$downloadedArchivePath.sha256"
|
|
538
|
+
$downloadedManifestPath = Join-Path $TemporaryDir 'release-manifest.json'
|
|
539
|
+
$downloadedSignaturePath = "$downloadedManifestPath.sig"
|
|
540
|
+
if ($localInputs.Count -eq 3) {
|
|
541
|
+
Write-Host "Loading local Free Code $Version candidate files..."
|
|
542
|
+
Copy-Item -LiteralPath ([IO.Path]::GetFullPath($ArchivePath)) -Destination $downloadedArchivePath
|
|
543
|
+
Copy-Item -LiteralPath ([IO.Path]::GetFullPath($ChecksumPath)) -Destination $downloadedChecksumPath
|
|
544
|
+
Copy-Item -LiteralPath ([IO.Path]::GetFullPath($ManifestPath)) -Destination $downloadedManifestPath
|
|
545
|
+
$localSignaturePath = "$([IO.Path]::GetFullPath($ManifestPath)).sig"
|
|
546
|
+
if (Test-Path -LiteralPath $localSignaturePath -PathType Leaf) {
|
|
547
|
+
Copy-Item -LiteralPath $localSignaturePath -Destination $downloadedSignaturePath
|
|
548
|
+
}
|
|
549
|
+
} else {
|
|
550
|
+
$releaseRoot = "$BaseUrl/v$Version"
|
|
551
|
+
Write-Host "Downloading Free Code $Version for Windows x64..."
|
|
552
|
+
Receive-File "$releaseRoot/$AssetName" $downloadedArchivePath
|
|
553
|
+
Receive-File "$releaseRoot/$AssetName.sha256" $downloadedChecksumPath
|
|
554
|
+
Receive-File "$releaseRoot/release-manifest.json" $downloadedManifestPath
|
|
555
|
+
Receive-File "$releaseRoot/release-manifest.json.sig" $downloadedSignaturePath
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
$manifest = Read-Json $downloadedManifestPath "release $Version"
|
|
559
|
+
$asset = Get-Asset $manifest "release $Version"
|
|
560
|
+
$localUnsignedOverride = $localInputs.Count -eq 3 -and $AllowUnsigned -and (Test-UnsignedAsset $asset.security)
|
|
561
|
+
if (-not $localUnsignedOverride) {
|
|
562
|
+
Assert-ReleaseManifestSignature $downloadedManifestPath $downloadedSignaturePath "release $Version"
|
|
563
|
+
}
|
|
564
|
+
if ([string]$manifest.version -ne $Version) {
|
|
565
|
+
Fail "release manifest version $($manifest.version) does not match requested version $Version"
|
|
566
|
+
}
|
|
567
|
+
if ($manifest.channel -ne $Channel) { Fail "release manifest channel $($manifest.channel) does not match $Channel" }
|
|
568
|
+
Assert-PayloadContract $manifest "release $Version"
|
|
569
|
+
Assert-WindowsAssetSecurity $asset "release $Version"
|
|
570
|
+
if ($asset.archive -ne $AssetName) { Fail "release manifest names unexpected asset $($asset.archive)" }
|
|
571
|
+
|
|
572
|
+
$checksumLine = (Get-Content -LiteralPath $downloadedChecksumPath | Select-Object -First 1)
|
|
573
|
+
if ($checksumLine -notmatch '^([a-fA-F0-9]{64})\s{1,2}(\*?)([^\s]+)$') {
|
|
574
|
+
Fail 'release checksum file is malformed'
|
|
575
|
+
}
|
|
576
|
+
$expectedChecksum = $Matches[1].ToLowerInvariant()
|
|
577
|
+
if ($Matches[3] -ne $AssetName) { Fail "checksum file names unexpected asset $($Matches[3])" }
|
|
578
|
+
if ([string]$asset.sha256 -ne $expectedChecksum) { Fail 'release manifest and checksum file disagree' }
|
|
579
|
+
$actualChecksum = Get-Sha256Hex $downloadedArchivePath
|
|
580
|
+
if ($actualChecksum -ne $expectedChecksum) { Fail "checksum verification failed for $AssetName" }
|
|
581
|
+
|
|
582
|
+
$InstallMutex = Enter-InstallLock
|
|
583
|
+
New-Item -ItemType Directory -Path $VersionsDir -Force | Out-Null
|
|
584
|
+
$versionRoot = Join-Path $VersionsDir $Version
|
|
585
|
+
$newInstall = -not (Test-Path -LiteralPath $versionRoot)
|
|
586
|
+
if ($newInstall) {
|
|
587
|
+
$StageDir = Join-Path $InstallDir ".install-$Version-$([Guid]::NewGuid().ToString('N'))"
|
|
588
|
+
New-Item -ItemType Directory -Path $StageDir -Force | Out-Null
|
|
589
|
+
Assert-ZipPaths $downloadedArchivePath $StageDir
|
|
590
|
+
[IO.Compression.ZipFile]::ExtractToDirectory($downloadedArchivePath, $StageDir)
|
|
591
|
+
Assert-CompleteInstall $StageDir
|
|
592
|
+
Assert-BundleIntegrity $StageDir $Version
|
|
593
|
+
Assert-Signatures $StageDir $asset.security
|
|
594
|
+
@{
|
|
595
|
+
schemaVersion = 1
|
|
596
|
+
product = $Product
|
|
597
|
+
version = $Version
|
|
598
|
+
asset = $asset
|
|
599
|
+
} | ConvertTo-Json -Depth 8 | Set-Content -LiteralPath (Join-Path $StageDir 'release-metadata.json') -Encoding UTF8
|
|
600
|
+
Move-Item -LiteralPath $StageDir -Destination $versionRoot
|
|
601
|
+
$StageDir = $null
|
|
602
|
+
} else {
|
|
603
|
+
Assert-CompleteInstall $versionRoot
|
|
604
|
+
Assert-BundleIntegrity $versionRoot $Version
|
|
605
|
+
$storedMetadata = Read-Json (Join-Path $versionRoot 'release-metadata.json') 'stored release metadata'
|
|
606
|
+
if (
|
|
607
|
+
[string]$storedMetadata.version -ne $Version -or
|
|
608
|
+
[string]$storedMetadata.asset.sha256 -ne [string]$asset.sha256 -or
|
|
609
|
+
[string](Get-OptionalProperty $storedMetadata.asset.security 'publisher') -ne [string](Get-OptionalProperty $asset.security 'publisher') -or
|
|
610
|
+
[bool](Test-UnsignedAsset $storedMetadata.asset.security) -ne [bool](Test-UnsignedAsset $asset.security)
|
|
611
|
+
) {
|
|
612
|
+
Fail "existing version metadata does not match the verified release manifest for $Version"
|
|
613
|
+
}
|
|
614
|
+
Assert-Signatures $versionRoot $asset.security
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
New-Item -ItemType Directory -Path $BinDir -Force | Out-Null
|
|
618
|
+
@{
|
|
619
|
+
schemaVersion = 1
|
|
620
|
+
product = $Product
|
|
621
|
+
installDir = $InstallDir
|
|
622
|
+
binDir = $BinDir
|
|
623
|
+
} | ConvertTo-Json | Set-Content -LiteralPath $MarkerPath -Encoding UTF8
|
|
624
|
+
|
|
625
|
+
$previous = if (Test-Path -LiteralPath $CurrentVersionPath) { (Get-Content -LiteralPath $CurrentVersionPath -Raw).Trim() } else { '' }
|
|
626
|
+
if ($previous -and $previous -ne $Version) { Write-TextAtomically $PreviousVersionPath "$previous`n" }
|
|
627
|
+
Write-TextAtomically $CurrentVersionPath "$Version`n"
|
|
628
|
+
Write-CommandShim
|
|
629
|
+
|
|
630
|
+
if (-not (Test-VersionHealth $Version)) {
|
|
631
|
+
if ($previous) {
|
|
632
|
+
Write-TextAtomically $CurrentVersionPath "$previous`n"
|
|
633
|
+
} else {
|
|
634
|
+
Remove-Item -LiteralPath $CurrentVersionPath -Force -ErrorAction SilentlyContinue
|
|
635
|
+
}
|
|
636
|
+
if ($newInstall) { Remove-Item -LiteralPath $versionRoot -Recurse -Force -ErrorAction SilentlyContinue }
|
|
637
|
+
Fail 'Free Code health checks failed; restored the previous activation'
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
if (-not $NoModifyPath) { Set-UserPath $true }
|
|
641
|
+
Write-Host ''
|
|
642
|
+
Write-Host "Free Code $Version installed successfully."
|
|
643
|
+
Write-Host "Command: $ShimPath"
|
|
644
|
+
Write-Host 'Open a new terminal, then run: freecode doctor'
|
|
645
|
+
} finally {
|
|
646
|
+
if ($StageDir -and (Test-Path -LiteralPath $StageDir)) {
|
|
647
|
+
Remove-Item -LiteralPath $StageDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
648
|
+
}
|
|
649
|
+
if ($InstallMutex) {
|
|
650
|
+
Exit-InstallLock $InstallMutex
|
|
651
|
+
}
|
|
652
|
+
Remove-Item -LiteralPath $TemporaryDir -Recurse -Force -ErrorAction SilentlyContinue
|
|
653
|
+
}
|
package/install.sh
CHANGED
|
@@ -447,7 +447,8 @@ manifest_asset_value() {
|
|
|
447
447
|
target=$2
|
|
448
448
|
field=$3
|
|
449
449
|
awk -v target="$target" -v field="$field" '
|
|
450
|
-
|
|
450
|
+
/^[[:space:]]*"assets"[[:space:]]*:[[:space:]]*{/ { in_assets = 1; next }
|
|
451
|
+
in_assets && index($0, "\"" target "\"") && index($0, ":") { in_target = 1; next }
|
|
451
452
|
in_target && index($0, "\"" field "\"") {
|
|
452
453
|
value = $0
|
|
453
454
|
sub(/^[^:]*:[[:space:]]*/, "", value)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "freecode-sh",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.19",
|
|
4
4
|
"description": "A free coding agent for your terminal.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"author": "Free Code",
|
|
@@ -25,19 +25,22 @@
|
|
|
25
25
|
"freecode-sh": "./freecode.mjs"
|
|
26
26
|
},
|
|
27
27
|
"files": [
|
|
28
|
+
"check-platform.mjs",
|
|
28
29
|
"freecode.mjs",
|
|
29
|
-
"
|
|
30
|
+
"npm-bootstrap-environment.mjs",
|
|
31
|
+
"install.sh",
|
|
32
|
+
"install.ps1"
|
|
30
33
|
],
|
|
31
34
|
"engines": {
|
|
32
35
|
"node": ">=20.9.0"
|
|
33
36
|
},
|
|
34
37
|
"os": [
|
|
35
|
-
"darwin"
|
|
36
|
-
|
|
37
|
-
"cpu": [
|
|
38
|
-
"arm64",
|
|
39
|
-
"x64"
|
|
38
|
+
"darwin",
|
|
39
|
+
"win32"
|
|
40
40
|
],
|
|
41
|
+
"scripts": {
|
|
42
|
+
"preinstall": "node check-platform.mjs"
|
|
43
|
+
},
|
|
41
44
|
"publishConfig": {
|
|
42
45
|
"access": "public",
|
|
43
46
|
"tag": "latest"
|