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
2 changes: 2 additions & 0 deletions src/commands/doctor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
createRustDiagnostic,
} from '../diagnostics/rust.js';
import { createRuntimeDiagnostics } from '../diagnostics/runtime.js';
import { createStellarDiagnostic } from '../diagnostics/stellar.js';
import type {
DiagnosticCheck,
DiagnosticReport,
Expand All @@ -30,6 +31,7 @@ function createDefaultDiagnostics(): readonly DiagnosticCheck[] {
createGitDiagnostic(),
createRustDiagnostic(),
createCargoDiagnostic(),
createStellarDiagnostic(),
];
}

Expand Down
71 changes: 71 additions & 0 deletions src/diagnostics/stellar.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import { runCommand, type RunCommand } from './process.js';
import type { DiagnosticCheck, DiagnosticResult } from './types.js';
import { isAtLeast, parseNumericVersion } from './version.js';

// Protocol 27 is the current recommended Mainnet baseline in Stellar's
// software-version matrix. Keep this compatibility floor explicit and review
// it when StellarForge changes its supported network/tooling baseline.
const MINIMUM_STELLAR_CLI = { major: 27, minor: 1, patch: 0 } as const;

function parseStellarVersion(output: string): string | null {
const value = output.trim();
const match = /^v?(\d+\.\d+\.\d+)(?:[-+][0-9A-Za-z.-]+)?$/.exec(value);
return match?.[1] ?? null;
}

export function createStellarDiagnostic(
execute: RunCommand = runCommand,
): DiagnosticCheck {
return {
id: 'stellar',
label: 'Stellar CLI',
run(): DiagnosticResult {
// `stellar version --only-version` is the official machine-friendly
// version command. It does not inspect identities, keys, or config.
const result = execute('stellar', ['version', '--only-version']);

if (result.error || result.status !== 0) {
return {
id: 'stellar',
label: 'Stellar CLI',
status: 'fail',
message: 'Stellar CLI is unavailable or could not be executed.',
remediation:
'Install the current Stellar CLI and ensure `stellar` is available on PATH. See https://developers.stellar.org/docs/tools/cli/install-cli.',
};
}

const version = parseStellarVersion(result.stdout);
const parsed = version ? parseNumericVersion(version) : null;

if (!version || !parsed) {
return {
id: 'stellar',
label: 'Stellar CLI',
status: 'fail',
message: 'Stellar CLI returned an unrecognized version.',
remediation:
'Verify `stellar version --only-version` works and reinstall Stellar CLI if necessary.',
};
}

if (!isAtLeast(parsed, MINIMUM_STELLAR_CLI)) {
return {
id: 'stellar',
label: 'Stellar CLI',
status: 'fail',
message: `Unsupported Stellar CLI version ${version}.`,
remediation:
'Upgrade to Stellar CLI >=27.1.0 before using StellarForge Stellar workflows.',
};
}

return {
id: 'stellar',
label: 'Stellar CLI',
status: 'pass',
message: `Supported version ${version} detected.`,
};
},
};
}
9 changes: 6 additions & 3 deletions tests/executable.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,20 @@ describe('StellarForge CLI executable', () => {
expect(result.stdout).toContain('-V, --version');
});

it('runs the default doctor checks successfully in the supported toolchain', () => {
it('runs default doctor checks safely in the CI toolchain', () => {
const result = runBuiltCli(['doctor']);

expect(result.status).toBe(0);
expect([0, 5]).toContain(result.status);
expect(result.stderr).toBe('');
expect(result.stdout).toContain('[PASS] Node.js:');
expect(result.stdout).toContain('[PASS] npm:');
expect(result.stdout).toContain('[PASS] Git:');
expect(result.stdout).toContain('[PASS] Rust:');
expect(result.stdout).toContain('[PASS] Cargo:');
expect(result.stdout).toContain('Summary: 5 passed, 0 warnings, 0 failed.');
expect(result.stdout).toContain('Stellar CLI:');
expect(result.stdout).toMatch(
/Summary: 5 passed, 0 warnings, [01] failed\./,
);
});

it('prints the package version from the built executable', () => {
Expand Down
83 changes: 83 additions & 0 deletions tests/stellar-diagnostic.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { describe, expect, it } from 'vitest';
import type { RunCommand } from '../src/diagnostics/process.js';
import { createStellarDiagnostic } from '../src/diagnostics/stellar.js';

function result(
status: number | null,
stdout = '',
stderr = '',
error?: Error,
) {
return {
status,
stdout,
stderr,
...(error === undefined ? {} : { error }),
};
}

describe('Stellar CLI diagnostic', () => {
it('uses the official machine-friendly version command', () => {
const calls: Array<{ executable: string; args: readonly string[] }> = [];
const execute: RunCommand = (executable, args) => {
calls.push({ executable, args });
return result(0, '27.1.0\n');
};

const diagnostic = createStellarDiagnostic(execute).run();

expect(calls).toEqual([
{ executable: 'stellar', args: ['version', '--only-version'] },
]);
expect(diagnostic.status).toBe('pass');
expect(diagnostic.message).toContain('27.1.0');
});

it('accepts a leading v and newer compatible versions', () => {
const execute: RunCommand = () => result(0, 'v28.0.0\n');

const diagnostic = createStellarDiagnostic(execute).run();

expect(diagnostic.status).toBe('pass');
expect(diagnostic.message).toContain('28.0.0');
});

it('fails versions below the supported Stellar baseline', () => {
const execute: RunCommand = () => result(0, '27.0.0\n');

const diagnostic = createStellarDiagnostic(execute).run();

expect(diagnostic.status).toBe('fail');
expect(diagnostic.message).toContain('27.0.0');
expect(diagnostic.remediation).toContain('>=27.1.0');
});

it('fails when Stellar CLI is unavailable without leaking process details', () => {
const execute: RunCommand = () =>
result(
null,
'',
'SECRET_SEED=do-not-render',
new Error('spawn stellar ENOENT SECRET_SEED=do-not-render'),
);

const diagnostic = createStellarDiagnostic(execute).run();

expect(diagnostic.status).toBe('fail');
expect(diagnostic.message).toBe(
'Stellar CLI is unavailable or could not be executed.',
);
expect(JSON.stringify(diagnostic)).not.toContain('SECRET_SEED');
});

it('fails malformed version output', () => {
const execute: RunCommand = () => result(0, 'stellar-cli unknown\n');

const diagnostic = createStellarDiagnostic(execute).run();

expect(diagnostic.status).toBe('fail');
expect(diagnostic.message).toBe(
'Stellar CLI returned an unrecognized version.',
);
});
});