Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 42 additions & 41 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"@agent-relay/cloud": "^8.1.2",
"@agent-relay/harness-driver": "^8.1.2",
"@agent-relay/sdk": "^8.1.2",
"@agentworkforce/deploy": "^3.0.42",
"@agentworkforce/deploy": "^3.0.50",
"@relayburn/sdk": "^3.2.0",
"@relayfile/sdk": "^0.8.10",
"@xterm/addon-fit": "^0.10.0",
Expand All @@ -36,7 +36,7 @@
"@xterm/xterm": "^5.5.0",
"@xyflow/react": "^12.4.0",
"agent-relay": "^8.1.2",
"agentworkforce": "^3.0.42",
"agentworkforce": "^3.0.50",
"ai-hist": "^0.2.3",
"allotment": "^1.0.9",
"electron-updater": "^6.3.9",
Expand Down
32 changes: 18 additions & 14 deletions scripts/install-relayfile-mount.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
// Release mode (`--release` or RELAYFILE_MOUNT_INSTALL_SOURCE=release) ignores
// local binaries and installs from the matching GitHub release unless that
// release binary is already present.
import { access, chmod, copyFile, mkdir, readFile, realpath, rename, writeFile } from 'node:fs/promises'
import { access, chmod, copyFile, lstat, mkdir, readFile, realpath, rename, rm, writeFile } from 'node:fs/promises'
import { constants, createWriteStream } from 'node:fs'
import { dirname, join, resolve, sep } from 'node:path'
import { fileURLToPath } from 'node:url'
Expand Down Expand Up @@ -75,36 +75,40 @@ function fail(message) {

async function installFromFile(source, marker = `local:${source}`) {
await mkdir(dirname(target), { recursive: true })
if ((await realpath(source).catch(() => null)) === (await realpath(target).catch(() => null))) {
const targetIsSymlink = await lstat(target).then((stats) => stats.isSymbolicLink()).catch(() => false)
if (!targetIsSymlink && (await realpath(source).catch(() => null)) === (await realpath(target).catch(() => null))) {
await chmod(target, 0o755)
await writeFile(versionMarker, `${marker}\n`)
console.log(`[relayfile-mount] already installed at ${target}`)
return
}
await copyFile(source, target)
await chmod(target, 0o755)
await replaceTargetWithExecutable((tempPath) => copyFile(source, tempPath))
await writeFile(versionMarker, `${marker}\n`)
console.log(`[relayfile-mount] installed ${source} -> ${target}`)
}

async function downloadRelease(version) {
const asset = `relayfile-mount-${archSuffix()}`
const url = `https://github.com/AgentWorkforce/relayfile/releases/download/v${version}/${asset}`
console.log(`[relayfile-mount] downloading ${url}`)
const response = await fetch(url, { redirect: 'follow' })
if (!response.ok || !response.body) {
throw new Error(`download failed: ${response.status} ${response.statusText} for ${url}`)
}
async function replaceTargetWithExecutable(writeTemp) {
await mkdir(dirname(target), { recursive: true })
const tempPath = `${target}.tmp-${process.pid}`
try {
await pipeline(response.body, createWriteStream(tempPath))
await writeTemp(tempPath)
await chmod(tempPath, 0o755)
await rename(tempPath, target)
} catch (error) {
await import('node:fs/promises').then(({ rm }) => rm(tempPath, { force: true }))
await rm(tempPath, { force: true }).catch(() => {})
throw error
}
}

async function downloadRelease(version) {
const asset = `relayfile-mount-${archSuffix()}`
const url = `https://github.com/AgentWorkforce/relayfile/releases/download/v${version}/${asset}`
console.log(`[relayfile-mount] downloading ${url}`)
const response = await fetch(url, { redirect: 'follow' })
if (!response.ok || !response.body) {
throw new Error(`download failed: ${response.status} ${response.statusText} for ${url}`)
}
await replaceTargetWithExecutable((tempPath) => pipeline(response.body, createWriteStream(tempPath)))
await writeFile(versionMarker, `${version}\n`)
console.log(`[relayfile-mount] installed v${version} -> ${target}`)
}
Expand Down
93 changes: 93 additions & 0 deletions src/main/__tests__/relayfile-mount-install-script.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import assert from 'node:assert/strict'
import { execFile } from 'node:child_process'
import { mkdtemp, mkdir, copyFile, lstat, readFile, rm, symlink, writeFile, readdir, stat } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { dirname, join } from 'node:path'
import { promisify } from 'node:util'
import { fileURLToPath } from 'node:url'
import test from 'node:test'

const execFileAsync = promisify(execFile)
const __dirname = dirname(fileURLToPath(import.meta.url))
const repoRoot = join(__dirname, '..', '..', '..')
const installScriptPath = join(repoRoot, 'scripts', 'install-relayfile-mount.mjs')

async function withTempRepo(run: (repoRoot: string) => Promise<void>) {
const tempRoot = await mkdtemp(join(tmpdir(), 'pear-relayfile-install-'))
try {
await mkdir(join(tempRoot, 'scripts'), { recursive: true })
await copyFile(installScriptPath, join(tempRoot, 'scripts', 'install-relayfile-mount.mjs'))
await writeFile(join(tempRoot, 'package.json'), '{"name":"pear-install-test"}\n')
await run(tempRoot)
} finally {
await rm(tempRoot, { recursive: true, force: true })
}
}

async function runInstall(repoRoot: string, source: string) {
await execFileAsync(process.execPath, [join(repoRoot, 'scripts', 'install-relayfile-mount.mjs')], {
cwd: repoRoot,
env: {
...process.env,
RELAYFILE_MOUNT_BIN: source
}
})
}

test('installFromFile replaces the target instead of copying through an existing symlink', {
skip: process.platform === 'win32' ? 'Symlinks on Windows require elevated privileges or Developer Mode' : false
}, async () => {
await withTempRepo(async (repoRoot) => {
const source = join(repoRoot, 'source-relayfile-mount')
const target = join(repoRoot, 'bin', 'relayfile-mount')
const previousTarget = join(repoRoot, 'previous-target')

await writeFile(source, 'new-binary\n')
await mkdir(dirname(target), { recursive: true })
await writeFile(previousTarget, 'old-binary\n')
await symlink(previousTarget, target)
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

await runInstall(repoRoot, source)

assert.equal(await readFile(previousTarget, 'utf8'), 'old-binary\n')
assert.equal(await readFile(target, 'utf8'), 'new-binary\n')
assert.equal((await lstat(target)).isSymbolicLink(), false)
assert.equal((await stat(target)).mode & 0o777, 0o755)
assert.equal(await readFile(join(repoRoot, 'bin', '.relayfile-mount-version'), 'utf8'), `local:${source}\n`)
})
})

test('installFromFile replaces an existing symlink to the requested source', {
skip: process.platform === 'win32' ? 'Symlinks on Windows require elevated privileges or Developer Mode' : false
}, async () => {
await withTempRepo(async (repoRoot) => {
const source = join(repoRoot, 'source-relayfile-mount')
const target = join(repoRoot, 'bin', 'relayfile-mount')

await writeFile(source, 'new-binary\n')
await mkdir(dirname(target), { recursive: true })
await symlink(source, target)

await runInstall(repoRoot, source)

assert.equal(await readFile(target, 'utf8'), 'new-binary\n')
assert.equal((await lstat(target)).isSymbolicLink(), false)
assert.equal((await stat(target)).mode & 0o777, 0o755)
assert.equal(await readFile(join(repoRoot, 'bin', '.relayfile-mount-version'), 'utf8'), `local:${source}\n`)
})
})

test('installFromFile removes the staged temp file when publish fails', async () => {
await withTempRepo(async (repoRoot) => {
const source = join(repoRoot, 'source-relayfile-mount')
const target = join(repoRoot, 'bin', 'relayfile-mount')

await writeFile(source, 'new-binary\n')
await mkdir(target, { recursive: true })

await assert.rejects(runInstall(repoRoot, source), /EISDIR|ENOTDIR|EPERM|EACCES|directory/)

const binEntries = await readdir(join(repoRoot, 'bin'))
assert.deepEqual(binEntries, ['relayfile-mount'])
})
})
2 changes: 1 addition & 1 deletion src/main/broker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -365,7 +365,7 @@ const MAX_BROKER_TIMEOUTS_BEFORE_REVIVE = 2
const BROKER_REVIVE_TERM_GRACE_MS = 1_500
const PERSONA_REGISTRATION_TIMEOUT_MS = 5_000
const PERSONA_REGISTRATION_STABILITY_MS = 1_000
const AGENTWORKFORCE_CLI_VERSION = '3.0.35'
const AGENTWORKFORCE_CLI_VERSION = '3.0.50'

function delay(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms))
Expand Down
Loading