Skip to content

Commit 280b36a

Browse files
committed
Improve build tooling and code quality
Add clang-format/clang-tidy targets (optional, found via Homebrew LLVM hints). Fix macOS SDK path for Homebrew clang-tidy. Switch FetchContent to tarballs, set CMP0135 policy. Add helpful first-run error with setup instructions when config is missing. Apply clang-format, fix clang-tidy warnings (const-correctness, nodiscard, designated initializers, explicit nullptr checks).
1 parent f41287b commit 280b36a

16 files changed

Lines changed: 183 additions & 119 deletions

.clang-format

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
BasedOnStyle: LLVM
2+
IndentWidth: 4
3+
ColumnLimit: 100
4+
PointerAlignment: Left
5+
ReferenceAlignment: Left
6+
AllowShortFunctionsOnASingleLine: Empty
7+
AllowShortIfStatementsOnASingleLine: Never
8+
AllowShortLoopsOnASingleLine: false
9+
BreakBeforeBraces: Attach
10+
SortIncludes: CaseSensitive
11+
IncludeBlocks: Preserve

.clang-tidy

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,16 @@ Checks: >
1212
-cppcoreguidelines-avoid-magic-numbers,
1313
-readability-magic-numbers,
1414
-cppcoreguidelines-pro-bounds-pointer-arithmetic,
15-
-misc-include-cleaner
15+
-misc-include-cleaner,
16+
-bugprone-easily-swappable-parameters,
17+
-cppcoreguidelines-avoid-const-or-ref-data-members,
18+
-cppcoreguidelines-pro-type-member-init,
19+
-cppcoreguidelines-pro-bounds-avoid-unchecked-container-access,
20+
-modernize-return-braced-init-list,
21+
-bugprone-exception-escape,
22+
-performance-enum-size,
23+
-readability-convert-member-functions-to-static,
24+
-cppcoreguidelines-pro-bounds-array-to-pointer-decay
1625
WarningsAsErrors: ''
1726
HeaderFilterRegex: 'include/.*\.h$'
1827
FormatStyle: file

CMakeLists.txt

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
cmake_minimum_required(VERSION 3.20)
2+
cmake_policy(SET CMP0135 NEW)
23
project(HowUtility VERSION 1.0 LANGUAGES CXX)
34

45
set(CMAKE_CXX_STANDARD 20)
@@ -30,8 +31,6 @@ endif()
3031

