Skip to content

Improve doctor output and local backend management#53

Merged
markcallen merged 5 commits intomainfrom
codex/doctor-report-all-backends
Mar 23, 2026
Merged

Improve doctor output and local backend management#53
markcallen merged 5 commits intomainfrom
codex/doctor-report-all-backends

Conversation

@markcallen
Copy link
Contributor

Summary

  • fix wrapper and backend doctor/help/version flows, including visible cross-language ballast doctor output
  • add .rulesrc.json ballastVersion support, doctor checks, wrapper install/refresh flows, and regression coverage across Go, Python, and TypeScript
  • ignore local .ballast/ and .ci/ workspace directories and include the remaining requested workspace updates

Testing

  • go test ./... in cli/ballast
  • pre-push checks passed for packages/ballast-typescript, packages/ballast-python, packages/ballast-go, and cli/ballast during git push

Copilot AI review requested due to automatic review settings March 23, 2026 09:03
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR improves the Ballast “doctor/help/version” UX across the Go wrapper and the TypeScript/Python/Go backends, adds persisted ballastVersion metadata to .rulesrc.json, and introduces local backend installation/refresh flows (with supporting docs and regression tests).

Changes:

  • Add doctor command across wrapper and backend CLIs, with formatted output and recommendations.
  • Persist and read ballastVersion in .rulesrc.json across TypeScript/Python/Go.
  • Add wrapper install-cli support for managing local backend binaries under .ballast/, plus docs/test updates and workspace ignore rules.

Reviewed changes

