Addded Romeo that generates some doc. WIP.
This commit is contained in:
@@ -0,0 +1,290 @@
|
||||
#include "AiGenerator.h"
|
||||
#include "HttpClient.h"
|
||||
#include "JsonParser.h"
|
||||
#include <Core/Memory/MemoryArena.h>
|
||||
#include <Core/Common/CoreUtils.h>
|
||||
#include <stdio.h>
|
||||
#include <string.h>
|
||||
#include <Windows.h>
|
||||
|
||||
namespace Romeo
|
||||
{
|
||||
static Juliet::String ReadFileContent(Juliet::Arena* arena, const Juliet::String& path)
|
||||
{
|
||||
Assert(arena != nullptr);
|
||||
if (path.Size == 0)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
char pathBuf[512];
|
||||
size_t copyLen = path.Size < 511 ? path.Size : 511;
|
||||
memcpy(pathBuf, path.Data, copyLen);
|
||||
pathBuf[copyLen] = '\0';
|
||||
|
||||
FILE* f = nullptr;
|
||||
if (fopen_s(&f, pathBuf, "rb") != 0 || !f)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
fseek(f, 0, SEEK_END);
|
||||
long size = ftell(f);
|
||||
fseek(f, 0, SEEK_SET);
|
||||
|
||||
if (size <= 0)
|
||||
{
|
||||
fclose(f);
|
||||
return {};
|
||||
}
|
||||
|
||||
// Limit to 20KB to keep prompt token size reasonable
|
||||
if (size > 20480)
|
||||
{
|
||||
size = 20480;
|
||||
}
|
||||
|
||||
char* buf = Juliet::ArenaPushArray<char>(arena, static_cast<size_t>(size + 1) JULIET_DEBUG_PARAM("FileReadContent"));
|
||||
size_t readBytes = fread(buf, 1, static_cast<size_t>(size), f);
|
||||
buf[readBytes] = '\0';
|
||||
|
||||
fclose(f);
|
||||
return { buf, readBytes };
|
||||
}
|
||||
|
||||
static Juliet::String GetEnvironmentVar(Juliet::Arena* arena, const char* varName)
|
||||
{
|
||||
Assert(arena != nullptr);
|
||||
Assert(varName != nullptr);
|
||||
|
||||
DWORD size = GetEnvironmentVariableA(varName, nullptr, 0);
|
||||
if (size == 0)
|
||||
{
|
||||
return {};
|
||||
}
|
||||
|
||||
char* buf = Juliet::ArenaPushArray<char>(arena, size JULIET_DEBUG_PARAM("EnvVar"));
|
||||
GetEnvironmentVariableA(varName, buf, size);
|
||||
return { buf, static_cast<size_t>(size - 1) };
|
||||
}
|
||||
|
||||
static bool G_OllamaReady = false;
|
||||
|
||||
static bool PostWithRetry(
|
||||
Juliet::Arena* arena,
|
||||
const Juliet::String& host,
|
||||
int port,
|
||||
const Juliet::String& path,
|
||||
bool isHttps,
|
||||
const Juliet::String& requestBody,
|
||||
Juliet::String& outResponse
|
||||
)
|
||||
{
|
||||
if (isHttps)
|
||||
{
|
||||
return HttpClient::Post(arena, host, port, path, isHttps, requestBody, outResponse);
|
||||
}
|
||||
|
||||
if (G_OllamaReady)
|
||||
{
|
||||
return HttpClient::Post(arena, host, port, path, isHttps, requestBody, outResponse);
|
||||
}
|
||||
|
||||
int retries = 15;
|
||||
while (retries > 0)
|
||||
{
|
||||
if (HttpClient::Post(arena, host, port, path, isHttps, requestBody, outResponse))
|
||||
{
|
||||
G_OllamaReady = true;
|
||||
return true;
|
||||
}
|
||||
retries--;
|
||||
if (retries > 0)
|
||||
{
|
||||
printf("Romeo AI: Local Ollama serve not ready yet. Retrying in 1s (%d retries remaining)...\n", retries);
|
||||
Sleep(1000);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool PullOllamaModelIfNeeded(Juliet::Arena* arena, const Juliet::String& host, int port, const Juliet::String& modelName)
|
||||
{
|
||||
size_t bodyCap = modelName.Size + 64;
|
||||
char* showBodyBuf = Juliet::ArenaPushArray<char>(arena, bodyCap JULIET_DEBUG_PARAM("ShowBody"));
|
||||
snprintf(showBodyBuf, bodyCap, "{\"name\":\"%.*s\"}", static_cast<int>(modelName.Size), CStr(modelName));
|
||||
Juliet::String showBody = Juliet::WrapString(showBodyBuf);
|
||||
|
||||
Juliet::String showResponse = {};
|
||||
bool showSuccess = PostWithRetry(arena, host, port, ConstString("/api/show"), false, showBody, showResponse);
|
||||
|
||||
bool needsPull = false;
|
||||
if (!showSuccess || showResponse.Size == 0)
|
||||
{
|
||||
needsPull = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
JsonValue root = {};
|
||||
if (Json_Parse(arena, showResponse, root))
|
||||
{
|
||||
Juliet::String errorMsg = {};
|
||||
if (Json_Query(root, "error", errorMsg))
|
||||
{
|
||||
needsPull = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
needsPull = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (needsPull)
|
||||
{
|
||||
printf("Romeo AI: Ollama model '%.*s' not found. Pulling model automatically (this may take a few minutes)...\n",
|
||||
static_cast<int>(modelName.Size), CStr(modelName));
|
||||
OutputDebugStringA("Romeo AI: Model not found. Pulling model...\n");
|
||||
|
||||
char* pullBodyBuf = Juliet::ArenaPushArray<char>(arena, bodyCap + 32 JULIET_DEBUG_PARAM("PullBody"));
|
||||
snprintf(pullBodyBuf, bodyCap + 32, "{\"name\":\"%.*s\",\"stream\":false}", static_cast<int>(modelName.Size), CStr(modelName));
|
||||
Juliet::String pullBody = Juliet::WrapString(pullBodyBuf);
|
||||
|
||||
Juliet::String pullResponse = {};
|
||||
bool pullSuccess = PostWithRetry(arena, host, port, ConstString("/api/pull"), false, pullBody, pullResponse);
|
||||
if (pullSuccess)
|
||||
{
|
||||
printf("Romeo AI: Model '%.*s' pulled successfully!\n", static_cast<int>(modelName.Size), CStr(modelName));
|
||||
OutputDebugStringA("Romeo AI: Model pulled successfully!\n");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("Romeo AI: Failed to pull model '%.*s' from Ollama API.\n", static_cast<int>(modelName.Size), CStr(modelName));
|
||||
OutputDebugStringA("Romeo AI: Failed to pull model.\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AI_GenerateDescription(
|
||||
Juliet::Arena* arena,
|
||||
FileEntry& entry
|
||||
)
|
||||
{
|
||||
Assert(arena != nullptr);
|
||||
|
||||
Juliet::String headerContent = ReadFileContent(arena, entry.HeaderPath);
|
||||
Juliet::String cppContent = ReadFileContent(arena, entry.CppPath);
|
||||
|
||||
if (headerContent.Size == 0 && cppContent.Size == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Formulate the prompt
|
||||
const char* sysPrompt = "You are a technical documentation assistant. Provide a very short, concise, one-paragraph overview (MAXIMUM 50 WORDS) describing the purpose, main responsibilities, and design of this C++ component. Do not include markdown code block styling or titles in your response; return ONLY the description paragraph. If the current description provided below is already good and accurate, please return it exactly as is.";
|
||||
|
||||
size_t promptCap = strlen(sysPrompt) + entry.HeaderPath.Size + headerContent.Size + entry.CppPath.Size + cppContent.Size + entry.Description.Size + 512;
|
||||
char* promptBuf = Juliet::ArenaPushArray<char>(arena, promptCap JULIET_DEBUG_PARAM("PromptBuf"));
|
||||
|
||||
int written = 0;
|
||||
if (entry.Description.Size > 0)
|
||||
{
|
||||
written = snprintf(promptBuf, promptCap,
|
||||
"%s\n\n"
|
||||
"Current description:\n%.*s\n\n"
|
||||
"Header file: %.*s\n"
|
||||
"%.*s\n\n"
|
||||
"Source file: %.*s\n"
|
||||
"%.*s\n",
|
||||
sysPrompt,
|
||||
static_cast<int>(entry.Description.Size), CStr(entry.Description),
|
||||
static_cast<int>(entry.HeaderPath.Size), CStr(entry.HeaderPath),
|
||||
static_cast<int>(headerContent.Size), CStr(headerContent),
|
||||
static_cast<int>(entry.CppPath.Size), CStr(entry.CppPath),
|
||||
static_cast<int>(cppContent.Size), CStr(cppContent)
|
||||
);
|
||||
}
|
||||
else
|
||||
{
|
||||
written = snprintf(promptBuf, promptCap,
|
||||
"%s\n\n"
|
||||
"Header file: %.*s\n"
|
||||
"%.*s\n\n"
|
||||
"Source file: %.*s\n"
|
||||
"%.*s\n",
|
||||
sysPrompt,
|
||||
static_cast<int>(entry.HeaderPath.Size), CStr(entry.HeaderPath),
|
||||
static_cast<int>(headerContent.Size), CStr(headerContent),
|
||||
static_cast<int>(entry.CppPath.Size), CStr(entry.CppPath),
|
||||
static_cast<int>(cppContent.Size), CStr(cppContent)
|
||||
);
|
||||
}
|
||||
|
||||
Juliet::String prompt = { promptBuf, static_cast<size_t>(written) };
|
||||
Juliet::String escapedPrompt = Json_EscapeString(arena, prompt);
|
||||
|
||||
// Try local Ollama
|
||||
// Default Host: localhost:11434
|
||||
// Default Model: qwen2.5-coder:1.5b
|
||||
Juliet::String host = ConstString("127.0.0.1");
|
||||
int port = 11434;
|
||||
Juliet::String modelName = GetEnvironmentVar(arena, "ROMEO_LLM_MODEL");
|
||||
if (modelName.Size == 0)
|
||||
{
|
||||
modelName = ConstString("qwen3.5:4b");
|
||||
}
|
||||
|
||||
// Check / pull model if needed
|
||||
if (!PullOllamaModelIfNeeded(arena, host, port, modelName))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
printf("Romeo AI: Querying local Ollama (%.*s) for '%.*s' description...\n",
|
||||
static_cast<int>(modelName.Size), CStr(modelName),
|
||||
static_cast<int>(entry.HeaderPath.Size > 0 ? entry.HeaderPath.Size : entry.CppPath.Size),
|
||||
CStr(entry.HeaderPath.Size > 0 ? entry.HeaderPath : entry.CppPath));
|
||||
|
||||
size_t bodyCap = modelName.Size + escapedPrompt.Size + 256;
|
||||
char* bodyBuf = Juliet::ArenaPushArray<char>(arena, bodyCap JULIET_DEBUG_PARAM("OllamaBody"));
|
||||
snprintf(bodyBuf, bodyCap,
|
||||
"{\"model\":\"%.*s\",\"prompt\":\"%.*s\",\"stream\":false,\"think\":false}",
|
||||
static_cast<int>(modelName.Size), CStr(modelName),
|
||||
static_cast<int>(escapedPrompt.Size), CStr(escapedPrompt)
|
||||
);
|
||||
Juliet::String body = Juliet::WrapString(bodyBuf);
|
||||
|
||||
|
||||
Juliet::String response = {};
|
||||
bool success = PostWithRetry(
|
||||
arena,
|
||||
host,
|
||||
port,
|
||||
ConstString("/api/generate"),
|
||||
false, // isHttps
|
||||
body,
|
||||
response
|
||||
);
|
||||
|
||||
if (success && response.Size > 0)
|
||||
{
|
||||
JsonValue root = {};
|
||||
if (Json_Parse(arena, response, root))
|
||||
{
|
||||
Juliet::String desc = {};
|
||||
if (Json_Query(root, "response", desc))
|
||||
{
|
||||
entry.Description = Juliet::StringCopy(arena, desc);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
printf("Romeo AI: Failed to communicate with local Ollama.\n");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <Core/Common/String.h>
|
||||
#include "Database.h"
|
||||
|
||||
namespace Romeo
|
||||
{
|
||||
// Generates a description for the file entry using local Ollama or cloud Gemini API.
|
||||
// Returns true if generation was successful and entry.Description was populated.
|
||||
[[nodiscard]] bool AI_GenerateDescription(
|
||||
Juliet::Arena* arena,
|
||||
FileEntry& entry
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,300 @@
|
||||
#include "Database.h"
|
||||
|
||||
#include <Core/HAL/Filesystem/Filesystem.h>
|
||||
#include <Core/Memory/MemoryArena.h>
|
||||
#include <Windows.h>
|
||||
|
||||
namespace Romeo
|
||||
{
|
||||
void DB_Init(Database& db, Juliet::Arena* arena)
|
||||
{
|
||||
printf("Entering DB_Init...\n");
|
||||
db.DbArena = arena;
|
||||
db.Entries.Create(arena JULIET_DEBUG_ONLY(, "RomeoDB"));
|
||||
printf("Exiting DB_Init...\n");
|
||||
}
|
||||
|
||||
struct ScanContext
|
||||
{
|
||||
Database* Db;
|
||||
Juliet::Arena* TempArena;
|
||||
};
|
||||
|
||||
static void ProcessFile(ScanContext& ctx, const Juliet::String& path, const WIN32_FIND_DATAW& findData)
|
||||
{
|
||||
// Simple filter: only .h and .cpp
|
||||
const char* pPath = CStr(path);
|
||||
size_t len = StringLength(path);
|
||||
|
||||
if (len < 3)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
bool isHeader = false;
|
||||
bool isCpp = false;
|
||||
|
||||
if (pPath[len - 2] == '.' && pPath[len - 1] == 'h')
|
||||
{
|
||||
isHeader = true;
|
||||
}
|
||||
else if (len >= 4 && pPath[len - 4] == '.' && pPath[len - 3] == 'c' && pPath[len - 2] == 'p' && pPath[len - 1] == 'p')
|
||||
{
|
||||
isCpp = true;
|
||||
}
|
||||
|
||||
if (!isHeader && !isCpp)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
uint64_t lastModified = (static_cast<uint64_t>(findData.ftLastWriteTime.dwHighDateTime) << 32) | findData.ftLastWriteTime.dwLowDateTime;
|
||||
|
||||
// Try to find if we already have it in the DB (ignoring \include\ or \src\ offset)
|
||||
const char* relStart = pPath;
|
||||
if (const char* p1 = strstr(pPath, "\\include\\"))
|
||||
{
|
||||
relStart = p1 + 9;
|
||||
}
|
||||
else if (const char* p2 = strstr(pPath, "\\src\\"))
|
||||
{
|
||||
relStart = p2 + 5;
|
||||
}
|
||||
|
||||
size_t baseLen = len - static_cast<size_t>(relStart - pPath);
|
||||
baseLen -= (isHeader ? 2 : 4);
|
||||
|
||||
FileEntry* entry = nullptr;
|
||||
for (FileEntry& e : ctx.Db->Entries)
|
||||
{
|
||||
Juliet::String dbPath = e.HeaderPath.Size > 0 ? e.HeaderPath : e.CppPath;
|
||||
if (dbPath.Size > 0)
|
||||
{
|
||||
const char* pDbPath = CStr(dbPath);
|
||||
const char* dbRelStart = pDbPath;
|
||||
if (const char* p1 = strstr(pDbPath, "\\include\\"))
|
||||
{
|
||||
dbRelStart = p1 + 9;
|
||||
}
|
||||
else if (const char* p2 = strstr(pDbPath, "\\src\\"))
|
||||
{
|
||||
dbRelStart = p2 + 5;
|
||||
}
|
||||
|
||||
bool dbIsHeader = (pDbPath[dbPath.Size - 1] == 'h');
|
||||
size_t dbBaseLen = dbPath.Size - static_cast<size_t>(dbRelStart - pDbPath);
|
||||
dbBaseLen -= (dbIsHeader ? 2 : 4);
|
||||
|
||||
if (baseLen == dbBaseLen && strncmp(relStart, dbRelStart, baseLen) == 0)
|
||||
{
|
||||
entry = &e;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!entry)
|
||||
{
|
||||
FileEntry newEntry = {};
|
||||
ctx.Db->Entries.PushBack(newEntry);
|
||||
entry = ctx.Db->Entries.Back();
|
||||
}
|
||||
|
||||
if (isHeader)
|
||||
{
|
||||
if (entry->HeaderPath.Size == 0)
|
||||
{
|
||||
entry->HeaderPath = Juliet::StringCopy(ctx.Db->DbArena, path);
|
||||
}
|
||||
entry->LastModifiedHeader = lastModified;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (entry->CppPath.Size == 0)
|
||||
{
|
||||
entry->CppPath = Juliet::StringCopy(ctx.Db->DbArena, path);
|
||||
}
|
||||
entry->LastModifiedCpp = lastModified;
|
||||
}
|
||||
}
|
||||
|
||||
static void ScanDirectoryRecursive(ScanContext& ctx, const Juliet::String& currentDir)
|
||||
{
|
||||
size_t searchPathLen = currentDir.Size + 3; // "\*" + null
|
||||
char* searchPathBuf = ArenaPushArray<char>(ctx.TempArena, searchPathLen JULIET_DEBUG_PARAM("RomeoDbSearchPathBuf"));
|
||||
snprintf(searchPathBuf, searchPathLen, "%s\\*", CStr(currentDir));
|
||||
Juliet::String searchPath = {searchPathBuf, searchPathLen - 1};
|
||||
|
||||
// Convert to wide string for Win32
|
||||
int wLen = MultiByteToWideChar(CP_UTF8, 0, CStr(searchPath), -1, nullptr, 0);
|
||||
if (wLen <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
wchar_t* wSearchPath = ArenaPushArray<wchar_t>(ctx.TempArena, static_cast<size_t>(wLen) JULIET_DEBUG_PARAM("RomeoDbWSearch"));
|
||||
MultiByteToWideChar(CP_UTF8, 0, CStr(searchPath), -1, wSearchPath, wLen);
|
||||
|
||||
WIN32_FIND_DATAW findData;
|
||||
HANDLE hFind = FindFirstFileExW(wSearchPath, FindExInfoBasic, &findData, FindExSearchNameMatch, nullptr, 0);
|
||||
|
||||
if (hFind == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
do
|
||||
{
|
||||
if (wcscmp(findData.cFileName, L".") == 0 || wcscmp(findData.cFileName, L"..") == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Convert filename back to UTF8
|
||||
int uLen = WideCharToMultiByte(CP_UTF8, 0, findData.cFileName, -1, nullptr, 0, nullptr, nullptr);
|
||||
char* uFileName = ArenaPushArray<char>(ctx.TempArena, static_cast<size_t>(uLen) JULIET_DEBUG_PARAM("RomeoDbUFileName"));
|
||||
WideCharToMultiByte(CP_UTF8, 0, findData.cFileName, -1, uFileName, uLen, nullptr, nullptr);
|
||||
|
||||
size_t fullPathLen = currentDir.Size + 1 + static_cast<size_t>(uLen); // "\" + null
|
||||
char* fullPathBuf = ArenaPushArray<char>(ctx.TempArena, fullPathLen JULIET_DEBUG_PARAM("RomeoDbFullPathBuf"));
|
||||
snprintf(fullPathBuf, fullPathLen, "%s\\%s", CStr(currentDir), uFileName);
|
||||
Juliet::String fullPath = {fullPathBuf, fullPathLen - 1};
|
||||
|
||||
if (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
|
||||
{
|
||||
// Skip external/build dirs
|
||||
if (strcmp(uFileName, "External") != 0 && strcmp(uFileName, "Intermediate") != 0 && strcmp(uFileName, "bin") != 0 && strcmp(uFileName, ".git") != 0)
|
||||
{
|
||||
ScanDirectoryRecursive(ctx, fullPath);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ProcessFile(ctx, fullPath, findData);
|
||||
}
|
||||
} while (FindNextFileW(hFind, &findData) != 0);
|
||||
|
||||
FindClose(hFind);
|
||||
}
|
||||
|
||||
void DB_ScanWorkspace(Database& db, Juliet::Arena* tempArena, const Juliet::String& rootDir)
|
||||
{
|
||||
Assert(tempArena != nullptr);
|
||||
ScanContext ctx;
|
||||
ctx.Db = &db;
|
||||
ctx.TempArena = tempArena;
|
||||
|
||||
// Scan the src and include directories specifically to avoid trash
|
||||
size_t srcDirLen = rootDir.Size + 12; // "\Juliet\src" + null
|
||||
char* srcDirBuf = ArenaPushArray<char>(tempArena, srcDirLen JULIET_DEBUG_PARAM("RomeoDbSrcDirBuf"));
|
||||
snprintf(srcDirBuf, srcDirLen, "%s\\Juliet\\src", CStr(rootDir));
|
||||
Juliet::String srcDir = {srcDirBuf, srcDirLen - 1};
|
||||
|
||||
size_t incDirLen = rootDir.Size + 16; // "\Juliet\include" + null
|
||||
char* incDirBuf = ArenaPushArray<char>(tempArena, incDirLen JULIET_DEBUG_PARAM("RomeoDbIncDirBuf"));
|
||||
snprintf(incDirBuf, incDirLen, "%s\\Juliet\\include", CStr(rootDir));
|
||||
Juliet::String incDir = {incDirBuf, incDirLen - 1};
|
||||
|
||||
ScanDirectoryRecursive(ctx, srcDir);
|
||||
ScanDirectoryRecursive(ctx, incDir);
|
||||
}
|
||||
|
||||
bool DB_Save(const Database& db, const Juliet::String& filePath)
|
||||
{
|
||||
FILE* file = nullptr;
|
||||
if (fopen_s(&file, CStr(filePath), "wb") != 0 || !file)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint64_t entryCount = db.Entries.Size();
|
||||
fwrite(&entryCount, sizeof(uint64_t), 1, file);
|
||||
|
||||
for (const FileEntry& entry : db.Entries)
|
||||
{
|
||||
uint64_t hLen = entry.HeaderPath.Size;
|
||||
fwrite(&hLen, sizeof(uint64_t), 1, file);
|
||||
if (hLen > 0) fwrite(entry.HeaderPath.Data, 1, static_cast<size_t>(hLen), file);
|
||||
|
||||
uint64_t cLen = entry.CppPath.Size;
|
||||
fwrite(&cLen, sizeof(uint64_t), 1, file);
|
||||
if (cLen > 0) fwrite(entry.CppPath.Data, 1, static_cast<size_t>(cLen), file);
|
||||
|
||||
fwrite(&entry.LastModifiedHeader, sizeof(uint64_t), 1, file);
|
||||
fwrite(&entry.LastModifiedCpp, sizeof(uint64_t), 1, file);
|
||||
fwrite(&entry.LastMarkdownCreatedTime, sizeof(uint64_t), 1, file);
|
||||
fwrite(&entry.LastAIGeneratedTime, sizeof(uint64_t), 1, file);
|
||||
|
||||
uint64_t dLen = entry.Description.Size;
|
||||
fwrite(&dLen, sizeof(uint64_t), 1, file);
|
||||
if (dLen > 0) fwrite(entry.Description.Data, 1, static_cast<size_t>(dLen), file);
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DB_Load(Database& db, const Juliet::String& filePath)
|
||||
{
|
||||
FILE* file = nullptr;
|
||||
if (fopen_s(&file, CStr(filePath), "rb") != 0 || !file)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
db.Entries.Clear();
|
||||
|
||||
uint64_t entryCount = 0;
|
||||
if (fread(&entryCount, sizeof(uint64_t), 1, file) != 1)
|
||||
{
|
||||
fclose(file);
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint64_t i = 0; i < entryCount; ++i)
|
||||
{
|
||||
FileEntry entry = {};
|
||||
|
||||
uint64_t hLen = 0;
|
||||
if (fread(&hLen, sizeof(uint64_t), 1, file) != 1) break;
|
||||
if (hLen > 0)
|
||||
{
|
||||
entry.HeaderPath.Data = ArenaPushArray<char>(db.DbArena, static_cast<size_t>(hLen + 1) JULIET_DEBUG_PARAM("DbLoadHeader"));
|
||||
entry.HeaderPath.Size = static_cast<size_t>(hLen);
|
||||
fread(entry.HeaderPath.Data, 1, static_cast<size_t>(hLen), file);
|
||||
entry.HeaderPath.Data[hLen] = '\0';
|
||||
}
|
||||
|
||||
uint64_t cLen = 0;
|
||||
if (fread(&cLen, sizeof(uint64_t), 1, file) != 1) break;
|
||||
if (cLen > 0)
|
||||
{
|
||||
entry.CppPath.Data = ArenaPushArray<char>(db.DbArena, static_cast<size_t>(cLen + 1) JULIET_DEBUG_PARAM("DbLoadCpp"));
|
||||
entry.CppPath.Size = static_cast<size_t>(cLen);
|
||||
fread(entry.CppPath.Data, 1, static_cast<size_t>(cLen), file);
|
||||
entry.CppPath.Data[cLen] = '\0';
|
||||
}
|
||||
|
||||
fread(&entry.LastModifiedHeader, sizeof(uint64_t), 1, file);
|
||||
fread(&entry.LastModifiedCpp, sizeof(uint64_t), 1, file);
|
||||
fread(&entry.LastMarkdownCreatedTime, sizeof(uint64_t), 1, file);
|
||||
fread(&entry.LastAIGeneratedTime, sizeof(uint64_t), 1, file);
|
||||
|
||||
uint64_t dLen = 0;
|
||||
if (fread(&dLen, sizeof(uint64_t), 1, file) != 1) break;
|
||||
if (dLen > 0)
|
||||
{
|
||||
entry.Description.Data = ArenaPushArray<char>(db.DbArena, static_cast<size_t>(dLen + 1) JULIET_DEBUG_PARAM("DbLoadDesc"));
|
||||
entry.Description.Size = static_cast<size_t>(dLen);
|
||||
fread(entry.Description.Data, 1, static_cast<size_t>(dLen), file);
|
||||
entry.Description.Data[dLen] = '\0';
|
||||
}
|
||||
|
||||
db.Entries.PushBack(entry);
|
||||
}
|
||||
|
||||
fclose(file);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace Romeo
|
||||
@@ -0,0 +1,43 @@
|
||||
#pragma once
|
||||
|
||||
#include <Core/Common/String.h>
|
||||
#include <Core/Container/Vector.h>
|
||||
#include <Core/Common/CoreUtils.h>
|
||||
|
||||
#include <Core/Thread/Mutex.h>
|
||||
|
||||
namespace Romeo
|
||||
{
|
||||
struct FileEntry
|
||||
{
|
||||
Juliet::String HeaderPath;
|
||||
Juliet::String CppPath;
|
||||
|
||||
// Stored as Windows FILETIME (uint64_t) for easy comparison
|
||||
uint64_t LastModifiedHeader = 0;
|
||||
uint64_t LastModifiedCpp = 0;
|
||||
|
||||
uint64_t LastMarkdownCreatedTime = 0;
|
||||
uint64_t LastAIGeneratedTime = 0;
|
||||
|
||||
Juliet::String Description;
|
||||
};
|
||||
|
||||
struct Database
|
||||
{
|
||||
Juliet::VectorArena<FileEntry, 512, true> Entries = {};
|
||||
Juliet::Arena* DbArena = nullptr;
|
||||
Juliet::Mutex Mutex;
|
||||
};
|
||||
|
||||
// Initialize the DB using the provided memory arena
|
||||
void DB_Init(Database& db, Juliet::Arena* arena);
|
||||
|
||||
// Scan a workspace directory for pairs of .h/.cpp and update the database structure.
|
||||
void DB_ScanWorkspace(Database& db, Juliet::Arena* tempArena, const Juliet::String& rootDir);
|
||||
|
||||
// Save and load DB state from disk (e.g. Romeo/docs/romeo_db.json or .bin)
|
||||
bool DB_Save(const Database& db, const Juliet::String& filePath);
|
||||
bool DB_Load(Database& db, const Juliet::String& filePath);
|
||||
|
||||
} // namespace Romeo
|
||||
@@ -0,0 +1,196 @@
|
||||
#include "HttpClient.h"
|
||||
#include <Windows.h>
|
||||
#include <winhttp.h>
|
||||
#include <Core/Memory/MemoryArena.h>
|
||||
#include <Core/Common/CoreUtils.h>
|
||||
|
||||
namespace Romeo
|
||||
{
|
||||
static wchar_t* ConvertToWide(Juliet::Arena* arena, const Juliet::String& str)
|
||||
{
|
||||
Assert(arena != nullptr);
|
||||
if (str.Size == 0)
|
||||
{
|
||||
return const_cast<wchar_t*>(L"");
|
||||
}
|
||||
int wLen = MultiByteToWideChar(CP_UTF8, 0, CStr(str), static_cast<int>(str.Size), nullptr, 0);
|
||||
if (wLen <= 0)
|
||||
{
|
||||
return const_cast<wchar_t*>(L"");
|
||||
}
|
||||
wchar_t* wBuf = Juliet::ArenaPushArray<wchar_t>(arena, static_cast<size_t>(wLen + 1) JULIET_DEBUG_PARAM("WideStr"));
|
||||
MultiByteToWideChar(CP_UTF8, 0, CStr(str), static_cast<int>(str.Size), wBuf, wLen);
|
||||
wBuf[wLen] = L'\0';
|
||||
return wBuf;
|
||||
}
|
||||
|
||||
bool HttpClient::Post(
|
||||
Juliet::Arena* arena,
|
||||
const Juliet::String& host,
|
||||
int port,
|
||||
const Juliet::String& path,
|
||||
bool isHttps,
|
||||
const Juliet::String& requestBody,
|
||||
Juliet::String& outResponse
|
||||
)
|
||||
{
|
||||
Assert(arena != nullptr);
|
||||
Assert(host.Size > 0);
|
||||
|
||||
bool success = false;
|
||||
HINTERNET hSession = nullptr;
|
||||
HINTERNET hConnect = nullptr;
|
||||
HINTERNET hRequest = nullptr;
|
||||
|
||||
hSession = WinHttpOpen(
|
||||
L"Romeo/1.0",
|
||||
WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
|
||||
nullptr,
|
||||
nullptr,
|
||||
0
|
||||
);
|
||||
|
||||
if (hSession != nullptr)
|
||||
{
|
||||
WinHttpSetTimeouts(hSession, 60000, 60000, 60000, 1800000);
|
||||
wchar_t* wHost = ConvertToWide(arena, host);
|
||||
hConnect = WinHttpConnect(
|
||||
hSession,
|
||||
wHost,
|
||||
static_cast<INTERNET_PORT>(port),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
if (hConnect != nullptr)
|
||||
{
|
||||
wchar_t* wPath = ConvertToWide(arena, path);
|
||||
hRequest = WinHttpOpenRequest(
|
||||
hConnect,
|
||||
L"POST",
|
||||
wPath,
|
||||
nullptr,
|
||||
nullptr,
|
||||
nullptr,
|
||||
isHttps ? WINHTTP_FLAG_SECURE : 0
|
||||
);
|
||||
}
|
||||
|
||||
if (hRequest != nullptr)
|
||||
{
|
||||
const wchar_t* headers = L"Content-Type: application/json\r\n";
|
||||
DWORD headersLen = static_cast<DWORD>(-1);
|
||||
|
||||
BOOL sendRes = WinHttpSendRequest(
|
||||
hRequest,
|
||||
headers,
|
||||
headersLen,
|
||||
const_cast<char*>(CStr(requestBody)),
|
||||
static_cast<DWORD>(requestBody.Size),
|
||||
static_cast<DWORD>(requestBody.Size),
|
||||
0
|
||||
);
|
||||
|
||||
if (sendRes != 0)
|
||||
{
|
||||
BOOL recvRes = WinHttpReceiveResponse(hRequest, nullptr);
|
||||
if (recvRes != 0)
|
||||
{
|
||||
DWORD dwSize = 0;
|
||||
DWORD dwDownloaded = 0;
|
||||
|
||||
size_t totalBytes = 0;
|
||||
size_t capacity = 1024;
|
||||
char* responseData = Juliet::ArenaPushArray<char>(arena, capacity JULIET_DEBUG_PARAM("HttpResponse"));
|
||||
|
||||
do
|
||||
{
|
||||
dwSize = 0;
|
||||
if (WinHttpQueryDataAvailable(hRequest, &dwSize) == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (dwSize == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (totalBytes + dwSize >= capacity)
|
||||
{
|
||||
size_t newCapacity = capacity * 2;
|
||||
if (newCapacity < totalBytes + dwSize + 1)
|
||||
{
|
||||
newCapacity = totalBytes + dwSize + 1;
|
||||
}
|
||||
|
||||
char* newBuf = Juliet::ArenaPushArray<char>(arena, newCapacity JULIET_DEBUG_PARAM("HttpResponseGrow"));
|
||||
memcpy(newBuf, responseData, totalBytes);
|
||||
responseData = newBuf;
|
||||
capacity = newCapacity;
|
||||
}
|
||||
|
||||
dwDownloaded = 0;
|
||||
if (WinHttpReadData(hRequest, responseData + totalBytes, dwSize, &dwDownloaded) != 0)
|
||||
{
|
||||
totalBytes += dwDownloaded;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
} while (dwSize > 0);
|
||||
|
||||
DWORD dwStatusCode = 0;
|
||||
DWORD dwHeaderSize = sizeof(dwStatusCode);
|
||||
WinHttpQueryHeaders(
|
||||
hRequest,
|
||||
WINHTTP_QUERY_STATUS_CODE | WINHTTP_QUERY_FLAG_NUMBER,
|
||||
nullptr,
|
||||
&dwStatusCode,
|
||||
&dwHeaderSize,
|
||||
nullptr
|
||||
);
|
||||
|
||||
if (dwStatusCode == 200)
|
||||
{
|
||||
if (totalBytes > 0)
|
||||
{
|
||||
responseData[totalBytes] = '\0';
|
||||
outResponse.Data = responseData;
|
||||
outResponse.Size = totalBytes;
|
||||
success = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if (totalBytes > 0)
|
||||
{
|
||||
responseData[totalBytes] = '\0';
|
||||
printf("Romeo HttpClient: HTTP Error Status %u. Response: %s\n", static_cast<unsigned int>(dwStatusCode), responseData);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("Romeo HttpClient: HTTP Error Status %u. Empty response.\n", static_cast<unsigned int>(dwStatusCode));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (hRequest != nullptr)
|
||||
{
|
||||
WinHttpCloseHandle(hRequest);
|
||||
}
|
||||
if (hConnect != nullptr)
|
||||
{
|
||||
WinHttpCloseHandle(hConnect);
|
||||
}
|
||||
if (hSession != nullptr)
|
||||
{
|
||||
WinHttpCloseHandle(hSession);
|
||||
}
|
||||
|
||||
return success;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
#pragma once
|
||||
|
||||
#include <Core/Common/String.h>
|
||||
|
||||
namespace Romeo
|
||||
{
|
||||
class HttpClient
|
||||
{
|
||||
public:
|
||||
// Sends a POST request over HTTP/HTTPS.
|
||||
// Response data is allocated on the provided arena and returned in outResponse.
|
||||
[[nodiscard]] static bool Post(
|
||||
Juliet::Arena* arena,
|
||||
const Juliet::String& host,
|
||||
int port,
|
||||
const Juliet::String& path,
|
||||
bool isHttps,
|
||||
const Juliet::String& requestBody,
|
||||
Juliet::String& outResponse
|
||||
);
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,467 @@
|
||||
#include "JsonParser.h"
|
||||
#include <Core/Memory/MemoryArena.h>
|
||||
#include <Core/Common/CoreUtils.h>
|
||||
#include <ctype.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <new>
|
||||
|
||||
namespace Romeo
|
||||
{
|
||||
struct JsonParserState
|
||||
{
|
||||
Juliet::Arena* Arena;
|
||||
const char* Ptr;
|
||||
const char* End;
|
||||
};
|
||||
|
||||
static void SkipWhitespace(JsonParserState& state)
|
||||
{
|
||||
while (state.Ptr < state.End)
|
||||
{
|
||||
char c = *state.Ptr;
|
||||
if (c == ' ' || c == '\t' || c == '\n' || c == '\r')
|
||||
{
|
||||
state.Ptr++;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static bool ParseString(JsonParserState& state, Juliet::String& outStr)
|
||||
{
|
||||
if (state.Ptr >= state.End || *state.Ptr != '"')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
state.Ptr++;
|
||||
const char* start = state.Ptr;
|
||||
|
||||
size_t len = 0;
|
||||
bool hasEscape = false;
|
||||
while (state.Ptr < state.End)
|
||||
{
|
||||
char c = *state.Ptr;
|
||||
if (c == '"')
|
||||
{
|
||||
break;
|
||||
}
|
||||
else if (c == '\\')
|
||||
{
|
||||
hasEscape = true;
|
||||
state.Ptr++;
|
||||
if (state.Ptr >= state.End)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
state.Ptr++;
|
||||
len++;
|
||||
}
|
||||
|
||||
if (state.Ptr >= state.End)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const char* endQuote = state.Ptr;
|
||||
state.Ptr++;
|
||||
|
||||
char* buf = Juliet::ArenaPushArray<char>(state.Arena, len + 1 JULIET_DEBUG_PARAM("JsonParsedStr"));
|
||||
|
||||
if (hasEscape)
|
||||
{
|
||||
size_t writeIdx = 0;
|
||||
const char* readPtr = start;
|
||||
while (readPtr < endQuote)
|
||||
{
|
||||
char c = *readPtr;
|
||||
if (c == '\\')
|
||||
{
|
||||
readPtr++;
|
||||
if (readPtr >= endQuote)
|
||||
{
|
||||
break;
|
||||
}
|
||||
char esc = *readPtr;
|
||||
if (esc == 'n') { buf[writeIdx++] = '\n'; }
|
||||
else if (esc == 'r') { buf[writeIdx++] = '\r'; }
|
||||
else if (esc == 't') { buf[writeIdx++] = '\t'; }
|
||||
else if (esc == 'b') { buf[writeIdx++] = '\b'; }
|
||||
else if (esc == 'f') { buf[writeIdx++] = '\f'; }
|
||||
else if (esc == '"') { buf[writeIdx++] = '"'; }
|
||||
else if (esc == '\\') { buf[writeIdx++] = '\\'; }
|
||||
else if (esc == '/') { buf[writeIdx++] = '/'; }
|
||||
else if (esc == 'u')
|
||||
{
|
||||
if (readPtr + 4 < endQuote)
|
||||
{
|
||||
buf[writeIdx++] = '?';
|
||||
readPtr += 4;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
buf[writeIdx++] = esc;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
buf[writeIdx++] = c;
|
||||
}
|
||||
readPtr++;
|
||||
}
|
||||
buf[writeIdx] = '\0';
|
||||
outStr.Data = buf;
|
||||
outStr.Size = writeIdx;
|
||||
}
|
||||
else
|
||||
{
|
||||
memcpy(buf, start, len);
|
||||
buf[len] = '\0';
|
||||
outStr.Data = buf;
|
||||
outStr.Size = len;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ParseValue(JsonParserState& state, JsonValue& outVal);
|
||||
|
||||
static bool ParseObject(JsonParserState& state, JsonValue& outVal)
|
||||
{
|
||||
if (state.Ptr >= state.End || *state.Ptr != '{')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
state.Ptr++;
|
||||
outVal.Type = JsonType::Object;
|
||||
outVal.ObjectVal.Create(state.Arena JULIET_DEBUG_ONLY(, "JsonObject"));
|
||||
|
||||
while (state.Ptr < state.End)
|
||||
{
|
||||
SkipWhitespace(state);
|
||||
if (state.Ptr < state.End && *state.Ptr == '}')
|
||||
{
|
||||
state.Ptr++;
|
||||
return true;
|
||||
}
|
||||
|
||||
Juliet::String key = {};
|
||||
if (!ParseString(state, key))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
SkipWhitespace(state);
|
||||
if (state.Ptr >= state.End || *state.Ptr != ':')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
state.Ptr++;
|
||||
|
||||
JsonKeyValue kv = {};
|
||||
kv.Key = key;
|
||||
kv.Value = Juliet::ArenaPushArray<JsonValue>(state.Arena, 1 JULIET_DEBUG_PARAM("JsonValueRef"));
|
||||
new (kv.Value) JsonValue();
|
||||
|
||||
if (!ParseValue(state, *kv.Value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
outVal.ObjectVal.PushBack(kv);
|
||||
|
||||
SkipWhitespace(state);
|
||||
if (state.Ptr < state.End && *state.Ptr == ',')
|
||||
{
|
||||
state.Ptr++;
|
||||
}
|
||||
else if (state.Ptr < state.End && *state.Ptr == '}')
|
||||
{
|
||||
state.Ptr++;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool ParseArray(JsonParserState& state, JsonValue& outVal)
|
||||
{
|
||||
if (state.Ptr >= state.End || *state.Ptr != '[')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
state.Ptr++;
|
||||
outVal.Type = JsonType::Array;
|
||||
outVal.ArrayVal.Create(state.Arena JULIET_DEBUG_ONLY(, "JsonArray"));
|
||||
|
||||
while (state.Ptr < state.End)
|
||||
{
|
||||
SkipWhitespace(state);
|
||||
if (state.Ptr < state.End && *state.Ptr == ']')
|
||||
{
|
||||
state.Ptr++;
|
||||
return true;
|
||||
}
|
||||
|
||||
JsonValue val = {};
|
||||
if (!ParseValue(state, val))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
outVal.ArrayVal.PushBack(val);
|
||||
|
||||
SkipWhitespace(state);
|
||||
if (state.Ptr < state.End && *state.Ptr == ',')
|
||||
{
|
||||
state.Ptr++;
|
||||
}
|
||||
else if (state.Ptr < state.End && *state.Ptr == ']')
|
||||
{
|
||||
state.Ptr++;
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static bool ParseNumber(JsonParserState& state, JsonValue& outVal)
|
||||
{
|
||||
const char* start = state.Ptr;
|
||||
while (state.Ptr < state.End)
|
||||
{
|
||||
char c = *state.Ptr;
|
||||
if (isdigit(c) || c == '-' || c == '+' || c == '.' || c == 'e' || c == 'E')
|
||||
{
|
||||
state.Ptr++;
|
||||
}
|
||||
else
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
size_t len = static_cast<size_t>(state.Ptr - start);
|
||||
if (len == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
char temp[64];
|
||||
if (len >= sizeof(temp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
memcpy(temp, start, len);
|
||||
temp[len] = '\0';
|
||||
|
||||
char* endPtr = nullptr;
|
||||
outVal.NumVal = strtod(temp, &endPtr);
|
||||
if (endPtr == temp)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
outVal.Type = JsonType::Number;
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool ParseValue(JsonParserState& state, JsonValue& outVal)
|
||||
{
|
||||
SkipWhitespace(state);
|
||||
if (state.Ptr >= state.End)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
char c = *state.Ptr;
|
||||
if (c == '{')
|
||||
{
|
||||
return ParseObject(state, outVal);
|
||||
}
|
||||
else if (c == '[')
|
||||
{
|
||||
return ParseArray(state, outVal);
|
||||
}
|
||||
else if (c == '"')
|
||||
{
|
||||
outVal.Type = JsonType::String;
|
||||
return ParseString(state, outVal.StrVal);
|
||||
}
|
||||
else if (c == 't' || c == 'f')
|
||||
{
|
||||
size_t remaining = static_cast<size_t>(state.End - state.Ptr);
|
||||
if (c == 't' && remaining >= 4 && strncmp(state.Ptr, "true", 4) == 0)
|
||||
{
|
||||
outVal.Type = JsonType::Bool;
|
||||
outVal.BoolVal = true;
|
||||
state.Ptr += 4;
|
||||
return true;
|
||||
}
|
||||
else if (c == 'f' && remaining >= 5 && strncmp(state.Ptr, "false", 5) == 0)
|
||||
{
|
||||
outVal.Type = JsonType::Bool;
|
||||
outVal.BoolVal = false;
|
||||
state.Ptr += 5;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (c == 'n')
|
||||
{
|
||||
size_t remaining = static_cast<size_t>(state.End - state.Ptr);
|
||||
if (remaining >= 4 && strncmp(state.Ptr, "null", 4) == 0)
|
||||
{
|
||||
outVal.Type = JsonType::Null;
|
||||
state.Ptr += 4;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
else if (isdigit(c) || c == '-')
|
||||
{
|
||||
return ParseNumber(state, outVal);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Json_Parse(Juliet::Arena* arena, const Juliet::String& jsonStr, JsonValue& outValue)
|
||||
{
|
||||
Assert(arena != nullptr);
|
||||
if (jsonStr.Size == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
JsonParserState state;
|
||||
state.Arena = arena;
|
||||
state.Ptr = CStr(jsonStr);
|
||||
state.End = state.Ptr + jsonStr.Size;
|
||||
|
||||
return ParseValue(state, outValue);
|
||||
}
|
||||
|
||||
bool Json_Query(const JsonValue& root, const char* path, Juliet::String& outResult)
|
||||
{
|
||||
Assert(path != nullptr);
|
||||
|
||||
const JsonValue* current = &root;
|
||||
const char* p = path;
|
||||
|
||||
while (*p)
|
||||
{
|
||||
const char* start = p;
|
||||
while (*p && *p != '/')
|
||||
{
|
||||
p++;
|
||||
}
|
||||
size_t len = static_cast<size_t>(p - start);
|
||||
if (len == 0)
|
||||
{
|
||||
if (*p == '/') { p++; }
|
||||
continue;
|
||||
}
|
||||
|
||||
if (current->Type == JsonType::Object)
|
||||
{
|
||||
const JsonValue* next = nullptr;
|
||||
for (const JsonKeyValue& kv : current->ObjectVal)
|
||||
{
|
||||
if (kv.Key.Size == len && strncmp(CStr(kv.Key), start, len) == 0)
|
||||
{
|
||||
next = kv.Value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!next)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
else if (current->Type == JsonType::Array)
|
||||
{
|
||||
char tempIndex[32];
|
||||
if (len >= sizeof(tempIndex))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
memcpy(tempIndex, start, len);
|
||||
tempIndex[len] = '\0';
|
||||
|
||||
int index = atoi(tempIndex);
|
||||
if (index < 0 || static_cast<size_t>(index) >= current->ArrayVal.Size())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
current = ¤t->ArrayVal[static_cast<size_t>(index)];
|
||||
}
|
||||
else
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (*p == '/')
|
||||
{
|
||||
p++;
|
||||
}
|
||||
}
|
||||
|
||||
if (current->Type == JsonType::String)
|
||||
{
|
||||
outResult = current->StrVal;
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
Juliet::String Json_EscapeString(Juliet::Arena* arena, const Juliet::String& str)
|
||||
{
|
||||
Assert(arena != nullptr);
|
||||
|
||||
size_t escLen = 0;
|
||||
const char* p = CStr(str);
|
||||
const char* end = p + str.Size;
|
||||
while (p < end)
|
||||
{
|
||||
char c = *p;
|
||||
if (c == '"' || c == '\\' || c == '\n' || c == '\r' || c == '\t')
|
||||
{
|
||||
escLen += 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
escLen += 1;
|
||||
}
|
||||
p++;
|
||||
}
|
||||
|
||||
char* buf = Juliet::ArenaPushArray<char>(arena, escLen + 1 JULIET_DEBUG_PARAM("JsonEscapedStr"));
|
||||
char* dst = buf;
|
||||
p = CStr(str);
|
||||
while (p < end)
|
||||
{
|
||||
char c = *p;
|
||||
if (c == '"') { *dst++ = '\\'; *dst++ = '"'; }
|
||||
else if (c == '\\') { *dst++ = '\\'; *dst++ = '\\'; }
|
||||
else if (c == '\n') { *dst++ = '\\'; *dst++ = 'n'; }
|
||||
else if (c == '\r') { *dst++ = '\\'; *dst++ = 'r'; }
|
||||
else if (c == '\t') { *dst++ = '\\'; *dst++ = 't'; }
|
||||
else { *dst++ = c; }
|
||||
p++;
|
||||
}
|
||||
*dst = '\0';
|
||||
return { buf, escLen };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#pragma once
|
||||
|
||||
#include <Core/Common/String.h>
|
||||
#include <Core/Container/Vector.h>
|
||||
|
||||
namespace Romeo
|
||||
{
|
||||
enum class JsonType
|
||||
{
|
||||
Null,
|
||||
Bool,
|
||||
Number,
|
||||
String,
|
||||
Array,
|
||||
Object
|
||||
};
|
||||
|
||||
struct JsonValue;
|
||||
|
||||
struct JsonKeyValue
|
||||
{
|
||||
Juliet::String Key;
|
||||
// Pointer is used here since JsonValue contains JsonKeyValue, forming mutual dependency
|
||||
JsonValue* Value = nullptr;
|
||||
};
|
||||
|
||||
struct JsonValue
|
||||
{
|
||||
JsonType Type = JsonType::Null;
|
||||
bool BoolVal = false;
|
||||
double NumVal = 0.0;
|
||||
Juliet::String StrVal = {};
|
||||
|
||||
Juliet::VectorArena<JsonValue, 32, true> ArrayVal = {};
|
||||
Juliet::VectorArena<JsonKeyValue, 32, true> ObjectVal = {};
|
||||
};
|
||||
|
||||
// Parses a JSON string. Memory is allocated on the provided arena.
|
||||
[[nodiscard]] bool Json_Parse(Juliet::Arena* arena, const Juliet::String& jsonStr, JsonValue& outValue);
|
||||
|
||||
// Queries a nested string value by path (e.g., "candidates/0/content/parts/0/text" or "response").
|
||||
[[nodiscard]] bool Json_Query(const JsonValue& root, const char* path, Juliet::String& outResult);
|
||||
|
||||
// Escapes a string to be safely embedded inside JSON.
|
||||
[[nodiscard]] Juliet::String Json_EscapeString(Juliet::Arena* arena, const Juliet::String& str);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
#include "Database.h"
|
||||
|
||||
namespace Romeo
|
||||
{
|
||||
void MD_GenerateDocumentation(Database& db, const Juliet::String& workspaceRoot, const Juliet::String& docsDir);
|
||||
}
|
||||
@@ -0,0 +1,549 @@
|
||||
#ifndef WIN32_LEAN_AND_MEAN
|
||||
#define WIN32_LEAN_AND_MEAN
|
||||
#endif
|
||||
#ifndef NOMINMAX
|
||||
#define NOMINMAX
|
||||
#endif
|
||||
#include <Windows.h>
|
||||
#include <shellapi.h>
|
||||
#include <stdio.h>
|
||||
|
||||
#include <Core/Common/CoreUtils.h>
|
||||
#include <Core/Main.h>
|
||||
#include <Core/Memory/MemoryArena.h>
|
||||
#include <Core/JulietInit.h>
|
||||
#include <Core/HAL/Filesystem/Filesystem.h>
|
||||
#include <Core/Thread/Mutex.h>
|
||||
#include <thread>
|
||||
#include "Database.h"
|
||||
#include "MarkdownGenerator.h"
|
||||
|
||||
#define ROMEO_TRAY_ICON_ID 1
|
||||
#define ROMEO_WM_TRAYICON (WM_USER + 1)
|
||||
|
||||
static Romeo::Database G_Db;
|
||||
static Juliet::Arena* G_ScratchArena = nullptr;
|
||||
static Juliet::String G_WorkspaceRoot;
|
||||
static Juliet::String G_DocsDir;
|
||||
static Juliet::String G_DbFilePath;
|
||||
|
||||
static Juliet::String GetWorkspaceRoot(Juliet::Arena* arena)
|
||||
{
|
||||
Assert(arena != nullptr);
|
||||
Juliet::String base = Juliet::GetBasePath();
|
||||
|
||||
// Copy to arena so we can safely modify it
|
||||
char* buf = Juliet::ArenaPushArray<char>(arena, base.Size + 1 JULIET_DEBUG_PARAM("WorkspaceRootPath"));
|
||||
memcpy(buf, base.Data, base.Size);
|
||||
buf[base.Size] = '\0';
|
||||
|
||||
size_t count = 0;
|
||||
for (size_t i = base.Size; i > 0; --i)
|
||||
{
|
||||
size_t idx = i - 1;
|
||||
if (buf[idx] == '\\' || buf[idx] == '/')
|
||||
{
|
||||
count++;
|
||||
if (count == 3)
|
||||
{
|
||||
buf[idx] = '\0';
|
||||
return { buf, idx };
|
||||
}
|
||||
}
|
||||
}
|
||||
return { buf, base.Size };
|
||||
}
|
||||
|
||||
static HANDLE G_ExitEvent = INVALID_HANDLE_VALUE;
|
||||
static std::thread G_MonitorThread;
|
||||
static bool G_MonitorRunning = false;
|
||||
static HANDLE G_OllamaProcessHandle = INVALID_HANDLE_VALUE;
|
||||
static HANDLE G_OllamaJobHandle = nullptr;
|
||||
|
||||
static void MonitorThreadProc()
|
||||
{
|
||||
int wLen = MultiByteToWideChar(CP_UTF8, 0, CStr(G_WorkspaceRoot), -1, nullptr, 0);
|
||||
if (wLen <= 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
wchar_t* wRoot = new wchar_t[static_cast<size_t>(wLen + 1)];
|
||||
MultiByteToWideChar(CP_UTF8, 0, CStr(G_WorkspaceRoot), -1, wRoot, wLen);
|
||||
|
||||
HANDLE hDir = CreateFileW(
|
||||
wRoot,
|
||||
FILE_LIST_DIRECTORY,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE,
|
||||
nullptr,
|
||||
OPEN_EXISTING,
|
||||
FILE_FLAG_BACKUP_SEMANTICS | FILE_FLAG_OVERLAPPED,
|
||||
nullptr
|
||||
);
|
||||
|
||||
delete[] wRoot;
|
||||
|
||||
if (hDir == INVALID_HANDLE_VALUE)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
HANDLE hEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||
if (hEvent == nullptr)
|
||||
{
|
||||
CloseHandle(hDir);
|
||||
return;
|
||||
}
|
||||
|
||||
OVERLAPPED overlapped = {};
|
||||
overlapped.hEvent = hEvent;
|
||||
|
||||
alignas(FILE_NOTIFY_INFORMATION) BYTE buffer[64 * 1024];
|
||||
bool pendingChanges = false;
|
||||
|
||||
BOOL readRes = ReadDirectoryChangesW(
|
||||
hDir,
|
||||
buffer,
|
||||
sizeof(buffer),
|
||||
TRUE,
|
||||
FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_FILE_NAME,
|
||||
nullptr,
|
||||
&overlapped,
|
||||
nullptr
|
||||
);
|
||||
|
||||
if (readRes == 0)
|
||||
{
|
||||
CloseHandle(hEvent);
|
||||
CloseHandle(hDir);
|
||||
return;
|
||||
}
|
||||
|
||||
HANDLE waitHandles[2] = { G_ExitEvent, hEvent };
|
||||
|
||||
while (G_MonitorRunning)
|
||||
{
|
||||
DWORD timeout = pendingChanges ? 500 : INFINITE;
|
||||
DWORD waitRes = WaitForMultipleObjects(2, waitHandles, FALSE, timeout);
|
||||
|
||||
if (waitRes == WAIT_OBJECT_0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else if (waitRes == WAIT_OBJECT_0 + 1)
|
||||
{
|
||||
DWORD bytesTransferred = 0;
|
||||
if (GetOverlappedResult(hDir, &overlapped, &bytesTransferred, FALSE) != 0)
|
||||
{
|
||||
FILE_NOTIFY_INFORMATION* info = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(buffer);
|
||||
while (info != nullptr)
|
||||
{
|
||||
size_t wCharCount = info->FileNameLength / sizeof(WCHAR);
|
||||
if (wCharCount > 2)
|
||||
{
|
||||
const WCHAR* fName = info->FileName;
|
||||
if ((fName[wCharCount - 1] == L'h' && fName[wCharCount - 2] == L'.') ||
|
||||
(wCharCount > 4 && fName[wCharCount - 1] == L'p' && fName[wCharCount - 2] == L'p' && fName[wCharCount - 3] == L'c' && fName[wCharCount - 4] == L'.'))
|
||||
{
|
||||
bool ignore = false;
|
||||
for (size_t i = 0; i < wCharCount; ++i)
|
||||
{
|
||||
if (i + 12 <= wCharCount && wcsncmp(fName + i, L"Intermediate", 12) == 0) { ignore = true; }
|
||||
if (i + 3 <= wCharCount && wcsncmp(fName + i, L"bin", 3) == 0) { ignore = true; }
|
||||
if (i + 8 <= wCharCount && wcsncmp(fName + i, L"External", 8) == 0) { ignore = true; }
|
||||
if (i + 4 <= wCharCount && wcsncmp(fName + i, L".git", 4) == 0) { ignore = true; }
|
||||
if (ignore) { break; }
|
||||
}
|
||||
|
||||
if (!ignore)
|
||||
{
|
||||
pendingChanges = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (info->NextEntryOffset == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
info = reinterpret_cast<FILE_NOTIFY_INFORMATION*>(reinterpret_cast<BYTE*>(info) + info->NextEntryOffset);
|
||||
}
|
||||
}
|
||||
|
||||
ResetEvent(hEvent);
|
||||
ReadDirectoryChangesW(
|
||||
hDir,
|
||||
buffer,
|
||||
sizeof(buffer),
|
||||
TRUE,
|
||||
FILE_NOTIFY_CHANGE_LAST_WRITE | FILE_NOTIFY_CHANGE_FILE_NAME,
|
||||
nullptr,
|
||||
&overlapped,
|
||||
nullptr
|
||||
);
|
||||
}
|
||||
else if (waitRes == WAIT_TIMEOUT)
|
||||
{
|
||||
printf("Romeo Monitor: Change detected, updating documentation...\n");
|
||||
OutputDebugStringA("Romeo Monitor: Change detected, updating...\n");
|
||||
|
||||
{
|
||||
Juliet::LockGuard lock(G_Db.Mutex);
|
||||
Juliet::ArenaClear(G_ScratchArena);
|
||||
|
||||
Romeo::DB_ScanWorkspace(G_Db, G_ScratchArena, G_WorkspaceRoot);
|
||||
|
||||
bool anyChanged = false;
|
||||
for (const Romeo::FileEntry& entry : G_Db.Entries)
|
||||
{
|
||||
if (entry.LastModifiedHeader > entry.LastMarkdownCreatedTime ||
|
||||
entry.LastModifiedCpp > entry.LastMarkdownCreatedTime)
|
||||
{
|
||||
anyChanged = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (anyChanged)
|
||||
{
|
||||
Romeo::MD_GenerateDocumentation(G_Db, G_WorkspaceRoot, G_DocsDir);
|
||||
Romeo::DB_Save(G_Db, G_DbFilePath);
|
||||
}
|
||||
}
|
||||
|
||||
pendingChanges = false;
|
||||
}
|
||||
}
|
||||
|
||||
CancelIo(hDir);
|
||||
CloseHandle(hEvent);
|
||||
CloseHandle(hDir);
|
||||
}
|
||||
|
||||
LRESULT CALLBACK RomeoWindowProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
switch (uMsg)
|
||||
{
|
||||
case ROMEO_WM_TRAYICON:
|
||||
if (LOWORD(lParam) == WM_RBUTTONUP)
|
||||
{
|
||||
POINT pt;
|
||||
GetCursorPos(&pt);
|
||||
HMENU hMenu = CreatePopupMenu();
|
||||
if (hMenu)
|
||||
{
|
||||
InsertMenuW(hMenu, 0, MF_BYPOSITION | MF_STRING, 1001, L"Exit");
|
||||
SetForegroundWindow(hwnd);
|
||||
TrackPopupMenu(hMenu, TPM_BOTTOMALIGN | TPM_LEFTALIGN, pt.x, pt.y, 0, hwnd, nullptr);
|
||||
DestroyMenu(hMenu);
|
||||
}
|
||||
}
|
||||
else if (LOWORD(lParam) == WM_LBUTTONUP)
|
||||
{
|
||||
OutputDebugStringA("Tray icon left-clicked!\n");
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_COMMAND:
|
||||
if (LOWORD(wParam) == 1001)
|
||||
{
|
||||
DestroyWindow(hwnd);
|
||||
}
|
||||
return 0;
|
||||
|
||||
case WM_DESTROY:
|
||||
PostQuitMessage(0);
|
||||
return 0;
|
||||
}
|
||||
return DefWindowProc(hwnd, uMsg, wParam, lParam);
|
||||
}
|
||||
|
||||
int JulietMain(int argc, wchar_t** argv)
|
||||
{
|
||||
printf("JulietMain Entry!\n");
|
||||
|
||||
// Single instance mutex check
|
||||
HANDLE hMutex = CreateMutexW(nullptr, TRUE, L"RomeoSingleInstanceMutex");
|
||||
if (hMutex == nullptr || GetLastError() == ERROR_ALREADY_EXISTS)
|
||||
{
|
||||
printf("Romeo is already running. Exiting.\n");
|
||||
if (hMutex != nullptr)
|
||||
{
|
||||
CloseHandle(hMutex);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Launch Ollama serve subprocess
|
||||
{
|
||||
// Try to find ollama.exe - check common install locations
|
||||
char ollamaPath[512] = {};
|
||||
bool foundOllama = false;
|
||||
|
||||
// 1. Try PATH first via SearchPathA
|
||||
if (SearchPathA(nullptr, "ollama.exe", nullptr, sizeof(ollamaPath), ollamaPath, nullptr) > 0)
|
||||
{
|
||||
foundOllama = true;
|
||||
}
|
||||
|
||||
// 2. Check %LOCALAPPDATA%\Programs\Ollama\ollama.exe
|
||||
if (!foundOllama)
|
||||
{
|
||||
char localAppData[256] = {};
|
||||
DWORD envLen = GetEnvironmentVariableA("LOCALAPPDATA", localAppData, sizeof(localAppData));
|
||||
if (envLen > 0 && envLen < sizeof(localAppData))
|
||||
{
|
||||
snprintf(ollamaPath, sizeof(ollamaPath), "%s\\Programs\\Ollama\\ollama.exe", localAppData);
|
||||
DWORD attribs = GetFileAttributesA(ollamaPath);
|
||||
if (attribs != INVALID_FILE_ATTRIBUTES)
|
||||
{
|
||||
foundOllama = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 3. Check %USERPROFILE%\AppData\Local\Programs\Ollama\ollama.exe
|
||||
if (!foundOllama)
|
||||
{
|
||||
char userProfile[256] = {};
|
||||
DWORD envLen = GetEnvironmentVariableA("USERPROFILE", userProfile, sizeof(userProfile));
|
||||
if (envLen > 0 && envLen < sizeof(userProfile))
|
||||
{
|
||||
snprintf(ollamaPath, sizeof(ollamaPath), "%s\\AppData\\Local\\Programs\\Ollama\\ollama.exe", userProfile);
|
||||
DWORD attribs = GetFileAttributesA(ollamaPath);
|
||||
if (attribs != INVALID_FILE_ATTRIBUTES)
|
||||
{
|
||||
foundOllama = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (foundOllama)
|
||||
{
|
||||
printf("Romeo: Found Ollama at: %s\n", ollamaPath);
|
||||
|
||||
// Create a Job Object so child processes auto-terminate when Romeo exits (even on crash)
|
||||
G_OllamaJobHandle = CreateJobObjectA(nullptr, nullptr);
|
||||
if (G_OllamaJobHandle != nullptr)
|
||||
{
|
||||
JOBOBJECT_EXTENDED_LIMIT_INFORMATION jobInfo = {};
|
||||
jobInfo.BasicLimitInformation.LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE;
|
||||
SetInformationJobObject(G_OllamaJobHandle, JobObjectExtendedLimitInformation,
|
||||
&jobInfo, sizeof(jobInfo));
|
||||
}
|
||||
|
||||
// Build a mutable command line buffer (CreateProcessA requires mutable string)
|
||||
char cmdLine[600] = {};
|
||||
snprintf(cmdLine, sizeof(cmdLine), "\"%s\" serve", ollamaPath);
|
||||
|
||||
STARTUPINFOA si = {};
|
||||
si.cb = sizeof(si);
|
||||
PROCESS_INFORMATION pi = {};
|
||||
BOOL createRes = CreateProcessA(
|
||||
ollamaPath,
|
||||
cmdLine,
|
||||
nullptr,
|
||||
nullptr,
|
||||
FALSE,
|
||||
CREATE_NEW_CONSOLE | CREATE_SUSPENDED,
|
||||
nullptr,
|
||||
nullptr,
|
||||
&si,
|
||||
&pi
|
||||
);
|
||||
if (createRes != 0)
|
||||
{
|
||||
// Assign to job object before resuming so it cannot escape
|
||||
if (G_OllamaJobHandle != nullptr)
|
||||
{
|
||||
AssignProcessToJobObject(G_OllamaJobHandle, pi.hProcess);
|
||||
}
|
||||
ResumeThread(pi.hThread);
|
||||
|
||||
// Wait briefly to check if the process crashed immediately
|
||||
DWORD waitResult = WaitForSingleObject(pi.hProcess, 2000);
|
||||
if (waitResult == WAIT_TIMEOUT)
|
||||
{
|
||||
// Process is still running after 2 seconds - good
|
||||
G_OllamaProcessHandle = pi.hProcess;
|
||||
CloseHandle(pi.hThread);
|
||||
printf("Romeo: Launched local Ollama serve subprocess (PID: %lu).\n", pi.dwProcessId);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Process exited within 2 seconds - likely crashed or port in use
|
||||
DWORD exitCode = 0;
|
||||
GetExitCodeProcess(pi.hProcess, &exitCode);
|
||||
printf("Romeo: WARNING - Ollama serve process exited immediately (exit code: %lu).\n", exitCode);
|
||||
printf("Romeo: This may mean Ollama is already running or the port 11434 is in use.\n");
|
||||
CloseHandle(pi.hProcess);
|
||||
CloseHandle(pi.hThread);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
DWORD err = GetLastError();
|
||||
printf("Romeo: ERROR - Failed to launch Ollama serve (Win32 error: %lu).\n", err);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("Romeo: WARNING - Could not find ollama.exe. Searched PATH and common install locations.\n");
|
||||
printf("Romeo: Please ensure Ollama is installed (https://ollama.com) and available in your PATH.\n");
|
||||
printf("Romeo: Continuing without auto-launching Ollama serve...\n");
|
||||
}
|
||||
}
|
||||
|
||||
Juliet::ArenaParams params{};
|
||||
Juliet::Arena* mainArena = Juliet::ArenaAllocate(params JULIET_DEBUG_PARAM("RomeoMainArena"));
|
||||
Assert(mainArena != nullptr);
|
||||
|
||||
G_ScratchArena = Juliet::ArenaAllocate(params JULIET_DEBUG_PARAM("RomeoScratchArena"));
|
||||
Assert(G_ScratchArena != nullptr);
|
||||
|
||||
printf("Arena Allocated! Pointer: %p\n", static_cast<void*>(mainArena));
|
||||
|
||||
G_WorkspaceRoot = GetWorkspaceRoot(mainArena);
|
||||
|
||||
char docsDirBuf[512];
|
||||
snprintf(docsDirBuf, sizeof(docsDirBuf), "%s\\Romeo\\docs", CStr(G_WorkspaceRoot));
|
||||
G_DocsDir = Juliet::StringCopy(mainArena, Juliet::WrapString(docsDirBuf));
|
||||
|
||||
char dbFilePathBuf[512];
|
||||
snprintf(dbFilePathBuf, sizeof(dbFilePathBuf), "%s\\Romeo\\docs\\romeo.db", CStr(G_WorkspaceRoot));
|
||||
G_DbFilePath = Juliet::StringCopy(mainArena, Juliet::WrapString(dbFilePathBuf));
|
||||
|
||||
Romeo::DB_Init(G_Db, mainArena);
|
||||
|
||||
printf("DB Init!\n");
|
||||
|
||||
bool forceRegenerate = false;
|
||||
if (argc > 1 && (wcscmp(argv[1], L"-force") == 0 || wcscmp(argv[1], L"-f") == 0))
|
||||
{
|
||||
forceRegenerate = true;
|
||||
}
|
||||
|
||||
{
|
||||
Juliet::LockGuard lock(G_Db.Mutex);
|
||||
if (!forceRegenerate)
|
||||
{
|
||||
Romeo::DB_Load(G_Db, G_DbFilePath);
|
||||
}
|
||||
else
|
||||
{
|
||||
printf("Romeo: Force flag detected, clearing cache and rebuilding all documentation...\n");
|
||||
}
|
||||
|
||||
Romeo::DB_ScanWorkspace(G_Db, G_ScratchArena, G_WorkspaceRoot);
|
||||
|
||||
if (forceRegenerate)
|
||||
{
|
||||
for (Romeo::FileEntry& entry : G_Db.Entries)
|
||||
{
|
||||
entry.Description = {};
|
||||
entry.LastMarkdownCreatedTime = 0;
|
||||
entry.LastAIGeneratedTime = 0;
|
||||
}
|
||||
}
|
||||
|
||||
Romeo::MD_GenerateDocumentation(G_Db, G_WorkspaceRoot, G_DocsDir);
|
||||
Romeo::DB_Save(G_Db, G_DbFilePath);
|
||||
}
|
||||
|
||||
char debugMsg[256];
|
||||
snprintf(debugMsg, sizeof(debugMsg), "Romeo scanned workspace. Found %llu C++ files.\n", static_cast<unsigned long long>(G_Db.Entries.Size()));
|
||||
printf("%s", debugMsg);
|
||||
|
||||
// Register hidden window class
|
||||
const wchar_t CLASS_NAME[] = L"RomeoHiddenWindowClass";
|
||||
WNDCLASSW wc = {};
|
||||
HINSTANCE hInstance = GetModuleHandleW(nullptr);
|
||||
wc.lpfnWndProc = RomeoWindowProc;
|
||||
wc.hInstance = hInstance;
|
||||
wc.lpszClassName = CLASS_NAME;
|
||||
|
||||
RegisterClassW(&wc);
|
||||
|
||||
// Create hidden window to receive messages
|
||||
HWND hwnd = CreateWindowExW(
|
||||
0,
|
||||
CLASS_NAME,
|
||||
L"Romeo Tool",
|
||||
0,
|
||||
CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
|
||||
nullptr,
|
||||
nullptr,
|
||||
hInstance,
|
||||
nullptr
|
||||
);
|
||||
|
||||
if (hwnd == nullptr)
|
||||
{
|
||||
CloseHandle(hMutex);
|
||||
return 0;
|
||||
}
|
||||
|
||||
G_ExitEvent = CreateEventW(nullptr, TRUE, FALSE, nullptr);
|
||||
if (G_ExitEvent != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
G_MonitorRunning = true;
|
||||
G_MonitorThread = std::thread(MonitorThreadProc);
|
||||
}
|
||||
|
||||
// Add tray icon
|
||||
NOTIFYICONDATAW nid = {};
|
||||
nid.cbSize = sizeof(NOTIFYICONDATAW);
|
||||
nid.hWnd = hwnd;
|
||||
nid.uID = ROMEO_TRAY_ICON_ID;
|
||||
nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
|
||||
nid.uCallbackMessage = ROMEO_WM_TRAYICON;
|
||||
nid.hIcon = LoadIcon(nullptr, IDI_APPLICATION);
|
||||
wcscpy_s(nid.szTip, L"Romeo Documentation Tool");
|
||||
|
||||
Shell_NotifyIconW(NIM_ADD, &nid);
|
||||
|
||||
// Message loop
|
||||
MSG msg = {};
|
||||
while (GetMessage(&msg, nullptr, 0, 0))
|
||||
{
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
Shell_NotifyIconW(NIM_DELETE, &nid);
|
||||
|
||||
G_MonitorRunning = false;
|
||||
if (G_ExitEvent != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
SetEvent(G_ExitEvent);
|
||||
}
|
||||
if (G_MonitorThread.joinable())
|
||||
{
|
||||
G_MonitorThread.join();
|
||||
}
|
||||
if (G_ExitEvent != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
CloseHandle(G_ExitEvent);
|
||||
G_ExitEvent = INVALID_HANDLE_VALUE;
|
||||
}
|
||||
|
||||
if (G_OllamaProcessHandle != INVALID_HANDLE_VALUE)
|
||||
{
|
||||
TerminateProcess(G_OllamaProcessHandle, 0);
|
||||
CloseHandle(G_OllamaProcessHandle);
|
||||
G_OllamaProcessHandle = INVALID_HANDLE_VALUE;
|
||||
}
|
||||
if (G_OllamaJobHandle != nullptr)
|
||||
{
|
||||
CloseHandle(G_OllamaJobHandle);
|
||||
G_OllamaJobHandle = nullptr;
|
||||
}
|
||||
|
||||
Juliet::ArenaRelease(mainArena);
|
||||
Juliet::ArenaRelease(G_ScratchArena);
|
||||
|
||||
CloseHandle(hMutex);
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user