Skip to content

Commit 88fb792

Browse files
jorgemoyaclaude
andcommitted
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 <noreply@anthropic.com>
1 parent d62cc59 commit 88fb792

3 files changed

Lines changed: 91 additions & 18 deletions

File tree

packages/catalyst/src/cli/commands/logs.spec.ts

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,23 @@
11
import { Command } from 'commander';
2+
import Conf from 'conf';
23
import { http, HttpResponse } from 'msw';
3-
import { afterEach, beforeAll, describe, expect, MockInstance, test, vi } from 'vitest';
4+
import { afterAll, afterEach, beforeAll, describe, expect, MockInstance, test, vi } from 'vitest';
45

56
import { server } from '../../../tests/mocks/node';
67
import { consola } from '../lib/logger';
8+
import { mkTempDir } from '../lib/mk-temp-dir';
9+
import { getProjectConfig, ProjectConfigSchema } from '../lib/project-config';
710
import { program } from '../program';
811

912
import { logs, parseSSEEvent, tailLogs } from './logs';
1013

1114
let exitMock: MockInstance;
1215
let stdoutWriteMock: MockInstance;
1316

17+
let tmpDir: string;
18+
let cleanup: () => Promise<void>;
19+
let config: Conf<ProjectConfigSchema>;
20+
1421
const projectUuid = '6b202364-10f3-11f1-8bc7-fe9b9d8b14ab';
1522
const storeHash = 'test-store';
1623
const accessToken = 'test-token';
@@ -69,15 +76,28 @@ const callTailLogs = async (format: Parameters<typeof tailLogs>[4], events?: str
6976
});
7077
};
7178