3132
# Fetch dependencies (downloads may take a moment on first configure)
3233
include(FetchContent)
33-
set(FETCHCONTENT_QUIET OFF)
34-
3534
message(STATUS "Fetching nlohmann/json 3.12.0...")
3635
set(JSON_Install OFF CACHE BOOL "" FORCE)
3736
FetchContent_Declare(json
@@ -74,6 +73,46 @@ if(BUILD_TESTS)
7473

7574
include(GoogleTest)
7675
gtest_discover_tests(how_tests)
76+
77+
# 'make test' rebuilds before running tests
78+
add_custom_target(check
79+
COMMAND ${CMAKE_CTEST_COMMAND} --output-on-failure
80+
DEPENDS how_tests
81+
)
82+
endif()
83+
84+
# Optional clang-tidy target
85+
file(GLOB_RECURSE ALL_SOURCES src/*.cpp include/*.h)
86+
find_program(CLANG_TIDY clang-tidy
87+
HINTS /opt/homebrew/opt/llvm/bin /usr/local/opt/llvm/bin)
88+
if(CLANG_TIDY)
89+
# When using Homebrew clang-tidy with Apple Clang, pass the SDK path
90+
set(CLANG_TIDY_EXTRA_ARGS "")
91+
if(APPLE)
92+
execute_process(COMMAND xcrun --show-sdk-path OUTPUT_VARIABLE SDKROOT OUTPUT_STRIP_TRAILING_WHITESPACE)
93+
set(CLANG_TIDY_EXTRA_ARGS "--extra-arg=-isysroot${SDKROOT}")
94+
endif()
95+
add_custom_target(tidy
96+
COMMAND ${CLANG_TIDY} -p ${CMAKE_BINARY_DIR} ${CLANG_TIDY_EXTRA_ARGS} ${ALL_SOURCES}
97+
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
98+
COMMENT "Running clang-tidy..."
99+
)
100+
endif()
101+
102+
# Optional clang-format target
103+
find_program(CLANG_FORMAT clang-format
104+
HINTS /opt/homebrew/opt/llvm/bin /usr/local/opt/llvm/bin)
105+
if(CLANG_FORMAT)
106+
add_custom_target(format
107+
COMMAND ${CLANG_FORMAT} -i ${ALL_SOURCES}
108+
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
109+
COMMENT "Running clang-format..."
110+
)
111+
add_custom_target(format-check
112+
COMMAND ${CLANG_FORMAT} --dry-run --Werror ${ALL_SOURCES}
113+
WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}
114+
COMMENT "Checking clang-format..."
115+
)
77116
endif()
78117

79118
# Install

include/ConfigManager.h

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
#include <unordered_map>
55

66
class ConfigManager {
7-
public:
7+
public:
88
explicit ConfigManager(std::string path = "~/.config/how/config");
99

1010
void load();
@@ -17,8 +17,8 @@ class ConfigManager {
1717
[[nodiscard]] bool allowInsecureSsl() const;
1818
[[nodiscard]] bool isLoaded() const;
1919

20-
private:
21-
std::string resolvePath(const std::string& path) const;
20+
private:
21+
[[nodiscard]] std::string resolvePath(const std::string& path) const;
2222
void checkPermissions(const std::string& resolved) const;
2323
void parseFile(const std::string& resolved);
2424
[[nodiscard]] std::string get(const std::string& key, const std::string& fallback = "") const;

include/ContextGatherer.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,14 @@
33
#include <string>
44

55
class ContextGatherer {
6-
public:
6+
public:
77
void gather();
88

99
[[nodiscard]] std::string os() const;
1010
[[nodiscard]] std::string shell() const;
1111
[[nodiscard]] std::string workingDirectory() const;
1212

13-
private:
13+
private:
1414
std::string os_ = "unknown";
1515
std::string shell_ = "unknown";
1616
std::string workingDirectory_ = "unknown";

include/HistoryManager.h

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
#pragma once
22

3-
#include <string>
43
#include <optional>
4+
#include <string>
55
#include <utility>
66

77
struct Exchange {
@@ -10,7 +10,7 @@ struct Exchange {
1010
};
1111

1212
class HistoryManager {
13-
public:
13+
public:
1414
explicit HistoryManager(std::string path = "~/.cache/how/history");
1515

1616
/// Load the previous exchange, if any.
@@ -19,8 +19,8 @@ class HistoryManager {
1919
/// Save the current exchange, creating the directory and enforcing 0600.
2020
void save(const std::string& userQuery, const std::string& assistantReply) const;
2121

22-
private:
23-
std::string resolvePath() const;
22+
private:
23+
[[nodiscard]] std::string resolvePath() const;
2424

2525
std::string path_;
2626
};

include/LLMClient.h

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,14 @@ struct Message {
1111
};
1212

1313
class LLMClient {
14-
public:
14+
public:
1515
explicit LLMClient(const Provider& provider, bool allowInsecureSsl = false);
1616

1717
/// Send a chat completion request and return the assistant's reply.
18-
[[nodiscard]] std::string complete(
19-
const std::string& systemPrompt,
20-
const std::vector<Message>& messages) const;
18+
[[nodiscard]] std::string complete(const std::string& systemPrompt,
19+
const std::vector<Message>& messages) const;
2120

22-
private:
21+
private:
2322
static size_t writeCallback(char* data, size_t size, size_t nmemb, std::string* out);
2423

2524
const Provider& provider_;

include/PromptBuilder.h

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
#include <string>
44

55
class PromptBuilder {
6-
public:
6+
public:
77
void setQuery(const std::string& query);
88
void setOS(const std::string& os);
99
void setShell(const std::string& shell);
@@ -12,7 +12,7 @@ class PromptBuilder {
1212
[[nodiscard]] std::string buildSystemPrompt() const;
1313
[[nodiscard]] std::string buildUserMessage() const;
1414

15-
private:
15+
private:
1616
std::string query_;
1717
std::string os_;
1818
std::string shell_;

include/Provider.h

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,31 @@
11
#pragma once
22

3+
#include <nlohmann/json.hpp>
34
#include <string>
45
#include <vector>
5-
#include <nlohmann/json.hpp>
66

77
struct Message;
88

99
class Provider {
10-
public:
10+
public:
1111
Provider() = default;
1212

1313
/// Create a provider by name. Throws if name is unknown.
14-
static Provider create(const std::string& name,
15-
const std::string& apiKey,
16-
const std::string& model,
17-
const std::string& customEndpoint = "");
14+
static Provider create(const std::string& name, const std::string& apiKey,
15+
const std::string& model, const std::string& customEndpoint = "");
1816

1917
[[nodiscard]] std::string endpoint() const;
2018
[[nodiscard]] std::vector<std::string> headers() const;
21-
[[nodiscard]] nlohmann::json formatRequest(
22-
const std::string& systemPrompt,
23-
const std::vector<Message>& messages) const;
19+
[[nodiscard]] nlohmann::json formatRequest(const std::string& systemPrompt,
20+
const std::vector<Message>& messages) const;
2421
[[nodiscard]] std::string parseResponse(const nlohmann::json& json) const;
2522
[[nodiscard]] const std::string& name() const;
2623

27-
private:
24+
private:
2825
enum class Format { OpenAI, Anthropic, Google };
2926

30-
Provider(std::string name, std::string endpoint, std::string apiKey,
31-
std::string model, Format format);
27+
Provider(std::string name, std::string endpoint, std::string apiKey, std::string model,
28+
Format format);
3229

3330
std::string name_;
3431
std::string endpoint_;

src/ConfigManager.cpp

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@
66
#include <stdexcept>
77
#include <sys/stat.h>
88

9-
ConfigManager::ConfigManager(std::string path)
10-
: path_(std::move(path)) {}
9+
ConfigManager::ConfigManager(std::string path) : path_(std::move(path)) {}
1110

1211
std::string ConfigManager::resolvePath(const std::string& path) const {
1312
if (path.starts_with("~/")) {
1413
const char* home = std::getenv("HOME");
15-
if (!home) {
14+
if (home == nullptr) {
1615
throw std::runtime_error("HOME environment variable is not set");
1716
}
1817
return std::string(home) + path.substr(1);
@@ -23,14 +22,23 @@ std::string ConfigManager::resolvePath(const std::string& path) const {
2322
void ConfigManager::checkPermissions(const std::string& resolved) const {
2423
struct stat st{};
2524
if (stat(resolved.c_str(), &st) != 0) {
26-
throw std::runtime_error("Cannot stat config file: " + resolved);
25+
throw std::runtime_error("Config file not found: " + resolved +
26+
"\n"
27+
"Create it with:\n\n"
28+
" mkdir -p ~/.config/how\n"
29+
" cat > ~/.config/how/config << 'EOF'\n"
30+
" default_provider = mistral\n"
31+
" mistral_api_key = YOUR_API_KEY\n"
32+
" EOF\n"
33+
" chmod 600 ~/.config/how/config\n\n"
34+
"Replace YOUR_API_KEY with a key from your provider.\n"
35+
"See the README for other providers and options.");
2736
}
2837

29-
mode_t perms = st.st_mode & 0777;
30-
if (perms & ~static_cast<mode_t>(0600)) {
38+
const mode_t perms = st.st_mode & 0777;
39+
if ((perms & ~static_cast<mode_t>(0600)) != 0) {
3140
std::ostringstream msg;
32-
msg << "Config file permissions are too open (0"
33-
<< std::oct << perms << "). "
41+
msg << "Config file permissions are too open (0" << std::oct << perms << "). "
3442
<< "Run: chmod 600 " << resolved;
3543
throw std::runtime_error(msg.str());
3644
}
@@ -103,7 +111,7 @@ std::string ConfigManager::customEndpoint() const {
103111
}
104112

105113
bool ConfigManager::allowInsecureSsl() const {
106-
auto val = get("allow_insecure_ssl", "false");
114+
const auto val = get("allow_insecure_ssl", "false");
107115
return val == "true" || val == "1" || val == "yes";
108116
}
109117

0 commit comments

Comments
 (0)