mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-10-30 07:23:47 +08:00
feat(cli): accept flags, stdin, and --args-file for tools + per-tool --help (#680)
The CLI only accepted a single inline JSON argument (cbm cli <tool> '<json>'),
which PowerShell mangles on Windows and which made `cli <tool> --help` return
"repo_path is required" -- the "--help" token was parsed as the JSON args, and
there was no per-tool help at all.
run_cli now resolves tool arguments by precedence: --args-file <path>, a raw
JSON object (back-compat), --flag / --flag=value / bare-boolean flags, piped
stdin, then empty {}. Flags are converted to a JSON object via the tool's
input_schema: kebab-case keys map to snake_case, values are typed
(string/integer/boolean), and repeated flags accumulate into array-typed
properties (--semantic-query a --semantic-query b -> ["a","b"]). stdin and
--args-file are UTF-8-clean and bypass shell quoting entirely (the robust path
on Windows). `cli <tool> --help` prints usage + the tool's flags, generated
from the same input_schema the MCP tools/list advertises -- no hand-maintained
table.
cbm_mcp_tool_input_schema() exposes a tool's schema; cbm_cli_build_args_json()
and cbm_cli_print_tool_help() do the conversion and help.
Reproduce-first: 8 cli tests (string / integer-not-string / bare-boolean /
repeated-array / kebab->snake / --key=value with spaces / bad-positional-errors
/ help) -- RED on a stub, GREEN after. Full suite 5730/0.
Note: true CJK-path support on Windows still needs a UTF-8/wmain entry point
(argv is ANSI-codepage-mangled upstream of parsing); stdin/--args-file mitigate
it and are tracked separately.
Signed-off-by: Martin Vogel <martin.vogel.tech@gmail.com>
This commit is contained in:
+252
@@ -8,6 +8,7 @@
|
||||
#include "foundation/compat.h"
|
||||
#include "foundation/platform.h"
|
||||
#include "foundation/constants.h"
|
||||
#include "mcp/mcp.h" // cbm_mcp_tool_input_schema — CLI flag parser + per-tool --help
|
||||
|
||||
/* CLI buffer size constants. */
|
||||
enum {
|
||||
@@ -4441,3 +4442,254 @@ int cbm_cmd_update(int argc, char **argv) {
|
||||
(void)variant;
|
||||
return 0;
|
||||
}
|
||||
|
||||
/* ── CLI tool arguments (flags / --args-file / --help) ────────────── */
|
||||
|
||||
/* Flag-name normalization: kebab-case CLI flags map to snake_case JSON keys
|
||||
* (--name-pattern -> name_pattern). In-place; buffer is NUL-terminated. */
|
||||
static void cli_kebab_to_snake(char *s) {
|
||||
for (; *s; s++) {
|
||||
if (*s == '-') {
|
||||
*s = '_';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* snake_case JSON key -> kebab-case flag name (for --help display). In-place. */
|
||||
static void cli_snake_to_kebab(char *s) {
|
||||
for (; *s; s++) {
|
||||
if (*s == '_') {
|
||||
*s = '-';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/* Heap-format a one-argument error message for *err_out. Caller frees. */
|
||||
static char *cli_heap_msgf(const char *fmt, const char *arg) {
|
||||
char buf[CLI_BUF_512];
|
||||
snprintf(buf, sizeof(buf), fmt, arg);
|
||||
return cbm_strdup(buf);
|
||||
}
|
||||
|
||||
/* True if the schema's required[] array contains `key`. */
|
||||
static bool cli_schema_required_has(yyjson_val *required, const char *key) {
|
||||
if (!required || !yyjson_is_arr(required)) {
|
||||
return false;
|
||||
}
|
||||
size_t idx;
|
||||
size_t max;
|
||||
yyjson_val *v;
|
||||
yyjson_arr_foreach(required, idx, max, v) {
|
||||
if (yyjson_is_str(v) && strcmp(yyjson_get_str(v), key) == 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Look up a property's JSON-schema "type" string (string/integer/number/
|
||||
* boolean/array). Returns NULL when the schema or property is unknown — the
|
||||
* caller then treats the value as a plain string. */
|
||||
static const char *cli_schema_type(yyjson_val *props, const char *key) {
|
||||
if (!props || !yyjson_is_obj(props)) {
|
||||
return NULL;
|
||||
}
|
||||
yyjson_val *p = yyjson_obj_get(props, key);
|
||||
if (!p || !yyjson_is_obj(p)) {
|
||||
return NULL;
|
||||
}
|
||||
yyjson_val *t = yyjson_obj_get(p, "type");
|
||||
return (t && yyjson_is_str(t)) ? yyjson_get_str(t) : NULL;
|
||||
}
|
||||
|
||||
/* Append a typed value to the output object under `key`. For array-typed
|
||||
* properties, repeated flags accumulate into a single JSON array. */
|
||||
static void cli_add_typed(yyjson_mut_doc *out, yyjson_mut_val *obj, const char *key,
|
||||
const char *type, const char *value, bool have_value) {
|
||||
if (type && strcmp(type, "array") == 0) {
|
||||
yyjson_mut_val *arr = yyjson_mut_obj_get(obj, key);
|
||||
if (!arr || !yyjson_mut_is_arr(arr)) {
|
||||
arr = yyjson_mut_arr(out);
|
||||
yyjson_mut_obj_add(obj, yyjson_mut_strcpy(out, key), arr);
|
||||
}
|
||||
yyjson_mut_arr_add_strcpy(out, arr, have_value ? value : "");
|
||||
return;
|
||||
}
|
||||
|
||||
yyjson_mut_val *vv;
|
||||
if (type && strcmp(type, "boolean") == 0) {
|
||||
bool b = !have_value || strcmp(value, "true") == 0 || strcmp(value, "1") == 0 ||
|
||||
strcmp(value, "yes") == 0;
|
||||
vv = yyjson_mut_bool(out, b);
|
||||
} else if (type && strcmp(type, "integer") == 0) {
|
||||
char *endp = NULL;
|
||||
const char *v = have_value ? value : "";
|
||||
long n = strtol(v, &endp, CLI_STRTOL_BASE);
|
||||
vv = (endp && endp != v && *endp == '\0') ? yyjson_mut_int(out, (int64_t)n)
|
||||
: yyjson_mut_strcpy(out, v);
|
||||
} else if (type && strcmp(type, "number") == 0) {
|
||||
char *endp = NULL;
|
||||
const char *v = have_value ? value : "";
|
||||
double d = strtod(v, &endp);
|
||||
vv = (endp && endp != v && *endp == '\0') ? yyjson_mut_real(out, d)
|
||||
: yyjson_mut_strcpy(out, v);
|
||||
} else {
|
||||
/* string or unknown type */
|
||||
vv = yyjson_mut_strcpy(out, have_value ? value : "");
|
||||
}
|
||||
yyjson_mut_obj_add(obj, yyjson_mut_strcpy(out, key), vv);
|
||||
}
|
||||
|
||||
char *cbm_cli_build_args_json(const char *tool_name, int argc, char **argv, char **err_out) {
|
||||
if (err_out) {
|
||||
*err_out = NULL;
|
||||
}
|
||||
|
||||
/* The tool's input_schema (may be NULL for an unknown tool — then every
|
||||
* value is treated as a string). Static lifetime; do not free. */
|
||||
const char *schema_str = cbm_mcp_tool_input_schema(tool_name);
|
||||
yyjson_doc *schema_doc = NULL;
|
||||
yyjson_val *props = NULL;
|
||||
if (schema_str) {
|
||||
schema_doc = yyjson_read(schema_str, strlen(schema_str), 0);
|
||||
if (schema_doc) {
|
||||
props = yyjson_obj_get(yyjson_doc_get_root(schema_doc), "properties");
|
||||
}
|
||||
}
|
||||
|
||||
yyjson_mut_doc *out = yyjson_mut_doc_new(NULL);
|
||||
if (!out) {
|
||||
if (schema_doc) {
|
||||
yyjson_doc_free(schema_doc);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
yyjson_mut_val *obj = yyjson_mut_obj(out);
|
||||
yyjson_mut_doc_set_root(out, obj);
|
||||
|
||||
bool ok = true;
|
||||
for (int i = 0; i < argc && ok; i++) {
|
||||
const char *arg = argv[i];
|
||||
if (strcmp(arg, "--") == 0) {
|
||||
break; /* end of flag parsing */
|
||||
}
|
||||
if (strncmp(arg, "--", CLI_PAIR_LEN) != 0) {
|
||||
if (err_out) {
|
||||
*err_out = cli_heap_msgf("unexpected argument '%s' (expected --flag value)", arg);
|
||||
}
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
|
||||
const char *body = arg + CLI_PAIR_LEN; /* skip leading "--" */
|
||||
const char *eq = strchr(body, '=');
|
||||
char key[CLI_BUF_256];
|
||||
const char *value = NULL;
|
||||
bool have_value = false;
|
||||
|
||||
if (eq) {
|
||||
/* --key=value : split on the FIRST '='; value may contain '='/spaces. */
|
||||
size_t klen = (size_t)(eq - body);
|
||||
if (klen >= sizeof(key)) {
|
||||
klen = sizeof(key) - CLI_SKIP_ONE;
|
||||
}
|
||||
memcpy(key, body, klen);
|
||||
key[klen] = '\0';
|
||||
value = eq + CLI_SKIP_ONE;
|
||||
have_value = true;
|
||||
} else {
|
||||
snprintf(key, sizeof(key), "%s", body);
|
||||
/* Consume the next token as the value unless it is itself a flag
|
||||
* (then this is a bare boolean/string flag). */
|
||||
if (i + CLI_SKIP_ONE < argc &&
|
||||
strncmp(argv[i + CLI_SKIP_ONE], "--", CLI_PAIR_LEN) != 0) {
|
||||
value = argv[i + CLI_SKIP_ONE];
|
||||
have_value = true;
|
||||
i++;
|
||||
}
|
||||
}
|
||||
|
||||
cli_kebab_to_snake(key);
|
||||
const char *type = cli_schema_type(props, key);
|
||||
|
||||
if (type && strcmp(type, "array") == 0 && !have_value) {
|
||||
if (err_out) {
|
||||
*err_out = cli_heap_msgf("flag --%s requires a value", key);
|
||||
}
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
|
||||
cli_add_typed(out, obj, key, type, value, have_value);
|
||||
}
|
||||
|
||||
char *result = NULL;
|
||||
if (ok) {
|
||||
size_t len = 0;
|
||||
result = yyjson_mut_write(out, 0, &len); /* malloc'd; caller frees */
|
||||
}
|
||||
|
||||
yyjson_mut_doc_free(out);
|
||||
if (schema_doc) {
|
||||
yyjson_doc_free(schema_doc);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
int cbm_cli_print_tool_help(const char *tool_name) {
|
||||
const char *schema_str = cbm_mcp_tool_input_schema(tool_name);
|
||||
if (!schema_str) {
|
||||
return CLI_ERR;
|
||||
}
|
||||
|
||||
yyjson_doc *doc = yyjson_read(schema_str, strlen(schema_str), 0);
|
||||
yyjson_val *root = doc ? yyjson_doc_get_root(doc) : NULL;
|
||||
yyjson_val *props = root ? yyjson_obj_get(root, "properties") : NULL;
|
||||
yyjson_val *required = root ? yyjson_obj_get(root, "required") : NULL;
|
||||
|
||||
printf("Usage:\n");
|
||||
printf(" codebase-memory-mcp cli %s --flag value [--flag2 value2 ...]\n", tool_name);
|
||||
printf(" codebase-memory-mcp cli %s --args-file <path-to-json>\n", tool_name);
|
||||
printf(" echo '<json>' | codebase-memory-mcp cli %s\n", tool_name);
|
||||
printf(" codebase-memory-mcp cli %s '<raw-json-args>'\n", tool_name);
|
||||
|
||||
printf("\nFlags:\n");
|
||||
if (props && yyjson_is_obj(props)) {
|
||||
yyjson_obj_iter iter;
|
||||
yyjson_obj_iter_init(props, &iter);
|
||||
yyjson_val *pkey;
|
||||
while ((pkey = yyjson_obj_iter_next(&iter)) != NULL) {
|
||||
yyjson_val *pval = yyjson_obj_iter_get_val(pkey);
|
||||
const char *name = yyjson_get_str(pkey);
|
||||
if (!name) {
|
||||
continue;
|
||||
}
|
||||
const char *type = "string";
|
||||
const char *desc = "";
|
||||
if (yyjson_is_obj(pval)) {
|
||||
yyjson_val *t = yyjson_obj_get(pval, "type");
|
||||
if (t && yyjson_is_str(t)) {
|
||||
type = yyjson_get_str(t);
|
||||
}
|
||||
yyjson_val *d = yyjson_obj_get(pval, "description");
|
||||
if (d && yyjson_is_str(d)) {
|
||||
desc = yyjson_get_str(d);
|
||||
}
|
||||
}
|
||||
char flag[CLI_BUF_256];
|
||||
snprintf(flag, sizeof(flag), "%s", name);
|
||||
cli_snake_to_kebab(flag);
|
||||
bool req = cli_schema_required_has(required, name);
|
||||
printf(" --%s <%s>%s", flag, type, req ? " [required]" : "");
|
||||
if (desc[0]) {
|
||||
printf(" %s", desc);
|
||||
}
|
||||
printf("\n");
|
||||
}
|
||||
}
|
||||
|
||||
if (doc) {
|
||||
yyjson_doc_free(doc);
|
||||
}
|
||||
return CLI_OK;
|
||||
}
|
||||
|
||||
@@ -19,6 +19,22 @@ void cbm_cli_set_version(const char *ver);
|
||||
/* Get the version string. */
|
||||
const char *cbm_cli_get_version(void);
|
||||
|
||||
/* ── CLI tool arguments (flags / --args-file / --help) ────────── */
|
||||
|
||||
/* Convert `--flag value` / `--flag=value` / bare-boolean `--flag` arguments for
|
||||
* a tool into a JSON arguments object string, using the tool's input_schema to
|
||||
* type values (string/integer/boolean) and to collect repeated flags into
|
||||
* array-typed properties. kebab-case flags map to snake_case keys
|
||||
* (--repo-path -> repo_path). A bare `--` ends flag parsing. On error returns
|
||||
* NULL and, if err_out is non-NULL, sets *err_out to a heap message the caller
|
||||
* must free. Caller frees the returned JSON string. */
|
||||
char *cbm_cli_build_args_json(const char *tool_name, int argc, char **argv, char **err_out);
|
||||
|
||||
/* Print per-tool help (usage + the tool's flags with type/description/required)
|
||||
* derived from its input_schema, to stdout. Returns 0 if the tool is known,
|
||||
* non-zero (and prints nothing) if it is not. */
|
||||
int cbm_cli_print_tool_help(const char *tool_name);
|
||||
|
||||
/* ── Self-update: version comparison ──────────────────────────── */
|
||||
|
||||
/* Compare two semver strings (e.g. "0.2.1" vs "0.2.0").
|
||||
|
||||
+124
-1
@@ -228,6 +228,65 @@ static bool cli_strip_flag(int *argc, char **argv, const char *flag) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/* Portable "is fd a terminal?" — _isatty on Windows, isatty on POSIX. */
|
||||
#ifdef _WIN32
|
||||
#define cli_isatty(fd) _isatty(fd)
|
||||
#else
|
||||
#define cli_isatty(fd) isatty(fd)
|
||||
#endif
|
||||
|
||||
enum { CLI_SLURP_CHUNK = 4096 };
|
||||
|
||||
/* Read an open stream fully into a heap, NUL-terminated string. Caller frees.
|
||||
* Returns NULL on allocation failure. Reads binary-clean (UTF-8 JSON, no shell
|
||||
* quoting needed). */
|
||||
static char *cli_slurp_stream(FILE *f) {
|
||||
size_t cap = CLI_SLURP_CHUNK;
|
||||
size_t len = 0;
|
||||
char *buf = malloc(cap);
|
||||
if (!buf) {
|
||||
return NULL;
|
||||
}
|
||||
char tmp[CLI_SLURP_CHUNK];
|
||||
size_t n;
|
||||
while ((n = fread(tmp, 1, sizeof(tmp), f)) > 0) {
|
||||
if (len + n + 1 > cap) {
|
||||
while (len + n + 1 > cap) {
|
||||
cap *= 2;
|
||||
}
|
||||
char *nb = realloc(buf, cap);
|
||||
if (!nb) {
|
||||
free(buf);
|
||||
return NULL;
|
||||
}
|
||||
buf = nb;
|
||||
}
|
||||
memcpy(buf + len, tmp, n);
|
||||
len += n;
|
||||
}
|
||||
buf[len] = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
/* Slurp a file path into a heap, NUL-terminated string. Caller frees. */
|
||||
static char *cli_slurp_file(const char *path) {
|
||||
FILE *f = fopen(path, "rb");
|
||||
if (!f) {
|
||||
return NULL;
|
||||
}
|
||||
char *s = cli_slurp_stream(f);
|
||||
(void)fclose(f);
|
||||
return s;
|
||||
}
|
||||
|
||||
/* True if the first non-whitespace byte of s is '{' (raw-JSON detection). */
|
||||
static bool cli_first_nonspace_is_brace(const char *s) {
|
||||
while (*s == ' ' || *s == '\t' || *s == '\n' || *s == '\r') {
|
||||
s++;
|
||||
}
|
||||
return *s == '{';
|
||||
}
|
||||
|
||||
static int run_cli(int argc, char **argv) {
|
||||
if (argc < MAIN_MIN_ARGC) {
|
||||
(void)fprintf(stderr, CLI_USAGE);
|
||||
@@ -243,7 +302,70 @@ static int run_cli(int argc, char **argv) {
|
||||
}
|
||||
|
||||
const char *tool_name = argv[0];
|
||||
const char *args_json = argc >= MAIN_CLI_ARGC ? argv[SKIP_ONE] : "{}";
|
||||
int rem_argc = argc - SKIP_ONE; /* args following the tool name */
|
||||
char **rem_argv = argv + SKIP_ONE;
|
||||
|
||||
/* --help / -h : print per-tool help (from the tool's input_schema) and exit
|
||||
* before any server work. */
|
||||
for (int i = 0; i < rem_argc; i++) {
|
||||
if (strcmp(rem_argv[i], "--help") == 0 || strcmp(rem_argv[i], "-h") == 0) {
|
||||
if (cbm_cli_print_tool_help(tool_name) != 0) {
|
||||
(void)fprintf(stderr, "error: unknown tool '%s'\n", tool_name);
|
||||
return SKIP_ONE;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/* Resolve the JSON arguments. Precedence: --args-file, then raw JSON
|
||||
* (back-compat), then --flags, then piped stdin, then empty {}. */
|
||||
char *heap_args = NULL; /* freed before return when set */
|
||||
const char *args_json = "{}";
|
||||
|
||||
int args_file_idx = -1;
|
||||
for (int i = 0; i < rem_argc; i++) {
|
||||
if (strcmp(rem_argv[i], "--args-file") == 0) {
|
||||
args_file_idx = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (args_file_idx >= 0) {
|
||||
if (args_file_idx + SKIP_ONE >= rem_argc) {
|
||||
(void)fprintf(stderr, "error: --args-file requires a path argument\n");
|
||||
return SKIP_ONE;
|
||||
}
|
||||
const char *path = rem_argv[args_file_idx + SKIP_ONE];
|
||||
heap_args = cli_slurp_file(path);
|
||||
if (!heap_args) {
|
||||
(void)fprintf(stderr, "error: cannot read args file '%s'\n", path);
|
||||
return SKIP_ONE;
|
||||
}
|
||||
args_json = heap_args;
|
||||
} else if (rem_argc >= SKIP_ONE && cli_first_nonspace_is_brace(rem_argv[0])) {
|
||||
/* raw-JSON back-compat: cli <tool> '{"k":"v"}' */
|
||||
args_json = rem_argv[0];
|
||||
} else if (rem_argc >= SKIP_ONE && strncmp(rem_argv[0], "--", 2) == 0) {
|
||||
/* flag form: cli <tool> --flag value --bare-bool ... */
|
||||
char *err = NULL;
|
||||
heap_args = cbm_cli_build_args_json(tool_name, rem_argc, rem_argv, &err);
|
||||
if (!heap_args) {
|
||||
(void)fprintf(stderr, "error: %s\n", err ? err : "invalid arguments");
|
||||
free(err);
|
||||
return SKIP_ONE;
|
||||
}
|
||||
args_json = heap_args;
|
||||
} else if (!cli_isatty(0)) {
|
||||
/* piped stdin (UTF-8 clean, no shell quoting): cli <tool> < args.json */
|
||||
heap_args = cli_slurp_stream(stdin);
|
||||
if (heap_args && heap_args[0]) {
|
||||
args_json = heap_args;
|
||||
} else {
|
||||
free(heap_args);
|
||||
heap_args = NULL;
|
||||
args_json = "{}";
|
||||
}
|
||||
}
|
||||
|
||||
if (progress) {
|
||||
cbm_progress_sink_init(stderr);
|
||||
@@ -274,6 +396,7 @@ static int run_cli(int argc, char **argv) {
|
||||
if (progress) {
|
||||
cbm_progress_sink_fini();
|
||||
}
|
||||
free(heap_args);
|
||||
return exit_code;
|
||||
}
|
||||
|
||||
|
||||
@@ -565,6 +565,21 @@ char *cbm_mcp_tools_list(void) {
|
||||
return cbm_mcp_tools_list_range(0, TOOL_COUNT, false);
|
||||
}
|
||||
|
||||
/* Return the JSON input_schema string for a tool by name, or NULL if unknown.
|
||||
* Used by the CLI to build --flag arguments and per-tool --help from the same
|
||||
* source of truth the MCP tools/list advertises. Static lifetime; do not free. */
|
||||
const char *cbm_mcp_tool_input_schema(const char *tool_name) {
|
||||
if (!tool_name) {
|
||||
return NULL;
|
||||
}
|
||||
for (int i = 0; i < TOOL_COUNT; i++) {
|
||||
if (strcmp(TOOLS[i].name, tool_name) == 0) {
|
||||
return TOOLS[i].input_schema;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static int mcp_tools_cursor_offset(const char *params_json) {
|
||||
if (!params_json) {
|
||||
return 0;
|
||||
|
||||
@@ -61,6 +61,10 @@ bool cbm_mcp_cancel_request_matches(const char *params_json, int64_t active_id,
|
||||
/* Format the tools/list response. Returns heap-allocated JSON. */
|
||||
char *cbm_mcp_tools_list(void);
|
||||
|
||||
/* Return a tool's JSON input_schema string by name (static; do not free), or
|
||||
* NULL if the tool is unknown. Backs the CLI flag parser + per-tool --help. */
|
||||
const char *cbm_mcp_tool_input_schema(const char *tool_name);
|
||||
|
||||
/* Format the initialize response. params_json is the raw initialize params
|
||||
* (used for protocol version negotiation). Returns heap-allocated JSON. */
|
||||
char *cbm_mcp_initialize_response(const char *params_json);
|
||||
|
||||
@@ -2800,6 +2800,96 @@ TEST(replace_binary_creates_new_file) {
|
||||
|
||||
#endif /* _WIN32 */
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
* CLI tool-argument flags / per-tool --help (#680)
|
||||
* ═══════════════════════════════════════════════════════════════════ */
|
||||
|
||||
/* A plain `--flag value` pair maps to a string property by schema type. */
|
||||
TEST(cli_build_args_json_string_flag_issue680) {
|
||||
char *err = NULL;
|
||||
char *argv[] = {"--repo-path", "/x"};
|
||||
char *json = cbm_cli_build_args_json("index_repository", 2, argv, &err);
|
||||
ASSERT_NOT_NULL(json);
|
||||
ASSERT_NULL(err);
|
||||
ASSERT(strstr(json, "\"repo_path\":\"/x\"") != NULL);
|
||||
free(json);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* An integer-typed property serializes as a JSON NUMBER, not a quoted string. */
|
||||
TEST(cli_build_args_json_integer_flag_issue680) {
|
||||
char *err = NULL;
|
||||
char *argv[] = {"--limit", "100"};
|
||||
char *json = cbm_cli_build_args_json("search_graph", 2, argv, &err);
|
||||
ASSERT_NOT_NULL(json);
|
||||
ASSERT(strstr(json, "\"limit\":100") != NULL);
|
||||
ASSERT(strstr(json, "\"limit\":\"100\"") == NULL);
|
||||
free(json);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A bare boolean flag (no value) becomes true. */
|
||||
TEST(cli_build_args_json_bare_boolean_issue680) {
|
||||
char *err = NULL;
|
||||
char *argv[] = {"--exclude-entry-points"};
|
||||
char *json = cbm_cli_build_args_json("search_graph", 1, argv, &err);
|
||||
ASSERT_NOT_NULL(json);
|
||||
ASSERT(strstr(json, "\"exclude_entry_points\":true") != NULL);
|
||||
free(json);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A repeated array-typed flag accumulates into a JSON array. */
|
||||
TEST(cli_build_args_json_repeated_array_issue680) {
|
||||
char *err = NULL;
|
||||
char *argv[] = {"--semantic-query", "send", "--semantic-query", "publish"};
|
||||
char *json = cbm_cli_build_args_json("search_graph", 4, argv, &err);
|
||||
ASSERT_NOT_NULL(json);
|
||||
ASSERT(strstr(json, "\"semantic_query\":[\"send\",\"publish\"]") != NULL);
|
||||
free(json);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* kebab-case flag names map to snake_case JSON keys. */
|
||||
TEST(cli_build_args_json_kebab_to_snake_issue680) {
|
||||
char *err = NULL;
|
||||
char *argv[] = {"--name-pattern", "Foo.*"};
|
||||
char *json = cbm_cli_build_args_json("search_graph", 2, argv, &err);
|
||||
ASSERT_NOT_NULL(json);
|
||||
ASSERT(strstr(json, "\"name_pattern\":\"Foo.*\"") != NULL);
|
||||
free(json);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* `--key=value` form splits on the FIRST `=`; value may contain spaces/dashes. */
|
||||
TEST(cli_build_args_json_key_equals_value_issue680) {
|
||||
char *err = NULL;
|
||||
char *argv[] = {"--repo-path=/a b"};
|
||||
char *json = cbm_cli_build_args_json("index_repository", 1, argv, &err);
|
||||
ASSERT_NOT_NULL(json);
|
||||
ASSERT(strstr(json, "\"repo_path\":\"/a b\"") != NULL);
|
||||
free(json);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* A non-`--` positional is an error: returns NULL and sets *err_out. */
|
||||
TEST(cli_build_args_json_bad_positional_errors_issue680) {
|
||||
char *err = NULL;
|
||||
char *argv[] = {"foo"};
|
||||
char *json = cbm_cli_build_args_json("search_graph", 1, argv, &err);
|
||||
ASSERT_NULL(json);
|
||||
ASSERT_NOT_NULL(err);
|
||||
free(err);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Per-tool --help returns 0 for a known tool, -1 for an unknown one. */
|
||||
TEST(cli_print_tool_help_issue680) {
|
||||
ASSERT_EQ(cbm_cli_print_tool_help("index_repository"), 0);
|
||||
ASSERT_EQ(cbm_cli_print_tool_help("nope_not_a_tool"), -1);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* ═══════════════════════════════════════════════════════════════════
|
||||
* Suite definition
|
||||
* ═══════════════════════════════════════════════════════════════════ */
|
||||
@@ -2963,4 +3053,14 @@ SUITE(cli) {
|
||||
RUN_TEST(replace_binary_overwrites_readonly);
|
||||
RUN_TEST(replace_binary_creates_new_file);
|
||||
#endif
|
||||
|
||||
/* CLI tool-argument flags / per-tool --help (#680) */
|
||||
RUN_TEST(cli_build_args_json_string_flag_issue680);
|
||||
RUN_TEST(cli_build_args_json_integer_flag_issue680);
|
||||
RUN_TEST(cli_build_args_json_bare_boolean_issue680);
|
||||
RUN_TEST(cli_build_args_json_repeated_array_issue680);
|
||||
RUN_TEST(cli_build_args_json_kebab_to_snake_issue680);
|
||||
RUN_TEST(cli_build_args_json_key_equals_value_issue680);
|
||||
RUN_TEST(cli_build_args_json_bad_positional_errors_issue680);
|
||||
RUN_TEST(cli_print_tool_help_issue680);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user