From 22e41c2b27bc33aea1c5b59f09721b43689732ea Mon Sep 17 00:00:00 2001 From: Kurt Overmier Date: Wed, 6 May 2026 05:25:14 -0500 Subject: [PATCH] cli: add deprecation warnings for build commands --- .../src/__tests__/deprecated-commands.test.ts | 72 +++++++++++++++++++ packages/cli/src/__tests__/login.test.ts | 30 +++++++- packages/cli/src/commands/architect.ts | 3 + .../cli/src/commands/deprecation-warning.ts | 20 ++++++ packages/cli/src/commands/login.ts | 14 +--- packages/cli/src/commands/run.ts | 3 + packages/cli/src/commands/scaffold.ts | 3 + packages/cli/src/index.ts | 2 + 8 files changed, 135 insertions(+), 12 deletions(-) create mode 100644 packages/cli/src/__tests__/deprecated-commands.test.ts create mode 100644 packages/cli/src/commands/deprecation-warning.ts diff --git a/packages/cli/src/__tests__/deprecated-commands.test.ts b/packages/cli/src/__tests__/deprecated-commands.test.ts new file mode 100644 index 0000000..3d41922 --- /dev/null +++ b/packages/cli/src/__tests__/deprecated-commands.test.ts @@ -0,0 +1,72 @@ +import { describe, expect, it, vi, beforeEach, afterEach } from 'vitest'; +import { architectCommand } from '../commands/architect'; +import { runCommand } from '../commands/run'; +import { scaffoldCommand } from '../commands/scaffold'; +import { loginCommand } from '../commands/login'; +import type { CLIOptions } from '../index'; +import { CLIError } from '../index'; +import { DEPRECATION_WARNING_ENV_VAR } from '../commands/deprecation-warning'; + +const baseOptions: CLIOptions = { + format: 'text', + configPath: '.charter', + ciMode: false, + yes: false, +}; + +describe('deprecated build commands warnings', () => { + const originalSuppress = process.env[DEPRECATION_WARNING_ENV_VAR]; + + beforeEach(() => { + delete process.env[DEPRECATION_WARNING_ENV_VAR]; + vi.restoreAllMocks(); + }); + + afterEach(() => { + if (originalSuppress === undefined) { + delete process.env[DEPRECATION_WARNING_ENV_VAR]; + } else { + process.env[DEPRECATION_WARNING_ENV_VAR] = originalSuppress; + } + vi.restoreAllMocks(); + }); + + it('emits warning for login', async () => { + const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); + vi.spyOn(console, 'log').mockImplementation(() => {}); + + await loginCommand(baseOptions, []); + + const stderrOutput = stderr.mock.calls.map((c) => String(c[0])).join(''); + expect(stderrOutput).toContain('charter login'); + expect(stderrOutput).toContain('@stackbilt/build'); + }); + + it('emits warning for architect', async () => { + const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); + + await expect(architectCommand(baseOptions, [])).rejects.toBeInstanceOf(CLIError); + + const stderrOutput = stderr.mock.calls.map((c) => String(c[0])).join(''); + expect(stderrOutput).toContain('charter architect'); + }); + + it('emits warning for run', async () => { + const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); + + await expect(runCommand(baseOptions, [])).rejects.toBeInstanceOf(CLIError); + + const stderrOutput = stderr.mock.calls.map((c) => String(c[0])).join(''); + expect(stderrOutput).toContain('charter run'); + }); + + it('emits warning for scaffold', async () => { + const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); + const options = { ...baseOptions, configPath: '.charter-missing-cache-for-test' }; + + await expect(scaffoldCommand(options, [])).rejects.toBeInstanceOf(CLIError); + + const stderrOutput = stderr.mock.calls.map((c) => String(c[0])).join(''); + expect(stderrOutput).toContain('charter scaffold'); + }); +}); diff --git a/packages/cli/src/__tests__/login.test.ts b/packages/cli/src/__tests__/login.test.ts index df2b231..836df4c 100644 --- a/packages/cli/src/__tests__/login.test.ts +++ b/packages/cli/src/__tests__/login.test.ts @@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; import { loginCommand } from '../commands/login'; import type { CLIOptions } from '../index'; import { API_KEY_ENV_VAR } from '../credentials'; +import { DEPRECATION_WARNING_ENV_VAR } from '../commands/deprecation-warning'; const options: CLIOptions = { format: 'text', @@ -12,6 +13,7 @@ const options: CLIOptions = { describe('charter login — deprecation notice', () => { const originalEnv = process.env[API_KEY_ENV_VAR]; + const originalSuppress = process.env[DEPRECATION_WARNING_ENV_VAR]; beforeEach(() => { delete process.env[API_KEY_ENV_VAR]; @@ -23,6 +25,11 @@ describe('charter login — deprecation notice', () => { } else { process.env[API_KEY_ENV_VAR] = originalEnv; } + if (originalSuppress === undefined) { + delete process.env[DEPRECATION_WARNING_ENV_VAR]; + } else { + process.env[DEPRECATION_WARNING_ENV_VAR] = originalSuppress; + } vi.restoreAllMocks(); }); @@ -34,7 +41,7 @@ describe('charter login — deprecation notice', () => { const stderrOutput = stderr.mock.calls.map((c) => String(c[0])).join(''); expect(stderrOutput).toMatch(/deprecated/i); - expect(stderrOutput).toContain(API_KEY_ENV_VAR); + expect(stderrOutput).toContain('@stackbilt/build'); }); it('reports env-var usage when STACKBILT_API_KEY is set and no --key flag', async () => { @@ -47,4 +54,25 @@ describe('charter login — deprecation notice', () => { const stdoutOutput = log.mock.calls.map((c) => String(c[0])).join('\n'); expect(stdoutOutput).toMatch(new RegExp(`Using ${API_KEY_ENV_VAR} from environment`)); }); + + it('suppresses warning when CHARTER_NO_DEPRECATION_WARNING=1', async () => { + process.env[DEPRECATION_WARNING_ENV_VAR] = '1'; + const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); + vi.spyOn(console, 'log').mockImplementation(() => {}); + + await loginCommand(options, []); + + const stderrOutput = stderr.mock.calls.map((c) => String(c[0])).join(''); + expect(stderrOutput).toBe(''); + }); + + it('suppresses warning when --no-deprecation-warning is passed', async () => { + const stderr = vi.spyOn(process.stderr, 'write').mockImplementation(() => true); + vi.spyOn(console, 'log').mockImplementation(() => {}); + + await loginCommand(options, ['--no-deprecation-warning']); + + const stderrOutput = stderr.mock.calls.map((c) => String(c[0])).join(''); + expect(stderrOutput).toBe(''); + }); }); diff --git a/packages/cli/src/commands/architect.ts b/packages/cli/src/commands/architect.ts index 7208d3b..5974b70 100644 --- a/packages/cli/src/commands/architect.ts +++ b/packages/cli/src/commands/architect.ts @@ -15,8 +15,11 @@ import { EXIT_CODE, CLIError } from '../index'; import { getFlag } from '../flags'; import { resolveApiKey, API_KEY_ENV_VAR } from '../credentials'; import { EngineClient, type BuildRequest, type BuildResult } from '../http-client'; +import { printBuildCommandDeprecationWarning } from './deprecation-warning'; export async function architectCommand(options: CLIOptions, args: string[]): Promise { + printBuildCommandDeprecationWarning('architect', args); + // Parse description from positional arg or --file const filePath = getFlag(args, '--file'); const positional = args.filter(a => !a.startsWith('-') && a !== filePath); diff --git a/packages/cli/src/commands/deprecation-warning.ts b/packages/cli/src/commands/deprecation-warning.ts new file mode 100644 index 0000000..d81384a --- /dev/null +++ b/packages/cli/src/commands/deprecation-warning.ts @@ -0,0 +1,20 @@ +const RFC_112_URL = 'https://github.com/Stackbilt-dev/charter/issues/112'; +export const DEPRECATION_WARNING_ENV_VAR = 'CHARTER_NO_DEPRECATION_WARNING'; +export const DEPRECATION_WARNING_FLAG = '--no-deprecation-warning'; + +function warningSuppressed(args: string[]): boolean { + return args.includes(DEPRECATION_WARNING_FLAG) || process.env[DEPRECATION_WARNING_ENV_VAR] === '1'; +} + +export function printBuildCommandDeprecationWarning(command: string, args: string[]): void { + if (warningSuppressed(args)) { + return; + } + + process.stderr.write( + `⚠ charter ${command} is deprecated and will be removed in Charter 1.0.\n` + + ' Install @stackbilt/build for the long-term home of this command:\n' + + ' npm install -g @stackbilt/build\n' + + ` See ${RFC_112_URL} for context.\n`, + ); +} diff --git a/packages/cli/src/commands/login.ts b/packages/cli/src/commands/login.ts index ceb5e14..72d6daf 100644 --- a/packages/cli/src/commands/login.ts +++ b/packages/cli/src/commands/login.ts @@ -12,6 +12,7 @@ import type { CLIOptions } from '../index'; import { EXIT_CODE, CLIError } from '../index'; import { getFlag } from '../flags'; +import { printBuildCommandDeprecationWarning } from './deprecation-warning'; import { loadCredentials, saveCredentials, @@ -20,16 +21,10 @@ import { } from '../credentials'; import { EngineClient } from '../http-client'; -function printDeprecationNotice(): void { - process.stderr.write( - `[deprecated] 'charter login' will be removed in 1.0. ` + - `Set ${API_KEY_ENV_VAR} in the environment instead.\n`, - ); -} - export async function loginCommand(options: CLIOptions, args: string[]): Promise { + printBuildCommandDeprecationWarning('login', args); + if (args.includes('--logout')) { - printDeprecationNotice(); clearCredentials(); console.log('Credentials cleared.'); return EXIT_CODE.SUCCESS; @@ -37,7 +32,6 @@ export async function loginCommand(options: CLIOptions, args: string[]): Promise const key = getFlag(args, '--key'); if (!key) { - printDeprecationNotice(); const existing = loadCredentials(); const envKey = process.env[API_KEY_ENV_VAR]; if (envKey && envKey.trim().length > 0) { @@ -64,8 +58,6 @@ export async function loginCommand(options: CLIOptions, args: string[]): Promise return EXIT_CODE.SUCCESS; } - printDeprecationNotice(); - const VALID_PREFIXES = ['ea_', 'sb_live_', 'sb_test_']; if (!VALID_PREFIXES.some((p) => key.startsWith(p))) { throw new CLIError( diff --git a/packages/cli/src/commands/run.ts b/packages/cli/src/commands/run.ts index 12732ed..62afc9d 100644 --- a/packages/cli/src/commands/run.ts +++ b/packages/cli/src/commands/run.ts @@ -21,6 +21,7 @@ import { EXIT_CODE, CLIError } from '../index'; import { getFlag } from '../flags'; import { resolveApiKey, API_KEY_ENV_VAR } from '../credentials'; import { EngineClient, type BuildRequest, type ScaffoldResult } from '../http-client'; +import { printBuildCommandDeprecationWarning } from './deprecation-warning'; // ─── Animation ────────────────────────────────────────────── @@ -69,6 +70,8 @@ function phaseDetail(label: string, result: ScaffoldResult): string { // ─── Command ──────────────────────────────────────────────── export async function runCommand(options: CLIOptions, args: string[]): Promise { + printBuildCommandDeprecationWarning('run', args); + // Parse flags first (getFlag consumes flag + value from args) const filePath = getFlag(args, '--file'); const outputDir = getFlag(args, '--output'); diff --git a/packages/cli/src/commands/scaffold.ts b/packages/cli/src/commands/scaffold.ts index 2758538..d6c8184 100644 --- a/packages/cli/src/commands/scaffold.ts +++ b/packages/cli/src/commands/scaffold.ts @@ -13,8 +13,11 @@ import type { CLIOptions } from '../index'; import { EXIT_CODE, CLIError } from '../index'; import { getFlag } from '../flags'; import type { BuildResult } from '../http-client'; +import { printBuildCommandDeprecationWarning } from './deprecation-warning'; export async function scaffoldCommand(options: CLIOptions, args: string[]): Promise { + printBuildCommandDeprecationWarning('scaffold', args); + const configPath = options.configPath || '.charter'; const cachePath = path.join(configPath, 'last-build.json'); diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index ed3d528..bf77378 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -86,6 +86,8 @@ Options: --detect-only Setup only: print detected stack/preset and exit --no-dependency-sync Setup only: do not rewrite devDependencies["@stackbilt/cli"] + --no-deprecation-warning + Suppress deprecation warnings for login/architect/scaffold/run `; export const EXIT_CODE = {