From 63423db49597b6f1f988276827dea055846822a3 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 8 Sep 2026 06:33:06 +0100 Subject: [PATCH 1/8] feat: implement Git environment diagnostic --- src/diagnostics/git.ts | 48 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 src/diagnostics/git.ts diff --git a/src/diagnostics/git.ts b/src/diagnostics/git.ts new file mode 100644 index 0000000..6d5a38a --- /dev/null +++ b/src/diagnostics/git.ts @@ -0,0 +1,48 @@ +import { runCommand, type RunCommand } from './process.js'; +import type { DiagnosticCheck, DiagnosticResult } from './types.js'; + +function parseGitVersion(output: string): string | null { + const match = /^git version (\d+\.\d+\.\d+)/i.exec(output.trim()); + return match?.[1] ?? null; +} + +export function createGitDiagnostic( + execute: RunCommand = runCommand, +): DiagnosticCheck { + return { + id: 'git', + label: 'Git', + run(): DiagnosticResult { + const result = execute('git', ['--version']); + + if (result.error || result.status !== 0) { + return { + id: 'git', + label: 'Git', + status: 'fail', + message: 'Git is unavailable or could not be executed.', + remediation: 'Install Git and ensure `git` is available on PATH.', + }; + } + + const version = parseGitVersion(result.stdout); + + if (!version) { + return { + id: 'git', + label: 'Git', + status: 'fail', + message: 'Git returned an unrecognized version.', + remediation: 'Verify `git --version` works and reinstall Git if necessary.', + }; + } + + return { + id: 'git', + label: 'Git', + status: 'pass', + message: `Version ${version} detected.`, + }; + }, + }; +} From 4f13c5bd9b434d3e240ace64307c355b2165153c Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 8 Sep 2026 06:33:22 +0100 Subject: [PATCH 2/8] feat: register Git diagnostic in doctor --- src/commands/doctor.ts | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index f9c14e6..eb72063 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -1,5 +1,5 @@ import { Command } from 'commander'; -import { EXIT_CODES } from '../errors/errors.js'; +import { createGitDiagnostic } from '../diagnostics/git.js'; import { runDiagnostics } from '../diagnostics/run.js'; import { createRuntimeDiagnostics } from '../diagnostics/runtime.js'; import type { @@ -7,6 +7,7 @@ import type { DiagnosticReport, DiagnosticResult, } from '../diagnostics/types.js'; +import { EXIT_CODES } from '../errors/errors.js'; import { TerminalOutput } from '../output/terminal.js'; export interface DoctorCommandOptions { @@ -19,6 +20,10 @@ function formatResult(result: DiagnosticResult): string { return `[${result.status.toUpperCase()}] ${result.label}: ${result.message}`; } +function createDefaultDiagnostics(): readonly DiagnosticCheck[] { + return [...createRuntimeDiagnostics(), createGitDiagnostic()]; +} + export function renderDiagnosticReport( report: DiagnosticReport, output: TerminalOutput, @@ -43,7 +48,7 @@ export function renderDiagnosticReport( export function createDoctorCommand( options: DoctorCommandOptions = {}, ): Command { - const checks = options.checks ?? createRuntimeDiagnostics(); + const checks = options.checks ?? createDefaultDiagnostics(); const output = options.output ?? new TerminalOutput(); const setExitCode = options.setExitCode ?? ((code) => (process.exitCode = code)); From 8fb63a83bd77b614ddc8ba904d99526048704cf1 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 8 Sep 2026 06:33:32 +0100 Subject: [PATCH 3/8] test: cover Git environment diagnostic --- tests/git-diagnostic.test.ts | 68 ++++++++++++++++++++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 tests/git-diagnostic.test.ts diff --git a/tests/git-diagnostic.test.ts b/tests/git-diagnostic.test.ts new file mode 100644 index 0000000..f5a7406 --- /dev/null +++ b/tests/git-diagnostic.test.ts @@ -0,0 +1,68 @@ +import { describe, expect, it } from 'vitest'; +import { createGitDiagnostic } from '../src/diagnostics/git.js'; +import type { RunCommand } from '../src/diagnostics/process.js'; + +function result( + status: number | null, + stdout = '', + stderr = '', + error?: Error, +) { + return { + status, + stdout, + stderr, + ...(error === undefined ? {} : { error }), + }; +} + +describe('Git diagnostic', () => { + it('executes git with an argument array and parses standard version output', () => { + const calls: Array<{ executable: string; args: readonly string[] }> = []; + const execute: RunCommand = (executable, args) => { + calls.push({ executable, args }); + return result(0, 'git version 2.47.1\n'); + }; + + const diagnostic = createGitDiagnostic(execute).run(); + + expect(calls).toEqual([{ executable: 'git', args: ['--version'] }]); + expect(diagnostic.status).toBe('pass'); + expect(diagnostic.message).toContain('2.47.1'); + }); + + it('accepts platform-suffixed Git version output', () => { + const execute: RunCommand = () => result(0, 'git version 2.47.1.windows.1\n'); + + const diagnostic = createGitDiagnostic(execute).run(); + + expect(diagnostic.status).toBe('pass'); + expect(diagnostic.message).toContain('2.47.1'); + }); + + it('fails when Git is unavailable without leaking process details', () => { + const execute: RunCommand = () => + result( + null, + '', + 'SECRET_TOKEN=do-not-render', + new Error('spawn git ENOENT SECRET_TOKEN=do-not-render'), + ); + + const diagnostic = createGitDiagnostic(execute).run(); + + expect(diagnostic.status).toBe('fail'); + expect(diagnostic.message).toBe('Git is unavailable or could not be executed.'); + expect(JSON.stringify(diagnostic)).not.toContain('SECRET_TOKEN'); + }); + + it('fails malformed Git version output with remediation guidance', () => { + const execute: RunCommand = () => result(0, 'git version unknown\n'); + + const diagnostic = createGitDiagnostic(execute).run(); + + expect(diagnostic.status).toBe('fail'); + expect(diagnostic.message).toBe('Git returned an unrecognized version.'); + expect(diagnostic.remediation).toContain('git --version'); + }); +}); From 2b6b8cb4d83f15a124cf3628e3dd18969e45d214 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 8 Sep 2026 06:33:53 +0100 Subject: [PATCH 4/8] test: include Git in default doctor integration --- tests/executable.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/executable.test.ts b/tests/executable.test.ts index 81c5a10..5e01ac1 100644 --- a/tests/executable.test.ts +++ b/tests/executable.test.ts @@ -48,14 +48,15 @@ describe('StellarForge CLI executable', () => { expect(result.stdout).toContain('-V, --version'); }); - it('runs the default runtime doctor checks successfully in the supported toolchain', () => { + it('runs the default doctor checks successfully in the supported toolchain', () => { const result = runBuiltCli(['doctor']); expect(result.status).toBe(0); expect(result.stderr).toBe(''); expect(result.stdout).toContain('[PASS] Node.js:'); expect(result.stdout).toContain('[PASS] npm:'); - expect(result.stdout).toContain('Summary: 2 passed, 0 warnings, 0 failed.'); + expect(result.stdout).toContain('[PASS] Git:'); + expect(result.stdout).toContain('Summary: 3 passed, 0 warnings, 0 failed.'); }); it('prints the package version from the built executable', () => { From 4859e7ffa728921310244d4cd18e75f0cbe765e9 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 8 Sep 2026 06:34:00 +0100 Subject: [PATCH 5/8] chore: add CLI-018 release intent --- .changeset/git-doctor-diagnostic.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/git-doctor-diagnostic.md diff --git a/.changeset/git-doctor-diagnostic.md b/.changeset/git-doctor-diagnostic.md new file mode 100644 index 0000000..630b826 --- /dev/null +++ b/.changeset/git-doctor-diagnostic.md @@ -0,0 +1,5 @@ +--- +"@stellarforge/cli": minor +--- + +Add Git availability and version detection to `stellarforge doctor`, including safe failure messages and remediation guidance. From 9f5bfd154241c559025531815e54d51fd187ac4d Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 8 Sep 2026 06:34:51 +0100 Subject: [PATCH 6/8] chore: apply canonical CLI-018 formatting --- .github/workflows/format-cli-018.yml | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/format-cli-018.yml diff --git a/.github/workflows/format-cli-018.yml b/.github/workflows/format-cli-018.yml new file mode 100644 index 0000000..c9fa486 --- /dev/null +++ b/.github/workflows/format-cli-018.yml @@ -0,0 +1,44 @@ +name: Format CLI-018 + +on: + push: + branches: + - feat/cli-018-git-diagnostic + paths: + - .github/workflows/format-cli-018.yml + +permissions: + contents: write + +jobs: + format: + if: github.repository == 'DigiNodes/stellarforge-cli' + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout maintainer branch + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Setup Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: '22.13.0' + package-manager-cache: false + + - name: Install from committed lockfile + run: npm ci --ignore-scripts --no-audit --no-fund + + - name: Apply repository Prettier + run: node node_modules/prettier/bin/prettier.cjs --write src/diagnostics/git.ts tests/git-diagnostic.test.ts + + - name: Commit formatting + run: | + git config user.name 'github-actions[bot]' + git config user.email '41898282+github-actions[bot]@users.noreply.github.com' + git add src/diagnostics/git.ts tests/git-diagnostic.test.ts + if git diff --cached --quiet; then + exit 0 + fi + git commit -m 'style: format Git diagnostic' + git push origin HEAD:feat/cli-018-git-diagnostic From 47cd2d428d3b455cb75610f8cc92576a24b30810 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 8 Sep 2026 05:35:05 +0000 Subject: [PATCH 7/8] style: format Git diagnostic --- src/diagnostics/git.ts | 3 ++- tests/git-diagnostic.test.ts | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/diagnostics/git.ts b/src/diagnostics/git.ts index 6d5a38a..0669300 100644 --- a/src/diagnostics/git.ts +++ b/src/diagnostics/git.ts @@ -33,7 +33,8 @@ export function createGitDiagnostic( label: 'Git', status: 'fail', message: 'Git returned an unrecognized version.', - remediation: 'Verify `git --version` works and reinstall Git if necessary.', + remediation: + 'Verify `git --version` works and reinstall Git if necessary.', }; } diff --git a/tests/git-diagnostic.test.ts b/tests/git-diagnostic.test.ts index f5a7406..94ffed4 100644 --- a/tests/git-diagnostic.test.ts +++ b/tests/git-diagnostic.test.ts @@ -32,7 +32,8 @@ describe('Git diagnostic', () => { }); it('accepts platform-suffixed Git version output', () => { - const execute: RunCommand = () => result(0, 'git version 2.47.1.windows.1\n'); + const execute: RunCommand = () => + result(0, 'git version 2.47.1.windows.1\n'); const diagnostic = createGitDiagnostic(execute).run(); @@ -52,7 +53,9 @@ describe('Git diagnostic', () => { const diagnostic = createGitDiagnostic(execute).run(); expect(diagnostic.status).toBe('fail'); - expect(diagnostic.message).toBe('Git is unavailable or could not be executed.'); + expect(diagnostic.message).toBe( + 'Git is unavailable or could not be executed.', + ); expect(JSON.stringify(diagnostic)).not.toContain('SECRET_TOKEN'); }); From 3347502ca47527c25708a6517adea07f63cb11e9 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 8 Sep 2026 06:35:25 +0100 Subject: [PATCH 8/8] chore: remove temporary CLI-018 formatter --- .github/workflows/format-cli-018.yml | 44 ---------------------------- 1 file changed, 44 deletions(-) delete mode 100644 .github/workflows/format-cli-018.yml diff --git a/.github/workflows/format-cli-018.yml b/.github/workflows/format-cli-018.yml deleted file mode 100644 index c9fa486..0000000 --- a/.github/workflows/format-cli-018.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Format CLI-018 - -on: - push: - branches: - - feat/cli-018-git-diagnostic - paths: - - .github/workflows/format-cli-018.yml - -permissions: - contents: write - -jobs: - format: - if: github.repository == 'DigiNodes/stellarforge-cli' - runs-on: ubuntu-latest - timeout-minutes: 10 - - steps: - - name: Checkout maintainer branch - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - - name: Setup Node.js - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 - with: - node-version: '22.13.0' - package-manager-cache: false - - - name: Install from committed lockfile - run: npm ci --ignore-scripts --no-audit --no-fund - - - name: Apply repository Prettier - run: node node_modules/prettier/bin/prettier.cjs --write src/diagnostics/git.ts tests/git-diagnostic.test.ts - - - name: Commit formatting - run: | - git config user.name 'github-actions[bot]' - git config user.email '41898282+github-actions[bot]@users.noreply.github.com' - git add src/diagnostics/git.ts tests/git-diagnostic.test.ts - if git diff --cached --quiet; then - exit 0 - fi - git commit -m 'style: format Git diagnostic' - git push origin HEAD:feat/cli-018-git-diagnostic