Skip to content

feat(crm): add dynamic CRM command tree from OpenAPI spec#15

Merged
Palbahngmiyine merged 8 commits intosolapi:mainfrom
Palbahngmiyine:worktree-buzzing-watching-manatee
May 8, 2026
Merged

feat(crm): add dynamic CRM command tree from OpenAPI spec#15
Palbahngmiyine merged 8 commits intosolapi:mainfrom
Palbahngmiyine:worktree-buzzing-watching-manatee

Conversation

@Palbahngmiyine
Copy link
Copy Markdown
Member

Summary

  • 새 `solactl crm ` 동적 명령 트리: OpenAPI 스펙에서 자동 생성, `--format json|table|csv`, `--data`/`--data-file`, path/query 파라미터 매핑, `solactl crm config clear-cache` 포함
  • 1시간 TTL 디스크 캐시(`~/.solactl/cache/`) + 네트워크 실패 시 stale fallback
  • 기존 인증/HMAC/profile/HTTP 인프라 재사용 — 신규 인증 코드 없음
  • `pkg/client`에 `Patch` 추가, `cmd/solactl/main.go`에서 argv 게이팅으로 비-CRM 호출(`--help`, `configure`, `send` 등)에 spec-fetch 비용 없음
  • `go fix ./...` 적용 (for-range / min / wg.Go 자동 변환)

Test plan

  • `go build ./...`
  • `go vet ./...`
  • `go test ./...` 모든 패키지 통과
  • `go test -race ./pkg/crm/... ./cmd` race 통과
  • `gofmt -l` 깨끗
  • `solactl --help` startup ≈ 0.28s (spec-fetch 미발생 확인)
  • `solactl crm records list --help` 등 동적 명령이 한국어 설명 + query/path 파라미터와 함께 정상 노출
  • 인증 환경에서 실제 endpoint 호출 (수동 검증 필요)

명세: `docs/crm-cli-spec.md` (gitignore된 로컬 전용 문서)

🤖 Generated with Claude Code

Palbahngmiyine and others added 2 commits May 8, 2026 09:39
Mount `solactl crm <resource> <action>` subcommands by fetching the CRM
OpenAPI spec at startup, mirroring the upstream @solapi/crm-cli mapping
(action naming, duplicate-action suffixing, --data/--data-file/--format)
while reusing solactl's existing auth, HMAC client, and profile stack.

- pkg/crm/spec: OpenAPI loader with 1h TTL on-disk cache + stale fallback,
  path → resource/action mapper, OpenAPI struct types.
- pkg/crm/output: --format json|table|csv with RFC 4180 CSV escaping,
  rune-aware truncation, pagination meta footer.
- cmd/crm{,_config,_dynamic}.go: dynamic resource trees,
  `crm config clear-cache`, --format resolution honouring root --json.
- cmd/solactl/main.go: gate RegisterDynamicCRM on argv so non-`crm`
  invocations (--help, configure, send) don't pay the spec-fetch cost.
- pkg/client: add Patch helper for PATCH-based dynamic operations.
- Apply \`go fix ./...\` cleanup across existing tests/utilities.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
go fix inlined every boolPtr(true) call into new(true), leaving the
function definition unused. golangci-lint's `unused` check failed on the
PR. The helper has no remaining callers — drop it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces dynamic CRM command support by fetching and mapping an OpenAPI spec, featuring a local caching mechanism and multiple output formatters (JSON, Table, CSV). It also performs general refactoring, such as adopting Go 1.21+ features and replacing interface{} with any. However, several critical issues were found, including compilation errors due to the misuse of the new function and sync.WaitGroup, as well as flawed logic in subcommand detection and redundant manual encoding implementations.

I am having trouble creating individual review comments. Click here to see my feedback.

cmd/send.go (275)

critical

The use of new(v) is invalid Go syntax because the built-in new function expects a type as its argument, not a value. Since v is a variable of type bool, this will cause a compilation error. The previous implementation return &v was correct and idiomatic for creating a pointer to a parameter.

func boolPtr(v bool) *bool { return &v }

cmd/send_ata.go (77)

critical

The expression new(true) is invalid Go syntax. The new function takes a type (e.g., new(bool)), not a literal value. To get a pointer to a boolean literal, you should use the boolPtr helper function defined in cmd/send.go.

		kakaoOpts.DisableSms = boolPtr(true)

pkg/validation/validate_test.go (195-200)

critical

The sync.WaitGroup type does not have a Go method in the standard library. This will result in a compilation error. You should use the standard wg.Add(1) and go func() { defer wg.Done(); ... }() pattern, or switch to golang.org/x/sync/errgroup if that was the intended behavior.

	for range 20 {
		wg.Add(1)
		go func() {
			defer wg.Done()
			msgs := []types.Message{
				{To: "01012345678", From: "01011112222", Text: "hello"},
			}
			_ = ValidateMessages(msgs, Options{AutoTypeDetect: true})
		}()
	}

cmd/solactl/main.go (39-44)

high

The invokesCRM logic is flawed because it returns early as soon as it encounters the first argument that does not start with a hyphen. This breaks command invocation when global flags with values are used before the crm command (e.g., solactl --profile myprof crm ...). In this case, it would check if myprof == "crm" and return false, preventing dynamic command registration. A safer approach is to check if crm exists anywhere in the arguments slice.

	for _, a := range args {
		if a == "crm" {
			return true
		}
	}
	return false

cmd/crm_dynamic.go (216-235)

medium

The manual implementation of encodePathArg is redundant. Go's url.PathEscape already provides the correct encoding for path segments, including the conversion of / to %2F to prevent breaking the path structure.

func encodePathArg(s string) string {
	return url.PathEscape(s)
}

cmd/crm_dynamic.go (237-245)

medium

Manually constructing URLs by string concatenation is fragile. It is safer to use the net/url package to manipulate query parameters and paths.

func withQuery(path string, q url.Values) string {
	if len(q) == 0 {
		return path
	}
	u, err := url.Parse(path)
	if err != nil {
		return path + "?" + q.Encode()
	}
	query := u.Query()
	for k, vs := range q {
		for _, v := range vs {
			query.Add(k, v)
		}
	}
	u.RawQuery = query.Encode()
	return u.String()
}

Palbahngmiyine and others added 6 commits May 8, 2026 10:45
The "재시도" prefix in the retry debug message had its final syllable
replaced by two U+FFFD bytes, producing "재시�� %d/%d" in --debug
output. Restore the intended Korean text.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@Palbahngmiyine Palbahngmiyine merged commit 9a3830f into solapi:main May 8, 2026
2 checks passed
@Palbahngmiyine Palbahngmiyine deleted the worktree-buzzing-watching-manatee branch May 8, 2026 06:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant