import { describe, expect, test } from 'bun:test'; import { mkdtempSync, readFileSync, writeFileSync, mkdirSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { ensureCliShim, onPath, SHIM_MARKER, shimPlan } from '../cli/lib/cli-shim'; describe('desktop terminal launcher', () => { test('per platform: file, content, PATH folder, profile', () => { const w = shimPlan({ exe: 'C:\\Users\\you\\AppData\\Local\\storebound\\storebound.exe', platform: 'win32', home: 'C:\\Users\\you', env: { LOCALAPPDATA: 'C:\\Users\\you\\AppData\\Local' } }); expect(w.file.endsWith('storebound.cmd')).toBe(true); expect(w.content).toContain('"C:\\Users\\you\\AppData\\Local\\storebound\\storebound.exe" %*'); expect(w.skip).toBeNull(); const m = shimPlan({ exe: "/Applications/store'bound.app/Contents/MacOS/storebound", platform: 'darwin', home: '/Users/you', env: {} }); expect(m.content).toContain(`exec '/Applications/store'\\''bound.app/Contents/MacOS/storebound' "$@"`); expect(m.profile!.endsWith('.zprofile')).toBe(true); expect(shimPlan({ exe: '/usr/bin/storebound', platform: 'linux', home: '/home/you', env: {} }).profile!.endsWith('.profile')).toBe(true); }); test('skips AppImage runs and the opt-out', () => { expect(shimPlan({ exe: '/tmp/.mount_sbX/usr/bin/storebound', platform: 'linux', home: '/h', env: {} }).skip).toContain('AppImage'); expect(shimPlan({ exe: '/usr/bin/storebound', platform: 'linux', home: '/h', env: { STOREBOUND_NO_CLI_SHIM: '1' } }).skip).toContain('STOREBOUND_NO_CLI_SHIM'); }); test('onPath: trailing slashes, case on Windows only', () => { expect(onPath('C:\\A\\bin', 'c:\\a\\bin\\;D:\\x', 'win32')).toBe(true); expect(onPath('/h/.local/bin', '/usr/bin:/h/.local/bin/', 'linux')).toBe(true); expect(onPath('/h/.local/bin', '/usr/bin:/H/.local/bin', 'linux')).toBe(false); }); test('writes once, rewrites a moved app, never touches a file that is not ours, adds the profile line once', () => { const home = mkdtempSync(join(tmpdir(), 'sb-shim-')); const env = { PATH: '/usr/bin' }; const first = ensureCliShim('/opt/a/storebound', 'linux', env, home); expect(first.join('\n')).toContain('terminal launcher written'); expect(first.join('\n')).toContain('added ~/.local/bin to PATH'); const file = join(home, '.local', 'bin', 'storebound'); expect(readFileSync(file, 'utf8')).toContain(SHIM_MARKER); expect(ensureCliShim('/opt/a/storebound', 'linux', env, home).join()).toContain('up to date'); expect(ensureCliShim('/opt/b/storebound', 'linux', env, home).join()).toContain('written'); expect(readFileSync(file, 'utf8')).toContain('/opt/b/storebound'); expect(readFileSync(join(home, '.profile'), 'utf8').split(SHIM_MARKER).length - 1).toBe(2 - 1); writeFileSync(file, '#!/bin/sh\nexec npm-storebound "$@"\n'); expect(ensureCliShim('/opt/c/storebound', 'linux', env, home).join()).toContain('is not ours'); expect(readFileSync(file, 'utf8')).toContain('npm-storebound'); if (process.platform === 'win32') return; // a C:\ temp folder has a colon: no POSIX PATH can name it const home2 = mkdtempSync(join(tmpdir(), 'sb-shim-')); mkdirSync(join(home2, '.local', 'bin'), { recursive: true }); expect(ensureCliShim('/opt/a/storebound', 'linux', { PATH: `/usr/bin:${join(home2, '.local', 'bin')}` }, home2).join()).not.toContain('PATH'); }); });