diff --git a/README.md b/README.md
index 87df9e57bc..6d48c4c758 100644
--- a/README.md
+++ b/README.md
@@ -72,6 +72,7 @@ Output controls:
| Need | Option |
|---|---|
| Owner-only persistent stderr logs on POSIX | Global tool stderr logs are forced to `0600` permissions on every open, including existing date-stamped log files. |
+| Checked-in configuration | Use `.cdidx/config.json` for repository defaults such as `search.limit`, `search.snippet_lines`, and `search.max_line_width`; run `cdidx validate-config` to validate the discovered file. |
| ASCII-only terminal output | Use `--ascii`, `CDIDX_ASCII=1`, `NO_UNICODE`, `TERM=dumb`, accessibility env hints, or a non-UTF-8 locale. Spinners use pipe, slash, dash, and backslash frames; progress bars use `#` / `-`; very narrow terminals fall back to percentage-only progress. |
| Script-friendly query pipelines | Use `--quiet`, `-q`, `--silent`, or `CDIDX_QUIET=1` to suppress informational stderr text while preserving errors. `--quiet` takes precedence over `--verbose`. |
@@ -91,14 +92,14 @@ file completion.
| Area | What cdidx provides |
|---|---|
| Search surfaces | CLI-first output for humans and machines; full-text, symbol, reference, caller/callee, dependency, map, inspect, and excerpt commands. |
-| Ranking and filters | Public/exported symbol matches rank ahead of protected, internal, and private matches. Use `--no-visibility-rank` for legacy order, and `--visibility` / `--exclude-visibility` with `symbols`, `definition`, `unused`, and `hotspots`. |
+| Ranking and filters | Public/exported symbol matches rank ahead of protected, internal, and private matches. Use `--no-visibility-rank` for legacy order, and `--visibility` / `--exclude-visibility` with `symbols`, `definition`, `unused`, and `hotspots`. Query defaults can be adjusted with `CDIDX_DEFAULT_LIMIT`, `CDIDX_DEFAULT_SNIPPET_LINES`, and `CDIDX_DEFAULT_MAX_LINE_WIDTH`; explicit CLI flags still win. |
| Project scoping | `.sln` / `.csproj`-aware --project <name|path> filters for indexing and queries, plus `--solution ` when a workspace has multiple solution files. |
| MCP integration | MCP server support for AI clients such as Claude Code, Cursor, and Windsurf, including tools, indexed-file resources, starter prompts, and `Language support:` descriptions sourced from the same registries as `cdidx languages`. |
| Freshness | Parallel full-scan extraction with `--parallelism`, incremental refreshes with `--files` and `--commits`, continuous `--watch`, exact `status --check`, and configurable stale thresholds via `--stale-after` / `CDIDX_STALE_AFTER`. |
-| Storage | Local-first `.cdidx/codeindex.db` storage. `--data-dir `, `CDIDX_DATA_DIR`, or `XDG_DATA_HOME` can move default SQLite storage outside the workspace; explicit `--db ` still wins. |
+| Storage | Local-first `.cdidx/codeindex.db` storage. Query commands run from nested directories prefer the outermost ancestor `.cdidx/codeindex.db` before falling back to the current directory. `--data-dir `, `CDIDX_DATA_DIR`, or `XDG_DATA_HOME` can move default SQLite storage outside the workspace; explicit `--db ` still wins. |
| DB maintenance | New indexes use SQLite incremental auto-vacuum. `cdidx vacuum` reclaims free pages from existing DBs, including a one-time full `VACUUM` conversion for legacy no-autovacuum DBs, and `status --json` reports metrics under `db_pragma_settings`. |
| Security defaults | On POSIX systems, `.cdidx` is created with `0700` permissions and `status --json` reports the effective `data_dir_mode` when available. |
-| Diagnostics | `status --explain ` describes readiness fields and remediation. Read commands support `--profile`, `--slow-query-ms `, and --trace=stderr|file|none; file traces write daily `query-trace-YYYYMMDD.jsonl` files next to the lifecycle log. |
+| Diagnostics | `status --config` prints effective configuration with source attribution, and `status --explain ` describes readiness fields and remediation. Read commands support `--profile`, `--slow-query-ms `, and --trace=stderr|file|none; file traces write daily `query-trace-YYYYMMDD.jsonl` files next to the lifecycle log. |
| Query exit codes | Valid zero-result query commands exit `0` by default. Pass `--strict-not-found` when scripts should treat zero rows as exit code `2`. |
| Drift checks | `cdidx diff ` compares schema, file, symbol, and reference deltas with stable exit codes: `0` identical, `1` drift, `2` schema mismatch, `3` unreadable DB. |
| Extensibility and feedback | Post-extraction hooks from `~/.config/cdidx/hooks/*.dll` or `CDIDX_HOOKS_DIR` can enrich symbols and references. `cdidx suggestions` lists, inspects, and exports local suggestion history, with fuzzy MCP suggestion deduplication controlled by CLI, env, or `.cdidxrc.json`. |
@@ -256,6 +257,7 @@ cdidx mcp
| 目的 | option / 動作 |
|---|---|
| POSIX の persistent stderr log を owner-only にする | global tool stderr log は開くたびに `0600` 権限へ補正され、既存の日付付き log file も同じ扱いになります。 |
+| checked-in configuration | repository 既定値には `.cdidx/config.json` を使えます。例: `search.limit`、`search.snippet_lines`、`search.max_line_width`。検出された file は `cdidx validate-config` で検証できます。 |
| ASCII-only 端末で崩れない表示にする | `--ascii`、`CDIDX_ASCII=1`、`NO_UNICODE`、`TERM=dumb`、accessibility 系の環境変数、非 UTF-8 locale を使います。スピナーは pipe、slash、dash、backslash の frame、進捗バーは `#` / `-` になり、幅が非常に狭い端末では percentage-only になります。 |
| script 向け query pipeline の stderr を静かにする | `--quiet`、`-q`、`--silent`、`CDIDX_QUIET=1` で informational stderr を抑制し、error 行だけを残します。`--quiet` は `--verbose` より優先されます。 |
@@ -274,14 +276,14 @@ cdidx mcp
| 分野 | 内容 |
|---|---|
| 検索面 | CLI-first の人間向け / 機械処理向け出力。全文検索、シンボル、参照、caller/callee、依存関係、map、inspect、excerpt コマンドを提供します。 |
-| 順位と filter | public/exported なシンボル一致を protected、internal、private より優先します。従来順は `--no-visibility-rank`、可視性の include / exclude は `symbols`、`definition`、`unused`、`hotspots` の `--visibility` / `--exclude-visibility` で指定できます。 |
+| 順位と filter | public/exported なシンボル一致を protected、internal、private より優先します。従来順は `--no-visibility-rank`、可視性の include / exclude は `symbols`、`definition`、`unused`、`hotspots` の `--visibility` / `--exclude-visibility` で指定できます。query 既定値は `CDIDX_DEFAULT_LIMIT`、`CDIDX_DEFAULT_SNIPPET_LINES`、`CDIDX_DEFAULT_MAX_LINE_WIDTH` で調整でき、明示 CLI flag が常に優先されます。 |
| project scope | `.sln` / `.csproj` を使った --project <name|path> filter で index と query を .NET project 配下へ絞り込めます。workspace に solution が複数ある場合は `--solution ` を指定します。 |
| MCP 連携 | Claude Code、Cursor、Windsurf などの AI クライアント向け MCP server。tools、インデックス済みファイル resources、starter prompts、`cdidx languages` と同じ言語レジストリ由来の `Language support:` 説明を提供します。 |
| freshness | `--parallelism` による parallel full-scan、`--files` / `--commits` による差分更新、`--watch` による継続更新、`status --check` による完全一致確認、`--stale-after` / `CDIDX_STALE_AFTER` による age threshold 上書きに対応します。 |
-| storage | `.cdidx/codeindex.db` に保存する local-first 設計。既定の SQLite 保存先は `--data-dir `、`CDIDX_DATA_DIR`、`XDG_DATA_HOME` で workspace 外へ移せます。明示的な `--db ` は引き続き最優先です。 |
+| storage | `.cdidx/codeindex.db` に保存する local-first 設計。ネストしたディレクトリからの query コマンドは、current directory にフォールバックする前に最上位祖先の `.cdidx/codeindex.db` を優先します。既定の SQLite 保存先は `--data-dir `、`CDIDX_DATA_DIR`、`XDG_DATA_HOME` で workspace 外へ移せます。明示的な `--db ` は引き続き最優先です。 |
| DB maintenance | 新規 index DB は SQLite incremental auto-vacuum を使います。既存 DB は `cdidx vacuum` で free page を回収でき、legacy no-autovacuum DB は初回だけ full `VACUUM` で変換します。`status --json` は `db_pragma_settings` 配下に metrics を出力します。 |
| security defaults | POSIX では `.cdidx` を `0700` 権限で作成します。`status --json` は利用可能な場合に実効 POSIX mode を `data_dir_mode` として報告します。 |
-| diagnostics | `status --explain ` は readiness field の意味と対処を説明します。read 系コマンドは `--profile`、`--slow-query-ms `、--trace=stderr|file|none に対応し、file trace は lifecycle log と同じ場所に日次 `query-trace-YYYYMMDD.jsonl` を書きます。 |
+| diagnostics | `status --config` は source attribution 付きの effective configuration を出力し、`status --explain ` は readiness field の意味と対処を説明します。read 系コマンドは `--profile`、`--slow-query-ms `、--trace=stderr|file|none に対応し、file trace は lifecycle log と同じ場所に日次 `query-trace-YYYYMMDD.jsonl` を書きます。 |
| drift checks | `cdidx diff ` は schema、file、symbol、reference の差分を比較します。exit code は `0` identical、`1` drift、`2` schema mismatch、`3` unreadable DB です。 |
| extensibility / feedback | `~/.config/cdidx/hooks/*.dll` または `CDIDX_HOOKS_DIR` の post-extraction hook で永続化前のシンボルと参照を拡張できます。`cdidx suggestions` はローカル提案履歴の一覧表示、詳細表示、エクスポートに対応し、MCP 提案の近似重複排除しきい値は CLI、env、`.cdidxrc.json` で調整できます。 |
| language coverage | 78 言語を検出し、対応言語ではシンボルとグラフも利用可能です。 |
diff --git a/changelog.d/unreleased/1806.fixed.md b/changelog.d/unreleased/1806.fixed.md
new file mode 100644
index 0000000000..f703b72b1a
--- /dev/null
+++ b/changelog.d/unreleased/1806.fixed.md
@@ -0,0 +1,15 @@
+---
+category: fixed
+issues:
+ - 1806
+affected:
+ - src/CodeIndex/Cli/DbPathResolver.cs
+---
+
+## English
+
+- **Query commands now prefer the workspace-root `.cdidx/codeindex.db` when run from nested directories (#1806)** — implicit query DB resolution walks ancestor `.cdidx` directories and uses the outermost workspace root before falling back to the current directory.
+
+## 日本語
+
+- **ネストしたディレクトリからの query コマンドが workspace-root の `.cdidx/codeindex.db` を優先するようになりました (#1806)** — 明示指定なしの query DB 解決は祖先の `.cdidx` を辿り、最上位の workspace root を使ってから current directory にフォールバックします。
diff --git a/changelog.d/unreleased/1813.added.md b/changelog.d/unreleased/1813.added.md
new file mode 100644
index 0000000000..4db3ebfa1b
--- /dev/null
+++ b/changelog.d/unreleased/1813.added.md
@@ -0,0 +1,15 @@
+---
+category: added
+issues:
+ - 1813
+affected:
+ - src/CodeIndex/Cli/QueryCommandRunner.cs
+---
+
+## English
+
+- **`status --config` now prints effective configuration with source attribution (#1813)** — the JSON output includes resolved DB/data-dir settings, numeric query defaults, log directory, and version without requiring the DB to exist.
+
+## 日本語
+
+- **`status --config` が source attribution 付きの effective configuration を出力するようになりました (#1813)** — JSON 出力に解決済み DB/data-dir 設定、数値 query 既定値、log directory、version が含まれ、DB が存在しなくても確認できます。
diff --git a/changelog.d/unreleased/1926.added.md b/changelog.d/unreleased/1926.added.md
new file mode 100644
index 0000000000..8fdbf8e3d3
--- /dev/null
+++ b/changelog.d/unreleased/1926.added.md
@@ -0,0 +1,15 @@
+---
+category: added
+issues:
+ - 1926
+affected:
+ - src/CodeIndex/Cli/CdidxConfigFile.cs
+---
+
+## English
+
+- **Project `.cdidx/config.json` is now a supported config-file surface (#1926)** — cdidx validates the versioned sections and can materialize search defaults from checked-in config; `cdidx validate-config` reports whether the discovered config is valid.
+
+## 日本語
+
+- **project `.cdidx/config.json` を config-file surface としてサポートしました (#1926)** — cdidx は versioned section を検証し、checked-in config から search 既定値を反映できます。`cdidx validate-config` で検出された config が valid か確認できます。
diff --git a/changelog.d/unreleased/2036.added.md b/changelog.d/unreleased/2036.added.md
new file mode 100644
index 0000000000..0cc587c000
--- /dev/null
+++ b/changelog.d/unreleased/2036.added.md
@@ -0,0 +1,15 @@
+---
+category: added
+issues:
+ - 2036
+affected:
+ - src/CodeIndex/Cli/QueryCommandRunner.cs
+---
+
+## English
+
+- **Numeric query defaults can now be set with environment variables (#2036)** — `CDIDX_DEFAULT_LIMIT`, `CDIDX_DEFAULT_SNIPPET_LINES`, and `CDIDX_DEFAULT_MAX_LINE_WIDTH` set the default values that CLI flags can still override.
+
+## 日本語
+
+- **数値 query 既定値を環境変数で設定できるようになりました (#2036)** — `CDIDX_DEFAULT_LIMIT`、`CDIDX_DEFAULT_SNIPPET_LINES`、`CDIDX_DEFAULT_MAX_LINE_WIDTH` で既定値を設定でき、CLI フラグは引き続きそれらを上書きします。
diff --git a/src/CodeIndex/Cli/CdidxConfigFile.cs b/src/CodeIndex/Cli/CdidxConfigFile.cs
index 2197366c07..f8a493f88c 100644
--- a/src/CodeIndex/Cli/CdidxConfigFile.cs
+++ b/src/CodeIndex/Cli/CdidxConfigFile.cs
@@ -23,7 +23,9 @@ namespace CodeIndex.Cli;
internal static class CdidxConfigFile
{
internal const string FileName = ".cdidxrc.json";
+ internal static readonly string ProjectConfigRelativePath = Path.Combine(".cdidx", "config.json");
internal const string DisableEnvVar = "CDIDX_DISABLE_CONFIG_FILE";
+ internal const string ConfigSourceEnvironmentVariablePrefix = "CDIDX_CONFIG_SOURCE__";
private static readonly IReadOnlyList KnownTopLevelKeys = new[]
{
@@ -34,11 +36,19 @@ internal static class CdidxConfigFile
"global_tool_log_dir",
"stale_after",
"indexing",
+ "search",
+ "output",
+ "graph",
+ "folding",
"suggestion_dedup_threshold",
"mcp",
};
private static readonly IReadOnlyList KnownIndexingKeys = new[] { "includeKinds", "excludeKinds" };
+ private static readonly IReadOnlyList KnownSearchKeys = new[] { "limit", "snippet_lines", "max_line_width" };
+ private static readonly IReadOnlyList KnownOutputKeys = new[] { "format", "locale" };
+ private static readonly IReadOnlyList KnownGraphKeys = new[] { "max_hops" };
+ private static readonly IReadOnlyList KnownFoldingKeys = new[] { "fold_key_version" };
private static readonly IReadOnlyList KnownMcpKeys = new[] { "tools", "rate_limit" };
private static readonly IReadOnlyList KnownMcpToolsKeys = new[] { "allow", "deny" };
private static readonly IReadOnlyList KnownMcpRateLimitKeys = new[] { "rps", "burst" };
@@ -179,6 +189,37 @@ internal static LoadResult LoadAndApply(
}
}
+ if (root.TryGetProperty("search", out var search))
+ {
+ if (search.ValueKind != JsonValueKind.Object)
+ return new LoadResult(Path: path, Error: $"[cdidx] {path}: `search` must be a JSON object.");
+ if (TryFindUnknownKey(search, KnownSearchKeys, out var unknownSearchKey))
+ return new LoadResult(Path: path, Error: $"[cdidx] {path}: unknown key `search.{unknownSearchKey}`. Supported keys: {string.Join(", ", KnownSearchKeys)}.");
+ if (search.TryGetProperty("limit", out var limit))
+ {
+ if (!TryReadSearchInteger(limit, "search.limit", "--limit", allowZero: false, path, out var value, out var err))
+ return new LoadResult(Path: path, Error: err);
+ pending.Add((QueryCommandRunner.DefaultLimitEnvironmentVariable, value!));
+ }
+ if (search.TryGetProperty("snippet_lines", out var snippetLines))
+ {
+ if (!TryReadSearchInteger(snippetLines, "search.snippet_lines", "--snippet-lines", allowZero: false, path, out var value, out var err))
+ return new LoadResult(Path: path, Error: err);
+ pending.Add((QueryCommandRunner.DefaultSnippetLinesEnvironmentVariable, value!));
+ }
+ if (search.TryGetProperty("max_line_width", out var maxLineWidth))
+ {
+ if (!TryReadSearchInteger(maxLineWidth, "search.max_line_width", "--max-line-width", allowZero: true, path, out var value, out var err))
+ return new LoadResult(Path: path, Error: err);
+ pending.Add((QueryCommandRunner.DefaultMaxLineWidthEnvironmentVariable, value!));
+ }
+ }
+
+ if (!ValidateOptionalObject(root, "output", KnownOutputKeys, path, out var optionalObjectError)
+ || !ValidateOptionalObject(root, "graph", KnownGraphKeys, path, out optionalObjectError)
+ || !ValidateOptionalObject(root, "folding", KnownFoldingKeys, path, out optionalObjectError))
+ return new LoadResult(Path: path, Error: optionalObjectError);
+
if (root.TryGetProperty("mcp", out var mcp))
{
if (mcp.ValueKind != JsonValueKind.Object)
@@ -241,7 +282,10 @@ internal static LoadResult LoadAndApply(
foreach (var (name, value) in pending)
{
if (envReader(name) is null)
+ {
envWriter(name, value);
+ envWriter(ConfigSourceEnvironmentVariablePrefix + name, path);
+ }
}
return new LoadResult(Path: path, Error: null);
@@ -265,6 +309,9 @@ internal static LoadResult LoadAndApply(
while (current is not null)
{
+ var projectCandidate = Path.Combine(current.FullName, ProjectConfigRelativePath);
+ if (File.Exists(LongPath.EnsureWindowsPrefix(projectCandidate)))
+ return projectCandidate;
var candidate = Path.Combine(current.FullName, FileName);
if (File.Exists(LongPath.EnsureWindowsPrefix(candidate)))
return candidate;
@@ -273,6 +320,48 @@ internal static LoadResult LoadAndApply(
return null;
}
+ internal static int RunValidate(string[] args, JsonSerializerOptions jsonOptions)
+ {
+ if (args.Length > 0)
+ {
+ CommandErrorWriter.Write("validate-config does not accept positional arguments.", "run `cdidx validate-config` from the workspace whose config should be validated.");
+ return CommandExitCodes.UsageError;
+ }
+
+ var result = LoadAndApply(Environment.CurrentDirectory, name => name == DisableEnvVar ? null : Environment.GetEnvironmentVariable(name), (_, _) => { });
+ if (result.Failed)
+ {
+ Console.Error.WriteLine(result.Error);
+ return CommandExitCodes.UsageError;
+ }
+
+ var payload = new Dictionary
+ {
+ ["valid"] = true,
+ ["path"] = result.Path,
+ };
+ Console.WriteLine(JsonSerializer.Serialize(payload, jsonOptions));
+ return CommandExitCodes.Success;
+ }
+
+ private static bool ValidateOptionalObject(JsonElement root, string key, IReadOnlyList knownKeys, string path, out string? error)
+ {
+ error = null;
+ if (!root.TryGetProperty(key, out var value))
+ return true;
+ if (value.ValueKind != JsonValueKind.Object)
+ {
+ error = $"[cdidx] {path}: `{key}` must be a JSON object.";
+ return false;
+ }
+ if (TryFindUnknownKey(value, knownKeys, out var unknownKey))
+ {
+ error = $"[cdidx] {path}: unknown key `{key}.{unknownKey}`. Supported keys: {string.Join(", ", knownKeys)}.";
+ return false;
+ }
+ return true;
+ }
+
private static bool TryFindUnknownKey(JsonElement obj, IReadOnlyList knownKeys, out string? unknown)
{
foreach (var property in obj.EnumerateObject())
@@ -353,4 +442,32 @@ private static bool TryReadNumberAsString(JsonElement element, string key, strin
value = element.GetRawText();
return true;
}
+
+ private static bool TryReadSearchInteger(JsonElement element, string key, string optionName, bool allowZero, string path, out string? value, out string? error)
+ {
+ value = null;
+ error = null;
+ if (element.ValueKind != JsonValueKind.Number)
+ {
+ error = $"[cdidx] {path}: `{key}` must be a number.";
+ return false;
+ }
+
+ if (!element.TryGetInt32(out var parsed) || parsed < 0 || (!allowZero && parsed == 0))
+ {
+ error = allowZero
+ ? $"[cdidx] {path}: `{key}` must be a non-negative integer."
+ : $"[cdidx] {path}: `{key}` must be a positive integer.";
+ return false;
+ }
+
+ if (QueryCommandRunner.NumericFlagUpperBounds.TryGetValue(optionName, out var maxAllowed) && parsed > maxAllowed)
+ {
+ error = $"[cdidx] {path}: `{key}` must be <= {maxAllowed}.";
+ return false;
+ }
+
+ value = parsed.ToString(System.Globalization.CultureInfo.InvariantCulture);
+ return true;
+ }
}
diff --git a/src/CodeIndex/Cli/CliFlagSchema.cs b/src/CodeIndex/Cli/CliFlagSchema.cs
index 60c68433ea..9e26b563c4 100644
--- a/src/CodeIndex/Cli/CliFlagSchema.cs
+++ b/src/CodeIndex/Cli/CliFlagSchema.cs
@@ -53,7 +53,7 @@ internal static class CliFlagSchema
public static IReadOnlyList AllCommands { get; } =
[
"index", "backfill-fold", "optimize", "search", "definition", "references", "callers", "callees",
- "symbols", "files", "find", "excerpt", "map", "inspect", "outline", "status",
+ "symbols", "files", "find", "excerpt", "map", "inspect", "outline", "status", "validate-config",
"validate", "deps", "impact", "unused", "hotspots", "languages", "batch", "mcp", "completions", "db", "vacuum", "report", "license",
];
@@ -241,6 +241,7 @@ private static IReadOnlyList BuildAll()
new() { Name = "--group-by", ValuePlaceholder = "", Description = "Hotspots: choose grouping unit", Commands = Set("hotspots") },
new() { Name = "--group-by-name", Description = "Hotspots: collapse same-name rows across files", Commands = Set("hotspots") },
new() { Name = "--check", Description = "Verify status freshness/readiness", Commands = Set("status") },
+ new() { Name = "--config", Description = "Print effective configuration with source attribution", Commands = Set("status") },
new() { Name = "--stale-after", ValuePlaceholder = "", Description = "Status: freshness age threshold (e.g. 30m, 2h, 7d)", Commands = Set("status") },
new() { Name = "--explain", ValuePlaceholder = "", Description = "Explain one status readiness field", Commands = Set("status") },
new() { Name = "--log-path", Description = "Print the active persistent log directory", Commands = Set("status") },
diff --git a/src/CodeIndex/Cli/ConsoleUi.cs b/src/CodeIndex/Cli/ConsoleUi.cs
index a4cd0b082c..45bc2741ec 100644
--- a/src/CodeIndex/Cli/ConsoleUi.cs
+++ b/src/CodeIndex/Cli/ConsoleUi.cs
@@ -80,7 +80,8 @@ private static readonly (string Command, string Usage)[] CommandUsageLines =
("map", "cdidx map [--db ] [--json] [--verbose] [--limit ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--bytes]"),
("inspect", "cdidx inspect |--query |-- [--db ] [--json] [--verbose] [--limit ] [--lang ] [--path ] [--exclude-path ] [--exclude-tests] [--body] [--max-line-width ] [--exact|--exact-name]"),
("outline", "cdidx outline [--db ] [--json] [--verbose]"),
- ("status", "cdidx status [--db ] [--json] [--verbose] [--check[=workspace,fold,graph,issues,hotspot,csharp,sql,newer]] [--stale-after ] [--explain ] [--log-path]"),
+ ("status", "cdidx status [--db ] [--json] [--verbose] [--check[=workspace,fold,graph,issues,hotspot,csharp,sql,newer]] [--stale-after ] [--explain ] [--log-path] [--config]"),
+ ("validate-config", "cdidx validate-config"),
("db", "cdidx db --integrity-check [--db ] [--json]"),
("diff", "cdidx diff [--json] [--summary-only] [--detailed] [--limit ]"),
("report", "cdidx report --output [--db ] [--json] [--log-lines ] [--no-log] [--include-args]"),
@@ -648,7 +649,8 @@ void WriteHelpLine(string line = "")
Console.WriteLine(" map Show a repo-level overview for AI orientation");
Console.WriteLine(" inspect Bundle definition, graph, and nearby symbol context");
Console.WriteLine(" outline Show a file outline ordered by line, start column, kind, and name");
- Console.WriteLine(" status Show database statistics; add --check for freshness, --explain for readiness, or --log-path for logs");
+ Console.WriteLine(" status Show database statistics; add --check for freshness, --config for effective config, --explain for readiness, or --log-path for logs");
+ Console.WriteLine(" validate-config Validate .cdidx/config.json or .cdidxrc.json");
Console.WriteLine(" db --integrity-check Run SQLite `PRAGMA integrity_check` and report findings");
Console.WriteLine(" diff Compare two index databases; exit 0 identical, 1 drift, 2 schema mismatch, 3 unreadable");
Console.WriteLine(" report --output Build a redacted crash-repro tarball (.tgz) for bug reports");
@@ -793,6 +795,8 @@ void WriteHelpLine(string line = "")
Console.WriteLine(" cdidx files --lang python List Python files");
Console.WriteLine(" cdidx files --since 2024-01-01 Files modified since a date");
Console.WriteLine(" cdidx status --json DB stats as JSON");
+ Console.WriteLine(" cdidx status --config Effective configuration as JSON");
+ Console.WriteLine(" cdidx validate-config Validate checked-in config");
Console.WriteLine(" cdidx languages Show supported languages");
Console.WriteLine(" cdidx --completions zsh > ~/.zfunc/_cdidx Generate a zsh completion script");
Console.WriteLine(" cdidx license Show licensing and commercial-use terms");
@@ -1019,7 +1023,7 @@ private static int DamerauLevenshteinDistance(string s, string t)
private static readonly string[] Commands =
[
"index", "backfill-fold", "optimize", "search", "definition", "references", "callers", "callees",
- "symbols", "files", "find", "excerpt", "map", "inspect", "outline", "status",
+ "symbols", "files", "find", "excerpt", "map", "inspect", "outline", "status", "validate-config",
"validate", "deps", "impact", "unused", "hotspots", "languages", "batch", "mcp", "completions", "db", "vacuum", "report", "license",
];
@@ -1087,7 +1091,7 @@ private static string GetCompletionKinds() =>
// generic catch-all となるよう揃える。テストもこの並びを前提にしている。
private static readonly string[] EnumeratedCompletionCommands =
[
- "find", "excerpt", "references", "inspect", "hotspots", "status", "db", "report", "search",
+ "find", "excerpt", "references", "inspect", "hotspots", "status", "validate-config", "db", "report", "search",
];
// Generic-branch representative set: union of completion flags from these commands populates
diff --git a/src/CodeIndex/Cli/DbPathResolver.cs b/src/CodeIndex/Cli/DbPathResolver.cs
index c59310363b..96f7126989 100644
--- a/src/CodeIndex/Cli/DbPathResolver.cs
+++ b/src/CodeIndex/Cli/DbPathResolver.cs
@@ -39,7 +39,7 @@ public static DbPathResolution ResolveForQuery(string workspacePath, string? exp
if (!string.IsNullOrWhiteSpace(explicitDbPath))
return new DbPathResolution(explicitDbPath, null, null);
- return ResolveDataDir(workspacePath, explicitDataDir, Environment.GetEnvironmentVariable(DataDirEnvironmentVariable), Environment.GetEnvironmentVariable("XDG_DATA_HOME"));
+ return ResolveDataDirForQuery(workspacePath, explicitDataDir, Environment.GetEnvironmentVariable(DataDirEnvironmentVariable), Environment.GetEnvironmentVariable("XDG_DATA_HOME"));
}
internal static DbPathResolution ResolveDataDir(string workspacePath, string? explicitDataDir, string? environmentDataDir, string? xdgDataHome)
@@ -53,25 +53,100 @@ internal static DbPathResolution ResolveDataDir(string workspacePath, string? ex
if (!string.IsNullOrWhiteSpace(xdgDataHome))
{
- var workspaceHash = ComputeWorkspaceHash(fullWorkspacePath);
- return BuildDataDirResolution(Path.Combine(xdgDataHome, "cdidx", workspaceHash), DataDirSourceXdg);
+ return BuildDataDirResolution(BuildXdgDataDir(xdgDataHome, fullWorkspacePath), DataDirSourceXdg);
}
return BuildDataDirResolution(Path.Combine(fullWorkspacePath, ".cdidx"), DataDirSourceWorkspace);
}
+ internal static DbPathResolution ResolveDataDirForQuery(string workspacePath, string? explicitDataDir, string? environmentDataDir, string? xdgDataHome)
+ {
+ var fullWorkspacePath = Path.GetFullPath(workspacePath);
+ if (!string.IsNullOrWhiteSpace(explicitDataDir))
+ return BuildDataDirResolution(explicitDataDir, DataDirSourceFlag);
+
+ if (!string.IsNullOrWhiteSpace(environmentDataDir))
+ return BuildDataDirResolution(environmentDataDir, DataDirSourceEnv);
+
+ if (!string.IsNullOrWhiteSpace(xdgDataHome))
+ {
+ var ancestorXdgDataDir = TryResolveOutermostAncestorXdgDataDir(fullWorkspacePath, xdgDataHome);
+ if (ancestorXdgDataDir != null)
+ return BuildDataDirResolution(ancestorXdgDataDir, DataDirSourceXdg);
+ return BuildDataDirResolution(BuildXdgDataDir(xdgDataHome, fullWorkspacePath), DataDirSourceXdg);
+ }
+
+ var workspaceRootDataDir = TryResolveOutermostAncestorDataDir(fullWorkspacePath);
+ if (workspaceRootDataDir != null)
+ return BuildDataDirResolution(workspaceRootDataDir, DataDirSourceWorkspace);
+
+ return BuildDataDirResolution(Path.Combine(fullWorkspacePath, ".cdidx"), DataDirSourceWorkspace);
+ }
+
private static DbPathResolution BuildDataDirResolution(string dataDir, string source)
{
var fullDataDir = Path.GetFullPath(dataDir);
return new DbPathResolution(Path.Combine(fullDataDir, "codeindex.db"), fullDataDir, source);
}
+ private static string BuildXdgDataDir(string xdgDataHome, string workspacePath)
+ => Path.Combine(xdgDataHome, "cdidx", ComputeWorkspaceHash(workspacePath));
+
private static string ComputeWorkspaceHash(string workspacePath)
{
var bytes = SHA256.HashData(Encoding.UTF8.GetBytes(Path.GetFullPath(workspacePath)));
return Convert.ToHexString(bytes, 0, 8).ToLowerInvariant();
}
+ private static string? TryResolveOutermostAncestorDataDir(string workspacePath)
+ {
+ DirectoryInfo? current;
+ try
+ {
+ current = new DirectoryInfo(Path.GetFullPath(workspacePath));
+ }
+ catch
+ {
+ return null;
+ }
+
+ string? selected = null;
+ while (current is not null)
+ {
+ var candidate = Path.Combine(current.FullName, ".cdidx");
+ if (Directory.Exists(LongPath.EnsureWindowsPrefix(candidate)))
+ selected = candidate;
+ current = current.Parent;
+ }
+
+ return selected;
+ }
+
+ private static string? TryResolveOutermostAncestorXdgDataDir(string workspacePath, string xdgDataHome)
+ {
+ string? current;
+ try
+ {
+ current = Path.GetFullPath(workspacePath);
+ }
+ catch
+ {
+ return null;
+ }
+
+ string? selected = null;
+ while (!string.IsNullOrWhiteSpace(current))
+ {
+ var candidate = BuildXdgDataDir(xdgDataHome, current);
+ if (Directory.Exists(LongPath.EnsureWindowsPrefix(candidate)) ||
+ File.Exists(LongPath.EnsureWindowsPrefix(Path.Combine(candidate, "codeindex.db"))))
+ selected = candidate;
+ current = Path.GetDirectoryName(current);
+ }
+
+ return selected;
+ }
+
///
/// Resolve the most likely project root for query commands from the DB path.
/// クエリ系コマンドのDBパスから、もっとも可能性が高いプロジェクトルートを解決する。
diff --git a/src/CodeIndex/Cli/ProgramRunner.cs b/src/CodeIndex/Cli/ProgramRunner.cs
index 0b6be8c144..f3c0196c3d 100644
--- a/src/CodeIndex/Cli/ProgramRunner.cs
+++ b/src/CodeIndex/Cli/ProgramRunner.cs
@@ -209,6 +209,7 @@ internal static int Run(
"backfill-fold" => IndexCommandRunner.RunBackfillFold(subArgs, jsonOptions),
"optimize" => IndexCommandRunner.RunOptimizeFts(subArgs, jsonOptions),
"vacuum" => QueryCommandRunner.RunVacuum(subArgs, jsonOptions),
+ "validate-config" => CdidxConfigFile.RunValidate(subArgs, jsonOptions),
"db" => DbCommandRunner.RunIntegrityCheck(subArgs, jsonOptions),
"report" => ReportCommandRunner.Run(subArgs, jsonOptions, appVersion),
_ when IsProjectPathArg(commandName)
diff --git a/src/CodeIndex/Cli/QueryCommandRunner.cs b/src/CodeIndex/Cli/QueryCommandRunner.cs
index 73fa381b1f..2a4ca8aeb3 100644
--- a/src/CodeIndex/Cli/QueryCommandRunner.cs
+++ b/src/CodeIndex/Cli/QueryCommandRunner.cs
@@ -15,6 +15,9 @@ namespace CodeIndex.Cli;
///
public static class QueryCommandRunner
{
+ internal const string DefaultLimitEnvironmentVariable = "CDIDX_DEFAULT_LIMIT";
+ internal const string DefaultSnippetLinesEnvironmentVariable = "CDIDX_DEFAULT_SNIPPET_LINES";
+ internal const string DefaultMaxLineWidthEnvironmentVariable = "CDIDX_DEFAULT_MAX_LINE_WIDTH";
internal const string StaleAfterEnvironmentVariable = "CDIDX_STALE_AFTER";
internal static readonly TimeSpan DefaultStaleAfter = TimeSpan.FromHours(24);
[ThreadStatic]
@@ -465,7 +468,12 @@ public static int RunDefinition(string[] cmdArgs, JsonSerializerOptions jsonOpti
Console.Error.WriteLine(previewOptionError);
return CommandExitCodes.UsageError;
}
- var options = ParseArgs(cmdArgs, jsonDefault: false, allowNamedQuery: true);
+ var options = ParseArgs(
+ cmdArgs,
+ jsonDefault: false,
+ allowNamedQuery: true,
+ validateDefaultSnippetLines: false,
+ validateDefaultMaxLineWidth: false);
if (TryWriteUnsupportedOptionError("definition", cmdArgs, CliFlagSchema.GetAcceptedFlagNamesForCommand("definition"), options.Query))
return CommandExitCodes.UsageError;
if (TryWriteParseError(options, "definition"))
@@ -1163,7 +1171,12 @@ public static int RunSymbols(string[] cmdArgs, JsonSerializerOptions jsonOptions
Console.Error.WriteLine(previewOptionError);
return CommandExitCodes.UsageError;
}
- var options = ParseArgs(cmdArgs, jsonDefault: false, allowNamedQuery: true);
+ var options = ParseArgs(
+ cmdArgs,
+ jsonDefault: false,
+ allowNamedQuery: true,
+ validateDefaultSnippetLines: false,
+ validateDefaultMaxLineWidth: false);
if (TryWriteUnsupportedOptionError("symbols", cmdArgs, CliFlagSchema.GetAcceptedFlagNamesForCommand("symbols"), options.Query))
return CommandExitCodes.UsageError;
if (TryWriteInvalidKindFilterError(options, "symbols", KnownSymbolKindFilters))
@@ -1315,7 +1328,12 @@ public static int RunFiles(string[] cmdArgs, JsonSerializerOptions jsonOptions)
Console.Error.WriteLine(previewOptionError);
return CommandExitCodes.UsageError;
}
- var options = ParseArgs(cmdArgs, jsonDefault: false, allowNamedQuery: true);
+ var options = ParseArgs(
+ cmdArgs,
+ jsonDefault: false,
+ allowNamedQuery: true,
+ validateDefaultSnippetLines: false,
+ validateDefaultMaxLineWidth: false);
if (TryWriteUnsupportedOptionError("files", cmdArgs, CliFlagSchema.GetAcceptedFlagNamesForCommand("files"), options.Query))
return CommandExitCodes.UsageError;
if (TryWriteParseError(options, "files"))
@@ -1382,7 +1400,11 @@ public static int RunExcerpt(string[] cmdArgs, JsonSerializerOptions jsonOptions
Console.Error.WriteLine(previewOptionError);
return CommandExitCodes.UsageError;
}
- var options = ParseArgs(cmdArgs, jsonDefault: false);
+ var options = ParseArgs(
+ cmdArgs,
+ jsonDefault: false,
+ validateDefaultLimit: false,
+ validateDefaultSnippetLines: false);
if (TryWriteUnsupportedOptionError("excerpt", cmdArgs, CliFlagSchema.GetAcceptedFlagNamesForCommand("excerpt")))
return CommandExitCodes.UsageError;
if (TryWriteParseError(options, "excerpt"))
@@ -1503,7 +1525,11 @@ public static int RunFind(string[] cmdArgs, JsonSerializerOptions jsonOptions)
return CommandExitCodes.UsageError;
}
- var options = ParseArgs(preparedFindArgs, jsonDefault: false, allowNamedQuery: true);
+ var options = ParseArgs(
+ preparedFindArgs,
+ jsonDefault: false,
+ allowNamedQuery: true,
+ validateDefaultSnippetLines: false);
if (options.ParseError != null)
{
Console.Error.WriteLine(options.ParseError);
@@ -1752,7 +1778,11 @@ public static int RunMap(string[] cmdArgs, JsonSerializerOptions jsonOptions)
Console.Error.WriteLine(previewOptionError);
return CommandExitCodes.UsageError;
}
- var options = ParseArgs(cmdArgs, jsonDefault: false);
+ var options = ParseArgs(
+ cmdArgs,
+ jsonDefault: false,
+ validateDefaultSnippetLines: false,
+ validateDefaultMaxLineWidth: false);
if (TryWriteUnsupportedOptionError("map", cmdArgs, CliFlagSchema.GetAcceptedFlagNamesForCommand("map")))
return CommandExitCodes.UsageError;
if (TryWriteParseError(options, "map"))
@@ -1832,7 +1862,11 @@ public static int RunInspect(string[] cmdArgs, JsonSerializerOptions jsonOptions
Console.Error.WriteLine(previewOptionError);
return CommandExitCodes.UsageError;
}
- var options = ParseArgs(cmdArgs, jsonDefault: false, allowNamedQuery: true);
+ var options = ParseArgs(
+ cmdArgs,
+ jsonDefault: false,
+ allowNamedQuery: true,
+ validateDefaultSnippetLines: false);
if (TryWriteUnsupportedOptionError("inspect", cmdArgs, CliFlagSchema.GetAcceptedFlagNamesForCommand("inspect"), options.Query))
return CommandExitCodes.UsageError;
if (TryWriteParseError(options, "inspect"))
@@ -1956,7 +1990,12 @@ public static int RunOutline(string[] cmdArgs, JsonSerializerOptions jsonOptions
Console.Error.WriteLine(previewOptionError);
return CommandExitCodes.UsageError;
}
- var options = ParseArgs(cmdArgs[1..], jsonDefault: false);
+ var options = ParseArgs(
+ cmdArgs[1..],
+ jsonDefault: false,
+ validateDefaultLimit: false,
+ validateDefaultSnippetLines: false,
+ validateDefaultMaxLineWidth: false);
if (TryWriteUnsupportedOptionError("outline", cmdArgs[1..], CliFlagSchema.GetAcceptedFlagNamesForCommand("outline")))
return CommandExitCodes.UsageError;
if (TryWriteParseError(options, "outline"))
@@ -2130,13 +2169,30 @@ public static int RunStatus(string[] cmdArgs, JsonSerializerOptions jsonOptions,
Console.Error.WriteLine(previewOptionError);
return CommandExitCodes.UsageError;
}
- var options = ParseArgs(cmdArgs, jsonDefault: false, allowStatusCheck: true);
+ var options = ParseArgs(
+ cmdArgs,
+ jsonDefault: false,
+ allowStatusCheck: true,
+ validateDefaultLimit: false,
+ validateDefaultSnippetLines: false,
+ validateDefaultMaxLineWidth: false);
if (TryWriteUnsupportedOptionError("status", cmdArgs, CliFlagSchema.GetAcceptedFlagNamesForCommand("status")))
return CommandExitCodes.UsageError;
if (TryWriteParseError(options, "status"))
return CommandExitCodes.UsageError;
if (TryWriteUnexpectedPositionals("status", options))
return CommandExitCodes.UsageError;
+ if (options.StatusConfig)
+ {
+ if (options.CheckWorkspace || options.StatusLogPath || options.StatusExplainField != null)
+ {
+ Console.Error.WriteLine("Error: status --config cannot be combined with --check, --log-path, or --explain.");
+ return CommandExitCodes.UsageError;
+ }
+
+ Console.WriteLine(BuildEffectiveConfigJson(options, cmdArgs, appVersion).ToJsonString(jsonOptions));
+ return CommandExitCodes.Success;
+ }
if (options.StatusLogPath)
{
if (options.CheckWorkspace)
@@ -2376,9 +2432,90 @@ public static int RunStatus(string[] cmdArgs, JsonSerializerOptions jsonOptions,
});
}
+ private static JsonObject BuildEffectiveConfigJson(QueryCommandOptions options, string[] cmdArgs, string? appVersion)
+ {
+ JsonObject Entry(T? value, string source) => new()
+ {
+ ["value"] = JsonSerializer.SerializeToNode(value),
+ ["source"] = source,
+ };
+ var staleAfterEnvValue = Environment.GetEnvironmentVariable(StaleAfterEnvironmentVariable);
+
+ var payload = new JsonObject
+ {
+ ["api_version"] = "1",
+ ["effective_config"] = new JsonObject
+ {
+ ["db_path"] = Entry(options.DbPath, ResolveDbPathConfigSource(options)),
+ ["data_dir"] = Entry(options.DataDir, options.DataDirSource ?? "flag"),
+ ["limit"] = Entry(options.Limit, ResolveNumericConfigSource(cmdArgs, "--limit", "--top", DefaultLimitEnvironmentVariable)),
+ ["snippet_lines"] = Entry(options.SnippetLines, ResolveNumericConfigSource(cmdArgs, "--snippet-lines", null, DefaultSnippetLinesEnvironmentVariable)),
+ ["max_line_width"] = Entry(options.MaxLineWidth, ResolveNumericConfigSource(cmdArgs, "--max-line-width", null, DefaultMaxLineWidthEnvironmentVariable)),
+ ["json"] = Entry(options.Json, HasOption(cmdArgs, "--json") ? "flag" : "default"),
+ ["stale_after"] = Entry(options.StaleAfter?.ToString() ?? staleAfterEnvValue, options.StaleAfter.HasValue ? "flag" : ResolveEnvSource(StaleAfterEnvironmentVariable)),
+ ["global_tool_log_dir"] = Entry(GlobalToolLog.ResolveLogDirectoryForStatus(), ResolveEnvSource("CDIDX_GLOBAL_TOOL_LOG_DIR")),
+ ["version"] = Entry(appVersion ?? ConsoleUi.LoadVersion(), "build"),
+ },
+ };
+ return payload;
+ }
+
+ private static string ResolveDbPathConfigSource(QueryCommandOptions options)
+ {
+ if (options.DbPathExplicit)
+ return "flag";
+ return options.DataDirSource switch
+ {
+ DbPathResolver.DataDirSourceFlag => "flag",
+ DbPathResolver.DataDirSourceEnv => $"env:{DbPathResolver.DataDirEnvironmentVariable}",
+ DbPathResolver.DataDirSourceXdg => "env:XDG_DATA_HOME",
+ DbPathResolver.DataDirSourceWorkspace => "workspace",
+ _ => "default",
+ };
+ }
+
+ private static string ResolveNumericConfigSource(string[] args, string primaryFlag, string? aliasFlag, string envName)
+ {
+ if (HasOption(args, primaryFlag) || (aliasFlag != null && HasOption(args, aliasFlag)))
+ return "flag";
+ if (Environment.GetEnvironmentVariable(envName) is null)
+ return "default";
+ var configSource = Environment.GetEnvironmentVariable(CdidxConfigFile.ConfigSourceEnvironmentVariablePrefix + envName);
+ if (!string.IsNullOrWhiteSpace(configSource))
+ return $"config:{configSource}";
+ return $"env:{envName}";
+ }
+
+ private static string ResolveEnvSource(string envName)
+ {
+ if (Environment.GetEnvironmentVariable(envName) is null)
+ return "default";
+ var configSource = Environment.GetEnvironmentVariable(CdidxConfigFile.ConfigSourceEnvironmentVariablePrefix + envName);
+ if (!string.IsNullOrWhiteSpace(configSource))
+ return $"config:{configSource}";
+ return $"env:{envName}";
+ }
+
+ private static bool HasOption(string[] args, string optionName)
+ {
+ foreach (var arg in args)
+ {
+ if (string.Equals(arg, optionName, StringComparison.Ordinal))
+ return true;
+ if (arg.StartsWith(optionName + "=", StringComparison.Ordinal))
+ return true;
+ }
+ return false;
+ }
+
public static int RunVacuum(string[] cmdArgs, JsonSerializerOptions jsonOptions)
{
- var options = ParseArgs(cmdArgs, jsonDefault: false);
+ var options = ParseArgs(
+ cmdArgs,
+ jsonDefault: false,
+ validateDefaultLimit: false,
+ validateDefaultSnippetLines: false,
+ validateDefaultMaxLineWidth: false);
if (TryWriteUnsupportedOptionError("vacuum", cmdArgs, CliFlagSchema.GetAcceptedFlagNamesForCommand("vacuum")))
return CommandExitCodes.UsageError;
var explicitDbPathError = BuildExplicitDbPathParseError(options);
@@ -2764,7 +2901,11 @@ public static int RunDeps(string[] cmdArgs, JsonSerializerOptions jsonOptions)
Console.Error.WriteLine(previewOptionError);
return CommandExitCodes.UsageError;
}
- var options = ParseArgs(cmdArgs, jsonDefault: false);
+ var options = ParseArgs(
+ cmdArgs,
+ jsonDefault: false,
+ validateDefaultSnippetLines: false,
+ validateDefaultMaxLineWidth: false);
if (TryWriteUnsupportedOptionError("deps", cmdArgs, CliFlagSchema.GetAcceptedFlagNamesForCommand("deps")))
return CommandExitCodes.UsageError;
if (TryWriteParseError(options, "deps"))
@@ -2989,7 +3130,11 @@ public static int RunHotspots(string[] cmdArgs, JsonSerializerOptions jsonOption
Console.Error.WriteLine(previewOptionError);
return CommandExitCodes.UsageError;
}
- var options = ParseArgs(cmdArgs, jsonDefault: false);
+ var options = ParseArgs(
+ cmdArgs,
+ jsonDefault: false,
+ validateDefaultSnippetLines: false,
+ validateDefaultMaxLineWidth: false);
if (TryWriteUnsupportedOptionError("hotspots", cmdArgs, CliFlagSchema.GetAcceptedFlagNamesForCommand("hotspots")))
return CommandExitCodes.UsageError;
if (TryWriteParseError(options, "hotspots"))
@@ -3394,7 +3539,11 @@ public static int RunUnused(string[] cmdArgs, JsonSerializerOptions jsonOptions)
Console.Error.WriteLine(previewOptionError);
return CommandExitCodes.UsageError;
}
- var options = ParseArgs(cmdArgs, jsonDefault: false);
+ var options = ParseArgs(
+ cmdArgs,
+ jsonDefault: false,
+ validateDefaultSnippetLines: false,
+ validateDefaultMaxLineWidth: false);
if (TryWriteUnsupportedOptionError("unused", cmdArgs, CliFlagSchema.GetAcceptedFlagNamesForCommand("unused")))
return CommandExitCodes.UsageError;
if (TryWriteParseError(options, "unused"))
@@ -3662,7 +3811,12 @@ public static int RunValidate(string[] cmdArgs, JsonSerializerOptions jsonOption
Console.Error.WriteLine(previewOptionError);
return CommandExitCodes.UsageError;
}
- var options = ParseArgs(cmdArgs, jsonDefault: false);
+ var options = ParseArgs(
+ cmdArgs,
+ jsonDefault: false,
+ validateDefaultLimit: false,
+ validateDefaultSnippetLines: false,
+ validateDefaultMaxLineWidth: false);
if (TryWriteUnsupportedOptionError("validate", cmdArgs, CliFlagSchema.GetAcceptedFlagNamesForCommand("validate")))
return CommandExitCodes.UsageError;
if (TryWriteParseError(options, "validate"))
@@ -3718,7 +3872,12 @@ public static int RunValidate(string[] cmdArgs, JsonSerializerOptions jsonOption
public static int RunLanguages(string[] cmdArgs, JsonSerializerOptions jsonOptions)
{
- var options = ParseArgs(cmdArgs, jsonDefault: false);
+ var options = ParseArgs(
+ cmdArgs,
+ jsonDefault: false,
+ validateDefaultLimit: false,
+ validateDefaultSnippetLines: false,
+ validateDefaultMaxLineWidth: false);
if (TryWriteUnsupportedOptionError("languages", cmdArgs, CliFlagSchema.GetAcceptedFlagNamesForCommand("languages")))
return CommandExitCodes.UsageError;
if (TryWriteParseError(options, "languages"))
@@ -3792,13 +3951,20 @@ public static int RunLanguages(string[] cmdArgs, JsonSerializerOptions jsonOptio
return CommandExitCodes.Success;
}
- public static QueryCommandOptions ParseArgs(string[] args, bool jsonDefault, bool allowNamedQuery = false, bool allowStatusCheck = false)
+ public static QueryCommandOptions ParseArgs(
+ string[] args,
+ bool jsonDefault,
+ bool allowNamedQuery = false,
+ bool allowStatusCheck = false,
+ bool validateDefaultLimit = true,
+ bool validateDefaultSnippetLines = true,
+ bool validateDefaultMaxLineWidth = true)
{
string? dbPath = null;
string? dataDir = null;
bool? json = null;
string jsonOutputFormat = JsonOutputFormatNdjson;
- int limit = 20;
+ int limit = ResolveDefaultPositiveInt(DefaultLimitEnvironmentVariable, 20, "--limit", out var defaultLimitError);
string? lang = null;
string? kind = null;
string? query = null;
@@ -3813,9 +3979,9 @@ public static QueryCommandOptions ParseArgs(string[] args, bool jsonDefault, boo
int? focusLine = null;
int? focusColumn = null;
int focusLength = 1;
- int snippetLines = SearchSnippetFormatter.DefaultSnippetLines;
+ int snippetLines = ResolveDefaultPositiveInt(DefaultSnippetLinesEnvironmentVariable, SearchSnippetFormatter.DefaultSnippetLines, "--snippet-lines", out var defaultSnippetLinesError);
var snippetFocus = SearchSnippetFocusMode.Quality;
- int maxLineWidth = LineWidthFormatter.DefaultMaxLineWidth;
+ int maxLineWidth = ResolveDefaultNonNegativeInt(DefaultMaxLineWidthEnvironmentVariable, LineWidthFormatter.DefaultMaxLineWidth, "--max-line-width", out var defaultMaxLineWidthError);
bool contextAfterExplicit = false;
var pathPatterns = new List();
var userPathPatterns = new List();
@@ -3848,6 +4014,10 @@ public static QueryCommandOptions ParseArgs(string[] args, bool jsonDefault, boo
int? slowQueryMs = null;
string? statusExplainField = null;
bool statusLogPath = false;
+ bool statusConfig = false;
+ bool limitExplicit = false;
+ bool snippetLinesExplicit = false;
+ bool maxLineWidthExplicit = false;
var rankMode = ReferenceRankMode.Weighted;
var extraNames = new List();
bool impactDeprecatedDepthUsed = false;
@@ -3985,6 +4155,7 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue)
{
WarnIfDuplicateSingleValueOption("--limit", limitValue!);
limit = parsedLimit;
+ limitExplicit = true;
}
else
AddParseError(limitError!);
@@ -4210,6 +4381,16 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue)
AddParseError("Error: --log-path is not supported by this command.");
}
break;
+ case "--config":
+ if (allowStatusCheck)
+ {
+ statusConfig = true;
+ }
+ else
+ {
+ AddParseError("Error: --config is only supported by status.");
+ }
+ break;
case "--path":
if (TryReadStringOptionValue(args, ref i, "--path", inlineValue, allowSeparatedDashPrefixedLiteralValue: true, out var pathPattern, out var pathError))
{
@@ -4347,6 +4528,7 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue)
{
WarnIfDuplicateSingleValueOption("--snippet-lines", snippetLinesValue!);
snippetLines = parsedSnippetLines;
+ snippetLinesExplicit = true;
}
else
AddParseError(snippetLinesError!);
@@ -4373,6 +4555,7 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue)
{
WarnIfDuplicateSingleValueOption("--max-line-width", maxLineWidthValue!);
maxLineWidth = parsedMaxLineWidth;
+ maxLineWidthExplicit = true;
}
else
AddParseError(maxLineWidthError!);
@@ -4412,6 +4595,13 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue)
ValidateQueryPathOptionValues(userPathPatterns, excludePaths, AddParseError);
+ if (validateDefaultLimit && !limitExplicit && defaultLimitError != null)
+ AddParseError(defaultLimitError);
+ if (validateDefaultSnippetLines && !snippetLinesExplicit && defaultSnippetLinesError != null)
+ AddParseError(defaultSnippetLinesError);
+ if (validateDefaultMaxLineWidth && !maxLineWidthExplicit && defaultMaxLineWidthError != null)
+ AddParseError(defaultMaxLineWidthError);
+
var dbResolution = DbPathResolver.ResolveForQuery(Environment.CurrentDirectory, dbPath, dataDir);
return new QueryCommandOptions
@@ -4470,6 +4660,7 @@ void WarnIfDuplicateSingleValueOption(string canonicalName, string newValue)
SlowQueryMs = slowQueryMs,
StatusExplainField = statusExplainField,
StatusLogPath = statusLogPath,
+ StatusConfig = statusConfig,
RankMode = rankMode,
ExtraNames = extraNames,
ParseError = parseErrors == null ? null : string.Join(Environment.NewLine, parseErrors),
@@ -5096,6 +5287,8 @@ private static bool TryWriteParseError(QueryCommandOptions options, string comma
private static string? BuildExplicitDbPathParseError(QueryCommandOptions options)
{
+ if (options.StatusConfig)
+ return null;
if (!options.DbPathExplicit)
return null;
if (string.IsNullOrWhiteSpace(options.DbPath))
@@ -6622,6 +6815,44 @@ private static string BuildMissingOptionValueError(string optionName, params str
return sb.ToString();
}
+ private static int ResolveDefaultPositiveInt(string environmentVariable, int fallback, string optionName, out string? error)
+ {
+ var raw = Environment.GetEnvironmentVariable(environmentVariable);
+ if (string.IsNullOrWhiteSpace(raw))
+ {
+ error = null;
+ return fallback;
+ }
+
+ if (TryParsePositiveInt(raw, optionName, out var value, out var parseError))
+ {
+ error = null;
+ return value;
+ }
+
+ error = parseError!.Replace(optionName, environmentVariable, StringComparison.Ordinal);
+ return fallback;
+ }
+
+ private static int ResolveDefaultNonNegativeInt(string environmentVariable, int fallback, string optionName, out string? error)
+ {
+ var raw = Environment.GetEnvironmentVariable(environmentVariable);
+ if (string.IsNullOrWhiteSpace(raw))
+ {
+ error = null;
+ return fallback;
+ }
+
+ if (TryParseNonNegativeInt(raw, optionName, out var value, out var parseError))
+ {
+ error = null;
+ return value;
+ }
+
+ error = parseError!.Replace(optionName, environmentVariable, StringComparison.Ordinal);
+ return fallback;
+ }
+
private static bool TryParsePositiveInt(string rawValue, string optionName, out int value, out string? error)
{
if (string.Equals(optionName, "--max-line-width", StringComparison.Ordinal))
@@ -6935,6 +7166,7 @@ public sealed class QueryCommandOptions
public int? SlowQueryMs { get; init; }
public string? StatusExplainField { get; init; }
public bool StatusLogPath { get; init; }
+ public bool StatusConfig { get; init; }
public ReferenceRankMode RankMode { get; init; } = ReferenceRankMode.Weighted;
public List ExtraNames { get; init; } = [];
public string? ParseError { get; init; }
diff --git a/tests/CodeIndex.Tests/CdidxConfigFileTests.cs b/tests/CodeIndex.Tests/CdidxConfigFileTests.cs
index 871e01899e..031a3d1fa1 100644
--- a/tests/CodeIndex.Tests/CdidxConfigFileTests.cs
+++ b/tests/CodeIndex.Tests/CdidxConfigFileTests.cs
@@ -67,6 +67,65 @@ public void LoadAndApply_MaterializesKnownKeysIntoEnvironment()
finally { TestProjectHelper.DeleteDirectory(dir); }
}
+ [Fact]
+ public void LoadAndApply_ProjectConfigJsonMaterializesSearchDefaults()
+ {
+ var dir = CreateTempDir();
+ try
+ {
+ Directory.CreateDirectory(Path.Combine(dir, ".cdidx"));
+ Directory.CreateDirectory(Path.Combine(dir, "src"));
+ File.WriteAllText(Path.Combine(dir, ".cdidx", "config.json"), """
+ {
+ "$schema": "https://example.invalid/cdidx.schema.json",
+ "search": {
+ "limit": 41,
+ "snippet_lines": 5,
+ "max_line_width": 120
+ },
+ "output": { "format": "json", "locale": "en" },
+ "graph": { "max_hops": 4 },
+ "folding": { "fold_key_version": 1 }
+ }
+ """);
+
+ var env = new TestEnvironment();
+ var result = CdidxConfigFile.LoadAndApply(Path.Combine(dir, "src"), env.Read, env.Write);
+
+ Assert.True(result.Loaded);
+ Assert.EndsWith(Path.Combine(".cdidx", "config.json"), result.Path);
+ Assert.Equal("41", env.Writes[QueryCommandRunner.DefaultLimitEnvironmentVariable]);
+ Assert.Equal("5", env.Writes[QueryCommandRunner.DefaultSnippetLinesEnvironmentVariable]);
+ Assert.Equal("120", env.Writes[QueryCommandRunner.DefaultMaxLineWidthEnvironmentVariable]);
+ Assert.EndsWith(Path.Combine(".cdidx", "config.json"), env.Writes[CdidxConfigFile.ConfigSourceEnvironmentVariablePrefix + QueryCommandRunner.DefaultLimitEnvironmentVariable]);
+ }
+ finally { TestProjectHelper.DeleteDirectory(dir); }
+ }
+
+ [Theory]
+ [InlineData("""{ "search": { "limit": 0 } }""", "positive integer")]
+ [InlineData("""{ "search": { "snippet_lines": -1 } }""", "positive integer")]
+ [InlineData("""{ "search": { "max_line_width": -1 } }""", "non-negative integer")]
+ [InlineData("""{ "search": { "limit": 1.5 } }""", "positive integer")]
+ [InlineData("""{ "search": { "limit": 10001 } }""", "<= 10000")]
+ public void LoadAndApply_ProjectConfigJsonRejectsInvalidSearchDefaults(string json, string expectedError)
+ {
+ var dir = CreateTempDir();
+ try
+ {
+ Directory.CreateDirectory(Path.Combine(dir, ".cdidx"));
+ File.WriteAllText(Path.Combine(dir, ".cdidx", "config.json"), json);
+
+ var env = new TestEnvironment();
+ var result = CdidxConfigFile.LoadAndApply(dir, env.Read, env.Write);
+
+ Assert.True(result.Failed);
+ Assert.Contains(expectedError, result.Error);
+ Assert.Empty(env.Writes);
+ }
+ finally { TestProjectHelper.DeleteDirectory(dir); }
+ }
+
[Fact]
public void LoadAndApply_RealEnvVarWinsOverConfigFile()
{
diff --git a/tests/CodeIndex.Tests/DbPathResolverTests.cs b/tests/CodeIndex.Tests/DbPathResolverTests.cs
index 44ab2676ee..47cff4fbb8 100644
--- a/tests/CodeIndex.Tests/DbPathResolverTests.cs
+++ b/tests/CodeIndex.Tests/DbPathResolverTests.cs
@@ -76,6 +76,73 @@ public void ResolveDataDir_UsesStableXdgWorkspaceHashBeforeWorkspaceDefault()
Assert.Equal(DbPathResolver.DataDirSourceXdg, first.DataDirSource);
}
+ [Fact]
+ public void ResolveDataDirForQuery_WithXdgPrefersAncestorWorkspaceDataDir()
+ {
+ var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_xdg_root_db");
+ var xdgDir = Path.Combine(Path.GetTempPath(), $"cdidx_xdg_dir_{Guid.NewGuid():N}");
+ try
+ {
+ var child = Path.Combine(projectRoot, "src", "App");
+ Directory.CreateDirectory(child);
+ var indexedRootResolution = DbPathResolver.ResolveDataDir(projectRoot, explicitDataDir: null, environmentDataDir: null, xdgDataHome: xdgDir);
+ Directory.CreateDirectory(indexedRootResolution.DataDir!);
+
+ var resolved = DbPathResolver.ResolveDataDirForQuery(child, explicitDataDir: null, environmentDataDir: null, xdgDataHome: xdgDir);
+
+ Assert.Equal(indexedRootResolution.DbPath, resolved.DbPath);
+ Assert.Equal(indexedRootResolution.DataDir, resolved.DataDir);
+ Assert.Equal(DbPathResolver.DataDirSourceXdg, resolved.DataDirSource);
+ }
+ finally
+ {
+ TestProjectHelper.DeleteDirectory(projectRoot);
+ TestProjectHelper.DeleteDirectory(xdgDir);
+ }
+ }
+
+ [Fact]
+ public void ResolveDataDirForQuery_PrefersOutermostAncestorCdidx()
+ {
+ var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_root_db");
+ try
+ {
+ var child = Path.Combine(projectRoot, "src", "App");
+ Directory.CreateDirectory(child);
+ Directory.CreateDirectory(Path.Combine(projectRoot, ".cdidx"));
+ Directory.CreateDirectory(Path.Combine(projectRoot, "src", ".cdidx"));
+
+ var resolved = DbPathResolver.ResolveDataDirForQuery(child, explicitDataDir: null, environmentDataDir: null, xdgDataHome: null);
+
+ Assert.Equal(Path.Combine(projectRoot, ".cdidx", "codeindex.db"), resolved.DbPath);
+ Assert.Equal(DbPathResolver.DataDirSourceWorkspace, resolved.DataDirSource);
+ }
+ finally
+ {
+ TestProjectHelper.DeleteDirectory(projectRoot);
+ }
+ }
+
+ [Fact]
+ public void ResolveDataDirForQuery_FallsBackToCurrentDirectoryWhenNoAncestorCdidxExists()
+ {
+ var projectRoot = TestProjectHelper.CreateTempProject("cdidx_query_no_root_db");
+ try
+ {
+ var child = Path.Combine(projectRoot, "src", "App");
+ Directory.CreateDirectory(child);
+
+ var resolved = DbPathResolver.ResolveDataDirForQuery(child, explicitDataDir: null, environmentDataDir: null, xdgDataHome: null);
+
+ Assert.Equal(Path.Combine(child, ".cdidx", "codeindex.db"), resolved.DbPath);
+ Assert.Equal(DbPathResolver.DataDirSourceWorkspace, resolved.DataDirSource);
+ }
+ finally
+ {
+ TestProjectHelper.DeleteDirectory(projectRoot);
+ }
+ }
+
[Fact]
public void ResolveProjectRootForQuery_UsesParentOfCdidxDirectory()
{
diff --git a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs
index eed901a04b..42a8d74b35 100644
--- a/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs
+++ b/tests/CodeIndex.Tests/QueryCommandRunnerTests.cs
@@ -86,6 +86,143 @@ public void ParseArgs_AllowsZeroMaxLineWidth()
Assert.Equal(0, options.MaxLineWidth);
}
+ [Fact]
+ public void ParseArgs_UsesNumericDefaultEnvironmentVariables()
+ {
+ using var env = EnvironmentVariableScope.Capture(
+ QueryCommandRunner.DefaultLimitEnvironmentVariable,
+ QueryCommandRunner.DefaultSnippetLinesEnvironmentVariable,
+ QueryCommandRunner.DefaultMaxLineWidthEnvironmentVariable);
+ Environment.SetEnvironmentVariable(QueryCommandRunner.DefaultLimitEnvironmentVariable, "42");
+ Environment.SetEnvironmentVariable(QueryCommandRunner.DefaultSnippetLinesEnvironmentVariable, "6");
+ Environment.SetEnvironmentVariable(QueryCommandRunner.DefaultMaxLineWidthEnvironmentVariable, "120");
+
+ var options = QueryCommandRunner.ParseArgs(["RunSearch"], jsonDefault: false, allowNamedQuery: true);
+
+ Assert.Equal(42, options.Limit);
+ Assert.Equal(6, options.SnippetLines);
+ Assert.Equal(120, options.MaxLineWidth);
+ Assert.Null(options.ParseError);
+ }
+
+ [Fact]
+ public void ParseArgs_CliNumericFlagsOverrideDefaultEnvironmentVariables()
+ {
+ using var env = EnvironmentVariableScope.Capture(
+ QueryCommandRunner.DefaultLimitEnvironmentVariable,
+ QueryCommandRunner.DefaultSnippetLinesEnvironmentVariable,
+ QueryCommandRunner.DefaultMaxLineWidthEnvironmentVariable);
+ Environment.SetEnvironmentVariable(QueryCommandRunner.DefaultLimitEnvironmentVariable, "42");
+ Environment.SetEnvironmentVariable(QueryCommandRunner.DefaultSnippetLinesEnvironmentVariable, "6");
+ Environment.SetEnvironmentVariable(QueryCommandRunner.DefaultMaxLineWidthEnvironmentVariable, "120");
+
+ var options = QueryCommandRunner.ParseArgs(
+ ["RunSearch", "--limit", "7", "--snippet-lines", "3", "--max-line-width", "80"],
+ jsonDefault: false,
+ allowNamedQuery: true);
+
+ Assert.Equal(7, options.Limit);
+ Assert.Equal(3, options.SnippetLines);
+ Assert.Equal(80, options.MaxLineWidth);
+ Assert.Null(options.ParseError);
+ }
+
+ [Fact]
+ public void ParseArgs_InvalidNumericDefaultEnvironmentVariableReportsParseError()
+ {
+ using var env = EnvironmentVariableScope.Capture(QueryCommandRunner.DefaultLimitEnvironmentVariable);
+ Environment.SetEnvironmentVariable(QueryCommandRunner.DefaultLimitEnvironmentVariable, "0");
+
+ var options = QueryCommandRunner.ParseArgs(["RunSearch"], jsonDefault: false, allowNamedQuery: true);
+
+ Assert.Contains(QueryCommandRunner.DefaultLimitEnvironmentVariable, options.ParseError);
+ }
+
+ [Theory]
+ [InlineData("limit")]
+ [InlineData("snippet-lines")]
+ [InlineData("max-line-width")]
+ public void ParseArgs_CliNumericFlagsOverrideInvalidDefaultEnvironmentVariables(string option)
+ {
+ using var env = EnvironmentVariableScope.Capture(
+ QueryCommandRunner.DefaultLimitEnvironmentVariable,
+ QueryCommandRunner.DefaultSnippetLinesEnvironmentVariable,
+ QueryCommandRunner.DefaultMaxLineWidthEnvironmentVariable);
+ Environment.SetEnvironmentVariable(QueryCommandRunner.DefaultLimitEnvironmentVariable, "0");
+ Environment.SetEnvironmentVariable(QueryCommandRunner.DefaultSnippetLinesEnvironmentVariable, "0");
+ Environment.SetEnvironmentVariable(QueryCommandRunner.DefaultMaxLineWidthEnvironmentVariable, "-1");
+
+ var args = option switch
+ {
+ "limit" => new[] { "RunSearch", "--limit", "7", "--snippet-lines", "3", "--max-line-width", "80" },
+ "snippet-lines" => new[] { "RunSearch", "--limit", "7", "--snippet-lines", "3", "--max-line-width", "80" },
+ _ => new[] { "RunSearch", "--limit", "7", "--snippet-lines", "3", "--max-line-width", "80" },
+ };
+
+ var options = QueryCommandRunner.ParseArgs(args, jsonDefault: false, allowNamedQuery: true);
+
+ Assert.Equal(7, options.Limit);
+ Assert.Equal(3, options.SnippetLines);
+ Assert.Equal(80, options.MaxLineWidth);
+ Assert.Null(options.ParseError);
+ }
+
+ [Fact]
+ public void RunLanguages_IgnoresInvalidNumericDefaultEnvironmentVariables()
+ {
+ using var env = EnvironmentVariableScope.Capture(
+ QueryCommandRunner.DefaultLimitEnvironmentVariable,
+ QueryCommandRunner.DefaultSnippetLinesEnvironmentVariable,
+ QueryCommandRunner.DefaultMaxLineWidthEnvironmentVariable);
+ Environment.SetEnvironmentVariable(QueryCommandRunner.DefaultLimitEnvironmentVariable, "0");
+ Environment.SetEnvironmentVariable(QueryCommandRunner.DefaultSnippetLinesEnvironmentVariable, "0");
+ Environment.SetEnvironmentVariable(QueryCommandRunner.DefaultMaxLineWidthEnvironmentVariable, "-1");
+
+ var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunLanguages([], _jsonOptions));
+
+ Assert.Equal(CommandExitCodes.Success, exitCode);
+ Assert.Contains("Language", stdout);
+ Assert.DoesNotContain(QueryCommandRunner.DefaultLimitEnvironmentVariable, stderr);
+ }
+
+ [Fact]
+ public void ParseArgs_ScopesNumericDefaultValidationByOption()
+ {
+ using var env = EnvironmentVariableScope.Capture(
+ QueryCommandRunner.DefaultLimitEnvironmentVariable,
+ QueryCommandRunner.DefaultSnippetLinesEnvironmentVariable,
+ QueryCommandRunner.DefaultMaxLineWidthEnvironmentVariable);
+ Environment.SetEnvironmentVariable(QueryCommandRunner.DefaultLimitEnvironmentVariable, "0");
+ Environment.SetEnvironmentVariable(QueryCommandRunner.DefaultSnippetLinesEnvironmentVariable, "0");
+ Environment.SetEnvironmentVariable(QueryCommandRunner.DefaultMaxLineWidthEnvironmentVariable, "-1");
+
+ var limitOnly = QueryCommandRunner.ParseArgs(
+ [],
+ jsonDefault: false,
+ validateDefaultSnippetLines: false,
+ validateDefaultMaxLineWidth: false);
+ Assert.Contains(QueryCommandRunner.DefaultLimitEnvironmentVariable, limitOnly.ParseError);
+ Assert.DoesNotContain(QueryCommandRunner.DefaultSnippetLinesEnvironmentVariable, limitOnly.ParseError);
+ Assert.DoesNotContain(QueryCommandRunner.DefaultMaxLineWidthEnvironmentVariable, limitOnly.ParseError);
+
+ var maxLineWidthOnly = QueryCommandRunner.ParseArgs(
+ [],
+ jsonDefault: false,
+ validateDefaultLimit: false,
+ validateDefaultSnippetLines: false);
+ Assert.Contains(QueryCommandRunner.DefaultMaxLineWidthEnvironmentVariable, maxLineWidthOnly.ParseError);
+ Assert.DoesNotContain(QueryCommandRunner.DefaultLimitEnvironmentVariable, maxLineWidthOnly.ParseError);
+ Assert.DoesNotContain(QueryCommandRunner.DefaultSnippetLinesEnvironmentVariable, maxLineWidthOnly.ParseError);
+
+ var none = QueryCommandRunner.ParseArgs(
+ [],
+ jsonDefault: false,
+ validateDefaultLimit: false,
+ validateDefaultSnippetLines: false,
+ validateDefaultMaxLineWidth: false);
+ Assert.Null(none.ParseError);
+ }
+
[Fact]
public void ParseArgs_ProjectFilterExpandsSolutionProjectToPathGlob_Issue1707()
{
@@ -151,6 +288,80 @@ public void ParseArgs_StatusStaleAfterStoresDuration()
Assert.Equal(TimeSpan.FromHours(2), options.StaleAfter);
}
+ [Fact]
+ public void RunStatusConfig_PrintsEffectiveConfigWithoutOpeningDb()
+ {
+ using var env = EnvironmentVariableScope.Capture(
+ QueryCommandRunner.DefaultLimitEnvironmentVariable,
+ CdidxConfigFile.ConfigSourceEnvironmentVariablePrefix + QueryCommandRunner.DefaultLimitEnvironmentVariable);
+ Environment.SetEnvironmentVariable(QueryCommandRunner.DefaultLimitEnvironmentVariable, "33");
+ var missingDb = Path.Combine(Path.GetTempPath(), $"cdidx_missing_{Guid.NewGuid():N}.db");
+ var parsed = QueryCommandRunner.ParseArgs(["--config", "--db", missingDb, "--json"], jsonDefault: false, allowStatusCheck: true);
+ Assert.True(parsed.StatusConfig);
+
+ var (exitCode, stdout, stderr) = CaptureConsole(() => QueryCommandRunner.RunStatus(
+ ["--config", "--db", missingDb, "--json"],
+ _jsonOptions));
+
+ Assert.Equal(CommandExitCodes.Success, exitCode);
+ Assert.Equal(string.Empty, stderr);
+ using var document = ParseJsonOutput(stdout);
+ var effective = document.RootElement.GetProperty("effective_config");
+ Assert.Equal(missingDb, effective.GetProperty("db_path").GetProperty("value").GetString());
+ Assert.Equal("flag", effective.GetProperty("db_path").GetProperty("source").GetString());
+ Assert.Equal(33, effective.GetProperty("limit").GetProperty("value").GetInt32());
+ Assert.Equal($"env:{QueryCommandRunner.DefaultLimitEnvironmentVariable}", effective.GetProperty("limit").GetProperty("source").GetString());
+ }
+
+ [Fact]
+ public void RunStatusConfig_ReportsConfigFileSourceForSearchDefaults()
+ {
+ var projectRoot = TestProjectHelper.CreateTempProject("cdidx_status_config_source");
+ using var env = EnvironmentVariableScope.Capture(
+ QueryCommandRunner.DefaultLimitEnvironmentVariable,
+ QueryCommandRunner.StaleAfterEnvironmentVariable,
+ "CDIDX_GLOBAL_TOOL_LOG_DIR",
+ CdidxConfigFile.ConfigSourceEnvironmentVariablePrefix + QueryCommandRunner.DefaultLimitEnvironmentVariable,
+ CdidxConfigFile.ConfigSourceEnvironmentVariablePrefix + QueryCommandRunner.StaleAfterEnvironmentVariable,
+ CdidxConfigFile.ConfigSourceEnvironmentVariablePrefix + "CDIDX_GLOBAL_TOOL_LOG_DIR");
+ try
+ {
+ var configDir = Path.Combine(projectRoot, ".cdidx");
+ Directory.CreateDirectory(configDir);
+ var configPath = Path.Combine(configDir, "config.json");
+ var logDir = Path.Combine(projectRoot, "logs");
+ File.WriteAllText(configPath, $$"""
+ {
+ "search": { "limit": 44 },
+ "stale_after": "2h",
+ "global_tool_log_dir": {{JsonSerializer.Serialize(logDir)}}
+ }
+ """);
+
+ var (exitCode, stdout, stderr) = CaptureConsole(() => ProgramRunner.Run(
+ ["status", "--config", "--json"],
+ appVersion: "test-version",
+ configStartDirectory: projectRoot));
+
+ Assert.Equal(CommandExitCodes.Success, exitCode);
+ Assert.Equal(string.Empty, stderr);
+ using var document = ParseJsonOutput(stdout);
+ var limit = document.RootElement.GetProperty("effective_config").GetProperty("limit");
+ Assert.Equal(44, limit.GetProperty("value").GetInt32());
+ Assert.Equal($"config:{configPath}", limit.GetProperty("source").GetString());
+ var staleAfter = document.RootElement.GetProperty("effective_config").GetProperty("stale_after");
+ Assert.Equal("2h", staleAfter.GetProperty("value").GetString());
+ Assert.Equal($"config:{configPath}", staleAfter.GetProperty("source").GetString());
+ var logPath = document.RootElement.GetProperty("effective_config").GetProperty("global_tool_log_dir");
+ Assert.Equal(logDir, logPath.GetProperty("value").GetString());
+ Assert.Equal($"config:{configPath}", logPath.GetProperty("source").GetString());
+ }
+ finally
+ {
+ TestProjectHelper.DeleteDirectory(projectRoot);
+ }
+ }
+
[Fact]
public void RunStatusJson_ReportsSqlitePageMetrics_Issue1631()
{