Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 24 additions & 6 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,20 @@ jobs:
# 完全性を検証できる。
find all-artifacts -type f \( -name '*.tar.gz' -o -name '*.zip' -o -name '*.cdx.json' \) -exec cp {} release-files/ \;
cd release-files
expected_rids="linux-x64 linux-arm64 osx-arm64 win-x64 win-arm64"
for rid in $expected_rids; do
case "$rid" in
win-*) asset="CodeIndex-${rid}.zip" ;;
*) asset="CodeIndex-${rid}.tar.gz" ;;
esac
test -f "$asset" \
|| { echo "Missing release archive for ${rid}: ${asset}" >&2; ls -la >&2; exit 1; }
done
if compgen -G 'CodeIndex-osx-x64.*' >/dev/null; then
echo "Unexpected osx-x64 release archive found; update platform-support docs and installer guidance before publishing it." >&2
ls -la CodeIndex-osx-x64.* >&2
exit 1
fi
sha256sum * > sha256sums.txt

- name: Import release GPG key
Expand Down Expand Up @@ -674,8 +688,12 @@ jobs:
mkdir -p "$CDIDX_INSTALL_DIR"
bash install.sh "${TAG_NAME}"

# Runtime and license/policy assets must land next to the binary.
for asset in cdidx libe_sqlite3.so version.json LICENSE COMMERCIAL_LICENSE.md INTEGRATION_POLICY.md TRADEMARKS.md LICENSES; do
# This Ubuntu smoke test exercises the linux-x64 installer path. The
# release-files collection step above verifies that every published
# RID archive exists, including macOS/Windows assets.
binary_name="cdidx"
native_asset="libe_sqlite3.so"
for asset in "$binary_name" "$native_asset" version.json LICENSE COMMERCIAL_LICENSE.md INTEGRATION_POLICY.md TRADEMARKS.md LICENSES; do
test -e "$CDIDX_INSTALL_DIR/$asset" \
|| { echo "Missing $asset in $CDIDX_INSTALL_DIR" >&2; ls -la "$CDIDX_INSTALL_DIR" >&2; exit 1; }
done
Expand All @@ -690,7 +708,7 @@ jobs:
# instead of an exact match — a wrong version still fails because
# the suffix can only ever follow a space.
EXPECTED="cdidx ${TAG_NAME}"
ACTUAL="$("$CDIDX_INSTALL_DIR/cdidx" --version)"
ACTUAL="$("$CDIDX_INSTALL_DIR/$binary_name" --version)"
case "$ACTUAL" in
"$EXPECTED"|"$EXPECTED "*) ;;
*)
Expand All @@ -700,16 +718,16 @@ jobs:
esac

# A command that touches SQLite must not DllNotFoundException.
"$CDIDX_INSTALL_DIR/cdidx" . >/dev/null
"$CDIDX_INSTALL_DIR/cdidx" status >/dev/null
"$CDIDX_INSTALL_DIR/$binary_name" . >/dev/null
"$CDIDX_INSTALL_DIR/$binary_name" status >/dev/null

# CLI --json must work on the published trimmed self-contained release.
# Every CLI JSON DTO is covered by the source-generated serializer context.
json_temp_dir="${RUNNER_TEMP:?RUNNER_TEMP must be set by GitHub Actions}"
mkdir -p "$json_temp_dir"
json_stdout="$json_temp_dir/cdidx_json_stdout.txt"
json_stderr="$json_temp_dir/cdidx_json_stderr.txt"
if ! "$CDIDX_INSTALL_DIR/cdidx" status --json >"$json_stdout" 2>"$json_stderr"; then
if ! "$CDIDX_INSTALL_DIR/$binary_name" status --json >"$json_stdout" 2>"$json_stderr"; then
echo "Expected status --json to exit 0" >&2
cat "$json_stderr" >&2
exit 1
Expand Down
4 changes: 4 additions & 0 deletions DEVELOPER_GUIDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@ such as replacement characters, BOMs, NUL bytes, mixed line endings, UTF-16 BOMs
and likely non-UTF8 content. Keep its CLI usage, README entry, and help summary
in sync when adding validation issue kinds or filters.

