Skip to content

Commit 37245a2

Browse files
committed
Merge origin/main into fix/mcp-frontend-recovery
Signed-off-by: Martin Vogel <[email protected]>
2 parents e417bbf + 9c19b5d commit 37245a2

18 files changed

Lines changed: 1281 additions & 12 deletions

File tree

‎.github/workflows/_test.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -318,7 +318,7 @@ jobs:
318318
with:
319319
node-version: "22"
320320

321-
- uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
321+
- uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
322322
with:
323323
python-version: "3.12"
324324

‎.github/workflows/release.yml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -535,7 +535,7 @@ jobs:
535535
536536
# ── PyPI ─────────────────────────────────────────────────
537537
- name: Setup Python
538-
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
538+
uses: actions/setup-python@6d4fd22b20465ccc0f7354476588e8df7c9ca0e0 # v7.1.0
539539
with:
540540
python-version: '3.12'
541541

‎Makefile.cbm‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -415,6 +415,7 @@ EXTRACTION_SRCS = \
415415
$(CBM_DIR)/diagram.c \
416416
$(CBM_DIR)/route_path.c \
417417
$(CBM_DIR)/openapi.c \
418+
$(CBM_DIR)/extract_nextjs.c \
418419
$(CBM_DIR)/sql_values.c \
419420
$(CBM_DIR)/helpers.c \
420421
$(CBM_DIR)/cpp_branch_views.c \

‎install.sh‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,8 +148,19 @@ detect_arch() {
148148
esac
149149
}
150150

