Skip to content

Commit 464be5a

Browse files
DeDuckProjectclaude
andcommitted
feat: initial scaffold for git-glimpse
Sets up the full monorepo structure (pnpm workspaces) with: - @git-glimpse/core: diff parsing, route detection, LLM script generation, Playwright recording, GIF post-processing, PR comment publishing - @git-glimpse/action: GitHub Action wrapper - git-glimpse (CLI): npx git-glimpse run / init commands - 24 unit tests covering diff parser, route detector, script validator, and config loader - GitHub Actions workflow for self-dogfooding Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
0 parents  commit 464be5a

37 files changed

Lines changed: 3699 additions & 0 deletions

.github/workflows/demo.yml

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# GitGlimpse Demo Workflow
2+
# Safe: no untrusted user input (PR titles/bodies) is used in run: commands.
3+
# All PR context is passed via the GITHUB_TOKEN to the action itself.
4+
name: GitGlimpse Demo
5+
6+
on:
7+
pull_request:
8+
types: [opened, synchronize]
9+
paths:
10+
- 'packages/**'
11+
- 'frameworks/**'
12+
13+
jobs:
14+
demo:
15+
runs-on: ubuntu-latest
16+
permissions:
17+
pull-requests: write
18+
contents: read
19+
20+
steps:
21+
- uses: actions/checkout@v4
22+
with:
23+
fetch-depth: 0
24+
25+
- uses: actions/setup-node@v4
26+
with:
27+
node-version: '20'
28+
29+
- uses: pnpm/action-setup@v4
30+
with:
31+
version: 9
32+
33+
- run: pnpm install
34+
35+
- uses: git-glimpse/action@v1
36+
with:
37+
config-path: git-glimpse.config.ts
38+
env:
39+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
40+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

.gitignore

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
node_modules/
2+
dist/
3+
*.js
4+
*.d.ts
5+
*.d.ts.map
6+
*.js.map
7+
!jest.config.js
8+
!.eslintrc.js
9+
.env
10+
.env.local
11+
recordings/
12+
screenshots/
13+
*.webm
14+
*.gif
15+
*.mp4
16+
.DS_Store
17+
coverage/

CLAUDE.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# git-glimpse — Developer Guide
2+
3+
## What is this?
4+
5+
git-glimpse is a GitHub Action + CLI that automatically generates visual demo clips (GIF/video) of UI changes in pull requests. When a PR is opened or updated, it analyzes the diff, generates a Playwright interaction script via LLM, records a demo, and posts it as a PR comment.
6+
7+
## Repo Structure
8+
9+
```
10+
packages/
11+
core/ — Core library: diff analysis, script generation, recording, publishing
12+
action/ — GitHub Action wrapper
13+
cli/ — CLI for local use (`npx git-glimpse`)
14+
frameworks/ — Framework-specific route detectors (Remix, Next.js, SvelteKit)
15+
examples/ — Example project configurations
16+
tests/ — Integration tests
17+
```
18+
19+
## Development Setup
20+
21+
```bash
22+
pnpm install
23+
pnpm build
24+
pnpm test
25+
```
26+
27+
## Config File
28+
29+
Users place a `git-glimpse.config.ts` at their repo root. See `packages/core/src/config/schema.ts` for the full type definition.
30+
31+
## Pipeline Stages
32+
33+
1. **Diff Analyzer** — parse changed files, detect affected routes
34+
2. **Preview Env** — start the app or use a deploy preview URL
35+
3. **Script Generator** — LLM reads diff → Playwright interaction script
36+
4. **Recorder** — run script with Playwright video capture
37+
5. **Publisher** — convert to GIF, post as PR comment
38+
39+
## LLM Integration
40+
41+
Uses Anthropic Claude by default. API key read from `ANTHROPIC_API_KEY` env var. See `packages/core/src/generator/script-generator.ts` for prompt design.
42+
43+
## Testing
44+
45+
```bash
46+
pnpm test # all packages
47+
pnpm test --watch # watch mode
48+
```
49+
50+
Integration tests require a running app. See `tests/integration/`.
51+
52+
## Branching Convention
53+
54+
- Features: `feat/<name>`
55+
- Bug fixes: `fix/<name>`
56+
- Releases: `release/v<version>`

