init
This commit is contained in:
9
.idea/Marshmallow.iml
generated
Normal file
9
.idea/Marshmallow.iml
generated
Normal file
@@ -0,0 +1,9 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="JAVA_MODULE" version="4">
|
||||
<component name="NewModuleRootManager" inherit-compiler-output="true">
|
||||
<exclude-output />
|
||||
<content url="file://$MODULE_DIR$" />
|
||||
<orderEntry type="inheritedJdk" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
21
AbstractWizard/Dockerfile
Normal file
21
AbstractWizard/Dockerfile
Normal file
@@ -0,0 +1,21 @@
|
||||
FROM golang:1.24-alpine AS build
|
||||
|
||||
WORKDIR /src
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -ldflags="-s -w" -o /abstract-wizard .
|
||||
|
||||
FROM gcr.io/distroless/static-debian12
|
||||
|
||||
COPY --from=build /abstract-wizard /abstract-wizard
|
||||
|
||||
USER nonroot:nonroot
|
||||
|
||||
ENV CONFIG_DIR=/config
|
||||
ENV LISTEN_ADDR=0.0.0.0:8080
|
||||
EXPOSE 8080
|
||||
|
||||
VOLUME /config
|
||||
|
||||
ENTRYPOINT ["/abstract-wizard"]
|
||||
287
AbstractWizard/README.md
Normal file
287
AbstractWizard/README.md
Normal file
@@ -0,0 +1,287 @@
|
||||
# Abstract Wizard
|
||||
|
||||
[English](#english) | [中文](#中文)
|
||||
|
||||
---
|
||||
|
||||
## English
|
||||
|
||||
Secure configuration file management service. Read, modify, and version-control JSON/YAML config files via REST API. Listens on localhost only — access remotely via SSH tunnel.
|
||||
|
||||
### Quick Start
|
||||
|
||||
#### Docker Compose (Recommended)
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
The service listens on `127.0.0.1:18080`.
|
||||
|
||||
#### Run Locally
|
||||
|
||||
```bash
|
||||
go build -o abstract-wizard .
|
||||
CONFIG_DIR=./configs ./abstract-wizard
|
||||
```
|
||||
|
||||
### Environment Variables
|
||||
|
||||
| Variable | Default | Description |
|
||||
|----------|---------|-------------|
|
||||
| `CONFIG_DIR` | `/config` | Base directory for config files |
|
||||
| `LISTEN_ADDR` | `127.0.0.1:8080` | HTTP listen address |
|
||||
| `MAX_BACKUPS` | `10` | Max backup versions per file |
|
||||
|
||||
### API
|
||||
|
||||
#### Health Check
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:18080/health
|
||||
```
|
||||
|
||||
#### Read Config
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:18080/api/v1/config/app.json
|
||||
```
|
||||
|
||||
#### Full Replace
|
||||
|
||||
```bash
|
||||
curl -X PUT http://127.0.0.1:18080/api/v1/config/app.json \
|
||||
-d '{"database": {"host": "localhost", "port": 5432}}'
|
||||
```
|
||||
|
||||
A backup is created automatically before each write. The response includes `backup_version`.
|
||||
|
||||
#### Partial Update (Deep Merge)
|
||||
|
||||
```bash
|
||||
curl -X PATCH http://127.0.0.1:18080/api/v1/config/app.json \
|
||||
-d '{"database": {"port": 3306}}'
|
||||
```
|
||||
|
||||
Only the specified fields are updated; nested objects are merged recursively.
|
||||
|
||||
#### List Backups
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:18080/api/v1/backups/app.json
|
||||
```
|
||||
|
||||
#### Rollback
|
||||
|
||||
```bash
|
||||
# Get the version from the backup list, then:
|
||||
curl -X POST http://127.0.0.1:18080/api/v1/rollback/app.json \
|
||||
-d '{"version": "20260215T120000Z"}'
|
||||
```
|
||||
|
||||
#### Mode Switching
|
||||
|
||||
Get current mode:
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:18080/api/v1/mode
|
||||
```
|
||||
|
||||
Switch to readonly (rejects all writes):
|
||||
|
||||
```bash
|
||||
curl -X PUT http://127.0.0.1:18080/api/v1/mode \
|
||||
-d '{"mode": "readonly"}'
|
||||
```
|
||||
|
||||
Switch back to init (read/write):
|
||||
|
||||
```bash
|
||||
curl -X PUT http://127.0.0.1:18080/api/v1/mode \
|
||||
-d '{"mode": "init"}'
|
||||
```
|
||||
|
||||
### YAML Support
|
||||
|
||||
All endpoints support both JSON and YAML. The format is detected by file extension:
|
||||
|
||||
```bash
|
||||
curl -X PUT http://127.0.0.1:18080/api/v1/config/app.yaml \
|
||||
-H "Content-Type: text/yaml" \
|
||||
-d '
|
||||
database:
|
||||
host: localhost
|
||||
port: 5432
|
||||
'
|
||||
```
|
||||
|
||||
### Security Model
|
||||
|
||||
The service binds to `127.0.0.1` by default and is not exposed externally. Use an SSH tunnel for remote access:
|
||||
|
||||
```bash
|
||||
ssh -L 18080:127.0.0.1:18080 user@server
|
||||
```
|
||||
|
||||
Then access via `http://127.0.0.1:18080` locally.
|
||||
|
||||
### Project Structure
|
||||
|
||||
```
|
||||
├── main.go # Entry point, env config, graceful shutdown
|
||||
├── config/
|
||||
│ ├── validation.go # Path validation, traversal prevention
|
||||
│ ├── parser.go # JSON/YAML parse, serialize, deep merge
|
||||
│ ├── atomic.go # Atomic write (temp → fsync → rename)
|
||||
│ └── backup.go # Timestamped backups, pruning, rollback
|
||||
├── audit/
|
||||
│ └── logger.go # Structured JSON audit log
|
||||
├── server/
|
||||
│ ├── server.go # HTTP server, routing, mode state machine
|
||||
│ ├── middleware.go # Request logging middleware
|
||||
│ └── handlers.go # API handlers
|
||||
├── Dockerfile # Multi-stage build
|
||||
└── docker-compose.yaml # Example deployment
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 中文
|
||||
|
||||
安全的配置文件管理服务。通过 REST API 对 JSON/YAML 配置文件进行读取、修改和版本管理,仅监听 localhost,通过 SSH 隧道访问。
|
||||
|
||||
### 快速开始
|
||||
|
||||
#### Docker Compose(推荐)
|
||||
|
||||
```bash
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
服务启动后监听 `127.0.0.1:18080`。
|
||||
|
||||
#### 本地运行
|
||||
|
||||
```bash
|
||||
go build -o abstract-wizard .
|
||||
CONFIG_DIR=./configs ./abstract-wizard
|
||||
```
|
||||
|
||||
### 环境变量
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `CONFIG_DIR` | `/config` | 配置文件存放目录 |
|
||||
| `LISTEN_ADDR` | `127.0.0.1:8080` | 监听地址 |
|
||||
| `MAX_BACKUPS` | `10` | 每个文件保留的最大备份数 |
|
||||
|
||||
### API
|
||||
|
||||
#### 健康检查
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:18080/health
|
||||
```
|
||||
|
||||
#### 读取配置
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:18080/api/v1/config/app.json
|
||||
```
|
||||
|
||||
#### 完整替换配置
|
||||
|
||||
```bash
|
||||
curl -X PUT http://127.0.0.1:18080/api/v1/config/app.json \
|
||||
-d '{"database": {"host": "localhost", "port": 5432}}'
|
||||
```
|
||||
|
||||
写入前会自动创建备份,响应中包含 `backup_version`。
|
||||
|
||||
#### 局部更新(深度合并)
|
||||
|
||||
```bash
|
||||
curl -X PATCH http://127.0.0.1:18080/api/v1/config/app.json \
|
||||
-d '{"database": {"port": 3306}}'
|
||||
```
|
||||
|
||||
仅更新指定字段,嵌套对象递归合并。
|
||||
|
||||
#### 查看备份列表
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:18080/api/v1/backups/app.json
|
||||
```
|
||||
|
||||
#### 回滚到指定版本
|
||||
|
||||
```bash
|
||||
# 先从备份列表获取版本号,然后:
|
||||
curl -X POST http://127.0.0.1:18080/api/v1/rollback/app.json \
|
||||
-d '{"version": "20260215T120000Z"}'
|
||||
```
|
||||
|
||||
#### 模式切换
|
||||
|
||||
查看当前模式:
|
||||
|
||||
```bash
|
||||
curl http://127.0.0.1:18080/api/v1/mode
|
||||
```
|
||||
|
||||
切换为只读模式(拒绝所有写操作):
|
||||
|
||||
```bash
|
||||
curl -X PUT http://127.0.0.1:18080/api/v1/mode \
|
||||
-d '{"mode": "readonly"}'
|
||||
```
|
||||
|
||||
切换回初始化模式(允许读写):
|
||||
|
||||
```bash
|
||||
curl -X PUT http://127.0.0.1:18080/api/v1/mode \
|
||||
-d '{"mode": "init"}'
|
||||
```
|
||||
|
||||
### YAML 支持
|
||||
|
||||
所有端点同时支持 JSON 和 YAML,格式由文件扩展名自动判断:
|
||||
|
||||
```bash
|
||||
curl -X PUT http://127.0.0.1:18080/api/v1/config/app.yaml \
|
||||
-H "Content-Type: text/yaml" \
|
||||
-d '
|
||||
database:
|
||||
host: localhost
|
||||
port: 5432
|
||||
'
|
||||
```
|
||||
|
||||
### 安全模型
|
||||
|
||||
服务默认仅监听 `127.0.0.1`,不暴露到外部网络。远程访问通过 SSH 隧道实现:
|
||||
|
||||
```bash
|
||||
ssh -L 18080:127.0.0.1:18080 user@server
|
||||
```
|
||||
|
||||
之后本地即可通过 `http://127.0.0.1:18080` 访问。
|
||||
|
||||
### 项目结构
|
||||
|
||||
```
|
||||
├── main.go # 入口:环境变量、初始化、优雅关闭
|
||||
├── config/
|
||||
│ ├── validation.go # 路径校验,防止目录穿越
|
||||
│ ├── parser.go # JSON/YAML 解析、序列化、深度合并
|
||||
│ ├── atomic.go # 原子写入(temp → fsync → rename)
|
||||
│ └── backup.go # 时间戳备份、清理、回滚
|
||||
├── audit/
|
||||
│ └── logger.go # 结构化 JSON 审计日志
|
||||
├── server/
|
||||
│ ├── server.go # HTTP 服务、路由、模式状态机
|
||||
│ ├── middleware.go # 请求日志中间件
|
||||
│ └── handlers.go # API 处理函数
|
||||
├── Dockerfile # 多阶段构建
|
||||
└── docker-compose.yaml # 示例部署配置
|
||||
```
|
||||
46
AbstractWizard/audit/logger.go
Normal file
46
AbstractWizard/audit/logger.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package audit
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Entry represents a single audit log record.
|
||||
type Entry struct {
|
||||
Timestamp string `json:"timestamp"`
|
||||
RemoteIP string `json:"remote_ip"`
|
||||
Action string `json:"action"`
|
||||
Path string `json:"path"`
|
||||
Summary string `json:"summary"`
|
||||
}
|
||||
|
||||
// Logger writes structured audit entries as JSON to stdout.
|
||||
type Logger struct {
|
||||
out *log.Logger
|
||||
}
|
||||
|
||||
// NewLogger creates a new audit logger writing to stdout.
|
||||
func NewLogger() *Logger {
|
||||
return &Logger{
|
||||
out: log.New(os.Stdout, "", 0),
|
||||
}
|
||||
}
|
||||
|
||||
// Log writes a structured audit entry.
|
||||
func (l *Logger) Log(remoteIP, action, path, summary string) {
|
||||
entry := Entry{
|
||||
Timestamp: time.Now().UTC().Format(time.RFC3339),
|
||||
RemoteIP: remoteIP,
|
||||
Action: action,
|
||||
Path: path,
|
||||
Summary: summary,
|
||||
}
|
||||
data, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
l.out.Printf(`{"error":"failed to marshal audit entry: %v"}`, err)
|
||||
return
|
||||
}
|
||||
l.out.Print(string(data))
|
||||
}
|
||||
55
AbstractWizard/config/atomic.go
Normal file
55
AbstractWizard/config/atomic.go
Normal file
@@ -0,0 +1,55 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
// AtomicWrite writes data to the target path atomically by writing to a temp file
|
||||
// in the same directory, fsyncing, then renaming over the target.
|
||||
func AtomicWrite(path string, data []byte, perm os.FileMode) error {
|
||||
dir := filepath.Dir(path)
|
||||
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return fmt.Errorf("create directory: %w", err)
|
||||
}
|
||||
|
||||
tmp, err := os.CreateTemp(dir, ".tmp-*")
|
||||
if err != nil {
|
||||
return fmt.Errorf("create temp file: %w", err)
|
||||
}
|
||||
tmpName := tmp.Name()
|
||||
|
||||
cleanup := func() {
|
||||
tmp.Close()
|
||||
os.Remove(tmpName)
|
||||
}
|
||||
|
||||
if _, err := tmp.Write(data); err != nil {
|
||||
cleanup()
|
||||
return fmt.Errorf("write temp file: %w", err)
|
||||
}
|
||||
|
||||
if err := tmp.Sync(); err != nil {
|
||||
cleanup()
|
||||
return fmt.Errorf("fsync temp file: %w", err)
|
||||
}
|
||||
|
||||
if err := tmp.Close(); err != nil {
|
||||
os.Remove(tmpName)
|
||||
return fmt.Errorf("close temp file: %w", err)
|
||||
}
|
||||
|
||||
if err := os.Chmod(tmpName, perm); err != nil {
|
||||
os.Remove(tmpName)
|
||||
return fmt.Errorf("chmod temp file: %w", err)
|
||||
}
|
||||
|
||||
if err := os.Rename(tmpName, path); err != nil {
|
||||
os.Remove(tmpName)
|
||||
return fmt.Errorf("rename temp to target: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
156
AbstractWizard/config/backup.go
Normal file
156
AbstractWizard/config/backup.go
Normal file
@@ -0,0 +1,156 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const backupDirName = ".backups"
|
||||
|
||||
// BackupInfo holds metadata about a single backup.
|
||||
type BackupInfo struct {
|
||||
Version string `json:"version"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// CreateBackup copies the current file into a timestamped backup before modification.
|
||||
// Returns the backup version string. If the source file doesn't exist, returns "" with no error.
|
||||
func CreateBackup(baseDir, relPath string, maxBackups int) (string, error) {
|
||||
srcPath, err := FullPath(baseDir, relPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if _, err := os.Stat(srcPath); os.IsNotExist(err) {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(srcPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("read source for backup: %w", err)
|
||||
}
|
||||
|
||||
backupDir := filepath.Join(baseDir, backupDirName, filepath.Dir(filepath.Clean(relPath)))
|
||||
if err := os.MkdirAll(backupDir, 0o755); err != nil {
|
||||
return "", fmt.Errorf("create backup dir: %w", err)
|
||||
}
|
||||
|
||||
version := time.Now().UTC().Format("20060102T150405Z")
|
||||
base := filepath.Base(relPath)
|
||||
ext := filepath.Ext(base)
|
||||
name := strings.TrimSuffix(base, ext)
|
||||
backupName := fmt.Sprintf("%s.%s%s", name, version, ext)
|
||||
backupPath := filepath.Join(backupDir, backupName)
|
||||
|
||||
if err := AtomicWrite(backupPath, data, 0o644); err != nil {
|
||||
return "", fmt.Errorf("write backup: %w", err)
|
||||
}
|
||||
|
||||
if err := pruneBackups(backupDir, name, ext, maxBackups); err != nil {
|
||||
return "", fmt.Errorf("prune backups: %w", err)
|
||||
}
|
||||
|
||||
return version, nil
|
||||
}
|
||||
|
||||
// ListBackups returns all backup versions for a given config file, newest first.
|
||||
func ListBackups(baseDir, relPath string) ([]BackupInfo, error) {
|
||||
if err := ValidatePath(relPath); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
backupDir := filepath.Join(baseDir, backupDirName, filepath.Dir(filepath.Clean(relPath)))
|
||||
base := filepath.Base(relPath)
|
||||
ext := filepath.Ext(base)
|
||||
name := strings.TrimSuffix(base, ext)
|
||||
prefix := name + "."
|
||||
|
||||
entries, err := os.ReadDir(backupDir)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("read backup dir: %w", err)
|
||||
}
|
||||
|
||||
var backups []BackupInfo
|
||||
for _, e := range entries {
|
||||
fname := e.Name()
|
||||
if !strings.HasPrefix(fname, prefix) || !strings.HasSuffix(fname, ext) {
|
||||
continue
|
||||
}
|
||||
version := strings.TrimSuffix(strings.TrimPrefix(fname, prefix), ext)
|
||||
backups = append(backups, BackupInfo{
|
||||
Version: version,
|
||||
Timestamp: version,
|
||||
Path: filepath.Join(backupDir, fname),
|
||||
})
|
||||
}
|
||||
|
||||
sort.Slice(backups, func(i, j int) bool {
|
||||
return backups[i].Version > backups[j].Version
|
||||
})
|
||||
|
||||
return backups, nil
|
||||
}
|
||||
|
||||
// Rollback restores a config file to the given backup version.
|
||||
func Rollback(baseDir, relPath, version string) error {
|
||||
if err := ValidatePath(relPath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
backupDir := filepath.Join(baseDir, backupDirName, filepath.Dir(filepath.Clean(relPath)))
|
||||
base := filepath.Base(relPath)
|
||||
ext := filepath.Ext(base)
|
||||
name := strings.TrimSuffix(base, ext)
|
||||
backupName := fmt.Sprintf("%s.%s%s", name, version, ext)
|
||||
backupPath := filepath.Join(backupDir, backupName)
|
||||
|
||||
data, err := os.ReadFile(backupPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("read backup %q: %w", version, err)
|
||||
}
|
||||
|
||||
targetPath, err := FullPath(baseDir, relPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return AtomicWrite(targetPath, data, 0o644)
|
||||
}
|
||||
|
||||
// pruneBackups removes oldest backups exceeding maxBackups.
|
||||
func pruneBackups(backupDir, name, ext string, maxBackups int) error {
|
||||
if maxBackups <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
prefix := name + "."
|
||||
entries, err := os.ReadDir(backupDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var matching []string
|
||||
for _, e := range entries {
|
||||
if strings.HasPrefix(e.Name(), prefix) && strings.HasSuffix(e.Name(), ext) {
|
||||
matching = append(matching, e.Name())
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(matching)
|
||||
|
||||
if len(matching) > maxBackups {
|
||||
for _, old := range matching[:len(matching)-maxBackups] {
|
||||
os.Remove(filepath.Join(backupDir, old))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
83
AbstractWizard/config/parser.go
Normal file
83
AbstractWizard/config/parser.go
Normal file
@@ -0,0 +1,83 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
// Format represents a config file format.
|
||||
type Format int
|
||||
|
||||
const (
|
||||
FormatJSON Format = iota
|
||||
FormatYAML
|
||||
)
|
||||
|
||||
// DetectFormat returns the format based on file extension.
|
||||
func DetectFormat(path string) (Format, error) {
|
||||
ext := strings.ToLower(filepath.Ext(path))
|
||||
switch ext {
|
||||
case ".json":
|
||||
return FormatJSON, nil
|
||||
case ".yaml", ".yml":
|
||||
return FormatYAML, nil
|
||||
default:
|
||||
return 0, fmt.Errorf("unsupported format: %s", ext)
|
||||
}
|
||||
}
|
||||
|
||||
// Parse deserializes raw bytes into a map based on the given format.
|
||||
func Parse(data []byte, format Format) (map[string]any, error) {
|
||||
var result map[string]any
|
||||
switch format {
|
||||
case FormatJSON:
|
||||
if err := json.Unmarshal(data, &result); err != nil {
|
||||
return nil, fmt.Errorf("invalid JSON: %w", err)
|
||||
}
|
||||
case FormatYAML:
|
||||
if err := yaml.Unmarshal(data, &result); err != nil {
|
||||
return nil, fmt.Errorf("invalid YAML: %w", err)
|
||||
}
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown format")
|
||||
}
|
||||
if result == nil {
|
||||
result = make(map[string]any)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// Serialize converts a map back to bytes in the given format.
|
||||
func Serialize(data map[string]any, format Format) ([]byte, error) {
|
||||
switch format {
|
||||
case FormatJSON:
|
||||
return json.MarshalIndent(data, "", " ")
|
||||
case FormatYAML:
|
||||
return yaml.Marshal(data)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown format")
|
||||
}
|
||||
}
|
||||
|
||||
// DeepMerge merges src into dst recursively. Values in src override those in dst.
|
||||
// Nested maps are merged; all other types are replaced.
|
||||
func DeepMerge(dst, src map[string]any) map[string]any {
|
||||
out := make(map[string]any, len(dst))
|
||||
for k, v := range dst {
|
||||
out[k] = v
|
||||
}
|
||||
for k, v := range src {
|
||||
if srcMap, ok := v.(map[string]any); ok {
|
||||
if dstMap, ok := out[k].(map[string]any); ok {
|
||||
out[k] = DeepMerge(dstMap, srcMap)
|
||||
continue
|
||||
}
|
||||
}
|
||||
out[k] = v
|
||||
}
|
||||
return out
|
||||
}
|
||||
49
AbstractWizard/config/validation.go
Normal file
49
AbstractWizard/config/validation.go
Normal file
@@ -0,0 +1,49 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var allowedExtensions = map[string]bool{
|
||||
".json": true,
|
||||
".yaml": true,
|
||||
".yml": true,
|
||||
}
|
||||
|
||||
// ValidatePath checks that the given relative path is safe and points to a supported file type.
|
||||
// It prevents directory traversal and rejects unsupported extensions.
|
||||
func ValidatePath(relPath string) error {
|
||||
if relPath == "" {
|
||||
return fmt.Errorf("empty path")
|
||||
}
|
||||
|
||||
cleaned := filepath.Clean(relPath)
|
||||
|
||||
if filepath.IsAbs(cleaned) {
|
||||
return fmt.Errorf("absolute paths not allowed")
|
||||
}
|
||||
|
||||
for _, part := range strings.Split(cleaned, string(filepath.Separator)) {
|
||||
if part == ".." {
|
||||
return fmt.Errorf("path traversal not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(cleaned))
|
||||
if !allowedExtensions[ext] {
|
||||
return fmt.Errorf("unsupported file extension %q; allowed: .json, .yaml, .yml", ext)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// FullPath joins the base config directory with the validated relative path.
|
||||
func FullPath(baseDir, relPath string) (string, error) {
|
||||
if err := ValidatePath(relPath); err != nil {
|
||||
return "", err
|
||||
}
|
||||
full := filepath.Join(baseDir, filepath.Clean(relPath))
|
||||
return full, nil
|
||||
}
|
||||
14
AbstractWizard/docker-compose.yaml
Normal file
14
AbstractWizard/docker-compose.yaml
Normal file
@@ -0,0 +1,14 @@
|
||||
services:
|
||||
abstract-wizard:
|
||||
build: .
|
||||
ports:
|
||||
- "127.0.0.1:18080:8080"
|
||||
volumes:
|
||||
- app_config:/config
|
||||
environment:
|
||||
CONFIG_DIR: /config
|
||||
LISTEN_ADDR: "0.0.0.0:8080"
|
||||
MAX_BACKUPS: "10"
|
||||
|
||||
volumes:
|
||||
app_config:
|
||||
5
AbstractWizard/go.mod
Normal file
5
AbstractWizard/go.mod
Normal file
@@ -0,0 +1,5 @@
|
||||
module AbstractWizard
|
||||
|
||||
go 1.24
|
||||
|
||||
require gopkg.in/yaml.v3 v3.0.1
|
||||
4
AbstractWizard/go.sum
Normal file
4
AbstractWizard/go.sum
Normal file
@@ -0,0 +1,4 @@
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
51
AbstractWizard/main.go
Normal file
51
AbstractWizard/main.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"AbstractWizard/audit"
|
||||
"AbstractWizard/server"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := server.AppConfig{
|
||||
ConfigDir: envOrDefault("CONFIG_DIR", "/config"),
|
||||
ListenAddr: envOrDefault("LISTEN_ADDR", "127.0.0.1:8080"),
|
||||
MaxBackups: envOrDefaultInt("MAX_BACKUPS", 10),
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(cfg.ConfigDir, 0o755); err != nil {
|
||||
log.Fatalf("failed to create config directory %s: %v", cfg.ConfigDir, err)
|
||||
}
|
||||
|
||||
auditLog := audit.NewLogger()
|
||||
srv := server.New(cfg, auditLog)
|
||||
|
||||
log.Printf("config_dir=%s listen_addr=%s max_backups=%d", cfg.ConfigDir, cfg.ListenAddr, cfg.MaxBackups)
|
||||
|
||||
if err := srv.ListenAndServe(); err != nil {
|
||||
log.Fatalf("server error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func envOrDefault(key, defaultVal string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
func envOrDefaultInt(key string, defaultVal int) int {
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
return defaultVal
|
||||
}
|
||||
n, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
log.Printf("warning: invalid %s=%q, using default %d", key, v, defaultVal)
|
||||
return defaultVal
|
||||
}
|
||||
return n
|
||||
}
|
||||
265
AbstractWizard/server/handlers.go
Normal file
265
AbstractWizard/server/handlers.go
Normal file
@@ -0,0 +1,265 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
|
||||
"AbstractWizard/config"
|
||||
)
|
||||
|
||||
func (s *Server) handleHealth(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||
}
|
||||
|
||||
func (s *Server) handleGetConfig(w http.ResponseWriter, r *http.Request) {
|
||||
relPath := r.PathValue("path")
|
||||
|
||||
fullPath, err := config.FullPath(s.cfg.ConfigDir, relPath)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(fullPath)
|
||||
if os.IsNotExist(err) {
|
||||
writeJSON(w, http.StatusNotFound, map[string]string{"error": "file not found"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to read file"})
|
||||
return
|
||||
}
|
||||
|
||||
format, err := config.DetectFormat(fullPath)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
parsed, err := config.Parse(data, format)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
s.audit.Log(r.RemoteAddr, "read", relPath, "config read")
|
||||
writeJSON(w, http.StatusOK, parsed)
|
||||
}
|
||||
|
||||
func (s *Server) handlePutConfig(w http.ResponseWriter, r *http.Request) {
|
||||
if s.GetMode() == ModeReadOnly {
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": "server is in readonly mode"})
|
||||
return
|
||||
}
|
||||
|
||||
relPath := r.PathValue("path")
|
||||
|
||||
fullPath, err := config.FullPath(s.cfg.ConfigDir, relPath)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(io.LimitReader(r.Body, 10<<20)) // 10 MB limit
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "failed to read request body"})
|
||||
return
|
||||
}
|
||||
|
||||
format, err := config.DetectFormat(fullPath)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Validate that the body is valid config
|
||||
if _, err := config.Parse(body, format); err != nil {
|
||||
writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
version, err := config.CreateBackup(s.cfg.ConfigDir, relPath, s.cfg.MaxBackups)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "backup failed: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := config.AtomicWrite(fullPath, body, 0o644); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "write failed: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
s.audit.Log(r.RemoteAddr, "replace", relPath, "full replace")
|
||||
resp := map[string]string{"status": "ok"}
|
||||
if version != "" {
|
||||
resp["backup_version"] = version
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (s *Server) handlePatchConfig(w http.ResponseWriter, r *http.Request) {
|
||||
if s.GetMode() == ModeReadOnly {
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": "server is in readonly mode"})
|
||||
return
|
||||
}
|
||||
|
||||
relPath := r.PathValue("path")
|
||||
|
||||
fullPath, err := config.FullPath(s.cfg.ConfigDir, relPath)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
format, err := config.DetectFormat(fullPath)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// Read existing config
|
||||
existing := make(map[string]any)
|
||||
data, err := os.ReadFile(fullPath)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "failed to read existing file"})
|
||||
return
|
||||
}
|
||||
if err == nil {
|
||||
existing, err = config.Parse(data, format)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "existing file is invalid: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Parse patch body
|
||||
body, err := io.ReadAll(io.LimitReader(r.Body, 10<<20))
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "failed to read request body"})
|
||||
return
|
||||
}
|
||||
|
||||
patch, err := config.Parse(body, format)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
merged := config.DeepMerge(existing, patch)
|
||||
|
||||
out, err := config.Serialize(merged, format)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "serialization failed"})
|
||||
return
|
||||
}
|
||||
|
||||
version, err := config.CreateBackup(s.cfg.ConfigDir, relPath, s.cfg.MaxBackups)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "backup failed: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := config.AtomicWrite(fullPath, out, 0o644); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "write failed"})
|
||||
return
|
||||
}
|
||||
|
||||
s.audit.Log(r.RemoteAddr, "patch", relPath, "deep merge update")
|
||||
resp := map[string]string{"status": "ok"}
|
||||
if version != "" {
|
||||
resp["backup_version"] = version
|
||||
}
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (s *Server) handleListBackups(w http.ResponseWriter, r *http.Request) {
|
||||
relPath := r.PathValue("path")
|
||||
|
||||
backups, err := config.ListBackups(s.cfg.ConfigDir, relPath)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
type backupResponse struct {
|
||||
Version string `json:"version"`
|
||||
Timestamp string `json:"timestamp"`
|
||||
}
|
||||
var resp []backupResponse
|
||||
for _, b := range backups {
|
||||
resp = append(resp, backupResponse{Version: b.Version, Timestamp: b.Timestamp})
|
||||
}
|
||||
if resp == nil {
|
||||
resp = []backupResponse{}
|
||||
}
|
||||
|
||||
s.audit.Log(r.RemoteAddr, "list_backups", relPath, "listed backups")
|
||||
writeJSON(w, http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (s *Server) handleRollback(w http.ResponseWriter, r *http.Request) {
|
||||
if s.GetMode() == ModeReadOnly {
|
||||
writeJSON(w, http.StatusForbidden, map[string]string{"error": "server is in readonly mode"})
|
||||
return
|
||||
}
|
||||
|
||||
relPath := r.PathValue("path")
|
||||
|
||||
var body struct {
|
||||
Version string `json:"version"`
|
||||
}
|
||||
if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
|
||||
return
|
||||
}
|
||||
if body.Version == "" {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "version is required"})
|
||||
return
|
||||
}
|
||||
|
||||
// Backup current state before rollback
|
||||
if _, err := config.CreateBackup(s.cfg.ConfigDir, relPath, s.cfg.MaxBackups); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "pre-rollback backup failed: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := config.Rollback(s.cfg.ConfigDir, relPath, body.Version); err != nil {
|
||||
writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "rollback failed: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
s.audit.Log(r.RemoteAddr, "rollback", relPath, "rolled back to "+body.Version)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok", "restored_version": body.Version})
|
||||
}
|
||||
|
||||
func (s *Server) handleGetMode(w http.ResponseWriter, r *http.Request) {
|
||||
writeJSON(w, http.StatusOK, map[string]string{"mode": s.GetMode().String()})
|
||||
}
|
||||
|
||||
func (s *Server) handleSetMode(w http.ResponseWriter, r *http.Request) {
|
||||
var body struct {
|
||||
Mode string `json:"mode"`
|
||||
}
|
||||
if err := json.NewDecoder(io.LimitReader(r.Body, 1<<20)).Decode(&body); err != nil {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
|
||||
return
|
||||
}
|
||||
|
||||
mode, ok := ParseMode(body.Mode)
|
||||
if !ok {
|
||||
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid mode; must be 'init' or 'readonly'"})
|
||||
return
|
||||
}
|
||||
|
||||
s.SetMode(mode)
|
||||
s.audit.Log(r.RemoteAddr, "set_mode", "", "mode changed to "+body.Mode)
|
||||
writeJSON(w, http.StatusOK, map[string]string{"status": "ok", "mode": mode.String()})
|
||||
}
|
||||
|
||||
func writeJSON(w http.ResponseWriter, status int, v any) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(status)
|
||||
json.NewEncoder(w).Encode(v)
|
||||
}
|
||||
27
AbstractWizard/server/middleware.go
Normal file
27
AbstractWizard/server/middleware.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type responseRecorder struct {
|
||||
http.ResponseWriter
|
||||
statusCode int
|
||||
}
|
||||
|
||||
func (r *responseRecorder) WriteHeader(code int) {
|
||||
r.statusCode = code
|
||||
r.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
// LoggingMiddleware logs each incoming request with method, path, status, and duration.
|
||||
func LoggingMiddleware(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
rec := &responseRecorder{ResponseWriter: w, statusCode: http.StatusOK}
|
||||
next.ServeHTTP(rec, r)
|
||||
log.Printf("%s %s %d %s", r.Method, r.URL.Path, rec.statusCode, time.Since(start).Round(time.Millisecond))
|
||||
})
|
||||
}
|
||||
137
AbstractWizard/server/server.go
Normal file
137
AbstractWizard/server/server.go
Normal file
@@ -0,0 +1,137 @@
|
||||
package server
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"AbstractWizard/audit"
|
||||
)
|
||||
|
||||
// Mode represents the server operating mode.
|
||||
type Mode int
|
||||
|
||||
const (
|
||||
ModeInit Mode = iota // init: read/write allowed
|
||||
ModeReadOnly // readonly: only reads allowed
|
||||
)
|
||||
|
||||
func (m Mode) String() string {
|
||||
switch m {
|
||||
case ModeInit:
|
||||
return "init"
|
||||
case ModeReadOnly:
|
||||
return "readonly"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// ParseMode converts a string to a Mode.
|
||||
func ParseMode(s string) (Mode, bool) {
|
||||
switch s {
|
||||
case "init":
|
||||
return ModeInit, true
|
||||
case "readonly":
|
||||
return ModeReadOnly, true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
|
||||
// AppConfig holds environment-based configuration.
|
||||
type AppConfig struct {
|
||||
ConfigDir string
|
||||
ListenAddr string
|
||||
MaxBackups int
|
||||
}
|
||||
|
||||
// Server is the main HTTP server.
|
||||
type Server struct {
|
||||
cfg AppConfig
|
||||
audit *audit.Logger
|
||||
mode Mode
|
||||
modeMu sync.RWMutex
|
||||
srv *http.Server
|
||||
}
|
||||
|
||||
// New creates a new Server.
|
||||
func New(cfg AppConfig, auditLog *audit.Logger) *Server {
|
||||
s := &Server{
|
||||
cfg: cfg,
|
||||
audit: auditLog,
|
||||
mode: ModeInit,
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
s.registerRoutes(mux)
|
||||
|
||||
s.srv = &http.Server{
|
||||
Addr: cfg.ListenAddr,
|
||||
Handler: LoggingMiddleware(mux),
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// registerRoutes sets up all API routes.
|
||||
func (s *Server) registerRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /health", s.handleHealth)
|
||||
mux.HandleFunc("GET /api/v1/config/{path...}", s.handleGetConfig)
|
||||
mux.HandleFunc("PUT /api/v1/config/{path...}", s.handlePutConfig)
|
||||
mux.HandleFunc("PATCH /api/v1/config/{path...}", s.handlePatchConfig)
|
||||
mux.HandleFunc("GET /api/v1/backups/{path...}", s.handleListBackups)
|
||||
mux.HandleFunc("POST /api/v1/rollback/{path...}", s.handleRollback)
|
||||
mux.HandleFunc("GET /api/v1/mode", s.handleGetMode)
|
||||
mux.HandleFunc("PUT /api/v1/mode", s.handleSetMode)
|
||||
}
|
||||
|
||||
// GetMode returns the current server mode.
|
||||
func (s *Server) GetMode() Mode {
|
||||
s.modeMu.RLock()
|
||||
defer s.modeMu.RUnlock()
|
||||
return s.mode
|
||||
}
|
||||
|
||||
// SetMode changes the server mode.
|
||||
func (s *Server) SetMode(m Mode) {
|
||||
s.modeMu.Lock()
|
||||
defer s.modeMu.Unlock()
|
||||
s.mode = m
|
||||
}
|
||||
|
||||
// ListenAndServe starts the HTTP server and blocks until shutdown.
|
||||
func (s *Server) ListenAndServe() error {
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
log.Printf("listening on %s", s.cfg.ListenAddr)
|
||||
if err := s.srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
errCh <- err
|
||||
}
|
||||
close(errCh)
|
||||
}()
|
||||
|
||||
select {
|
||||
case sig := <-stop:
|
||||
log.Printf("received signal %v, shutting down", sig)
|
||||
case err := <-errCh:
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
return s.srv.Shutdown(ctx)
|
||||
}
|
||||
10
DependencyExtractor/.idea/DependencyExtractor.iml
generated
Normal file
10
DependencyExtractor/.idea/DependencyExtractor.iml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.12 (DependencyExtractor)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
57
Dialectic/docker-compose.yaml
Normal file
57
Dialectic/docker-compose.yaml
Normal file
@@ -0,0 +1,57 @@
|
||||
services:
|
||||
frontend:
|
||||
build:
|
||||
context: ./Dialectic.Frontend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "${DIALECTIC_FRONTEND_PORT:-3000}:3000"
|
||||
environment:
|
||||
- REACT_APP_DIALECTIC_BACKEND_HOST=${DIALECTIC_BACKEND_URL:-http://localhost:8000}
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
backend:
|
||||
build:
|
||||
context: ./Dialectic.Backend
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "${DIALECTIC_BACKEND_PORT:-8000}:8000"
|
||||
- "127.0.0.1:${DIALECTIC_CONFIG_PORT:-9090}:9090"
|
||||
volumes:
|
||||
- dialectic_config:/app/config
|
||||
environment:
|
||||
- ENCRYPTION_KEY=${ENCRYPTION_KEY:-BgS9_9K2UMYxiYnP-BE63UEdi7a6PHaaZ6rQZQnSx54=}
|
||||
- CONFIG_PATH=/app/config/dialectic.yaml
|
||||
- CONFIG_PORT=${DIALECTIC_CONFIG_PORT:-9090}
|
||||
depends_on:
|
||||
db:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- app-network
|
||||
|
||||
db:
|
||||
image: mysql:8.0
|
||||
restart: always
|
||||
environment:
|
||||
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD:-rootpassword}
|
||||
MYSQL_DATABASE: ${MYSQL_DATABASE:-dialectic}
|
||||
MYSQL_USER: ${MYSQL_USER:-dialectic}
|
||||
MYSQL_PASSWORD: ${MYSQL_PASSWORD:-dialectic}
|
||||
ports:
|
||||
- "${DB_PORT:-3306}:3306"
|
||||
volumes:
|
||||
- mysql_data:/var/lib/mysql
|
||||
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
|
||||
healthcheck:
|
||||
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
|
||||
timeout: 20s
|
||||
retries: 10
|
||||
networks:
|
||||
- app-network
|
||||
volumes:
|
||||
mysql_data:
|
||||
dialectic_config:
|
||||
|
||||
networks:
|
||||
app-network:
|
||||
driver: bridge
|
||||
47
Dialectic/init.sql
Normal file
47
Dialectic/init.sql
Normal file
@@ -0,0 +1,47 @@
|
||||
-- MySQL initialization script for Dialectic
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS dialectic;
|
||||
USE dialectic;
|
||||
|
||||
-- Table for storing API keys for different providers
|
||||
CREATE TABLE IF NOT EXISTS api_keys (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
provider VARCHAR(50) NOT NULL UNIQUE,
|
||||
api_key_encrypted TEXT NOT NULL,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Table for storing model configurations
|
||||
CREATE TABLE IF NOT EXISTS model_configs (
|
||||
id INT AUTO_INCREMENT PRIMARY KEY,
|
||||
provider VARCHAR(50) NOT NULL,
|
||||
model_name VARCHAR(100) NOT NULL,
|
||||
display_name VARCHAR(100),
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||||
UNIQUE KEY unique_provider_model (provider, model_name)
|
||||
);
|
||||
|
||||
-- Insert default API key records (encrypted field will be empty initially)
|
||||
INSERT IGNORE INTO api_keys (provider, api_key_encrypted) VALUES
|
||||
('openai', ''),
|
||||
('claude', ''),
|
||||
('qwen', ''),
|
||||
('deepseek', '');
|
||||
|
||||
-- Insert default model configurations
|
||||
INSERT IGNORE INTO model_configs (provider, model_name, display_name) VALUES
|
||||
('openai', 'gpt-4', 'GPT-4'),
|
||||
('openai', 'gpt-3.5-turbo', 'GPT-3.5 Turbo'),
|
||||
('claude', 'claude-3-opus', 'Claude 3 Opus'),
|
||||
('claude', 'claude-3-sonnet', 'Claude 3 Sonnet'),
|
||||
('claude', 'claude-3-haiku', 'Claude 3 Haiku'),
|
||||
('qwen', 'qwen-max', 'Qwen Max'),
|
||||
('qwen', 'qwen-plus', 'Qwen Plus'),
|
||||
('deepseek', 'deepseek-chat', 'DeepSeek Chat'),
|
||||
('deepseek', 'deepseek-coder', 'DeepSeek Coder');
|
||||
|
||||
-- Add evidence_library column to debate_sessions (migration for existing databases)
|
||||
ALTER TABLE debate_sessions ADD COLUMN IF NOT EXISTS evidence_library TEXT NULL;
|
||||
115
DungeonSolver.Workspace/CLAUDE.md
Normal file
115
DungeonSolver.Workspace/CLAUDE.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# CLAUDE.md
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Workspace Architecture
|
||||
|
||||
**DungeonSolver.Workspace** is a multi-project C# solution (.NET 8.0) organized as a modular game development workspace. Each project serves a specific purpose and can be built independently.
|
||||
|
||||
### Project Structure
|
||||
|
||||
- **DungeonSolver/**: Main Godot 4.5 game project with Python integration (see `DungeonSolver/CLAUDE.md` for detailed game-specific guidance)
|
||||
- **DungeonSolver.Global/**: Core game systems and global providers, outputs to `DungeonSolver/Data/` for game consumption
|
||||
- **DungeonSolver.PythonBridge/**: Python integration layer using IronPython/pythonnet
|
||||
- **DungeonSolver.Game/**: Game-specific logic and systems
|
||||
- **DungeonSolver.Generators/**: Roslyn source generators for automated code generation
|
||||
- **DungeonSolver.Global.Generators/**: Source generators specifically for the Global project
|
||||
- **DungeonSolver.Tasks/**: Build tasks and utilities
|
||||
|
||||
### Key Architectural Patterns
|
||||
|
||||
**Modular Project Design**: Each component is isolated in its own project with specific responsibilities:
|
||||
- Global project compiles to `DungeonSolver/Data/` for runtime access
|
||||
- Source generators use `OutputItemType="Analyzer" ReferenceOutputAssembly="false"` pattern
|
||||
- Python bridge allows C# game systems to be accessed from Python via `clr.AddReference()`
|
||||
|
||||
**Source Generator Integration**: Projects use Roslyn source generators for code generation:
|
||||
- Generators project contains sample incremental source generators
|
||||
- Target classes use `[Generators.ReportAttribute]` for report generation
|
||||
- Build generators to see generated code in IDE
|
||||
|
||||
**Python-C# Interop**:
|
||||
- **Python Package Generator**: Automatically generates Python wrapper classes for C# types marked with `[PythonSharedType]` attributes
|
||||
- Generates type-safe Python interfaces from C# classes, interfaces, and enums
|
||||
- Handles complex generic types and nested class hierarchies
|
||||
- Supports inheritance, member access, and method calls with proper type hints
|
||||
- Generated Python packages are output to `user://dungeon_solver/types/`
|
||||
- **Type Registration System**: Uses `PythonSharedTypeRegister` to map C# types to Python equivalents
|
||||
- **Shared Memory Communication**: Advanced IPC system for real-time C#-Python communication (see `Documents/SharedMemoryCommunicationProtocol.md`)
|
||||
- **Runtime Access**: Python scripts access C# via `DungeonSolver.Global.dll` using IronPython/pythonnet
|
||||
- **Bridge Pattern**: Example implementation in `DungeonSolver/Data/Solver.py`
|
||||
|
||||
## Build and Development Commands
|
||||
|
||||
**Building Individual Projects**:
|
||||
```bash
|
||||
# Build specific project
|
||||
dotnet build DungeonSolver.Global/
|
||||
dotnet build DungeonSolver.Generators/
|
||||
dotnet build DungeonSolver.PythonBridge/
|
||||
|
||||
# Build main game project (includes all dependencies)
|
||||
cd DungeonSolver && dotnet build
|
||||
```
|
||||
|
||||
**Building the Entire Workspace**:
|
||||
```bash
|
||||
# Build all projects from workspace root
|
||||
dotnet build DungeonSolver/DungeonSolver.sln
|
||||
```
|
||||
|
||||
**Source Generator Development**:
|
||||
```bash
|
||||
# Build generators to see generated code
|
||||
dotnet build DungeonSolver.Generators/
|
||||
dotnet build DungeonSolver.Global.Generators/
|
||||
```
|
||||
|
||||
**Testing**:
|
||||
```bash
|
||||
# Run generator unit tests
|
||||
cd DungeonSolver.Generators && dotnet test
|
||||
cd DungeonSolver.Global.Generators && dotnet test
|
||||
```
|
||||
|
||||
## Code Style Guidelines
|
||||
|
||||
**C# Coding Standards**:
|
||||
- **No `var` keyword**: Always use explicit types instead of `var`
|
||||
```csharp
|
||||
string name = "example"; // Correct
|
||||
List<int> numbers = new List<int>(); // Correct
|
||||
// var name = "example"; // Wrong
|
||||
```
|
||||
- **No comments**: Do not write any comments in code (XML documentation, inline comments, or block comments)
|
||||
- Write self-documenting code with clear variable and method names
|
||||
|
||||
## Development Workflow Notes
|
||||
|
||||
**Project Dependencies**: The main solution (`DungeonSolver/DungeonSolver.sln`) references all other projects, creating a unified build process while maintaining modular development.
|
||||
|
||||
**Global System Access**: Python scripts can access the Global project's compiled output via the established bridge pattern in `Data/Solver.py`.
|
||||
|
||||
**Source Generator Workflow**:
|
||||
1. Modify generator code in `*.Generators` projects
|
||||
2. Build generator project to compile changes
|
||||
3. Target projects automatically regenerate code during their build
|
||||
4. Use Rossynt plugin for syntax tree analysis during development
|
||||
|
||||
**Cross-Project Development**: Each project can be developed and tested independently, with the main game project serving as the integration point.
|
||||
|
||||
**Python Type System Development**:
|
||||
1. Mark C# types with appropriate `[PythonSharedType]` attributes (`[PythonSharedClass]`, `[PythonSharedInterface]`, `[PythonSharedEnum]`)
|
||||
2. Add `[PythonSharedMember]` and `[PythonSharedMethod]` attributes to expose specific members
|
||||
3. Build `DungeonSolver.Global` project to register types in `PythonSharedTypeRegister`
|
||||
4. Generated Python types automatically handle complex scenarios like:
|
||||
- Generic type parameters and constraints
|
||||
- Nested class inheritance hierarchies
|
||||
- Interface implementation and multiple inheritance
|
||||
- Enum value mapping and type safety
|
||||
|
||||
**Important Development Notes**:
|
||||
- Always build `DungeonSolver.Global` before the main game project to ensure Python bindings are up-to-date
|
||||
- When modifying Python-shared types, rebuild is required to regenerate Python wrapper classes
|
||||
- Use `PythonPackageGenerator.Generate()` to manually trigger Python code generation during development
|
||||
- Nested classes inherit sharing attributes from parent classes automatically via `NestedClassWrapper` pattern
|
||||
1
DungeonSolver.Workspace/DungeonSolver
Submodule
1
DungeonSolver.Workspace/DungeonSolver
Submodule
Submodule DungeonSolver.Workspace/DungeonSolver added at 0d81d98a35
1
DungeonSolver.Workspace/DungeonSolver.Analyzer
Submodule
1
DungeonSolver.Workspace/DungeonSolver.Analyzer
Submodule
Submodule DungeonSolver.Workspace/DungeonSolver.Analyzer added at b82ca1657e
1
DungeonSolver.Workspace/DungeonSolver.Core
Submodule
1
DungeonSolver.Workspace/DungeonSolver.Core
Submodule
Submodule DungeonSolver.Workspace/DungeonSolver.Core added at 9de186f41a
Submodule DungeonSolver.Workspace/DungeonSolver.Core.Analyzer added at d7d39d2332
Submodule DungeonSolver.Workspace/DungeonSolver.Core.Generators added at 056e924943
Submodule DungeonSolver.Workspace/DungeonSolver.Generator.Core added at 8c66583d23
1
DungeonSolver.Workspace/DungeonSolver.Generators
Submodule
1
DungeonSolver.Workspace/DungeonSolver.Generators
Submodule
Submodule DungeonSolver.Workspace/DungeonSolver.Generators added at 57afa2cc50
@@ -0,0 +1,132 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AutoGeneratedRunConfigurationManager">
|
||||
<projectFile profileName="DebugRoslynAnalyzers">DungeonSolver.Global.Analyzer/DungeonSolver.Global.Analyzer.csproj</projectFile>
|
||||
</component>
|
||||
<component name="AutoImportSettings">
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="e8a473e9-efa8-4c17-9674-1989a6c91473" name="Changes" comment="" />
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||
</component>
|
||||
<component name="MetaFilesCheckinStateConfiguration" checkMetaFiles="true" />
|
||||
<component name="ProjectColorInfo">{
|
||||
"associatedIndex": 6
|
||||
}</component>
|
||||
<component name="ProjectId" id="34mRHwSVMSZy66teK5Ol8c3JLUl" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent">{
|
||||
"keyToString": {
|
||||
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultAutoModeForALLUsers.v1": "true",
|
||||
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
|
||||
"git-widget-placeholder": "master",
|
||||
"junie.onboarding.icon.badge.shown": "true",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"to.speed.mode.migration.done": "true",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}</component>
|
||||
<component name="RunManager">
|
||||
<configuration name="DungeonSolver.Global.Analyzer: DebugRoslynAnalyzers" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/DungeonSolver.Global.Analyzer/DungeonSolver.Global.Analyzer.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value=".NETStandard,Version=v2.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="DebugRoslynAnalyzers" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<option name="AUTO_ATTACH_CHILDREN" value="0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="e8a473e9-efa8-4c17-9674-1989a6c91473" name="Changes" comment="" />
|
||||
<created>1761816676447</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1761816676447</updated>
|
||||
<workItem from="1761816677692" duration="695000" />
|
||||
<workItem from="1762861373046" duration="167000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="init">
|
||||
<option name="closed" value="true" />
|
||||
<created>1761816967095</created>
|
||||
<option name="number" value="00001" />
|
||||
<option name="presentableId" value="LOCAL-00001" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1761816967095</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00002" summary="clean">
|
||||
<option name="closed" value="true" />
|
||||
<created>1761817160460</created>
|
||||
<option name="number" value="00002" />
|
||||
<option name="presentableId" value="LOCAL-00002" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1761817160460</updated>
|
||||
</task>
|
||||
<task id="LOCAL-00003" summary="re init">
|
||||
<option name="closed" value="true" />
|
||||
<created>1761817211100</created>
|
||||
<option name="number" value="00003" />
|
||||
<option name="presentableId" value="LOCAL-00003" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1761817211100</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="4" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
<component name="UnityCheckinConfiguration" checkUnsavedScenes="true" />
|
||||
<component name="UnityProjectConfiguration" hasMinimizedUI="false" />
|
||||
<component name="VcsManagerConfiguration">
|
||||
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
|
||||
<MESSAGE value="init" />
|
||||
<MESSAGE value="clean" />
|
||||
<MESSAGE value="re init" />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="re init" />
|
||||
</component>
|
||||
<component name="XDebuggerManager">
|
||||
<breakpoint-manager>
|
||||
<breakpoints>
|
||||
<breakpoint enabled="true" type="DotNet_Exception_Breakpoints">
|
||||
<properties exception="System.OperationCanceledException" breakIfHandledByOtherCode="false" displayValue="System.OperationCanceledException" />
|
||||
<option name="timeStamp" value="1" />
|
||||
</breakpoint>
|
||||
<breakpoint enabled="true" type="DotNet_Exception_Breakpoints">
|
||||
<properties exception="System.Threading.Tasks.TaskCanceledException" breakIfHandledByOtherCode="false" displayValue="System.Threading.Tasks.TaskCanceledException" />
|
||||
<option name="timeStamp" value="2" />
|
||||
</breakpoint>
|
||||
<breakpoint enabled="true" type="DotNet_Exception_Breakpoints">
|
||||
<properties exception="System.Threading.ThreadAbortException" breakIfHandledByOtherCode="false" displayValue="System.Threading.ThreadAbortException" />
|
||||
<option name="timeStamp" value="3" />
|
||||
</breakpoint>
|
||||
</breakpoints>
|
||||
</breakpoint-manager>
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,2 @@
|
||||
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:ss="urn:shemas-jetbrains-com:settings-storage-xaml" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
|
||||
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DungeonSolver_002ECore_002EAnalyzer_002FResources/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>
|
||||
@@ -0,0 +1,120 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AutoGeneratedRunConfigurationManager">
|
||||
<projectFile profileName="DebugRoslynSourceGenerator">../DungeonSolver.Core.Generators/DungeonSolver.Core.Generators.csproj</projectFile>
|
||||
<projectFile profileName="DebugRoslynSourceGenerator">DungeonSolver.Global.Generators/DungeonSolver.Global.Generators/DungeonSolver.Global.Generators.csproj</projectFile>
|
||||
</component>
|
||||
<component name="AutoImportSettings">
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="a079404d-2a75-4a6c-acbc-be2886fe3cec" name="Changes" comment="" />
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/../DungeonSolver.Core.Generators" />
|
||||
</component>
|
||||
<component name="ProblemsViewState">
|
||||
<option name="selectedTabId" value="Toolset" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"associatedIndex": 7
|
||||
}</component>
|
||||
<component name="ProjectId" id="2okB8CsMteI0pzMz1Ui1IJ6NbpA" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultAutoModeForALLUsers.v1": "true",
|
||||
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
|
||||
"git-widget-placeholder": "master",
|
||||
"junie.onboarding.icon.badge.shown": "true",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"to.speed.mode.migration.done": "true",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}]]></component>
|
||||
<component name="RunManager" selected=".NET Launch Settings Profile.DungeonSolver.Global.Generators: DebugRoslynSourceGenerator">
|
||||
<configuration name="DungeonSolver.Core.Generators: DebugRoslynSourceGenerator" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/../DungeonSolver.Core.Generators/DungeonSolver.Core.Generators.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value=".NETStandard,Version=v2.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="DebugRoslynSourceGenerator" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<option name="AUTO_ATTACH_CHILDREN" value="0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
<configuration name="DungeonSolver.Global.Generators: DebugRoslynSourceGenerator" type="LaunchSettings" factoryName=".NET Launch Settings Profile">
|
||||
<option name="LAUNCH_PROFILE_PROJECT_FILE_PATH" value="$PROJECT_DIR$/DungeonSolver.Global.Generators/DungeonSolver.Global.Generators/DungeonSolver.Global.Generators.csproj" />
|
||||
<option name="LAUNCH_PROFILE_TFM" value=".NETStandard,Version=v2.0" />
|
||||
<option name="LAUNCH_PROFILE_NAME" value="DebugRoslynSourceGenerator" />
|
||||
<option name="USE_EXTERNAL_CONSOLE" value="0" />
|
||||
<option name="USE_MONO" value="0" />
|
||||
<option name="RUNTIME_ARGUMENTS" value="" />
|
||||
<option name="GENERATE_APPLICATIONHOST_CONFIG" value="1" />
|
||||
<option name="SHOW_IIS_EXPRESS_OUTPUT" value="0" />
|
||||
<option name="SEND_DEBUG_REQUEST" value="1" />
|
||||
<option name="ADDITIONAL_IIS_EXPRESS_ARGUMENTS" value="" />
|
||||
<option name="AUTO_ATTACH_CHILDREN" value="0" />
|
||||
<method v="2">
|
||||
<option name="Build" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="a079404d-2a75-4a6c-acbc-be2886fe3cec" name="Changes" comment="" />
|
||||
<created>1731403531514</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1731403531514</updated>
|
||||
<workItem from="1731403532630" duration="194000" />
|
||||
<workItem from="1762860510814" duration="327000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
<component name="UnityProjectConfiguration" hasMinimizedUI="false" />
|
||||
<component name="VcsManagerConfiguration">
|
||||
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
|
||||
</component>
|
||||
<component name="XDebuggerManager">
|
||||
<breakpoint-manager>
|
||||
<breakpoints>
|
||||
<breakpoint enabled="true" type="DotNet_Exception_Breakpoints">
|
||||
<properties exception="System.OperationCanceledException" breakIfHandledByOtherCode="false" displayValue="System.OperationCanceledException" />
|
||||
<option name="timeStamp" value="1" />
|
||||
</breakpoint>
|
||||
<breakpoint enabled="true" type="DotNet_Exception_Breakpoints">
|
||||
<properties exception="System.Threading.Tasks.TaskCanceledException" breakIfHandledByOtherCode="false" displayValue="System.Threading.Tasks.TaskCanceledException" />
|
||||
<option name="timeStamp" value="2" />
|
||||
</breakpoint>
|
||||
<breakpoint enabled="true" type="DotNet_Exception_Breakpoints">
|
||||
<properties exception="System.Threading.ThreadAbortException" breakIfHandledByOtherCode="false" displayValue="System.Threading.ThreadAbortException" />
|
||||
<option name="timeStamp" value="3" />
|
||||
</breakpoint>
|
||||
</breakpoints>
|
||||
</breakpoint-manager>
|
||||
</component>
|
||||
</project>
|
||||
87
DungeonSolver.Workspace/DungeonSolver.Global/.idea/.idea.DungeonSolver.Global/.idea/workspace.xml
generated
Normal file
87
DungeonSolver.Workspace/DungeonSolver.Global/.idea/.idea.DungeonSolver.Global/.idea/workspace.xml
generated
Normal file
@@ -0,0 +1,87 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="AutoImportSettings">
|
||||
<option name="autoReloadType" value="SELECTIVE" />
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="da8668bf-f36a-441e-9a5c-e37101dd3fe9" name="Changes" comment="" />
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
|
||||
<option name="LAST_RESOLUTION" value="IGNORE" />
|
||||
</component>
|
||||
<component name="Git.Settings">
|
||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$/../DungeonSolver.Core" />
|
||||
</component>
|
||||
<component name="HighlightingSettingsPerFile">
|
||||
<setting file="file://$PROJECT_DIR$/Class1.cs" root0="FORCE_HIGHLIGHTING" />
|
||||
</component>
|
||||
<component name="ProblemsViewState">
|
||||
<option name="selectedTabId" value="Toolset" />
|
||||
</component>
|
||||
<component name="ProjectColorInfo">{
|
||||
"associatedIndex": 7
|
||||
}</component>
|
||||
<component name="ProjectId" id="2mmtyf0EpF2l5xWX0Aory29SVMk" />
|
||||
<component name="ProjectViewState">
|
||||
<option name="hideEmptyMiddlePackages" value="true" />
|
||||
<option name="showLibraryContents" value="true" />
|
||||
</component>
|
||||
<component name="PropertiesComponent"><![CDATA[{
|
||||
"keyToString": {
|
||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||
"RunOnceActivity.git.unshallow": "true",
|
||||
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultAutoModeForALLUsers.v1": "true",
|
||||
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
|
||||
"git-widget-placeholder": "master",
|
||||
"junie.onboarding.icon.badge.shown": "true",
|
||||
"last_opened_file_path": "/home/hzhang/RiderProjects/Marshmallow/DungeonSolver.Workspace/DungeonSolver.Global.Generators/DungeonSolver.Global.Generators.sln",
|
||||
"node.js.detected.package.eslint": "true",
|
||||
"node.js.detected.package.tslint": "true",
|
||||
"node.js.selected.package.eslint": "(autodetect)",
|
||||
"node.js.selected.package.tslint": "(autodetect)",
|
||||
"nodejs_package_manager_path": "npm",
|
||||
"to.speed.mode.migration.done": "true",
|
||||
"vue.rearranger.settings.migration": "true"
|
||||
}
|
||||
}]]></component>
|
||||
<component name="SpellCheckerSettings" RuntimeDictionaries="0" Folders="0" CustomDictionaries="0" DefaultDictionary="application-level" UseSingleDictionary="true" transferred="true" />
|
||||
<component name="TaskManager">
|
||||
<task active="true" id="Default" summary="Default task">
|
||||
<changelist id="da8668bf-f36a-441e-9a5c-e37101dd3fe9" name="Changes" comment="" />
|
||||
<created>1727693834377</created>
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1727693834377</updated>
|
||||
<workItem from="1727693835415" duration="220000" />
|
||||
<workItem from="1732397814070" duration="338000" />
|
||||
<workItem from="1762860049139" duration="457000" />
|
||||
</task>
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
<option name="version" value="3" />
|
||||
</component>
|
||||
<component name="UnityProjectConfiguration" hasMinimizedUI="false" />
|
||||
<component name="VcsManagerConfiguration">
|
||||
<option name="CLEAR_INITIAL_COMMIT_MESSAGE" value="true" />
|
||||
</component>
|
||||
<component name="XDebuggerManager">
|
||||
<breakpoint-manager>
|
||||
<breakpoints>
|
||||
<breakpoint enabled="true" type="DotNet_Exception_Breakpoints">
|
||||
<properties exception="System.OperationCanceledException" breakIfHandledByOtherCode="false" displayValue="System.OperationCanceledException" />
|
||||
<option name="timeStamp" value="1" />
|
||||
</breakpoint>
|
||||
<breakpoint enabled="true" type="DotNet_Exception_Breakpoints">
|
||||
<properties exception="System.Threading.Tasks.TaskCanceledException" breakIfHandledByOtherCode="false" displayValue="System.Threading.Tasks.TaskCanceledException" />
|
||||
<option name="timeStamp" value="2" />
|
||||
</breakpoint>
|
||||
<breakpoint enabled="true" type="DotNet_Exception_Breakpoints">
|
||||
<properties exception="System.Threading.ThreadAbortException" breakIfHandledByOtherCode="false" displayValue="System.Threading.ThreadAbortException" />
|
||||
<option name="timeStamp" value="3" />
|
||||
</breakpoint>
|
||||
</breakpoints>
|
||||
</breakpoint-manager>
|
||||
</component>
|
||||
</project>
|
||||
1
DungeonSolver.Workspace/Skeleton
Submodule
1
DungeonSolver.Workspace/Skeleton
Submodule
Submodule DungeonSolver.Workspace/Skeleton added at 7299a2dba3
20
DungeonSolver.Workspace/statistic.py
Normal file
20
DungeonSolver.Workspace/statistic.py
Normal file
@@ -0,0 +1,20 @@
|
||||
import os
|
||||
|
||||
def count_cs_lines(root_dir: str):
|
||||
total = 0
|
||||
for dirpath, _, filenames in os.walk(root_dir):
|
||||
for filename in filenames:
|
||||
if filename.endswith(".cs"):
|
||||
filepath = os.path.join(dirpath, filename)
|
||||
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
|
||||
lines = [
|
||||
line for line in f
|
||||
if line.strip() and not line.strip().startswith("//")
|
||||
]
|
||||
total += len(lines)
|
||||
print(f"Total non-empty lines in .cs files: {total}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
os.chdir(os.path.dirname(os.path.abspath(__file__)))
|
||||
count_cs_lines(".")
|
||||
|
||||
10
Hangman-Lab/SkeletonTest/.idea/SkeletonTest.iml
generated
Normal file
10
Hangman-Lab/SkeletonTest/.idea/SkeletonTest.iml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.12 (SkeletonTest)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
10
JupyterProject/.idea/JupyterProject.iml
generated
Normal file
10
JupyterProject/.idea/JupyterProject.iml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.13 (JupyterProject)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
47
JupyterProject/s.ipynb
Normal file
47
JupyterProject/s.ipynb
Normal file
@@ -0,0 +1,47 @@
|
||||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "code",
|
||||
"id": "initial_id",
|
||||
"metadata": {
|
||||
"collapsed": true,
|
||||
"ExecuteTime": {
|
||||
"end_time": "2025-09-29T14:08:48.168337Z",
|
||||
"start_time": "2025-09-29T14:08:48.157155Z"
|
||||
}
|
||||
},
|
||||
"source": "import sys",
|
||||
"outputs": [],
|
||||
"execution_count": 2
|
||||
},
|
||||
{
|
||||
"metadata": {},
|
||||
"cell_type": "code",
|
||||
"outputs": [],
|
||||
"execution_count": null,
|
||||
"source": "",
|
||||
"id": "652c2222ded0ab4f"
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"codemirror_mode": {
|
||||
"name": "ipython",
|
||||
"version": 2
|
||||
},
|
||||
"file_extension": ".py",
|
||||
"mimetype": "text/x-python",
|
||||
"name": "python",
|
||||
"nbconvert_exporter": "python",
|
||||
"pygments_lexer": "ipython2",
|
||||
"version": "2.7.6"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
10
structured-output-parser-mcp/.idea/structured-output-parser-mcp.iml
generated
Normal file
10
structured-output-parser-mcp/.idea/structured-output-parser-mcp.iml
generated
Normal file
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="uv (structured-output-parser-mcp)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
Reference in New Issue
Block a user