commit 7e15c36e2f4197eec078773f6649763dae770df0 Author: teernisse Date: Thu Jan 29 22:55:31 2026 -0500 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 diff --git a/bin/session-viewer.js b/bin/session-viewer.js new file mode 100644 index 0000000..6d6e3a6 --- /dev/null +++ b/bin/session-viewer.js @@ -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); +} diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..9188236 --- /dev/null +++ b/eslint.config.js @@ -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: "^_" }, + ], + }, + } +); diff --git a/package.json b/package.json new file mode 100644 index 0000000..6fe7342 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/playwright.config.ts b/playwright.config.ts new file mode 100644 index 0000000..5fa77aa --- /dev/null +++ b/playwright.config.ts @@ -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, + }, +}); diff --git a/postcss.config.js b/postcss.config.js new file mode 100644 index 0000000..2aa7205 --- /dev/null +++ b/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {}, + }, +}; diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..de50715 --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,8 @@ +/** @type {import('tailwindcss').Config} */ +export default { + content: ["./src/client/**/*.{html,tsx,ts}"], + theme: { + extend: {}, + }, + plugins: [], +}; diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..a901544 --- /dev/null +++ b/tsconfig.json @@ -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"] +} diff --git a/tsconfig.server.json b/tsconfig.server.json new file mode 100644 index 0000000..65be72c --- /dev/null +++ b/tsconfig.server.json @@ -0,0 +1,10 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "bundler", + "outDir": "dist/server", + "rootDir": "src" + }, + "include": ["src/server/**/*", "src/shared/**/*"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..701546a --- /dev/null +++ b/vite.config.ts @@ -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, + }, +}); diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..3fa3f85 --- /dev/null +++ b/vitest.config.ts @@ -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", + ], + }, +});