git-glimpse.config.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { GitGlimpseConfig } from '@git-glimpse/core';
2+
3+
// Self-referential config: git-glimpse uses git-glimpse to demo itself.
4+
export default {
5+
app: {
6+
startCommand: 'pnpm dev',
7+
readyWhen: { url: 'http://localhost:3000' },
8+
},
9+
recording: {
10+
format: 'gif',
11+
maxDuration: 30,
12+
viewport: { width: 1280, height: 720 },
13+
},
14+
llm: {
15+
provider: 'anthropic',
16+
model: 'claude-sonnet-4-6',
17+
},
18+
} satisfies GitGlimpseConfig;

package.json

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
{
2+
"name": "git-glimpse-monorepo",
3+
"private": true,
4+
"version": "0.1.0",
5+
"description": "Auto-generate visual demo clips of UI changes in pull requests",
6+
"license": "MIT",
7+
"engines": {
8+
"node": ">=20"
9+
},
10+
"packageManager": "pnpm@9.0.0",
11+
"scripts": {
12+
"build": "pnpm -r build",
13+
"test": "vitest run",
14+
"typecheck": "pnpm -r typecheck",
15+
"lint": "pnpm -r lint"
16+
},
17+
"devDependencies": {
18+
"typescript": "^5.4.5",
19+
"vitest": "^1.6.0",
20+
"@types/node": "^20.12.0"
21+
}
22+
}

packages/action/action.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: 'GitGlimpse'
2+
description: 'Auto-generate visual demo clips for UI changes in pull requests'
3+
author: 'git-glimpse'
4+
branding:
5+
icon: 'video'
6+
color: 'purple'
7+
8+
inputs:
9+
start-command:
10+
description: 'Command to start the app (overrides config file)'
11+
required: false
12+
preview-url:
13+
description: 'External preview deployment URL (e.g., from Vercel)'
14+
required: false
15+
config-path:
16+
description: 'Path to git-glimpse.config.ts'
17+
default: 'git-glimpse.config.ts'
18+
required: false
19+
format:
20+
description: 'Output format: gif, mp4, or webm'
21+
default: 'gif'
22+
required: false
23+
max-duration:
24+
description: 'Max recording duration in seconds'
25+
default: '30'
26+
required: false
27+
28+
outputs:
29+
recording-url:
30+
description: 'URL of the uploaded recording artifact'
31+
comment-url:
32+
description: 'URL of the posted PR comment'
33+
success:
34+
description: 'Whether the recording succeeded (true/false)'
35+
36+
runs:
37+
using: 'node20'
38+
main: 'dist/index.js'

packages/action/package.json

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{
2+
"name": "@git-glimpse/action",
3+
"version": "0.1.0",
4+
"description": "GitHub Action for git-glimpse",
5+
"license": "MIT",
6+
"type": "module",
7+
"main": "./dist/index.js",
8+
"scripts": {
9+
"build": "tsc -p tsconfig.json",
10+
"typecheck": "tsc --noEmit",
11+
"test": "vitest run"
12+
},
13+
"dependencies": {
14+
"@actions/core": "^1.10.1",
15+
"@actions/github": "^6.0.0",
16+
"@git-glimpse/core": "workspace:*"
17+
},
18+
"devDependencies": {
19+
"typescript": "^5.4.5",
20+
"vitest": "^1.6.0",
21+
"@types/node": "^20.12.0"
22+
}
23+
}

packages/action/tsconfig.json

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"compilerOptions": {
4+
"rootDir": "src",
5+
"outDir": "dist"
6+
},
7+
"include": ["src/**/*"]
8+
}

packages/cli/package.json

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
"name": "git-glimpse",
3+
"version": "0.1.0",
4+
"description": "CLI for git-glimpse — auto-generate visual demo clips for PR changes",
5+
"license": "MIT",
6+
"type": "module",
7+
"bin": {
8+
"git-glimpse": "./dist/index.js"
9+
},
10+
"main": "./dist/index.js",
11+
"scripts": {
12+
"build": "tsc -p tsconfig.json",
13+
"typecheck": "tsc --noEmit",
14+
"test": "vitest run"
15+
},
16+
"dependencies": {
17+
"@git-glimpse/core": "workspace:*",
18+
"commander": "^12.1.0"
19+
},
20+
"devDependencies": {
21+
"typescript": "^5.4.5",
22+
"vitest": "^1.6.0",
23+
"@types/node": "^20.12.0"
24+
}
25+
}

