Repository navigation
Expand file tree
/
Copy pathinit.sh
More file actions
executable file
·1325 lines (1041 loc) · 39.1 KB
/
Copy pathinit.sh
File metadata and controls
executable file
·1325 lines (1041 loc) · 39.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/bin/bash
set -euo pipefail
# ─── Configuration ────────────────────────────────────────────────────────────
PROJECT_NAME="${1:-harness_project}"
# ─── Colors & Helpers ─────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; NC='\033[0m'
info() { echo -e "${CYAN}[INFO]${NC} $1"; }
success() { echo -e "${GREEN}[ OK]${NC} $1"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $1"; }
fail() { echo -e "${RED}[FAIL]${NC} $1"; exit 1; }
step() { echo -e "\n${BOLD}━━━ $1 ━━━${NC}"; }
# ─── Pre-flight Checks ───────────────────────────────────────────────────────
step "Pre-flight Checks"
command -v node >/dev/null 2>&1 || fail "node is not installed"
command -v pnpm >/dev/null 2>&1 || fail "pnpm is not installed (npm i -g pnpm)"
command -v python3 >/dev/null 2>&1 || fail "python3 is not installed"
info "node $(node -v)"
info "pnpm $(pnpm -v)"
info "python $(python3 --version | awk '{print $2}')"
if [ -d "frontend" ] || [ -d "backend" ]; then
warn "frontend/ or backend/ already exists — run clean.sh first"
read -rp "Continue anyway? (y/N): " confirm
[[ "$confirm" =~ ^[Yy]$ ]] || exit 0
fi
success "All checks passed"
# ─── Harness Meta Files ──────────────────────────────────────────────────────
step "Harness Meta Files"
cat > AGENTS.md << 'AGENTS_EOF'
# AGENTS.md — AI Agent Guidance
## Project Overview
- **Name**: __PROJECT_NAME__
- **Stack**: Next.js (frontend) + FastAPI (backend)
- **Monorepo**: `frontend/` and `backend/` at repo root
## Architecture
```
nginx/ → Nginx reverse proxy (port 80 → frontend/backend)
frontend/ → Next.js 14+ App Router, TypeScript, Tailwind, shadcn/ui
backend/ → FastAPI, Pydantic v2, graph-tool (graph analysis), loguru (logging)
docs/ → Design docs, ADRs, API specs
```
## Conventions
- Backend: snake_case for files/functions, PascalCase for classes/models
- Frontend: kebab-case for files, PascalCase for components
- API routes: RESTful, versioned under `/api/v1/`
- All new features need tests before merge
## Tech Preferences (IMPORTANT)
**Before adding ANY dependency, read `docs/tech_preferences.md`**
**Every code change MUST pass `make lint` — see `docs/dev_rules.md`**
Key constraints:
- Charts → **ECharts** (not Recharts, Chart.js, Nivo)
- Graph/Network visualization → **Cytoscape.js** with layout extensions (not D3-force, vis.js, react-flow)
- Backend graph analysis → **graph-tool** (not NetworkX, igraph)
- UI components → **shadcn/ui** only (not Ant Design, MUI, Chakra)
- State management → **zustand** (client) + **@tanstack/react-query** (server)
- Logging → **loguru** (not stdlib logging)
- Always prefer mature, well-maintained libraries (see full list in docs/tech_preferences.md)
## Key Commands
| Task | Command |
|------------------|--------------------------------------|
| Backend dev | `cd backend && uvicorn app.main:app --reload` |
| Frontend dev | `cd frontend && pnpm dev` |
| Run all (Docker) | `docker compose up --build` |
| Access (Docker) | `http://localhost` (nginx :80) |
| Backend tests | `cd backend && pytest` |
| Frontend tests | `cd frontend && pnpm test` |
| Lint all | `make lint` |
## Agent 使用策略(重要)
### 何时用主 Agent(直接对话)
适合需要 **全局上下文** 的任务:
- 需求分析、架构设计讨论
- 跨前后端的功能实现(需要同时改 backend + frontend)
- Code review、Bug 分析
- 修改 design.md、AGENTS.md 等项目级文件
- 涉及 ≤ 3 个文件的小改动
### 何时用 Subagent(子任务委派)
适合 **独立、边界清晰** 的任务,避免撑大主 Agent 的上下文:
| 场景 | 为什么用 Subagent |
|------|-------------------|
| 单独实现一个后端 API endpoint | 只需 backend 上下文,不需要前端 |
| 单独实现一个前端页面 | 只需 frontend 上下文,不需要后端 |
| 写测试用例 | 只需被测代码的上下文 |
| 修复 lint/build 错误 | 范围小,独立完成 |
| 文档更新(README、注释) | 不影响代码逻辑 |
| 并行开发多个独立功能 | 各 subagent 互不干扰 |
### 上下文管理原则
1. **单一职责**: 每个 Agent/Subagent 只处理一个功能域
2. **上下文预加载**: 告诉 Agent 先读哪些文件,不要让它自己猜
- 后端任务: "先读 AGENTS.md、docs/design.md 的第 X 节、backend/app/main.py"
- 前端任务: "先读 AGENTS.md、docs/tech_preferences.md、frontend/src/lib/api.ts"
3. **及时收尾**: 完成一个功能后,让 Agent 更新 progress.md,然后开新会话
4. **避免超长会话**: 如果一个会话超过 20 轮对话,考虑总结当前进展后开新会话
5. **Subagent 结果校验**: 主 Agent 应检查 Subagent 的产出(运行 lint + test)
### Prompt 模板
**主 Agent — 分析需求**:
```
读取 AGENTS.md 和 docs/design.md,帮我分析 F-001 功能的实现方案。
不要写代码,只输出任务拆解和技术方案。
```
**Subagent — 后端实现**:
```
读取 AGENTS.md 和 docs/design.md 第 4-5 节。
在 backend/ 中实现 /api/v1/users 的 CRUD。
遵循 docs/tech_preferences.md 的后端选型。
完成后运行 pytest 确认通过。
```
**Subagent — 前端实现**:
```
读取 AGENTS.md 和 docs/tech_preferences.md。
在 frontend/src/app/users/ 中实现用户列表页面。
使用 shadcn/ui 组件,调用 lib/api.ts 获取数据。
```
**主 Agent — 验收集成**:
```
运行 make lint && make test,检查所有代码是否通过。
更新 progress.md 标记已完成的任务。
```
AGENTS_EOF
sed -i '' "s/__PROJECT_NAME__/${PROJECT_NAME}/g" AGENTS.md
success "AGENTS.md"
# ─── Docs ─────────────────────────────────────────────────────────────────────
step "Documentation"
mkdir -p docs
# design.md — 用户需要填写的项目设计模板
cat > docs/design.md << 'DESIGN_EOF'
# 项目设计文档
<!-- ============================================================
这是你的项目设计文档模板。请按提示填写每个章节。
填完后,让 AI 工具读取本文件即可开始开发:
Cursor: "请阅读 @docs/design.md 并按计划开始实现"
Claude Code: "读取 docs/design.md,按开发计划开始工作"
============================================================ -->
## 1. 项目名称 & 简介
**名称**: <!-- 填写你的项目名称 -->
**一句话描述**: <!-- 这个项目是什么?解决什么问题? -->
**目标用户**: <!-- 谁会用这个产品? -->
## 2. 核心功能
<!-- 列出 3-5 个核心功能,每个功能用一行描述 -->
| ID | 功能名称 | 描述 | 优先级 |
|----|---------|------|--------|
| F-001 | <!-- 功能名 --> | <!-- 简要描述 --> | P0 |
| F-002 | <!-- 功能名 --> | <!-- 简要描述 --> | P0 |
| F-003 | <!-- 功能名 --> | <!-- 简要描述 --> | P1 |
## 3. 页面规划
<!-- 列出前端需要的页面和路由 -->
```
/ → <!-- 首页/Dashboard 做什么? -->
/login → 登录
/register → 注册
/??? → <!-- 你的核心页面 -->
/settings → 设置
```
## 4. 数据模型
<!-- 描述核心实体和字段 -->
```
实体名:
- id: str (PK)
- 字段名: 类型
- 字段名: 类型
- created_at: datetime
- updated_at: datetime
```
## 5. API 设计
<!-- 列出后端需要的 API 端点 -->
```
GET /api/v1/??? → 描述
POST /api/v1/??? → 描述
PUT /api/v1/???/:id → 描述
DELETE /api/v1/???/:id → 描述
```
## 6. 架构图
```
┌────────────┐
┌────▶│ Frontend │
┌──────┐│ │ Next.js │
│Nginx ││ │ :3000 │
│ :80 ││ └────────────┘
└──────┘│ ┌────────────┐
└────▶│ Backend │
│ FastAPI │
│ :8000 │
└────────────┘
```
<!-- 如果有额外服务(数据库、Redis、消息队列、外部 API),在这里补充 -->
## 7. 开发计划
### Phase 1 — MVP
- [ ] <!-- 任务 1 -->
- [ ] <!-- 任务 2 -->
- [ ] <!-- 任务 3 -->
### Phase 2 — 完善
- [ ] <!-- 任务 4 -->
- [ ] <!-- 任务 5 -->
## 8. 备注 & 开放问题
- [ ] <!-- 还没确定的事项 -->
- [ ] <!-- 需要调研的技术点 -->
DESIGN_EOF
success "docs/design.md"
# dev_rules.md — 开发规范约束(给 AI 编码工具的硬性规则)
cat > docs/dev_rules.md << 'RULES_EOF'
# 开发规范 — AI 编码工具必须遵守
> 本文档是给 AI 编码工具(Cursor / Claude Code / Codex 等)的 **硬性约束**。
> 每次编写或修改代码后 **必须** 执行以下检查。
## 代码质量检查(每次修改后必做)
```bash
# 后端 — 修改 Python 文件后
make lint-backend # ruff 检查,必须 0 error
make format # 自动格式化
# 前端 — 修改 TS/TSX 文件后
make lint-frontend # ESLint 检查,必须 0 error
# 一键全量检查
make lint # 同时 lint 前后端
```
## 测试(功能变更后必做)
```bash
# 后端
make test-backend # pytest,新增 API 必须有对应测试
# 全量
make test # 前后端一起跑
```
## 提交前检查清单
| 检查项 | 命令 | 要求 |
|--------|------|------|
| Python lint | `make lint-backend` | 0 error, 0 warning |
| TS/JS lint | `make lint-frontend` | 0 error |
| 格式化 | `make format` | 已执行 |
| 后端测试 | `make test-backend` | 全部通过 |
| 类型安全 | 前端 `pnpm build` 无 TS error | 编译通过 |
## 日志规范
- **后端一律使用 `loguru`**,禁止 `import logging` 或 `print()` 调试
- 使用方式:`from loguru import logger`,然后 `logger.info(...)`
- 敏感信息(密码、token、密钥)**禁止**出现在日志中
## 依赖管理
- 新增前端依赖:先查看 `docs/tech_preferences.md`,同类库已有指定的不要引入新的
- 新增后端依赖:加入 `backend/requirements.txt` 并说明用途
- **禁止** 引入与 tech_preferences.md 冲突的库
```
RULES_EOF
success "docs/dev_rules.md"
# tech_preferences.md — 技术选型偏好
cat > docs/tech_preferences.md << 'TECH_EOF'
# Tech Preferences — 技术选型偏好
> AI 编码工具和开发者在做技术选型时 **必须** 参考本文档。
> 任何新增依赖都应优先使用下列指定库,避免引入同类竞品。
## 前端
### 图表 (Charts)
**必须使用**: [ECharts](https://echarts.apache.org/)
```bash
pnpm add echarts echarts-for-react
```
- 所有统计图表统一用 ECharts,React 中用 `echarts-for-react` wrapper
- 不要使用 Chart.js、Recharts、Nivo、Victory 等
### 图/网络可视化 (Graph / Network)
**必须使用**: [Cytoscape.js](https://js.cytoscape.org/) + 布局扩展
```bash
pnpm add cytoscape react-cytoscapejs cytoscape-cola cytoscape-dagre cytoscape-fcose
pnpm add -D @types/cytoscape
```
- 所有关系图、网络图、拓扑图统一用 Cytoscape.js
- 默认启用自动布局(fcose / dagre / cola / grid / circle),展示时必须包含排布功能
- 不要使用 D3-force、vis.js、Sigma.js、react-flow 等
### UI 组件库
**必须使用**: [shadcn/ui](https://ui.shadcn.com/) + Tailwind CSS
- 不要额外引入 Ant Design、Material UI、Chakra UI
- 按需安装: `npx shadcn@latest add <component>`
### 常用库偏好
| 场景 | 首选库 | 备注 |
|------|--------|------|
| HTTP 请求 | 内置 `fetch` + `lib/api.ts` | 无需 axios |
| 服务端状态 | `@tanstack/react-query` | 缓存、重试、乐观更新 |
| 客户端状态 | `zustand` | 轻量级 |
| 表单 | `react-hook-form` + `zod` | shadcn/ui form 已集成 |
| 日期 | `date-fns` | 不用 moment.js / dayjs |
| 拖拽 | `@dnd-kit/core` | 看板、排序 |
| 动画 | `framer-motion` | 页面过渡 |
| 图标 | `lucide-react` | shadcn/ui 默认 |
| 数据表格 | `@tanstack/react-table` | 配合 shadcn DataTable |
## 后端
| 场景 | 首选库 |
|------|--------|
| Web 框架 | `FastAPI` |
| 数据校验 | `Pydantic v2` |
| 配置 | `pydantic-settings` |
| 日志 | `loguru` (不用 stdlib logging) |
| 图分析 | `graph-tool` (不用 NetworkX、igraph) |
| 认证 | `python-jose` + `passlib` |
| HTTP 客户端 | `httpx` |
| AI / LLM | `litellm` |
| 测试 | `pytest` + `pytest-asyncio` |
| Lint | `ruff` |
## 开发工具链
| 用途 | 工具 | 命令 |
|------|------|------|
| Python lint + format | `ruff` | `make lint-backend` / `make format` |
| JS/TS format | `prettier` | `make format` |
| JS/TS lint | `ESLint` (Next.js 内置) | `pnpm lint` |
| 构建命令 | `Makefile` | `make help` |
## 通用原则
1. **成熟度优先**: GitHub stars > 5k,npm 周下载 > 50k
2. **维护活跃**: 最近 6 个月有更新
3. **类型安全**: 优先选有 TypeScript 类型定义的库
4. **避免重复**: 同一场景只引入一个库
5. **引入新库前**: 先在本文档中记录并说明理由
TECH_EOF
success "docs/tech_preferences.md"
# ─── Backend (FastAPI) ────────────────────────────────────────────────────────
step "Backend — FastAPI"
mkdir -p backend/app/api/routes backend/app/models backend/app/schemas \
backend/app/services backend/app/prompts backend/app/core \
backend/tests
# requirements.txt
cat > backend/requirements.txt << 'REQ_EOF'
fastapi>=0.111.0
uvicorn[standard]>=0.30.0
pydantic>=2.7.0
pydantic-settings>=2.3.0
loguru>=0.7.0
python-jose[cryptography]>=3.3.0
passlib[bcrypt]>=1.7.4
httpx>=0.27.0
python-dotenv>=1.0.1
pytest>=8.2.0
pytest-asyncio>=0.23.0
ruff>=0.5.0
# graph-tool: installed via conda in Dockerfile
langchain>=0.3.0
langchain-openai>=0.2.0
langchain-community>=0.3.0
langchain-core>=0.3.0
langgraph>=0.2.0
langsmith>=0.1.0
openai>=1.50.0
tiktoken>=0.7.0
REQ_EOF
success "requirements.txt"
# .env.example
cat > backend/.env.example << 'ENV_EOF'
# App
APP_NAME=harness_project
APP_ENV=development
DEBUG=true
# Server
BACKEND_PORT=8000
BACKEND_HOST=0.0.0.0
# Auth
SECRET_KEY=change-me-in-production
ACCESS_TOKEN_EXPIRE_MINUTES=30
# CORS (local dev: :3000 direct, Docker: via nginx :80)
CORS_ORIGINS=["http://localhost:3000","http://localhost","http://localhost:80"]
ENV_EOF
cp backend/.env.example backend/.env
success ".env"
# core/config.py
cat > backend/app/core/__init__.py << 'EOF'
EOF
cat > backend/app/core/config.py << 'PYEOF'
from pydantic_settings import BaseSettings
class Settings(BaseSettings):
app_name: str = "harness_project"
app_env: str = "development"
debug: bool = True
backend_host: str = "0.0.0.0"
backend_port: int = 8000
secret_key: str = "change-me-in-production"
access_token_expire_minutes: int = 30
cors_origins: list[str] = ["http://localhost:3000"]
model_config = {"env_file": ".env", "env_file_encoding": "utf-8"}
settings = Settings()
PYEOF
success "core/config.py"
# app/main.py
cat > backend/app/__init__.py << 'EOF'
EOF
cat > backend/app/core/logging.py << 'PYEOF'
import sys
from loguru import logger
from app.core.config import settings
logger.remove()
if settings.debug:
logger.add(sys.stderr, level="DEBUG", format="{time:HH:mm:ss} | {level:<7} | {message}")
else:
logger.add(sys.stderr, level="INFO", format="{time:YYYY-MM-DD HH:mm:ss} | {level:<7} | {name}:{function}:{line} | {message}")
logger.add(
"logs/{time:YYYY-MM-DD}.log",
rotation="00:00",
retention="30 days",
level="INFO",
format="{time:YYYY-MM-DD HH:mm:ss} | {level:<7} | {name}:{function}:{line} | {message}",
)
PYEOF
success "core/logging.py (loguru)"
cat > backend/app/main.py << 'PYEOF'
from contextlib import asynccontextmanager
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from loguru import logger
from app.core.config import settings
import app.core.logging # noqa: F401 — init loguru
from app.api.routes import graph, health, items, llm
@asynccontextmanager
async def lifespan(app: FastAPI):
logger.info("Starting {} ...", settings.app_name)
yield
logger.info("Shutting down {} ...", settings.app_name)
app = FastAPI(
title=settings.app_name,
debug=settings.debug,
lifespan=lifespan,
)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
app.include_router(health.router, tags=["health"])
app.include_router(items.router, prefix="/api/v1", tags=["items"])
app.include_router(graph.router, prefix="/api/v1", tags=["graph"])
app.include_router(llm.router, prefix="/api/v1", tags=["llm"])
PYEOF
success "app/main.py"
# API routes
cat > backend/app/api/__init__.py << 'EOF'
EOF
cat > backend/app/api/routes/__init__.py << 'EOF'
EOF
cat > backend/app/api/routes/health.py << 'PYEOF'
from fastapi import APIRouter
router = APIRouter()
@router.get("/health")
async def health_check():
return {"status": "healthy"}
PYEOF
success "api/routes/health.py"
cat > backend/app/api/routes/items.py << 'PYEOF'
"""Item CRUD routes.
Endpoints:
GET /items — list all items
POST /items — create item (ItemCreate → ItemResponse)
GET /items/{item_id} — get single item (404 if missing)
Schema: app.schemas.item (ItemCreate, ItemResponse)
"""
from fastapi import APIRouter
router = APIRouter()
PYEOF
success "api/routes/items.py"
cat > backend/app/api/routes/graph.py << 'PYEOF'
"""Graph analysis routes.
Endpoints:
POST /graph/analyze — accept {nodes, edges}, return pagerank & betweenness metrics
Request body:
nodes: list[{id: str, label: str}]
edges: list[{source: str, target: str}]
Uses: app.services.graph_analysis (graph-tool backend)
"""
from fastapi import APIRouter
router = APIRouter()
PYEOF
success "api/routes/graph.py"
cat > backend/app/api/routes/llm.py << 'PYEOF'
"""LLM Provider management routes.
Endpoints:
POST /llm/providers — register or update a provider
GET /llm/providers — list all providers (api_key hidden)
DELETE /llm/providers/{name} — remove a provider
POST /llm/chat — chat using a registered provider
"""
from fastapi import APIRouter
router = APIRouter()
PYEOF
success "api/routes/llm.py"
# Schemas
mkdir -p backend/app/schemas
cat > backend/app/schemas/__init__.py << 'EOF'
EOF
cat > backend/app/schemas/item.py << 'PYEOF'
from pydantic import BaseModel
class ItemCreate(BaseModel):
name: str
description: str = ""
class ItemResponse(ItemCreate):
id: int
PYEOF
success "schemas/item.py"
# Models placeholder
cat > backend/app/models/__init__.py << 'EOF'
EOF
# Models placeholder
cat > backend/app/models/base.py << 'PYEOF'
from pydantic import BaseModel
class TimestampMixin(BaseModel):
created_at: str | None = None
updated_at: str | None = None
PYEOF
success "models/base.py"
# Services
cat > backend/app/services/__init__.py << 'EOF'
EOF
cat > backend/app/services/graph_analysis.py << 'PYEOF'
"""Graph analysis service using graph-tool.
Graceful fallback: if graph-tool is not installed, return basic stats only.
Public API:
build_graph(nodes, edges) -> gt.Graph | None
analyze_graph(nodes, edges) -> dict
Returns: {num_nodes, num_edges, metrics: [{id, pagerank, betweenness}]}
"""
PYEOF
success "services/graph_analysis.py"
cat > backend/app/services/llm_registry.py << 'PYEOF'
"""LLM Provider Registry — runtime registration of LLM providers.
Storage: data/llm_providers.json (persisted via Docker volume)
Supported provider_type: "openai_compatible" (via langchain_openai.ChatOpenAI)
Public API:
register_provider(name, api_key, api_base, model, provider_type, extra) -> dict
remove_provider(name) -> bool
list_providers() -> list[dict] (api_key redacted)
get_provider(name) -> dict | None
build_chat_model(name) -> BaseChatModel
"""
PYEOF
success "services/llm_registry.py"
# Prompts placeholder
cat > backend/app/prompts/__init__.py << 'EOF'
EOF
cat > backend/app/prompts/system.py << 'PYEOF'
SYSTEM_PROMPT = """You are a helpful AI assistant for the harness project.
Respond concisely and accurately.
"""
PYEOF
success "prompts/system.py"
# Tests
cat > backend/tests/__init__.py << 'EOF'
EOF
cat > backend/tests/conftest.py << 'PYEOF'
import pytest
from httpx import ASGITransport, AsyncClient
from app.main import app
@pytest.fixture
async def client():
transport = ASGITransport(app=app)
async with AsyncClient(transport=transport, base_url="http://test") as ac:
yield ac
PYEOF
cat > backend/tests/test_health.py << 'PYEOF'
import pytest
@pytest.mark.asyncio
async def test_health(client):
resp = await client.get("/health")
assert resp.status_code == 200
assert resp.json()["status"] == "healthy"
PYEOF
success "tests/"
cat > backend/pytest.ini << 'PYEOF'
[pytest]
asyncio_mode = auto
testpaths = tests
PYEOF
# pyproject.toml with ruff config
cat > backend/pyproject.toml << 'PYEOF'
[project]
name = "harness-backend"
version = "0.1.0"
requires-python = ">=3.11"
[tool.ruff]
target-version = "py311"
line-length = 100
[tool.ruff.lint]
select = [
"E", # pycodestyle errors
"W", # pycodestyle warnings
"F", # pyflakes
"I", # isort
"N", # pep8-naming
"UP", # pyupgrade
"B", # flake8-bugbear
"S", # flake8-bandit (security)
"A", # flake8-builtins
"T20", # flake8-print
"RUF", # ruff-specific rules
]
ignore = ["S101"] # allow assert in tests
[tool.ruff.lint.isort]
known-first-party = ["app"]
[tool.ruff.format]
quote-style = "double"
indent-style = "space"
PYEOF
success "pyproject.toml (ruff)"
# ─── Frontend (Next.js + shadcn/ui) ──────────────────────────────────────────
step "Frontend — Next.js + shadcn/ui"
info "Creating Next.js app (this may take a minute)..."
pnpm dlx create-next-app@latest ./frontend \
--typescript --tailwind --eslint --app --src-dir \
--import-alias "@/*" --yes
info "Installing shadcn/ui..."
npx --yes shadcn@latest init -y --defaults --cwd ./frontend
# Additional frontend dependencies
info "Installing extra dependencies..."
(cd frontend && pnpm add lucide-react cytoscape react-cytoscapejs cytoscape-cola cytoscape-dagre cytoscape-fcose)
(cd frontend && pnpm add react-markdown remark-gfm rehype-highlight)
(cd frontend && pnpm add -D @types/cytoscape)
# Create directory structure
mkdir -p frontend/src/app/\(dashboard\) \
frontend/src/app/\(auth\)/login \
frontend/src/app/\(auth\)/register \
frontend/src/app/settings \
frontend/src/components/ui \
frontend/src/components/layout \
frontend/src/lib
# Graph visualization component (Cytoscape.js with layout)
mkdir -p frontend/src/components/graph
cat > frontend/src/components/graph/graph-viewer.tsx << 'TSEOF'
/**
* GraphViewer — Cytoscape.js graph visualization component.
*
* Props:
* elements: cytoscape.ElementDefinition[] — nodes & edges data
* layout: "fcose" | "dagre" | "cola" | "grid" | "circle"
* style: cytoscape.Stylesheet[] — custom node/edge styling
* className: string
*
* Features:
* - Layout switcher toolbar (fcose, dagre, cola, grid, circle)
* - Fit-to-view button
* - Default indigo color scheme for nodes, light edges with arrows
*
* Dependencies: cytoscape, cytoscape-cola, cytoscape-dagre, cytoscape-fcose
*/
export {};
TSEOF
success "components/graph/graph-viewer.tsx"
# .env.local
cat > frontend/.env.local << 'ENV_EOF'
NEXT_PUBLIC_API_URL=http://localhost:8000
NEXT_PUBLIC_APP_NAME=Harness Project
ENV_EOF
cat > frontend/.env.example << 'ENV_EOF'
NEXT_PUBLIC_API_URL=http://localhost:8000
NEXT_PUBLIC_APP_NAME=Harness Project
ENV_EOF
success ".env.local"
# API client utility
cat > frontend/src/lib/api.ts << 'TSEOF'
/**
* Typed API client wrapping fetch.
*
* Base URL from NEXT_PUBLIC_API_URL (default http://localhost:8000).
*
* Exports:
* api.get<T>(path) — GET request
* api.post<T>(path, body) — POST with JSON body
* api.put<T>(path, body) — PUT with JSON body
* api.delete<T>(path) — DELETE request
*
* Auto JSON serialization, error extraction from {detail} response.
*/
export {};
TSEOF
success "lib/api.ts"
# Shared layout component
cat > frontend/src/components/layout/sidebar.tsx << 'TSEOF'
/**
* Sidebar navigation component.
*
* "use client" — uses usePathname for active route highlighting.
*
* Nav items: Dashboard (/), Settings (/settings), Login (/login)
* Icons: lucide-react (LayoutDashboard, Settings, LogIn)
* Width: w-60, border-r, bg-muted/40
* App name from NEXT_PUBLIC_APP_NAME env var.
*/
export {};
TSEOF
success "components/layout/sidebar.tsx"
# Root layout with sidebar
cat > frontend/src/app/layout.tsx << 'TSEOF'
/**
* Root layout — Inter font, globals.css, flex row: Sidebar + main content.
*
* Structure: <html> → <body> → flex h-screen → <Sidebar /> + <main>{children}</main>
* Metadata title from NEXT_PUBLIC_APP_NAME.
*/
export default function RootLayout({ children }: { children: React.ReactNode }) {
return <html lang="en"><body>{children}</body></html>;
}
TSEOF
success "app/layout.tsx"
# Dashboard page
cat > frontend/src/app/page.tsx << 'TSEOF'
/**
* Dashboard page — 3-column stats cards grid.
* Cards: Total Items, API Health, Uptime.
*/
export default function DashboardPage() {
return <div>Dashboard</div>;
}
TSEOF
success "app/page.tsx (dashboard)"
# Settings page
cat > frontend/src/app/settings/page.tsx << 'TSEOF'
/** Settings page — project configuration panel. */
export default function SettingsPage() {
return <div>Settings</div>;
}
TSEOF
success "app/settings/page.tsx"
# Login page
cat > frontend/src/app/\(auth\)/login/page.tsx << 'TSEOF'
/** Login page — email + password form, centered layout. */
export default function LoginPage() {
return <div>Login</div>;
}
TSEOF
success "app/(auth)/login/page.tsx"
# Register page
cat > frontend/src/app/\(auth\)/register/page.tsx << 'TSEOF'
/** Register page — name + email + password form, centered layout. */
export default function RegisterPage() {
return <div>Register</div>;
}
TSEOF
success "app/(auth)/register/page.tsx"
# Enable standalone output for Docker multi-stage build
info "Configuring Next.js standalone output..."
if [ -f frontend/next.config.ts ]; then
NEXT_CONFIG="frontend/next.config.ts"
elif [ -f frontend/next.config.mjs ]; then
NEXT_CONFIG="frontend/next.config.mjs"
else
NEXT_CONFIG="frontend/next.config.js"
fi
cat > "$NEXT_CONFIG" << 'NEXTCFG_EOF'
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
output: "standalone",
};
export default nextConfig;
NEXTCFG_EOF
success "next.config (standalone output)"
# Prettier config
cat > frontend/.prettierrc << 'PRETTIER_EOF'
{
"semi": true,
"singleQuote": false,
"tabWidth": 2,
"trailingComma": "all",
"printWidth": 100,
"plugins": ["prettier-plugin-tailwindcss"]
}
PRETTIER_EOF
(cd frontend && pnpm add -D prettier prettier-plugin-tailwindcss)
success "prettier config"
# ─── Docker ───────────────────────────────────────────────────────────────────
step "Docker"
cat > docker-compose.yml << 'DOCKER_EOF'
services:
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: __PROJECT_NAME__-backend
restart: unless-stopped
environment:
- ENVIRONMENT=production
- SECRET_KEY=${SECRET_KEY:-change-me-in-production}
- CORS_ORIGINS=["http://localhost","http://localhost:80"]
volumes:
- backend-data:/app/data
- backend-logs:/app/logs
networks:
- __PROJECT_NAME__-network
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')"]
interval: 30s
timeout: 10s
retries: 3
start_period: 10s
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: __PROJECT_NAME__-frontend
restart: unless-stopped
environment:
- NODE_ENV=production
depends_on:
backend:
condition: service_healthy
networks:
- __PROJECT_NAME__-network
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://127.0.0.1:3000/"]
interval: 30s
timeout: 10s