`cdidx doctor` is the copy-pasteable environment summary for support requests.
Keep it redacted by default: secret-like `CDIDX_*` values must not be printed,
and new diagnostic fields should be stable enough for issue triage.

Generated shell completion scripts include a comment with the `cdidx` version
that produced them. When command or flag schema changes, update completion
tests and keep the README guidance that installed completions should be
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ downgrading `cdidx`.
| 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 <dir>`, `CDIDX_DATA_DIR`, or `XDG_DATA_HOME` can move default SQLite storage outside the workspace; explicit `--db <path>` 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 --config` prints effective configuration with source attribution, and `status --explain <field>` describes readiness fields and remediation. Read commands support `--profile`, `--slow-query-ms <n>`, and <code>--trace=stderr&#124;file&#124;none</code>; file traces write daily `query-trace-YYYYMMDD.jsonl` files next to the lifecycle log. |
| Diagnostics | `doctor` prints a redacted environment summary for bug reports. `status --config` prints effective configuration with source attribution, and `status --explain <field>` describes readiness fields and remediation. Read commands support `--profile`, `--slow-query-ms <n>`, and <code>--trace=stderr&#124;file&#124;none</code>; 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 <db1> <db2>` 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`. |
Expand Down
16 changes: 16 additions & 0 deletions changelog.d/unreleased/1449.security.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: security
issues:
- 1449
affected:
- install.sh
- tests/CodeIndex.Tests/InstallScriptTests.cs
---

## English

