Skip to content
Draft
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
9 changes: 9 additions & 0 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,15 @@ function resolveManagedRuntimeEnv(opts: {
}

const toolchainPaths: string[] = []
const managedSearchToolsDir = existingDir(join(
opts.appHome,
'vendor',
'tools',
platformArch,
'bin',
))
if (managedSearchToolsDir) toolchainPaths.push(managedSearchToolsDir)

if (process.platform === 'win32') {
const gitDir = existingDir(join(opts.appHome, 'vendor', 'git', platformArch))
if (gitDir) {
Expand Down
51 changes: 44 additions & 7 deletions scripts/assert-desktop-package.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ export function assertDesktopPackage(options = {}) {

const platform = options.platform ?? platformFromAppRoot(appRoot)
const arch = options.arch ?? process.arch
const platformArch = platform === 'win32' ? `win32-${arch}` : null
const platformArch = `${platform}-${arch}`
const requiredFiles = [...BASE_REQUIRED_FILES, ...platformRequiredFiles(platform, platformArch)]
const missing = requiredFiles.filter((file) => !existsSync(join(appRoot, file)))
if (missing.length > 0) {
Expand All @@ -78,6 +78,29 @@ export function assertDesktopPackage(options = {}) {
errors.push(`[desktop-package] unexpected manifest.pi.cli: ${JSON.stringify(manifest?.pi?.cli)}`)
}

if ((platform === 'win32' || platform === 'darwin') && platformArch) {
const searchTools = manifest?.searchTools?.[platformArch]
if (!searchTools) {
errors.push(`[desktop-package] expected manifest.searchTools.${platformArch} for managed fd/rg`)
} else {
if (searchTools.path !== `vendor/tools/${platformArch}`) {
errors.push(
`[desktop-package] unexpected manifest.searchTools.${platformArch}.path: ${JSON.stringify(searchTools.path)}`,
)
}
if (normalizeManifestPath(searchTools.fd?.binary) !== `bin/fd${platform === 'win32' ? '.exe' : ''}`) {
errors.push(
`[desktop-package] unexpected manifest.searchTools.${platformArch}.fd.binary: ${JSON.stringify(searchTools.fd?.binary)}`,
)
}
if (normalizeManifestPath(searchTools.rg?.binary) !== `bin/rg${platform === 'win32' ? '.exe' : ''}`) {
errors.push(
`[desktop-package] unexpected manifest.searchTools.${platformArch}.rg.binary: ${JSON.stringify(searchTools.rg?.binary)}`,
)
}
}
}

if (platform === 'win32' && platformArch) {
const git = manifest?.git?.[platformArch]
if (!git) {
Expand Down Expand Up @@ -110,12 +133,20 @@ export function platformFromAppRoot(appRoot) {
}

function platformRequiredFiles(platform, platformArch) {
if (platform !== 'win32' || !platformArch) return []
return [
`vendor/git/${platformArch}/cmd/git.exe`,
`vendor/git/${platformArch}/bin/bash.exe`,
`vendor/git/${platformArch}/bin/sh.exe`,
]
if (!platformArch) return []
const searchTools = platform === 'win32'
? [`vendor/tools/${platformArch}/bin/fd.exe`, `vendor/tools/${platformArch}/bin/rg.exe`]
: platform === 'darwin'
? [`vendor/tools/${platformArch}/bin/fd`, `vendor/tools/${platformArch}/bin/rg`]
: []
const git = platform === 'win32'
? [
`vendor/git/${platformArch}/cmd/git.exe`,
`vendor/git/${platformArch}/bin/bash.exe`,
`vendor/git/${platformArch}/bin/sh.exe`,
]
: []
return [...searchTools, ...git]
}

function normalizeManifestPath(value) {
Expand All @@ -130,6 +161,12 @@ function main() {
}
console.log(`[desktop-package] app resources OK: ${relative(repoRoot, result.appRoot)}`)
console.log(`[desktop-package] managed Pi: ${result.manifest.pi.version} (${result.manifest.pi.mode})`)
if (result.platform === 'win32' || result.platform === 'darwin') {
const tools = result.manifest.searchTools[result.platformArch]
console.log(
`[desktop-package] managed fd/rg: ${tools.fd.version}/${tools.rg.version} (${result.platformArch})`,
)
}
if (result.platform === 'win32') {
const git = result.manifest.git[result.platformArch]
console.log(`[desktop-package] managed Git Bash: ${git.version} (${result.platformArch})`)
Expand Down
22 changes: 20 additions & 2 deletions scripts/assert-desktop-package.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,12 +32,27 @@ function piManifest() {
}
}

function searchToolsManifest(platformArch: string, windows = false) {
return {
searchTools: {
[platformArch]: {
path: `vendor/tools/${platformArch}`,
binPath: 'bin',
fd: { version: '10.4.2', binary: `bin/fd${windows ? '.exe' : ''}` },
rg: { version: '15.1.0', binary: `bin/rg${windows ? '.exe' : ''}` },
},
},
}
}

describe('assertDesktopPackage', () => {
it('does not require vendor Git in macOS packages', () => {
it('requires managed search tools but not vendor Git in macOS packages', () => {
const root = mkdtempSync(join(tmpdir(), 'openalice-package-mac-'))
try {
const appRoot = join(root, 'mac-arm64/OpenAlice.app/Contents/Resources/app')
writeBasePackage(appRoot, piManifest())
writeBasePackage(appRoot, { ...piManifest(), ...searchToolsManifest('darwin-arm64') })
writePackageFile(appRoot, 'vendor/tools/darwin-arm64/bin/fd')
writePackageFile(appRoot, 'vendor/tools/darwin-arm64/bin/rg')

const result = assertDesktopPackage({ packageRoot: root, repoRoot: root, arch: 'arm64' })

Expand Down Expand Up @@ -71,6 +86,7 @@ describe('assertDesktopPackage', () => {
const appRoot = join(root, 'win-unpacked/resources/app')
writeBasePackage(appRoot, {
...piManifest(),
...searchToolsManifest('win32-x64', true),
git: {
'win32-x64': {
version: '2.55.0.2',
Expand All @@ -84,6 +100,8 @@ describe('assertDesktopPackage', () => {
writePackageFile(appRoot, 'vendor/git/win32-x64/cmd/git.exe')
writePackageFile(appRoot, 'vendor/git/win32-x64/bin/bash.exe')
writePackageFile(appRoot, 'vendor/git/win32-x64/bin/sh.exe')
writePackageFile(appRoot, 'vendor/tools/win32-x64/bin/fd.exe')
writePackageFile(appRoot, 'vendor/tools/win32-x64/bin/rg.exe')

const result = assertDesktopPackage({ packageRoot: root, repoRoot: root, arch: 'x64' })

Expand Down
14 changes: 13 additions & 1 deletion scripts/guardian/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
* Replaces the previous `scripts/dev.ts`. Same `pnpm dev` UX.
*/

import { resolve } from 'node:path'
import { delimiter, resolve } from 'node:path'
import { homedir } from 'node:os'
import { existsSync } from 'node:fs'
import type { ChildProcess } from 'node:child_process'
Expand Down Expand Up @@ -113,6 +113,17 @@ async function main(): Promise<void> {
const flagPath = resolve(dataHome, 'data/control/restart-uta.flag')
const utaUrl = `http://127.0.0.1:${ports.utaPort}`
const backendHotReload = isBackendHotReloadEnabled(process.env)
const managedSearchToolsDir = resolve(
process.cwd(),
'vendor',
'tools',
`${process.platform}-${process.arch}`,
'bin',
)
const managedToolchainPath = [
...(existsSync(managedSearchToolsDir) ? [managedSearchToolsDir] : []),
...(process.env['OPENALICE_MANAGED_TOOLCHAIN_PATH'] ?? '').split(delimiter).filter(Boolean),
].join(delimiter)

console.log('')
console.log(`[guardian] mode → ${initialMode.mode} (${initialMode.source}${initialMode.envLocked ? ', env-locked' : ''})`)
Expand All @@ -137,6 +148,7 @@ async function main(): Promise<void> {
OPENALICE_LAUNCHER: 'dev',
OPENALICE_GUARDIAN_PID: String(process.pid),
OPENALICE_GUARDIAN_STARTED_AT: String(guardianStartedAt),
...(managedToolchainPath ? { OPENALICE_MANAGED_TOOLCHAIN_PATH: managedToolchainPath } : {}),
...(takeover ? { OPENALICE_TAKEOVER: '1' } : {}),
}

Expand Down
62 changes: 61 additions & 1 deletion scripts/smoke-packaged-toolchain.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,61 @@ export function buildPackagedToolchainSmokePlan(packageResult) {
expectStdout: /\b0\.80\.6\b/,
})

const searchTools = packageResult.manifest.searchTools?.[packageResult.platformArch]
if ((packageResult.platform === 'win32' || packageResult.platform === 'darwin') && !searchTools) {
errors.push(`[packaged-toolchain] missing managed fd/rg manifest entry ${packageResult.platformArch}`)
return { ok: false, errors, commands }
}
if (searchTools) {
const searchToolsRoot = join(packageResult.appRoot, searchTools.path)
const searchToolsBin = join(searchToolsRoot, searchTools.binPath)
const fdBinary = join(searchToolsRoot, searchTools.fd.binary)
const rgBinary = join(searchToolsRoot, searchTools.rg.binary)
const searchToolsEnv = {
PATH: [searchToolsBin, process.env['PATH']].filter(Boolean).join(delimiter),
}
commands.push({
label: 'managed fd',
command: fdBinary,
args: ['--version'],
expectStdout: /^fd \d+\.\d+\.\d+/m,
})
commands.push({
label: 'managed ripgrep',
command: rgBinary,
args: ['--version'],
expectStdout: /^ripgrep \d+\.\d+\.\d+/m,
})

const piToolsManager = join(
packageResult.appRoot,
'vendor',
'pi',
'node_modules',
'@earendil-works',
'pi-coding-agent',
'dist',
'utils',
'tools-manager.js',
)
const piProbe = [
`import(${JSON.stringify(pathToFileURL(piToolsManager).href)})`,
'.then((m) => console.log("OPENALICE_PI_SEARCH_TOOLS_OK " + m.getToolPath("fd") + " " + m.getToolPath("rg")))',
'.catch((err) => { console.error(err); process.exit(1) })',
].join('')
commands.push({
label: 'managed Pi resolves packaged fd/rg without download',
command: electron,
args: ['-e', piProbe],
env: {
...searchToolsEnv,
ELECTRON_RUN_AS_NODE: '1',
PI_CODING_AGENT_DIR: join(packageResult.appRoot, '.missing-smoke-pi-agent'),
},
expectStdout: /OPENALICE_PI_SEARCH_TOOLS_OK fd rg/,
})
}

commands.push({
label: 'workspace CLI payload through packaged Electron Node',
command: electron,
Expand All @@ -62,8 +117,13 @@ export function buildPackagedToolchainSmokePlan(packageResult) {
const toolchainPath = (Array.isArray(git.toolchainPaths) ? git.toolchainPaths : ['cmd', 'bin', 'usr/bin'])
.map((entry) => join(gitRoot, entry))
.join(delimiter)
const managedSearchToolsBin = searchTools
? join(packageResult.appRoot, searchTools.path, searchTools.binPath)
: null
const winEnv = {
PATH: [workspaceCliDir, toolchainPath, process.env['PATH']].filter(Boolean).join(delimiter),
PATH: [workspaceCliDir, managedSearchToolsBin, toolchainPath, process.env['PATH']]
.filter(Boolean)
.join(delimiter),
CHERE_INVOKING: '1',
MSYSTEM: packageResult.platformArch.endsWith('arm64') ? 'CLANGARM64' : 'MINGW64',
OPENALICE_MANAGED_PI_NODE_PATH: electron,
Expand Down
34 changes: 29 additions & 5 deletions scripts/smoke-packaged-toolchain.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,29 @@ describe('buildPackagedToolchainSmokePlan', () => {
errors: [],
appRoot,
platform: 'darwin',
platformArch: null,
platformArch: 'darwin-arm64',
manifest: {
pi: {
cli: 'vendor/pi/node_modules/@earendil-works/pi-coding-agent/dist/cli.js',
},
searchTools: {
'darwin-arm64': {
path: 'vendor/tools/darwin-arm64',
binPath: 'bin',
fd: { binary: 'bin/fd' },
rg: { binary: 'bin/rg' },
},
},
},
})

expect(plan.ok).toBe(true)
expect(plan.commands.map((command) => command.label)).toEqual([
'packaged Electron Node mode',
'managed Pi through packaged Electron Node',
'managed fd',
'managed ripgrep',
'managed Pi resolves packaged fd/rg without download',
'workspace CLI payload through packaged Electron Node',
])
expect(packagedElectronExecutable(appRoot, 'darwin')?.replaceAll('\\', '/'))
Expand All @@ -61,6 +72,14 @@ describe('buildPackagedToolchainSmokePlan', () => {
pi: {
cli: 'vendor/pi/node_modules/@earendil-works/pi-coding-agent/dist/cli.js',
},
searchTools: {
'win32-x64': {
path: 'vendor/tools/win32-x64',
binPath: 'bin',
fd: { binary: 'bin/fd.exe' },
rg: { binary: 'bin/rg.exe' },
},
},
git: {
'win32-x64': {
path: 'vendor/git/win32-x64',
Expand All @@ -77,18 +96,23 @@ describe('buildPackagedToolchainSmokePlan', () => {
expect(plan.commands.map((command) => command.label)).toEqual([
'packaged Electron Node mode',
'managed Pi through packaged Electron Node',
'managed fd',
'managed ripgrep',
'managed Pi resolves packaged fd/rg without download',
'workspace CLI payload through packaged Electron Node',
'managed git.exe',
'managed bash.exe',
'managed sh.exe can resolve git and bash on PATH',
'Workspace CLI launcher through managed Git Bash',
'Workspace CLI transport env through managed Git Bash',
])
expect(plan.commands[3].command.replaceAll('\\', '/')).toContain('vendor/git/win32-x64/cmd/git.exe')
expect(plan.commands[5].env?.PATH.replaceAll('\\', '/')).toContain('vendor/git/win32-x64/mingw64/bin')
expect(plan.commands[6].env?.OPENALICE_MANAGED_PI_NODE_PATH.replaceAll('\\', '/'))
expect(plan.commands[2].command.replaceAll('\\', '/')).toContain('vendor/tools/win32-x64/bin/fd.exe')
expect(plan.commands[6].command.replaceAll('\\', '/')).toContain('vendor/git/win32-x64/cmd/git.exe')
expect(plan.commands[8].env?.PATH.replaceAll('\\', '/')).toContain('vendor/git/win32-x64/mingw64/bin')
expect(plan.commands[8].env?.PATH.replaceAll('\\', '/')).toContain('vendor/tools/win32-x64/bin')
expect(plan.commands[9].env?.OPENALICE_MANAGED_PI_NODE_PATH.replaceAll('\\', '/'))
.toContain('win-unpacked/OpenAlice.exe')
expect(plan.commands[7].env?.OPENALICE_TOOL_URL).toBe('/cli')
expect(plan.commands[10].env?.OPENALICE_TOOL_URL).toBe('/cli')
} finally {
rmSync(root, { recursive: true, force: true })
}
Expand Down
Loading
Loading