From 0217a36c7ca64e0215750af14684cb809e00d35b Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 8 Sep 2026 06:37:29 +0100 Subject: [PATCH 1/8] feat: implement Rust and Cargo diagnostics --- src/diagnostics/rust.ts | 108 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 src/diagnostics/rust.ts diff --git a/src/diagnostics/rust.ts b/src/diagnostics/rust.ts new file mode 100644 index 0000000..b2f21c6 --- /dev/null +++ b/src/diagnostics/rust.ts @@ -0,0 +1,108 @@ +import { runCommand, type RunCommand } from './process.js'; +import type { DiagnosticCheck, DiagnosticResult } from './types.js'; +import { isAtLeast, parseNumericVersion } from './version.js'; + +const MINIMUM_RUST = { major: 1, minor: 84, patch: 0 } as const; + +function parseToolVersion(output: string, tool: 'rustc' | 'cargo'): string | null { + const match = new RegExp(`^${tool} (\\d+\\.\\d+\\.\\d+)`, 'i').exec( + output.trim(), + ); + return match?.[1] ?? null; +} + +export function createRustDiagnostic( + execute: RunCommand = runCommand, +): DiagnosticCheck { + return { + id: 'rust', + label: 'Rust', + run(): DiagnosticResult { + const result = execute('rustc', ['--version']); + + if (result.error || result.status !== 0) { + return { + id: 'rust', + label: 'Rust', + status: 'fail', + message: 'Rust is unavailable or could not be executed.', + remediation: + 'Install Rust >=1.84.0 and ensure `rustc` is available on PATH.', + }; + } + + const version = parseToolVersion(result.stdout, 'rustc'); + const parsed = version ? parseNumericVersion(version) : null; + + if (!version || !parsed) { + return { + id: 'rust', + label: 'Rust', + status: 'fail', + message: 'Rust returned an unrecognized version.', + remediation: 'Verify `rustc --version` works and reinstall Rust if necessary.', + }; + } + + if (!isAtLeast(parsed, MINIMUM_RUST)) { + return { + id: 'rust', + label: 'Rust', + status: 'fail', + message: `Unsupported Rust version ${version}.`, + remediation: + 'Upgrade to Rust >=1.84.0 before building Stellar smart contracts.', + }; + } + + return { + id: 'rust', + label: 'Rust', + status: 'pass', + message: `Supported version ${version} detected.`, + }; + }, + }; +} + +export function createCargoDiagnostic( + execute: RunCommand = runCommand, +): DiagnosticCheck { + return { + id: 'cargo', + label: 'Cargo', + run(): DiagnosticResult { + const result = execute('cargo', ['--version']); + + if (result.error || result.status !== 0) { + return { + id: 'cargo', + label: 'Cargo', + status: 'fail', + message: 'Cargo is unavailable or could not be executed.', + remediation: + 'Install Cargo with the Rust toolchain and ensure `cargo` is available on PATH.', + }; + } + + const version = parseToolVersion(result.stdout, 'cargo'); + + if (!version || !parseNumericVersion(version)) { + return { + id: 'cargo', + label: 'Cargo', + status: 'fail', + message: 'Cargo returned an unrecognized version.', + remediation: 'Verify `cargo --version` works and reinstall Rust if necessary.', + }; + } + + return { + id: 'cargo', + label: 'Cargo', + status: 'pass', + message: `Version ${version} detected.`, + }; + }, + }; +} From 7d07637cb705f961ffa192fead6b161d9b6aa5a1 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 8 Sep 2026 06:37:45 +0100 Subject: [PATCH 2/8] feat: register Rust and Cargo diagnostics --- src/commands/doctor.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/commands/doctor.ts b/src/commands/doctor.ts index eb72063..ece8c12 100644 --- a/src/commands/doctor.ts +++ b/src/commands/doctor.ts @@ -1,6 +1,10 @@ import { Command } from 'commander'; import { createGitDiagnostic } from '../diagnostics/git.js'; import { runDiagnostics } from '../diagnostics/run.js'; +import { + createCargoDiagnostic, + createRustDiagnostic, +} from '../diagnostics/rust.js'; import { createRuntimeDiagnostics } from '../diagnostics/runtime.js'; import type { DiagnosticCheck, @@ -21,7 +25,12 @@ function formatResult(result: DiagnosticResult): string { } function createDefaultDiagnostics(): readonly DiagnosticCheck[] { - return [...createRuntimeDiagnostics(), createGitDiagnostic()]; + return [ + ...createRuntimeDiagnostics(), + createGitDiagnostic(), + createRustDiagnostic(), + createCargoDiagnostic(), + ]; } export function renderDiagnosticReport( From b371ba319db25264e12b6f0fca997840cf4031df Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 8 Sep 2026 06:37:58 +0100 Subject: [PATCH 3/8] test: cover Rust and Cargo diagnostics --- tests/rust-diagnostics.test.ts | 102 +++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 tests/rust-diagnostics.test.ts diff --git a/tests/rust-diagnostics.test.ts b/tests/rust-diagnostics.test.ts new file mode 100644 index 0000000..76b4e81 --- /dev/null +++ b/tests/rust-diagnostics.test.ts @@ -0,0 +1,102 @@ +import { describe, expect, it } from 'vitest'; +import { + createCargoDiagnostic, + createRustDiagnostic, +} from '../src/diagnostics/rust.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('Rust diagnostic', () => { + it('passes Rust 1.84.0 and newer', () => { + const execute: RunCommand = () => + result(0, 'rustc 1.84.0 (9fc6b4312 2025-01-07)\n'); + + const diagnostic = createRustDiagnostic(execute).run(); + + expect(diagnostic.status).toBe('pass'); + expect(diagnostic.message).toContain('1.84.0'); + }); + + it('fails Rust below the Stellar smart-contract minimum', () => { + const execute: RunCommand = () => + result(0, 'rustc 1.83.0 (90b35a623 2024-11-26)\n'); + + const diagnostic = createRustDiagnostic(execute).run(); + + expect(diagnostic.status).toBe('fail'); + expect(diagnostic.remediation).toContain('>=1.84.0'); + }); + + it('fails when rustc is unavailable without leaking process details', () => { + const execute: RunCommand = () => + result( + null, + '', + 'SECRET_TOKEN=do-not-render', + new Error('spawn rustc ENOENT SECRET_TOKEN=do-not-render'), + ); + + const diagnostic = createRustDiagnostic(execute).run(); + + expect(diagnostic.status).toBe('fail'); + expect(diagnostic.message).toBe('Rust is unavailable or could not be executed.'); + expect(JSON.stringify(diagnostic)).not.toContain('SECRET_TOKEN'); + }); + + it('fails malformed rustc output', () => { + const execute: RunCommand = () => result(0, 'rustc unknown\n'); + + const diagnostic = createRustDiagnostic(execute).run(); + + expect(diagnostic.status).toBe('fail'); + expect(diagnostic.message).toBe('Rust returned an unrecognized version.'); + }); +}); + +describe('Cargo diagnostic', () => { + it('checks Cargo independently and parses its version', () => { + const calls: Array<{ executable: string; args: readonly string[] }> = []; + const execute: RunCommand = (executable, args) => { + calls.push({ executable, args }); + return result(0, 'cargo 1.89.0 (c24e10642 2025-06-23)\n'); + }; + + const diagnostic = createCargoDiagnostic(execute).run(); + + expect(calls).toEqual([{ executable: 'cargo', args: ['--version'] }]); + expect(diagnostic.status).toBe('pass'); + expect(diagnostic.message).toContain('1.89.0'); + }); + + it('fails when Cargo is unavailable without affecting Rust checks', () => { + const execute: RunCommand = () => + result(null, '', '', new Error('spawn cargo ENOENT')); + + const diagnostic = createCargoDiagnostic(execute).run(); + + expect(diagnostic.status).toBe('fail'); + expect(diagnostic.remediation).toContain('cargo'); + }); + + it('fails malformed Cargo output', () => { + const execute: RunCommand = () => result(0, 'cargo unknown\n'); + + const diagnostic = createCargoDiagnostic(execute).run(); + + expect(diagnostic.status).toBe('fail'); + expect(diagnostic.message).toBe('Cargo returned an unrecognized version.'); + }); +}); From a095ee4607356cb278066928be54e8e96c221189 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 8 Sep 2026 06:38:15 +0100 Subject: [PATCH 4/8] test: include Rust and Cargo in doctor integration --- tests/executable.test.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/executable.test.ts b/tests/executable.test.ts index 5e01ac1..75bb04a 100644 --- a/tests/executable.test.ts +++ b/tests/executable.test.ts @@ -56,7 +56,9 @@ describe('StellarForge CLI executable', () => { expect(result.stdout).toContain('[PASS] Node.js:'); expect(result.stdout).toContain('[PASS] npm:'); expect(result.stdout).toContain('[PASS] Git:'); - expect(result.stdout).toContain('Summary: 3 passed, 0 warnings, 0 failed.'); + expect(result.stdout).toContain('[PASS] Rust:'); + expect(result.stdout).toContain('[PASS] Cargo:'); + expect(result.stdout).toContain('Summary: 5 passed, 0 warnings, 0 failed.'); }); it('prints the package version from the built executable', () => { From f60ef4b0058d0663f7e540604552a1e3817169d6 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 8 Sep 2026 06:38:23 +0100 Subject: [PATCH 5/8] chore: add CLI-019 release intent --- .changeset/rust-cargo-doctor-diagnostics.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/rust-cargo-doctor-diagnostics.md diff --git a/.changeset/rust-cargo-doctor-diagnostics.md b/.changeset/rust-cargo-doctor-diagnostics.md new file mode 100644 index 0000000..7e60cff --- /dev/null +++ b/.changeset/rust-cargo-doctor-diagnostics.md @@ -0,0 +1,5 @@ +--- +"@stellarforge/cli": minor +--- + +Add Rust and Cargo environment checks to `stellarforge doctor`, including the Stellar smart-contract Rust 1.84.0 minimum and remediation guidance. From 2f1029918a1ac2fe4ac755c16671aa4658c3ae20 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 8 Sep 2026 06:40:01 +0100 Subject: [PATCH 6/8] chore: apply canonical CLI-019 formatting --- .github/workflows/format-cli-019.yml | 44 ++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) create mode 100644 .github/workflows/format-cli-019.yml diff --git a/.github/workflows/format-cli-019.yml b/.github/workflows/format-cli-019.yml new file mode 100644 index 0000000..11b72a4 --- /dev/null +++ b/.github/workflows/format-cli-019.yml @@ -0,0 +1,44 @@ +name: Format CLI-019 + +on: + push: + branches: + - feat/cli-019-rust-cargo-diagnostics + paths: + - .github/workflows/format-cli-019.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/rust.ts tests/rust-diagnostics.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/rust.ts tests/rust-diagnostics.test.ts + if git diff --cached --quiet; then + exit 0 + fi + git commit -m 'style: format Rust and Cargo diagnostics' + git push origin HEAD:feat/cli-019-rust-cargo-diagnostics From bfd363d8dbbfdcf25fcfb21f428b2b4f1bf304ca 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:40:16 +0000 Subject: [PATCH 7/8] style: format Rust and Cargo diagnostics --- src/diagnostics/rust.ts | 11 ++++++++--- tests/rust-diagnostics.test.ts | 4 +++- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/diagnostics/rust.ts b/src/diagnostics/rust.ts index b2f21c6..b271bcc 100644 --- a/src/diagnostics/rust.ts +++ b/src/diagnostics/rust.ts @@ -4,7 +4,10 @@ import { isAtLeast, parseNumericVersion } from './version.js'; const MINIMUM_RUST = { major: 1, minor: 84, patch: 0 } as const; -function parseToolVersion(output: string, tool: 'rustc' | 'cargo'): string | null { +function parseToolVersion( + output: string, + tool: 'rustc' | 'cargo', +): string | null { const match = new RegExp(`^${tool} (\\d+\\.\\d+\\.\\d+)`, 'i').exec( output.trim(), ); @@ -40,7 +43,8 @@ export function createRustDiagnostic( label: 'Rust', status: 'fail', message: 'Rust returned an unrecognized version.', - remediation: 'Verify `rustc --version` works and reinstall Rust if necessary.', + remediation: + 'Verify `rustc --version` works and reinstall Rust if necessary.', }; } @@ -93,7 +97,8 @@ export function createCargoDiagnostic( label: 'Cargo', status: 'fail', message: 'Cargo returned an unrecognized version.', - remediation: 'Verify `cargo --version` works and reinstall Rust if necessary.', + remediation: + 'Verify `cargo --version` works and reinstall Rust if necessary.', }; } diff --git a/tests/rust-diagnostics.test.ts b/tests/rust-diagnostics.test.ts index 76b4e81..6924282 100644 --- a/tests/rust-diagnostics.test.ts +++ b/tests/rust-diagnostics.test.ts @@ -52,7 +52,9 @@ describe('Rust diagnostic', () => { const diagnostic = createRustDiagnostic(execute).run(); expect(diagnostic.status).toBe('fail'); - expect(diagnostic.message).toBe('Rust is unavailable or could not be executed.'); + expect(diagnostic.message).toBe( + 'Rust is unavailable or could not be executed.', + ); expect(JSON.stringify(diagnostic)).not.toContain('SECRET_TOKEN'); }); From e43367acc44ef3fcd92ff9eb7af1fe85c7a42409 Mon Sep 17 00:00:00 2001 From: Ahmed Mahmud Date: Tue, 8 Sep 2026 06:40:40 +0100 Subject: [PATCH 8/8] chore: remove temporary CLI-019 formatter --- .github/workflows/format-cli-019.yml | 44 ---------------------------- 1 file changed, 44 deletions(-) delete mode 100644 .github/workflows/format-cli-019.yml diff --git a/.github/workflows/format-cli-019.yml b/.github/workflows/format-cli-019.yml deleted file mode 100644 index 11b72a4..0000000 --- a/.github/workflows/format-cli-019.yml +++ /dev/null @@ -1,44 +0,0 @@ -name: Format CLI-019 - -on: - push: - branches: - - feat/cli-019-rust-cargo-diagnostics - paths: - - .github/workflows/format-cli-019.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/rust.ts tests/rust-diagnostics.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/rust.ts tests/rust-diagnostics.test.ts - if git diff --cached --quiet; then - exit 0 - fi - git commit -m 'style: format Rust and Cargo diagnostics' - git push origin HEAD:feat/cli-019-rust-cargo-diagnostics