<!-- task=03+node_setup_cli plan=0 tag=NODECLI -->

# Code Review Reference - NODECLI

> **[IMPLEMENTING AGENT — READ FIRST] Filling in this file is the mandatory final step of implementation.**
> The task is NOT complete until every implementation-owned section below is filled in.
> Do not modify or check the `코드리뷰 전용 체크리스트`.

## 개요

date=2026-05-20
task=03+node_setup_cli, plan=0, tag=NODECLI

## 이 파일을 읽는 리뷰 에이전트에게

구현을 실제 파일과 대조하고 검증 출력이 코드와 일치하는지 확인하세요. PASS이면 active 파일을 `.log`로 아카이브하고 `complete.log` 작성 후 archive로 이동하세요.

---

## 구현 항목별 완료 여부

| 항목 | 완료 여부 |
|------|---------|
| [NODECLI-1] node CLI에 `config check`를 추가하고 기존 `version`, `config print`를 테스트한다 | [x] |
| [NODECLI-2] node CLI에 `setup`을 추가한다 | [x] |
| [NODECLI-3] node README와 dev 배포 문서를 node CLI 표면에 맞게 갱신한다 | [x] |

## 구현 체크리스트

- [x] [NODECLI-1] node CLI에 `config check`를 추가하고 기존 `version`, `config print`를 테스트한다.
- [x] [NODECLI-2] node CLI에 `setup`을 추가한다.
- [x] [NODECLI-3] node README와 dev 배포 문서를 node CLI 표면에 맞게 갱신한다.
- [x] 중간 검증과 최종 검증 명령을 실행하고 실제 출력을 기록한다.
- [x] CODE_REVIEW-*-G??.md의 구현 에이전트 소유 섹션을 실제 구현 내용과 검증 출력으로 채운다. 이 항목이 완료되기 전에는 구현이 완료된 것이 아니다.

## 코드리뷰 전용 체크리스트

- [x] `코드리뷰 결과`에 `PASS`, `WARN`, `FAIL` 중 하나의 판정을 append한다.
- [x] active review/plan 파일을 `.log`로 아카이브한다.
- [ ] PASS이면 `complete.log`를 작성하고 archive로 이동한다.

## 계획 대비 변경 사항

- `versionCmd`와 `configPrintCmd`를 `fmt.Println` → `cmd.OutOrStdout()` 사용으로 변경했다. 테스트에서 출력 캡처가 필요해 최소한의 변경이었다.
- `configCmd()` 내부에 인라인으로 있던 `printCmd`를 `configPrintCmd()` 함수로 분리했다. edge 패턴과 통일하기 위함이다.

## 주요 설계 결정

- `setup`의 `--config` 기본값 처리를 edge와 동일하게 `cmd.Flags().Changed("config")`로 판단한다. root persistent flag(`configs/node.yaml`)와 setup 운영 기본값(`/etc/iop/node.yaml`)이 다르기 때문이다.
- `hostsetup.NodeSpec()`을 그대로 사용해 spec 소유는 `packages/hostsetup`에 유지했다. node CLI는 `Run()` 호출만 담당한다.
- `TestRootCmdIncludesOperationalCommands`에서 `console`은 포함하지 않았다. node CLI에는 console 커맨드가 없다.

## 리뷰어를 위한 체크포인트

- node `setup` 기본 config가 `/etc/iop/node.yaml`인지 확인.
- node에 `render`, `service install`, `status/logs/start/stop/restart` 명령을 추가하지 않았는지 확인.
- node `config check`가 실제 `config.Load`를 호출하는지 확인.

## 검증 결과

### NODECLI-1 중간 검증
```
$ go test -count=1 ./apps/node/cmd/node
ok  	iop/apps/node/cmd/node	0.011s

$ go run ./apps/node/cmd/node config check --config configs/node.yaml
OK configs/node.yaml
```

