<!-- task=01_setup_cli_core plan=0 tag=SETUPCORE -->

# Host Setup Core Plan

## 이 파일을 읽는 구현 에이전트에게

**구현 마지막 단계에서 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채우는 것은 필수다. 이 파일을 채우기 전에는 작업이 완료된 것이 아니다.** 구현 체크리스트를 기준으로 작업하고, 계획 파일과 리뷰 스텁의 모든 구현 체크리스트를 완료하라. 중간/최종 검증을 실행한 뒤 실제 stdout/stderr를 리뷰 스텁에 기록하라. 리뷰 파일의 아카이브 지시는 실행하지 말고, `코드리뷰 전용 체크리스트`도 수정하거나 체크하지 말라.

## 배경

Edge/Node는 host binary + systemd로 운영한다. 운영 경로는 `setup` 하나로 고정하되 edge/node가 같은 설치 로직을 공유해야 한다. 이 계획은 CLI에 연결하기 전, 공통 setup 엔진만 만든다.

## 의존 관계 및 구현 순서

- 선행 작업 없음.
- 후속 작업: `agent-task/02+edge_setup_cli`, `agent-task/03+node_setup_cli`는 이 작업이 `complete.log`를 만든 뒤 시작한다.
- `02+edge_setup_cli`와 `03+node_setup_cli`는 모두 이 작업에만 의존하며 서로에게는 의존하지 않는다. 둘은 `01_setup_cli_core` 완료 후 병렬 구현 가능하다.

## 분석 결과

### 읽은 파일

- `agent-ops/skills/common/plan/SKILL.md`
- `apps/edge/cmd/edge/main.go`
- `apps/node/cmd/node/main.go`
- `packages/config/config.go`
- `packages/config/config_test.go`
- `configs/edge.yaml`
- `configs/node.yaml`
- `docs/architecture.md`
- `docs/deploy-dev.md`
- `go.mod`

### 테스트 커버리지 공백

- systemd unit rendering: 기존 테스트 없음. 새 `packages/hostsetup` 테스트 필요.
- dry-run이 파일과 system 명령을 건드리지 않는지: 기존 테스트 없음.
- 기존 config no-overwrite: 기존 테스트 없음.
- 실제 root 권한 설치: 자동 테스트에서 제외하고 fake runner/dry-run으로 검증.

### 심볼 참조

- renamed/removed symbol: none.
- 추가 후보: `hostsetup.AppSpec`, `hostsetup.SetupOptions`, `hostsetup.Run`, `hostsetup.EdgeSpec`, `hostsetup.NodeSpec`.

### 범위 결정 근거

- edge/node cobra 연결은 후속 task에서 처리한다.
- Control Plane enrollment/config sync는 제외한다.
- `render`, `service install`, `status`, `logs`, `start`, `stop`, `restart`, `remove` 명령은 제외한다.
- systemd 외 init system 지원은 제외한다.

### 빌드 등급

- build: `cloud-G07`, review: `cloud-G07`. systemd, 파일 권한, 외부 명령 실행 계획이 포함된 terminal-agent 성격 작업이다.

## 구현 체크리스트

- [x] [SETUPCORE-1] `packages/hostsetup` 공통 setup 엔진을 추가한다.
- [x] [SETUPCORE-2] host setup 단위 테스트를 추가한다.
- [x] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 기록한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

### [SETUPCORE-1] `packages/hostsetup` 공통 setup 엔진을 추가한다

#### 문제

공통 설치 흐름이 없어서 edge/node가 각자 systemd와 파일 작업을 구현하게 될 위험이 있다. 현재 edge root는 `apps/edge/cmd/edge/main.go:29`에서 config flag를 등록하고 `main.go:30`에서 `serve`, `console`만 등록한다. node root는 `apps/node/cmd/node/main.go:30`에서 config flag를 등록하고 `main.go:32`에서 `serve`, `version`, `config`만 등록한다.

#### 해결 방법

`packages/hostsetup`을 추가한다. 공통 패키지는 `apps/*/internal`을 import하지 않는다.

```go
type AppSpec struct {
    Name           string
    Description    string
    DefaultConfig  string
    DefaultDataDir string
    DefaultUnit    string
    ConfigTemplate string
}

type SetupOptions struct {
    BinaryPath      string
    ConfigPath      string
    DataDir         string
    UnitPath        string
    User            string
    Group           string
    Enable          bool
    Start           bool
    Restart         bool
    DryRun          bool
    OverwriteConfig bool
}
```

`Run(ctx, spec, opts, out)` 순서:
1. 기본값 보정: binary는 `os.Executable`, user/group은 `iop`.
2. dry-run이면 계획, unit preview, config preview만 출력.
3. `/etc` 또는 `/var/lib` 대상 실쓰기에서 root가 아니면 실패.
4. `getent`, `id`, `groupadd --system`, `useradd --system`로 user/group 준비.
5. config parent/data dir 생성, data dir ownership 설정.
6. config 파일이 없으면 `0600`으로 생성. 있으면 기본 no-overwrite.
7. unit 파일 `0644` 생성/갱신.
8. `systemctl daemon-reload`, 옵션별 `enable`, `start`, `restart`.

#### 수정 파일 및 체크리스트

- [x] `packages/hostsetup/setup.go` 추가.
- [x] `packages/hostsetup/systemd.go` 추가.
- [x] `packages/hostsetup/templates.go` 추가.

#### 테스트 작성

`SETUPCORE-2`에서 작성한다.

#### 중간 검증

```bash
gofmt -w packages/hostsetup
go test -count=1 ./packages/hostsetup
```

기대 결과: gofmt 적용, hostsetup 테스트 통과.

### [SETUPCORE-2] host setup 단위 테스트를 추가한다

#### 문제

파일 생성, dry-run, no-overwrite, systemd unit rendering은 운영 설치 경로라 회귀 위험이 높지만 기존 테스트가 없다.

#### 해결 방법

fake command runner와 `t.TempDir()`을 사용한다. 실제 `/etc`, `/var/lib`, `systemctl`을 건드리지 않는다.

#### 수정 파일 및 체크리스트

- [x] `packages/hostsetup/setup_test.go` 추가.
- [x] `TestRunDryRunDoesNotWriteFiles` 추가.
- [x] `TestRunWritesMissingConfigAndUnit` 추가.
- [x] `TestRunDoesNotOverwriteExistingConfig` 추가.
- [x] `TestRenderUnit` 추가.

#### 테스트 작성

테스트 작성. assertion은 파일 존재/내용, fake runner 호출 목록, dry-run output을 검증한다.

#### 중간 검증

```bash
go test -count=1 ./packages/hostsetup
```

기대 결과: cache 없는 fresh run 통과.

## 수정 파일 요약

| 파일 | 항목 |
|---|---|
| `packages/hostsetup/setup.go` | SETUPCORE-1 |
| `packages/hostsetup/systemd.go` | SETUPCORE-1 |
| `packages/hostsetup/templates.go` | SETUPCORE-1 |
| `packages/hostsetup/setup_test.go` | SETUPCORE-2 |

## 최종 검증

```bash
gofmt -w packages/hostsetup
go test -count=1 ./packages/hostsetup
go test -count=1 ./packages/config ./packages/hostsetup
```

모든 코드 변경 완료 후 반드시 `CODE_REVIEW-*-G??.md`의 구현 에이전트 소유 섹션을 채운다. 이 파일 작성이 구현의 마지막 단계다.
