claude-session-share 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -113
- package/dist/__tests__/cli.test.js +120 -0
- package/dist/__tests__/e2e.test.js +4 -2
- package/dist/__tests__/path-encoding.test.js +37 -15
- package/dist/__tests__/prompts.test.js +147 -0
- package/dist/__tests__/sanitizer.test.js +323 -0
- package/dist/__tests__/session-writer.test.js +11 -15
- package/dist/cli.js +195 -0
- package/dist/index.js +98 -6
- package/dist/sanitization/sanitizer.js +39 -10
- package/dist/services/session-importer.js +50 -4
- package/dist/session/writer.js +7 -5
- package/dist/utils/path-encoding.js +21 -15
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
1
|
# claude-session-share
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
MCP server for sharing Claude Code sessions via GitHub Gist with automatic privacy protection.
|
|
4
4
|
|
|
5
|
-
Share your Claude Code conversations
|
|
5
|
+
Share your Claude Code conversations while keeping private data safe. Export sessions to shareable GitHub Gist links and import them back—all through natural language or CLI.
|
|
6
6
|
|
|
7
7
|
[](https://www.npmjs.com/package/claude-session-share)
|
|
8
8
|
[](https://opensource.org/licenses/MIT)
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Features
|
|
11
11
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
12
|
+
- **One-Click Sharing** - Export sessions to GitHub Gist with a simple command
|
|
13
|
+
- **Privacy First** - Automatically strips thinking blocks, sanitizes paths, and redacts secrets
|
|
14
|
+
- **Seamless Import** - Import shared sessions that work exactly like native Claude Code sessions
|
|
15
|
+
- **Natural Language** - Just ask Claude to "share my session" or "import from [link]"
|
|
16
|
+
- **Full Compatibility** - Imported sessions appear in `claude --resume` and preserve conversation context
|
|
17
17
|
|
|
18
|
-
##
|
|
18
|
+
## Installation
|
|
19
19
|
|
|
20
20
|
### Prerequisites
|
|
21
21
|
|
|
@@ -37,11 +37,11 @@ npm install -g claude-session-share
|
|
|
37
37
|
4. Check the **`gist`** scope
|
|
38
38
|
5. Generate and copy the token
|
|
39
39
|
|
|
40
|
-
##
|
|
40
|
+
## Configuration
|
|
41
41
|
|
|
42
|
-
Add the MCP server to your Claude Code configuration
|
|
42
|
+
Add the MCP server to your Claude Code configuration.
|
|
43
43
|
|
|
44
|
-
###
|
|
44
|
+
### User Config (Recommended)
|
|
45
45
|
|
|
46
46
|
Create or edit `~/.claude/mcp.json`:
|
|
47
47
|
|
|
@@ -59,24 +59,23 @@ Create or edit `~/.claude/mcp.json`:
|
|
|
59
59
|
}
|
|
60
60
|
```
|
|
61
61
|
|
|
62
|
-
###
|
|
62
|
+
### Project-Specific Config
|
|
63
63
|
|
|
64
64
|
Create `.mcp.json` in your project directory with the same structure.
|
|
65
65
|
|
|
66
66
|
### Verify Installation
|
|
67
67
|
|
|
68
68
|
```bash
|
|
69
|
-
# Check if the MCP server is recognized
|
|
70
69
|
claude # Start Claude Code
|
|
71
70
|
# Then type: /mcp
|
|
72
71
|
# You should see "claude-session-share" in the list
|
|
73
72
|
```
|
|
74
73
|
|
|
75
|
-
##
|
|
74
|
+
## Usage
|
|
76
75
|
|
|
77
|
-
###
|
|
76
|
+
### Natural Language (via MCP)
|
|
78
77
|
|
|
79
|
-
In any Claude Code conversation
|
|
78
|
+
In any Claude Code conversation:
|
|
80
79
|
|
|
81
80
|
```
|
|
82
81
|
"Share my current session to GitHub Gist"
|
|
@@ -88,28 +87,33 @@ Claude will:
|
|
|
88
87
|
3. Upload to a secret (unlisted) GitHub Gist
|
|
89
88
|
4. Return a shareable link
|
|
90
89
|
|
|
91
|
-
|
|
92
|
-
```
|
|
93
|
-
✓ Session shared successfully!
|
|
94
|
-
Link: https://gist.github.com/username/abc123...
|
|
90
|
+
To import a shared session:
|
|
95
91
|
|
|
96
|
-
|
|
97
|
-
"Import session
|
|
92
|
+
```
|
|
93
|
+
"Import this session: https://gist.github.com/username/abc123..."
|
|
98
94
|
```
|
|
99
95
|
|
|
100
|
-
###
|
|
96
|
+
### Command Line (Standalone CLI)
|
|
101
97
|
|
|
102
|
-
|
|
98
|
+
#### Share a session
|
|
103
99
|
|
|
104
|
-
```
|
|
105
|
-
|
|
100
|
+
```bash
|
|
101
|
+
# Share most recent session
|
|
102
|
+
npx claude-session-share share
|
|
103
|
+
|
|
104
|
+
# Share specific session file
|
|
105
|
+
npx claude-session-share share --session-path ~/.claude/projects/abc/session.jsonl
|
|
106
106
|
```
|
|
107
107
|
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
108
|
+
#### Import a session
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
# Import to current directory
|
|
112
|
+
npx claude-session-share import https://gist.github.com/user/abc123
|
|
113
|
+
|
|
114
|
+
# Import to specific directory
|
|
115
|
+
npx claude-session-share import abc123 --project-path /Users/name/project
|
|
116
|
+
```
|
|
113
117
|
|
|
114
118
|
### Resuming an Imported Session
|
|
115
119
|
|
|
@@ -119,13 +123,17 @@ claude --resume
|
|
|
119
123
|
# Select the imported session from the list
|
|
120
124
|
```
|
|
121
125
|
|
|
122
|
-
|
|
126
|
+
Or resume directly with the session ID:
|
|
127
|
+
|
|
128
|
+
```bash
|
|
129
|
+
claude --resume <session-id>
|
|
130
|
+
```
|
|
123
131
|
|
|
124
|
-
##
|
|
132
|
+
## Privacy Protection
|
|
125
133
|
|
|
126
134
|
Every shared session is automatically sanitized:
|
|
127
135
|
|
|
128
|
-
###
|
|
136
|
+
### What Gets Removed/Sanitized
|
|
129
137
|
|
|
130
138
|
- **Thinking Blocks** - Internal reasoning stripped completely
|
|
131
139
|
- **Absolute Paths** - `/Users/you/project/file.ts` → `file.ts`
|
|
@@ -133,7 +141,7 @@ Every shared session is automatically sanitized:
|
|
|
133
141
|
- **Tokens** - Bearer tokens, OAuth tokens → `[REDACTED]`
|
|
134
142
|
- **Secrets** - Environment variables, passwords (key=value format) → `[REDACTED]`
|
|
135
143
|
|
|
136
|
-
###
|
|
144
|
+
### What Gets Preserved
|
|
137
145
|
|
|
138
146
|
- Conversation flow and context
|
|
139
147
|
- Code examples and explanations
|
|
@@ -145,11 +153,8 @@ Every shared session is automatically sanitized:
|
|
|
145
153
|
|
|
146
154
|
- Passwords in connection strings (e.g., `postgresql://user:pass@host/db`) are not detected
|
|
147
155
|
- Secrets in natural language (not key=value format) may not be redacted
|
|
148
|
-
- These tradeoffs prevent false positives on legitimate content
|
|
149
|
-
|
|
150
|
-
## 📚 MCP Tools Reference
|
|
151
156
|
|
|
152
|
-
|
|
157
|
+
## MCP Tools Reference
|
|
153
158
|
|
|
154
159
|
### `share_session`
|
|
155
160
|
|
|
@@ -167,17 +172,17 @@ Exports the current session to GitHub Gist.
|
|
|
167
172
|
Imports a session from a GitHub Gist.
|
|
168
173
|
|
|
169
174
|
**Parameters:**
|
|
170
|
-
- `gistUrl` - GitHub Gist URL
|
|
171
|
-
- `projectPath`
|
|
175
|
+
- `gistUrl` - GitHub Gist URL or bare gist ID
|
|
176
|
+
- `projectPath` - Local project directory for import
|
|
172
177
|
|
|
173
178
|
**Returns:**
|
|
174
179
|
- `sessionPath` - Path to imported session file
|
|
175
180
|
- `sessionId` - New session ID
|
|
176
181
|
- `messageCount` - Number of messages imported
|
|
177
182
|
|
|
178
|
-
##
|
|
183
|
+
## Development
|
|
179
184
|
|
|
180
|
-
###
|
|
185
|
+
### Setup
|
|
181
186
|
|
|
182
187
|
```bash
|
|
183
188
|
git clone https://github.com/OmkarKovvali/claude-session-share.git
|
|
@@ -195,7 +200,7 @@ npm run build
|
|
|
195
200
|
|
|
196
201
|
```bash
|
|
197
202
|
npm test
|
|
198
|
-
#
|
|
203
|
+
# 420 tests
|
|
199
204
|
```
|
|
200
205
|
|
|
201
206
|
### Project Structure
|
|
@@ -204,64 +209,21 @@ npm test
|
|
|
204
209
|
claude-session-share/
|
|
205
210
|
├── src/
|
|
206
211
|
│ ├── index.ts # MCP server entry point
|
|
212
|
+
│ ├── cli.ts # CLI entry point
|
|
207
213
|
│ ├── gist/ # GitHub Gist integration
|
|
208
214
|
│ ├── sanitization/ # Privacy protection
|
|
209
215
|
│ ├── services/ # Share/import orchestration
|
|
210
216
|
│ ├── session/ # Session read/write
|
|
211
|
-
│ └── utils/ # UUID remapping,
|
|
217
|
+
│ └── utils/ # UUID remapping, path encoding
|
|
212
218
|
├── dist/ # Compiled output
|
|
213
|
-
├── .planning/ # Project planning docs
|
|
214
219
|
└── package.json
|
|
215
220
|
```
|
|
216
221
|
|
|
217
|
-
##
|
|
218
|
-
|
|
219
|
-
The project includes comprehensive test coverage:
|
|
220
|
-
|
|
221
|
-
- **Unit Tests** - All modules tested individually
|
|
222
|
-
- **Integration Tests** - Service orchestration verified
|
|
223
|
-
- **E2E Tests** - Full share→import→resume workflow validated
|
|
224
|
-
- **Real API Tests** - GitHub Gist integration tested with actual API
|
|
225
|
-
|
|
226
|
-
Run tests:
|
|
227
|
-
```bash
|
|
228
|
-
npm test # All tests
|
|
229
|
-
npm test -- session-reader # Specific test file
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
## 🤝 Contributing
|
|
233
|
-
|
|
234
|
-
Contributions welcome! Please:
|
|
235
|
-
|
|
236
|
-
1. Fork the repository
|
|
237
|
-
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
238
|
-
3. Commit your changes (`git commit -m 'Add amazing feature'`)
|
|
239
|
-
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
240
|
-
5. Open a Pull Request
|
|
241
|
-
|
|
242
|
-
### Code Style
|
|
243
|
-
|
|
244
|
-
- TypeScript with strict mode
|
|
245
|
-
- ESM modules
|
|
246
|
-
- Functional programming style (immutable transformations)
|
|
247
|
-
- Comprehensive tests for new features
|
|
248
|
-
|
|
249
|
-
## 📋 Roadmap
|
|
250
|
-
|
|
251
|
-
- [x] Core share/import functionality
|
|
252
|
-
- [x] Privacy sanitization
|
|
253
|
-
- [x] MCP server integration
|
|
254
|
-
- [x] End-to-end testing
|
|
255
|
-
- [ ] Web interface for browsing shared sessions
|
|
256
|
-
- [ ] Session versioning and updates
|
|
257
|
-
- [ ] Organization/team sharing features
|
|
258
|
-
- [ ] Custom sanitization rules
|
|
259
|
-
|
|
260
|
-
## 🐛 Troubleshooting
|
|
222
|
+
## Troubleshooting
|
|
261
223
|
|
|
262
224
|
### "Not authenticated" Error
|
|
263
225
|
|
|
264
|
-
|
|
226
|
+
Ensure `GITHUB_TOKEN` is set in MCP configuration:
|
|
265
227
|
```json
|
|
266
228
|
"env": {
|
|
267
229
|
"GITHUB_TOKEN": "ghp_your_token_here"
|
|
@@ -274,12 +236,7 @@ Ensure you're in a directory with an active Claude Code session. Sessions are st
|
|
|
274
236
|
|
|
275
237
|
### Imported Session Doesn't Appear
|
|
276
238
|
|
|
277
|
-
|
|
278
|
-
```bash
|
|
279
|
-
ls -la ~/.claude/projects/*/
|
|
280
|
-
```
|
|
281
|
-
|
|
282
|
-
Each project directory should have a `.jsonl` file—that's your session.
|
|
239
|
+
After importing, restart Claude Code to refresh the session list. The session file should be in `~/.claude/projects/-{encoded-path}/`.
|
|
283
240
|
|
|
284
241
|
### MCP Server Not Listed
|
|
285
242
|
|
|
@@ -287,27 +244,21 @@ Verify your MCP configuration:
|
|
|
287
244
|
```bash
|
|
288
245
|
cat ~/.claude/mcp.json
|
|
289
246
|
```
|
|
290
|
-
|
|
291
247
|
Then restart Claude Code.
|
|
292
248
|
|
|
293
|
-
##
|
|
249
|
+
## Contributing
|
|
294
250
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
251
|
+
1. Fork the repository
|
|
252
|
+
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
|
|
253
|
+
3. Commit your changes
|
|
254
|
+
4. Push to the branch
|
|
255
|
+
5. Open a Pull Request
|
|
298
256
|
|
|
299
|
-
##
|
|
257
|
+
## License
|
|
300
258
|
|
|
301
|
-
|
|
302
|
-
- Uses [GitHub Gist API](https://docs.github.com/en/rest/gists)
|
|
303
|
-
- Powered by [Claude Code](https://www.anthropic.com/claude)
|
|
259
|
+
MIT © Omkar Kovvali
|
|
304
260
|
|
|
305
|
-
##
|
|
261
|
+
## Support
|
|
306
262
|
|
|
307
263
|
- **Issues**: [GitHub Issues](https://github.com/OmkarKovvali/claude-session-share/issues)
|
|
308
|
-
- **Discussions**: [GitHub Discussions](https://github.com/OmkarKovvali/claude-session-share/discussions)
|
|
309
264
|
- **Email**: okovvali5@gmail.com
|
|
310
|
-
|
|
311
|
-
---
|
|
312
|
-
|
|
313
|
-
**Made with ❤️ for the Claude Code community**
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for CLI entry point
|
|
3
|
+
*
|
|
4
|
+
* Validates command parsing and integration with services
|
|
5
|
+
*/
|
|
6
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
7
|
+
import { uploadSession } from '../services/session-uploader.js';
|
|
8
|
+
import { importSession } from '../services/session-importer.js';
|
|
9
|
+
// Mock the services at module level
|
|
10
|
+
vi.mock('../services/session-uploader.js', () => ({
|
|
11
|
+
uploadSession: vi.fn(),
|
|
12
|
+
}));
|
|
13
|
+
vi.mock('../services/session-importer.js', () => ({
|
|
14
|
+
importSession: vi.fn(),
|
|
15
|
+
}));
|
|
16
|
+
// Mock fs/promises for findMostRecentSession
|
|
17
|
+
vi.mock('fs/promises', async () => {
|
|
18
|
+
const actual = await vi.importActual('fs/promises');
|
|
19
|
+
return {
|
|
20
|
+
...actual,
|
|
21
|
+
readdir: vi.fn(),
|
|
22
|
+
stat: vi.fn(),
|
|
23
|
+
};
|
|
24
|
+
});
|
|
25
|
+
describe('CLI argument parsing', () => {
|
|
26
|
+
it('should parse share command with session path', () => {
|
|
27
|
+
// Test that share command with --session-path flag works
|
|
28
|
+
const args = ['share', '--session-path', '/path/to/session.jsonl'];
|
|
29
|
+
// Find the session-path value
|
|
30
|
+
const pathIndex = args.indexOf('--session-path');
|
|
31
|
+
const sessionPath = pathIndex !== -1 && pathIndex + 1 < args.length
|
|
32
|
+
? args[pathIndex + 1]
|
|
33
|
+
: null;
|
|
34
|
+
expect(sessionPath).toBe('/path/to/session.jsonl');
|
|
35
|
+
});
|
|
36
|
+
it('should parse share command without session path', () => {
|
|
37
|
+
const args = ['share'];
|
|
38
|
+
const pathIndex = args.indexOf('--session-path');
|
|
39
|
+
const sessionPath = pathIndex !== -1 && pathIndex + 1 < args.length
|
|
40
|
+
? args[pathIndex + 1]
|
|
41
|
+
: null;
|
|
42
|
+
expect(sessionPath).toBeNull();
|
|
43
|
+
});
|
|
44
|
+
it('should parse import command with gist URL and default project path', () => {
|
|
45
|
+
// Simulate: node cli.js import https://gist...
|
|
46
|
+
// After "import" is consumed, remaining args are:
|
|
47
|
+
const argsAfterCommand = ['https://gist.github.com/user/abc123'];
|
|
48
|
+
const gistUrl = argsAfterCommand[0];
|
|
49
|
+
const pathIndex = argsAfterCommand.indexOf('--project-path');
|
|
50
|
+
const projectPath = pathIndex !== -1 && pathIndex + 1 < argsAfterCommand.length
|
|
51
|
+
? argsAfterCommand[pathIndex + 1]
|
|
52
|
+
: process.cwd();
|
|
53
|
+
expect(gistUrl).toBe('https://gist.github.com/user/abc123');
|
|
54
|
+
expect(projectPath).toBe(process.cwd());
|
|
55
|
+
});
|
|
56
|
+
it('should parse import command with project-path flag', () => {
|
|
57
|
+
// Simulate: node cli.js import abc123 --project-path /tmp/test
|
|
58
|
+
const argsAfterCommand = ['abc123', '--project-path', '/tmp/test'];
|
|
59
|
+
const gistUrl = argsAfterCommand[0];
|
|
60
|
+
const pathIndex = argsAfterCommand.indexOf('--project-path');
|
|
61
|
+
const projectPath = pathIndex !== -1 && pathIndex + 1 < argsAfterCommand.length
|
|
62
|
+
? argsAfterCommand[pathIndex + 1]
|
|
63
|
+
: process.cwd();
|
|
64
|
+
expect(gistUrl).toBe('abc123');
|
|
65
|
+
expect(projectPath).toBe('/tmp/test');
|
|
66
|
+
});
|
|
67
|
+
it('should handle missing gist URL in import command', () => {
|
|
68
|
+
// Simulate: node cli.js import (no URL provided)
|
|
69
|
+
const argsAfterCommand = [];
|
|
70
|
+
const gistUrl = argsAfterCommand[0];
|
|
71
|
+
expect(gistUrl).toBeUndefined();
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
describe('CLI service integration', () => {
|
|
75
|
+
it('uploadSession service should be importable', () => {
|
|
76
|
+
expect(uploadSession).toBeDefined();
|
|
77
|
+
expect(typeof uploadSession).toBe('function');
|
|
78
|
+
});
|
|
79
|
+
it('importSession service should be importable', () => {
|
|
80
|
+
expect(importSession).toBeDefined();
|
|
81
|
+
expect(typeof importSession).toBe('function');
|
|
82
|
+
});
|
|
83
|
+
it('uploadSession mock can be configured', () => {
|
|
84
|
+
vi.mocked(uploadSession).mockResolvedValue('https://gist.github.com/test/123');
|
|
85
|
+
expect(vi.mocked(uploadSession)).toBeDefined();
|
|
86
|
+
});
|
|
87
|
+
it('importSession mock can be configured', () => {
|
|
88
|
+
vi.mocked(importSession).mockResolvedValue({
|
|
89
|
+
sessionPath: '/path/to/session.jsonl',
|
|
90
|
+
sessionId: 'test-id',
|
|
91
|
+
messageCount: 10,
|
|
92
|
+
projectPath: '/test',
|
|
93
|
+
});
|
|
94
|
+
expect(vi.mocked(importSession)).toBeDefined();
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
describe('CLI usage validation', () => {
|
|
98
|
+
it('should have share and import as valid commands', () => {
|
|
99
|
+
const validCommands = ['share', 'import'];
|
|
100
|
+
expect(validCommands).toContain('share');
|
|
101
|
+
expect(validCommands).toContain('import');
|
|
102
|
+
expect(validCommands).not.toContain('unknown');
|
|
103
|
+
});
|
|
104
|
+
it('should validate share command structure', () => {
|
|
105
|
+
// share command can have optional --session-path
|
|
106
|
+
const shareArgs1 = ['share'];
|
|
107
|
+
const shareArgs2 = ['share', '--session-path', '/path'];
|
|
108
|
+
expect(shareArgs1[0]).toBe('share');
|
|
109
|
+
expect(shareArgs2[0]).toBe('share');
|
|
110
|
+
expect(shareArgs2.includes('--session-path')).toBe(true);
|
|
111
|
+
});
|
|
112
|
+
it('should validate import command structure', () => {
|
|
113
|
+
// import command requires gist URL, optional --project-path
|
|
114
|
+
const importArgsAfterCommand1 = ['https://gist.github.com/user/id'];
|
|
115
|
+
const importArgsAfterCommand2 = ['abc123', '--project-path', '/path'];
|
|
116
|
+
expect(importArgsAfterCommand1[0]).toBe('https://gist.github.com/user/id');
|
|
117
|
+
expect(importArgsAfterCommand2[0]).toBe('abc123');
|
|
118
|
+
expect(importArgsAfterCommand2.includes('--project-path')).toBe(true);
|
|
119
|
+
});
|
|
120
|
+
});
|
|
@@ -162,10 +162,12 @@ describe('End-to-End Session Sharing Workflow', () => {
|
|
|
162
162
|
for (const msg of assistantMsgs) {
|
|
163
163
|
expect(msg.snapshot.thinking).toBeNull();
|
|
164
164
|
}
|
|
165
|
-
// Verify:
|
|
165
|
+
// Verify: Original paths sanitized, then restored to import project path
|
|
166
|
+
// - Original path (/Users/test/myproject) is NOT preserved
|
|
167
|
+
// - cwd is restored to the import directory (absolute path)
|
|
166
168
|
const userMsg = importedMessages[0];
|
|
167
169
|
expect(userMsg.cwd).not.toContain('/Users/test/');
|
|
168
|
-
expect(userMsg.cwd).
|
|
170
|
+
expect(userMsg.cwd).toBe(importDir); // Restored to import project path
|
|
169
171
|
const firstAssistant = importedMessages[1];
|
|
170
172
|
const toolResultContent = firstAssistant.snapshot.messages[1].content;
|
|
171
173
|
expect(toolResultContent).not.toContain('/Users/test/myproject/');
|
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
* Tests for path encoding utilities
|
|
3
3
|
*
|
|
4
4
|
* Validates Claude Code's path encoding scheme for session directories.
|
|
5
|
+
* Claude Code encoding:
|
|
6
|
+
* 1. Replaces `/` with `-` (keeping leading dash from root /)
|
|
7
|
+
* 2. Replaces `_` with `-` (normalizes underscores to hyphens)
|
|
5
8
|
*/
|
|
6
9
|
import { describe, it, expect } from 'vitest';
|
|
7
10
|
import { encodeProjectPath, decodeProjectPath, getSessionDirectory } from '../utils/path-encoding.js';
|
|
@@ -9,68 +12,87 @@ import { homedir } from 'os';
|
|
|
9
12
|
import { join } from 'path';
|
|
10
13
|
describe('path-encoding', () => {
|
|
11
14
|
describe('encodeProjectPath', () => {
|
|
12
|
-
it('encodes absolute Unix path correctly', () => {
|
|
15
|
+
it('encodes absolute Unix path correctly (keeps leading dash)', () => {
|
|
13
16
|
const result = encodeProjectPath('/Users/name/project');
|
|
14
|
-
expect(result).toBe('Users-name-project');
|
|
17
|
+
expect(result).toBe('-Users-name-project');
|
|
15
18
|
});
|
|
16
19
|
it('encodes path with multiple segments', () => {
|
|
17
20
|
const result = encodeProjectPath('/Users/name/my-project/subdir');
|
|
18
|
-
expect(result).toBe('Users-name-my-project-subdir');
|
|
21
|
+
expect(result).toBe('-Users-name-my-project-subdir');
|
|
19
22
|
});
|
|
20
23
|
it('handles path with existing dashes', () => {
|
|
21
24
|
const result = encodeProjectPath('/Users/name/my-awesome-project');
|
|
22
|
-
expect(result).toBe('Users-name-my-awesome-project');
|
|
25
|
+
expect(result).toBe('-Users-name-my-awesome-project');
|
|
23
26
|
});
|
|
24
27
|
it('handles single segment path', () => {
|
|
25
28
|
const result = encodeProjectPath('/project');
|
|
26
|
-
expect(result).toBe('project');
|
|
29
|
+
expect(result).toBe('-project');
|
|
27
30
|
});
|
|
28
31
|
it('handles path with multiple consecutive slashes', () => {
|
|
29
32
|
const result = encodeProjectPath('/Users//name///project');
|
|
30
|
-
expect(result).toBe('Users--name---project');
|
|
33
|
+
expect(result).toBe('-Users--name---project');
|
|
31
34
|
});
|
|
32
35
|
it('handles path with trailing slash', () => {
|
|
33
36
|
const result = encodeProjectPath('/Users/name/project/');
|
|
34
|
-
expect(result).toBe('Users-name-project-');
|
|
37
|
+
expect(result).toBe('-Users-name-project-');
|
|
38
|
+
});
|
|
39
|
+
it('converts underscores to hyphens', () => {
|
|
40
|
+
const result = encodeProjectPath('/Users/name/my_project');
|
|
41
|
+
expect(result).toBe('-Users-name-my-project');
|
|
42
|
+
});
|
|
43
|
+
it('handles path with both underscores and dashes', () => {
|
|
44
|
+
const result = encodeProjectPath('/Users/name/my_cool-project');
|
|
45
|
+
expect(result).toBe('-Users-name-my-cool-project');
|
|
35
46
|
});
|
|
36
47
|
});
|
|
37
48
|
describe('decodeProjectPath', () => {
|
|
38
|
-
it('decodes encoded path correctly', () => {
|
|
39
|
-
const result = decodeProjectPath('Users-name-project');
|
|
49
|
+
it('decodes encoded path correctly (leading dash becomes /)', () => {
|
|
50
|
+
const result = decodeProjectPath('-Users-name-project');
|
|
40
51
|
expect(result).toBe('/Users/name/project');
|
|
41
52
|
});
|
|
42
53
|
it('decodes path with multiple segments', () => {
|
|
43
|
-
const result = decodeProjectPath('Users-name-myproject-subdir');
|
|
54
|
+
const result = decodeProjectPath('-Users-name-myproject-subdir');
|
|
44
55
|
expect(result).toBe('/Users/name/myproject/subdir');
|
|
45
56
|
});
|
|
46
|
-
it('is inverse of encodeProjectPath', () => {
|
|
57
|
+
it('is inverse of encodeProjectPath for paths without underscores', () => {
|
|
47
58
|
const original = '/Users/name/project';
|
|
48
59
|
const encoded = encodeProjectPath(original);
|
|
49
60
|
const decoded = decodeProjectPath(encoded);
|
|
50
61
|
expect(decoded).toBe(original);
|
|
51
62
|
});
|
|
52
63
|
it('handles single segment', () => {
|
|
53
|
-
const result = decodeProjectPath('project');
|
|
64
|
+
const result = decodeProjectPath('-project');
|
|
54
65
|
expect(result).toBe('/project');
|
|
55
66
|
});
|
|
67
|
+
it('handles paths without leading dash (legacy format)', () => {
|
|
68
|
+
// Legacy format without leading dash
|
|
69
|
+
const result = decodeProjectPath('Users-name-project');
|
|
70
|
+
expect(result).toBe('Users/name/project');
|
|
71
|
+
});
|
|
56
72
|
});
|
|
57
73
|
describe('getSessionDirectory', () => {
|
|
58
74
|
it('constructs correct session directory path', () => {
|
|
59
75
|
const projectPath = '/Users/name/project';
|
|
60
76
|
const result = getSessionDirectory(projectPath);
|
|
61
|
-
const expected = join(homedir(), '.claude', 'projects', 'Users-name-project');
|
|
77
|
+
const expected = join(homedir(), '.claude', 'projects', '-Users-name-project');
|
|
62
78
|
expect(result).toBe(expected);
|
|
63
79
|
});
|
|
64
80
|
it('handles complex project paths', () => {
|
|
65
81
|
const projectPath = '/Users/name/my-awesome-project/subdir';
|
|
66
82
|
const result = getSessionDirectory(projectPath);
|
|
67
|
-
const expected = join(homedir(), '.claude', 'projects', 'Users-name-my-awesome-project-subdir');
|
|
83
|
+
const expected = join(homedir(), '.claude', 'projects', '-Users-name-my-awesome-project-subdir');
|
|
68
84
|
expect(result).toBe(expected);
|
|
69
85
|
});
|
|
70
86
|
it('handles project path with existing dashes', () => {
|
|
71
87
|
const projectPath = '/opt/code/my-app';
|
|
72
88
|
const result = getSessionDirectory(projectPath);
|
|
73
|
-
const expected = join(homedir(), '.claude', 'projects', 'opt-code-my-app');
|
|
89
|
+
const expected = join(homedir(), '.claude', 'projects', '-opt-code-my-app');
|
|
90
|
+
expect(result).toBe(expected);
|
|
91
|
+
});
|
|
92
|
+
it('handles project path with underscores', () => {
|
|
93
|
+
const projectPath = '/Users/name/cc_links';
|
|
94
|
+
const result = getSessionDirectory(projectPath);
|
|
95
|
+
const expected = join(homedir(), '.claude', 'projects', '-Users-name-cc-links');
|
|
74
96
|
expect(result).toBe(expected);
|
|
75
97
|
});
|
|
76
98
|
});
|