-
Notifications
You must be signed in to change notification settings - Fork 157
fix: unified system dependency check; start without Git Bash (#291) #305
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
fancyboi999
wants to merge
1
commit into
MoonshotAI:main
Choose a base branch
from
fancyboi999:fix/291-unified-system-dependency-check
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,10 @@ | ||
| --- | ||
| "@moonshot-ai/agent-core": minor | ||
| "@moonshot-ai/kaos": minor | ||
| "@moonshot-ai/kimi-code-sdk": minor | ||
| "@moonshot-ai/kimi-code": minor | ||
| --- | ||
|
|
||
| Add a unified system-dependency check for ripgrep, fd, and the shell. Kimi Code CLI now starts on Windows even when Git Bash is missing (the Bash tool is omitted and the model is told why, instead of the CLI crashing), warns at startup when `fd` is unavailable outside a git repository, and reports the health of all three external tools in `/status`. Dependency metadata lives in a single declarative registry so detection and messaging stay consistent. | ||
|
|
||
| BREAKING: the now-unused `KaosShellNotFoundError` (`@moonshot-ai/kaos`) and `ErrorCodes.SHELL_GIT_BASH_NOT_FOUND` (`@moonshot-ai/agent-core`, re-exported by `@moonshot-ai/kimi-code-sdk`) are removed, since a missing shell is no longer a hard failure. | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,129 @@ | ||
| /** | ||
| * System-dependency evaluation. | ||
| * | ||
| * `evaluateDependencies` is a pure function of an injected {@link DependencyProbe} | ||
| * so the resolution/warning logic can be unit-tested without spawning processes. | ||
| * `isRipgrepOnSystemPath` is the only side-effecting helper; the shell and fd | ||
| * facts are gathered by the caller (shell from `harness.getEnvironment()`, fd | ||
| * from the TUI's existing `detectFdPath()` result) so we never re-probe. | ||
| */ | ||
|
|
||
| import { spawnSync } from 'node:child_process'; | ||
|
|
||
| import type { Environment } from '@moonshot-ai/kimi-code-sdk'; | ||
|
|
||
| import { SYSTEM_DEPENDENCIES, type DependencyId, type SystemDependency } from './registry'; | ||
|
|
||
| /** Raw, already-gathered facts about the current environment. */ | ||
| export interface DependencyProbe { | ||
| readonly environment: Environment; | ||
| /** Result of the TUI's `detectFdPath()` (reused, not re-probed). */ | ||
| readonly fdAvailable: boolean; | ||
| /** Whether `rg` resolves on the system PATH right now. */ | ||
| readonly rgOnSystemPath: boolean; | ||
| /** Whether this platform/arch is one the ripgrep bootstrapper can download for. */ | ||
| readonly rgBootstrappable: boolean; | ||
| /** Whether the working directory is a git repository (fd fallback scope). */ | ||
| readonly isGitRepo: boolean; | ||
| } | ||
|
|
||
| export interface DependencyStatus { | ||
| readonly dependency: SystemDependency; | ||
| readonly available: boolean; | ||
| /** One-line, user-facing detail (resolved source, or why/how to fix). */ | ||
| readonly detail: string; | ||
| /** Whether this missing dependency warrants a startup warning right now. */ | ||
| readonly shouldWarnAtStartup: boolean; | ||
| } | ||
|
|
||
| /** Probe whether `rg` is resolvable on the system PATH (cheap, ~ms). */ | ||
| export function isRipgrepOnSystemPath(): boolean { | ||
| try { | ||
| return spawnSync('rg', ['--version'], { stdio: 'ignore' }).status === 0; | ||
| } catch { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Whether the ripgrep bootstrapper can download a prebuilt binary for this | ||
| * host. Mirrors `rg-locator`'s `detectTarget()` support matrix (darwin / linux | ||
| * / win32 on x64 / arm64); on anything else the auto-download throws, so a | ||
| * missing system `rg` is genuinely unavailable rather than self-healing. | ||
| * Keep in sync with packages/agent-core/src/tools/support/rg-locator.ts. | ||
| */ | ||
| export function isRipgrepBootstrapSupported(): boolean { | ||
| const platformOk = | ||
| process.platform === 'darwin' || | ||
| process.platform === 'linux' || | ||
| process.platform === 'win32'; | ||
| const archOk = process.arch === 'x64' || process.arch === 'arm64'; | ||
| return platformOk && archOk; | ||
| } | ||
|
|
||
| /** | ||
| * `available` means "the capability this dependency provides is currently | ||
| * satisfied" — directly, by auto-bootstrap, or by a fallback — NOT merely | ||
| * "the binary is on PATH". Kept uniform across dependencies so the `/status` | ||
| * marker never contradicts its own detail text. | ||
| */ | ||
| function isAvailable(id: DependencyId, probe: DependencyProbe): boolean { | ||
| switch (id) { | ||
| case 'ripgrep': | ||
| // On PATH now, or auto-downloadable on this platform — otherwise the | ||
| // first Grep call will fail, so it is genuinely unavailable. | ||
| return probe.rgOnSystemPath || probe.rgBootstrappable; | ||
| case 'fd': | ||
| // The binary, or the `git ls-files` fallback inside a git repository. | ||
| return probe.fdAvailable || probe.isGitRepo; | ||
| case 'shell': | ||
| return probe.environment.shellAvailable !== false; | ||
| } | ||
| } | ||
|
|
||
| function detailFor( | ||
| dep: SystemDependency, | ||
| available: boolean, | ||
| probe: DependencyProbe, | ||
| ): string { | ||
| switch (dep.id) { | ||
| case 'ripgrep': | ||
| if (probe.rgOnSystemPath) return 'Found on system PATH.'; | ||
| return probe.rgBootstrappable | ||
| ? 'Not on PATH — downloaded and cached on first use.' | ||
| : `Not on PATH and no prebuilt binary for this platform. ${dep.fixHint}`; | ||
| case 'fd': | ||
| if (probe.fdAvailable) return 'Found on system PATH.'; | ||
| return probe.isGitRepo | ||
| ? 'Not installed; using the `git ls-files` fallback in this git repository.' | ||
| : `Missing and not in a git repository. ${dep.fixHint}`; | ||
| case 'shell': | ||
| if (available) return `Using ${probe.environment.shellPath}.`; | ||
| return probe.environment.shellUnavailableReason ?? dep.fixHint; | ||
| } | ||
| } | ||
|
|
||
| function shouldWarn(dep: SystemDependency, available: boolean, probe: DependencyProbe): boolean { | ||
| if (available) return false; | ||
| switch (dep.startupWarning) { | ||
| case 'always': | ||
| return true; | ||
| case 'outside-git-repo': | ||
| return !probe.isGitRepo; | ||
| case 'never': | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| /** Pure evaluation of every registered dependency against the probe. */ | ||
| export function evaluateDependencies(probe: DependencyProbe): DependencyStatus[] { | ||
| return SYSTEM_DEPENDENCIES.map((dependency) => { | ||
| const available = isAvailable(dependency.id, probe); | ||
| return { | ||
| dependency, | ||
| available, | ||
| detail: detailFor(dependency, available, probe), | ||
| shouldWarnAtStartup: shouldWarn(dependency, available, probe), | ||
| }; | ||
| }); | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| /** | ||
| * System-dependency registry — the single source of truth for the external | ||
| * command-line tools Kimi Code CLI relies on (`rg`, `fd`, and a POSIX shell). | ||
| * | ||
| * Historically each dependency was probed, degraded, and described in its own | ||
| * corner of the codebase (ripgrep in `rg-locator`, fd in `fd-detect`, the | ||
| * shell in KAOS environment detection). That made "is X a dependency, and what | ||
| * happens when it is missing?" impossible to answer in one place. This module | ||
| * declares each dependency once — its purpose, whether it is required, whether | ||
| * the CLI can self-heal by downloading it, its graceful-degradation path, and | ||
| * when its absence should warn the user. `check.ts` and `report.ts` read from | ||
| * here so detection and messaging stay consistent and new dependencies are a | ||
| * one-line addition. | ||
| */ | ||
|
|
||
| export type DependencyId = 'ripgrep' | 'fd' | 'shell'; | ||
|
|
||
| export type DependencyRequirement = 'required' | 'optional'; | ||
|
|
||
| /** | ||
| * When a missing dependency should surface a startup warning: | ||
| * - `always` — whenever it is not available (shell: Bash tool | ||
| * dropped; ripgrep: not on PATH and no prebuilt | ||
| * binary for this platform). | ||
| * - `outside-git-repo` — only when its fallback is unavailable (fd: the | ||
| * `git ls-files` fallback only covers git repos). | ||
| * - `never` — never warn, even when missing. | ||
| */ | ||
| export type StartupWarningPolicy = 'always' | 'outside-git-repo' | 'never'; | ||
|
|
||
| export interface SystemDependency { | ||
| readonly id: DependencyId; | ||
| readonly displayName: string; | ||
| readonly purpose: string; | ||
| readonly requirement: DependencyRequirement; | ||
| /** Whether the CLI can fetch this binary on demand (only ripgrep, today). */ | ||
| readonly autoBootstrap: boolean; | ||
| /** Human note on the graceful-degradation path, if any. */ | ||
| readonly fallback?: string; | ||
| readonly startupWarning: StartupWarningPolicy; | ||
| /** Short, actionable install hint, aligned with `rgUnavailableMessage`. */ | ||
| readonly fixHint: string; | ||
| } | ||
|
|
||
| export const SYSTEM_DEPENDENCIES: readonly SystemDependency[] = [ | ||
| { | ||
| id: 'ripgrep', | ||
| displayName: 'ripgrep (rg)', | ||
| purpose: 'Powers the Grep tool and file-content search.', | ||
| requirement: 'required', | ||
| autoBootstrap: true, | ||
| // Self-heals on supported platforms (auto-download), so a warning only | ||
| // fires when it is neither on PATH nor downloadable for this platform. | ||
| startupWarning: 'always', | ||
| fixHint: | ||
| 'Install ripgrep: macOS `brew install ripgrep`, Ubuntu `sudo apt-get install ripgrep`, others https://github.com/BurntSushi/ripgrep#installation.', | ||
| }, | ||
| { | ||
| id: 'fd', | ||
| displayName: 'fd', | ||
| purpose: 'Cross-directory fuzzy file search for `@` mentions.', | ||
| requirement: 'optional', | ||
| autoBootstrap: false, | ||
| fallback: 'Inside a git repository, `git ls-files` still powers `@` completion.', | ||
| startupWarning: 'outside-git-repo', | ||
| fixHint: 'Install fd: macOS `brew install fd`, Ubuntu `sudo apt-get install fd-find`.', | ||
| }, | ||
| { | ||
| id: 'shell', | ||
| displayName: 'shell (Git Bash on Windows)', | ||
| purpose: 'Required by the Bash tool to run shell commands.', | ||
| requirement: 'required', | ||
| autoBootstrap: false, | ||
| fallback: 'The Bash tool is omitted; file, search, and planning tools still work.', | ||
| startupWarning: 'always', | ||
| fixHint: | ||
| 'Install Git for Windows from https://gitforwindows.org/ or set KIMI_SHELL_PATH to a bash.exe.', | ||
| }, | ||
| ]; | ||
|
|
||
| export function getDependency(id: DependencyId): SystemDependency { | ||
| const dep = SYSTEM_DEPENDENCIES.find((d) => d.id === id); | ||
| if (dep === undefined) { | ||
| throw new Error(`Unknown system dependency: ${id}`); | ||
| } | ||
| return dep; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| /** | ||
| * System-dependency rendering — turns evaluated {@link DependencyStatus} into | ||
| * (1) plain startup-warning strings and (2) a `/status` report section. Kept | ||
| * separate from `check.ts` so the formatting is pure and snapshot-friendly. | ||
| */ | ||
|
|
||
| import chalk from 'chalk'; | ||
|
|
||
| import type { ColorPalette } from '#/tui/theme/colors'; | ||
|
|
||
| import type { DependencyStatus } from './check'; | ||
|
|
||
| /** | ||
| * One concise warning line per missing dependency that matters right now | ||
| * (shell unavailable, or fd missing outside a git repo). Returned plain so the | ||
| * caller can route each through `showStatus(..., warning)`. | ||
| */ | ||
| export function startupDependencyWarnings(statuses: readonly DependencyStatus[]): string[] { | ||
| return statuses | ||
| .filter((status) => status.shouldWarnAtStartup) | ||
| .map((status) => `${status.dependency.displayName}: ${status.detail}`); | ||
| } | ||
|
|
||
| /** A "System dependencies" section for the `/status` report. */ | ||
| export function buildDependencyReportLines(options: { | ||
| readonly colors: ColorPalette; | ||
| readonly statuses: readonly DependencyStatus[]; | ||
| }): string[] { | ||
| const { colors, statuses } = options; | ||
| const accent = chalk.hex(colors.primary).bold; | ||
| const value = chalk.hex(colors.text); | ||
| const muted = chalk.hex(colors.textDim); | ||
|
|
||
| const labelWidth = Math.max(...statuses.map((s) => s.dependency.displayName.length)); | ||
| const lines: string[] = [accent('System dependencies')]; | ||
| for (const status of statuses) { | ||
| const marker = chalk.hex(status.available ? colors.success : colors.warning)('●'); | ||
| const label = value(status.dependency.displayName.padEnd(labelWidth, ' ')); | ||
| const tag = muted(`(${status.dependency.requirement})`); | ||
| lines.push(` ${marker} ${label} ${tag} ${muted(status.detail)}`); | ||
| } | ||
| return lines; | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.