feat(scaffold): 초기 프로젝트 골격을 구성한다
Gito를 platformless Git control plane으로 시작할 수 있도록 Go core, Flutter control surface, contracts, ops 규칙, 검증 헬퍼를 함께 구성한다.
This commit is contained in:
commit
e2bc7aa8b6
37 changed files with 1349 additions and 0 deletions
19
.gitignore
vendored
Normal file
19
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
.DS_Store
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
|
|
||||||
|
# Go
|
||||||
|
bin/gito-*
|
||||||
|
coverage.out
|
||||||
|
|
||||||
|
# Flutter/Dart
|
||||||
|
.dart_tool/
|
||||||
|
.flutter-plugins
|
||||||
|
.flutter-plugins-dependencies
|
||||||
|
build/
|
||||||
|
|
||||||
|
# Local runtime
|
||||||
|
.gito/
|
||||||
|
tmp/
|
||||||
|
|
||||||
34
AGENTS.md
Normal file
34
AGENTS.md
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
# Gito Agent Instructions
|
||||||
|
|
||||||
|
Read this file before making changes in this repository.
|
||||||
|
|
||||||
|
## Common Rules
|
||||||
|
|
||||||
|
- Prefer the existing structure over creating new top-level patterns.
|
||||||
|
- Keep user-facing final answers in Korean unless the user asks otherwise.
|
||||||
|
- Check the relevant domain rule before code changes.
|
||||||
|
- Do not expand scope beyond the current request.
|
||||||
|
- Do not write secrets, tokens, passwords, private endpoints, or raw credentials to tracked files.
|
||||||
|
- Use `rg` or `rg --files` for searches when available.
|
||||||
|
- Use root helpers under `bin/` before ad hoc commands when they cover the task.
|
||||||
|
|
||||||
|
## Required First Reads
|
||||||
|
|
||||||
|
Read these files once per session when they exist:
|
||||||
|
|
||||||
|
1. `agent-ops/rules/project/rules.md`
|
||||||
|
2. `agent-ops/rules/private/rules.md`
|
||||||
|
|
||||||
|
## Domain Rules
|
||||||
|
|
||||||
|
| Path | Domain | Rules |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `services/core/**` | core | `agent-ops/rules/project/domain/core/rules.md` |
|
||||||
|
| `apps/client/**` | client | `agent-ops/rules/project/domain/client/rules.md` |
|
||||||
|
| `packages/contracts/**` | contracts | `agent-ops/rules/project/domain/contracts/rules.md` |
|
||||||
|
| `bin/**`, `docs/**`, `AGENTS.md`, `README.md`, `agent-ops/**`, `agent-test/**` | workspace-ops | `agent-ops/rules/project/domain/workspace-ops/rules.md` |
|
||||||
|
|
||||||
|
## Test Rules
|
||||||
|
|
||||||
|
When validation is part of the task, read `agent-test/local/rules.md` first.
|
||||||
|
|
||||||
59
README.md
Normal file
59
README.md
Normal file
|
|
@ -0,0 +1,59 @@
|
||||||
|
# Gito
|
||||||
|
|
||||||
|
Gito is a platformless Git control plane for agent-driven development workflows.
|
||||||
|
|
||||||
|
It manages registered repositories, workspace leases, Git operations, agent-shell
|
||||||
|
execution, provider adapters, and normalized events. Git itself stays
|
||||||
|
platform-neutral; GitHub, GitLab, Gitea, Plane, Jira, and similar systems are
|
||||||
|
integrated through adapters.
|
||||||
|
|
||||||
|
## Direction
|
||||||
|
|
||||||
|
- Server: Go
|
||||||
|
- Control surface: Flutter app and `gitoctl` in future slices
|
||||||
|
- State: PostgreSQL as source of truth
|
||||||
|
- Event acceleration: Redis optional, not required for the first slice
|
||||||
|
- Internal transport: proto-socket first
|
||||||
|
- REST: health/readiness, smoke/curl, provider callbacks, simple admin bootstrap
|
||||||
|
- gRPC: intentionally excluded from the initial design
|
||||||
|
|
||||||
|
## Runtime Shape
|
||||||
|
|
||||||
|
```text
|
||||||
|
Flutter UI / gitoctl
|
||||||
|
|
|
||||||
|
Control Plane
|
||||||
|
|
|
||||||
|
Core Domain
|
||||||
|
|
|
||||||
|
Worker / Scheduler
|
||||||
|
|
|
||||||
|
Agent Shell / IOP / Git Engine
|
||||||
|
|
|
||||||
|
Provider Adapters
|
||||||
|
```
|
||||||
|
|
||||||
|
## Repository Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/client/ Flutter control surface scaffold
|
||||||
|
services/core/ Go control plane, worker, shell commands
|
||||||
|
packages/contracts/ proto-socket, REST, event, and provider contract notes
|
||||||
|
docs/ Architecture and operation notes
|
||||||
|
bin/ Root test/lint/build helpers
|
||||||
|
agent-ops/ Agent operating rules
|
||||||
|
agent-test/ Local validation rules
|
||||||
|
```
|
||||||
|
|
||||||
|
## First Commands
|
||||||
|
|
||||||
|
```sh
|
||||||
|
bin/test
|
||||||
|
bin/lint
|
||||||
|
bin/build
|
||||||
|
```
|
||||||
|
|
||||||
|
The initial Go scaffold is intentionally small but buildable. Flutter platform
|
||||||
|
directories are not generated yet; `apps/client` currently records the control
|
||||||
|
surface boundary and a minimal Dart entry point.
|
||||||
|
|
||||||
2
agent-ops/rules/private/rules.md
Normal file
2
agent-ops/rules/private/rules.md
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# Gito Private Rules
|
||||||
|
|
||||||
13
agent-ops/rules/project/domain/client/rules.md
Normal file
13
agent-ops/rules/project/domain/client/rules.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# client
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
The client domain covers the Flutter control surface under `apps/client`.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- The client is a control surface, not a Git execution runtime.
|
||||||
|
- Prefer proto-socket for operation requests, events, and log streams.
|
||||||
|
- REST should be used only for health/readiness or compatibility endpoints.
|
||||||
|
- Keep platform/provider-specific presentation behind adapter-friendly models.
|
||||||
|
|
||||||
14
agent-ops/rules/project/domain/contracts/rules.md
Normal file
14
agent-ops/rules/project/domain/contracts/rules.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# contracts
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
The contracts domain covers `packages/contracts`.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Record transport-independent DTOs before binding them to one transport.
|
||||||
|
- proto-socket is the first internal transport target.
|
||||||
|
- REST exceptions must be documented.
|
||||||
|
- gRPC is out of scope until explicitly added.
|
||||||
|
- Contract notes must not include raw secrets or private credentials.
|
||||||
|
|
||||||
25
agent-ops/rules/project/domain/core/rules.md
Normal file
25
agent-ops/rules/project/domain/core/rules.md
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
# core
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
The core domain covers the Go control plane, worker, agent-shell command, core
|
||||||
|
models, platformless Git engine, provider adapters, storage, migrations, and
|
||||||
|
events.
|
||||||
|
|
||||||
|
## Included Paths
|
||||||
|
|
||||||
|
- `services/core/cmd/**`
|
||||||
|
- `services/core/internal/**`
|
||||||
|
- `services/core/migrations/**`
|
||||||
|
- `services/core/go.mod`
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Keep provider-specific API details out of `internal/core`.
|
||||||
|
- Keep GitHub/GitLab/Gitea/Plane/Jira calls behind `internal/provider`.
|
||||||
|
- Keep platformless Git operations in `internal/gitengine`.
|
||||||
|
- Agent command execution belongs in `internal/agentshell`.
|
||||||
|
- Store durable state and audit trails in PostgreSQL first.
|
||||||
|
- Redis, if added, is an acceleration layer and not the source of truth.
|
||||||
|
- Large behavior changes require tests.
|
||||||
|
|
||||||
14
agent-ops/rules/project/domain/workspace-ops/rules.md
Normal file
14
agent-ops/rules/project/domain/workspace-ops/rules.md
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
# workspace-ops
|
||||||
|
|
||||||
|
## Scope
|
||||||
|
|
||||||
|
Workspace operations cover root helpers, docs, agent rules, test rules, and
|
||||||
|
repository metadata.
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Keep helper commands simple and runnable from the repository root.
|
||||||
|
- Do not commit local secret files.
|
||||||
|
- Do not add generated build outputs.
|
||||||
|
- Prefer documenting exceptions in `docs/` or `packages/contracts/notes/`.
|
||||||
|
|
||||||
52
agent-ops/rules/project/rules.md
Normal file
52
agent-ops/rules/project/rules.md
Normal file
|
|
@ -0,0 +1,52 @@
|
||||||
|
# Gito Project Rules
|
||||||
|
|
||||||
|
## Language
|
||||||
|
|
||||||
|
- User-facing final answers default to Korean.
|
||||||
|
- Code identifiers, paths, commands, and commit scopes stay in the language used
|
||||||
|
by the project.
|
||||||
|
|
||||||
|
## Project Summary
|
||||||
|
|
||||||
|
Gito is a platformless Git control plane for agent-driven development. It owns
|
||||||
|
repo registry, workspace leases, Git operations, agent-shell execution,
|
||||||
|
provider-neutral change requests, provider adapters, and normalized events.
|
||||||
|
|
||||||
|
## Main Structure
|
||||||
|
|
||||||
|
- `services/core/` - Go server, worker, shell, core domain, storage, adapters.
|
||||||
|
- `apps/client/` - Flutter control surface.
|
||||||
|
- `packages/contracts/` - proto-socket, REST, event, and DTO contract notes.
|
||||||
|
- `bin/` - root test/lint/build helpers.
|
||||||
|
- `docs/` - architecture and operation documents.
|
||||||
|
- `agent-ops/` - agent rules.
|
||||||
|
- `agent-test/` - local validation rules.
|
||||||
|
|
||||||
|
## Stack
|
||||||
|
|
||||||
|
- Backend: Go, PostgreSQL, optional Redis.
|
||||||
|
- Client: Flutter/Dart.
|
||||||
|
- Internal transport: proto-socket first.
|
||||||
|
- REST: health/readiness, provider callbacks, smoke/curl, bootstrap.
|
||||||
|
- gRPC: excluded from the initial design.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- Prefer `bin/test`, `bin/lint`, and `bin/build` from the repository root.
|
||||||
|
- Keep Git operations platformless unless a provider adapter is explicitly in
|
||||||
|
scope.
|
||||||
|
- Provider APIs belong behind adapter boundaries.
|
||||||
|
- Agent-shell owns local command execution; control plane and core should not
|
||||||
|
run arbitrary workspace commands directly.
|
||||||
|
- Credential values must be referenced by `credential_ref` or local secret files,
|
||||||
|
never stored in tracked docs.
|
||||||
|
|
||||||
|
## Domain Map
|
||||||
|
|
||||||
|
| Path | Domain | Rules |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `services/core/**` | core | `agent-ops/rules/project/domain/core/rules.md` |
|
||||||
|
| `apps/client/**` | client | `agent-ops/rules/project/domain/client/rules.md` |
|
||||||
|
| `packages/contracts/**` | contracts | `agent-ops/rules/project/domain/contracts/rules.md` |
|
||||||
|
| `bin/**`, `docs/**`, `AGENTS.md`, `README.md`, `agent-ops/**`, `agent-test/**` | workspace-ops | `agent-ops/rules/project/domain/workspace-ops/rules.md` |
|
||||||
|
|
||||||
16
agent-test/local/rules.md
Normal file
16
agent-test/local/rules.md
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
# Local Test Rules
|
||||||
|
|
||||||
|
Read this file before validation work.
|
||||||
|
|
||||||
|
## Baseline
|
||||||
|
|
||||||
|
- Core: `cd services/core && go test ./...`
|
||||||
|
- Go lint: `cd services/core && go vet ./...`
|
||||||
|
- Root: `bin/test`, `bin/lint`, `bin/build`
|
||||||
|
- Flutter: run `flutter test` and `flutter analyze --no-fatal-infos` when the
|
||||||
|
Flutter toolchain is available and client changes are in scope.
|
||||||
|
|
||||||
|
## Reporting
|
||||||
|
|
||||||
|
Report executed commands, pass/fail result, skipped tools, and remaining risk.
|
||||||
|
|
||||||
8
apps/client/README.md
Normal file
8
apps/client/README.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Gito Client
|
||||||
|
|
||||||
|
Flutter control surface for Gito.
|
||||||
|
|
||||||
|
The first slice keeps the client scaffold light. It should consume Gito through
|
||||||
|
proto-socket for operation requests, event streams, and log streams. REST is only
|
||||||
|
for health/readiness and operational compatibility endpoints.
|
||||||
|
|
||||||
32
apps/client/lib/main.dart
Normal file
32
apps/client/lib/main.dart
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
|
void main() {
|
||||||
|
runApp(const GitoClientApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class GitoClientApp extends StatelessWidget {
|
||||||
|
const GitoClientApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
title: 'Gito',
|
||||||
|
theme: ThemeData(colorSchemeSeed: Colors.teal),
|
||||||
|
home: const GitoHomePage(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class GitoHomePage extends StatelessWidget {
|
||||||
|
const GitoHomePage({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return const Scaffold(
|
||||||
|
body: Center(
|
||||||
|
child: Text('Gito control surface'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
205
apps/client/pubspec.lock
Normal file
205
apps/client/pubspec.lock
Normal file
|
|
@ -0,0 +1,205 @@
|
||||||
|
# Generated by pub
|
||||||
|
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||||
|
packages:
|
||||||
|
async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: async
|
||||||
|
sha256: e2eb0491ba5ddb6177742d2da23904574082139b07c1e33b8503b9f46f3e1a37
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.13.1"
|
||||||
|
boolean_selector:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: boolean_selector
|
||||||
|
sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.2"
|
||||||
|
characters:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: characters
|
||||||
|
sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
clock:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: clock
|
||||||
|
sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.1.2"
|
||||||
|
collection:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: collection
|
||||||
|
sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.19.1"
|
||||||
|
fake_async:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: fake_async
|
||||||
|
sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.3.3"
|
||||||
|
flutter:
|
||||||
|
dependency: "direct main"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
flutter_lints:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description:
|
||||||
|
name: flutter_lints
|
||||||
|
sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.0.0"
|
||||||
|
flutter_test:
|
||||||
|
dependency: "direct dev"
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
leak_tracker:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker
|
||||||
|
sha256: "33e2e26bdd85a0112ec15400c8cbffea70d0f9c3407491f672a2fad47915e2de"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "11.0.2"
|
||||||
|
leak_tracker_flutter_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_flutter_testing
|
||||||
|
sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.10"
|
||||||
|
leak_tracker_testing:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: leak_tracker_testing
|
||||||
|
sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "3.0.2"
|
||||||
|
lints:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: lints
|
||||||
|
sha256: "12f842a479589fea194fe5c5a3095abc7be0c1f2ddfa9a0e76aed1dbd26a87df"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "6.1.0"
|
||||||
|
matcher:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: matcher
|
||||||
|
sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.12.19"
|
||||||
|
material_color_utilities:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: material_color_utilities
|
||||||
|
sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.13.0"
|
||||||
|
meta:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: meta
|
||||||
|
sha256: "23f08335362185a5ea2ad3a4e597f1375e78bce8a040df5c600c8d3552ef2394"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.17.0"
|
||||||
|
path:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: path
|
||||||
|
sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.9.1"
|
||||||
|
sky_engine:
|
||||||
|
dependency: transitive
|
||||||
|
description: flutter
|
||||||
|
source: sdk
|
||||||
|
version: "0.0.0"
|
||||||
|
source_span:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: source_span
|
||||||
|
sha256: "56a02f1f4cd1a2d96303c0144c93bd6d909eea6bee6bf5a0e0b685edbd4c47ab"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.10.2"
|
||||||
|
stack_trace:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stack_trace
|
||||||
|
sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.12.1"
|
||||||
|
stream_channel:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: stream_channel
|
||||||
|
sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.1.4"
|
||||||
|
string_scanner:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: string_scanner
|
||||||
|
sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.4.1"
|
||||||
|
term_glyph:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: term_glyph
|
||||||
|
sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "1.2.2"
|
||||||
|
test_api:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: test_api
|
||||||
|
sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "0.7.10"
|
||||||
|
vector_math:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vector_math
|
||||||
|
sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "2.2.0"
|
||||||
|
vm_service:
|
||||||
|
dependency: transitive
|
||||||
|
description:
|
||||||
|
name: vm_service
|
||||||
|
sha256: "0016aef94fc66495ac78af5859181e3f3bf2026bd8eecc72b9565601e19ab360"
|
||||||
|
url: "https://pub.dev"
|
||||||
|
source: hosted
|
||||||
|
version: "15.2.0"
|
||||||
|
sdks:
|
||||||
|
dart: ">=3.9.0-0 <4.0.0"
|
||||||
|
flutter: ">=3.18.0-18.0.pre.54"
|
||||||
20
apps/client/pubspec.yaml
Normal file
20
apps/client/pubspec.yaml
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
name: gito_client
|
||||||
|
description: Flutter control surface for Gito.
|
||||||
|
publish_to: none
|
||||||
|
version: 0.1.0
|
||||||
|
|
||||||
|
environment:
|
||||||
|
sdk: ">=3.4.0 <4.0.0"
|
||||||
|
|
||||||
|
dependencies:
|
||||||
|
flutter:
|
||||||
|
sdk: flutter
|
||||||
|
|
||||||
|
dev_dependencies:
|
||||||
|
flutter_test:
|
||||||
|
sdk: flutter
|
||||||
|
flutter_lints: ^6.0.0
|
||||||
|
|
||||||
|
flutter:
|
||||||
|
uses-material-design: true
|
||||||
|
|
||||||
14
bin/build
Executable file
14
bin/build
Executable file
|
|
@ -0,0 +1,14 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||||
|
OUT_DIR="$ROOT_DIR/bin"
|
||||||
|
|
||||||
|
if command -v go >/dev/null 2>&1; then
|
||||||
|
(cd "$ROOT_DIR/services/core" && go build -o "$OUT_DIR/gito-server" ./cmd/server)
|
||||||
|
(cd "$ROOT_DIR/services/core" && go build -o "$OUT_DIR/gito-worker" ./cmd/worker)
|
||||||
|
(cd "$ROOT_DIR/services/core" && go build -o "$OUT_DIR/gito-shell" ./cmd/shell)
|
||||||
|
else
|
||||||
|
echo "skip services/core: go not found"
|
||||||
|
fi
|
||||||
|
|
||||||
17
bin/lint
Executable file
17
bin/lint
Executable file
|
|
@ -0,0 +1,17 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||||
|
|
||||||
|
if command -v go >/dev/null 2>&1; then
|
||||||
|
(cd "$ROOT_DIR/services/core" && go vet ./...)
|
||||||
|
else
|
||||||
|
echo "skip services/core: go not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v flutter >/dev/null 2>&1 && [ -f "$ROOT_DIR/apps/client/pubspec.yaml" ]; then
|
||||||
|
(cd "$ROOT_DIR/apps/client" && flutter analyze --no-fatal-infos)
|
||||||
|
else
|
||||||
|
echo "skip apps/client: flutter not found or app not generated"
|
||||||
|
fi
|
||||||
|
|
||||||
16
bin/test
Executable file
16
bin/test
Executable file
|
|
@ -0,0 +1,16 @@
|
||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
ROOT_DIR="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||||
|
|
||||||
|
if command -v go >/dev/null 2>&1; then
|
||||||
|
(cd "$ROOT_DIR/services/core" && go test ./...)
|
||||||
|
else
|
||||||
|
echo "skip services/core: go not found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if command -v flutter >/dev/null 2>&1 && [ -f "$ROOT_DIR/apps/client/pubspec.yaml" ] && [ -d "$ROOT_DIR/apps/client/test" ]; then
|
||||||
|
(cd "$ROOT_DIR/apps/client" && flutter test)
|
||||||
|
else
|
||||||
|
echo "skip apps/client: flutter not found or no test scaffold"
|
||||||
|
fi
|
||||||
68
docs/architecture.md
Normal file
68
docs/architecture.md
Normal file
|
|
@ -0,0 +1,68 @@
|
||||||
|
# Architecture
|
||||||
|
|
||||||
|
Gito starts as a modular monolith with separable process roles. The codebase can
|
||||||
|
run as one service at first, but its boundaries should allow separate server,
|
||||||
|
worker, and shell deployments later.
|
||||||
|
|
||||||
|
## Layers
|
||||||
|
|
||||||
|
### Control Plane
|
||||||
|
|
||||||
|
- Owns proto-socket and REST surfaces.
|
||||||
|
- Accepts user, automation, and provider callback requests.
|
||||||
|
- Applies auth, policy, and approval gates.
|
||||||
|
- Creates operations and exposes event streams.
|
||||||
|
- Does not execute Git commands directly.
|
||||||
|
|
||||||
|
### Core Domain
|
||||||
|
|
||||||
|
- Owns repository registry, workspace leases, operations, revision cursors,
|
||||||
|
idempotency keys, audit records, and normalized events.
|
||||||
|
- Knows provider-neutral concepts such as `Repo`, `WorkspaceLease`,
|
||||||
|
`GitOperation`, `RevisionEvent`, and `ChangeRequest`.
|
||||||
|
- Does not know GitHub, GitLab, Gitea, Plane, Jira, or IOP HTTP details.
|
||||||
|
|
||||||
|
### Worker
|
||||||
|
|
||||||
|
- Picks pending operations.
|
||||||
|
- Coordinates workspace leases.
|
||||||
|
- Calls agent-shell for filesystem and command execution.
|
||||||
|
- Records operation outcomes and emits events.
|
||||||
|
- Handles retry and backoff policy.
|
||||||
|
|
||||||
|
### Agent Shell
|
||||||
|
|
||||||
|
- Runs on a machine that owns local workspace access.
|
||||||
|
- Executes Git CLI and IOP CLI commands.
|
||||||
|
- Streams logs, handles cancellation, and enforces dirty-workspace guards.
|
||||||
|
- Connects outbound to the control plane where possible.
|
||||||
|
|
||||||
|
### Git Engine
|
||||||
|
|
||||||
|
- Platformless Git operation layer.
|
||||||
|
- Supports clone, fetch, pull, checkout, status, diff, commit, push, branch, tag,
|
||||||
|
and revision scans.
|
||||||
|
- Must be testable against local bare repositories without GitHub, GitLab, or
|
||||||
|
Gitea.
|
||||||
|
|
||||||
|
### Provider Adapters
|
||||||
|
|
||||||
|
- Own platform APIs and DTOs.
|
||||||
|
- Map GitHub PR, GitLab MR, and Gitea PR into provider-neutral
|
||||||
|
`ChangeRequest` operations.
|
||||||
|
- Treat webhooks as wakeup signals. Final state must be verified through Git
|
||||||
|
revision or provider read APIs.
|
||||||
|
|
||||||
|
## Transport
|
||||||
|
|
||||||
|
- proto-socket is the default internal runtime transport.
|
||||||
|
- REST is reserved for health/readiness, provider callbacks, smoke/curl, and
|
||||||
|
simple bootstrap endpoints.
|
||||||
|
- gRPC is intentionally out of scope for the initial architecture.
|
||||||
|
|
||||||
|
## Event Policy
|
||||||
|
|
||||||
|
All external side effects should produce durable operation and event records in
|
||||||
|
PostgreSQL. Redis may be added later for fanout or stream acceleration, but it is
|
||||||
|
not the source of truth.
|
||||||
|
|
||||||
8
packages/contracts/README.md
Normal file
8
packages/contracts/README.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# Gito Contracts
|
||||||
|
|
||||||
|
This package records transport-independent contract candidates for Gito.
|
||||||
|
|
||||||
|
Use it for proto-socket channel/action notes, REST compatibility maps, event
|
||||||
|
schemas, provider-neutral DTOs, and compatibility notes shared by the Go core and
|
||||||
|
Flutter client.
|
||||||
|
|
||||||
100
packages/contracts/notes/gito-control-plane.md
Normal file
100
packages/contracts/notes/gito-control-plane.md
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
# Gito Control Plane Contract Candidates
|
||||||
|
|
||||||
|
## Transport Policy
|
||||||
|
|
||||||
|
- Internal runtime calls use proto-socket first.
|
||||||
|
- REST remains for health/readiness, provider callbacks, smoke/curl, and simple
|
||||||
|
bootstrap endpoints.
|
||||||
|
- gRPC is excluded from the first design.
|
||||||
|
|
||||||
|
## proto-socket Channels
|
||||||
|
|
||||||
|
| Channel | Purpose |
|
||||||
|
| --- | --- |
|
||||||
|
| `repo` | Register, inspect, and list managed repositories. |
|
||||||
|
| `workspace` | Lease, release, and inspect workspace slots. |
|
||||||
|
| `operation` | Create, cancel, inspect, and stream operations. |
|
||||||
|
| `git` | Request platformless Git operations. |
|
||||||
|
| `change_request` | Manage provider-neutral PR/MR operations. |
|
||||||
|
| `agent_shell` | Shell heartbeat, command dispatch, and log streaming. |
|
||||||
|
| `event` | Subscribe to normalized events. |
|
||||||
|
|
||||||
|
## Core DTO Candidates
|
||||||
|
|
||||||
|
### Repo
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `id` | Stable Gito repo id. |
|
||||||
|
| `name` | Display name. |
|
||||||
|
| `remote_url` | Git remote URL. |
|
||||||
|
| `default_branch` | Default source-of-truth branch. |
|
||||||
|
| `workspace_root` | Local workspace root for slots. |
|
||||||
|
| `credential_ref` | Reference to credentials, never the raw secret. |
|
||||||
|
|
||||||
|
### WorkspaceLease
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `id` | Lease id. |
|
||||||
|
| `repo_id` | Managed repo id. |
|
||||||
|
| `slot` | Zero-padded slot index. |
|
||||||
|
| `path` | Local workspace path. |
|
||||||
|
| `state` | `available`, `leased`, `dirty`, or `error`. |
|
||||||
|
| `expires_at` | Optional lease expiry. |
|
||||||
|
|
||||||
|
### Operation
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `id` | Operation id. |
|
||||||
|
| `repo_id` | Target repo. |
|
||||||
|
| `type` | `clone`, `fetch`, `commit`, `push`, `agent_run`, etc. |
|
||||||
|
| `state` | `queued`, `running`, `succeeded`, `failed`, `cancelled`. |
|
||||||
|
| `idempotency_key` | Duplicate guard. |
|
||||||
|
| `created_by` | Actor id or system source. |
|
||||||
|
|
||||||
|
### RevisionEvent
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `repo_id` | Target repo. |
|
||||||
|
| `branch` | Branch name. |
|
||||||
|
| `before` | Previous revision. |
|
||||||
|
| `after` | New revision. |
|
||||||
|
| `changed_files` | Changed file paths and change types. |
|
||||||
|
| `observed_at` | Observation timestamp. |
|
||||||
|
|
||||||
|
### ChangeRequest
|
||||||
|
|
||||||
|
Provider-neutral abstraction for GitHub PR, GitLab MR, Gitea PR, and similar
|
||||||
|
platform features.
|
||||||
|
|
||||||
|
| Field | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `provider` | `github`, `gitlab`, `gitea`, etc. |
|
||||||
|
| `external_id` | Provider PR/MR id. |
|
||||||
|
| `repo_id` | Target repo. |
|
||||||
|
| `source_branch` | Source branch. |
|
||||||
|
| `target_branch` | Target branch. |
|
||||||
|
| `state` | `open`, `merged`, `closed`, `draft`, etc. |
|
||||||
|
|
||||||
|
## Normalized Events
|
||||||
|
|
||||||
|
| Event | Meaning |
|
||||||
|
| --- | --- |
|
||||||
|
| `repo.changed` | Repo registry changed. |
|
||||||
|
| `branch.updated` | Branch revision changed. |
|
||||||
|
| `workspace.leased` | Workspace lease acquired. |
|
||||||
|
| `workspace.released` | Workspace lease released. |
|
||||||
|
| `workspace.dirty` | Workspace has uncommitted changes. |
|
||||||
|
| `operation.started` | Operation execution started. |
|
||||||
|
| `operation.completed` | Operation succeeded. |
|
||||||
|
| `operation.failed` | Operation failed. |
|
||||||
|
| `agent.run.started` | Agent execution started. |
|
||||||
|
| `agent.run.completed` | Agent execution completed. |
|
||||||
|
| `change_request.opened` | PR/MR-like object opened. |
|
||||||
|
| `change_request.updated` | PR/MR-like object changed. |
|
||||||
|
| `change_request.merged` | PR/MR-like object merged. |
|
||||||
|
| `provider.webhook.received` | Provider webhook received. |
|
||||||
|
|
||||||
26
services/core/cmd/server/main.go
Normal file
26
services/core/cmd/server/main.go
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.toki-labs.com/toki/gito/services/core/internal/config"
|
||||||
|
"git.toki-labs.com/toki/gito/services/core/internal/controlplane"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := config.Load()
|
||||||
|
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||||
|
|
||||||
|
server := &http.Server{
|
||||||
|
Addr: cfg.HTTPAddr,
|
||||||
|
Handler: controlplane.NewRouter(cfg, logger),
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.Info("gito control plane listening", "addr", cfg.HTTPAddr)
|
||||||
|
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
|
logger.Error("server stopped", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
20
services/core/cmd/shell/main.go
Normal file
20
services/core/cmd/shell/main.go
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.toki-labs.com/toki/gito/services/core/internal/agentshell"
|
||||||
|
"git.toki-labs.com/toki/gito/services/core/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := config.Load()
|
||||||
|
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||||
|
|
||||||
|
shell := agentshell.New(cfg, logger)
|
||||||
|
if err := shell.Run(); err != nil {
|
||||||
|
logger.Error("agent shell stopped", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
20
services/core/cmd/worker/main.go
Normal file
20
services/core/cmd/worker/main.go
Normal file
|
|
@ -0,0 +1,20 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.toki-labs.com/toki/gito/services/core/internal/config"
|
||||||
|
"git.toki-labs.com/toki/gito/services/core/internal/worker"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
cfg := config.Load()
|
||||||
|
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
|
||||||
|
|
||||||
|
runner := worker.NewRunner(cfg, logger)
|
||||||
|
if err := runner.Run(); err != nil {
|
||||||
|
logger.Error("worker stopped", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
3
services/core/go.mod
Normal file
3
services/core/go.mod
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
module git.toki-labs.com/toki/gito/services/core
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
21
services/core/internal/agentshell/shell.go
Normal file
21
services/core/internal/agentshell/shell.go
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
package agentshell
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"git.toki-labs.com/toki/gito/services/core/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Shell struct {
|
||||||
|
cfg config.Config
|
||||||
|
logger *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func New(cfg config.Config, logger *slog.Logger) *Shell {
|
||||||
|
return &Shell{cfg: cfg, logger: logger}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Shell) Run() error {
|
||||||
|
s.logger.Info("agent shell scaffold ready", "shell_id", s.cfg.ShellID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
47
services/core/internal/config/config.go
Normal file
47
services/core/internal/config/config.go
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Config struct {
|
||||||
|
AppEnv string
|
||||||
|
HTTPAddr string
|
||||||
|
DatabaseURL string
|
||||||
|
RedisURL string
|
||||||
|
ProtoSocketPath string
|
||||||
|
WorkerEnabled bool
|
||||||
|
ShellID string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Load() Config {
|
||||||
|
return Config{
|
||||||
|
AppEnv: getEnv("APP_ENV", "local"),
|
||||||
|
HTTPAddr: getEnv("HTTP_ADDR", ":8080"),
|
||||||
|
DatabaseURL: os.Getenv("DATABASE_URL"),
|
||||||
|
RedisURL: os.Getenv("REDIS_URL"),
|
||||||
|
ProtoSocketPath: getEnv("PROTO_SOCKET_PATH", "/proto-socket"),
|
||||||
|
WorkerEnabled: getEnvBool("WORKER_ENABLED", true),
|
||||||
|
ShellID: getEnv("GITO_SHELL_ID", "local-shell"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnv(key, fallback string) string {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvBool(key string, fallback bool) bool {
|
||||||
|
value := os.Getenv(key)
|
||||||
|
if value == "" {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
parsed, err := strconv.ParseBool(value)
|
||||||
|
if err != nil {
|
||||||
|
return fallback
|
||||||
|
}
|
||||||
|
return parsed
|
||||||
|
}
|
||||||
45
services/core/internal/config/config_test.go
Normal file
45
services/core/internal/config/config_test.go
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
package config
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestLoadDefaults(t *testing.T) {
|
||||||
|
t.Setenv("APP_ENV", "")
|
||||||
|
t.Setenv("HTTP_ADDR", "")
|
||||||
|
t.Setenv("PROTO_SOCKET_PATH", "")
|
||||||
|
|
||||||
|
cfg := Load()
|
||||||
|
if cfg.AppEnv != "local" {
|
||||||
|
t.Fatalf("AppEnv: got %q, want local", cfg.AppEnv)
|
||||||
|
}
|
||||||
|
if cfg.HTTPAddr != ":8080" {
|
||||||
|
t.Fatalf("HTTPAddr: got %q, want :8080", cfg.HTTPAddr)
|
||||||
|
}
|
||||||
|
if cfg.ProtoSocketPath != "/proto-socket" {
|
||||||
|
t.Fatalf("ProtoSocketPath: got %q, want /proto-socket", cfg.ProtoSocketPath)
|
||||||
|
}
|
||||||
|
if !cfg.WorkerEnabled {
|
||||||
|
t.Fatal("WorkerEnabled default should be true")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLoadEnvOverrides(t *testing.T) {
|
||||||
|
t.Setenv("APP_ENV", "test")
|
||||||
|
t.Setenv("HTTP_ADDR", ":18080")
|
||||||
|
t.Setenv("PROTO_SOCKET_PATH", "/gito")
|
||||||
|
t.Setenv("WORKER_ENABLED", "false")
|
||||||
|
t.Setenv("GITO_SHELL_ID", "runner-1")
|
||||||
|
|
||||||
|
cfg := Load()
|
||||||
|
if cfg.AppEnv != "test" || cfg.HTTPAddr != ":18080" {
|
||||||
|
t.Fatalf("unexpected config: %+v", cfg)
|
||||||
|
}
|
||||||
|
if cfg.ProtoSocketPath != "/gito" {
|
||||||
|
t.Fatalf("ProtoSocketPath: got %q", cfg.ProtoSocketPath)
|
||||||
|
}
|
||||||
|
if cfg.WorkerEnabled {
|
||||||
|
t.Fatal("WorkerEnabled should be false")
|
||||||
|
}
|
||||||
|
if cfg.ShellID != "runner-1" {
|
||||||
|
t.Fatalf("ShellID: got %q", cfg.ShellID)
|
||||||
|
}
|
||||||
|
}
|
||||||
35
services/core/internal/controlplane/router.go
Normal file
35
services/core/internal/controlplane/router.go
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
package controlplane
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"git.toki-labs.com/toki/gito/services/core/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewRouter(cfg config.Config, logger *slog.Logger) http.Handler {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/healthz", func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
|
||||||
|
})
|
||||||
|
mux.HandleFunc("/readyz", func(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
writeJSON(w, http.StatusOK, map[string]string{
|
||||||
|
"status": "ready",
|
||||||
|
"env": cfg.AppEnv,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
mux.HandleFunc(cfg.ProtoSocketPath, func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
logger.Info("proto-socket endpoint placeholder", "path", r.URL.Path)
|
||||||
|
writeJSON(w, http.StatusNotImplemented, map[string]string{
|
||||||
|
"error": "proto-socket endpoint is not implemented yet",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
return mux
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeJSON(w http.ResponseWriter, status int, value any) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(status)
|
||||||
|
_ = json.NewEncoder(w).Encode(value)
|
||||||
|
}
|
||||||
42
services/core/internal/controlplane/router_test.go
Normal file
42
services/core/internal/controlplane/router_test.go
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
package controlplane
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log/slog"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.toki-labs.com/toki/gito/services/core/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestHealthz(t *testing.T) {
|
||||||
|
router := NewRouter(config.Config{AppEnv: "test", ProtoSocketPath: "/proto-socket"}, slog.Default())
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/healthz", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusOK {
|
||||||
|
t.Fatalf("status: got %d", rec.Code)
|
||||||
|
}
|
||||||
|
var body map[string]string
|
||||||
|
if err := json.NewDecoder(rec.Body).Decode(&body); err != nil {
|
||||||
|
t.Fatalf("decode: %v", err)
|
||||||
|
}
|
||||||
|
if body["status"] != "ok" {
|
||||||
|
t.Fatalf("body: %#v", body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestProtoSocketPlaceholder(t *testing.T) {
|
||||||
|
router := NewRouter(config.Config{AppEnv: "test", ProtoSocketPath: "/proto-socket"}, slog.Default())
|
||||||
|
req := httptest.NewRequest(http.MethodGet, "/proto-socket", nil)
|
||||||
|
rec := httptest.NewRecorder()
|
||||||
|
|
||||||
|
router.ServeHTTP(rec, req)
|
||||||
|
|
||||||
|
if rec.Code != http.StatusNotImplemented {
|
||||||
|
t.Fatalf("status: got %d", rec.Code)
|
||||||
|
}
|
||||||
|
}
|
||||||
84
services/core/internal/core/model.go
Normal file
84
services/core/internal/core/model.go
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
package core
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Repo struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
|
RemoteURL string
|
||||||
|
DefaultBranch string
|
||||||
|
WorkspaceRoot string
|
||||||
|
CredentialRef string
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkspaceLease struct {
|
||||||
|
ID string
|
||||||
|
RepoID string
|
||||||
|
Slot string
|
||||||
|
Path string
|
||||||
|
State WorkspaceState
|
||||||
|
ExpiresAt *time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type WorkspaceState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
WorkspaceAvailable WorkspaceState = "available"
|
||||||
|
WorkspaceLeased WorkspaceState = "leased"
|
||||||
|
WorkspaceDirty WorkspaceState = "dirty"
|
||||||
|
WorkspaceError WorkspaceState = "error"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Operation struct {
|
||||||
|
ID string
|
||||||
|
RepoID string
|
||||||
|
Type OperationType
|
||||||
|
State OperationState
|
||||||
|
IdempotencyKey string
|
||||||
|
CreatedBy string
|
||||||
|
CreatedAt time.Time
|
||||||
|
UpdatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type OperationType string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OperationClone OperationType = "clone"
|
||||||
|
OperationFetch OperationType = "fetch"
|
||||||
|
OperationCommit OperationType = "commit"
|
||||||
|
OperationPush OperationType = "push"
|
||||||
|
OperationAgentRun OperationType = "agent_run"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OperationState string
|
||||||
|
|
||||||
|
const (
|
||||||
|
OperationQueued OperationState = "queued"
|
||||||
|
OperationRunning OperationState = "running"
|
||||||
|
OperationSucceeded OperationState = "succeeded"
|
||||||
|
OperationFailed OperationState = "failed"
|
||||||
|
OperationCancelled OperationState = "cancelled"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RevisionEvent struct {
|
||||||
|
RepoID string
|
||||||
|
Branch string
|
||||||
|
Before string
|
||||||
|
After string
|
||||||
|
ChangedFiles []ChangedFile
|
||||||
|
ObservedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChangedFile struct {
|
||||||
|
Path string
|
||||||
|
ChangeType string
|
||||||
|
}
|
||||||
|
|
||||||
|
type ChangeRequest struct {
|
||||||
|
Provider string
|
||||||
|
ExternalID string
|
||||||
|
RepoID string
|
||||||
|
SourceBranch string
|
||||||
|
TargetBranch string
|
||||||
|
State string
|
||||||
|
}
|
||||||
28
services/core/internal/events/events.go
Normal file
28
services/core/internal/events/events.go
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
package events
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type Event struct {
|
||||||
|
ID string
|
||||||
|
Type string
|
||||||
|
Subject string
|
||||||
|
Payload []byte
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
RepoChanged = "repo.changed"
|
||||||
|
BranchUpdated = "branch.updated"
|
||||||
|
WorkspaceLeased = "workspace.leased"
|
||||||
|
WorkspaceReleased = "workspace.released"
|
||||||
|
WorkspaceDirty = "workspace.dirty"
|
||||||
|
OperationStarted = "operation.started"
|
||||||
|
OperationCompleted = "operation.completed"
|
||||||
|
OperationFailed = "operation.failed"
|
||||||
|
AgentRunStarted = "agent.run.started"
|
||||||
|
AgentRunCompleted = "agent.run.completed"
|
||||||
|
ChangeRequestOpened = "change_request.opened"
|
||||||
|
ChangeRequestUpdated = "change_request.updated"
|
||||||
|
ChangeRequestMerged = "change_request.merged"
|
||||||
|
ProviderWebhook = "provider.webhook.received"
|
||||||
|
)
|
||||||
64
services/core/internal/gitengine/command.go
Normal file
64
services/core/internal/gitengine/command.go
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
package gitengine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrInvalidGitInput = errors.New("invalid git input")
|
||||||
|
|
||||||
|
type CommandRunner interface {
|
||||||
|
Run(workdir string, args ...string) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CLI struct{}
|
||||||
|
|
||||||
|
func (CLI) Run(workdir string, args ...string) (string, error) {
|
||||||
|
if len(args) == 0 {
|
||||||
|
return "", ErrInvalidGitInput
|
||||||
|
}
|
||||||
|
cmd := exec.Command("git", args...)
|
||||||
|
if strings.TrimSpace(workdir) != "" {
|
||||||
|
cmd.Dir = workdir
|
||||||
|
}
|
||||||
|
out, err := cmd.CombinedOutput()
|
||||||
|
return string(out), err
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusResult struct {
|
||||||
|
Clean bool
|
||||||
|
Output string
|
||||||
|
}
|
||||||
|
|
||||||
|
func Status(runner CommandRunner, workdir string) (StatusResult, error) {
|
||||||
|
output, err := runner.Run(workdir, "status", "--porcelain")
|
||||||
|
if err != nil {
|
||||||
|
return StatusResult{}, err
|
||||||
|
}
|
||||||
|
return StatusResult{
|
||||||
|
Clean: strings.TrimSpace(output) == "",
|
||||||
|
Output: output,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChangedFiles(runner CommandRunner, workdir, before, after string) ([]string, error) {
|
||||||
|
before = strings.TrimSpace(before)
|
||||||
|
after = strings.TrimSpace(after)
|
||||||
|
if before == "" || after == "" {
|
||||||
|
return nil, ErrInvalidGitInput
|
||||||
|
}
|
||||||
|
output, err := runner.Run(workdir, "diff", "--name-only", before+".."+after)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
lines := strings.Split(output, "\n")
|
||||||
|
files := make([]string, 0, len(lines))
|
||||||
|
for _, line := range lines {
|
||||||
|
line = strings.TrimSpace(line)
|
||||||
|
if line != "" {
|
||||||
|
files = append(files, line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return files, nil
|
||||||
|
}
|
||||||
51
services/core/internal/gitengine/command_test.go
Normal file
51
services/core/internal/gitengine/command_test.go
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
package gitengine
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fakeRunner struct {
|
||||||
|
output string
|
||||||
|
err error
|
||||||
|
args []string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeRunner) Run(_ string, args ...string) (string, error) {
|
||||||
|
f.args = append([]string(nil), args...)
|
||||||
|
return f.output, f.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestStatusClean(t *testing.T) {
|
||||||
|
runner := &fakeRunner{output: "\n"}
|
||||||
|
result, err := Status(runner, "/repo")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Status: %v", err)
|
||||||
|
}
|
||||||
|
if !result.Clean {
|
||||||
|
t.Fatal("expected clean status")
|
||||||
|
}
|
||||||
|
if !reflect.DeepEqual(runner.args, []string{"status", "--porcelain"}) {
|
||||||
|
t.Fatalf("args: %#v", runner.args)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChangedFiles(t *testing.T) {
|
||||||
|
runner := &fakeRunner{output: "README.md\nservices/core/main.go\n\n"}
|
||||||
|
files, err := ChangedFiles(runner, "/repo", "abc", "def")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ChangedFiles: %v", err)
|
||||||
|
}
|
||||||
|
want := []string{"README.md", "services/core/main.go"}
|
||||||
|
if !reflect.DeepEqual(files, want) {
|
||||||
|
t.Fatalf("files: got %#v want %#v", files, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestChangedFilesRequiresRevisions(t *testing.T) {
|
||||||
|
_, err := ChangedFiles(&fakeRunner{}, "/repo", "", "def")
|
||||||
|
if !errors.Is(err, ErrInvalidGitInput) {
|
||||||
|
t.Fatalf("expected ErrInvalidGitInput, got %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
21
services/core/internal/provider/provider.go
Normal file
21
services/core/internal/provider/provider.go
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"git.toki-labs.com/toki/gito/services/core/internal/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ChangeRequestAdapter interface {
|
||||||
|
CreateChangeRequest(ctx context.Context, input core.ChangeRequest) (core.ChangeRequest, error)
|
||||||
|
UpdateChangeRequest(ctx context.Context, input core.ChangeRequest) (core.ChangeRequest, error)
|
||||||
|
CommentChangeRequest(ctx context.Context, id string, body string) error
|
||||||
|
CloseChangeRequest(ctx context.Context, id string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebhookEvent struct {
|
||||||
|
Provider string
|
||||||
|
EventType string
|
||||||
|
ExternalID string
|
||||||
|
Payload []byte
|
||||||
|
}
|
||||||
14
services/core/internal/storage/storage.go
Normal file
14
services/core/internal/storage/storage.go
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
package storage
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type Store struct{}
|
||||||
|
|
||||||
|
func New() *Store {
|
||||||
|
return &Store{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Store) Ping(ctx context.Context) error {
|
||||||
|
_ = ctx
|
||||||
|
return nil
|
||||||
|
}
|
||||||
25
services/core/internal/worker/runner.go
Normal file
25
services/core/internal/worker/runner.go
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
package worker
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log/slog"
|
||||||
|
|
||||||
|
"git.toki-labs.com/toki/gito/services/core/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Runner struct {
|
||||||
|
cfg config.Config
|
||||||
|
logger *slog.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewRunner(cfg config.Config, logger *slog.Logger) *Runner {
|
||||||
|
return &Runner{cfg: cfg, logger: logger}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Runner) Run() error {
|
||||||
|
if !r.cfg.WorkerEnabled {
|
||||||
|
r.logger.Info("worker disabled")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
r.logger.Info("worker scaffold ready")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
67
services/core/migrations/00001_initial.sql
Normal file
67
services/core/migrations/00001_initial.sql
Normal file
|
|
@ -0,0 +1,67 @@
|
||||||
|
-- +goose Up
|
||||||
|
-- +goose StatementBegin
|
||||||
|
CREATE TABLE IF NOT EXISTS repos (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
remote_url TEXT NOT NULL,
|
||||||
|
default_branch TEXT NOT NULL,
|
||||||
|
workspace_root TEXT NOT NULL,
|
||||||
|
credential_ref TEXT,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS workspace_leases (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
repo_id TEXT NOT NULL REFERENCES repos(id),
|
||||||
|
slot TEXT NOT NULL,
|
||||||
|
path TEXT NOT NULL,
|
||||||
|
state TEXT NOT NULL,
|
||||||
|
expires_at TIMESTAMPTZ,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
UNIQUE (repo_id, slot)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS operations (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
repo_id TEXT NOT NULL REFERENCES repos(id),
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
state TEXT NOT NULL,
|
||||||
|
idempotency_key TEXT,
|
||||||
|
created_by TEXT NOT NULL,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS ux_operations_idempotency
|
||||||
|
ON operations (idempotency_key)
|
||||||
|
WHERE idempotency_key IS NOT NULL;
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS operation_events (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
operation_id TEXT REFERENCES operations(id),
|
||||||
|
type TEXT NOT NULL,
|
||||||
|
subject TEXT NOT NULL,
|
||||||
|
payload JSONB NOT NULL DEFAULT '{}'::jsonb,
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS revision_cursors (
|
||||||
|
repo_id TEXT NOT NULL REFERENCES repos(id),
|
||||||
|
branch TEXT NOT NULL,
|
||||||
|
revision TEXT NOT NULL,
|
||||||
|
observed_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
PRIMARY KEY (repo_id, branch)
|
||||||
|
);
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
-- +goose StatementBegin
|
||||||
|
DROP TABLE IF EXISTS revision_cursors;
|
||||||
|
DROP TABLE IF EXISTS operation_events;
|
||||||
|
DROP TABLE IF EXISTS operations;
|
||||||
|
DROP TABLE IF EXISTS workspace_leases;
|
||||||
|
DROP TABLE IF EXISTS repos;
|
||||||
|
-- +goose StatementEnd
|
||||||
|
|
||||||
Loading…
Reference in a new issue