From 88fb7925f2f6cfa51b1e52d5ca2d51744ae1d029 Mon Sep 17 00:00:00 2001 From: Jorge Moya Date: Tue, 28 Apr 2026 17:12:02 -0500 Subject: [PATCH 1/2] LTRAC-444: fix(cli) - Read store hash and access token from project.json `catalyst logs tail` and `catalyst logs query` now resolve credentials via `resolveCredentials`, falling back to `.bigcommerce/project.json` when `--store-hash` / `--access-token` flags or `CATALYST_STORE_HASH` / `CATALYST_ACCESS_TOKEN` env vars are not provided. This matches the behavior already used by `project`, `deploy`, and `build`, and removes the need to re-pass credentials after running `catalyst project link`. Also migrates the shared `--store-hash` / `--access-token` option helpers from the legacy `BIGCOMMERCE_*` env var names to `CATALYST_*` for consistency with the rest of the CLI. Fixes LTRAC-444 Co-Authored-By: Claude --- .../catalyst/src/cli/commands/logs.spec.ts | 75 ++++++++++++++++++- packages/catalyst/src/cli/commands/logs.ts | 30 ++++---- .../catalyst/src/cli/lib/shared-options.ts | 4 +- 3 files changed, 91 insertions(+), 18 deletions(-) diff --git a/packages/catalyst/src/cli/commands/logs.spec.ts b/packages/catalyst/src/cli/commands/logs.spec.ts index cf695a303b..7dae33324a 100644 --- a/packages/catalyst/src/cli/commands/logs.spec.ts +++ b/packages/catalyst/src/cli/commands/logs.spec.ts @@ -1,9 +1,12 @@ import { Command } from 'commander'; +import Conf from 'conf'; import { http, HttpResponse } from 'msw'; -import { afterEach, beforeAll, describe, expect, MockInstance, test, vi } from 'vitest'; +import { afterAll, afterEach, beforeAll, describe, expect, MockInstance, test, vi } from 'vitest'; import { server } from '../../../tests/mocks/node'; import { consola } from '../lib/logger'; +import { mkTempDir } from '../lib/mk-temp-dir'; +import { getProjectConfig, ProjectConfigSchema } from '../lib/project-config'; import { program } from '../program'; import { logs, parseSSEEvent, tailLogs } from './logs'; @@ -11,6 +14,10 @@ import { logs, parseSSEEvent, tailLogs } from './logs'; let exitMock: MockInstance; let stdoutWriteMock: MockInstance; +let tmpDir: string; +let cleanup: () => Promise; +let config: Conf; + const projectUuid = '6b202364-10f3-11f1-8bc7-fe9b9d8b14ab'; const storeHash = 'test-store'; const accessToken = 'test-token'; @@ -69,15 +76,28 @@ const callTailLogs = async (format: Parameters[4], events?: str }); }; -beforeAll(() => { +beforeAll(async () => { consola.mockTypes(() => vi.fn()); // eslint-disable-next-line @typescript-eslint/consistent-type-assertions exitMock = vi.spyOn(process, 'exit').mockImplementation(() => null as never); stdoutWriteMock = vi.spyOn(process.stdout, 'write').mockImplementation(() => true); + + [tmpDir, cleanup] = await mkTempDir(); + + vi.spyOn(process, 'cwd').mockReturnValue(tmpDir); + + config = getProjectConfig(); }); afterEach(() => { vi.clearAllMocks(); + config.delete('storeHash'); + config.delete('accessToken'); + config.delete('projectUuid'); +}); + +afterAll(async () => { + await cleanup(); }); describe('command configuration', () => { @@ -486,6 +506,39 @@ describe('retry and reconnect', () => { }); }); +describe('credential resolution', () => { + test('falls back to project.json for storeHash and accessToken', async () => { + config.set('storeHash', storeHash); + config.set('accessToken', accessToken); + + server.use(createOneShotLogHandler([`data: ${JSON.stringify(validLogEvent)}\n\n`])); + + await program.parseAsync(['node', 'catalyst', 'logs', 'tail', '--project-uuid', projectUuid]); + + expect(consola.info).toHaveBeenCalledWith('Tailing logs...'); + expect(consola.log).toHaveBeenCalledWith(expect.stringContaining('hello world')); + }); + + test('exits with error when no credentials are provided', async () => { + const savedStoreHash = process.env.CATALYST_STORE_HASH; + const savedAccessToken = process.env.CATALYST_ACCESS_TOKEN; + + delete process.env.CATALYST_STORE_HASH; + delete process.env.CATALYST_ACCESS_TOKEN; + + await program.parseAsync(['node', 'catalyst', 'logs', 'tail', '--project-uuid', projectUuid]); + + if (savedStoreHash !== undefined) process.env.CATALYST_STORE_HASH = savedStoreHash; + if (savedAccessToken !== undefined) process.env.CATALYST_ACCESS_TOKEN = savedAccessToken; + + expect(consola.error).toHaveBeenCalledWith('Missing credentials.'); + expect(consola.info).toHaveBeenCalledWith( + 'Run `catalyst auth login`, or provide --store-hash and --access-token flags (or set CATALYST_STORE_HASH and CATALYST_ACCESS_TOKEN environment variables).', + ); + expect(exitMock).toHaveBeenCalledWith(1); + }); +}); + describe('query subcommand', () => { test('exits with error as not yet implemented', async () => { await program.parseAsync([ @@ -502,6 +555,24 @@ describe('query subcommand', () => { expect(consola.error).toHaveBeenCalledWith('The query command is not yet implemented.'); expect(exitMock).toHaveBeenCalledWith(1); }); + + test('exits with missing credentials error when none are provided', async () => { + const savedStoreHash = process.env.CATALYST_STORE_HASH; + const savedAccessToken = process.env.CATALYST_ACCESS_TOKEN; + + delete process.env.CATALYST_STORE_HASH; + delete process.env.CATALYST_ACCESS_TOKEN; + + await expect(program.parseAsync(['node', 'catalyst', 'logs', 'query'])).rejects.toThrow( + 'Missing credentials', + ); + + if (savedStoreHash !== undefined) process.env.CATALYST_STORE_HASH = savedStoreHash; + if (savedAccessToken !== undefined) process.env.CATALYST_ACCESS_TOKEN = savedAccessToken; + + expect(consola.error).toHaveBeenCalledWith('Missing credentials.'); + expect(exitMock).toHaveBeenCalledWith(1); + }); }); describe('program integration', () => { diff --git a/packages/catalyst/src/cli/commands/logs.ts b/packages/catalyst/src/cli/commands/logs.ts index e554c007a4..2aee0666fb 100644 --- a/packages/catalyst/src/cli/commands/logs.ts +++ b/packages/catalyst/src/cli/commands/logs.ts @@ -3,6 +3,8 @@ import { colorize } from 'consola/utils'; import { z } from 'zod'; import { consola } from '../lib/logger'; +import { getProjectConfig } from '../lib/project-config'; +import { resolveCredentials } from '../lib/resolve-credentials'; import { accessTokenOption, apiHostOption, @@ -245,8 +247,8 @@ Examples: # Tail logs as raw JSON (useful for piping to other tools) $ catalyst logs tail --format json`, ) - .addOption(storeHashOption().makeOptionMandatory()) - .addOption(accessTokenOption().makeOptionMandatory()) + .addOption(storeHashOption()) + .addOption(accessTokenOption()) .addOption(apiHostOption()) .addOption(projectUuidOption()) .addOption( @@ -256,17 +258,14 @@ Examples: ) .action(async (options) => { try { - await telemetry.identify(options.storeHash); + const config = getProjectConfig(); + const { storeHash, accessToken } = resolveCredentials(options, config); + + await telemetry.identify(storeHash); const projectUuid = resolveProjectUuid(options); - await tailLogs( - projectUuid, - options.storeHash, - options.accessToken, - options.apiHost, - options.format, - ); + await tailLogs(projectUuid, storeHash, accessToken, options.apiHost, options.format); } catch (error) { consola.error(error); process.exit(1); @@ -281,12 +280,15 @@ const query = new Command('query') Example: $ catalyst logs query`, ) - .addOption(storeHashOption().makeOptionMandatory()) - .addOption(accessTokenOption().makeOptionMandatory()) + .addOption(storeHashOption()) + .addOption(accessTokenOption()) .addOption(apiHostOption()) .addOption(projectUuidOption()) - // eslint-disable-next-line @typescript-eslint/no-unused-vars - .action((_options) => { + .action((options) => { + const config = getProjectConfig(); + + resolveCredentials(options, config); + consola.error('The query command is not yet implemented.'); process.exit(1); }); diff --git a/packages/catalyst/src/cli/lib/shared-options.ts b/packages/catalyst/src/cli/lib/shared-options.ts index 5bca46a8ee..93861b7d57 100644 --- a/packages/catalyst/src/cli/lib/shared-options.ts +++ b/packages/catalyst/src/cli/lib/shared-options.ts @@ -6,13 +6,13 @@ export const storeHashOption = () => new Option( '--store-hash ', 'BigCommerce store hash. Can be found in the URL of your store Control Panel.', - ).env('BIGCOMMERCE_STORE_HASH'); + ).env('CATALYST_STORE_HASH'); export const accessTokenOption = () => new Option( '--access-token ', 'BigCommerce access token. Can be found after creating a store-level API account.', - ).env('BIGCOMMERCE_ACCESS_TOKEN'); + ).env('CATALYST_ACCESS_TOKEN'); export const apiHostOption = () => new Option('--api-host ', 'BigCommerce API host. The default is api.bigcommerce.com.') From abbf7b98074b54fb73796f7b2f993b9b1ad7eb90 Mon Sep 17 00:00:00 2001 From: Jorge Moya Date: Wed, 29 Apr 2026 10:40:20 -0500 Subject: [PATCH 2/2] LTRAC-444: fix(cli) - Migrate projectUuidOption env var to CATALYST_PROJECT_UUID The shared `projectUuidOption()` helper was still using the legacy `BIGCOMMERCE_PROJECT_UUID` env var name. It was missed in the earlier `CATALYST_*` rename sweep because `logs` was the only consumer at the time. The inline declarations in `build.ts` and `deploy.ts` already use `CATALYST_PROJECT_UUID`, so this aligns the helper with the rest of the CLI and the alpha.2 patch notes. Refs LTRAC-444 Co-Authored-By: Claude --- packages/catalyst/src/cli/lib/shared-options.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/catalyst/src/cli/lib/shared-options.ts b/packages/catalyst/src/cli/lib/shared-options.ts index 93861b7d57..f753314b23 100644 --- a/packages/catalyst/src/cli/lib/shared-options.ts +++ b/packages/catalyst/src/cli/lib/shared-options.ts @@ -24,7 +24,7 @@ export const projectUuidOption = () => new Option( '--project-uuid ', 'BigCommerce infrastructure project UUID. Can be found via the BigCommerce API (GET /v3/infrastructure/projects).', - ).env('BIGCOMMERCE_PROJECT_UUID'); + ).env('CATALYST_PROJECT_UUID'); export const resolveProjectUuid = (options: { projectUuid?: string }) => { const config = getProjectConfig();