From 4dd42786778d4e7dec775151b4b41cd44c3484e2 Mon Sep 17 00:00:00 2001 From: barsa Date: Tue, 13 Jan 2026 14:39:33 +0900 Subject: [PATCH] Add Windows CMD bootstrap script for Claude Code installation --- CLAUDE.md | 196 +++++++++++++++++++++++++++++++++++++++++++++++ install.cmd | 216 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 412 insertions(+) create mode 100644 CLAUDE.md create mode 100644 install.cmd diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..276d0dd2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,196 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Customer portal with BFF (Backend for Frontend) architecture. Users can self-register, manage subscriptions, view/pay invoices, and manage support cases. + +**Systems of Record:** + +- **WHMCS**: Billing, subscriptions, invoices, authoritative address storage +- **Salesforce**: CRM (Accounts, Contacts, Cases), order address snapshots +- **Portal**: Next.js UI + NestJS BFF + +## Development Commands + +```bash +# Start development environment (PostgreSQL + Redis via Docker) +pnpm dev:start + +# Start both frontend and backend with hot reload +pnpm dev + +# Build domain package (required before running apps if domain changed) +pnpm domain:build + +# Type checking +pnpm type-check + +# Linting +pnpm lint +pnpm lint:fix + +# Database commands +pnpm db:migrate # Run migrations +pnpm db:studio # Open Prisma Studio GUI +pnpm db:generate # Generate Prisma client + +# Run tests +pnpm test # All packages +pnpm --filter @customer-portal/bff test # BFF only + +# Stop services +pnpm dev:stop +``` + +**Access points:** + +- Frontend: http://localhost:3000 +- Backend API: http://localhost:4000/api +- Prisma Studio: http://localhost:5555 + +## Architecture + +### Monorepo Structure + +``` +apps/ +├── portal/ # Next.js 15 frontend (React 19, Tailwind, shadcn/ui) +└── bff/ # NestJS 11 backend (Prisma, BullMQ, Zod validation) + +packages/ +└── domain/ # Unified domain layer (contracts, schemas, provider mappers) +``` + +### Three-Layer Boundary (Non-Negotiable) + +| Layer | Location | Purpose | +| ------ | ------------------ | ------------------------------------------------------------------------------ | +| Domain | `packages/domain/` | Shared contracts, Zod validation, provider mappers. Framework-agnostic. | +| BFF | `apps/bff/` | HTTP boundary, orchestration, external integrations (Salesforce/WHMCS/Freebit) | +| Portal | `apps/portal/` | UI layer. Pages are thin wrappers over feature modules. | + +### Domain Package Structure + +Each domain module follows this pattern: + +``` +packages/domain// +├── contract.ts # Normalized types (provider-agnostic) +├── schema.ts # Zod validation schemas +├── index.ts # Public exports +└── providers/ # Provider-specific adapters (BFF-only) + └── whmcs/ + ├── raw.types.ts # Raw API response types + └── mapper.ts # Transform raw → domain +``` + +### Import Rules (ESLint Enforced) + +**Allowed (Portal + BFF):** + +```typescript +import type { Invoice } from "@customer-portal/domain/billing"; +import { invoiceSchema } from "@customer-portal/domain/billing"; +import { Formatting } from "@customer-portal/domain/toolkit"; +``` + +**Allowed (BFF only):** + +```typescript +import { Whmcs } from "@customer-portal/domain/billing/providers"; +``` + +**Forbidden everywhere:** + +```typescript +// Root import +import { Billing } from "@customer-portal/domain"; +// Deep imports beyond entrypoints +import { Invoice } from "@customer-portal/domain/billing/contract"; +import { transformWhmcsInvoice } from "@customer-portal/domain/billing/providers/whmcs/mapper"; +``` + +**Forbidden in Portal:** + +```typescript +// Portal must NEVER import provider adapters +import { Whmcs } from "@customer-portal/domain/billing/providers"; +``` + +### Portal Feature Architecture + +``` +apps/portal/src/ +├── app/ # Next.js App Router (thin route shells, no API calls) +├── components/ # Atomic design: atoms/, molecules/, organisms/, templates/ +├── core/ # App infrastructure: api/, logger/, providers/ +├── features/ # Feature modules with: api/, stores/, components/, hooks/, views/ +└── shared/ # Cross-feature: hooks/, utils/, constants/ +``` + +**Feature module pattern:** + +- `api/`: Data fetching layer (built on shared apiClient) +- `stores/`: Zustand state management +- `hooks/`: React Query hooks wrapping API services +- `components/`: Feature-specific UI +- `views/`: Page-level view components +- `index.ts`: Feature public API (barrel exports) + +### BFF Integration Pattern + +**Map Once, Use Everywhere:** + +``` +External API → Integration Service → Domain Mapper → Domain Type → Use Directly +``` + +Integration services live in `apps/bff/src/integrations/{provider}/`: + +- `services/`: Connection services, entity-specific services +- `utils/`: Query builders (SOQL, etc.) + +Domain mappers live in `packages/domain/{module}/providers/{provider}/`: + +- Integration services fetch data and call domain mappers +- No business logic in integration layer +- No double transformation + +## Key Patterns + +### Validation (Zod-First) + +- Schemas live in domain: `packages/domain//schema.ts` +- Derive types from schemas: `export type X = z.infer` +- Query params: use `z.coerce.*` for URL strings + +### BFF Controllers + +- Controllers are thin: no business logic, no Zod imports +- Use `createZodDto(schema)` with global `ZodValidationPipe` +- Integrations: build queries in utils, fetch data, transform via domain mappers + +### Logging + +- BFF: Use `nestjs-pino` Logger, inject via constructor +- Portal: Use `@/core/logger` +- No `console.log` in production code (ESLint enforced) + +### Naming + +- No `any` in public APIs +- No `console.log` (use logger) +- Avoid `V2` suffix in service names + +## Documentation + +Read before coding: + +- `docs/README.md` (entrypoint) +- `docs/development/` (BFF/Portal/Domain patterns) +- `docs/architecture/` (boundaries) +- `docs/integrations/` (external API details) + +**Rule: Never guess endpoint behavior or payload shapes. Find docs or existing implementation first.** diff --git a/install.cmd b/install.cmd new file mode 100644 index 00000000..19f56cb3 --- /dev/null +++ b/install.cmd @@ -0,0 +1,216 @@ +@echo off +setlocal enabledelayedexpansion + +REM Claude Code Windows CMD Bootstrap Script +REM Installs Claude Code for environments where PowerShell is not available + +REM Parse command line argument +set "TARGET=%~1" +if "!TARGET!"=="" set "TARGET=latest" + +REM Validate target parameter +if /i "!TARGET!"=="stable" goto :target_valid +if /i "!TARGET!"=="latest" goto :target_valid +echo !TARGET! | findstr /r "^[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*" >nul +if !ERRORLEVEL! equ 0 goto :target_valid + +echo Usage: %0 [stable^|latest^|VERSION] >&2 +echo Example: %0 1.0.58 >&2 +exit /b 1 + +:target_valid + +REM Check for 64-bit Windows +if /i "%PROCESSOR_ARCHITECTURE%"=="AMD64" goto :arch_valid +if /i "%PROCESSOR_ARCHITECTURE%"=="ARM64" goto :arch_valid +if /i "%PROCESSOR_ARCHITEW6432%"=="AMD64" goto :arch_valid +if /i "%PROCESSOR_ARCHITEW6432%"=="ARM64" goto :arch_valid + +echo Claude Code does not support 32-bit Windows. Please use a 64-bit version of Windows. >&2 +exit /b 1 + +:arch_valid + +REM Set constants +set "GCS_BUCKET=https://storage.googleapis.com/claude-code-dist-86c565f3-f756-42ad-8dfa-d59b1c096819/claude-code-releases" +set "DOWNLOAD_DIR=%USERPROFILE%\.claude\downloads" +set "PLATFORM=win32-x64" + +REM Create download directory +if not exist "!DOWNLOAD_DIR!" mkdir "!DOWNLOAD_DIR!" + +REM Check for curl availability +curl --version >nul 2>&1 +if !ERRORLEVEL! neq 0 ( + echo curl is required but not available. Please install curl or use PowerShell installer. >&2 + exit /b 1 +) + +REM Always download latest version (which has the most up-to-date installer) +call :download_file "!GCS_BUCKET!/latest" "!DOWNLOAD_DIR!\latest" +if !ERRORLEVEL! neq 0 ( + echo Failed to get latest version >&2 + exit /b 1 +) + +REM Read version from file +set /p VERSION=<"!DOWNLOAD_DIR!\latest" +del "!DOWNLOAD_DIR!\latest" + +REM Download manifest +call :download_file "!GCS_BUCKET!/!VERSION!/manifest.json" "!DOWNLOAD_DIR!\manifest.json" +if !ERRORLEVEL! neq 0 ( + echo Failed to get manifest >&2 + exit /b 1 +) + +REM Extract checksum from manifest +call :parse_manifest "!DOWNLOAD_DIR!\manifest.json" "!PLATFORM!" +if !ERRORLEVEL! neq 0 ( + echo Platform !PLATFORM! not found in manifest >&2 + del "!DOWNLOAD_DIR!\manifest.json" 2>nul + exit /b 1 +) +del "!DOWNLOAD_DIR!\manifest.json" + +REM Download binary +set "BINARY_PATH=!DOWNLOAD_DIR!\claude-!VERSION!-!PLATFORM!.exe" +call :download_file "!GCS_BUCKET!/!VERSION!/!PLATFORM!/claude.exe" "!BINARY_PATH!" +if !ERRORLEVEL! neq 0 ( + echo Failed to download binary >&2 + if exist "!BINARY_PATH!" del "!BINARY_PATH!" + exit /b 1 +) + +REM Verify checksum +call :verify_checksum "!BINARY_PATH!" "!EXPECTED_CHECKSUM!" +if !ERRORLEVEL! neq 0 ( + echo Checksum verification failed >&2 + del "!BINARY_PATH!" + exit /b 1 +) + +REM Run claude install to set up launcher and shell integration +echo Setting up Claude Code... +"!BINARY_PATH!" install "!TARGET!" +set "INSTALL_RESULT=!ERRORLEVEL!" + +REM Clean up downloaded file +REM Wait a moment for any file handles to be released +timeout /t 1 /nobreak >nul 2>&1 +del /f "!BINARY_PATH!" >nul 2>&1 +if exist "!BINARY_PATH!" ( + echo Warning: Could not remove temporary file: !BINARY_PATH! +) + +if !INSTALL_RESULT! neq 0 ( + echo Installation failed >&2 + exit /b 1 +) + +echo. +echo Installation complete^^! +echo. +exit /b 0 + +REM ============================================================================ +REM SUBROUTINES +REM ============================================================================ + +:download_file +REM Downloads a file using curl +REM Args: %1=URL, %2=OutputPath +set "URL=%~1" +set "OUTPUT=%~2" + +curl -fsSL "!URL!" -o "!OUTPUT!" +exit /b !ERRORLEVEL! + +:parse_manifest +REM Parse JSON manifest to extract checksum for platform +REM Args: %1=ManifestPath, %2=Platform +set "MANIFEST_PATH=%~1" +set "PLATFORM_NAME=%~2" +set "EXPECTED_CHECKSUM=" + +REM Use findstr to find platform section, then look for checksum +set "FOUND_PLATFORM=" +set "IN_PLATFORM_SECTION=" + +REM Read the manifest line by line +for /f "usebackq tokens=*" %%i in ("!MANIFEST_PATH!") do ( + set "LINE=%%i" + + REM Check if this line contains our platform + echo !LINE! | findstr /c:"\"%PLATFORM_NAME%\":" >nul + if !ERRORLEVEL! equ 0 ( + set "IN_PLATFORM_SECTION=1" + ) + + REM If we're in the platform section, look for checksum + if defined IN_PLATFORM_SECTION ( + echo !LINE! | findstr /c:"\"checksum\":" >nul + if !ERRORLEVEL! equ 0 ( + REM Extract checksum value + for /f "tokens=2 delims=:" %%j in ("!LINE!") do ( + set "CHECKSUM_PART=%%j" + REM Remove quotes, whitespace, and comma + set "CHECKSUM_PART=!CHECKSUM_PART: =!" + set "CHECKSUM_PART=!CHECKSUM_PART:"=!" + set "CHECKSUM_PART=!CHECKSUM_PART:,=!" + + REM Check if it looks like a SHA256 (64 hex chars) + if not "!CHECKSUM_PART!"=="" ( + call :check_length "!CHECKSUM_PART!" 64 + if !ERRORLEVEL! equ 0 ( + set "EXPECTED_CHECKSUM=!CHECKSUM_PART!" + exit /b 0 + ) + ) + ) + ) + + REM Check if we've left the platform section (closing brace) + echo !LINE! | findstr /c:"}" >nul + if !ERRORLEVEL! equ 0 set "IN_PLATFORM_SECTION=" + ) +) + +if "!EXPECTED_CHECKSUM!"=="" exit /b 1 +exit /b 0 + +:check_length +REM Check if string length equals expected length +REM Args: %1=String, %2=ExpectedLength +set "STR=%~1" +set "EXPECTED_LEN=%~2" +set "LEN=0" +:count_loop +if "!STR:~%LEN%,1!"=="" goto :count_done +set /a LEN+=1 +goto :count_loop +:count_done +if %LEN%==%EXPECTED_LEN% exit /b 0 +exit /b 1 + +:verify_checksum +REM Verify file checksum using certutil +REM Args: %1=FilePath, %2=ExpectedChecksum +set "FILE_PATH=%~1" +set "EXPECTED=%~2" + +for /f "skip=1 tokens=*" %%i in ('certutil -hashfile "!FILE_PATH!" SHA256') do ( + set "ACTUAL=%%i" + set "ACTUAL=!ACTUAL: =!" + if "!ACTUAL!"=="CertUtil:Thecommandcompletedsuccessfully." goto :verify_done + if "!ACTUAL!" neq "" ( + if /i "!ACTUAL!"=="!EXPECTED!" ( + exit /b 0 + ) else ( + exit /b 1 + ) + ) +) + +:verify_done +exit /b 1