specrails 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/.claude/skills/openspec-apply-change/SKILL.md +156 -0
  2. package/.claude/skills/openspec-archive-change/SKILL.md +114 -0
  3. package/.claude/skills/openspec-bulk-archive-change/SKILL.md +246 -0
  4. package/.claude/skills/openspec-continue-change/SKILL.md +118 -0
  5. package/.claude/skills/openspec-explore/SKILL.md +290 -0
  6. package/.claude/skills/openspec-ff-change/SKILL.md +101 -0
  7. package/.claude/skills/openspec-new-change/SKILL.md +74 -0
  8. package/.claude/skills/openspec-onboard/SKILL.md +529 -0
  9. package/.claude/skills/openspec-sync-specs/SKILL.md +138 -0
  10. package/.claude/skills/openspec-verify-change/SKILL.md +168 -0
  11. package/README.md +226 -0
  12. package/VERSION +1 -0
  13. package/bin/specrails.js +41 -0
  14. package/commands/setup.md +851 -0
  15. package/install.sh +488 -0
  16. package/package.json +34 -0
  17. package/prompts/analyze-codebase.md +87 -0
  18. package/prompts/generate-personas.md +61 -0
  19. package/prompts/infer-conventions.md +72 -0
  20. package/templates/agents/sr-architect.md +194 -0
  21. package/templates/agents/sr-backend-developer.md +54 -0
  22. package/templates/agents/sr-backend-reviewer.md +139 -0
  23. package/templates/agents/sr-developer.md +146 -0
  24. package/templates/agents/sr-doc-sync.md +167 -0
  25. package/templates/agents/sr-frontend-developer.md +48 -0
  26. package/templates/agents/sr-frontend-reviewer.md +132 -0
  27. package/templates/agents/sr-product-analyst.md +36 -0
  28. package/templates/agents/sr-product-manager.md +148 -0
  29. package/templates/agents/sr-reviewer.md +265 -0
  30. package/templates/agents/sr-security-reviewer.md +178 -0
  31. package/templates/agents/sr-test-writer.md +163 -0
  32. package/templates/claude-md/root.md +50 -0
  33. package/templates/commands/sr/batch-implement.md +282 -0
  34. package/templates/commands/sr/compat-check.md +271 -0
  35. package/templates/commands/sr/health-check.md +396 -0
  36. package/templates/commands/sr/implement.md +972 -0
  37. package/templates/commands/sr/product-backlog.md +195 -0
  38. package/templates/commands/sr/refactor-recommender.md +169 -0
  39. package/templates/commands/sr/update-product-driven-backlog.md +272 -0
  40. package/templates/commands/sr/why.md +96 -0
  41. package/templates/personas/persona.md +43 -0
  42. package/templates/personas/the-maintainer.md +78 -0
  43. package/templates/rules/layer.md +8 -0
  44. package/templates/security/security-exemptions.yaml +20 -0
  45. package/templates/settings/confidence-config.json +17 -0
  46. package/templates/settings/settings.json +15 -0
  47. package/templates/web-manager/README.md +107 -0
  48. package/templates/web-manager/client/index.html +12 -0
  49. package/templates/web-manager/client/package-lock.json +1727 -0
  50. package/templates/web-manager/client/package.json +20 -0
  51. package/templates/web-manager/client/src/App.tsx +83 -0
  52. package/templates/web-manager/client/src/components/AgentActivity.tsx +19 -0
  53. package/templates/web-manager/client/src/components/CommandInput.tsx +81 -0
  54. package/templates/web-manager/client/src/components/LogStream.tsx +57 -0
  55. package/templates/web-manager/client/src/components/PipelineSidebar.tsx +65 -0
  56. package/templates/web-manager/client/src/components/SearchBox.tsx +34 -0
  57. package/templates/web-manager/client/src/hooks/usePipeline.ts +62 -0
  58. package/templates/web-manager/client/src/hooks/useWebSocket.ts +59 -0
  59. package/templates/web-manager/client/src/main.tsx +9 -0
  60. package/templates/web-manager/client/tsconfig.json +21 -0
  61. package/templates/web-manager/client/tsconfig.node.json +11 -0
  62. package/templates/web-manager/client/vite.config.ts +13 -0
  63. package/templates/web-manager/package-lock.json +3327 -0
  64. package/templates/web-manager/package.json +30 -0
  65. package/templates/web-manager/server/hooks.test.ts +196 -0
  66. package/templates/web-manager/server/hooks.ts +71 -0
  67. package/templates/web-manager/server/index.test.ts +186 -0
  68. package/templates/web-manager/server/index.ts +99 -0
  69. package/templates/web-manager/server/spawner.test.ts +319 -0
  70. package/templates/web-manager/server/spawner.ts +89 -0
  71. package/templates/web-manager/server/types.ts +46 -0
  72. package/templates/web-manager/tsconfig.json +14 -0
  73. package/templates/web-manager/vitest.config.ts +8 -0
  74. package/update.sh +877 -0
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "specrails-web-client",
3
+ "private": true,
4
+ "scripts": {
5
+ "dev": "vite",
6
+ "build": "tsc && vite build",
7
+ "preview": "vite preview"
8
+ },
9
+ "dependencies": {
10
+ "react": "^18.3.0",
11
+ "react-dom": "^18.3.0"
12
+ },
13
+ "devDependencies": {
14
+ "@types/react": "^18.3.0",
15
+ "@types/react-dom": "^18.3.0",
16
+ "@vitejs/plugin-react": "^4.3.0",
17
+ "typescript": "^5.4.0",
18
+ "vite": "^5.3.0"
19
+ }
20
+ }
@@ -0,0 +1,83 @@
1
+ import { usePipeline } from './hooks/usePipeline'
2
+ import { PipelineSidebar } from './components/PipelineSidebar'
3
+ import { AgentActivity } from './components/AgentActivity'
4
+ import { CommandInput } from './components/CommandInput'
5
+
6
+ const GRID_STYLE: React.CSSProperties = {
7
+ display: 'grid',
8
+ gridTemplateAreas: '"header header" "sidebar activity"',
9
+ gridTemplateColumns: '240px 1fr',
10
+ gridTemplateRows: '48px 1fr',
11
+ height: '100vh',
12
+ backgroundColor: '#0f172a',
13
+ color: '#e2e8f0',
14
+ fontFamily: 'system-ui, sans-serif',
15
+ margin: 0,
16
+ overflow: 'hidden',
17
+ }
18
+
19
+ export default function App() {
20
+ const { phases, projectName, logLines, connectionStatus } = usePipeline()
21
+
22
+ return (
23
+ <>
24
+ <style>{`
25
+ *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
26
+ body { background: #0f172a; }
27
+ ::-webkit-scrollbar { width: 6px; }
28
+ ::-webkit-scrollbar-track { background: #0f172a; }
29
+ ::-webkit-scrollbar-thumb { background: #334155; border-radius: 3px; }
30
+ `}</style>
31
+ <div style={GRID_STYLE}>
32
+ {/* Header */}
33
+ <header
34
+ style={{
35
+ gridArea: 'header',
36
+ display: 'flex',
37
+ alignItems: 'center',
38
+ justifyContent: 'space-between',
39
+ padding: '0 16px',
40
+ borderBottom: '1px solid #1e293b',
41
+ fontSize: 14,
42
+ fontWeight: 600,
43
+ }}
44
+ >
45
+ <span>specrails manager</span>
46
+ <span style={{ color: '#64748b', fontSize: 12 }}>{projectName}</span>
47
+ </header>
48
+
49
+ {/* Left sidebar */}
50
+ <aside
51
+ style={{
52
+ gridArea: 'sidebar',
53
+ display: 'flex',
54
+ flexDirection: 'column',
55
+ justifyContent: 'space-between',
56
+ borderRight: '1px solid #1e293b',
57
+ overflow: 'hidden',
58
+ }}
59
+ >
60
+ <PipelineSidebar phases={phases} />
61
+ <CommandInput />
62
+ </aside>
63
+
64
+ {/* Main activity panel */}
65
+ <main style={{ gridArea: 'activity', overflow: 'hidden', display: 'flex', flexDirection: 'column' }}>
66
+ {connectionStatus === 'disconnected' && (
67
+ <div
68
+ style={{
69
+ background: '#7f1d1d',
70
+ color: '#fca5a5',
71
+ padding: '8px 16px',
72
+ fontSize: 13,
73
+ }}
74
+ >
75
+ Disconnected from server. Check that the web manager is running.
76
+ </div>
77
+ )}
78
+ <AgentActivity logLines={logLines} />
79
+ </main>
80
+ </div>
81
+ </>
82
+ )
83
+ }
@@ -0,0 +1,19 @@
1
+ import { useState } from 'react'
2
+ import { LogStream } from './LogStream'
3
+ import { SearchBox } from './SearchBox'
4
+ import type { LogLine } from '../hooks/usePipeline'
5
+
6
+ interface AgentActivityProps {
7
+ logLines: LogLine[]
8
+ }
9
+
10
+ export function AgentActivity({ logLines }: AgentActivityProps) {
11
+ const [filterText, setFilterText] = useState('')
12
+
13
+ return (
14
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%', overflow: 'hidden' }}>
15
+ <SearchBox value={filterText} onChange={setFilterText} />
16
+ <LogStream lines={logLines} filterText={filterText} />
17
+ </div>
18
+ )
19
+ }
@@ -0,0 +1,81 @@
1
+ import { useState, KeyboardEvent } from 'react'
2
+
3
+ export function CommandInput() {
4
+ const [command, setCommand] = useState('')
5
+ const [isLoading, setIsLoading] = useState(false)
6
+ const [errorMessage, setErrorMessage] = useState<string | null>(null)
7
+
8
+ async function handleRun() {
9
+ if (!command.trim() || isLoading) return
10
+ setIsLoading(true)
11
+ setErrorMessage(null)
12
+
13
+ try {
14
+ const res = await fetch('/api/spawn', {
15
+ method: 'POST',
16
+ headers: { 'Content-Type': 'application/json' },
17
+ body: JSON.stringify({ command }),
18
+ })
19
+
20
+ if (res.ok) {
21
+ setCommand('')
22
+ } else {
23
+ const body = await res.json().catch(() => ({}))
24
+ const msg = (body as { error?: string }).error ?? (res.status === 409 ? 'A process is already running' : 'Failed to start process')
25
+ setErrorMessage(msg)
26
+ }
27
+ } catch {
28
+ setErrorMessage('Failed to connect to server')
29
+ } finally {
30
+ setIsLoading(false)
31
+ }
32
+ }
33
+
34
+ function handleKeyDown(e: KeyboardEvent<HTMLInputElement>) {
35
+ if (e.key === 'Enter') handleRun()
36
+ }
37
+
38
+ return (
39
+ <div style={{ padding: '12px', borderTop: '1px solid #1e293b' }}>
40
+ <div style={{ fontSize: 11, fontWeight: 600, color: '#64748b', marginBottom: 8, textTransform: 'uppercase', letterSpacing: '0.05em' }}>
41
+ Actions
42
+ </div>
43
+ <div style={{ display: 'flex', gap: 6 }}>
44
+ <input
45
+ type="text"
46
+ value={command}
47
+ onChange={(e) => setCommand(e.target.value)}
48
+ onKeyDown={handleKeyDown}
49
+ placeholder="Enter command (e.g., /implement #42)"
50
+ style={{
51
+ flex: 1,
52
+ background: '#1e293b',
53
+ border: '1px solid #334155',
54
+ borderRadius: 4,
55
+ color: '#e2e8f0',
56
+ padding: '6px 8px',
57
+ fontSize: 13,
58
+ }}
59
+ />
60
+ <button
61
+ onClick={handleRun}
62
+ disabled={!command.trim() || isLoading}
63
+ style={{
64
+ background: isLoading || !command.trim() ? '#334155' : '#3b82f6',
65
+ color: '#e2e8f0',
66
+ border: 'none',
67
+ borderRadius: 4,
68
+ padding: '6px 14px',
69
+ fontSize: 13,
70
+ cursor: isLoading || !command.trim() ? 'not-allowed' : 'pointer',
71
+ }}
72
+ >
73
+ {isLoading ? 'Running...' : 'Run'}
74
+ </button>
75
+ </div>
76
+ {errorMessage && (
77
+ <div style={{ color: '#ef4444', fontSize: 12, marginTop: 6 }}>{errorMessage}</div>
78
+ )}
79
+ </div>
80
+ )
81
+ }
@@ -0,0 +1,57 @@
1
+ import { useEffect, useRef } from 'react'
2
+ import type { LogLine } from '../hooks/usePipeline'
3
+
4
+ interface LogStreamProps {
5
+ lines: LogLine[]
6
+ filterText: string
7
+ }
8
+
9
+ export function LogStream({ lines, filterText }: LogStreamProps) {
10
+ const bottomRef = useRef<HTMLDivElement>(null)
11
+ const containerRef = useRef<HTMLDivElement>(null)
12
+ const userScrolledRef = useRef(false)
13
+
14
+ const filtered = filterText
15
+ ? lines.filter((l) => l.line.toLowerCase().includes(filterText.toLowerCase()))
16
+ : lines
17
+
18
+ useEffect(() => {
19
+ const container = containerRef.current
20
+ if (!container || userScrolledRef.current) return
21
+ container.scrollTop = container.scrollHeight
22
+ }, [filtered.length])
23
+
24
+ function handleScroll() {
25
+ const el = containerRef.current
26
+ if (!el) return
27
+ const atBottom = el.scrollTop + el.clientHeight >= el.scrollHeight - 20
28
+ userScrolledRef.current = !atBottom
29
+ }
30
+
31
+ return (
32
+ <div
33
+ ref={containerRef}
34
+ onScroll={handleScroll}
35
+ style={{
36
+ flex: 1,
37
+ overflowY: 'auto',
38
+ fontFamily: 'monospace',
39
+ fontSize: 12,
40
+ padding: '8px',
41
+ backgroundColor: '#0f172a',
42
+ }}
43
+ >
44
+ {filtered.map((line, i) => (
45
+ <div key={`${line.processId}-${i}`} style={{ lineHeight: '1.5', whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
46
+ <span style={{ color: '#475569', marginRight: 8 }}>
47
+ {line.timestamp.slice(11, 19)}
48
+ </span>
49
+ <span style={{ color: line.source === 'stderr' ? '#fb923c' : '#e2e8f0' }}>
50
+ {line.line}
51
+ </span>
52
+ </div>
53
+ ))}
54
+ <div ref={bottomRef} />
55
+ </div>
56
+ )
57
+ }
@@ -0,0 +1,65 @@
1
+ import type { PhaseMap } from '../hooks/usePipeline'
2
+
3
+ type PhaseName = 'architect' | 'developer' | 'reviewer' | 'ship'
4
+ type PhaseState = 'idle' | 'running' | 'done' | 'error'
5
+
6
+ const PHASES: { name: PhaseName; label: string }[] = [
7
+ { name: 'architect', label: 'Architect' },
8
+ { name: 'developer', label: 'Developer' },
9
+ { name: 'reviewer', label: 'Reviewer' },
10
+ { name: 'ship', label: 'Ship' },
11
+ ]
12
+
13
+ const STATE_COLORS: Record<PhaseState, string> = {
14
+ idle: '#6b7280',
15
+ running: '#eab308',
16
+ done: '#22c55e',
17
+ error: '#ef4444',
18
+ }
19
+
20
+ interface PipelineSidebarProps {
21
+ phases: PhaseMap
22
+ }
23
+
24
+ export function PipelineSidebar({ phases }: PipelineSidebarProps) {
25
+ return (
26
+ <div style={{ padding: '16px 12px' }}>
27
+ <div style={{ fontSize: 11, fontWeight: 600, color: '#64748b', marginBottom: 12, textTransform: 'uppercase', letterSpacing: '0.05em' }}>
28
+ Pipeline
29
+ </div>
30
+ {PHASES.map((phase, idx) => {
31
+ const state = phases[phase.name]
32
+ const color = STATE_COLORS[state]
33
+ const isRunning = state === 'running'
34
+ return (
35
+ <div key={phase.name}>
36
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, padding: '6px 0' }}>
37
+ <span
38
+ style={{
39
+ width: 10,
40
+ height: 10,
41
+ borderRadius: '50%',
42
+ backgroundColor: color,
43
+ flexShrink: 0,
44
+ animation: isRunning ? 'pulse 1.5s ease-in-out infinite' : 'none',
45
+ }}
46
+ />
47
+ <span style={{ fontSize: 13, color: state === 'idle' ? '#64748b' : '#e2e8f0' }}>
48
+ {phase.label}
49
+ </span>
50
+ </div>
51
+ {idx < PHASES.length - 1 && (
52
+ <div style={{ color: '#334155', fontSize: 12, paddingLeft: 3, lineHeight: 1 }}>↓</div>
53
+ )}
54
+ </div>
55
+ )
56
+ })}
57
+ <style>{`
58
+ @keyframes pulse {
59
+ 0%, 100% { opacity: 1; }
60
+ 50% { opacity: 0.4; }
61
+ }
62
+ `}</style>
63
+ </div>
64
+ )
65
+ }
@@ -0,0 +1,34 @@
1
+ interface SearchBoxProps {
2
+ value: string
3
+ onChange: (value: string) => void
4
+ }
5
+
6
+ export function SearchBox({ value, onChange }: SearchBoxProps) {
7
+ return (
8
+ <div style={{ display: 'flex', alignItems: 'center', padding: '8px', borderBottom: '1px solid #1e293b' }}>
9
+ <input
10
+ type="text"
11
+ value={value}
12
+ onChange={(e) => onChange(e.target.value)}
13
+ placeholder="Search logs..."
14
+ style={{
15
+ flex: 1,
16
+ background: '#1e293b',
17
+ border: '1px solid #334155',
18
+ borderRadius: 4,
19
+ color: '#e2e8f0',
20
+ padding: '4px 8px',
21
+ fontSize: 13,
22
+ }}
23
+ />
24
+ {value && (
25
+ <button
26
+ onClick={() => onChange('')}
27
+ style={{ marginLeft: 6, background: 'none', border: 'none', color: '#94a3b8', cursor: 'pointer', fontSize: 16 }}
28
+ >
29
+ ×
30
+ </button>
31
+ )}
32
+ </div>
33
+ )
34
+ }
@@ -0,0 +1,62 @@
1
+ import { useState, useCallback } from 'react'
2
+ import { useWebSocket } from './useWebSocket'
3
+
4
+ type PhaseName = 'architect' | 'developer' | 'reviewer' | 'ship'
5
+ type PhaseState = 'idle' | 'running' | 'done' | 'error'
6
+
7
+ export interface PhaseMap {
8
+ architect: PhaseState
9
+ developer: PhaseState
10
+ reviewer: PhaseState
11
+ ship: PhaseState
12
+ }
13
+
14
+ export interface LogLine {
15
+ source: 'stdout' | 'stderr'
16
+ line: string
17
+ timestamp: string
18
+ processId: string
19
+ }
20
+
21
+ const INITIAL_PHASES: PhaseMap = {
22
+ architect: 'idle',
23
+ developer: 'idle',
24
+ reviewer: 'idle',
25
+ ship: 'idle',
26
+ }
27
+
28
+ export function usePipeline() {
29
+ const [phases, setPhases] = useState<PhaseMap>(INITIAL_PHASES)
30
+ const [projectName, setProjectName] = useState('')
31
+ const [logLines, setLogLines] = useState<LogLine[]>([])
32
+
33
+ const handleMessage = useCallback((data: unknown) => {
34
+ const msg = data as { type: string } & Record<string, unknown>
35
+
36
+ if (msg.type === 'init') {
37
+ setProjectName((msg.projectName as string) ?? '')
38
+ setPhases((msg.phases as PhaseMap) ?? INITIAL_PHASES)
39
+ const buf = (msg.logBuffer as LogLine[]) ?? []
40
+ setLogLines(buf)
41
+ } else if (msg.type === 'phase') {
42
+ setPhases((prev) => ({
43
+ ...prev,
44
+ [msg.phase as PhaseName]: msg.state as PhaseState,
45
+ }))
46
+ } else if (msg.type === 'log') {
47
+ setLogLines((prev) => [
48
+ ...prev,
49
+ {
50
+ source: msg.source as 'stdout' | 'stderr',
51
+ line: msg.line as string,
52
+ timestamp: msg.timestamp as string,
53
+ processId: msg.processId as string,
54
+ },
55
+ ])
56
+ }
57
+ }, [])
58
+
59
+ const { connectionStatus } = useWebSocket('ws://localhost:4200', handleMessage)
60
+
61
+ return { phases, projectName, logLines, connectionStatus }
62
+ }
@@ -0,0 +1,59 @@
1
+ import { useEffect, useRef, useState, useCallback } from 'react'
2
+
3
+ type ConnectionStatus = 'connecting' | 'connected' | 'disconnected'
4
+
5
+ const BACKOFF_DELAYS = [1000, 2000, 4000, 8000, 16000]
6
+
7
+ export function useWebSocket(
8
+ url: string,
9
+ onMessage: (data: unknown) => void
10
+ ): { connectionStatus: ConnectionStatus } {
11
+ const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('connecting')
12
+ const wsRef = useRef<WebSocket | null>(null)
13
+ const retryCountRef = useRef(0)
14
+ const retryTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
15
+ const onMessageRef = useRef(onMessage)
16
+ onMessageRef.current = onMessage
17
+
18
+ const connect = useCallback(() => {
19
+ const ws = new WebSocket(url)
20
+ wsRef.current = ws
21
+ setConnectionStatus('connecting')
22
+
23
+ ws.onopen = () => {
24
+ retryCountRef.current = 0
25
+ setConnectionStatus('connected')
26
+ }
27
+
28
+ ws.onmessage = (event) => {
29
+ try {
30
+ const parsed = JSON.parse(event.data as string)
31
+ onMessageRef.current(parsed)
32
+ } catch {
33
+ // ignore malformed messages
34
+ }
35
+ }
36
+
37
+ ws.onclose = () => {
38
+ wsRef.current = null
39
+ const attempt = retryCountRef.current
40
+ if (attempt >= BACKOFF_DELAYS.length) {
41
+ setConnectionStatus('disconnected')
42
+ return
43
+ }
44
+ const delay = BACKOFF_DELAYS[attempt]
45
+ retryCountRef.current += 1
46
+ retryTimeoutRef.current = setTimeout(connect, delay)
47
+ }
48
+ }, [url])
49
+
50
+ useEffect(() => {
51
+ connect()
52
+ return () => {
53
+ if (retryTimeoutRef.current) clearTimeout(retryTimeoutRef.current)
54
+ wsRef.current?.close()
55
+ }
56
+ }, [connect])
57
+
58
+ return { connectionStatus }
59
+ }
@@ -0,0 +1,9 @@
1
+ import { StrictMode } from 'react'
2
+ import { createRoot } from 'react-dom/client'
3
+ import App from './App'
4
+
5
+ createRoot(document.getElementById('root')!).render(
6
+ <StrictMode>
7
+ <App />
8
+ </StrictMode>
9
+ )
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
6
+ "module": "ESNext",
7
+ "skipLibCheck": true,
8
+ "moduleResolution": "bundler",
9
+ "allowImportingTsExtensions": true,
10
+ "resolveJsonModule": true,
11
+ "isolatedModules": true,
12
+ "noEmit": true,
13
+ "jsx": "react-jsx",
14
+ "strict": true,
15
+ "noUnusedLocals": true,
16
+ "noUnusedParameters": true,
17
+ "noFallthroughCasesInSwitch": true
18
+ },
19
+ "include": ["src"],
20
+ "references": [{ "path": "./tsconfig.node.json" }]
21
+ }
@@ -0,0 +1,11 @@
1
+ {
2
+ "compilerOptions": {
3
+ "composite": true,
4
+ "skipLibCheck": true,
5
+ "module": "ESNext",
6
+ "moduleResolution": "bundler",
7
+ "allowSyntheticDefaultImports": true,
8
+ "strict": true
9
+ },
10
+ "include": ["vite.config.ts"]
11
+ }
@@ -0,0 +1,13 @@
1
+ import { defineConfig } from 'vite'
2
+ import react from '@vitejs/plugin-react'
3
+
4
+ export default defineConfig({
5
+ plugins: [react()],
6
+ server: {
7
+ port: 4201,
8
+ proxy: {
9
+ '/api': 'http://localhost:4200',
10
+ '/hooks': 'http://localhost:4200',
11
+ },
12
+ },
13
+ })