diff --git a/.changeset/tidy-stars-tell.md b/.changeset/tidy-stars-tell.md new file mode 100644 index 0000000..6a1e9d7 --- /dev/null +++ b/.changeset/tidy-stars-tell.md @@ -0,0 +1,5 @@ +--- +"@diginodes/stellarforge-cli": patch +--- + +Clarify Stellar CLI diagnostic remediation for unavailable, malformed, and unsupported versions. diff --git a/src/diagnostics/stellar.ts b/src/diagnostics/stellar.ts index 216991e..e0ddaf0 100644 --- a/src/diagnostics/stellar.ts +++ b/src/diagnostics/stellar.ts @@ -10,6 +10,8 @@ import { isAtLeast, parseNumericVersion } from './version.js'; // 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; +const STELLAR_CLI_INSTALL_URL = + 'https://developers.stellar.org/docs/tools/cli/install-cli'; function parseStellarVersion(output: string): string | null { const value = output.trim(); @@ -35,7 +37,7 @@ export function createStellarDiagnostic( 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.', + `Install or update the official Stellar CLI (${STELLAR_CLI_INSTALL_URL}), then ensure \`stellar\` is on PATH and \`stellar version --only-version\` reports a version.`, }; } @@ -49,7 +51,7 @@ export function createStellarDiagnostic( status: 'fail', message: 'Stellar CLI returned an unrecognized version.', remediation: - 'Verify `stellar version --only-version` works and reinstall Stellar CLI if necessary.', + `Run \`stellar version --only-version\`; if it does not report a semantic version, reinstall or update the official Stellar CLI (${STELLAR_CLI_INSTALL_URL}).`, }; } @@ -60,7 +62,7 @@ export function createStellarDiagnostic( status: 'fail', message: `Unsupported Stellar CLI version ${version}.`, remediation: - 'Upgrade to Stellar CLI >=27.1.0 before using StellarForge Stellar workflows.', + `Update the official Stellar CLI to >=27.1.0 (${STELLAR_CLI_INSTALL_URL}), then verify \`stellar version --only-version\` reports a supported version.`, }; } diff --git a/tests/template-generation.test.ts b/tests/template-generation.test.ts index 283b35f..27d7798 100644 --- a/tests/template-generation.test.ts +++ b/tests/template-generation.test.ts @@ -80,12 +80,14 @@ describe('template generation', () => { withTempDirectory((root) => { const templateRoot = join(root, 'template'); mkdirSync(templateRoot); - writeFileSync(join(templateRoot, 'good.txt'), 'created first'); - writeFileSync(join(templateRoot, 'bad.txt'), '{{unsupportedToken}}'); + writeFileSync(join(templateRoot, 'a-good.txt'), 'created first'); + writeFileSync(join(templateRoot, 'z-bad.txt'), '{{unsupportedToken}}'); expect(() => generateProjectFromTemplate(context(root), { templateRoot }), - ).toThrow(ValidationCliError); + ).toThrow( + "Template contains unsupported substitution token '{{unsupportedToken}}'.", + ); expect(existsSync(join(root, 'demo-app'))).toBe(false); expect( @@ -95,6 +97,30 @@ describe('template generation', () => { ).toEqual([]); })); + it('preserves pre-existing user content and an empty destination after partial failure', () => + withTempDirectory((root) => { + const templateRoot = join(root, 'template'); + const destination = join(root, 'demo-app'); + const userFile = join(root, 'user-notes.txt'); + mkdirSync(templateRoot); + mkdirSync(destination); + writeFileSync(userFile, 'keep this content'); + writeFileSync(join(templateRoot, 'a-good.txt'), 'created first'); + writeFileSync(join(templateRoot, 'z-bad.txt'), '{{unsupportedToken}}'); + + expect(() => + generateProjectFromTemplate(context(root), { templateRoot }), + ).toThrow(ValidationCliError); + + expect(readdirSync(destination)).toEqual([]); + expect(readFileSync(userFile, 'utf8')).toBe('keep this content'); + expect( + readdirSync(root).filter((name) => + name.startsWith('.stellarforge-demo-app-'), + ), + ).toEqual([]); + })); + it('refuses to overwrite a non-empty destination', () => withTempDirectory((root) => { const templateRoot = join(root, 'template');