151+
is_android() {
152+
[ "$(uname -o 2>/dev/null || true)" = "Android" ] \
153+
|| [ -n "${TERMUX_VERSION:-}" ] \
154+
|| [[ "${PREFIX:-}" == /data/data/com.termux/* ]]
155+
}
156+
151157
OS=$(detect_os)
152158
ARCH=$(detect_arch)
159+
if [ "$OS" = "linux" ] && is_android; then
160+
echo 'error: Android/Termux cannot run the Linux portable binary (static glibc; Android kills it with "Bad system call").' >&2
161+
echo "Building from source in Termux may work but is not a supported platform yet - see issue #1202." >&2
162+
exit 1
163+
fi
153164

154165
echo "codebase-memory-mcp installer"
155166
echo " os: $OS"

‎internal/cbm/cbm.c‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3907,6 +3907,9 @@ static CBMFileResult *extract_file_ex_body(const char *source, int source_len, C
39073907
// OpenAPI operations as Routes (self-gated: API description documents).
39083908
cbm_extract_openapi(&ctx);
39093909

3910+
// Next.js App Router handlers' routes (self-gated: app/**/route.ts and route.js).
3911+
cbm_extract_nextjs_routes(&ctx);
3912+
39103913
// K8s / Kustomize semantic pass (additional structured extraction for YAML-based infra files).
39113914
if (ctx.language == CBM_LANG_KUSTOMIZE || ctx.language == CBM_LANG_K8S) {
39123915
cbm_extract_k8s(&ctx);

‎internal/cbm/cbm.h‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1409,6 +1409,11 @@ void cbm_extract_diagram_file(CBMExtractCtx *ctx);
14091409
* or JSON files whose root says `openapi` or `swagger`. */
14101410
void cbm_extract_openapi(CBMExtractCtx *ctx);
14111411

1412+
/* Next.js App Router route handlers (extract_nextjs.c): the HTTP verb Functions
1413+
* a route file exports carry the route path its directories name. Self-gated:
1414+
* TS / JS route.ts and route.js under app/ or src/app/. */
1415+
void cbm_extract_nextjs_routes(CBMExtractCtx *ctx);
1416+
14121417
/* Canonicalize route-path parameter placeholders (":id", "{id}", "<id>",
14131418
* "${...}") to a single "{}" token so that client call sites, server handlers
14141419
* and OpenAPI operations rendezvous on the same Route QN regardless of

‎internal/cbm/extract_nextjs.c‎

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,272 @@
1+
/*
2+
* extract_nextjs.c -- Next.js App Router route handlers (#759): an HTTP verb a
3+
* route file (`app/.../route.ts` or `route.js`, the app directory at the root
4+
* or under src/) exports serves the path its directories name.
5+
*
6+
* The handler is the Function definition extraction already made for the
7+
* export: this fills its route_path / route_method, so its Route node and
8+
* HANDLES edge come from the route pass exactly as a decorator route's do, on
9+
* a full and on an incremental index alike. The path is spelled as SvelteKit's
10+
* (sveltekit_route_path, pass_route_nodes.c): `[id]` is `:id`, `[...slug]` is
11+
* `*slug` and a `(group)` directory adds no segment.
12+
*
13+
* A directory this cannot spell makes the file no route at all: an optional
14+
* catch-all (`[[...slug]]`), a private folder (`_lib`), a parallel slot
15+
* (`@modal`), an intercepting route (`(.)photo`), a malformed bracket, or a
16+
* path that does not fit route_path. A handler is a Function of this file:
17+
* `export [async] function GET`, `export const GET = [async] (...) => ...`,
18+
* or `export { GET }` of such a declaration. A renamed
19+
* (`export { handler as GET }`), re-exported, destructured or wrapped
20+
* (`export const GET = auth(handler)`) handler, or a `function` expression
21+
* bound to a const, has no Function of its own here and is not routed. Each
22+
* file is judged alone: a src/app/ route file is routed even where a root
23+
* app/ directory makes Next.js ignore src/app/.
24+
*/
25+
#include "cbm.h"
26+
#include "foundation/arena.h"
27+
#include "helpers.h"
28+
#include "tree_sitter/api.h"
29+
#include "foundation/constants.h"
30+
#include <stdbool.h>
31+
#include <stdint.h>
32+
#include <string.h>
33+
34+
enum {
35+
NX_PATH_MAX = CBM_SZ_256, /* the route pass rebuilds a Route from this much */
36+
NX_REST_LEN = 3, /* "..." */
37+
};
38+
39+
/* The path of a route file below its app directory, NULL for any other file. */
40+
static const char *nx_app_relative(const char *rel_path) {
41+
const char *base = rel_path ? strrchr(rel_path, '/') : NULL;
42+
if (!base || (strcmp(base, "/route.ts") != 0 && strcmp(base, "/route.js") != 0)) {
43+
return NULL;
44+
}
45+
if (strncmp(rel_path, "app/", strlen("app/")) == 0) {
46+
return rel_path + strlen("app/");
47+
}
48+
if (strncmp(rel_path, "src/app/", strlen("src/app/")) == 0) {
49+
return rel_path + strlen("src/app/");
50+
}
51+
return NULL;
52+
}
53+
54+
static bool nx_has_any(const char *s, size_t n, const char *chars) {
55+
for (size_t i = 0; i < n; i++) {
56+
if (strchr(chars, s[i])) {
57+
return true;
58+
}
59+
}
60+
return false;
61+
}
62+
63+
/* Append directory segment seg (n bytes) to the route path out[*len]. False
64+
* when the segment cannot be spelled or the path would not fit. */
65+
static bool nx_append_segment(char *out, size_t *len, const char *seg, size_t n) {
66+
if (n == 0 || seg[0] == '_' || seg[0] == '@') {
67+
return false;
68+
}
69+
if (seg[0] == '(') {
70+
/* A route group adds nothing; `(.)x`, `(..)x` and `(...)x` intercept. */
71+
return n > PAIR_LEN && seg[n - SKIP_ONE] == ')' && seg[SKIP_ONE] != '.' &&
72+
!nx_has_any(seg + SKIP_ONE, n - PAIR_LEN, "()");
73+
}
74+
char marker = '\0';
75+
if (seg[0] == '[') {
76+
if (seg[n - SKIP_ONE] != ']') {
77+
return false;
78+
}
79+
marker = ':';
80+
seg += SKIP_ONE;
81+
n -= PAIR_LEN;
82+
if (n >= NX_REST_LEN && strncmp(seg, "...", NX_REST_LEN) == 0) {
83+
marker = '*';
84+
seg += NX_REST_LEN;
85+
n -= NX_REST_LEN;
86+
}
87+
if (n == 0 || nx_has_any(seg, n, "[].")) {
88+
return false;
89+
}
90+
} else if (nx_has_any(seg, n, "[]")) {
91+
return false;
92+
}
93+
size_t need = SKIP_ONE + (marker ? SKIP_ONE : 0) + n;
94+
if (*len + need >= NX_PATH_MAX) {
95+
return false;
96+
}
97+
out[(*len)++] = '/';
98+
if (marker) {
99+
out[(*len)++] = marker;
100+
}
101+
memcpy(out + *len, seg, n);
102+
*len += n;
103+
out[*len] = '\0';
104+
return true;
105+
}
106+
107+
/* The route path of the file being extracted, in the arena; NULL for none. */
108+
static const char *nx_route_path(CBMExtractCtx *ctx) {
109+
const char *seg = nx_app_relative(ctx->rel_path);
110+
if (!seg) {
111+
return NULL;
112+
}
113+
const char *base = strrchr(ctx->rel_path, '/');
114+
char path[NX_PATH_MAX] = "/";
115+
size_t len = 0;
116+
while (seg < base) {
117+
const char *end = strchr(seg, '/');
118+
if (!nx_append_segment(path, &len, seg, (size_t)(end - seg))) {
119+
return NULL;
120+
}
121+
seg = end + SKIP_ONE;
122+
}
123+
return cbm_arena_strdup(ctx->arena, path);
124+
}
125+
126+
static bool nx_is_method(const char *name) {
127+
static const char *const methods[] = {"GET", "POST", "PUT", "PATCH",
128+
"DELETE", "HEAD", "OPTIONS"};
129+
for (size_t i = 0; i < sizeof(methods) / sizeof(methods[0]); i++) {
130+
if (strcmp(name, methods[i]) == 0) {
131+
return true;
132+
}
133+
}
134+
return false;
135+
}
136+
137+
static bool nx_is(TSNode n, const char *type) {
138+
return !ts_node_is_null(n) && strcmp(ts_node_type(n), type) == 0;
139+
}
140+
141+
/* Does a node carry the anonymous keyword child kw (`default`, `type`)? */
142+
static bool nx_has_keyword(TSNode n, const char *kw) {
143+
uint32_t count = ts_node_child_count(n);
144+
for (uint32_t i = 0; i < count; i++) {
145+
TSNode c = ts_node_child(n, i);
146+
if (!ts_node_is_named(c) && strcmp(ts_node_type(c), kw) == 0) {
147+
return true;
148+
}
149+
}
150+
return false;
151+
}
152+
153+
/* The name of a declarator whose value is an arrow function, the Function
154+
* extraction names after the binding (`GET = () => ...`); a null node for any
155+
* other declarator. */
156+
static TSNode nx_arrow_binding(TSNode declarator) {
157+
TSNode value = ts_node_child_by_field_name(declarator, TS_FIELD("value"));
158+
TSNode name = ts_node_child_by_field_name(declarator, TS_FIELD("name"));
159+
if (!nx_is(name, "identifier") || !nx_is(value, "arrow_function")) {
160+
return (TSNode){0};
161+
}
162+
return name;
163+
}
164+
165+
/* Route the Function definition named by name_node, declared inside decl. */
166+
static void nx_route_handler(CBMExtractCtx *ctx, TSNode decl, TSNode name_node, const char *path) {
167+
const char *name = cbm_node_text(ctx->arena, name_node, ctx->source);
168+
if (!name || !nx_is_method(name)) {
169+
return;
170+
}
171+
const char *qn =
172+
cbm_fqn_compute_source_lang(ctx->arena, ctx->project, ctx->rel_path, name, ctx->language);
173+
uint32_t first = ts_node_start_point(decl).row + TS_LINE_OFFSET;
174+
uint32_t last = ts_node_end_point(decl).row + TS_LINE_OFFSET;
175+
for (int i = 0; qn && i < ctx->result->defs.count; i++) {
176+
CBMDefinition *def = &ctx->result->defs.items[i];
177+
if (!def->route_path && def->label && def->qualified_name &&
178+
strcmp(def->label, "Function") == 0 && strcmp(def->qualified_name, qn) == 0 &&
179+
def->start_line >= first && def->end_line <= last) {
180+
def->route_path = path;
181+
def->route_method = name;
182+
return;
183+
}
184+
}
185+
}
186+
187+
/* Does the name node, if any, spell want? Any name does when want is NULL. */
188+
static bool nx_wanted(CBMExtractCtx *ctx, TSNode name, const char *want) {
189+
if (ts_node_is_null(name)) {
190+
return false;
191+
}
192+
const char *text = want ? cbm_node_text(ctx->arena, name, ctx->source) : NULL;
193+
return !want || (text && strcmp(text, want) == 0);
194+
}
195+
196+
/* Route the handlers a top-level declaration (function or const) defines.
197+
* Only names equal to want are considered when want is set. */
198+
static void nx_route_declaration(CBMExtractCtx *ctx, TSNode decl, const char *path,
199+
const char *want) {
200+
if (nx_is(decl, "function_declaration")) {
201+
TSNode name = ts_node_child_by_field_name(decl, TS_FIELD("name"));
202+
if (nx_wanted(ctx, name, want)) {
203+
nx_route_handler(ctx, decl, name, path);
204+
}
205+
return;
206+
}
207+
if (!nx_is(decl, "lexical_declaration") && !nx_is(decl, "variable_declaration")) {
208+
return;
209+
}
210+
uint32_t count = ts_node_named_child_count(decl);
211+
for (uint32_t i = 0; i < count; i++) {
212+
TSNode declarator = ts_node_named_child(decl, i);
213+
TSNode name =
214+
nx_is(declarator, "variable_declarator") ? nx_arrow_binding(declarator) : (TSNode){0};
215+
if (nx_wanted(ctx, name, want)) {
216+
nx_route_handler(ctx, declarator, name, path);
217+
}
218+
}
219+
}
220+
221+
/* `export { GET }`: route the file's own top-level declaration of each
222+
* unrenamed, non-type specifier. */
223+
static void nx_route_export_clause(CBMExtractCtx *ctx, TSNode clause, const char *path) {
224+
uint32_t count = ts_node_named_child_count(clause);
225+
uint32_t top = ts_node_named_child_count(ctx->root);
226+
for (uint32_t i = 0; i < count; i++) {
227+
TSNode spec = ts_node_named_child(clause, i);
228+
TSNode name = ts_node_child_by_field_name(spec, TS_FIELD("name"));
229+
if (!nx_is(spec, "export_specifier") || !nx_is(name, "identifier") ||
230+
!ts_node_is_null(ts_node_child_by_field_name(spec, TS_FIELD("alias"))) ||
231+
nx_has_keyword(spec, "type") || nx_has_keyword(spec, "typeof")) {
232+
continue;
233+
}
234+
const char *want = cbm_node_text(ctx->arena, name, ctx->source);
235+
for (uint32_t j = 0; want && j < top; j++) {
236+
nx_route_declaration(ctx, ts_node_named_child(ctx->root, j), path, want);
237+
}
238+
}
239+
}
240+
241+
void cbm_extract_nextjs_routes(CBMExtractCtx *ctx) {
242+
if (ctx->language != CBM_LANG_TYPESCRIPT && ctx->language != CBM_LANG_JAVASCRIPT) {
243+
return;
244+
}
245+
const char *path = nx_route_path(ctx);
246+
if (!path) {
247+
return;
248+
}
249+
uint32_t count = ts_node_named_child_count(ctx->root);
250+
for (uint32_t i = 0; i < count; i++) {
251+
TSNode node = ts_node_named_child(ctx->root, i);
252+
/* A runtime export of this file: not `export default`, not
253+
* `export type { ... }`, not a re-export `... from './x'`. */
254+
if (!nx_is(node, "export_statement") || nx_has_keyword(node, "default") ||
255+
nx_has_keyword(node, "type") ||
256+
!ts_node_is_null(ts_node_child_by_field_name(node, TS_FIELD("source")))) {
257+
continue;
258+
}
259+
TSNode decl = ts_node_child_by_field_name(node, TS_FIELD("declaration"));
260+
if (!ts_node_is_null(decl)) {
261+
nx_route_declaration(ctx, decl, path, NULL);
262+
continue;
263+
}
264+
uint32_t parts = ts_node_named_child_count(node);
265+
for (uint32_t j = 0; j < parts; j++) {
266+
TSNode part = ts_node_named_child(node, j);
267+
if (nx_is(part, "export_clause")) {
268+
nx_route_export_clause(ctx, part, path);
269+
}
270+
}
271+
}
272+
}

‎scripts/test.sh‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -364,6 +364,9 @@ bash "$ROOT/tests/test_language_count_contract.sh"
364364
echo "=== Step 0x: packaging version-metadata contract ==="
365365
bash "$ROOT/tests/test_version_metadata_contract.sh"
366366

367+
echo "=== Step 0x2: installer Android/Termux guard contract (#1202) ==="
368+
bash "$ROOT/tests/test_install_android_guard.sh"
369+
367370
# Step 0y: the Windows leg must not decide its verdict from an exit status that
368371
# the ssh/msys2_shell chain can mangle — a channel that turns 0 into 1 can turn
369372
# 1 into 0, and that direction reports a RED Windows leg as green. Runs

0 commit comments

Comments
 (0)