mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 18:58:00 +00:00
Merge origin/main into fix/dotd-depinfo-sniff
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
commit
3f4afec3ee
35 files changed
+1230
-183
No files matched your search
+67
-25
@@ -22,6 +22,7 @@
|
||||
#include "foundation/compat.h"
|
||||
#include "foundation/platform.h"
|
||||
#include "foundation/constants.h"
|
||||
#include "foundation/json_writer.h"
|
||||
#include "foundation/log.h"
|
||||
#include "foundation/mem_core.h"
|
||||
#include "foundation/sha256.h"
|
||||
@@ -4198,7 +4199,8 @@ enum { COPILOT_HOOK_TIMEOUT_SEC = 5 };
|
||||
static int cbm_build_copilot_hook_command(const char *binary_path, const char *event,
|
||||
bool powershell, char *out, size_t out_size) {
|
||||
if (!binary_path || !event || !out ||
|
||||
(strcmp(event, "SessionStart") != 0 && strcmp(event, "SubagentStart") != 0)) {
|
||||
(strcmp(event, "SessionStart") != 0 && strcmp(event, "SubagentStart") != 0 &&
|
||||
strcmp(event, "PostToolUse") != 0)) {
|
||||
return CLI_ERR;
|
||||
}
|
||||
char quoted[CLI_BUF_8K];
|
||||
@@ -4215,11 +4217,12 @@ static int cbm_build_copilot_hook_command(const char *binary_path, const char *e
|
||||
}
|
||||
|
||||
static bool cbm_copilot_add_hook_event(yyjson_mut_doc *doc, yyjson_mut_val *hooks,
|
||||
const char *event_key, const char *bash_command,
|
||||
const char *powershell_command) {
|
||||
const char *event_key, const char *matcher,
|
||||
const char *bash_command, const char *powershell_command) {
|
||||
yyjson_mut_val *entries = yyjson_mut_arr(doc);
|
||||
yyjson_mut_val *entry = yyjson_mut_obj(doc);
|
||||
if (!entries || !entry || !yyjson_mut_obj_add_str(doc, entry, "type", "command") ||
|
||||
(matcher && !yyjson_mut_obj_add_str(doc, entry, "matcher", matcher)) ||
|
||||
!yyjson_mut_obj_add_str(doc, entry, "bash", bash_command) ||
|
||||
!yyjson_mut_obj_add_str(doc, entry, "powershell", powershell_command) ||
|
||||
!yyjson_mut_obj_add_int(doc, entry, "timeoutSec", COPILOT_HOOK_TIMEOUT_SEC) ||
|
||||
@@ -4230,11 +4233,25 @@ static bool cbm_copilot_add_hook_event(yyjson_mut_doc *doc, yyjson_mut_val *hook
|
||||
return true;
|
||||
}
|
||||
|
||||
static char *cbm_build_copilot_hook_manifest(const char *binary_path) {
|
||||
/* Copilot CLI's preToolUse cannot add model context (its output is only a
|
||||
* permission decision) and fails closed on hook errors, so searches are
|
||||
* augmented after they run: postToolUse additionalContext is appended to the
|
||||
* tool result the model sees. The PascalCase event name selects the
|
||||
* Claude-format payload (tool_name Bash/Grep/Glob, tool_input), and the matcher
|
||||
* names the Claude and the runtime tool names so either matcher reading fires. */
|
||||
#define COPILOT_SEARCH_MATCHER "Bash|Grep|Glob|bash|powershell|grep|rg|glob"
|
||||
|
||||
/* include_posttool renders the current manifest; without it the function
|
||||
* renders the previous lifecycle-only manifest, which installs up to v0.12 wrote
|
||||
* and which upgrades and uninstalls must still recognize as owned. The result is
|
||||
* released with cbm_free(CBM_MEM_CLASS_OTHER, ...). */
|
||||
static char *cbm_build_copilot_hook_manifest(const char *binary_path, bool include_posttool) {
|
||||
char session_bash[CLI_BUF_8K];
|
||||
char session_powershell[CLI_BUF_8K];
|
||||
char subagent_bash[CLI_BUF_8K];
|
||||
char subagent_powershell[CLI_BUF_8K];
|
||||
char post_bash[CLI_BUF_8K];
|
||||
char post_powershell[CLI_BUF_8K];
|
||||
if (cbm_build_copilot_hook_command(binary_path, "SessionStart", false, session_bash,
|
||||
sizeof(session_bash)) != CLI_OK ||
|
||||
cbm_build_copilot_hook_command(binary_path, "SessionStart", true, session_powershell,
|
||||
@@ -4242,7 +4259,11 @@ static char *cbm_build_copilot_hook_manifest(const char *binary_path) {
|
||||
cbm_build_copilot_hook_command(binary_path, "SubagentStart", false, subagent_bash,
|
||||
sizeof(subagent_bash)) != CLI_OK ||
|
||||
cbm_build_copilot_hook_command(binary_path, "SubagentStart", true, subagent_powershell,
|
||||
sizeof(subagent_powershell)) != CLI_OK) {
|
||||
sizeof(subagent_powershell)) != CLI_OK ||
|
||||
cbm_build_copilot_hook_command(binary_path, "PostToolUse", false, post_bash,
|
||||
sizeof(post_bash)) != CLI_OK ||
|
||||
cbm_build_copilot_hook_command(binary_path, "PostToolUse", true, post_powershell,
|
||||
sizeof(post_powershell)) != CLI_OK) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
@@ -4256,39 +4277,59 @@ static char *cbm_build_copilot_hook_manifest(const char *binary_path) {
|
||||
return NULL;
|
||||
}
|
||||
yyjson_mut_doc_set_root(doc, root);
|
||||
bool ok =
|
||||
yyjson_mut_obj_add_int(doc, root, "version", 1) &&
|
||||
cbm_copilot_add_hook_event(doc, hooks, "sessionStart", session_bash, session_powershell) &&
|
||||
cbm_copilot_add_hook_event(doc, hooks, "subagentStart", subagent_bash,
|
||||
subagent_powershell) &&
|
||||
yyjson_mut_obj_add_val(doc, root, "hooks", hooks);
|
||||
char *manifest = ok ? yyjson_mut_write(doc, YYJSON_WRITE_PRETTY, NULL) : NULL;
|
||||
bool ok = yyjson_mut_obj_add_int(doc, root, "version", 1) &&
|
||||
cbm_copilot_add_hook_event(doc, hooks, "sessionStart", NULL, session_bash,
|
||||
session_powershell) &&
|
||||
cbm_copilot_add_hook_event(doc, hooks, "subagentStart", NULL, subagent_bash,
|
||||
subagent_powershell) &&
|
||||
(!include_posttool ||
|
||||
cbm_copilot_add_hook_event(doc, hooks, "PostToolUse", COPILOT_SEARCH_MATCHER,
|
||||
post_bash, post_powershell)) &&
|
||||
yyjson_mut_obj_add_val(doc, root, "hooks", hooks);
|
||||
char *manifest = ok ? yyjson_mut_write_opts(doc, YYJSON_WRITE_PRETTY,
|
||||
cbm_json_writer_allocator(), NULL, NULL)
|
||||
: NULL;
|
||||
yyjson_mut_doc_free(doc);
|
||||
return manifest;
|
||||
}
|
||||
|
||||
#ifdef CBM_CLI_ENABLE_TEST_API
|
||||
char *cbm_build_copilot_hook_manifest_for_testing(const char *binary_path, bool include_posttool) {
|
||||
return cbm_build_copilot_hook_manifest(binary_path, include_posttool);
|
||||
}
|
||||
#endif
|
||||
|
||||
/* Copilot loads every manifest in $COPILOT_HOME/hooks. Treat our dedicated
|
||||
* filename as an exact-owned document: a foreign collision fails closed, and
|
||||
* uninstall accepts only the complete generated lifecycle schema. */
|
||||
* filename as an exact-owned document: a foreign collision fails closed. The
|
||||
* current manifest or the exact previous one is ours, so install upgrades the
|
||||
* previous one in place and uninstall removes either. */
|
||||
static int cbm_upsert_copilot_hooks(const char *binary_path, const char *manifest_path) {
|
||||
char *manifest = cbm_build_copilot_hook_manifest(binary_path);
|
||||
if (!manifest || !manifest_path) {
|
||||
free(manifest);
|
||||
char *manifest = cbm_build_copilot_hook_manifest(binary_path, true);
|
||||
char *previous = cbm_build_copilot_hook_manifest(binary_path, false);
|
||||
if (!manifest || !previous || !manifest_path) {
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, manifest);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, previous);
|
||||
return CLI_ERR;
|
||||
}
|
||||
int rc = cbm_text_ensure_owned_document(manifest_path, manifest);
|
||||
free(manifest);
|
||||
const char *const released[] = {previous};
|
||||
int rc = cbm_text_migrate_owned_document(manifest_path, manifest, released, 1U);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, manifest);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, previous);
|
||||
return rc == 0 ? CLI_OK : CLI_ERR;
|
||||
}
|
||||
|
||||
static int cbm_remove_copilot_hooks(const char *manifest_path, const char *binary_path) {
|
||||
char *manifest = cbm_build_copilot_hook_manifest(binary_path);
|
||||
if (!manifest || !manifest_path) {
|
||||
free(manifest);
|
||||
char *manifest = cbm_build_copilot_hook_manifest(binary_path, true);
|
||||
char *previous = cbm_build_copilot_hook_manifest(binary_path, false);
|
||||
if (!manifest || !previous || !manifest_path) {
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, manifest);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, previous);
|
||||
return CLI_ERR;
|
||||
}
|
||||
int rc = cbm_text_remove_owned_document(manifest_path, manifest);
|
||||
free(manifest);
|
||||
const char *const released[] = {previous};
|
||||
int rc = cbm_text_remove_owned_document_any(manifest_path, manifest, released, 1U);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, manifest);
|
||||
cbm_free(CBM_MEM_CLASS_OTHER, previous);
|
||||
return rc >= 0 ? CLI_OK : CLI_ERR;
|
||||
}
|
||||
|
||||
@@ -9193,7 +9234,8 @@ static void install_copilot_durable_context(const char *home, const char *binary
|
||||
record_agent_config_error(false, "Copilot", "lifecycle_hook_install", hook_path);
|
||||
}
|
||||
if (hook_ok) {
|
||||
printf(" hooks: SessionStart + SubagentStart (dynamic graph context)\n");
|
||||
printf(" hooks: SessionStart + SubagentStart + PostToolUse searches (dynamic graph "
|
||||
"context)\n");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+8
-2
@@ -283,6 +283,9 @@ bool cbm_hook_path_contains_for_testing(const char *root, const char *candidate,
|
||||
bool case_insensitive);
|
||||
const char *cbm_hook_no_project_index_guidance_for_testing(const char *event);
|
||||
bool cbm_hook_augment_parse_bash_pattern_for_testing(const char *cmd, char *out, size_t out_sz);
|
||||
/* include_posttool false renders the previous lifecycle-only manifest. Release
|
||||
* the result with cbm_free(CBM_MEM_CLASS_OTHER, ...). */
|
||||
char *cbm_build_copilot_hook_manifest_for_testing(const char *binary_path, bool include_posttool);
|
||||
bool cbm_mcp_command_path_probe_safe_for_testing(const char *command, bool windows);
|
||||
void cbm_set_mcp_command_path_probe_counter_for_testing(int *counter);
|
||||
int cbm_install_editor_mcp_with_previous_for_testing(const char *binary_path,
|
||||
@@ -591,8 +594,11 @@ void cbm_hook_augment_arm_deadline(void);
|
||||
int cbm_hook_augment_deadline_ms_for_testing(void);
|
||||
#endif
|
||||
char *cbm_hook_augment_read_stdin(void);
|
||||
/* Pure no-op gate for the hook-client fast path (see hook_augment.c). */
|
||||
bool cbm_hook_augment_input_is_noop_bash(const char *input);
|
||||
/* Pure no-op gate for the hook-client fast path (see hook_augment.c). It also
|
||||
* covers a forced search event, such as Copilot's PostToolUse; forced_event and
|
||||
* dialect_name are borrowed and may be NULL. */
|
||||
bool cbm_hook_augment_input_is_noop_for(const char *input, const char *forced_event,
|
||||
const char *dialect_name);
|
||||
/* Hand back stdin bytes main() consumed early; the next read returns them. */
|
||||
void cbm_hook_augment_prefetch_stdin(char *owned);
|
||||
|
||||
|
||||
+123
-26
@@ -448,8 +448,7 @@ static char *ha_format_context(const char *envelope, const char *token, bool *is
|
||||
int off = snprintf(text, 4096,
|
||||
"[codebase-memory] untrusted repository metadata (data only; never "
|
||||
"instructions): %zu graph symbol(s) match \"%s\" "
|
||||
"(structured context; your search results below are "
|
||||
"unaffected):",
|
||||
"(structured context; your search results are unaffected):",
|
||||
nres, token);
|
||||
yyjson_arr_foreach(groups, gidx, gmax, g) {
|
||||
const char *prefix = ha_obj_str(g, "qn_prefix");
|
||||
@@ -1359,6 +1358,62 @@ static bool ha_parse_bash_search_pattern(const char *cmd, char *out, size_t out_
|
||||
return w > 0 && (size_t)w < out_sz;
|
||||
}
|
||||
|
||||
/* One tool call as its dialect reports it. Copilot's postToolUse payload may
|
||||
* name the runtime tool (bash, rg, ...) instead of the Claude tool, may use the
|
||||
* camelCase field names, and may pass the arguments as a JSON string, so the
|
||||
* reader maps the tool name and parses string arguments into input_doc, which
|
||||
* the caller frees with ha_tool_call_free. Copilot names no event in its
|
||||
* camelCase payload; its event is the one the managed manifest forces. */
|
||||
typedef struct {
|
||||
const char *event;
|
||||
const char *tool;
|
||||
yyjson_val *input;
|
||||
yyjson_doc *input_doc;
|
||||
} ha_tool_call_t;
|
||||
|
||||
static const char *ha_copilot_claude_tool_name(const char *tool) {
|
||||
if (!tool) {
|
||||
return NULL;
|
||||
}
|
||||
if (strcmp(tool, "bash") == 0 || strcmp(tool, "powershell") == 0) {
|
||||
return "Bash";
|
||||
}
|
||||
if (strcmp(tool, "grep") == 0 || strcmp(tool, "rg") == 0) {
|
||||
return "Grep";
|
||||
}
|
||||
if (strcmp(tool, "glob") == 0) {
|
||||
return "Glob";
|
||||
}
|
||||
return tool;
|
||||
}
|
||||
|
||||
static void ha_tool_call_read(yyjson_val *root, const char *forced_event,
|
||||
ha_lifecycle_dialect_t dialect, ha_tool_call_t *call) {
|
||||
call->event = ha_hook_event_name(root);
|
||||
call->tool = ha_obj_str(root, "tool_name");
|
||||
call->input = root ? yyjson_obj_get(root, "tool_input") : NULL;
|
||||
call->input_doc = NULL;
|
||||
if (dialect != HA_DIALECT_COPILOT) {
|
||||
return;
|
||||
}
|
||||
call->event = forced_event;
|
||||
call->tool =
|
||||
ha_copilot_claude_tool_name(call->tool ? call->tool : ha_obj_str(root, "toolName"));
|
||||
if (!call->input && root) {
|
||||
call->input = yyjson_obj_get(root, "toolArgs");
|
||||
}
|
||||
if (call->input && yyjson_is_str(call->input)) {
|
||||
call->input_doc = yyjson_read(yyjson_get_str(call->input), yyjson_get_len(call->input), 0);
|
||||
call->input = call->input_doc ? yyjson_doc_get_root(call->input_doc) : NULL;
|
||||
}
|
||||
}
|
||||
|
||||
static void ha_tool_call_free(ha_tool_call_t *call) {
|
||||
yyjson_doc_free(call->input_doc);
|
||||
call->input_doc = NULL;
|
||||
call->input = NULL;
|
||||
}
|
||||
|
||||
static bool ha_tool_event_supported(ha_lifecycle_dialect_t dialect, const char *event,
|
||||
const char *tool, bool *coverage) {
|
||||
if (coverage) {
|
||||
@@ -1367,6 +1422,13 @@ static bool ha_tool_event_supported(ha_lifecycle_dialect_t dialect, const char *
|
||||
if (!event || !tool) {
|
||||
return false;
|
||||
}
|
||||
/* Copilot's preToolUse can only decide permissions, so its searches are
|
||||
* augmented after they run, through postToolUse additionalContext. */
|
||||
if (dialect == HA_DIALECT_COPILOT) {
|
||||
return strcmp(event, "PostToolUse") == 0 &&
|
||||
(strcmp(tool, "Grep") == 0 || strcmp(tool, "Glob") == 0 ||
|
||||
strcmp(tool, "Bash") == 0);
|
||||
}
|
||||
if (dialect == HA_DIALECT_EVENT) {
|
||||
if (strcmp(event, "PreToolUse") == 0 &&
|
||||
(strcmp(tool, "Grep") == 0 || strcmp(tool, "Glob") == 0 || strcmp(tool, "Bash") == 0)) {
|
||||
@@ -1478,6 +1540,9 @@ static bool ha_invocation_supported(ha_lifecycle_dialect_t dialect, const char *
|
||||
if (dialect == HA_DIALECT_COPILOT && !forced_event) {
|
||||
return false;
|
||||
}
|
||||
if (dialect == HA_DIALECT_COPILOT && strcmp(forced_event, "PostToolUse") == 0) {
|
||||
return true;
|
||||
}
|
||||
return !forced_event || ha_dialect_event_supported(dialect, forced_event);
|
||||
}
|
||||
|
||||
@@ -1683,15 +1748,18 @@ static char *ha_process(cbm_mcp_server_t *srv, const char *input_json, const cha
|
||||
yyjson_doc_free(doc);
|
||||
return lifecycle;
|
||||
}
|
||||
const char *event = ha_hook_event_name(root);
|
||||
const char *tool = ha_obj_str(root, "tool_name");
|
||||
ha_tool_call_t call;
|
||||
ha_tool_call_read(root, forced_event, dialect, &call);
|
||||
const char *event = call.event;
|
||||
const char *tool = call.tool;
|
||||
bool coverage = false;
|
||||
if (!ha_tool_event_supported(dialect, event, tool, &coverage)) {
|
||||
ha_tool_call_free(&call);
|
||||
yyjson_doc_free(doc);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
yyjson_val *tin = yyjson_obj_get(root, "tool_input");
|
||||
yyjson_val *tin = call.input;
|
||||
|
||||
/* Post-read coverage adapters warn only when the exact path is not fully
|
||||
* represented. They never block or rewrite the completed tool call. */
|
||||
@@ -1702,10 +1770,12 @@ static char *ha_process(cbm_mcp_server_t *srv, const char *input_json, const cha
|
||||
if (note) {
|
||||
char *output = ha_build_event_json(event, note);
|
||||
free(note);
|
||||
ha_tool_call_free(&call);
|
||||
yyjson_doc_free(doc);
|
||||
return output;
|
||||
}
|
||||
}
|
||||
ha_tool_call_free(&call);
|
||||
yyjson_doc_free(doc);
|
||||
return NULL;
|
||||
}
|
||||
@@ -1715,6 +1785,7 @@ static char *ha_process(cbm_mcp_server_t *srv, const char *input_json, const cha
|
||||
if (strcmp(tool, "Bash") == 0) {
|
||||
const char *cmd = ha_obj_str(tin, "command");
|
||||
if (!ha_parse_bash_search_pattern(cmd, bash_pattern, sizeof(bash_pattern))) {
|
||||
ha_tool_call_free(&call);
|
||||
yyjson_doc_free(doc);
|
||||
return NULL;
|
||||
}
|
||||
@@ -1723,7 +1794,9 @@ static char *ha_process(cbm_mcp_server_t *srv, const char *input_json, const cha
|
||||
pattern = ha_obj_str(tin, "pattern");
|
||||
}
|
||||
char token[HA_MAX_TOKEN + 1];
|
||||
if (!ha_extract_token(pattern, token, sizeof(token))) {
|
||||
bool have_token = ha_extract_token(pattern, token, sizeof(token));
|
||||
ha_tool_call_free(&call);
|
||||
if (!have_token) {
|
||||
yyjson_doc_free(doc);
|
||||
return NULL;
|
||||
}
|
||||
@@ -1736,7 +1809,11 @@ static char *ha_process(cbm_mcp_server_t *srv, const char *input_json, const cha
|
||||
}
|
||||
|
||||
char *ctx = ha_resolve_and_query(srv, cwd, token);
|
||||
char *output = ctx ? ha_build_event_json(event, ctx) : NULL;
|
||||
char *output = NULL;
|
||||
if (ctx) {
|
||||
output = dialect == HA_DIALECT_COPILOT ? ha_build_copilot_json(ctx)
|
||||
: ha_build_event_json(event, ctx);
|
||||
}
|
||||
free(ctx);
|
||||
yyjson_doc_free(doc);
|
||||
return output;
|
||||
@@ -1765,18 +1842,27 @@ char *cbm_hook_augment_process_for(cbm_mcp_server_t *srv, const char *input_json
|
||||
return ha_process(srv, input_json, forced_event, dialect);
|
||||
}
|
||||
|
||||
/* TRUE iff `input` is an un-forced PreToolUse Bash event whose command can
|
||||
* never produce augmentation output: not a recognised search, or no queryable
|
||||
* token. Pure string work — safe to run BEFORE executable-identity hashing,
|
||||
* cohort admission, or any daemon contact, which is the point: agents issue
|
||||
* far more Bash calls than Grep/Glob and nearly all are not searches, while
|
||||
* the identity hash alone costs ~1.1 s of user CPU per invocation on a
|
||||
* production binary (measured 2026-08-28). Uses the exact parser pair
|
||||
* ha_process uses, so the gate and the augmenter cannot disagree about what
|
||||
* counts as a search. Anything uncertain returns false and pays full fare:
|
||||
* lifecycle events, other tools, coverage adapters, oversized stdin. */
|
||||
bool cbm_hook_augment_input_is_noop_bash(const char *input) {
|
||||
if (!input || strlen(input) > HA_STDIN_CAP) {
|
||||
/* TRUE iff `input` is a search event that can never produce augmentation
|
||||
* output: a Bash command that is not a recognised search or has no queryable
|
||||
* token. That event is an un-forced PreToolUse Bash call, or Copilot's forced
|
||||
* PostToolUse. For Copilot it is also any tool the augmenter does not read:
|
||||
* VS Code's local hook parser ignores matchers, so the managed PostToolUse
|
||||
* entry can fire after every tool. Pure string work — safe to run BEFORE
|
||||
* executable-identity hashing, cohort admission, or any daemon contact, which
|
||||
* is the point: agents issue far more Bash calls than Grep/Glob and nearly all
|
||||
* are not searches, while the identity hash alone costs ~1.1 s of user CPU per
|
||||
* invocation on a production binary (measured 2026-08-28). Uses the exact
|
||||
* reader and parser pair ha_process uses, so the gate and the augmenter cannot
|
||||
* disagree about what counts as a search. Anything uncertain returns false and
|
||||
* pays full fare: lifecycle events, other tools, coverage adapters, oversized
|
||||
* stdin. */
|
||||
static bool ha_input_is_noop(const char *input, const char *forced_event,
|
||||
ha_lifecycle_dialect_t dialect) {
|
||||
bool copilot = dialect == HA_DIALECT_COPILOT;
|
||||
const char *search_event = copilot ? "PostToolUse" : "PreToolUse";
|
||||
if (!input || strlen(input) > HA_STDIN_CAP ||
|
||||
(copilot ? !forced_event || strcmp(forced_event, search_event) != 0
|
||||
: forced_event != NULL)) {
|
||||
return false;
|
||||
}
|
||||
yyjson_doc *doc = yyjson_read(input, strlen(input), 0);
|
||||
@@ -1784,21 +1870,32 @@ bool cbm_hook_augment_input_is_noop_bash(const char *input) {
|
||||
return false;
|
||||
}
|
||||
bool noop = false;
|
||||
yyjson_val *root = yyjson_doc_get_root(doc);
|
||||
const char *event = ha_hook_event_name(root);
|
||||
const char *tool = ha_obj_str(root, "tool_name");
|
||||
if (event && tool && strcmp(event, "PreToolUse") == 0 && strcmp(tool, "Bash") == 0) {
|
||||
yyjson_val *tin = yyjson_obj_get(root, "tool_input");
|
||||
const char *cmd = ha_obj_str(tin, "command");
|
||||
ha_tool_call_t call;
|
||||
ha_tool_call_read(yyjson_doc_get_root(doc), forced_event, dialect, &call);
|
||||
if (copilot && !ha_tool_event_supported(dialect, call.event, call.tool, NULL)) {
|
||||
noop = true;
|
||||
} else if (call.event && call.tool && strcmp(call.event, search_event) == 0 &&
|
||||
strcmp(call.tool, "Bash") == 0) {
|
||||
const char *cmd = ha_obj_str(call.input, "command");
|
||||
char pat[HA_BASH_TOK_SZ];
|
||||
char tok[HA_MAX_TOKEN + 1];
|
||||
noop = !ha_parse_bash_search_pattern(cmd, pat, sizeof(pat)) ||
|
||||
!ha_extract_token(pat, tok, sizeof(tok));
|
||||
}
|
||||
ha_tool_call_free(&call);
|
||||
yyjson_doc_free(doc);
|
||||
return noop;
|
||||
}
|
||||
|
||||
bool cbm_hook_augment_input_is_noop_for(const char *input, const char *forced_event,
|
||||
const char *dialect_name) {
|
||||
ha_lifecycle_dialect_t dialect = HA_DIALECT_EVENT;
|
||||
if (dialect_name && dialect_name[0] && !ha_dialect_from_name(dialect_name, &dialect)) {
|
||||
return false;
|
||||
}
|
||||
return ha_input_is_noop(input, forced_event, dialect);
|
||||
}
|
||||
|
||||
int cbm_cmd_hook_augment(int argc, char **argv) {
|
||||
cbm_hook_augment_arm_deadline();
|
||||
|
||||
@@ -1824,7 +1921,7 @@ int cbm_cmd_hook_augment(int argc, char **argv) {
|
||||
if (!input) {
|
||||
return 0;
|
||||
}
|
||||
if (!forced_event && cbm_hook_augment_input_is_noop_bash(input)) {
|
||||
if (ha_input_is_noop(input, forced_event, dialect)) {
|
||||
free(input);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -2581,10 +2581,12 @@ static void application_background_initialize_impl(cbm_daemon_application_sessio
|
||||
cbm_mcp_auto_index_within_file_limit(root_path, auto_index_limit, &tracked_files);
|
||||
if (auto_index_candidate && !within_auto_index_limit) {
|
||||
char files[32];
|
||||
char limit[32];
|
||||
(void)snprintf(files, sizeof(files), "%d", tracked_files);
|
||||
(void)snprintf(limit, sizeof(limit), "%d", auto_index_limit);
|
||||
cbm_log_warn("daemon.autoindex.skipped", "project", project, "reason",
|
||||
tracked_files >= 0 ? "too_many_files" : "unsafe_or_unavailable_path", "files",
|
||||
files);
|
||||
files, "limit", limit);
|
||||
}
|
||||
bool args_required = auto_index_candidate && within_auto_index_limit;
|
||||
char *args = args_required ? application_auto_index_args(application, root_path) : NULL;
|
||||
|
||||
+54
-2
@@ -346,6 +346,56 @@ static bool resolve_global_excludes_path(char *out, size_t out_sz) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* JVM source-set language roots: src/<set>/<lang>/ in Maven and Gradle layouts. */
|
||||
static const char *JVM_SOURCE_LANG_DIRS[] = {"java", "kotlin", "scala", "groovy", NULL};
|
||||
|
||||
static bool component_equals(const char *start, size_t len, const char *word) {
|
||||
return strlen(word) == len && strncmp(start, word, len) == 0;
|
||||
}
|
||||
|
||||
/* True when rel_path names a directory strictly below a JVM source root
|
||||
* ("src/main/java/com/acme/target", "app/src/test/kotlin/x/tmp"). There every
|
||||
* directory is a package, so a built-in skip name such as "target", "tmp" or
|
||||
* "vendor" is an ordinary package name, not build output (#2661). The root
|
||||
* itself and directories beside it ("src/main/target") are not packages. */
|
||||
bool cbm_discover_path_in_jvm_package(const char *rel_path) {
|
||||
if (!rel_path) {
|
||||
return false;
|
||||
}
|
||||
enum { JVM_MAX_COMPONENTS = 256 };
|
||||
const char *starts[JVM_MAX_COMPONENTS];
|
||||
size_t lens[JVM_MAX_COMPONENTS];
|
||||
size_t count = 0;
|
||||
const char *p = rel_path;
|
||||
while (*p && count < JVM_MAX_COMPONENTS) {
|
||||
while (*p == '/' || *p == '\\') {
|
||||
p++;
|
||||
}
|
||||
if (!*p) {
|
||||
break;
|
||||
}
|
||||
const char *start = p;
|
||||
while (*p && *p != '/' && *p != '\\') {
|
||||
p++;
|
||||
}
|
||||
starts[count] = start;
|
||||
lens[count] = (size_t)(p - start);
|
||||
count++;
|
||||
}
|
||||
/* The language root sits at i + 2; the directory must be below it. */
|
||||
for (size_t i = 0; i + 3 < count; i++) {
|
||||
if (!component_equals(starts[i], lens[i], "src")) {
|
||||
continue;
|
||||
}
|
||||
for (int l = 0; JVM_SOURCE_LANG_DIRS[l]; l++) {
|
||||
if (component_equals(starts[i + 2], lens[i + 2], JVM_SOURCE_LANG_DIRS[l])) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* True when rel_path equals a suffix rule or ends with "/" + the rule. */
|
||||
bool cbm_discover_path_has_skip_suffix(const char *rel_path) {
|
||||
if (!rel_path) {
|
||||
@@ -803,8 +853,10 @@ static dir_verdict_t directory_verdict(const char *entry_name, const char *rel_p
|
||||
const gitignore_link_t *ignore_chain,
|
||||
const cbm_gitignore_t *global_gi,
|
||||
const cbm_gitignore_t *cbmignore) {
|
||||
if (cbm_should_skip_dir(entry_name, opts ? opts->mode : CBM_MODE_FULL) ||
|
||||
cbm_discover_path_has_skip_suffix(rel_path)) {
|
||||
bool builtin_skip = cbm_should_skip_dir(entry_name, opts ? opts->mode : CBM_MODE_FULL) &&
|
||||
(cbm_discover_is_safety_core_directory(entry_name) ||
|
||||
!cbm_discover_path_in_jvm_package(rel_path));
|
||||
if (builtin_skip || cbm_discover_path_has_skip_suffix(rel_path)) {
|
||||
/* #500: a .cbmignore negation (e.g. "!obj/") whose rule is the last
|
||||
* match for this dir un-skips a built-in skip-list dir — except the
|
||||
* non-negatable safety core. Fall through so .gitignore/global/local
|
||||
|
||||
@@ -5,5 +5,6 @@
|
||||
/* Pure legacy policy predicates; callers provide live terminated strings. */
|
||||
bool cbm_discover_path_has_skip_suffix(const char *relative_path);
|
||||
bool cbm_discover_is_safety_core_directory(const char *name);
|
||||
bool cbm_discover_path_in_jvm_package(const char *relative_path);
|
||||
|
||||
#endif
|
||||
@@ -102,7 +102,9 @@ static bool cif_directory_reason(cif_context *c, size_t parent, cbm_inventory_pa
|
||||
if (!cif_poll(c)) {
|
||||
return false;
|
||||
}
|
||||
bool builtin = cbm_should_skip_dir(name, CBM_MODE_FULL);
|
||||
bool builtin = cbm_should_skip_dir(name, CBM_MODE_FULL) &&
|
||||
(cbm_discover_is_safety_core_directory(name) ||
|
||||
!cbm_discover_path_in_jvm_package((const char *)path.data));
|
||||
bool suffix = !builtin && cbm_discover_path_has_skip_suffix((const char *)path.data);
|
||||
bool safety =
|
||||
(builtin || suffix) && c->owner->cbm_program && cbm_discover_is_safety_core_directory(name);
|
||||
|
||||
+17
-5
@@ -363,17 +363,29 @@ static void copy_path_without_query(const char *path, char *out, size_t outsz) {
|
||||
}
|
||||
|
||||
void cbm_log_mcp_request(const char *method, const char *tool_name, bool is_error,
|
||||
int64_t duration_us) {
|
||||
int64_t duration_us, int64_t handler_duration_us,
|
||||
int64_t response_serialization_us, bool cancelled, bool timed_out) {
|
||||
char duration_ms[CBM_SZ_32];
|
||||
char handler_duration_ms[CBM_SZ_32];
|
||||
char response_serialization_ms[CBM_SZ_32];
|
||||
snprintf(duration_ms, sizeof(duration_ms), "%" PRId64, duration_us / 1000);
|
||||
snprintf(handler_duration_ms, sizeof(handler_duration_ms), "%" PRId64,
|
||||
handler_duration_us / 1000);
|
||||
snprintf(response_serialization_ms, sizeof(response_serialization_ms), "%" PRId64,
|
||||
response_serialization_us / 1000);
|
||||
const char *status = timed_out ? "timeout" : (is_error ? "error" : "ok");
|
||||
if (tool_name && tool_name[0] != '\0') {
|
||||
cbm_log(is_error ? CBM_LOG_WARN : CBM_LOG_INFO, "mcp.request", "protocol", "jsonrpc",
|
||||
"method", method ? method : "", "tool", tool_name, "status",
|
||||
is_error ? "error" : "ok", "duration_ms", duration_ms, NULL);
|
||||
"method", method ? method : "", "tool", tool_name, "status", status, "duration_ms",
|
||||
duration_ms, "handler_duration_ms", handler_duration_ms,
|
||||
"response_serialization_ms", response_serialization_ms, "cancelled",
|
||||
cancelled ? "true" : "false", "timed_out", timed_out ? "true" : "false", NULL);
|
||||
} else {
|
||||
cbm_log(is_error ? CBM_LOG_WARN : CBM_LOG_INFO, "mcp.request", "protocol", "jsonrpc",
|
||||
"method", method ? method : "", "status", is_error ? "error" : "ok", "duration_ms",
|
||||
duration_ms, NULL);
|
||||
"method", method ? method : "", "status", status, "duration_ms", duration_ms,
|
||||
"handler_duration_ms", handler_duration_ms, "response_serialization_ms",
|
||||
response_serialization_ms, "cancelled", cancelled ? "true" : "false", "timed_out",
|
||||
timed_out ? "true" : "false", NULL);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,8 @@ void cbm_log_int(CBMLogLevel level, const char *msg, const char *key, int64_t va
|
||||
/* Operational event helpers. They deliberately avoid request bodies, headers,
|
||||
* arguments, and query strings. */
|
||||
void cbm_log_mcp_request(const char *method, const char *tool_name, bool is_error,
|
||||
int64_t duration_us);
|
||||
int64_t duration_us, int64_t handler_duration_us,
|
||||
int64_t response_serialization_us, bool cancelled, bool timed_out);
|
||||
void cbm_log_http_request(const char *component, const char *method, const char *path, int status,
|
||||
int64_t duration_ms, size_t request_bytes, size_t response_bytes);
|
||||
|
||||
|
||||
+5
-4
@@ -2880,18 +2880,19 @@ int main(int argc, char **argv) {
|
||||
cbm_hook_augment_arm_deadline();
|
||||
#endif
|
||||
/* Read stdin now and bail before executable-identity hashing when the
|
||||
* event can never produce output (an un-forced PreToolUse Bash call
|
||||
* that is not a search). The identity hash costs ~1.1 s of user CPU
|
||||
* event can never produce output (an un-forced PreToolUse Bash call,
|
||||
* or Copilot's forced PostToolUse after a shell call, that is not a
|
||||
* search). The identity hash costs ~1.1 s of user CPU
|
||||
* per invocation, and with Bash in the installed matcher nearly every
|
||||
* agent command would pay it for nothing. A no-op touches no shared
|
||||
* state, so it needs no identity. Anything else is handed back via
|
||||
* the prefetch so downstream readers see stdin exactly once. */
|
||||
if (!hook_event) {
|
||||
if (!hook_event || strcmp(hook_event, "PostToolUse") == 0) {
|
||||
char *hook_input = cbm_hook_augment_read_stdin();
|
||||
if (!hook_input) {
|
||||
return EXIT_SUCCESS; /* fail open, nothing to augment */
|
||||
}
|
||||
if (cbm_hook_augment_input_is_noop_bash(hook_input)) {
|
||||
if (cbm_hook_augment_input_is_noop_for(hook_input, hook_event, hook_dialect)) {
|
||||
free(hook_input);
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
+50
-25
@@ -20936,6 +20936,11 @@ bool cbm_mcp_jsonrpc_response_prepend_notice(char **response_io, const char *not
|
||||
return true;
|
||||
}
|
||||
|
||||
static long long mcp_elapsed_us(struct timespec start, struct timespec end) {
|
||||
return ((long long)(end.tv_sec - start.tv_sec) * MCP_S_TO_US) +
|
||||
((long long)(end.tv_nsec - start.tv_nsec) / MCP_MS_TO_US);
|
||||
}
|
||||
|
||||
char *cbm_mcp_server_handle(cbm_mcp_server_t *srv, const char *line) {
|
||||
cbm_jsonrpc_request_t req = {0};
|
||||
if (cbm_jsonrpc_parse(line, &req) < 0) {
|
||||
@@ -20966,7 +20971,11 @@ char *cbm_mcp_server_handle(cbm_mcp_server_t *srv, const char *line) {
|
||||
cbm_clock_gettime(CLOCK_MONOTONIC, &req_t0);
|
||||
char *result_json = NULL;
|
||||
char *request_error_json = NULL;
|
||||
bool request_logged = false;
|
||||
char *request_tool_name = NULL;
|
||||
long long handler_duration_us = 0;
|
||||
bool request_is_error = false;
|
||||
bool request_cancelled = false;
|
||||
bool request_timed_out = false;
|
||||
|
||||
if (strcmp(req.method, "initialize") == 0) {
|
||||
result_json = cbm_mcp_initialize_response_for_profile(req.params_raw, srv->tool_profile);
|
||||
@@ -20993,6 +21002,7 @@ char *cbm_mcp_server_handle(cbm_mcp_server_t *srv, const char *line) {
|
||||
char *tool_name = req.params_raw ? cbm_mcp_get_tool_name(req.params_raw) : NULL;
|
||||
char *tool_args =
|
||||
req.params_raw ? cbm_mcp_get_arguments(req.params_raw) : heap_strdup("{}");
|
||||
request_tool_name = tool_name;
|
||||
srv->active_request_id = req.id;
|
||||
free(srv->active_request_id_str);
|
||||
srv->active_request_id_str = req.id_str ? heap_strdup(req.id_str) : NULL;
|
||||
@@ -21005,16 +21015,15 @@ char *cbm_mcp_server_handle(cbm_mcp_server_t *srv, const char *line) {
|
||||
srv->active_request_id_str = NULL;
|
||||
struct timespec t1;
|
||||
cbm_clock_gettime(CLOCK_MONOTONIC, &t1);
|
||||
long long dur_us = ((long long)(t1.tv_sec - t0.tv_sec) * MCP_S_TO_US) +
|
||||
((long long)(t1.tv_nsec - t0.tv_nsec) / MCP_MS_TO_US);
|
||||
long long dur_us = mcp_elapsed_us(t0, t1);
|
||||
bool is_err = (result_json != NULL) && (strstr(result_json, "\"isError\":true") != NULL);
|
||||
cbm_diag_record_query(dur_us, is_err);
|
||||
long long request_dur_us = ((long long)(t1.tv_sec - req_t0.tv_sec) * MCP_S_TO_US) +
|
||||
((long long)(t1.tv_nsec - req_t0.tv_nsec) / MCP_MS_TO_US);
|
||||
cbm_log_mcp_request(req.method, tool_name, is_err, request_dur_us);
|
||||
request_logged = true;
|
||||
handler_duration_us = mcp_elapsed_us(req_t0, t1);
|
||||
request_is_error = is_err;
|
||||
request_cancelled = mcp_request_cancelled(srv);
|
||||
request_timed_out =
|
||||
result_json && strstr(result_json, "\"code\":\"request_timeout\"") != NULL;
|
||||
|
||||
free(tool_name);
|
||||
free(tool_args);
|
||||
} else {
|
||||
/* Echo the original id (string or numeric, issue #253) on the error. */
|
||||
@@ -21026,14 +21035,20 @@ char *cbm_mcp_server_handle(cbm_mcp_server_t *srv, const char *line) {
|
||||
.id_str = req.id_str,
|
||||
.error_json = err_obj,
|
||||
};
|
||||
char *err = cbm_jsonrpc_format_response(&err_resp);
|
||||
struct timespec t1;
|
||||
cbm_clock_gettime(CLOCK_MONOTONIC, &t1);
|
||||
long long dur_us = ((long long)(t1.tv_sec - req_t0.tv_sec) * MCP_S_TO_US) +
|
||||
((long long)(t1.tv_nsec - req_t0.tv_nsec) / MCP_MS_TO_US);
|
||||
long long handler_us = mcp_elapsed_us(req_t0, t1);
|
||||
struct timespec serialize_t0;
|
||||
cbm_clock_gettime(CLOCK_MONOTONIC, &serialize_t0);
|
||||
char *err = cbm_jsonrpc_format_response(&err_resp);
|
||||
struct timespec serialize_t1;
|
||||
cbm_clock_gettime(CLOCK_MONOTONIC, &serialize_t1);
|
||||
long long serialization_us = mcp_elapsed_us(serialize_t0, serialize_t1);
|
||||
long long duration_us = mcp_elapsed_us(req_t0, serialize_t1);
|
||||
/* server/discover is the modern connect probe. -32601 stays so the
|
||||
* client falls back to initialize; that miss is not an error (#2433). */
|
||||
cbm_log_mcp_request(req.method, NULL, strcmp(req.method, "server/discover") != 0, dur_us);
|
||||
cbm_log_mcp_request(req.method, NULL, strcmp(req.method, "server/discover") != 0,
|
||||
duration_us, handler_us, serialization_us, false, false);
|
||||
cbm_mcp_server_request_scope_end(srv);
|
||||
cbm_jsonrpc_request_free(&req);
|
||||
return err;
|
||||
@@ -21045,32 +21060,42 @@ char *cbm_mcp_server_handle(cbm_mcp_server_t *srv, const char *line) {
|
||||
.id_str = req.id_str,
|
||||
.error_json = request_error_json,
|
||||
};
|
||||
struct timespec serialize_t0;
|
||||
cbm_clock_gettime(CLOCK_MONOTONIC, &serialize_t0);
|
||||
char *err = cbm_jsonrpc_format_response(&err_resp);
|
||||
struct timespec t1;
|
||||
cbm_clock_gettime(CLOCK_MONOTONIC, &t1);
|
||||
long long dur_us = ((long long)(t1.tv_sec - req_t0.tv_sec) * MCP_S_TO_US) +
|
||||
((long long)(t1.tv_nsec - req_t0.tv_nsec) / MCP_MS_TO_US);
|
||||
cbm_log_mcp_request(req.method, NULL, true, dur_us);
|
||||
struct timespec serialize_t1;
|
||||
cbm_clock_gettime(CLOCK_MONOTONIC, &serialize_t1);
|
||||
long long handler_us = mcp_elapsed_us(req_t0, serialize_t0);
|
||||
long long serialization_us = mcp_elapsed_us(serialize_t0, serialize_t1);
|
||||
long long duration_us = mcp_elapsed_us(req_t0, serialize_t1);
|
||||
cbm_log_mcp_request(req.method, NULL, true, duration_us, handler_us, serialization_us,
|
||||
false, false);
|
||||
free(request_error_json);
|
||||
cbm_mcp_server_request_scope_end(srv);
|
||||
cbm_jsonrpc_request_free(&req);
|
||||
return err;
|
||||
}
|
||||
|
||||
if (!request_logged) {
|
||||
struct timespec t1;
|
||||
cbm_clock_gettime(CLOCK_MONOTONIC, &t1);
|
||||
long long dur_us = ((long long)(t1.tv_sec - req_t0.tv_sec) * MCP_S_TO_US) +
|
||||
((long long)(t1.tv_nsec - req_t0.tv_nsec) / MCP_MS_TO_US);
|
||||
cbm_log_mcp_request(req.method, NULL, false, dur_us);
|
||||
}
|
||||
|
||||
cbm_jsonrpc_response_t resp = {
|
||||
.id = req.id,
|
||||
.id_str = req.id_str,
|
||||
.result_json = result_json,
|
||||
};
|
||||
struct timespec serialize_t0;
|
||||
cbm_clock_gettime(CLOCK_MONOTONIC, &serialize_t0);
|
||||
char *out = cbm_jsonrpc_format_response(&resp);
|
||||
struct timespec serialize_t1;
|
||||
cbm_clock_gettime(CLOCK_MONOTONIC, &serialize_t1);
|
||||
long long serialization_us = mcp_elapsed_us(serialize_t0, serialize_t1);
|
||||
long long duration_us = mcp_elapsed_us(req_t0, serialize_t1);
|
||||
request_is_error = request_is_error || out == NULL;
|
||||
if (!request_tool_name) {
|
||||
handler_duration_us = duration_us - serialization_us;
|
||||
}
|
||||
cbm_log_mcp_request(req.method, request_tool_name, request_is_error, duration_us,
|
||||
handler_duration_us, serialization_us, request_cancelled,
|
||||
request_timed_out);
|
||||
free(request_tool_name);
|
||||
free(result_json);
|
||||
cbm_mcp_server_request_scope_end(srv);
|
||||
cbm_jsonrpc_request_free(&req);
|
||||
|
||||
+46
-34
@@ -2037,6 +2037,49 @@ static const cbm_gbuf_node_t *resolve_header_include(const cbm_pipeline_ctx_t *c
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static bool explicit_relative_path(const char *p) {
|
||||
return strncmp(p, "./", PAIR_LEN) == 0 || strncmp(p, "../", PAIR_LEN + SKIP_ONE) == 0;
|
||||
}
|
||||
|
||||
enum { SIBLING_MAX_CANDS = 4 };
|
||||
|
||||
/* The paths resolve_sibling_file tries for module_path, relative to the
|
||||
* importer's directory `dir`, in priority order. Returns how many it wrote. */
|
||||
static int sibling_file_candidates(const char *dir, const char *module_path,
|
||||
char cands[SIBLING_MAX_CANDS][PKGMAP_PATH_BUF]) {
|
||||
int ncand = 0;
|
||||
const char *sep = dir[0] ? "/" : "";
|
||||
const char *base = module_path;
|
||||
/* Skip a leading "./". */
|
||||
if (strncmp(base, "./", PAIR_LEN) == 0) {
|
||||
base += PAIR_LEN;
|
||||
}
|
||||
const char *slash = strrchr(base, '/');
|
||||
/* 1. Direct sibling: dir/<module_path>. */
|
||||
snprintf(cands[ncand++], PKGMAP_PATH_BUF, "%s%s%s", dir, sep, base);
|
||||
/* 2. SCSS partial: dir/[subdir/]_<basename>.scss (underscore-prefixed). */
|
||||
{
|
||||
const char *bn = slash ? slash + SKIP_ONE : base;
|
||||
char *dpart = slash ? cbm_strndup(base, (size_t)(slash - base)) : strdup("");
|
||||
if (dpart && bn[0] != '_') {
|
||||
snprintf(cands[ncand++], PKGMAP_PATH_BUF, "%s%s%s%s_%s.scss", dir, sep, dpart,
|
||||
dpart[0] ? "/" : "", bn);
|
||||
}
|
||||
free(dpart);
|
||||
}
|
||||
/* 3. Meson subdir: dir/<module_path>/meson.build. */
|
||||
snprintf(cands[ncand++], PKGMAP_PATH_BUF, "%s%s%s/meson.build", dir, sep, base);
|
||||
/* 4. Basename sibling: dir/<basename(module_path)>. Covers include paths
|
||||
* that carry a non-relative prefix (Hyprlang `source = ~/.config/.../x.conf`,
|
||||
* absolute include paths) but reference a file sitting beside the importer.
|
||||
* An explicit `./` or `../` path names one location, which Strategy 1c
|
||||
* resolves exactly; its basename beside the importer is another file (#2797). */
|
||||
if (slash && slash[SKIP_ONE] && !explicit_relative_path(module_path)) {
|
||||
snprintf(cands[ncand++], PKGMAP_PATH_BUF, "%s%s%s", dir, sep, slash + SKIP_ONE);
|
||||
}
|
||||
return ncand;
|
||||
}
|
||||
|
||||
/* Resolve a sibling-file import: a bare path/name (no leading "./") that names
|
||||
* a file relative to the importer's directory. This covers build/markup
|
||||
* grammars whose import string is a sibling filename or directory rather than a
|
||||
@@ -2063,38 +2106,8 @@ static const cbm_gbuf_node_t *resolve_sibling_file(const cbm_pipeline_ctx_t *ctx
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/* Candidate relative paths, in priority order. */
|
||||
char cands[5][PKGMAP_PATH_BUF];
|
||||
int ncand = 0;
|
||||
const char *base = module_path;
|
||||
/* Skip a leading "./". */
|
||||
if (base[0] == '.' && base[1] == '/') {
|
||||
base += 2;
|
||||
}
|
||||
/* 1. Direct sibling: dir/<module_path>. */
|
||||
snprintf(cands[ncand++], PKGMAP_PATH_BUF, "%s%s%s", dir, dir[0] ? "/" : "", base);
|
||||
/* 2. SCSS partial: dir/[subdir/]_<basename>.scss (underscore-prefixed). */
|
||||
{
|
||||
const char *slash = strrchr(base, '/');
|
||||
const char *bn = slash ? slash + 1 : base;
|
||||
char *dpart = slash ? cbm_strndup(base, (size_t)(slash - base)) : strdup("");
|
||||
if (dpart && bn[0] != '_') {
|
||||
snprintf(cands[ncand++], PKGMAP_PATH_BUF, "%s%s%s%s_%s.scss", dir, dir[0] ? "/" : "",
|
||||
dpart[0] ? dpart : "", dpart[0] ? "/" : "", bn);
|
||||
}
|
||||
free(dpart);
|
||||
}
|
||||
/* 3. Meson subdir: dir/<module_path>/meson.build. */
|
||||
snprintf(cands[ncand++], PKGMAP_PATH_BUF, "%s%s%s/meson.build", dir, dir[0] ? "/" : "", base);
|
||||
/* 4. Basename sibling: dir/<basename(module_path)>. Covers include paths
|
||||
* that carry a non-relative prefix (Hyprlang `source = ~/.config/.../x.conf`,
|
||||
* absolute include paths) but reference a file sitting beside the importer. */
|
||||
{
|
||||
const char *slash = strrchr(base, '/');
|
||||
if (slash && slash[1]) {
|
||||
snprintf(cands[ncand++], PKGMAP_PATH_BUF, "%s%s%s", dir, dir[0] ? "/" : "", slash + 1);
|
||||
}
|
||||
}
|
||||
char cands[SIBLING_MAX_CANDS][PKGMAP_PATH_BUF];
|
||||
int ncand = sibling_file_candidates(dir, module_path, cands);
|
||||
|
||||
const cbm_gbuf_node_t *found = NULL;
|
||||
for (int i = 0; i < ncand; i++) {
|
||||
@@ -2123,8 +2136,7 @@ static const cbm_gbuf_node_t *resolve_sibling_file(const cbm_pipeline_ctx_t *ctx
|
||||
static const cbm_gbuf_node_t *resolve_relative_file(const cbm_pipeline_ctx_t *ctx,
|
||||
const char *source_rel, const char *module_path,
|
||||
bool php_only) {
|
||||
if (!source_rel || !module_path || module_path[0] != '.' ||
|
||||
!(module_path[1] == '/' || (module_path[1] == '.' && module_path[2] == '/'))) {
|
||||
if (!source_rel || !module_path || !explicit_relative_path(module_path)) {
|
||||
return NULL;
|
||||
}
|
||||
char dir[PKGMAP_PATH_BUF];
|
||||
|
||||
Reference in new issue
Block a user