Add Windows CMD bootstrap script for Claude Code installation
This commit is contained in:
parent
b49c94994d
commit
4dd4278677
196
CLAUDE.md
Normal file
196
CLAUDE.md
Normal file
@ -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/<module>/
|
||||||
|
├── 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/<module>/schema.ts`
|
||||||
|
- Derive types from schemas: `export type X = z.infer<typeof xSchema>`
|
||||||
|
- 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.**
|
||||||
216
install.cmd
Normal file
216
install.cmd
Normal file
@ -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
|
||||||
Loading…
x
Reference in New Issue
Block a user