Copilot reviewed 21 out of 23 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
scripts/release-cross-language-check.sh Adds smoke checks ensuring --help, --version, and doctor execute successfully.
packages/ballast-typescript/src/version.ts Centralizes TypeScript CLI version export from package.json.
packages/ballast-typescript/src/install.ts Persists ballastVersion into saved config on install.
packages/ballast-typescript/src/install.test.ts Updates install expectations to include ballastVersion.
packages/ballast-typescript/src/doctor.ts Introduces TypeScript doctor report collection/formatting.
packages/ballast-typescript/src/doctor.test.ts Adds unit coverage for doctor recommendations and formatting.
packages/ballast-typescript/src/config.ts Adds optional ballastVersion parsing/merging into config load/save.
packages/ballast-typescript/src/config.test.ts Adds tests for reading/writing ballastVersion.
packages/ballast-typescript/src/cli.ts Adds doctor command, exports helpers for testing, and uses shared version constant.
packages/ballast-typescript/src/cli.test.ts Adds parseArgs coverage for help/version/doctor.
packages/ballast-python/uv.lock Bumps Python package version to 5.0.2.
packages/ballast-python/tests/test_cli.py Adds tests for doctor output and ballastVersion presence in config writes.
packages/ballast-python/ballast/cli.py Adds Python doctor functionality and persists ballastVersion in config.
packages/ballast-go/cmd/ballast-go/main_test.go Adds Go backend tests for help/version/doctor and config ballastVersion behavior.
packages/ballast-go/cmd/ballast-go/main.go Adds Go backend doctor command/reporting and ballastVersion field in config.
docs/installation.md Documents wrapper doctor/install-cli and refresh-config behavior.
cli/ballast/main_test.go Adds wrapper tests for doctor/install-cli/refresh-config and local backend execution paths.
cli/ballast/main.go Implements wrapper doctor/install-cli flows and local .ballast/ backend resolution/installation.
README.md Updates wrapper examples and documents wrapper command surface.
AGENTS.md Adds “Command Sync” guidelines to keep help/docs/tests aligned with CLI surface changes.
.rulesrc.json Adds ballastVersion to repo config.
.gitignore Ignores local .ballast/ and .ci/ workspace directories.
.claude/rules/common/local-dev-license.md Updates license guidance snippet and clarifies precedence with AGENTS.md.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +60 to +70
const pathCheck = spawnSync('bash', ['-lc', `command -v ${name}`], {
encoding: 'utf8'
});
if (pathCheck.status !== 0) {
return { name, version: null, path: null };
}
const cliPath = pathCheck.stdout.trim();
const versionCheck = spawnSync(name, ['--version'], { encoding: 'utf8' });
if (versionCheck.status !== 0) {
return { name, version: null, path: cliPath };
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

detectInstalledCli shells out to bash -lc "command -v ..." to locate executables. This hard-requires bash and won’t work on Windows or minimal environments where bash isn’t present. Prefer a cross-platform approach (e.g., attempt spawnSync(name, ['--version']) and treat ENOENT as not found, or use process.platform to choose where vs command -v, or a small PATH-search helper) so doctor works everywhere the TypeScript CLI supports.

Suggested change
const pathCheck = spawnSync('bash', ['-lc', `command -v ${name}`], {
encoding: 'utf8'
});
if (pathCheck.status !== 0) {
return { name, version: null, path: null };
}
const cliPath = pathCheck.stdout.trim();
const versionCheck = spawnSync(name, ['--version'], { encoding: 'utf8' });
if (versionCheck.status !== 0) {
return { name, version: null, path: cliPath };
}
const isWindows = process.platform === 'win32';
const pathArgs = isWindows ? [name] : ['-v', name];
const pathCmd = isWindows ? 'where' : 'command';
const pathCheck = spawnSync(pathCmd, pathArgs, { encoding: 'utf8' });
if (
pathCheck.error?.code === 'ENOENT' ||
pathCheck.status !== 0 ||
!pathCheck.stdout
) {
return { name, version: null, path: null };
}
const cliPath = pathCheck.stdout
.split(/\r?\n/)
.map((line) => line.trim())
.filter((line) => line.length > 0)[0] ?? null;
if (!cliPath) {
return { name, version: null, path: null };
}
const versionCheck = spawnSync(name, ['--version'], { encoding: 'utf8' });
if (
versionCheck.error?.code === 'ENOENT' ||
versionCheck.status !== 0 ||
!versionCheck.stdout
) {
return { name, version: null, path: cliPath };
}

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tracked separately in #61. I did not fold native Windows support into this PR because it needs a cross-platform executable lookup change and dedicated test coverage beyond the scope of the current doctor/install fixes.

Comment on lines +82 to +87
if (arg === '--version' || arg === '-v') {
return { version: true };
}
if (arg === 'doctor') {
return { doctor: true };
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseArgs returns { doctor: true } if it encounters the literal token doctor anywhere in argv, even when the actual command is install. That means ballast-typescript install ... doctor ... (or any future flag value equal to doctor) would unexpectedly run doctor instead of install. Consider only treating doctor as a top-level command when it is the first positional command (e.g., argv[2]), and keep the install option parser focused on install flags.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2dfadce. parseArgs now only returns { doctor: true } when doctor is the top-level command, and I added a regression test to confirm later install arguments with the literal value doctor are treated as normal install input.

Comment on lines +439 to +440
if item.Version != "" && compareVersions(item.Version, targetVersion) < 0 {
needsCLIFix = true
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In buildDoctorReport, an installed CLI whose --version check failed ends up with Path set but Version empty (shown as unknown). The current logic only marks needsCLIFix when the CLI is missing or when a known version is older, so it won’t recommend doctor --fix for the unknown version case. Align with the TypeScript/Python doctor behavior by treating an empty/unknown version as needing a CLI fix.

Suggested change
if item.Version != "" && compareVersions(item.Version, targetVersion) < 0 {
needsCLIFix = true
if item.Version == "" {
needsCLIFix = true
} else if compareVersions(item.Version, targetVersion) < 0 {
needsCLIFix = true

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2dfadce. Go doctor now treats an empty/unknown installed CLI version as needing repair, and I added a test covering the unknown-version recommendation path.

Comment on lines +54 to +69
installCommand: func(version string, projectRoot string) []string {
binDir := filepath.Join(projectRoot, ".ballast", "bin")
toolDir := filepath.Join(projectRoot, ".ballast", "tools", "python")
release := releaseVersion(version)
if release == "" {
return []string{"uv", "tool", "install", "--reinstall", "ballast-python"}
if sourceRoot := localSourceRoot(); sourceRoot != "" {
return []string{"env", "UV_TOOL_DIR=" + toolDir, "UV_TOOL_BIN_DIR=" + binDir, "uv", "tool", "install", "--reinstall", filepath.Join(sourceRoot, "packages", "ballast-python")}
}
release = releaseVersion(resolveVersion())
}
wheel := fmt.Sprintf(
"https://github.com/everydaydevopsio/ballast/releases/download/v%s/ballast_python-%s-py3-none-any.whl",
release,
release,
)
return []string{"uv", "tool", "install", "--reinstall", "--from", wheel, "ballast-python"}
return []string{"env", "UV_TOOL_DIR=" + toolDir, "UV_TOOL_BIN_DIR=" + binDir, "uv", "tool", "install", "--reinstall", "--from", wheel, "ballast-python"}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the Python backend installer, when version resolves to a non-release value (e.g. dev) and localSourceRoot() is empty, release stays empty and the code still constructs a GitHub wheel URL with v%s/%s, producing an invalid download URL. This can break ballast doctor --fix and ensureInstalled() for dev wrapper builds outside the ballast source tree. Handle the release == "" case explicitly (e.g., fall back to uv tool install --reinstall ballast-python / latest release, or return an error explaining that a release version is required when not running from source).

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2dfadce. The wrapper no longer constructs an invalid Python wheel URL for dev builds outside the ballast source tree. The installer interface now returns an explicit error, and the Python path reports that a release version or a ballast source checkout is required.

Comment on lines +471 to 495
func installCLIs(selectedLanguage language, version string) int {
root := findProjectRoot("")
var languagesToInstall []language
if selectedLanguage != "" {
languagesToInstall = []language{selectedLanguage}
} else {
languagesToInstall = slices.Clone(supportedLanguages)
}

for _, lang := range languagesToInstall {
tool, ok := toolsByLanguage[lang]
if !ok {
fmt.Printf("Unsupported language: %s\n", lang)
return 1
}
command := tool.installCommand(version, root)
if len(command) == 0 {
fmt.Printf("No installer configured for %s\n", lang)
return 1
}
if err := runCommandFunc(command[0], command[1:]); err != nil {
fmt.Printf("failed to install %s: %v\n", tool.binary, err)
return 1
}
}
Copy link

Copilot AI Mar 23, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

installCLIs runs installers that assume .ballast/bin (and sometimes .ballast/tools/...) already exists (notably go install with GOBIN=... will fail if the directory doesn’t exist). ensureInstalled creates .ballast/bin, but install-cli/doctor --fix call installCLIs directly and skip that setup. Create the required .ballast directories (at least .ballast/bin) before executing installer commands.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 2dfadce. installCLIs now creates the local .ballast/bin and .ballast/tools directories before invoking installers, and there is a wrapper test covering the directory creation path.

@markcallen markcallen merged commit 3f2f47c into main Mar 23, 2026
11 checks passed
@markcallen markcallen deleted the codex/doctor-report-all-backends branch March 23, 2026 19:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants