init-frontend
Bootstrap a new frontend project with craft principles: React + Vite + TypeScript + Vitest + clean architecture
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Bootstrap a new frontend project with craft principles: React + Vite + TypeScript + Vitest + clean architecture
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Craft something. Claude orchestrates, agents execute.
Start a Clean Claude agent with optional reactive links. Examples: /agent frontend-engineer, /agent frontend-engineer --link qa-engineer, /agent architect --link frontend-engineer,qa-engineer
Auto-repair with smart routing: test failures → Dev, type errors → Architect, spec gaps → PO. Routes each problem to the right expert.
Re-run stack detection and skill generation. Use when stack evolved or on first run.
Set up the Clean Claude Reactive System in the current project. Configures hooks, shared state, and scripts for the multi-agent feedback loop
Add specialized craft skills to agents. Default craft principles always active. Everything MUST respect the craft philosophy.
| name | init-frontend |
| description | Bootstrap a new frontend project with craft principles: React + Vite + TypeScript + Vitest + clean architecture |
| user-invocable | false |
| context | fork |
| agent | architect |
| allowed-tools | Read, Grep, Glob, Edit, Write, Bash |
You are bootstrapping a new frontend project following craftsmanship principles.
When initializing a new frontend project, follow these steps:
npm create vite@latest PROJECT_NAME -- --template react-ts
cd PROJECT_NAME
npm install
Update tsconfig.json:
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx",
/* Strict Type Checking */
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": true,
/* Code Quality */
"noUnusedLocals": true,
"noUnusedParameters": true,
/* Path Aliases */
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@hooks/*": ["src/hooks/*"],
"@lib/*": ["src/lib/*"],
"@types/*": ["src/types/*"]
}
},
"include": ["src"],
"references": [{ "path": "./tsconfig.node.json" }]
}
# Testing
npm install -D vitest @testing-library/react @testing-library/jest-dom @testing-library/user-event jsdom @vitest/coverage-v8
# API & State
npm install @tanstack/react-query zod
# Development
npm install -D @types/node
Update vite.config.ts:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
import path from 'path'
export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@hooks': path.resolve(__dirname, './src/hooks'),
'@lib': path.resolve(__dirname, './src/lib'),
'@types': path.resolve(__dirname, './src/types'),
},
},
test: {
globals: true,
environment: 'jsdom',
setupFiles: ['./src/test/setup.ts'],
include: ['src/**/*.{test,spec}.{ts,tsx}'],
coverage: {
provider: 'v8',
reporter: ['text', 'html'],
exclude: [
'node_modules/',
'src/test/',
'**/*.d.ts',
'**/*.config.*',
'**/index.ts',
],
},
},
})
Create src/test/setup.ts:
import '@testing-library/jest-dom/vitest'
import { cleanup } from '@testing-library/react'
import { afterEach } from 'vitest'
// Cleanup after each test
afterEach(() => {
cleanup()
})
src/
├── components/
│ ├── ui/ # Generic, reusable UI components
│ │ └── Button/
│ │ ├── Button.tsx
│ │ ├── Button.test.tsx
│ │ └── index.ts
│ └── features/ # Feature-specific components
│ └── .gitkeep
├── hooks/ # Custom React hooks
│ └── index.ts
├── lib/ # Utilities, helpers, API clients
│ ├── api/
│ │ └── client.ts
│ └── utils/
│ └── index.ts
├── types/ # Shared TypeScript types
│ ├── common.ts
│ └── index.ts
├── test/ # Test utilities and setup
│ ├── setup.ts
│ ├── test-utils.tsx
│ └── factories/
│ └── index.ts
├── App.tsx
├── main.tsx
└── index.css
Create src/types/common.ts:
/**
* Brand type for nominal typing
*/
export type Brand<T, B> = T & { __brand: B }
/**
* Result type for explicit error handling
*/
export type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E }
/**
* Async data states for loading UI
*/
export type AsyncData<T, E = Error> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: E }
/**
* Helper to create success result
*/
export const ok = <T>(value: T): Result<T, never> => ({ ok: true, value })
/**
* Helper to create error result
*/
export const err = <E>(error: E): Result<never, E> => ({ ok: false, error })
Create src/types/index.ts:
export * from './common'
Create src/test/test-utils.tsx:
import { render, RenderOptions } from '@testing-library/react'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { ReactElement, ReactNode } from 'react'
// Create a fresh QueryClient for each test
function createTestQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
retry: false,
gcTime: 0,
},
},
})
}
type WrapperProps = {
children: ReactNode
}
function AllProviders({ children }: WrapperProps) {
const queryClient = createTestQueryClient()
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
function customRender(
ui: ReactElement,
options?: Omit<RenderOptions, 'wrapper'>
) {
return render(ui, { wrapper: AllProviders, ...options })
}
// Re-export everything from testing-library
export * from '@testing-library/react'
export { userEvent } from '@testing-library/user-event'
export { customRender as render }
Create src/components/ui/Button/Button.tsx:
import { ButtonHTMLAttributes, forwardRef } from 'react'
type ButtonVariant = 'primary' | 'secondary' | 'ghost'
type ButtonSize = 'sm' | 'md' | 'lg'
interface ButtonProps extends ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant
size?: ButtonSize
isLoading?: boolean
}
export const Button = forwardRef<HTMLButtonElement, ButtonProps>(
(
{
variant = 'primary',
size = 'md',
isLoading = false,
disabled,
children,
className = '',
...props
},
ref
) => {
const baseStyles = 'inline-flex items-center justify-center font-medium rounded-md transition-colors focus:outline-none focus:ring-2 focus:ring-offset-2'
const variantStyles: Record<ButtonVariant, string> = {
primary: 'bg-blue-600 text-white hover:bg-blue-700 focus:ring-blue-500',
secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300 focus:ring-gray-500',
ghost: 'bg-transparent text-gray-700 hover:bg-gray-100 focus:ring-gray-500',
}
const sizeStyles: Record<ButtonSize, string> = {
sm: 'px-3 py-1.5 text-sm',
md: 'px-4 py-2 text-base',
lg: 'px-6 py-3 text-lg',
}
return (
<button
ref={ref}
className={`${baseStyles} ${variantStyles[variant]} ${sizeStyles[size]} ${className}`}
disabled={disabled || isLoading}
aria-busy={isLoading}
{...props}
>
{isLoading ? (
<>
<span className="sr-only">Loading...</span>
<svg
className="animate-spin -ml-1 mr-2 h-4 w-4"
fill="none"
viewBox="0 0 24 24"
aria-hidden="true"
>
<circle
className="opacity-25"
cx="12"
cy="12"
r="10"
stroke="currentColor"
strokeWidth="4"
/>
<path
className="opacity-75"
fill="currentColor"
d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"
/>
</svg>
</>
) : null}
{children}
</button>
)
}
)
Button.displayName = 'Button'
Create src/components/ui/Button/Button.test.tsx:
import { describe, it, expect, vi } from 'vitest'
import { render, screen } from '@/test/test-utils'
import userEvent from '@testing-library/user-event'
import { Button } from './Button'
describe('Button', () => {
it('should render children', () => {
render(<Button>Click me</Button>)
expect(screen.getByRole('button', { name: /click me/i })).toBeInTheDocument()
})
it('should call onClick when clicked', async () => {
const handleClick = vi.fn()
render(<Button onClick={handleClick}>Click me</Button>)
await userEvent.click(screen.getByRole('button'))
expect(handleClick).toHaveBeenCalledTimes(1)
})
it('should be disabled when isLoading is true', () => {
render(<Button isLoading>Submit</Button>)
expect(screen.getByRole('button')).toBeDisabled()
})
it('should indicate loading state to screen readers', () => {
render(<Button isLoading>Submit</Button>)
expect(screen.getByRole('button')).toHaveAttribute('aria-busy', 'true')
})
it('should not call onClick when disabled', async () => {
const handleClick = vi.fn()
render(<Button disabled onClick={handleClick}>Click me</Button>)
await userEvent.click(screen.getByRole('button'))
expect(handleClick).not.toHaveBeenCalled()
})
})
Create src/components/ui/Button/index.ts:
export { Button } from './Button'
{
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview",
"test": "vitest",
"test:run": "vitest run",
"test:coverage": "vitest run --coverage",
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0",
"typecheck": "tsc --noEmit"
}
}
# Dependencies
node_modules
# Build
dist
dist-ssr
# IDE
.idea
.vscode/*
!.vscode/extensions.json
# Testing
coverage
# Environment
.env
.env.local
.env.*.local
# Logs
*.log
npm-debug.log*
# OS
.DS_Store
Thumbs.db
After setup, verify everything works:
# Type check
npm run typecheck
# Run tests
npm run test:run
# Start dev server
npm run dev
Based on project needs, consider adding:
npm install -D eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react-hooks eslint-plugin-react-refresh prettier eslint-config-prettier
npm install -D tailwindcss postcss autoprefixer
npx tailwindcss init -p
npm install react-router-dom
npm install -D msw
The project is now set up with: