mirror of
https://github.com/stablyai/orca.git
synced 2026-09-22 00:02:31 +00:00
Ship the WSL transcript helper with the Windows relay (STA-4831) (#15529)
This commit is contained in:
@@ -10,8 +10,22 @@
|
||||
*/
|
||||
import { build } from 'esbuild'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
||||
import {
|
||||
copyFileSync,
|
||||
existsSync,
|
||||
mkdirSync,
|
||||
readdirSync,
|
||||
readFileSync,
|
||||
rmSync,
|
||||
writeFileSync
|
||||
} from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import {
|
||||
RELAY_BUILD_PLATFORMS,
|
||||
RELAY_VERSION_FILENAME,
|
||||
isWindowsRelayPlatform,
|
||||
relayArtifactFilenames
|
||||
} from '../../src/shared/relay-artifacts.ts'
|
||||
|
||||
const __dirname = import.meta.dirname
|
||||
// Why: the script lives under config/scripts, so go two levels up to reach the repo root.
|
||||
@@ -19,6 +33,13 @@ const ROOT = join(__dirname, '..', '..')
|
||||
const RELAY_ENTRY = join(ROOT, 'src', 'relay', 'relay.ts')
|
||||
const WATCHER_ENTRY = join(ROOT, 'src', 'main', 'ipc', 'parcel-watcher-process-entry.ts')
|
||||
const AI_VAULT_SERVICE_ENTRY = join(ROOT, 'src', 'relay', 'ai-vault-service-entry.ts')
|
||||
const WSL_TRANSCRIPT_FS_PROCESS_ENTRY = join(
|
||||
ROOT,
|
||||
'src',
|
||||
'main',
|
||||
'native-chat',
|
||||
'wsl-transcript-fs-process-entry.ts'
|
||||
)
|
||||
const MANAGED_HOOK_RUNTIME_ENTRY = join(
|
||||
ROOT,
|
||||
'src',
|
||||
@@ -35,19 +56,17 @@ const NODE_PTY_CONSOLE_LIST_PATCH_SOURCE = join(
|
||||
NODE_PTY_CONSOLE_LIST_PATCH_FILENAME
|
||||
)
|
||||
|
||||
const PLATFORMS = [
|
||||
'linux-x64',
|
||||
'linux-arm64',
|
||||
'darwin-x64',
|
||||
'darwin-arm64',
|
||||
'win32-x64',
|
||||
'win32-arm64'
|
||||
]
|
||||
// Why: lets the packaging contract test build into a temp tree instead of
|
||||
// clobbering a developer's out/relay or racing tests that read it.
|
||||
const OUT_ROOT = process.env.ORCA_RELAY_OUT_ROOT ?? join(ROOT, 'out', 'relay')
|
||||
|
||||
const RELAY_VERSION = '0.1.0'
|
||||
|
||||
for (const platform of PLATFORMS) {
|
||||
const outDir = join(ROOT, 'out', 'relay', platform)
|
||||
for (const platform of RELAY_BUILD_PLATFORMS) {
|
||||
const outDir = join(OUT_ROOT, platform)
|
||||
// Why: a stale companion left by an earlier build would otherwise satisfy the
|
||||
// manifest check and be hashed into .version, shipping mixed-generation bytes.
|
||||
rmSync(outDir, { recursive: true, force: true })
|
||||
mkdirSync(outDir, { recursive: true })
|
||||
|
||||
await build({
|
||||
@@ -67,7 +86,7 @@ for (const platform of PLATFORMS) {
|
||||
}
|
||||
})
|
||||
|
||||
if (platform.startsWith('win32-')) {
|
||||
if (isWindowsRelayPlatform(platform)) {
|
||||
copyFileSync(
|
||||
NODE_PTY_CONSOLE_LIST_PATCH_SOURCE,
|
||||
join(outDir, NODE_PTY_CONSOLE_LIST_PATCH_FILENAME)
|
||||
@@ -104,6 +123,23 @@ for (const platform of PLATFORMS) {
|
||||
}
|
||||
})
|
||||
|
||||
// Why beside the service: the spawn resolves this child next to its own
|
||||
// bundle, and a relay host has no desktop out/main to fall back to.
|
||||
await build({
|
||||
entryPoints: [WSL_TRANSCRIPT_FS_PROCESS_ENTRY],
|
||||
bundle: true,
|
||||
platform: 'node',
|
||||
target: 'node18',
|
||||
format: 'cjs',
|
||||
outfile: join(outDir, 'wsl-transcript-fs-process-entry.js'),
|
||||
external: ['electron'],
|
||||
sourcemap: false,
|
||||
minify: true,
|
||||
define: {
|
||||
'process.env.NODE_ENV': '"production"'
|
||||
}
|
||||
})
|
||||
|
||||
await build({
|
||||
entryPoints: [MANAGED_HOOK_RUNTIME_ENTRY],
|
||||
bundle: true,
|
||||
@@ -121,24 +157,34 @@ for (const platform of PLATFORMS) {
|
||||
}
|
||||
})
|
||||
|
||||
// Why: include a content hash so the deploy check detects code changes
|
||||
// even when RELAY_VERSION hasn't been bumped. Hash every executable module
|
||||
// so a companion-only change always deploys beside the matching relay host.
|
||||
const relayContent = readFileSync(join(outDir, 'relay.js'))
|
||||
const watcherContent = readFileSync(join(outDir, 'relay-watcher.js'))
|
||||
const aiVaultServiceContent = readFileSync(join(outDir, 'relay-ai-vault-service.js'))
|
||||
const managedHookRuntimeContent = readFileSync(join(outDir, 'managed-hook-runtime.js'))
|
||||
// Why: include a content hash so the deploy check detects code changes even
|
||||
// when RELAY_VERSION hasn't been bumped. Hashing the whole manifest means a
|
||||
// companion-only change still selects a fresh immutable relay directory.
|
||||
const expected = relayArtifactFilenames(isWindowsRelayPlatform(platform))
|
||||
const hash = createHash('sha256')
|
||||
.update(relayContent)
|
||||
.update(watcherContent)
|
||||
.update(aiVaultServiceContent)
|
||||
.update(managedHookRuntimeContent)
|
||||
// Why: changing the remote node-pty patch must select a fresh immutable Windows relay directory.
|
||||
if (platform.startsWith('win32-')) {
|
||||
hash.update(readFileSync(join(outDir, NODE_PTY_CONSOLE_LIST_PATCH_FILENAME)))
|
||||
for (const filename of expected) {
|
||||
const artifactPath = join(outDir, filename)
|
||||
if (!existsSync(artifactPath)) {
|
||||
throw new Error(
|
||||
`Relay ${platform} declares ${filename} in RELAY_ARTIFACTS but never emitted it. ` +
|
||||
'Add the build step, or drop it from src/shared/relay-artifacts.ts.'
|
||||
)
|
||||
}
|
||||
hash.update(readFileSync(artifactPath))
|
||||
}
|
||||
const contentHash = hash.digest('hex').slice(0, 12)
|
||||
writeFileSync(join(outDir, '.version'), `${RELAY_VERSION}+${contentHash}`)
|
||||
|
||||
// Close the loop: an artifact emitted here but absent from the manifest would
|
||||
// ship unhashed and unprobed — exactly how the WSL helper went missing.
|
||||
const emitted = readdirSync(outDir).filter((name) => name !== RELAY_VERSION_FILENAME)
|
||||
const undeclared = emitted.filter((name) => !expected.includes(name))
|
||||
if (undeclared.length > 0) {
|
||||
throw new Error(
|
||||
`Relay ${platform} emitted undeclared artifacts: ${undeclared.join(', ')}. ` +
|
||||
'Add them to RELAY_ARTIFACTS in src/shared/relay-artifacts.ts.'
|
||||
)
|
||||
}
|
||||
writeFileSync(join(outDir, RELAY_VERSION_FILENAME), `${RELAY_VERSION}+${contentHash}`)
|
||||
|
||||
console.log(`Built relay for ${platform} → ${outDir}/relay.js`)
|
||||
}
|
||||
@@ -149,7 +195,7 @@ for (const platform of PLATFORMS) {
|
||||
// Windows app via the same out/relay extraResources mapping.
|
||||
{
|
||||
const wslEntry = join(ROOT, 'src', 'relay', 'wsl-agent-hook-relay.ts')
|
||||
const outDir = join(ROOT, 'out', 'relay', 'wsl')
|
||||
const outDir = join(OUT_ROOT, 'wsl')
|
||||
mkdirSync(outDir, { recursive: true })
|
||||
await build({
|
||||
entryPoints: [wslEntry],
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createRequire } from 'node:module'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { parse } from 'yaml'
|
||||
import { relayArtifactFilenames } from '../../src/shared/relay-artifacts.ts'
|
||||
|
||||
const projectDir = resolve(import.meta.dirname, '../..')
|
||||
const require = createRequire(import.meta.url)
|
||||
@@ -201,14 +202,15 @@ describe('Electron runtime package contract', () => {
|
||||
|
||||
expect(relayBuild).toContain("'parcel-watcher-process-entry.ts'")
|
||||
expect(relayBuild).toContain("outfile: join(outDir, 'relay-watcher.js')")
|
||||
expect(relayBuild).toContain("readFileSync(join(outDir, 'relay-watcher.js'))")
|
||||
expect(relayBuild).toContain("outfile: join(outDir, 'relay-ai-vault-service.js')")
|
||||
expect(relayBuild).toContain("readFileSync(join(outDir, 'relay-ai-vault-service.js'))")
|
||||
expect(builderConfig).toContain("from: 'out/relay'")
|
||||
expect(remoteCommands).toContain("joinRemotePath(host, remoteRelayDir, 'relay-watcher.js')")
|
||||
expect(remoteCommands).toContain(
|
||||
"joinRemotePath(host, remoteRelayDir, 'relay-ai-vault-service.js')"
|
||||
)
|
||||
|
||||
// Hashing and remote install probing are manifest-driven, so the contract
|
||||
// is that both companions are declared once and that both sites read it.
|
||||
expect(relayArtifactFilenames(true)).toContain('relay-watcher.js')
|
||||
expect(relayArtifactFilenames(true)).toContain('relay-ai-vault-service.js')
|
||||
expect(relayBuild).toContain('relayArtifactFilenames(')
|
||||
expect(remoteCommands).toContain('relayArtifactFilenames(')
|
||||
|
||||
const assertRelayGate = (steps, publishStepName) => {
|
||||
const names = steps.map((step) => step.name)
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
// Packaged-relay contract: what `build:relay` actually writes to disk.
|
||||
//
|
||||
// Asserts against a real build, not the source tree — the unit suites cannot see
|
||||
// this gap, because the WSL transcript dispatcher runs in-process under vitest
|
||||
// and never forks.
|
||||
import { execFileSync } from 'node:child_process'
|
||||
import { createHash } from 'node:crypto'
|
||||
import { existsSync, mkdtempSync, readdirSync, readFileSync, rmSync } from 'node:fs'
|
||||
import { tmpdir } from 'node:os'
|
||||
import { join, resolve } from 'node:path'
|
||||
import { afterAll, beforeAll, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
RELAY_BUILD_PLATFORMS,
|
||||
RELAY_VERSION_FILENAME,
|
||||
isWindowsRelayPlatform,
|
||||
relayArtifactFilenames
|
||||
} from '../../src/shared/relay-artifacts.ts'
|
||||
|
||||
const projectDir = resolve(import.meta.dirname, '../..')
|
||||
// Its own tree: building into out/relay would clobber a developer's build and
|
||||
// race the suites that read it.
|
||||
const relayOutDir = mkdtempSync(join(tmpdir(), 'orca-relay-contract-'))
|
||||
|
||||
beforeAll(() => {
|
||||
execFileSync('node', [join(projectDir, 'config', 'scripts', 'build-relay.mjs')], {
|
||||
cwd: projectDir,
|
||||
stdio: 'pipe',
|
||||
env: { ...process.env, ORCA_RELAY_OUT_ROOT: relayOutDir }
|
||||
})
|
||||
}, 120_000)
|
||||
|
||||
afterAll(() => {
|
||||
rmSync(relayOutDir, { recursive: true, force: true })
|
||||
})
|
||||
|
||||
describe('packaged relay artifact manifest', () => {
|
||||
it.each([...RELAY_BUILD_PLATFORMS])('emits exactly the declared artifacts for %s', (platform) => {
|
||||
const outDir = join(relayOutDir, platform)
|
||||
const expected = relayArtifactFilenames(isWindowsRelayPlatform(platform))
|
||||
|
||||
for (const filename of expected) {
|
||||
expect(existsSync(join(outDir, filename)), `${platform}/${filename} missing`).toBe(true)
|
||||
}
|
||||
// Exactly, not merely at least: an undeclared artifact ships unhashed and
|
||||
// unprobed, which is the same gap in the other direction.
|
||||
const emitted = readdirSync(outDir)
|
||||
.filter((name) => name !== RELAY_VERSION_FILENAME)
|
||||
.sort()
|
||||
expect(emitted).toEqual([...expected].sort())
|
||||
})
|
||||
|
||||
it.each([...RELAY_BUILD_PLATFORMS])('hashes every declared artifact for %s', (platform) => {
|
||||
const outDir = join(relayOutDir, platform)
|
||||
const hash = createHash('sha256')
|
||||
for (const filename of relayArtifactFilenames(isWindowsRelayPlatform(platform))) {
|
||||
hash.update(readFileSync(join(outDir, filename)))
|
||||
}
|
||||
const version = readFileSync(join(outDir, RELAY_VERSION_FILENAME), 'utf8')
|
||||
|
||||
// A companion left out of the hash lets a changed relay reuse an existing
|
||||
// immutable remote directory, serving a mixed-generation install forever.
|
||||
expect(version.split('+')[1]).toBe(hash.digest('hex').slice(0, 12))
|
||||
})
|
||||
|
||||
it('ships the WSL transcript helper beside the service that forks it', () => {
|
||||
for (const platform of RELAY_BUILD_PLATFORMS) {
|
||||
const outDir = join(relayOutDir, platform)
|
||||
const service = readFileSync(join(outDir, 'relay-ai-vault-service.js'), 'utf8')
|
||||
|
||||
// The bundled service reaches the fork, so the entry must sit beside it:
|
||||
// the spawn resolves the child relative to its own bundle directory.
|
||||
expect(service, `${platform} service no longer forks the helper`).toContain(
|
||||
'wsl-transcript-fs-process-entry.js'
|
||||
)
|
||||
expect(
|
||||
existsSync(join(outDir, 'wsl-transcript-fs-process-entry.js')),
|
||||
`${platform} forks a helper it does not ship`
|
||||
).toBe(true)
|
||||
}
|
||||
})
|
||||
})
|
||||
+1
-1
@@ -54,7 +54,7 @@
|
||||
"dev": "pnpm run ensure:electron-runtime && node config/scripts/run-electron-vite-dev.mjs",
|
||||
"dev-stable-name": "pnpm run ensure:electron-runtime && node config/scripts/run-electron-vite-dev.mjs --stable-name",
|
||||
"dev:web": "vite --config vite.web.config.ts --host 127.0.0.1",
|
||||
"build:relay": "node config/scripts/build-relay.mjs",
|
||||
"build:relay": "node --disable-warning=MODULE_TYPELESS_PACKAGE_JSON config/scripts/build-relay.mjs",
|
||||
"build:computer-macos": "node config/scripts/build-computer-macos.mjs",
|
||||
"build:keyboard-layout-macos": "node config/scripts/build-keyboard-layout-macos.mjs",
|
||||
"build:notification-status-macos": "node config/scripts/build-notification-status-macos.mjs",
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
mkdirSync,
|
||||
mkdtempSync,
|
||||
readdirSync,
|
||||
writeFileSync,
|
||||
rmSync,
|
||||
statSync,
|
||||
utimesSync
|
||||
@@ -30,6 +31,10 @@ import {
|
||||
relayLivenessProbeCommand
|
||||
} from './ssh-remote-commands'
|
||||
import { getRemoteHostPlatform } from './ssh-remote-platform'
|
||||
import {
|
||||
RELAY_INSTALL_COMPLETE_FILENAME,
|
||||
relayArtifactFilenames
|
||||
} from '../../shared/relay-artifacts'
|
||||
|
||||
const posix = getRemoteHostPlatform('linux-x64')
|
||||
const windows = getRemoteHostPlatform('win32-x64')
|
||||
@@ -120,6 +125,67 @@ describe('ssh remote command builders', () => {
|
||||
expect(probe).toContain('relay-ai-vault-service.js')
|
||||
})
|
||||
|
||||
it('requires every declared relay artifact before calling an install complete', () => {
|
||||
const posixProbe = probeRelayInstalledCommand(posix, '/home/me/relay')
|
||||
for (const filename of relayArtifactFilenames(false)) {
|
||||
expect(posixProbe, `POSIX probe ignores ${filename}`).toContain(filename)
|
||||
}
|
||||
expect(posixProbe).toContain(RELAY_INSTALL_COMPLETE_FILENAME)
|
||||
// A POSIX relay must not be asked for the Windows-only node-pty patch.
|
||||
expect(posixProbe).not.toContain('node-pty-1.1.0-console-list-agent-patch.cjs')
|
||||
|
||||
const windowsProbe = decodePowerShellCommand(
|
||||
probeRelayInstalledCommand(windows, 'C:/Users/me/relay')
|
||||
)
|
||||
for (const filename of relayArtifactFilenames(true)) {
|
||||
expect(windowsProbe, `Windows probe ignores ${filename}`).toContain(filename)
|
||||
}
|
||||
expect(windowsProbe).toContain(RELAY_INSTALL_COMPLETE_FILENAME)
|
||||
})
|
||||
|
||||
// Stage a complete install, then remove one companion: the probe must flip.
|
||||
function stageRelayInstall(isWindows: boolean): string {
|
||||
const dir = mkdtempSync(join(tmpdir(), 'orca-relay-probe-'))
|
||||
for (const filename of relayArtifactFilenames(isWindows)) {
|
||||
writeFileSync(join(dir, filename), '')
|
||||
}
|
||||
writeFileSync(join(dir, RELAY_INSTALL_COMPLETE_FILENAME), '')
|
||||
return dir
|
||||
}
|
||||
|
||||
it.skipIf(!powerShellExecutable)(
|
||||
'rejects a Windows install missing only the WSL transcript helper',
|
||||
async () => {
|
||||
const dir = stageRelayInstall(true)
|
||||
const probe = (): string => decodePowerShellCommand(probeRelayInstalledCommand(windows, dir))
|
||||
try {
|
||||
expect((await runPowerShellCommand(powerShellExecutable!, probe())).trim()).toBe('OK')
|
||||
|
||||
// The exact shape a pre-STA-4831 relay left behind: everything but this.
|
||||
rmSync(join(dir, 'wsl-transcript-fs-process-entry.js'))
|
||||
expect((await runPowerShellCommand(powerShellExecutable!, probe())).trim()).toBe('MISSING')
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it.skipIf(process.platform === 'win32')(
|
||||
'rejects a POSIX install missing only the WSL transcript helper',
|
||||
async () => {
|
||||
const dir = stageRelayInstall(false)
|
||||
const probe = (): string => probeRelayInstalledCommand(posix, dir)
|
||||
try {
|
||||
expect((await runShellCommand(probe())).trim()).toBe('OK')
|
||||
|
||||
rmSync(join(dir, 'wsl-transcript-fs-process-entry.js'))
|
||||
expect((await runShellCommand(probe())).trim()).toBe('MISSING')
|
||||
} finally {
|
||||
rmSync(dir, { recursive: true, force: true })
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
it('uses encoded PowerShell for Windows deploy commands', () => {
|
||||
expect(readRemoteHomeCommand(windows)).toContain('powershell.exe')
|
||||
expect(makeRemoteDirectoryCommand(windows, 'C:/Users/me/.orca-remote')).toContain(
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
RELAY_INSTALL_COMPLETE_FILENAME,
|
||||
relayArtifactFilenames
|
||||
} from '../../shared/relay-artifacts'
|
||||
import type { RemoteHostPlatform } from './ssh-remote-platform'
|
||||
import { isWindowsRemoteHost, joinRemotePath, remoteDirname } from './ssh-remote-platform'
|
||||
import { powerShellCommand, powerShellLiteral, powerShellNativeArg } from './ssh-remote-powershell'
|
||||
@@ -85,35 +89,31 @@ export function writeRemoteEmptyFileCommand(host: RemoteHostPlatform, remotePath
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* A partial install must read as MISSING, so every file the manifest ships is
|
||||
* probed — not a hand-kept subset. A relay that advertises the AI Vault title
|
||||
* service but lacks the WSL transcript helper would otherwise pass this probe
|
||||
* and then answer WSL title requests with silence.
|
||||
*/
|
||||
export function probeRelayInstalledCommand(
|
||||
host: RemoteHostPlatform,
|
||||
remoteRelayDir: string
|
||||
): string {
|
||||
const relayJs = joinRemotePath(host, remoteRelayDir, 'relay.js')
|
||||
const relayWatcherJs = joinRemotePath(host, remoteRelayDir, 'relay-watcher.js')
|
||||
const relayAiVaultServiceJs = joinRemotePath(host, remoteRelayDir, 'relay-ai-vault-service.js')
|
||||
const managedHookRuntimeJs = joinRemotePath(host, remoteRelayDir, 'managed-hook-runtime.js')
|
||||
const installComplete = joinRemotePath(host, remoteRelayDir, '.install-complete')
|
||||
const required = [
|
||||
...relayArtifactFilenames(isWindowsRemoteHost(host)),
|
||||
RELAY_INSTALL_COMPLETE_FILENAME
|
||||
].map((filename) => joinRemotePath(host, remoteRelayDir, filename))
|
||||
if (!isWindowsRemoteHost(host)) {
|
||||
return (
|
||||
`test -d ${shellEscape(remoteRelayDir)} ` +
|
||||
`&& test -f ${shellEscape(relayJs)} ` +
|
||||
`&& test -f ${shellEscape(relayWatcherJs)} ` +
|
||||
`&& test -f ${shellEscape(relayAiVaultServiceJs)} ` +
|
||||
`&& test -f ${shellEscape(managedHookRuntimeJs)} ` +
|
||||
`&& test -f ${shellEscape(installComplete)} ` +
|
||||
`&& echo OK || echo MISSING`
|
||||
)
|
||||
const fileTests = required.map((path) => `&& test -f ${shellEscape(path)} `).join('')
|
||||
return `test -d ${shellEscape(remoteRelayDir)} ${fileTests}&& echo OK || echo MISSING`
|
||||
}
|
||||
return powerShellCommand(
|
||||
[
|
||||
`$dir = ${powerShellLiteral(remoteRelayDir)}`,
|
||||
`$relay = ${powerShellLiteral(relayJs)}`,
|
||||
`$watcher = ${powerShellLiteral(relayWatcherJs)}`,
|
||||
`$aiVaultService = ${powerShellLiteral(relayAiVaultServiceJs)}`,
|
||||
`$managedHooks = ${powerShellLiteral(managedHookRuntimeJs)}`,
|
||||
`$complete = ${powerShellLiteral(installComplete)}`,
|
||||
"if ((Test-Path -LiteralPath $dir -PathType Container) -and (Test-Path -LiteralPath $relay -PathType Leaf) -and (Test-Path -LiteralPath $watcher -PathType Leaf) -and (Test-Path -LiteralPath $aiVaultService -PathType Leaf) -and (Test-Path -LiteralPath $managedHooks -PathType Leaf) -and (Test-Path -LiteralPath $complete -PathType Leaf)) { 'OK' } else { 'MISSING' }"
|
||||
`$required = @(${required.map((path) => powerShellLiteral(path)).join(', ')})`,
|
||||
'$ok = Test-Path -LiteralPath $dir -PathType Container',
|
||||
'foreach ($f in $required) { if (-not (Test-Path -LiteralPath $f -PathType Leaf)) { $ok = $false } }',
|
||||
"if ($ok) { 'OK' } else { 'MISSING' }"
|
||||
].join('; ')
|
||||
)
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import { deployAndLaunchRelay } from './ssh-relay-deploy'
|
||||
import { SshChannelMultiplexer } from './ssh-channel-multiplexer'
|
||||
import { uploadDirectoryViaSystemSsh } from './ssh-system-fallback'
|
||||
import type { SshTarget } from '../../shared/ssh-types'
|
||||
import { relayArtifactFilenames } from '../../shared/relay-artifacts'
|
||||
|
||||
const RELAY_VERSION = '0.1.0+systemtransport'
|
||||
|
||||
@@ -54,9 +55,13 @@ exec /bin/sh -c "$cmd"
|
||||
}
|
||||
|
||||
function writeFakeRelay(dir: string): void {
|
||||
writeFileSync(join(dir, 'relay-watcher.js'), '')
|
||||
writeFileSync(join(dir, 'relay-ai-vault-service.js'), '')
|
||||
writeFileSync(join(dir, 'managed-hook-runtime.js'), '')
|
||||
// Stage every companion the manifest declares — relay.js gets real content
|
||||
// below — so adding one cannot silently fail the completeness probe here.
|
||||
for (const filename of relayArtifactFilenames(false)) {
|
||||
if (filename !== 'relay.js') {
|
||||
writeFileSync(join(dir, filename), '')
|
||||
}
|
||||
}
|
||||
writeFileSync(
|
||||
join(dir, 'relay.js'),
|
||||
`
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/**
|
||||
* What a packaged relay directory must contain, declared once. The build, the
|
||||
* content hash, and the remote install probe all read this list; they used to
|
||||
* keep three of their own, which is how the WSL helper reached the desktop app
|
||||
* but never a relay.
|
||||
*
|
||||
* Order is load-bearing: the hash concatenates these files in sequence.
|
||||
*
|
||||
* Keep this file erasable-only TypeScript — build-relay.mjs imports it directly
|
||||
* under Node's type stripping, which rejects enums, namespaces, and parameter
|
||||
* properties.
|
||||
*/
|
||||
|
||||
/** Every platform the relay is bundled for; each gets the full artifact set. */
|
||||
export const RELAY_BUILD_PLATFORMS = [
|
||||
'linux-x64',
|
||||
'linux-arm64',
|
||||
'darwin-x64',
|
||||
'darwin-arm64',
|
||||
'win32-x64',
|
||||
'win32-arm64'
|
||||
] as const
|
||||
|
||||
export type RelayBuildPlatform = (typeof RELAY_BUILD_PLATFORMS)[number]
|
||||
|
||||
export function isWindowsRelayPlatform(platform: string): boolean {
|
||||
return platform.startsWith('win32-')
|
||||
}
|
||||
|
||||
export type RelayArtifact = {
|
||||
filename: string
|
||||
/** Only Windows relays ship it; other hosts must neither receive nor probe it. */
|
||||
windowsOnly?: boolean
|
||||
}
|
||||
|
||||
export const RELAY_ARTIFACTS: readonly RelayArtifact[] = [
|
||||
{ filename: 'relay.js' },
|
||||
{ filename: 'relay-watcher.js' },
|
||||
{ filename: 'relay-ai-vault-service.js' },
|
||||
{ filename: 'managed-hook-runtime.js' },
|
||||
// Forked by the AI Vault title reader; without it a relay answers every WSL
|
||||
// title request with no title and no error.
|
||||
{ filename: 'wsl-transcript-fs-process-entry.js' },
|
||||
{ filename: 'node-pty-1.1.0-console-list-agent-patch.cjs', windowsOnly: true }
|
||||
]
|
||||
|
||||
/** Written after the artifacts, so it is never an input to its own hash. */
|
||||
export const RELAY_VERSION_FILENAME = '.version'
|
||||
|
||||
/** Written last by the installer; its absence means a torn install. */
|
||||
export const RELAY_INSTALL_COMPLETE_FILENAME = '.install-complete'
|
||||
|
||||
export function relayArtifactFilenames(isWindows: boolean): string[] {
|
||||
return RELAY_ARTIFACTS.filter((artifact) => !artifact.windowsOnly || isWindows).map(
|
||||
(artifact) => artifact.filename
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user