72-
beforeAll(() => {
79+
beforeAll(async () => {
7380
consola.mockTypes(() => vi.fn());
7481
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
7582
exitMock = vi.spyOn(process, 'exit').mockImplementation(() => null as never);
7683
stdoutWriteMock = vi.spyOn(process.stdout, 'write').mockImplementation(() => true);
84+
85+
[tmpDir, cleanup] = await mkTempDir();
86+
87+
vi.spyOn(process, 'cwd').mockReturnValue(tmpDir);
88+
89+
config = getProjectConfig();
7790
});
7891

7992
afterEach(() => {
8093
vi.clearAllMocks();
94+
config.delete('storeHash');
95+
config.delete('accessToken');
96+
config.delete('projectUuid');
97+
});
98+
99+
afterAll(async () => {
100+
await cleanup();
81101
});
82102

83103
describe('command configuration', () => {
@@ -486,6 +506,39 @@ describe('retry and reconnect', () => {
486506
});
487507
});
488508

509+
describe('credential resolution', () => {
510+
test('falls back to project.json for storeHash and accessToken', async () => {
511+
config.set('storeHash', storeHash);
512+
config.set('accessToken', accessToken);
513+
514+
server.use(createOneShotLogHandler([`data: ${JSON.stringify(validLogEvent)}\n\n`]));
515+
516+
await program.parseAsync(['node', 'catalyst', 'logs', 'tail', '--project-uuid', projectUuid]);
517+
518+
expect(consola.info).toHaveBeenCalledWith('Tailing logs...');
519+
expect(consola.log).toHaveBeenCalledWith(expect.stringContaining('hello world'));
520+
});
521+
522+
test('exits with error when no credentials are provided', async () => {
523+
const savedStoreHash = process.env.CATALYST_STORE_HASH;
524+
const savedAccessToken = process.env.CATALYST_ACCESS_TOKEN;
525+
526+
delete process.env.CATALYST_STORE_HASH;
527+
delete process.env.CATALYST_ACCESS_TOKEN;
528+
529+
await program.parseAsync(['node', 'catalyst', 'logs', 'tail', '--project-uuid', projectUuid]);
530+
531+
if (savedStoreHash !== undefined) process.env.CATALYST_STORE_HASH = savedStoreHash;
532+
if (savedAccessToken !== undefined) process.env.CATALYST_ACCESS_TOKEN = savedAccessToken;
533+
534+
expect(consola.error).toHaveBeenCalledWith('Missing credentials.');
535+
expect(consola.info).toHaveBeenCalledWith(
536+
'Run `catalyst auth login`, or provide --store-hash and --access-token flags (or set CATALYST_STORE_HASH and CATALYST_ACCESS_TOKEN environment variables).',
537+
);
538+
expect(exitMock).toHaveBeenCalledWith(1);
539+
});
540+
});
541+
489542
describe('query subcommand', () => {
490543
test('exits with error as not yet implemented', async () => {
491544
await program.parseAsync([
@@ -502,6 +555,24 @@ describe('query subcommand', () => {
502555
expect(consola.error).toHaveBeenCalledWith('The query command is not yet implemented.');
503556
expect(exitMock).toHaveBeenCalledWith(1);
504557
});
558+
559+
test('exits with missing credentials error when none are provided', async () => {
560+
const savedStoreHash = process.env.CATALYST_STORE_HASH;
561+
const savedAccessToken = process.env.CATALYST_ACCESS_TOKEN;
562+
563+
delete process.env.CATALYST_STORE_HASH;
564+
delete process.env.CATALYST_ACCESS_TOKEN;
565+
566+
await expect(program.parseAsync(['node', 'catalyst', 'logs', 'query'])).rejects.toThrow(
567+
'Missing credentials',
568+
);
569+
570+
if (savedStoreHash !== undefined) process.env.CATALYST_STORE_HASH = savedStoreHash;
571+
if (savedAccessToken !== undefined) process.env.CATALYST_ACCESS_TOKEN = savedAccessToken;
572+
573+
expect(consola.error).toHaveBeenCalledWith('Missing credentials.');
574+
expect(exitMock).toHaveBeenCalledWith(1);
575+
});
505576
});
506577

507578
describe('program integration', () => {

packages/catalyst/src/cli/commands/logs.ts

Lines changed: 16 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import { colorize } from 'consola/utils';
33
import { z } from 'zod';
44

55
import { consola } from '../lib/logger';
6+
import { getProjectConfig } from '../lib/project-config';
7+
import { resolveCredentials } from '../lib/resolve-credentials';
68
import {
79
accessTokenOption,
810
apiHostOption,
@@ -245,8 +247,8 @@ Examples:
245247
# Tail logs as raw JSON (useful for piping to other tools)
246248
$ catalyst logs tail --format json`,
247249
)
248-
.addOption(storeHashOption().makeOptionMandatory())
249-
.addOption(accessTokenOption().makeOptionMandatory())
250+
.addOption(storeHashOption())
251+
.addOption(accessTokenOption())
250252
.addOption(apiHostOption())
251253
.addOption(projectUuidOption())
252254
.addOption(
@@ -256,17 +258,14 @@ Examples:
256258
)
257259
.action(async (options) => {
258260
try {
259-
await telemetry.identify(options.storeHash);
261+
const config = getProjectConfig();
262+
const { storeHash, accessToken } = resolveCredentials(options, config);
263+
264+
await telemetry.identify(storeHash);
260265

261266
const projectUuid = resolveProjectUuid(options);
262267

263-
await tailLogs(
264-
projectUuid,
265-
options.storeHash,
266-
options.accessToken,
267-
options.apiHost,
268-
options.format,
269-
);
268+
await tailLogs(projectUuid, storeHash, accessToken, options.apiHost, options.format);
270269
} catch (error) {
271270
consola.error(error);
272271
process.exit(1);
@@ -281,12 +280,15 @@ const query = new Command('query')
281280
Example:
282281
$ catalyst logs query`,
283282
)
284-
.addOption(storeHashOption().makeOptionMandatory())
285-
.addOption(accessTokenOption().makeOptionMandatory())
283+
.addOption(storeHashOption())
284+
.addOption(accessTokenOption())
286285
.addOption(apiHostOption())
287286
.addOption(projectUuidOption())
288-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
289-
.action((_options) => {
287+
.action((options) => {
288+
const config = getProjectConfig();
289+
290+
resolveCredentials(options, config);
291+
290292
consola.error('The query command is not yet implemented.');
291293
process.exit(1);
292294
});

packages/catalyst/src/cli/lib/shared-options.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,13 @@ export const storeHashOption = () =>
66
new Option(
77
'--store-hash <hash>',
88
'BigCommerce store hash. Can be found in the URL of your store Control Panel.',
9-
).env('BIGCOMMERCE_STORE_HASH');
9+
).env('CATALYST_STORE_HASH');
1010

1111
export const accessTokenOption = () =>
1212
new Option(
1313
'--access-token <token>',
1414
'BigCommerce access token. Can be found after creating a store-level API account.',
15-
).env('BIGCOMMERCE_ACCESS_TOKEN');
15+
).env('CATALYST_ACCESS_TOKEN');
1616

1717
export const apiHostOption = () =>
1818
new Option('--api-host <host>', 'BigCommerce API host. The default is api.bigcommerce.com.')

0 commit comments

Comments
 (0)