Skip to content

Commit 3ab3c67

Browse files
jorgemoyaclaude
andauthored
LTRAC-444: fix(cli) - Read store hash and access token from project.json (#2997)
* 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> * 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 <noreply@anthropic.com> --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 2e1e48a commit 3ab3c67

3 files changed

Lines changed: 92 additions & 19 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,
@@ -246,8 +248,8 @@ Examples:
246248
# Tail logs as raw JSON (useful for piping to other tools)
247249
$ catalyst logs tail --format json`,
248250
)
249-
.addOption(storeHashOption().makeOptionMandatory())
250-
.addOption(accessTokenOption().makeOptionMandatory())
251+
.addOption(storeHashOption())
252+
.addOption(accessTokenOption())
251253
.addOption(apiHostOption())
252254
.addOption(projectUuidOption())
253255
.addOption(
@@ -257,17 +259,14 @@ Examples:
257259
)
258260
.action(async (options) => {
259261
try {
260-
await telemetry.identify(options.storeHash);
262+
const config = getProjectConfig();
263+
const { storeHash, accessToken } = resolveCredentials(options, config);
264+
265+
await telemetry.identify(storeHash);
261266

262267
const projectUuid = resolveProjectUuid(options);
263268

264-
await tailLogs(
265-
projectUuid,
266-
options.storeHash,
267-
options.accessToken,
268-
options.apiHost,
269-
options.format,
270-
);
269+
await tailLogs(projectUuid, storeHash, accessToken, options.apiHost, options.format);
271270
} catch (error) {
272271
consola.error(error);
273272
process.exit(1);
@@ -283,12 +282,15 @@ const query = new Command('query')
283282
Example:
284283
$ catalyst logs query`,
285284
)
286-
.addOption(storeHashOption().makeOptionMandatory())
287-
.addOption(accessTokenOption().makeOptionMandatory())
285+
.addOption(storeHashOption())
286+
.addOption(accessTokenOption())
288287
.addOption(apiHostOption())
289288
.addOption(projectUuidOption())
290-
// eslint-disable-next-line @typescript-eslint/no-unused-vars
291-
.action((_options) => {
289+
.action((options) => {
290+
const config = getProjectConfig();
291+
292+
resolveCredentials(options, config);
293+
292294
consola.error('The query command is not yet implemented.');
293295
process.exit(1);
294296
});

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

Lines changed: 3 additions & 3 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.')
@@ -24,7 +24,7 @@ export const projectUuidOption = () =>
2424
new Option(
2525
'--project-uuid <uuid>',
2626
'BigCommerce infrastructure project UUID. Can be found via the BigCommerce API (GET /v3/infrastructure/projects).',
27-
).env('BIGCOMMERCE_PROJECT_UUID');
27+
).env('CATALYST_PROJECT_UUID');
2828

2929
export const resolveProjectUuid = (options: { projectUuid?: string }) => {
3030
const config = getProjectConfig();

0 commit comments

Comments
 (0)