Add project scaffolding and build configuration

Initialize the session-viewer project — a tool for browsing, filtering,
redacting, and exporting Claude Code session logs as self-contained HTML.

Scaffolding includes:
- package.json with Express server + React client dual-stack setup,
  dev/build/test/lint/typecheck scripts, and a CLI bin entry point
- TypeScript configs: base tsconfig.json (ESNext, bundler resolution,
  strict, react-jsx) and tsconfig.server.json extending it for the
  Express server compilation target
- Vite config: React plugin, Tailscale-aware dev server on :3847 with
  API proxy to :3848, client build output to dist/client
- Vitest config: node environment, test discovery from tests/unit and
  src/client/components
- ESLint flat config: typescript-eslint recommended, unused-vars with
  underscore exception
- Tailwind CSS config scoped to src/client, PostCSS with autoprefixer
- Playwright config: Chromium-only E2E against dev server
- bin/session-viewer.js: CLI entry point that re-execs via tsx with
  browser-open flag

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-29 22:55:31 -05:00
commit 7e15c36e2f
10 changed files with 217 additions and 0 deletions

24
bin/session-viewer.js Normal file
View File

@@ -0,0 +1,24 @@
#!/usr/bin/env node
// This bin script needs tsx to run TypeScript source directly.
// Use --import to register the tsx loader before importing our TS entry.
import { execFileSync } from "child_process";
import { fileURLToPath } from "url";
import path from "path";
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const entryPoint = path.resolve(__dirname, "../src/server/index.ts");
// Re-exec with tsx if not already running under it
try {
execFileSync(
"npx",
["tsx", entryPoint],
{
stdio: "inherit",
env: { ...process.env, SESSION_VIEWER_OPEN_BROWSER: "1" },
}
);
} catch {
process.exit(1);
}

18
eslint.config.js Normal file
View File

@@ -0,0 +1,18 @@
import js from "@eslint/js";
import tseslint from "typescript-eslint";
export default tseslint.config(
js.configs.recommended,
...tseslint.configs.recommended,
{
ignores: ["dist/**", "node_modules/**", "bin/**"],
},
{
rules: {
"@typescript-eslint/no-unused-vars": [
"error",
{ argsIgnorePattern: "^_" },
],
},
}
);

54
package.json Normal file
View File

@@ -0,0 +1,54 @@
{
"name": "session-viewer",
"version": "1.0.0",
"description": "Browse, filter, redact, and export Claude Code sessions as self-contained HTML",
"type": "module",
"bin": {
"session-viewer": "./bin/session-viewer.js"
},
"scripts": {
"dev": "concurrently \"npm run dev:server\" \"npm run dev:client\"",
"dev:server": "tsx watch src/server/index.ts",
"dev:client": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest run",
"test:watch": "vitest",
"test:e2e": "playwright test",
"typecheck": "tsc --noEmit",
"lint": "eslint . --ext .ts,.tsx"
},
"dependencies": {
"express": "^4.21.0",
"highlight.js": "^11.10.0",
"marked": "^14.0.0",
"marked-highlight": "^2.2.3",
"open": "^10.1.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
"devDependencies": {
"@eslint/js": "^9.0.0",
"@playwright/test": "^1.48.0",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/express": "^4.17.21",
"@types/node": "^22.0.0",
"@types/react": "^18.3.0",
"@types/react-dom": "^18.3.0",
"@types/supertest": "^6.0.0",
"@vitejs/plugin-react": "^4.3.0",
"autoprefixer": "^10.4.20",
"concurrently": "^9.0.0",
"eslint": "^9.0.0",
"jsdom": "^27.4.0",
"postcss": "^8.4.47",
"supertest": "^7.0.0",
"tailwindcss": "^3.4.13",
"tsx": "^4.19.0",
"typescript": "^5.6.0",
"typescript-eslint": "^8.0.0",
"vite": "^5.4.0",
"vitest": "^2.1.0"
}
}

26
playwright.config.ts Normal file
View File

@@ -0,0 +1,26 @@
import { defineConfig, devices } from "@playwright/test";
export default defineConfig({
testDir: "./tests/e2e",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: "html",
use: {
baseURL: "http://localhost:3847",
trace: "on-first-retry",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
webServer: {
command: "npm run dev",
url: "http://localhost:3847",
reuseExistingServer: !process.env.CI,
timeout: 30000,
},
});

6
postcss.config.js Normal file
View File

@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};

8
tailwind.config.js Normal file
View File

@@ -0,0 +1,8 @@
/** @type {import('tailwindcss').Config} */
export default {
content: ["./src/client/**/*.{html,tsx,ts}"],
theme: {
extend: {},
},
plugins: [],
};

23
tsconfig.json Normal file
View File

@@ -0,0 +1,23 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"outDir": "dist",
"rootDir": ".",
"baseUrl": ".",
"paths": {
"@shared/*": ["src/shared/*"]
}
},
"include": ["src/**/*", "tests/**/*", "vite.config.ts", "vitest.config.ts", "playwright.config.ts"],
"exclude": ["node_modules", "dist"]
}

10
tsconfig.server.json Normal file
View File

@@ -0,0 +1,10 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "dist/server",
"rootDir": "src"
},
"include": ["src/server/**/*", "src/shared/**/*"]
}

30
vite.config.ts Normal file
View File

@@ -0,0 +1,30 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import path from "path";
export default defineConfig({
plugins: [react()],
root: "src/client",
resolve: {
alias: {
"@shared": path.resolve(__dirname, "src/shared"),
},
},
server: {
port: 3847,
// Vite only supports one host. Use Tailscale IP so it's reachable
// from both the local machine (via the TS IP) and the tailnet.
// localhost:3847 won't work for the Vite dev server — use the TS IP.
host: "100.84.4.113",
proxy: {
"/api": {
target: "http://127.0.0.1:3848",
changeOrigin: true,
},
},
},
build: {
outDir: "../../dist/client",
emptyOutDir: true,
},
});

18
vitest.config.ts Normal file
View File

@@ -0,0 +1,18 @@
import { defineConfig } from "vitest/config";
import path from "path";
export default defineConfig({
resolve: {
alias: {
"@shared": path.resolve(__dirname, "src/shared"),
},
},
test: {
globals: true,
environment: "node",
include: [
"tests/unit/**/*.test.ts",
"src/client/components/**/*.test.tsx",
],
},
});