From ad5902f0d5adf60f8ad9fc533b8c23bed1524c5d Mon Sep 17 00:00:00 2001 From: Dipesh Babu Date: Mon, 5 Oct 2026 00:02:36 -0400 Subject: [PATCH 1/3] fix(ci): smoke the npm bundle across supported platforms --- .github/workflows/ci.yml | 13 +++++++++---- packages/cli/test/install.test.ts | 7 ++++--- 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 03f8deb..42a3e5a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,18 +2,23 @@ name: CI on: push: - branches: [main] + branches: [main, 'fix/**'] pull_request: jobs: check: - runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, windows-latest] + node: [20, 22] + runs-on: ${{ matrix.os }} steps: - uses: actions/checkout@v4 - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: - node-version: 22 + node-version: ${{ matrix.node }} cache: pnpm - uses: oven-sh/setup-bun@v2 with: @@ -22,4 +27,4 @@ jobs: - run: pnpm typecheck - run: pnpm test - name: Bundle for npm and smoke it - run: pnpm build:npm && node packages/cli/bin/vibi.mjs --version + run: pnpm build:npm && node packages/cli/dist/vibi.mjs --version && node packages/cli/dist/vibi.mjs --help diff --git a/packages/cli/test/install.test.ts b/packages/cli/test/install.test.ts index 80bfa39..3b2a76b 100644 --- a/packages/cli/test/install.test.ts +++ b/packages/cli/test/install.test.ts @@ -17,10 +17,11 @@ after(async () => { const meta = { title: 't', cwd: '/work/x', model: '', messageCount: 1, sourcePath: '', startedAt: null }; test('Claude Code project directories are encoded the way Claude Code does it', () => { - assert.equal(encodeClaudeProjectDir('/Users/hongyinluo/Desktop/cybermind'), '-Users-hongyinluo-Desktop-cybermind'); - assert.equal(encodeClaudeProjectDir('/Users/me/my_app.v2'), '-Users-me-my-app-v2'); + const prefix = process.platform === 'win32' ? `${path.parse(process.cwd()).root[0]}--` : '-'; + assert.equal(encodeClaudeProjectDir('/Users/hongyinluo/Desktop/cybermind'), `${prefix}Users-hongyinluo-Desktop-cybermind`); + assert.equal(encodeClaudeProjectDir('/Users/me/my_app.v2'), `${prefix}Users-me-my-app-v2`); const file = installPathFor({ harness: 'claude', harnessSessionId: 'abc', harnessUpdatedAt: '2026-10-01T10:00:00Z', metadata: meta, home, projectDir: '/work/x' }); - assert.equal(file, path.join(home, '.claude', 'projects', '-work-x', 'abc.jsonl')); + assert.equal(file, path.join(home, '.claude', 'projects', `${prefix}work-x`, 'abc.jsonl')); }); test('other harnesses keep their original file names and date folders', () => { From 40c62e8ce19caaca509012dd9e94726acea460ba Mon Sep 17 00:00:00 2001 From: Dipesh Babu Date: Mon, 5 Oct 2026 00:08:03 -0400 Subject: [PATCH 2/3] fix(ci): expand test files without shell glob support --- package.json | 2 +- scripts/test.mjs | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 scripts/test.mjs diff --git a/package.json b/package.json index 1529ea4..a726ce6 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "packageManager": "pnpm@12.8.1", "scripts": { "typecheck": "pnpm -r typecheck", - "test": "tsx --test packages/shared/test/*.test.ts packages/cli/test/*.test.ts", + "test": "node scripts/test.mjs", "build:npm": "pnpm --filter vibi build:npm", "build:host": "pnpm --filter vibi build:tui:host && pnpm --filter vibi build:host", "build": "pnpm --filter vibi build:tui && pnpm --filter vibi build" diff --git a/scripts/test.mjs b/scripts/test.mjs new file mode 100644 index 0000000..c1b7768 --- /dev/null +++ b/scripts/test.mjs @@ -0,0 +1,11 @@ +import { spawnSync } from 'node:child_process'; +import { readdirSync } from 'node:fs'; +import { fileURLToPath } from 'node:url'; + +const root = fileURLToPath(new URL('../', import.meta.url)); +const files = ['packages/shared/test', 'packages/cli/test'].flatMap((dir) => + readdirSync(new URL(`../${dir}/`, import.meta.url)).filter((name) => name.endsWith('.test.ts')).sort().map((name) => `${dir}/${name}`) +); +const result = spawnSync(process.execPath, ['--import', 'tsx', '--test', ...files], {cwd: root, stdio: 'inherit'}); +if (result.error) throw result.error; +process.exit(result.status ?? 1); From e285f22097504b4bcafb1b8d187bbeeca93fe915 Mon Sep 17 00:00:00 2001 From: Dipesh Babu Date: Mon, 5 Oct 2026 00:18:49 -0400 Subject: [PATCH 3/3] fix: Session installation accepts traversal IDs and loses filenames from other operating systems --- packages/cli/src/harnesses/install.ts | 11 +++++++++-- packages/cli/test/install-paths.test.ts | 22 ++++++++++++++++++++++ 2 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 packages/cli/test/install-paths.test.ts diff --git a/packages/cli/src/harnesses/install.ts b/packages/cli/src/harnesses/install.ts index c5bf529..28f94a2 100644 --- a/packages/cli/src/harnesses/install.ts +++ b/packages/cli/src/harnesses/install.ts @@ -41,11 +41,18 @@ export function encodeClaudeProjectDir(projectDir: string) { } function originalBasename(metadata: TraceMetadata, fallback: string) { - const base = metadata.sourcePath ? path.basename(metadata.sourcePath) : ''; - return base && base.endsWith('.jsonl') ? base : fallback; + const base = path.win32.basename(path.posix.basename(metadata.sourcePath)); + return isSafeFilename(base) && base.endsWith('.jsonl') ? base : fallback; +} + +function isSafeFilename(value: string): boolean { + return Boolean(value) && value !== '.' && value !== '..' && + !/[<>:"/\\|?*\x00-\x1f]/.test(value) && !/[. ]$/.test(value) && + !/^(con|prn|aux|nul|com[1-9]|lpt[1-9])(?:\.|$)/i.test(value); } export function installPathFor(t: Omit): string { + if (!isSafeFilename(t.harnessSessionId)) throw new Error('Invalid harness session id for a local file'); switch (t.harness) { case 'claude': return path.join( diff --git a/packages/cli/test/install-paths.test.ts b/packages/cli/test/install-paths.test.ts new file mode 100644 index 0000000..658c88c --- /dev/null +++ b/packages/cli/test/install-paths.test.ts @@ -0,0 +1,22 @@ +import assert from 'node:assert/strict'; +import path from 'node:path'; +import { test } from 'node:test'; +import { installPathFor } from '../src/harnesses/install'; +import type { Harness } from '@vibivibi/shared/sessions'; + +const base = { harnessSessionId: 'session-id', harnessUpdatedAt: '2026-10-01T10:00:00Z', home: path.resolve('test-home'), metadata: {title: '', cwd: '', model: '', messageCount: 0, sourcePath: '', startedAt: null} }; +test('untrusted session IDs cannot escape installation directories or create invalid Windows files', () => { + for (const harness of ['claude', 'codex', 'pi', 'sc', 'opencode'] as Harness[]) { + for (const id of ['../escape', '..\\escape', '/absolute', 'C:\\absolute', '', '.', '..', 'bad\0id', 'CON', 'name:stream', 'trailing.']) { + assert.throws(() => installPathFor({...base, harness, harnessSessionId: id}), /Invalid harness session id/); + } + } +}); +test('source filenames survive Windows to Unix and Unix to Windows transfers', () => { + for (const sourcePath of ['C:\\Users\\sender\\rollout-original.jsonl', '/Users/sender/rollout-original.jsonl']) { + const file = installPathFor({...base, harness: 'codex', metadata: {...base.metadata, sourcePath}}); + assert.equal(path.basename(file), 'rollout-original.jsonl'); + assert.equal(path.dirname(file), path.join(base.home, '.codex', 'sessions', '2026', '10', '01')); + } + assert.equal(path.basename(installPathFor({...base, harness: 'pi', metadata: {...base.metadata, sourcePath: '/x/bad:name.jsonl'}})), 'session-id.jsonl'); +});