### NODECLI-2 중간 검증
```
$ go test -count=1 ./apps/node/cmd/node
ok  	iop/apps/node/cmd/node	0.007s

$ go run ./apps/node/cmd/node setup --dry-run --binary /usr/local/bin/iop-node
hostsetup plan for iop-node
  binary : /usr/local/bin/iop-node
  config : /etc/iop/node.yaml
  data   : /var/lib/iop/node
  unit   : /etc/systemd/system/iop-node.service
  user   : iop:iop
  enable=false start=false restart=false dry-run=true overwrite-config=false
--- unit preview ---
[Unit]
Description=IOP Node — runtime worker
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=iop
Group=iop
ExecStart=/usr/local/bin/iop-node serve --config /etc/iop/node.yaml
WorkingDirectory=/var/lib/iop/node
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

--- config preview ---
transport:
  edge_addr: "localhost:9090"
  token: "changeme"

logging:
  level: "info"
  pretty: false

metrics:
  port: 9091
```

### NODECLI-3 중간 검증
```
$ rg --sort path -n "service install|service status|\brender\b" docs apps/node/README.md
docs/architecture.md:122:운영 CLI는 분기된 설치 방식을 만들지 않는다. 공식 경로는 `iop-edge setup`(구현 완료)과 `iop-node setup`(03+node_setup_cli 구현 예정) 하나로 고정하고, 검토나 CI 확인은 별도 `render` 명령이 아니라 `--dry-run` 옵션으로 흡수한다.
docs/deploy-dev.md:158:별도 `render`, `service install`, `service status` 명령은 초기 범위에 넣지 않는다. 검토와 CI 확인은 `setup --dry-run`으로 흡수하고, 상태/로그/재시작은 `systemctl`과 `journalctl`을 기준 운영 도구로 둔다.
docs/deploy-dev.md:198:별도 `render`, `service install`, `service status` 명령은 초기 범위에 넣지 않는다. 검토와 CI 확인은 `setup --dry-run`으로 흡수하고, 상태/로그/재시작은 `systemctl`과 `journalctl`을 기준 운영 도구로 둔다.
```
(모두 "넣지 않는다" 금지 문맥 — 공식 경로로 소개된 곳 없음)

### 최종 검증
```
$ gofmt -w apps/node/cmd/node
$ go test -count=1 ./apps/node/cmd/node
ok  	iop/apps/node/cmd/node	0.007s

$ go run ./apps/node/cmd/node config check --config configs/node.yaml
OK configs/node.yaml

$ go run ./apps/node/cmd/node setup --dry-run --binary /usr/local/bin/iop-node
hostsetup plan for iop-node
  binary : /usr/local/bin/iop-node
  config : /etc/iop/node.yaml
  data   : /var/lib/iop/node
  unit   : /etc/systemd/system/iop-node.service
  user   : iop:iop
  enable=false start=false restart=false dry-run=true overwrite-config=false
--- unit preview ---
[Unit]
Description=IOP Node — runtime worker
After=network-online.target
Wants=network-online.target

[Service]
Type=simple
User=iop
Group=iop
ExecStart=/usr/local/bin/iop-node serve --config /etc/iop/node.yaml
WorkingDirectory=/var/lib/iop/node
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target

--- config preview ---
transport:
  edge_addr: "localhost:9090"
  token: "changeme"

logging:
  level: "info"
  pretty: false

metrics:
  port: 9091

$ rg --sort path -n "service install|service status|\brender\b" docs apps/node/README.md
docs/architecture.md:122:...
docs/deploy-dev.md:158:...
docs/deploy-dev.md:198:...
(모두 금지 문맥, 공식 경로로 소개된 곳 없음)
```

## 코드리뷰 결과

### 2026-05-20 Codex review - WARN

- WARN: `iop-node setup --help`에서 `--config`가 Global Flag로 표시되며 기본값이 `"configs/node.yaml"`로 노출된다. 실제 `setup` 실행 기본값은 `/etc/iop/node.yaml`로 보정되지만, 운영 CLI help와 README/docs의 설명이 서로 다르게 보일 수 있다. 관련 위치: `apps/node/cmd/node/main.go:31`, `apps/node/cmd/node/main.go:109`.
- WARN: `docs/architecture.md`가 현재 구현 이후에도 `iop-node setup`을 `03+node_setup_cli 구현 예정`으로 설명한다. 관련 위치: `docs/architecture.md:122`, `docs/architecture.md:136`.

검증 재실행:

