diff --git a/src/main/cli/cli-installer.test.ts b/src/main/cli/cli-installer.test.ts index e9472d3bfa1..9f642eeda45 100644 --- a/src/main/cli/cli-installer.test.ts +++ b/src/main/cli/cli-installer.test.ts @@ -668,6 +668,46 @@ describe('CliInstaller', () => { ) }) + it('does not overwrite the packaged Windows launcher while registering PATH', async () => { + const fixture = await makeFixture() + const localAppDataPath = fixture.root + const resourcesPath = join(localAppDataPath, 'Programs', 'Orca', 'resources') + const bundledLauncher = join(resourcesPath, 'bin', 'orca.cmd') + const bundledContent = '@echo off\r\necho bundled-orca %*\r\n' + await mkdir(dirname(bundledLauncher), { recursive: true }) + await writeFile(bundledLauncher, bundledContent, 'utf8') + + let userPath: string | null = null + const installer = new CliInstaller({ + platform: 'win32', + isPackaged: true, + resourcesPath, + localAppDataPath, + userDataPath: fixture.userDataPath, + execPath: join(localAppDataPath, 'Programs', 'Orca', 'Orca.exe'), + appPath: fixture.appPath, + userPathReader: async () => userPath, + userPathWriter: async (value) => { + userPath = value + } + }) + + const installed = await installer.install() + + expect(installed.state).toBe('installed') + expect(installed.pathConfigured).toBe(true) + expect(installed.commandPath).toBe(bundledLauncher) + expect(userPath).toBe(dirname(bundledLauncher)) + await expect(readFile(bundledLauncher, 'utf8')).resolves.toBe(bundledContent) + + const removed = await installer.remove() + + expect(removed.state).toBe('not_installed') + expect(removed.pathConfigured).toBe(false) + expect(userPath).toBe('') + await expect(readFile(bundledLauncher, 'utf8')).resolves.toBe(bundledContent) + }) + // Why: the arm64 fallback must apply for packaged builds, not just dev launchers. it.skipIf(process.platform === 'win32')( 'resolves to ~/.local/bin/orca on arm64 even when isPackaged is true', diff --git a/src/main/cli/cli-installer.ts b/src/main/cli/cli-installer.ts index 1480ad20b55..043566db1f8 100644 --- a/src/main/cli/cli-installer.ts +++ b/src/main/cli/cli-installer.ts @@ -173,6 +173,9 @@ export class CliInstaller { } else if (this.isLinuxAppImage()) { await this.installAppImageWrapper(status.commandPath, status.launcherPath) await this.removeLegacyLinuxCommandIfManaged(status.launcherPath) + } else if (this.isWindowsPackagedBundledCommand(status.commandPath, status.launcherPath)) { + // Why: packaged Windows already ships resources/bin/orca.cmd. Registration + // only owns the user PATH entry; rewriting the asset makes it recurse. } else { // Why: mkdir stays here for the Windows wrapper path — the target dir is // user-writable (%LOCALAPPDATA%) so EACCES cannot occur. The symlink path @@ -215,6 +218,8 @@ export class CliInstaller { if (status.installMethod === 'symlink') { await this.removeSymlink(status.commandPath) await this.removeLegacyLinuxCommandIfManaged(status.launcherPath) + } else if (this.isWindowsPackagedBundledCommand(status.commandPath, status.launcherPath)) { + await this.removeWindowsPathEntry(dirname(status.commandPath)) } else { await unlink(status.commandPath) await this.removeWindowsPathEntry(dirname(status.commandPath)) @@ -571,6 +576,19 @@ export class CliInstaller { return this.platform === 'linux' && Boolean(this.appImagePath) } + private isWindowsPackagedBundledCommand( + commandPath: string | null, + launcherPath: string | null + ): commandPath is string { + return ( + this.platform === 'win32' && + this.isPackaged && + commandPath !== null && + launcherPath !== null && + samePathEntry('win32', commandPath, launcherPath) + ) + } + private async inspectWindowsWrapper( commandPath: string, launcherPath: string @@ -589,6 +607,18 @@ export class CliInstaller { }) } + if (this.isWindowsPackagedBundledCommand(commandPath, launcherPath)) { + return this.buildStatus({ + commandPath, + launcherPath, + installMethod: 'wrapper', + supported: true, + state: 'installed', + currentTarget: launcherPath, + detail: `Registered at ${commandPath}.` + }) + } + const currentContent = await readFile(commandPath, 'utf8') const expectedContent = buildWindowsForwarder(launcherPath) return this.buildStatus({ @@ -657,6 +687,21 @@ export class CliInstaller { pathDirectory: string, pathConfigured: boolean ): CliInstallStatus { + if ( + this.isWindowsPackagedBundledCommand(status.commandPath, status.launcherPath) && + status.state === 'installed' && + !pathConfigured + ) { + return { + ...status, + pathDirectory, + pathConfigured, + state: 'not_installed', + currentTarget: null, + detail: `Register ${status.commandPath} to use Orca from Command Prompt or PowerShell.` + } + } + if (status.state !== 'installed') { return { ...status,