packages/cli/src/index.ts

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,126 @@
1+
#!/usr/bin/env node
2+
import { program } from 'commander';
3+
import { loadConfig, runPipeline } from '@git-glimpse/core';
4+
import { execSync, execFile } from 'node:child_process';
5+
import { readFileSync, existsSync, writeFileSync } from 'node:fs';
6+
import { createRequire } from 'node:module';
7+
import { resolve } from 'node:path';
8+
9+
const require = createRequire(import.meta.url);
10+
const pkg = require('../../package.json') as { version: string };
11+
12+
program
13+
.name('git-glimpse')
14+
.description('Auto-generate visual demo clips of UI changes')
15+
.version(pkg.version);
16+
17+
program
18+
.command('run')
19+
.description('Generate a demo clip for the current working tree changes')
20+
.option('-d, --diff <diff>', 'Git ref or diff (e.g. HEAD~1, main..HEAD)')
21+
.option('-u, --url <url>', 'Base URL of the running app')
22+
.option('-c, --config <path>', 'Path to git-glimpse.config.ts')
23+
.option('-o, --output <dir>', 'Output directory for recordings', './recordings')
24+
.option('--open', 'Open the recording after generation')
25+
.action(async (options) => {
26+
try {
27+
const config = await loadConfig(options.config);
28+
29+
// Get diff
30+
const diffRef = options.diff ?? 'HEAD~1';
31+
let diff: string;
32+
if (existsSync(diffRef)) {
33+
diff = readFileSync(diffRef, 'utf-8');
34+
} else {
35+
// diffRef is a git ref like HEAD~1, safe to pass as argument
36+
diff = execSync(`git diff ${diffRef}`, { encoding: 'utf-8' });
37+
}
38+
39+
if (!diff.trim()) {
40+
console.error('No diff found. Did you forget to commit? Try: git-glimpse run --diff HEAD~1');
41+
process.exit(1);
42+
}
43+
44+
const baseUrl =
45+
options.url ??
46+
config.app.readyWhen?.url?.replace(/\/[^/]*$/, '') ??
47+
'http://localhost:3000';
48+
49+
console.log(`Running git-glimpse...`);
50+
console.log(` Base URL: ${baseUrl}`);
51+
console.log(` Diff: ${diffRef}`);
52+
console.log(` Output: ${options.output}`);
53+
54+
const result = await runPipeline({
55+
diff,
56+
baseUrl,
57+
outputDir: options.output,
58+
config,
59+
});
60+
61+
if (result.success && result.recording) {
62+
console.log(`\n✓ Demo recorded: ${result.recording.path}`);
63+
console.log(` Duration: ${result.recording.duration.toFixed(1)}s`);
64+
console.log(` Size: ${result.recording.sizeMB.toFixed(1)} MB`);
65+
console.log(`\nWhat changed: ${result.analysis.changeDescription}`);
66+
67+
if (options.open) {
68+
openFile(result.recording.path);
69+
}
70+
} else {
71+
console.warn('\n⚠ Recording failed, screenshots taken as fallback.');
72+
if (result.screenshots?.length) {
73+
console.log('Screenshots:', result.screenshots.join(', '));
74+
}
75+
if (result.errors.length) {
76+
console.error('Errors:', result.errors.join('\n'));
77+
}
78+
}
79+
} catch (err) {
80+
console.error('Error:', err instanceof Error ? err.message : String(err));
81+
process.exit(1);
82+
}
83+
});
84+
85+
program
86+
.command('init')
87+
.description('Create a git-glimpse.config.ts in the current directory')
88+
.action(async () => {
89+
const configPath = resolve(process.cwd(), 'git-glimpse.config.ts');
90+
if (existsSync(configPath)) {
91+
console.error('git-glimpse.config.ts already exists.');
92+
process.exit(1);
93+
}
94+
95+
const template = `import type { GitGlimpseConfig } from '@git-glimpse/core';
96+
97+
export default {
98+
app: {
99+
startCommand: 'npm run dev',
100+
readyWhen: { url: 'http://localhost:3000' },
101+
},
102+
recording: {
103+
format: 'gif',
104+
maxDuration: 30,
105+
},
106+
llm: {
107+
provider: 'anthropic',
108+
model: 'claude-sonnet-4-6',
109+
},
110+
} satisfies GitGlimpseConfig;
111+
`;
112+
113+
writeFileSync(configPath, template, 'utf-8');
114+
console.log('Created git-glimpse.config.ts');
115+
console.log('Next: set ANTHROPIC_API_KEY and run: npx git-glimpse run --diff HEAD~1');
116+
});
117+
118+
program.parse();
119+
120+
function openFile(filePath: string): void {
121+
const openCmd =
122+
process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'explorer' : 'xdg-open';
123+
execFile(openCmd, [filePath], (err) => {
124+
if (err) console.warn(`Could not open file: ${err.message}`);
125+
});
126+
}

0 commit comments

Comments
 (0)