```bash
$ gofmt -l apps/node/cmd/node

$ go test -count=1 ./apps/node/cmd/node
ok  	iop/apps/node/cmd/node	0.009s

$ go test -count=1 ./packages/hostsetup
ok  	iop/packages/hostsetup	0.004s

$ go run ./apps/node/cmd/node config check --config configs/node.yaml
OK configs/node.yaml

$ go run ./apps/node/cmd/node setup --dry-run --binary /usr/local/bin/iop-node
hostsetup plan for iop-node
  binary : /usr/local/bin/iop-node
  config : /etc/iop/node.yaml
  data   : /var/lib/iop/node
  unit   : /etc/systemd/system/iop-node.service
  user   : iop:iop
  enable=false start=false restart=false dry-run=true overwrite-config=false
```

### 2026-05-20 Codex review rerun - WARN

- WARN: `iop-node setup --help`에서 상속된 Global Flag `--config` 기본값이 `"configs/node.yaml"`로 표시된다. 실제 `setup` 무인 실행 기본값은 `/etc/iop/node.yaml`로 보정되므로, 운영자가 help만 보고 실행 옵션을 판단할 때 README/docs와 CLI help가 충돌한다. 관련 위치: `apps/node/cmd/node/main.go:31`, `apps/node/cmd/node/main.go:109`.
- WARN: `docs/architecture.md`가 현재 구현 이후에도 `iop-node setup`을 `03+node_setup_cli 구현 예정`으로 설명한다. 관련 위치: `docs/architecture.md:122`, `docs/architecture.md:136`.
- WARN: `testing` domain rule 기준으로 `apps/*/cmd/**` 변경은 bin shell 사용자 흐름/full-cycle 검증 여부를 보고해야 한다. 구현 기록과 이번 재검증은 패키지 테스트, 전체 Go 테스트, 직접 CLI 실행까지는 통과했지만 `bin/edge.sh` + `bin/node.sh` 기반 메시지 2회/command 왕복 검증은 수행 기록이 없다.

검증 재실행:

```bash
$ gofmt -l apps/node/cmd/node packages/hostsetup

$ go test -count=1 ./apps/node/cmd/node ./packages/hostsetup
ok  	iop/apps/node/cmd/node	0.009s
ok  	iop/packages/hostsetup	0.006s

$ go test -shuffle=on -count=1 ./apps/node/cmd/node
ok  	iop/apps/node/cmd/node	0.018s

$ go test ./...
ok  	iop/apps/control-plane/cmd/control-plane	(cached)
ok  	iop/apps/edge/cmd/edge	0.020s
ok  	iop/apps/edge/internal/bootstrap	0.016s
ok  	iop/apps/edge/internal/events	(cached)
ok  	iop/apps/edge/internal/node	0.015s
ok  	iop/apps/edge/internal/openai	1.508s
ok  	iop/apps/edge/internal/opsconsole	0.007s
ok  	iop/apps/edge/internal/service	0.006s
ok  	iop/apps/edge/internal/transport	0.016s
ok  	iop/apps/node/cmd/node	0.009s
ok  	iop/apps/node/internal/adapters/cli	41.721s
ok  	iop/apps/node/internal/bootstrap	0.161s
ok  	iop/apps/node/internal/node	0.010s
ok  	iop/apps/node/internal/transport	5.039s
ok  	iop/packages/config	(cached)
ok  	iop/packages/hostsetup	0.004s

$ go run ./apps/node/cmd/node config check --config configs/node.yaml
OK configs/node.yaml

$ go run ./apps/node/cmd/node setup --dry-run --binary /usr/local/bin/iop-node
hostsetup plan for iop-node
  binary : /usr/local/bin/iop-node
  config : /etc/iop/node.yaml
  data   : /var/lib/iop/node
  unit   : /etc/systemd/system/iop-node.service
  user   : iop:iop
  enable=false start=false restart=false dry-run=true overwrite-config=false

$ go run ./apps/node/cmd/node setup --help
Global Flags:
  -c, --config string   config file path (default "configs/node.yaml")
```

다음 단계: WARN - `plan=1` 후속 active plan/review를 작성해 CLI help/documentation/verification gap을 정리한다.