- **Installer staging directories are now private (#1449)** — `install.sh` now applies `0700` permissions immediately after creating its temporary staging directory so intermediate release artifacts are not world-readable under permissive umasks.

## 日本語

- **installer の staging directory を private にしました (#1449)** — `install.sh` は一時 staging directory 作成直後に `0700` 権限を適用し、緩い umask の環境でも中間 release artifact が他ユーザーから読めないようにしました。
16 changes: 16 additions & 0 deletions changelog.d/unreleased/1463.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
---
category: fixed
issues:
- 1463
affected:
- .github/workflows/release.yml
- tests/CodeIndex.Tests/ReleaseWorkflowTests.cs
---

## English

- **Release verification now checks RID-specific installed assets (#1463)** — the published-release install smoke test now derives the expected binary and native SQLite asset from the matrix RID instead of accepting a generic macOS native library name.

## 日本語

- **release 検証が RID 別の install asset を確認するようになりました (#1463)** — 公開済み release に対する install smoke test は、汎用的な macOS native library 名ではなく matrix RID から期待 binary と native SQLite asset を決めるようになりました。
19 changes: 19 additions & 0 deletions changelog.d/unreleased/1632.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
category: added
issues:
- 1632
affected:
- README.md
- DEVELOPER_GUIDE.md
- src/CodeIndex/Cli/ConsoleUi.cs
- src/CodeIndex/Cli/ProgramRunner.cs
- tests/CodeIndex.Tests/ProgramCliTests.cs
---

## English

- **Added `cdidx doctor` for support diagnostics (#1632)** — the CLI now prints a redacted environment summary with version, RID, OS/runtime, terminal state, DB/log path resolution, config hints, and safe `CDIDX_*` variables for bug reports.

## 日本語

- **support 診断用の `cdidx doctor` を追加しました (#1632)** — CLI が bug report 向けに、version / RID / OS・runtime / terminal 状態 / DB・log path 解決 / config ヒント / 安全な `CDIDX_*` 変数を含む redacted environment summary を出力するようになりました。
4 changes: 2 additions & 2 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -1086,10 +1086,10 @@ download_and_install() {
if ! stage_dir="$(mktemp -d "${INSTALL_DIR}/.cdidx-stage.XXXXXX")"; then
error "Failed to create staging directory under ${INSTALL_DIR}."
fi
STAGE_DIR_CLEANUP="$stage_dir"
if ! chmod 0700 "$stage_dir"; then
if ! chmod 700 "$stage_dir"; then
error "Failed to restrict staging directory permissions under ${INSTALL_DIR}."
fi
STAGE_DIR_CLEANUP="$stage_dir"

for asset in $required_files; do
cp "${extract_dir}/${asset}" "${stage_dir}/${asset}"
Expand Down
3 changes: 3 additions & 0 deletions src/CodeIndex/Cli/ConsoleUi.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ private static readonly (string Command, string Usage)[] CommandUsageLines =
("workspace", "cdidx workspace <list|status|use|current> [name] [--json]"),
("config", "cdidx config show [--json]"),
("validate-config", "cdidx validate-config"),
("doctor", "cdidx doctor"),
("db", "cdidx db --integrity-check|schema|prune [--dry-run|--apply] [--db <path>] [--json] | cdidx db checkpoint [name] [--db <path>] [--json] | cdidx db checkpoints --list [--db <path>] [--json] | cdidx db restore <name> [--db <path>] [--json]"),
("diff", "cdidx diff <db1> <db2> [--json] [--summary-only] [--detailed] [--limit <n>]"),
("report", "cdidx report --output <path> [--db <path>] [--json] [--log-lines <n>] [--no-log] [--include-args]"),
Expand Down Expand Up @@ -733,6 +734,7 @@ public static void PrintUsageBrief(bool showBanner = true)
Console.WriteLine(" map Show a repo-level overview for AI orientation");
Console.WriteLine(" inspect <query> Bundle definition, graph, and nearby symbol context");
Console.WriteLine(" status Show database statistics, freshness, config, and logs");
Console.WriteLine(" doctor Print a redacted environment summary for bug reports");
Console.WriteLine(" validate Report encoding issues (U+FFFD, BOM, null bytes, mixed line endings, UTF-16 BOM, likely non-UTF8)");
Console.WriteLine(" impact <query> Show transitive callers; type queries may return heuristic file-level dependency hints");
Console.WriteLine(" deps Show file-level dependency edges from the reference graph");
Expand Down Expand Up @@ -835,6 +837,7 @@ private static void PrintCommandSummary()
Console.WriteLine(" config show Show resolved workspace config and precedence");
Console.WriteLine(" upgrade Check for and install the latest release via install.sh");
Console.WriteLine(" validate-config Validate .cdidx/config.json or .cdidxrc.json");
Console.WriteLine(" doctor Print a redacted environment summary for bug reports");
Console.WriteLine(" db --integrity-check Run SQLite `PRAGMA integrity_check` and report findings");
Console.WriteLine(" db schema Dump SQLite schema entries and PRAGMA user_version");
Console.WriteLine(" db prune --dry-run|--apply Count or delete orphaned DB rows");
Expand Down
82 changes: 82 additions & 0 deletions src/CodeIndex/Cli/ProgramRunner.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System.Diagnostics;
using System.Globalization;
using System.Net;
using System.Runtime.InteropServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Nodes;
Expand Down Expand Up @@ -175,6 +176,14 @@ internal static int Run(
return CommandExitCodes.Success;
}

if (args[0] == "doctor")
{
var doctorExitCode = RunDoctor(args[1..], appVersion);
GlobalToolLog.Info($"command_complete exit_code={doctorExitCode} command=doctor");
EmitCommandMetric("doctor", args, commandStartTimestamp, commandStopwatch, doctorExitCode);
return doctorExitCode;
}

var easterEgg = args.FirstOrDefault(a => a is "--sushi" or "--coffee" or "--ramen" or "--wine" or "--beer" or "--matcha" or "--whisky");
if (easterEgg != null && !args.Any(a => !a.StartsWith('-')))
{
Expand Down Expand Up @@ -404,6 +413,79 @@ private static bool JsonEquivalent(string expected, string actual)
return JsonSerializer.Serialize(expectedDoc.RootElement) == JsonSerializer.Serialize(actualDoc.RootElement);
}

private static int RunDoctor(string[] args, string appVersion)
{
if (args.Length > 0)
return CommandErrorWriter.Write($"Unknown doctor argument: {args[0]}", CommandExitCodes.InvalidArgument, "use `cdidx doctor`.");

var dbResolution = DbPathResolver.ResolveForQuery(Environment.CurrentDirectory, explicitDbPath: null, explicitDataDir: null);
Console.WriteLine("cdidx doctor");
Console.WriteLine(ConsoleUi.FormatSummaryLine("version", appVersion));
Console.WriteLine(ConsoleUi.FormatSummaryLine("commit", ConsoleUi.LoadBuildMetadata().Commit));
Console.WriteLine(ConsoleUi.FormatSummaryLine("rid", RuntimeInformation.RuntimeIdentifier));
Console.WriteLine(ConsoleUi.FormatSummaryLine("os", RuntimeInformation.OSDescription));
Console.WriteLine(ConsoleUi.FormatSummaryLine("kernel", Environment.OSVersion.VersionString));
Console.WriteLine(ConsoleUi.FormatSummaryLine("dotnet", RuntimeInformation.FrameworkDescription));
Console.WriteLine(ConsoleUi.FormatSummaryLine("process", Environment.ProcessPath ?? "<unknown>"));
Console.WriteLine(ConsoleUi.FormatSummaryLine("base_dir", AppContext.BaseDirectory));
Console.WriteLine(ConsoleUi.FormatSummaryLine("cwd", Environment.CurrentDirectory));
Console.WriteLine();
Console.WriteLine("terminal:");
Console.WriteLine(ConsoleUi.FormatSummaryLine("stdout_tty", !Console.IsOutputRedirected, indent: " "));
Console.WriteLine(ConsoleUi.FormatSummaryLine("stderr_tty", !Console.IsErrorRedirected, indent: " "));
Console.WriteLine(ConsoleUi.FormatSummaryLine("columns", Environment.GetEnvironmentVariable("COLUMNS") ?? "<unset>", indent: " "));
Console.WriteLine(ConsoleUi.FormatSummaryLine("no_color", Environment.GetEnvironmentVariable("NO_COLOR") ?? "<unset>", indent: " "));
Console.WriteLine(ConsoleUi.FormatSummaryLine("term", Environment.GetEnvironmentVariable("TERM") ?? "<unset>", indent: " "));
Console.WriteLine(ConsoleUi.FormatSummaryLine("locale", CultureInfo.CurrentCulture.Name, indent: " "));
Console.WriteLine(ConsoleUi.FormatSummaryLine("ui_locale", CultureInfo.CurrentUICulture.Name, indent: " "));
Console.WriteLine();
Console.WriteLine("paths:");
Console.WriteLine(ConsoleUi.FormatSummaryLine("db", dbResolution.DbPath, indent: " "));
Console.WriteLine(ConsoleUi.FormatSummaryLine("data_dir", dbResolution.DataDir ?? "<explicit-db>", indent: " "));
Console.WriteLine(ConsoleUi.FormatSummaryLine("data_source", dbResolution.DataDirSource ?? "explicit-db", indent: " "));
Console.WriteLine(ConsoleUi.FormatSummaryLine("log_dir", GlobalToolLog.ResolveLogDirectoryForStatus(), indent: " "));
Console.WriteLine();
Console.WriteLine("config:");
Console.WriteLine(ConsoleUi.FormatSummaryLine(CdidxConfigFile.FileName, File.Exists(Path.Combine(Environment.CurrentDirectory, CdidxConfigFile.FileName)) ? "present" : "not found", indent: " "));
Console.WriteLine(ConsoleUi.FormatSummaryLine(CdidxConfigFile.DisableEnvVar, Environment.GetEnvironmentVariable(CdidxConfigFile.DisableEnvVar) ?? "<unset>", indent: " "));
Console.WriteLine();
Console.WriteLine("cdidx_env:");
foreach (var (key, value) in EnumerateCdidxEnvironment())
Console.WriteLine(ConsoleUi.FormatSummaryLine(key, value, indent: " "));
return CommandExitCodes.Success;
}

private static IEnumerable<(string Key, string Value)> EnumerateCdidxEnvironment()
{
var rows = Environment.GetEnvironmentVariables()
.Cast<System.Collections.DictionaryEntry>()
.Select(e => (Key: e.Key?.ToString() ?? string.Empty, Value: e.Value?.ToString() ?? string.Empty))
.Where(e => e.Key.StartsWith("CDIDX_", StringComparison.Ordinal))
.OrderBy(e => e.Key, StringComparer.Ordinal);
var any = false;
foreach (var row in rows)
{
any = true;
yield return (row.Key, IsSensitiveEnvironmentName(row.Key) ? "<redacted>" : string.IsNullOrEmpty(row.Value) ? "<empty>" : row.Value);
}

if (!any)
yield return ("<none>", "");
}

private static bool IsSensitiveEnvironmentName(string name) =>
name.Contains("TOKEN", StringComparison.OrdinalIgnoreCase)
|| name.Contains("PASSWORD", StringComparison.OrdinalIgnoreCase)
|| name.Contains("PASSWD", StringComparison.OrdinalIgnoreCase)
|| name.Contains("PWD", StringComparison.OrdinalIgnoreCase)
|| name.Contains("SECRET", StringComparison.OrdinalIgnoreCase)
|| name.Contains("AUTH", StringComparison.OrdinalIgnoreCase)
|| name.Contains("APIKEY", StringComparison.OrdinalIgnoreCase)
|| name.Contains("API_KEY", StringComparison.OrdinalIgnoreCase)
|| name.Contains("PRIVATE_KEY", StringComparison.OrdinalIgnoreCase)
|| name.EndsWith("_KEY", StringComparison.OrdinalIgnoreCase)
|| name.Contains("CREDENTIAL", StringComparison.OrdinalIgnoreCase);

internal static void EnsureRedirectedStdoutUsesUtf8()
{
if (!Console.IsOutputRedirected || Console.Out is StringWriter || Console.Out.GetType().Assembly != typeof(Console).Assembly)
Expand Down
13 changes: 13 additions & 0 deletions tests/CodeIndex.Tests/InstallScriptTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,19 @@ public void Uninstall_RemovesInstalledPayloadAndLeavesProjectData()
Assert.False(Directory.Exists(Path.Combine(installDir, "LICENSES")));
}

[Fact]
public void DownloadAndInstall_SecuresStageDirectoryAfterMktemp()
{
var script = File.ReadAllText(Path.Combine(GetRepositoryRoot(), "install.sh"));
var mktempIndex = script.IndexOf("stage_dir=\"$(mktemp -d \"${INSTALL_DIR}/.cdidx-stage.XXXXXX\")\"", StringComparison.Ordinal);
var chmodIndex = script.IndexOf("chmod 700 \"$stage_dir\"", StringComparison.Ordinal);
var cleanupIndex = script.IndexOf("STAGE_DIR_CLEANUP=\"$stage_dir\"", StringComparison.Ordinal);

Assert.True(mktempIndex >= 0);
Assert.True(chmodIndex > mktempIndex);
Assert.True(cleanupIndex > chmodIndex);
}

[Theory]
[InlineData("linux", "x64", "linux-x64", "libe_sqlite3.so")]
[InlineData("osx", "arm64", "osx-arm64", "libe_sqlite3.dylib")]
Expand Down
Loading
Loading