@agent-foundry/replay-server 1.0.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.
@@ -0,0 +1,694 @@
1
+ ---
2
+ alwaysApply: true
3
+ ---
4
+ # Replay Server - Project Documentation
5
+
6
+ ## Project Overview
7
+
8
+ Replay Server is a **video rendering service** for AgentFoundry mini-game replays. It uses Puppeteer to load games in a headless browser, injects replay manifests, and captures video frames that are streamed directly to FFmpeg for encoding.
9
+
10
+ **Purpose**: Convert game replay manifests into MP4 video files for sharing and playback.
11
+
12
+ **Tech Stack**:
13
+ - **Node.js 18+** - Runtime
14
+ - **TypeScript 5.3.3** - Type safety
15
+ - **Express 4.18.2** - HTTP API server
16
+ - **Puppeteer 22.0.0** - Headless browser automation
17
+ - **FFmpeg** - Video encoding (via fluent-ffmpeg)
18
+ - **pnpm** - Package manager (monorepo workspace)
19
+
20
+ **Key Features**:
21
+ - Headless browser rendering (zero code duplication with game)
22
+ - Static bundle hosting (no external game server needed)
23
+ - Streaming video encoding (no temporary files)
24
+ - HTTP API for async job processing
25
+ - CLI tool for offline rendering
26
+
27
+ ---
28
+
29
+ ## Architecture
30
+
31
+ ### High-Level Architecture
32
+
33
+ ```mermaid
34
+ graph TB
35
+ subgraph Client["Client"]
36
+ API["HTTP API<br/>POST /render"]
37
+ CLI["CLI Tool<br/>render.ts"]
38
+ end
39
+
40
+ subgraph Server["Replay Server"]
41
+ Express["Express Server<br/>Port 3001"]
42
+ JobManager["Job Manager<br/>In-Memory Store"]
43
+ Renderer["PuppeteerRenderer"]
44
+ end
45
+
46
+ subgraph Browser["Headless Browser"]
47
+ Puppeteer["Puppeteer<br/>Chromium"]
48
+ GamePage["Game Page<br/>?mode=replay"]
49
+ Manifest["Manifest Injection<br/>window.__REPLAY_MANIFEST__"]
50
+ end
51
+
52
+ subgraph Encoding["Video Encoding"]
53
+ FFmpeg["FFmpeg Process<br/>libx264/h264_nvenc"]
54
+ VideoFile["Output MP4<br/>H.264"]
55
+ end
56
+
57
+ subgraph Bundles["Bundle Hosting"]
58
+ StaticServer["Static Server<br/>/bundles/*"]
59
+ GameBundle["Game Bundle<br/>bundles/game-life-restart/"]
60
+ end
61
+
62
+ API --> Express
63
+ CLI --> Renderer
64
+ Express --> JobManager
65
+ Express --> Renderer
66
+ Renderer --> Puppeteer
67
+ Puppeteer --> GamePage
68
+ GamePage --> Manifest
69
+ Renderer --> FFmpeg
70
+ FFmpeg --> VideoFile
71
+ Puppeteer --> StaticServer
72
+ StaticServer --> GameBundle
73
+ ```
74
+
75
+ ### Rendering Flow
76
+
77
+ ```mermaid
78
+ sequenceDiagram
79
+ participant Client
80
+ participant Server
81
+ participant Renderer
82
+ participant Browser
83
+ participant Game
84
+ participant FFmpeg
85
+
86
+ Client->>Server: POST /render {manifest, config}
87
+ Server->>Server: Create job (pending)
88
+ Server->>Client: {jobId, status: "pending"}
89
+
90
+ Server->>Renderer: render(manifest, config)
91
+ Renderer->>Browser: Launch Chromium
92
+ Browser->>Game: Navigate to gameUrl?mode=replay
93
+ Game->>Game: Wait for React init
94
+ Renderer->>Game: Inject manifest (window.__REPLAY_MANIFEST__)
95
+ Renderer->>Game: Dispatch 'replay-manifest-loaded' event
96
+ Game->>Game: Enter replay mode
97
+ Game->>Renderer: Set data-replay-ready="true"
98
+
99
+ Renderer->>FFmpeg: Launch FFmpeg process
100
+ loop For each age in timeline
101
+ Renderer->>Game: Dispatch 'replay-next-age' event
102
+ Game->>Game: Advance to next age
103
+ loop For each frame (fps * secondsPerAge)
104
+ Renderer->>Browser: Capture screenshot (JPEG)
105
+ Renderer->>FFmpeg: Write frame to stdin
106
+ end
107
+ end
108
+
109
+ Renderer->>FFmpeg: Close stdin
110
+ FFmpeg->>FFmpeg: Encode video
111
+ FFmpeg->>Server: Write MP4 file
112
+ Renderer->>Server: Update job (completed)
113
+ Client->>Server: GET /status/:jobId
114
+ Server->>Client: {status: "completed", outputPath}
115
+ Client->>Server: GET /download/:jobId
116
+ Server->>Client: Video file
117
+ ```
118
+
119
+ ### Key Design Decisions
120
+
121
+ #### 1. Puppeteer-Based Rendering
122
+
123
+ **Why**: Zero code duplication. The actual game runs in a headless browser, ensuring the video looks exactly like gameplay.
124
+
125
+ **How**:
126
+ - Game is loaded from URL (local dev) or bundle (production)
127
+ - Replay manifest is injected via `window.__REPLAY_MANIFEST__`
128
+ - Game enters "replay mode" and plays back events
129
+ - Screenshots captured at each frame
130
+
131
+ #### 2. Bundle Hosting
132
+
133
+ **Why**: Eliminates need for external game server. Bundles are self-contained static files.
134
+
135
+ **How**:
136
+ - Game bundles built with `VITE_BASE_PATH=/bundles/<bundleId>/`
137
+ - Served from Express static middleware at `/bundles/<bundleId>/`
138
+ - Manifest includes `bundleId` field
139
+ - Server resolves game URL: `http://localhost:PORT/bundles/<bundleId>/`
140
+
141
+ #### 3. Streaming FFmpeg Encoding
142
+
143
+ **Why**: No temporary files, lower disk I/O, faster encoding.
144
+
145
+ **How**:
146
+ - Screenshots captured as JPEG buffers
147
+ - Frames written directly to FFmpeg stdin
148
+ - FFmpeg configured with `-f image2pipe -vcodec mjpeg -i -`
149
+ - Process handles stdin drain events for backpressure
150
+
151
+ #### 4. Game URL Resolution Priority
152
+
153
+ 1. `config.gameUrl` (explicit URL in render request)
154
+ 2. `manifest.bundleId` → `/bundles/<bundleId>/` (bundle hosting)
155
+ 3. `GAME_URL` environment variable (fallback)
156
+
157
+ ---
158
+
159
+ ## Core Components
160
+
161
+ ### 1. HTTP Server (`src/server/index.ts`)
162
+
163
+ Express-based API server handling render jobs, job status, and bundle management.
164
+
165
+ **Key Responsibilities**:
166
+ - Accept render requests with manifest
167
+ - Manage job lifecycle (pending → processing → completed/failed)
168
+ - Serve static game bundles
169
+ - Provide job status and download endpoints
170
+
171
+ **Job Storage**: In-memory Map (for now, could be Redis/DB in production)
172
+
173
+ **Routes**:
174
+ - `POST /render` - Start render job
175
+ - `GET /jobs` - List all jobs
176
+ - `GET /status/:jobId` - Get job status
177
+ - `GET /download/:jobId` - Download completed video
178
+ - `GET /bundles` - List available bundles
179
+ - `GET /health` - Health check
180
+ - `GET /debug/chrome` - Debug Chromium installation
181
+
182
+ ### 2. Puppeteer Renderer (`src/renderer/PuppeteerRenderer.ts`)
183
+
184
+ Core rendering engine that orchestrates browser, game loading, and frame capture.
185
+
186
+ **Key Responsibilities**:
187
+ - Launch and manage headless Chromium instance
188
+ - Navigate to game and inject manifest
189
+ - Wait for game to enter replay mode
190
+ - Capture frames and stream to FFmpeg
191
+ - Handle errors and cleanup
192
+
193
+ **Key Methods**:
194
+ - `render(manifest, config)` - Main rendering entry point
195
+ - `initBrowser(config)` - Launch Chromium with optimized flags
196
+ - `loadGame(gameUrl, manifest)` - Navigate and inject manifest
197
+ - `captureAndEncodeFrames(manifest, config)` - Frame capture loop
198
+ - `launchFFmpeg(encoder, config)` - Start FFmpeg process
199
+
200
+ **Browser Configuration**:
201
+ - Single-process mode (container-friendly)
202
+ - Disabled GPU, sandbox, extensions
203
+ - Optimized for headless rendering
204
+ - Chinese fonts support (Noto CJK, WQY Zenhei)
205
+
206
+ ### 3. CLI Tool (`src/cli/render.ts`)
207
+
208
+ Command-line interface for offline video rendering.
209
+
210
+ **Usage**:
211
+ ```bash
212
+ pnpm --filter replay-server render -- \
213
+ --manifest ./replay.json \
214
+ --output ./output.mp4 \
215
+ --game-url http://localhost:5173 \
216
+ --width 1080 \
217
+ --height 1920 \
218
+ --fps 30
219
+ ```
220
+
221
+ **Features**:
222
+ - Load manifest from file or URL
223
+ - Progress bar display
224
+ - Configurable video settings
225
+ - Hardware acceleration support
226
+
227
+ ---
228
+
229
+ ## API Endpoints
230
+
231
+ ### POST /render
232
+
233
+ Start a new render job.
234
+
235
+ **Request Body**:
236
+ ```json
237
+ {
238
+ "manifest": {
239
+ "schema": "lifeRestart.replay.v1",
240
+ "bundleId": "game-life-restart",
241
+ "gameId": "test-game",
242
+ "timeline": [...],
243
+ "highlights": []
244
+ },
245
+ "config": {
246
+ "width": 1080,
247
+ "height": 1920,
248
+ "fps": 30,
249
+ "secondsPerAge": 2,
250
+ "gameUrl": "http://localhost:5173" // Optional, overrides bundleId
251
+ }
252
+ }
253
+ ```
254
+
255
+ **Alternative** (with manifestUrl):
256
+ ```json
257
+ {
258
+ "manifestUrl": "https://example.com/replay.json",
259
+ "config": {...}
260
+ }
261
+ ```
262
+
263
+ **Response**:
264
+ ```json
265
+ {
266
+ "jobId": "5eaee18f-48a7-4193-b594-86756ac76ce2",
267
+ "status": "pending",
268
+ "message": "Render job started"
269
+ }
270
+ ```
271
+
272
+ **Status Codes**:
273
+ - `200` - Job created successfully
274
+ - `400` - Invalid manifest or missing required fields
275
+ - `500` - Internal server error
276
+
277
+ ### GET /jobs
278
+
279
+ List all render jobs.
280
+
281
+ **Response**:
282
+ ```json
283
+ {
284
+ "jobs": [
285
+ {
286
+ "jobId": "5eaee18f-48a7-4193-b594-86756ac76ce2",
287
+ "status": "completed",
288
+ "progress": {
289
+ "stage": "complete",
290
+ "current": 1,
291
+ "total": 1,
292
+ "message": "Completed in 45.2s"
293
+ },
294
+ "error": null,
295
+ "createdAt": "2024-01-01T00:00:00.000Z",
296
+ "completedAt": "2024-01-01T00:05:00.000Z"
297
+ }
298
+ ],
299
+ "count": 1
300
+ }
301
+ ```
302
+
303
+ ### GET /status/:jobId
304
+
305
+ Get status of a specific job.
306
+
307
+ **Response**:
308
+ ```json
309
+ {
310
+ "jobId": "5eaee18f-48a7-4193-b594-86756ac76ce2",
311
+ "status": "processing",
312
+ "progress": {
313
+ "stage": "capture",
314
+ "current": 150,
315
+ "total": 300,
316
+ "message": "Capturing & encoding frame 150/300 (libx264)"
317
+ },
318
+ "error": null,
319
+ "createdAt": "2024-01-01T00:00:00.000Z",
320
+ "completedAt": null
321
+ }
322
+ ```
323
+
324
+ **Status Values**:
325
+ - `pending` - Job created, not started
326
+ - `processing` - Currently rendering
327
+ - `completed` - Video ready for download
328
+ - `failed` - Error occurred (check `error` field)
329
+
330
+ ### GET /download/:jobId
331
+
332
+ Download completed video file.
333
+
334
+ **Response**: Binary MP4 file
335
+
336
+ **Status Codes**:
337
+ - `200` - Video file
338
+ - `400` - Job not completed
339
+ - `404` - Job or file not found
340
+
341
+ ### GET /bundles
342
+
343
+ List available game bundles.
344
+
345
+ **Response**:
346
+ ```json
347
+ {
348
+ "bundles": [
349
+ {
350
+ "bundleId": "game-life-restart",
351
+ "path": "/app/packages/replay-server/bundles/game-life-restart",
352
+ "url": "http://localhost:3001/bundles/game-life-restart/",
353
+ "ready": true
354
+ }
355
+ ],
356
+ "count": 1,
357
+ "bundlesDir": "/app/packages/replay-server/bundles"
358
+ }
359
+ ```
360
+
361
+ ### GET /health
362
+
363
+ Health check endpoint.
364
+
365
+ **Response**:
366
+ ```json
367
+ {
368
+ "status": "ok",
369
+ "gameUrl": "http://localhost:5173",
370
+ "bundlesDir": "/app/packages/replay-server/bundles",
371
+ "bundleCount": 1
372
+ }
373
+ ```
374
+
375
+ ### GET /debug/chrome
376
+
377
+ Debug Chromium installation (useful for containerized deployments).
378
+
379
+ **Response**:
380
+ ```json
381
+ {
382
+ "executablePath": "/usr/bin/chromium",
383
+ "tests": {
384
+ "version": {
385
+ "ok": true,
386
+ "output": "Chromium 120.0.6099.224"
387
+ },
388
+ "dependencies": {
389
+ "ok": true,
390
+ "summary": "All libraries found"
391
+ },
392
+ "chineseFonts": {
393
+ "ok": true,
394
+ "count": 15,
395
+ "sample": ["Noto Sans CJK SC", "WQY Zenhei"]
396
+ }
397
+ }
398
+ }
399
+ ```
400
+
401
+ ---
402
+
403
+ ## Game Integration Requirements
404
+
405
+ For a game to work with the replay renderer, it must support replay mode:
406
+
407
+ ### 1. Replay Mode Detection
408
+
409
+ The game should check for `?mode=replay` query parameter:
410
+
411
+ ```typescript
412
+ useEffect(() => {
413
+ const urlParams = new URLSearchParams(window.location.search);
414
+ if (urlParams.get('mode') === 'replay') {
415
+ // Enter replay mode
416
+ }
417
+ }, []);
418
+ ```
419
+
420
+ ### 2. Manifest Injection
421
+
422
+ The renderer injects the manifest in two ways:
423
+
424
+ **Method 1**: Window property
425
+ ```typescript
426
+ if ((window as any).__REPLAY_MANIFEST__) {
427
+ const manifest = (window as any).__REPLAY_MANIFEST__;
428
+ enterReplayMode(manifest);
429
+ }
430
+ ```
431
+
432
+ **Method 2**: Custom event
433
+ ```typescript
434
+ window.addEventListener('replay-manifest-loaded', (e: CustomEvent) => {
435
+ const manifest = e.detail;
436
+ enterReplayMode(manifest);
437
+ });
438
+ ```
439
+
440
+ ### 3. Replay Control
441
+
442
+ The renderer dispatches `replay-next-age` events to advance through the timeline:
443
+
444
+ ```typescript
445
+ window.addEventListener('replay-next-age', () => {
446
+ // Advance to next age in replay
447
+ replayNextAge();
448
+ });
449
+ ```
450
+
451
+ ### 4. Ready Indicator
452
+
453
+ The game must set `data-replay-ready="true"` when ready for capture:
454
+
455
+ ```tsx
456
+ <div data-replay-ready="true">
457
+ {/* game content */}
458
+ </div>
459
+ ```
460
+
461
+ The renderer waits for this attribute before starting frame capture.
462
+
463
+ ### 5. React Initialization
464
+
465
+ The renderer waits for React to initialize by checking for:
466
+ - `ion-app` element exists
467
+ - `ion-spinner` element does not exist (loading complete)
468
+
469
+ ---
470
+
471
+ ## Deployment
472
+
473
+ ### Local Development
474
+
475
+ ```bash
476
+ # From project root
477
+ pnpm dev:replay-server
478
+
479
+ # Or from package directory
480
+ cd packages/replay-server
481
+ pnpm run dev
482
+ ```
483
+
484
+ **Requirements**:
485
+ - Node.js 18+
486
+ - FFmpeg in PATH
487
+ - Game running at `http://localhost:5173` OR bundle in `bundles/` directory
488
+
489
+ ### Docker (Recommended)
490
+
491
+ Multi-stage build that:
492
+ 1. Builds game bundle (`game-life-restart`)
493
+ 2. Builds replay-server
494
+ 3. Creates runtime image with both embedded
495
+
496
+ **Build**:
497
+ ```bash
498
+ # From project root
499
+ docker build -f packages/replay-server/Dockerfile -t replay-server:latest .
500
+ ```
501
+
502
+ **Run**:
503
+ ```bash
504
+ docker run -d \
505
+ --name replay-server \
506
+ -p 3001:3001 \
507
+ --shm-size=2g \
508
+ replay-server:latest
509
+ ```
510
+
511
+ **Docker Compose**:
512
+ ```bash
513
+ cd packages/replay-server
514
+ docker-compose -f docker-compose.local.yml up --build
515
+ ```
516
+
517
+ ### Aliyun Function Compute
518
+
519
+ **Build for FC**:
520
+ ```bash
521
+ docker buildx build \
522
+ --platform linux/amd64 \
523
+ --provenance=false \
524
+ --sbom=false \
525
+ -t ${ACR_REGISTRY}/${ACR_NAMESPACE}/replay-server:latest \
526
+ -f packages/replay-server/Dockerfile \
527
+ --push \
528
+ .
529
+ ```
530
+
531
+ **Function Configuration**:
532
+ - **Memory**: Minimum 2048 MB (2GB), recommended 4GB
533
+ - **Timeout**: Minimum 300 seconds (5 minutes), recommended 600 seconds
534
+ - **Instance Concurrency**: 1 (required - prevents resource contention)
535
+ - **CPU**: Auto-scaled with memory
536
+
537
+ **Environment Variables**:
538
+ - `PORT=9000` (FC sets this automatically)
539
+ - `BUNDLES_DIR=/app/packages/replay-server/bundles`
540
+ - `OUTPUT_DIR=/tmp/output` (use /tmp for writable directory)
541
+
542
+ **Deploy Script**:
543
+ ```bash
544
+ cd packages/replay-server
545
+ ACR_NAMESPACE=your-namespace ./scripts/deploy-aliyun.sh
546
+ ```
547
+
548
+ ---
549
+
550
+ ## Configuration
551
+
552
+ ### Environment Variables
553
+
554
+ | Variable | Default | Description |
555
+ |----------|---------|-------------|
556
+ | `PORT` | `3001` | Server port |
557
+ | `GAME_URL` | `http://localhost:5173` | Fallback game URL when no bundleId |
558
+ | `BUNDLES_DIR` | `./bundles` | Directory containing game bundles |
559
+ | `OUTPUT_DIR` | `./output` | Directory for rendered videos |
560
+ | `PUPPETEER_EXECUTABLE_PATH` | `/usr/bin/chromium` | Chromium executable path |
561
+
562
+ ### Render Configuration
563
+
564
+ | Option | Default | Description |
565
+ |--------|---------|-------------|
566
+ | `width` | `720` | Video width (pixels) |
567
+ | `height` | `1280` | Video height (pixels) |
568
+ | `fps` | `16` | Frames per second |
569
+ | `secondsPerAge` | `1.2` | Seconds to display each age |
570
+ | `useHardwareAcceleration` | `false` | Use h264_nvenc (GPU) instead of libx264 (CPU) |
571
+
572
+ ### Video Output
573
+
574
+ - **Format**: MP4 (H.264)
575
+ - **Resolution**: 720x1280 (portrait, for mobile/Feed)
576
+ - **FPS**: 16 (configurable)
577
+ - **Duration**: ~1.2s per age (configurable)
578
+ - **Encoder**: libx264 (CPU) by default, h264_nvenc (GPU) optional
579
+
580
+ ---
581
+
582
+ ## Building Game Bundles
583
+
584
+ Before deploying, build game bundles that will be hosted by replay-server.
585
+
586
+ ### Using Build Script
587
+
588
+ ```bash
589
+ cd packages/replay-server
590
+ ./scripts/build-bundle.sh game-life-restart
591
+ ```
592
+
593
+ This will:
594
+ 1. Build the game from `repo/game-life-restart`
595
+ 2. Copy `dist/` to `bundles/game-life-restart/`
596
+
597
+ ### Manual Build
598
+
599
+ ```bash
600
+ # Build the game
601
+ cd repo/game-life-restart
602
+ pnpm install
603
+ VITE_BASE_PATH=/bundles/game-life-restart/ pnpm build
604
+
605
+ # Copy to bundles
606
+ mkdir -p packages/replay-server/bundles/game-life-restart
607
+ cp -r dist/* packages/replay-server/bundles/game-life-restart/
608
+ ```
609
+
610
+ ### Docker (Automatic)
611
+
612
+ The Dockerfile includes a multi-stage build that automatically builds and embeds the game bundle. No manual steps required.
613
+
614
+ ---
615
+
616
+ ## File Structure
617
+
618
+ ```
619
+ packages/replay-server/
620
+ ├── .cursor/
621
+ │ ├── project.mdc # This file
622
+ │ └── dev.mdc # Development rules
623
+ ├── bundles/ # Game bundles (gitignored)
624
+ │ └── game-life-restart/
625
+ ├── samples/ # Sample replay manifests
626
+ │ └── life-replay-*.json
627
+ ├── scripts/
628
+ │ ├── build-bundle.sh # Build game bundle
629
+ │ └── deploy-aliyun.sh # Deploy to ACR
630
+ ├── src/
631
+ │ ├── cli/
632
+ │ │ └── render.ts # CLI tool
633
+ │ ├── renderer/
634
+ │ │ ├── index.ts
635
+ │ │ └── PuppeteerRenderer.ts # Core renderer
636
+ │ ├── server/
637
+ │ │ └── index.ts # Express HTTP server
638
+ │ └── index.ts # Package exports
639
+ ├── Dockerfile # Multi-stage Docker build
640
+ ├── docker-compose.local.yml
641
+ ├── package.json
642
+ ├── tsconfig.json
643
+ └── README.md
644
+ ```
645
+
646
+ ---
647
+
648
+ ## Key Files Reference
649
+
650
+ ### Server
651
+ - `src/server/index.ts` - Express HTTP API server (407 lines)
652
+ - Handles render jobs, bundle serving, job status
653
+
654
+ ### Renderer
655
+ - `src/renderer/PuppeteerRenderer.ts` - Core rendering engine (513 lines)
656
+ - Browser management, game loading, frame capture, FFmpeg integration
657
+
658
+ ### CLI
659
+ - `src/cli/render.ts` - Command-line tool (244 lines)
660
+ - Manifest loading, progress display, renderer invocation
661
+
662
+ ### Configuration
663
+ - `package.json` - Dependencies and scripts
664
+ - `tsconfig.json` - TypeScript configuration
665
+ - `Dockerfile` - Multi-stage build for production
666
+
667
+ ---
668
+
669
+ ## Dependencies
670
+
671
+ ### Core Dependencies
672
+ - `express` - HTTP server framework
673
+ - `puppeteer` - Headless browser automation
674
+ - `fluent-ffmpeg` - FFmpeg wrapper (note: actual FFmpeg binary required)
675
+ - `cors` - CORS middleware
676
+ - `uuid` - Job ID generation
677
+
678
+ ### Type Dependencies
679
+ - `@types/express`
680
+ - `@types/fluent-ffmpeg`
681
+ - `@types/cors`
682
+ - `@types/uuid`
683
+ - `@types/node`
684
+
685
+ ### Shared Package
686
+ - `@agent-foundry/replay` - Replay manifest type definitions
687
+
688
+ ---
689
+
690
+ ## Related Documentation
691
+
692
+ - [README.md](../README.md) - Main documentation with usage examples
693
+ - [../replay-shared/README.md](../replay-shared/README.md) - Replay manifest schema
694
+ - [../../repo/game-life-restart/doc/replay/](../../repo/game-life-restart/doc/replay/) - Game replay integration guide
package/.dockerignore ADDED
@@ -0,0 +1,11 @@
1
+ node_modules
2
+ dist
3
+ output
4
+ temp
5
+ *.log
6
+ .git
7
+ .gitignore
8
+ README.md
9
+ samples
10
+ .DS_Store
11
+ *.md