diff --git a/src/lib/transfer.ts b/src/lib/transfer.ts index 9979882..64ae58e 100644 --- a/src/lib/transfer.ts +++ b/src/lib/transfer.ts @@ -127,7 +127,25 @@ export function exportSessions(options: ExportOptions = {}): ExportResult { let targetDirs: string[]; if (projectPath) { - targetDirs = findMatchingProjectDirs(allDirs, projectPath); + const encodedMatches = new Set( + findMatchingProjectDirs(allDirs, projectPath) + ); + targetDirs = allDirs.filter((dir) => { + if (encodedMatches.has(dir)) return true; + + const srcDir = join(projectsDir, dir); + try { + if (!statSync(srcDir).isDirectory()) return false; + } catch { + return false; + } + + // The manifest path is resolved from session metadata and can name a + // nested working directory even when Claude stores the transcript under + // the project root. Match that exact advertised path back to the + // directory that produced it so every manifest entry is selectable. + return resolveProjectPath(projectsDir, dir) === projectPath; + }); if (targetDirs.length === 0) { result.errors.push({ file: projectPath, diff --git a/test/transfer.test.ts b/test/transfer.test.ts index 178cb71..b95b505 100644 --- a/test/transfer.test.ts +++ b/test/transfer.test.ts @@ -144,6 +144,75 @@ describe("exportSessions", () => { ); expect(hyphenProject?.originalPath).toBe("/Users/alice/my-project"); }); + + it("re-exports an advertised project by its resolved originalPath", () => { + const projectDir = join(PROJECTS_DIR, "-Users-alice-project"); + const nestedProjectPath = "/Users/alice/project/packages/api"; + + writeFileSync( + join(projectDir, "sessions-index.json"), + JSON.stringify({ + version: 1, + entries: [ + { + sessionId: "sess-001", + fullPath: `${PROJECTS_DIR}/-Users-alice-project/sess-001.jsonl`, + projectPath: nestedProjectPath, + }, + ], + }), + "utf-8" + ); + + writeFileSync( + join(projectDir, "sess-001.jsonl"), + JSON.stringify({ + type: "user", + cwd: nestedProjectPath, + message: { role: "user", content: "nested working directory" }, + }), + "utf-8" + ); + + const origEnv = process.env.CLAUDE_PATH; + process.env.CLAUDE_PATH = TEST_DIR; + + const dryRun = exportSessions({ + outputDir: EXPORT_DIR, + outputName: "all-projects-dry-run", + dryRun: true, + }); + const advertised = dryRun.manifest.projects.find( + (project) => project.encodedDir === "-Users-alice-project" + ); + + expect(advertised?.originalPath).toBe(nestedProjectPath); + expect(advertised?.jsonlCount).toBeGreaterThan(0); + + const result = exportSessions({ + projectPath: advertised!.originalPath, + outputDir: EXPORT_DIR, + outputName: "selected-export", + }); + + process.env.CLAUDE_PATH = origEnv; + + expect(result.errors).toHaveLength(0); + expect(result.manifest.projects.map((project) => project.encodedDir)).toContain( + "-Users-alice-project" + ); + expect( + existsSync( + join( + EXPORT_DIR, + "selected-export", + "projects", + "-Users-alice-project", + "sess-001.jsonl" + ) + ) + ).toBe(true); + }); }); describe("importSessions", () => {