feat(crm): add dynamic CRM command tree from OpenAPI spec#15
Conversation
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>
There was a problem hiding this comment.
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)
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)
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)
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)
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)
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)
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()
}
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>
Summary
Test plan
명세: `docs/crm-cli-spec.md` (gitignore된 로컬 전용 문서)
🤖 Generated with Claude Code