From 9fb572922544fcba4a5e330cef52544a242704f4 Mon Sep 17 00:00:00 2001 From: jamesqquick Date: Wed, 4 Feb 2026 17:17:49 -0600 Subject: [PATCH 01/16] create project command with three nested commands (create, link, and list) --- packages/catalyst/src/cli/commands/build.ts | 2 +- packages/catalyst/src/cli/commands/project.ts | 3 --- packages/catalyst/src/cli/index.spec.ts | 2 -- packages/catalyst/tests/mocks/handlers.ts | 22 +++++++++++-------- 4 files changed, 14 insertions(+), 15 deletions(-) diff --git a/packages/catalyst/src/cli/commands/build.ts b/packages/catalyst/src/cli/commands/build.ts index 94898ba888..435e3c8d6c 100644 --- a/packages/catalyst/src/cli/commands/build.ts +++ b/packages/catalyst/src/cli/commands/build.ts @@ -60,7 +60,7 @@ export const build = new Command('build') if (!projectUuid) { throw new Error( - 'Project UUID is required. Please run `catalyst project create` or `catalyst project link` or this command again with --project-uuid .', + 'Project UUID is required. Please run `catalyst project create` or `catalyst project link` or provide `--project-uuid`', ); } diff --git a/packages/catalyst/src/cli/commands/project.ts b/packages/catalyst/src/cli/commands/project.ts index a97980e2e2..6fe751b527 100644 --- a/packages/catalyst/src/cli/commands/project.ts +++ b/packages/catalyst/src/cli/commands/project.ts @@ -34,7 +34,6 @@ const list = new Command('list') 'Provide both --store-hash and --access-token (or set BIGCOMMERCE_STORE_HASH and BIGCOMMERCE_ACCESS_TOKEN).', ); process.exit(1); - return; } @@ -98,7 +97,6 @@ const create = new Command('create') 'Provide both --store-hash and --access-token (or set BIGCOMMERCE_STORE_HASH and BIGCOMMERCE_ACCESS_TOKEN).', ); process.exit(1); - return; } @@ -118,7 +116,6 @@ const create = new Command('create') consola.success(`Project "${data.name}" created successfully.`); const config = getProjectConfig(options.rootDir); - consola.start('Writing project UUID to .bigcommerce/project.json...'); config.set('projectUuid', data.uuid); config.set('framework', 'catalyst'); diff --git a/packages/catalyst/src/cli/index.spec.ts b/packages/catalyst/src/cli/index.spec.ts index 76f750de9b..f55a90d46a 100644 --- a/packages/catalyst/src/cli/index.spec.ts +++ b/packages/catalyst/src/cli/index.spec.ts @@ -26,9 +26,7 @@ describe('CLI program', () => { expect(commands).toContain('build'); expect(commands).toContain('deploy'); expect(commands).toContain('project'); - const projectCmd = program.commands.find((cmd) => cmd.name() === 'project'); - expect(projectCmd?.commands.map((c) => c.name())).toEqual( expect.arrayContaining(['create', 'list', 'link']), ); diff --git a/packages/catalyst/tests/mocks/handlers.ts b/packages/catalyst/tests/mocks/handlers.ts index a2a46c3b42..0b4425ad1b 100644 --- a/packages/catalyst/tests/mocks/handlers.ts +++ b/packages/catalyst/tests/mocks/handlers.ts @@ -75,14 +75,18 @@ export const handlers = [ ), // Handle for createProjects - http.post('https://:apiHost/stores/:storeHash/v3/infrastructure/projects', () => - HttpResponse.json({ - data: { - uuid: 'c23f5785-fd99-4a94-9fb3-945551623925', - name: 'New Project', - date_created: new Date().toISOString(), - date_modified: new Date().toISOString(), - }, - }), + http.post( + 'https://:apiHost/stores/:storeHash/v3/infrastructure/projects', + async ({ request }) => { + const body = (await request.json()) as { name?: string }; + return HttpResponse.json({ + data: { + uuid: 'c23f5785-fd99-4a94-9fb3-945551623925', + name: body.name ?? 'New Project', + date_created: new Date().toISOString(), + date_modified: new Date().toISOString(), + }, + }); + }, ), ]; From d2751b89cdf7009b73a0607ee5144841a3b6fe21 Mon Sep 17 00:00:00 2001 From: jamesqquick Date: Thu, 5 Feb 2026 12:05:24 -0600 Subject: [PATCH 02/16] fix linting --- packages/catalyst/src/cli/commands/project.ts | 3 +++ packages/catalyst/src/cli/index.spec.ts | 2 ++ packages/catalyst/tests/mocks/handlers.ts | 22 ++++++++----------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/packages/catalyst/src/cli/commands/project.ts b/packages/catalyst/src/cli/commands/project.ts index 6fe751b527..a97980e2e2 100644 --- a/packages/catalyst/src/cli/commands/project.ts +++ b/packages/catalyst/src/cli/commands/project.ts @@ -34,6 +34,7 @@ const list = new Command('list') 'Provide both --store-hash and --access-token (or set BIGCOMMERCE_STORE_HASH and BIGCOMMERCE_ACCESS_TOKEN).', ); process.exit(1); + return; } @@ -97,6 +98,7 @@ const create = new Command('create') 'Provide both --store-hash and --access-token (or set BIGCOMMERCE_STORE_HASH and BIGCOMMERCE_ACCESS_TOKEN).', ); process.exit(1); + return; } @@ -116,6 +118,7 @@ const create = new Command('create') consola.success(`Project "${data.name}" created successfully.`); const config = getProjectConfig(options.rootDir); + consola.start('Writing project UUID to .bigcommerce/project.json...'); config.set('projectUuid', data.uuid); config.set('framework', 'catalyst'); diff --git a/packages/catalyst/src/cli/index.spec.ts b/packages/catalyst/src/cli/index.spec.ts index f55a90d46a..76f750de9b 100644 --- a/packages/catalyst/src/cli/index.spec.ts +++ b/packages/catalyst/src/cli/index.spec.ts @@ -26,7 +26,9 @@ describe('CLI program', () => { expect(commands).toContain('build'); expect(commands).toContain('deploy'); expect(commands).toContain('project'); + const projectCmd = program.commands.find((cmd) => cmd.name() === 'project'); + expect(projectCmd?.commands.map((c) => c.name())).toEqual( expect.arrayContaining(['create', 'list', 'link']), ); diff --git a/packages/catalyst/tests/mocks/handlers.ts b/packages/catalyst/tests/mocks/handlers.ts index 0b4425ad1b..a2a46c3b42 100644 --- a/packages/catalyst/tests/mocks/handlers.ts +++ b/packages/catalyst/tests/mocks/handlers.ts @@ -75,18 +75,14 @@ export const handlers = [ ), // Handle for createProjects - http.post( - 'https://:apiHost/stores/:storeHash/v3/infrastructure/projects', - async ({ request }) => { - const body = (await request.json()) as { name?: string }; - return HttpResponse.json({ - data: { - uuid: 'c23f5785-fd99-4a94-9fb3-945551623925', - name: body.name ?? 'New Project', - date_created: new Date().toISOString(), - date_modified: new Date().toISOString(), - }, - }); - }, + http.post('https://:apiHost/stores/:storeHash/v3/infrastructure/projects', () => + HttpResponse.json({ + data: { + uuid: 'c23f5785-fd99-4a94-9fb3-945551623925', + name: 'New Project', + date_created: new Date().toISOString(), + date_modified: new Date().toISOString(), + }, + }), ), ]; From bd5c88ed21b8be9e655021554d5636da703f81ff Mon Sep 17 00:00:00 2001 From: James Q Quick Date: Fri, 6 Feb 2026 09:56:54 -0500 Subject: [PATCH 03/16] Update packages/catalyst/src/cli/commands/build.ts Co-authored-by: Chancellor Clark --- packages/catalyst/src/cli/commands/build.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/catalyst/src/cli/commands/build.ts b/packages/catalyst/src/cli/commands/build.ts index 435e3c8d6c..94898ba888 100644 --- a/packages/catalyst/src/cli/commands/build.ts +++ b/packages/catalyst/src/cli/commands/build.ts @@ -60,7 +60,7 @@ export const build = new Command('build') if (!projectUuid) { throw new Error( - 'Project UUID is required. Please run `catalyst project create` or `catalyst project link` or provide `--project-uuid`', + 'Project UUID is required. Please run `catalyst project create` or `catalyst project link` or this command again with --project-uuid .', ); } From b2d95bbdd66b62a37a9029b0d789836d1c3af233 Mon Sep 17 00:00:00 2001 From: jamesqquick Date: Fri, 6 Feb 2026 09:06:10 -0600 Subject: [PATCH 04/16] merge link tests into project --- .../catalyst/src/cli/commands/project.spec.ts | 252 ++++++++++++++++++ 1 file changed, 252 insertions(+) diff --git a/packages/catalyst/src/cli/commands/project.spec.ts b/packages/catalyst/src/cli/commands/project.spec.ts index 68a32cab57..8bdb49142b 100644 --- a/packages/catalyst/src/cli/commands/project.spec.ts +++ b/packages/catalyst/src/cli/commands/project.spec.ts @@ -474,3 +474,255 @@ describe('project link', () => { expect(exitMock).toHaveBeenCalledWith(1); }); }); + +test('link: properly configured Command instance', () => { + expect(link).toBeInstanceOf(Command); + expect(link.name()).toBe('link'); + expect(link.description()).toBe( + 'Link your local Catalyst project to a BigCommerce infrastructure project. You can provide a project UUID directly, or fetch and select from available projects using your store credentials.', + ); + expect(link.options).toEqual( + expect.arrayContaining([ + expect.objectContaining({ flags: '--store-hash ' }), + expect.objectContaining({ flags: '--access-token ' }), + expect.objectContaining({ flags: '--api-host ', defaultValue: 'api.bigcommerce.com' }), + expect.objectContaining({ flags: '--project-uuid ' }), + expect.objectContaining({ flags: '--root-dir ', defaultValue: process.cwd() }), + ]), + ); +}); + +test('link: sets projectUuid when called with --project-uuid', async () => { + await program.parseAsync([ + 'node', + 'catalyst', + 'project', + 'link', + '--project-uuid', + projectUuid1, + '--root-dir', + tmpDir, + ]); + + expect(consola.start).toHaveBeenCalledWith( + 'Writing project UUID to .bigcommerce/project.json...', + ); + expect(consola.success).toHaveBeenCalledWith( + 'Project UUID written to .bigcommerce/project.json.', + ); + expect(exitMock).toHaveBeenCalledWith(0); + expect(config.get('projectUuid')).toBe(projectUuid1); + expect(config.get('framework')).toBe('catalyst'); +}); + +test('link: fetches projects and prompts user to select one', async () => { + const consolaPromptMock = vi + .spyOn(consola, 'prompt') + .mockImplementation(async (message, opts) => { + expect(message).toContain( + 'Select a project or create a new project (Press to select).', + ); + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const options = (opts as { options: Array<{ label: string; value: string }> }).options; + + expect(options).toHaveLength(3); + expect(options[0]).toMatchObject({ label: 'Project One', value: projectUuid1 }); + expect(options[1]).toMatchObject({ + label: 'Project Two', + value: projectUuid2, + }); + expect(options[2]).toMatchObject({ label: 'Create a new project', value: 'create' }); + + return new Promise((resolve) => resolve(projectUuid2)); + }); + + await program.parseAsync([ + 'node', + 'catalyst', + 'project', + 'link', + '--store-hash', + storeHash, + '--access-token', + accessToken, + '--root-dir', + tmpDir, + ]); + + expect(mockIdentify).toHaveBeenCalledWith(storeHash); + + expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); + expect(consola.success).toHaveBeenCalledWith('Projects fetched.'); + + expect(consola.start).toHaveBeenCalledWith( + 'Writing project UUID to .bigcommerce/project.json...', + ); + expect(consola.success).toHaveBeenCalledWith( + 'Project UUID written to .bigcommerce/project.json.', + ); + + expect(exitMock).toHaveBeenCalledWith(0); + + expect(config.get('projectUuid')).toBe(projectUuid2); + expect(config.get('framework')).toBe('catalyst'); + + consolaPromptMock.mockRestore(); +}); + +test('link: prompts to create a new project', async () => { + const consolaPromptMock = vi + .spyOn(consola, 'prompt') + .mockImplementationOnce(async (message, opts) => { + expect(message).toContain( + 'Select a project or create a new project (Press to select).', + ); + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const options = (opts as { options: Array<{ label: string; value: string }> }).options; + + expect(options).toHaveLength(3); + expect(options[0]).toMatchObject({ label: 'Project One', value: projectUuid1 }); + expect(options[1]).toMatchObject({ + label: 'Project Two', + value: projectUuid2, + }); + expect(options[2]).toMatchObject({ label: 'Create a new project', value: 'create' }); + + return new Promise((resolve) => resolve('create')); + }) + .mockImplementationOnce(async (message) => { + expect(message).toBe('Enter a name for the new project:'); + + return new Promise((resolve) => resolve('New Project')); + }); + + await program.parseAsync([ + 'node', + 'catalyst', + 'project', + 'link', + '--store-hash', + storeHash, + '--access-token', + accessToken, + '--root-dir', + tmpDir, + ]); + + expect(mockIdentify).toHaveBeenCalledWith(storeHash); + + expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); + expect(consola.success).toHaveBeenCalledWith('Projects fetched.'); + + expect(consola.success).toHaveBeenCalledWith('Project "New Project" created successfully.'); + + expect(exitMock).toHaveBeenCalledWith(0); + + expect(config.get('projectUuid')).toBe(projectUuid3); + expect(config.get('framework')).toBe('catalyst'); + + consolaPromptMock.mockRestore(); +}); + +test('link: errors when create project API fails', async () => { + server.use( + http.post('https://:apiHost/stores/:storeHash/v3/infrastructure/projects', () => + HttpResponse.json({}, { status: 502 }), + ), + ); + + const consolaPromptMock = vi + .spyOn(consola, 'prompt') + .mockImplementationOnce(async (message, opts) => { + expect(message).toContain( + 'Select a project or create a new project (Press to select).', + ); + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const options = (opts as { options: Array<{ label: string; value: string }> }).options; + + expect(options).toHaveLength(3); + expect(options[0]).toMatchObject({ label: 'Project One', value: projectUuid1 }); + expect(options[1]).toMatchObject({ + label: 'Project Two', + value: projectUuid2, + }); + expect(options[2]).toMatchObject({ label: 'Create a new project', value: 'create' }); + + return new Promise((resolve) => resolve('create')); + }) + .mockImplementationOnce(async (message) => { + expect(message).toBe('Enter a name for the new project:'); + + return new Promise((resolve) => resolve('New Project')); + }); + + await program.parseAsync([ + 'node', + 'catalyst', + 'project', + 'link', + '--store-hash', + storeHash, + '--access-token', + accessToken, + '--root-dir', + tmpDir, + ]); + + expect(mockIdentify).toHaveBeenCalledWith(storeHash); + + expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); + expect(consola.success).toHaveBeenCalledWith('Projects fetched.'); + + expect(consola.error).toHaveBeenCalledWith( + 'Failed to create project, is the name already in use?', + ); + + expect(exitMock).toHaveBeenCalledWith(1); + + consolaPromptMock.mockRestore(); +}); + +test('link: errors when infrastructure projects API is not found', async () => { + server.use( + http.get('https://:apiHost/stores/:storeHash/v3/infrastructure/projects', () => + HttpResponse.json({}, { status: 403 }), + ), + ); + + await program.parseAsync([ + 'node', + 'catalyst', + 'project', + 'link', + '--store-hash', + storeHash, + '--access-token', + accessToken, + '--root-dir', + tmpDir, + ]); + + expect(mockIdentify).toHaveBeenCalledWith(storeHash); + + expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); + expect(consola.error).toHaveBeenCalledWith( + 'Infrastructure Projects API not enabled. If you are part of the alpha, contact support@bigcommerce.com to enable it.', + ); +}); + +test('link: errors when no projectUuid, storeHash, or accessToken are provided', async () => { + await program.parseAsync(['node', 'catalyst', 'project', 'link', '--root-dir', tmpDir]); + + expect(consola.start).not.toHaveBeenCalled(); + expect(consola.success).not.toHaveBeenCalled(); + expect(consola.error).toHaveBeenCalledWith('Insufficient information to link a project.'); + expect(consola.info).toHaveBeenCalledWith('Provide a project UUID with --project-uuid, or'); + expect(consola.info).toHaveBeenCalledWith( + 'Provide both --store-hash and --access-token to fetch and select a project.', + ); + + expect(exitMock).toHaveBeenCalledWith(1); +}); From 1f8c78be575dbf46c645c97de8329cab51a99ae2 Mon Sep 17 00:00:00 2001 From: jamesqquick Date: Fri, 6 Feb 2026 09:15:44 -0600 Subject: [PATCH 05/16] organize tests for project using describe --- .../catalyst/src/cli/commands/project.spec.ts | 488 ++++++++++-------- 1 file changed, 267 insertions(+), 221 deletions(-) diff --git a/packages/catalyst/src/cli/commands/project.spec.ts b/packages/catalyst/src/cli/commands/project.spec.ts index 8bdb49142b..784dbc915b 100644 --- a/packages/catalyst/src/cli/commands/project.spec.ts +++ b/packages/catalyst/src/cli/commands/project.spec.ts @@ -475,254 +475,300 @@ describe('project link', () => { }); }); -test('link: properly configured Command instance', () => { - expect(link).toBeInstanceOf(Command); - expect(link.name()).toBe('link'); - expect(link.description()).toBe( - 'Link your local Catalyst project to a BigCommerce infrastructure project. You can provide a project UUID directly, or fetch and select from available projects using your store credentials.', - ); - expect(link.options).toEqual( - expect.arrayContaining([ - expect.objectContaining({ flags: '--store-hash ' }), - expect.objectContaining({ flags: '--access-token ' }), - expect.objectContaining({ flags: '--api-host ', defaultValue: 'api.bigcommerce.com' }), - expect.objectContaining({ flags: '--project-uuid ' }), - expect.objectContaining({ flags: '--root-dir ', defaultValue: process.cwd() }), - ]), - ); -}); +describe('project list', () => { + test('fetches and displays projects', async () => { + await program.parseAsync([ + 'node', + 'catalyst', + 'project', + 'list', + '--store-hash', + storeHash, + '--access-token', + accessToken, + ]); + + expect(mockIdentify).toHaveBeenCalledWith(storeHash); + expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); + expect(consola.success).toHaveBeenCalledWith('Projects fetched.'); + expect(consola.log).toHaveBeenCalledWith('Project One (a23f5785-fd99-4a94-9fb3-945551623923)'); + expect(consola.log).toHaveBeenCalledWith('Project Two (b23f5785-fd99-4a94-9fb3-945551623924)'); + expect(exitMock).toHaveBeenCalledWith(0); + }); -test('link: sets projectUuid when called with --project-uuid', async () => { - await program.parseAsync([ - 'node', - 'catalyst', - 'project', - 'link', - '--project-uuid', - projectUuid1, - '--root-dir', - tmpDir, - ]); - - expect(consola.start).toHaveBeenCalledWith( - 'Writing project UUID to .bigcommerce/project.json...', - ); - expect(consola.success).toHaveBeenCalledWith( - 'Project UUID written to .bigcommerce/project.json.', - ); - expect(exitMock).toHaveBeenCalledWith(0); - expect(config.get('projectUuid')).toBe(projectUuid1); - expect(config.get('framework')).toBe('catalyst'); + test('with insufficient credentials exits with error', async () => { + const savedStoreHash = process.env.BIGCOMMERCE_STORE_HASH; + const savedAccessToken = process.env.BIGCOMMERCE_ACCESS_TOKEN; + + delete process.env.BIGCOMMERCE_STORE_HASH; + delete process.env.BIGCOMMERCE_ACCESS_TOKEN; + + await program.parseAsync(['node', 'catalyst', 'project', 'list']); + + if (savedStoreHash !== undefined) process.env.BIGCOMMERCE_STORE_HASH = savedStoreHash; + if (savedAccessToken !== undefined) process.env.BIGCOMMERCE_ACCESS_TOKEN = savedAccessToken; + + expect(consola.error).toHaveBeenCalledWith('Insufficient information to list projects.'); + expect(consola.info).toHaveBeenCalledWith( + 'Provide both --store-hash and --access-token (or set BIGCOMMERCE_STORE_HASH and BIGCOMMERCE_ACCESS_TOKEN).', + ); + expect(exitMock).toHaveBeenCalledWith(1); + }); }); -test('link: fetches projects and prompts user to select one', async () => { - const consolaPromptMock = vi - .spyOn(consola, 'prompt') - .mockImplementation(async (message, opts) => { - expect(message).toContain( - 'Select a project or create a new project (Press to select).', - ); - - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const options = (opts as { options: Array<{ label: string; value: string }> }).options; - - expect(options).toHaveLength(3); - expect(options[0]).toMatchObject({ label: 'Project One', value: projectUuid1 }); - expect(options[1]).toMatchObject({ - label: 'Project Two', - value: projectUuid2, +describe('project link', () => { + test('properly configured Command instance', () => { + expect(link).toBeInstanceOf(Command); + expect(link.name()).toBe('link'); + expect(link.description()).toBe( + 'Link your local Catalyst project to a BigCommerce infrastructure project. You can provide a project UUID directly, or fetch and select from available projects using your store credentials.', + ); + expect(link.options).toEqual( + expect.arrayContaining([ + expect.objectContaining({ flags: '--store-hash ' }), + expect.objectContaining({ flags: '--access-token ' }), + expect.objectContaining({ + flags: '--api-host ', + defaultValue: 'api.bigcommerce.com', + }), + expect.objectContaining({ flags: '--project-uuid ' }), + expect.objectContaining({ flags: '--root-dir ', defaultValue: process.cwd() }), + ]), + ); + }); + + test('sets projectUuid when called with --project-uuid', async () => { + await program.parseAsync([ + 'node', + 'catalyst', + 'project', + 'link', + '--project-uuid', + projectUuid1, + '--root-dir', + tmpDir, + ]); + + expect(consola.start).toHaveBeenCalledWith( + 'Writing project UUID to .bigcommerce/project.json...', + ); + expect(consola.success).toHaveBeenCalledWith( + 'Project UUID written to .bigcommerce/project.json.', + ); + expect(exitMock).toHaveBeenCalledWith(0); + expect(config.get('projectUuid')).toBe(projectUuid1); + expect(config.get('framework')).toBe('catalyst'); + }); + + test('fetches projects and prompts user to select one', async () => { + const consolaPromptMock = vi + .spyOn(consola, 'prompt') + .mockImplementation(async (message, opts) => { + expect(message).toContain( + 'Select a project or create a new project (Press to select).', + ); + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const options = (opts as { options: Array<{ label: string; value: string }> }).options; + + expect(options).toHaveLength(3); + expect(options[0]).toMatchObject({ label: 'Project One', value: projectUuid1 }); + expect(options[1]).toMatchObject({ + label: 'Project Two', + value: projectUuid2, + }); + expect(options[2]).toMatchObject({ label: 'Create a new project', value: 'create' }); + + return new Promise((resolve) => resolve(projectUuid2)); }); - expect(options[2]).toMatchObject({ label: 'Create a new project', value: 'create' }); - - return new Promise((resolve) => resolve(projectUuid2)); - }); - - await program.parseAsync([ - 'node', - 'catalyst', - 'project', - 'link', - '--store-hash', - storeHash, - '--access-token', - accessToken, - '--root-dir', - tmpDir, - ]); - - expect(mockIdentify).toHaveBeenCalledWith(storeHash); - - expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); - expect(consola.success).toHaveBeenCalledWith('Projects fetched.'); - - expect(consola.start).toHaveBeenCalledWith( - 'Writing project UUID to .bigcommerce/project.json...', - ); - expect(consola.success).toHaveBeenCalledWith( - 'Project UUID written to .bigcommerce/project.json.', - ); - - expect(exitMock).toHaveBeenCalledWith(0); - - expect(config.get('projectUuid')).toBe(projectUuid2); - expect(config.get('framework')).toBe('catalyst'); - - consolaPromptMock.mockRestore(); -}); -test('link: prompts to create a new project', async () => { - const consolaPromptMock = vi - .spyOn(consola, 'prompt') - .mockImplementationOnce(async (message, opts) => { - expect(message).toContain( - 'Select a project or create a new project (Press to select).', - ); - - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const options = (opts as { options: Array<{ label: string; value: string }> }).options; - - expect(options).toHaveLength(3); - expect(options[0]).toMatchObject({ label: 'Project One', value: projectUuid1 }); - expect(options[1]).toMatchObject({ - label: 'Project Two', - value: projectUuid2, + await program.parseAsync([ + 'node', + 'catalyst', + 'project', + 'link', + '--store-hash', + storeHash, + '--access-token', + accessToken, + '--root-dir', + tmpDir, + ]); + + expect(mockIdentify).toHaveBeenCalledWith(storeHash); + + expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); + expect(consola.success).toHaveBeenCalledWith('Projects fetched.'); + + expect(consola.start).toHaveBeenCalledWith( + 'Writing project UUID to .bigcommerce/project.json...', + ); + expect(consola.success).toHaveBeenCalledWith( + 'Project UUID written to .bigcommerce/project.json.', + ); + + expect(exitMock).toHaveBeenCalledWith(0); + + expect(config.get('projectUuid')).toBe(projectUuid2); + expect(config.get('framework')).toBe('catalyst'); + + consolaPromptMock.mockRestore(); + }); + + test('prompts to create a new project', async () => { + const consolaPromptMock = vi + .spyOn(consola, 'prompt') + .mockImplementationOnce(async (message, opts) => { + expect(message).toContain( + 'Select a project or create a new project (Press to select).', + ); + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const options = (opts as { options: Array<{ label: string; value: string }> }).options; + + expect(options).toHaveLength(3); + expect(options[0]).toMatchObject({ label: 'Project One', value: projectUuid1 }); + expect(options[1]).toMatchObject({ + label: 'Project Two', + value: projectUuid2, + }); + expect(options[2]).toMatchObject({ label: 'Create a new project', value: 'create' }); + + return new Promise((resolve) => resolve('create')); + }) + .mockImplementationOnce(async (message) => { + expect(message).toBe('Enter a name for the new project:'); + + return new Promise((resolve) => resolve('New Project')); }); - expect(options[2]).toMatchObject({ label: 'Create a new project', value: 'create' }); - return new Promise((resolve) => resolve('create')); - }) - .mockImplementationOnce(async (message) => { - expect(message).toBe('Enter a name for the new project:'); + await program.parseAsync([ + 'node', + 'catalyst', + 'project', + 'link', + '--store-hash', + storeHash, + '--access-token', + accessToken, + '--root-dir', + tmpDir, + ]); + + expect(mockIdentify).toHaveBeenCalledWith(storeHash); - return new Promise((resolve) => resolve('New Project')); - }); + expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); + expect(consola.success).toHaveBeenCalledWith('Projects fetched.'); - await program.parseAsync([ - 'node', - 'catalyst', - 'project', - 'link', - '--store-hash', - storeHash, - '--access-token', - accessToken, - '--root-dir', - tmpDir, - ]); + expect(consola.success).toHaveBeenCalledWith('Project "New Project" created successfully.'); - expect(mockIdentify).toHaveBeenCalledWith(storeHash); + expect(exitMock).toHaveBeenCalledWith(0); + + expect(config.get('projectUuid')).toBe(projectUuid3); + expect(config.get('framework')).toBe('catalyst'); - expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); - expect(consola.success).toHaveBeenCalledWith('Projects fetched.'); + consolaPromptMock.mockRestore(); + }); - expect(consola.success).toHaveBeenCalledWith('Project "New Project" created successfully.'); + test('errors when create project API fails', async () => { + server.use( + http.post('https://:apiHost/stores/:storeHash/v3/infrastructure/projects', () => + HttpResponse.json({}, { status: 502 }), + ), + ); - expect(exitMock).toHaveBeenCalledWith(0); + const consolaPromptMock = vi + .spyOn(consola, 'prompt') + .mockImplementationOnce(async (message, opts) => { + expect(message).toContain( + 'Select a project or create a new project (Press to select).', + ); - expect(config.get('projectUuid')).toBe(projectUuid3); - expect(config.get('framework')).toBe('catalyst'); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const options = (opts as { options: Array<{ label: string; value: string }> }).options; - consolaPromptMock.mockRestore(); -}); + expect(options).toHaveLength(3); + expect(options[0]).toMatchObject({ label: 'Project One', value: projectUuid1 }); + expect(options[1]).toMatchObject({ + label: 'Project Two', + value: projectUuid2, + }); + expect(options[2]).toMatchObject({ label: 'Create a new project', value: 'create' }); -test('link: errors when create project API fails', async () => { - server.use( - http.post('https://:apiHost/stores/:storeHash/v3/infrastructure/projects', () => - HttpResponse.json({}, { status: 502 }), - ), - ); - - const consolaPromptMock = vi - .spyOn(consola, 'prompt') - .mockImplementationOnce(async (message, opts) => { - expect(message).toContain( - 'Select a project or create a new project (Press to select).', - ); - - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const options = (opts as { options: Array<{ label: string; value: string }> }).options; - - expect(options).toHaveLength(3); - expect(options[0]).toMatchObject({ label: 'Project One', value: projectUuid1 }); - expect(options[1]).toMatchObject({ - label: 'Project Two', - value: projectUuid2, + return new Promise((resolve) => resolve('create')); + }) + .mockImplementationOnce(async (message) => { + expect(message).toBe('Enter a name for the new project:'); + + return new Promise((resolve) => resolve('New Project')); }); - expect(options[2]).toMatchObject({ label: 'Create a new project', value: 'create' }); - return new Promise((resolve) => resolve('create')); - }) - .mockImplementationOnce(async (message) => { - expect(message).toBe('Enter a name for the new project:'); + await program.parseAsync([ + 'node', + 'catalyst', + 'project', + 'link', + '--store-hash', + storeHash, + '--access-token', + accessToken, + '--root-dir', + tmpDir, + ]); + + expect(mockIdentify).toHaveBeenCalledWith(storeHash); - return new Promise((resolve) => resolve('New Project')); - }); + expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); + expect(consola.success).toHaveBeenCalledWith('Projects fetched.'); - await program.parseAsync([ - 'node', - 'catalyst', - 'project', - 'link', - '--store-hash', - storeHash, - '--access-token', - accessToken, - '--root-dir', - tmpDir, - ]); + expect(consola.error).toHaveBeenCalledWith( + 'Failed to create project, is the name already in use?', + ); - expect(mockIdentify).toHaveBeenCalledWith(storeHash); + expect(exitMock).toHaveBeenCalledWith(1); - expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); - expect(consola.success).toHaveBeenCalledWith('Projects fetched.'); + consolaPromptMock.mockRestore(); + }); - expect(consola.error).toHaveBeenCalledWith( - 'Failed to create project, is the name already in use?', - ); + test('errors when infrastructure projects API is not found', async () => { + server.use( + http.get('https://:apiHost/stores/:storeHash/v3/infrastructure/projects', () => + HttpResponse.json({}, { status: 403 }), + ), + ); - expect(exitMock).toHaveBeenCalledWith(1); + await program.parseAsync([ + 'node', + 'catalyst', + 'project', + 'link', + '--store-hash', + storeHash, + '--access-token', + accessToken, + '--root-dir', + tmpDir, + ]); - consolaPromptMock.mockRestore(); -}); + expect(mockIdentify).toHaveBeenCalledWith(storeHash); -test('link: errors when infrastructure projects API is not found', async () => { - server.use( - http.get('https://:apiHost/stores/:storeHash/v3/infrastructure/projects', () => - HttpResponse.json({}, { status: 403 }), - ), - ); - - await program.parseAsync([ - 'node', - 'catalyst', - 'project', - 'link', - '--store-hash', - storeHash, - '--access-token', - accessToken, - '--root-dir', - tmpDir, - ]); - - expect(mockIdentify).toHaveBeenCalledWith(storeHash); - - expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); - expect(consola.error).toHaveBeenCalledWith( - 'Infrastructure Projects API not enabled. If you are part of the alpha, contact support@bigcommerce.com to enable it.', - ); -}); + expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); + expect(consola.error).toHaveBeenCalledWith( + 'Infrastructure Projects API not enabled. If you are part of the alpha, contact support@bigcommerce.com to enable it.', + ); + }); -test('link: errors when no projectUuid, storeHash, or accessToken are provided', async () => { - await program.parseAsync(['node', 'catalyst', 'project', 'link', '--root-dir', tmpDir]); + test('errors when no projectUuid, storeHash, or accessToken are provided', async () => { + await program.parseAsync(['node', 'catalyst', 'project', 'link', '--root-dir', tmpDir]); - expect(consola.start).not.toHaveBeenCalled(); - expect(consola.success).not.toHaveBeenCalled(); - expect(consola.error).toHaveBeenCalledWith('Insufficient information to link a project.'); - expect(consola.info).toHaveBeenCalledWith('Provide a project UUID with --project-uuid, or'); - expect(consola.info).toHaveBeenCalledWith( - 'Provide both --store-hash and --access-token to fetch and select a project.', - ); + expect(consola.start).not.toHaveBeenCalled(); + expect(consola.success).not.toHaveBeenCalled(); + expect(consola.error).toHaveBeenCalledWith('Insufficient information to link a project.'); + expect(consola.info).toHaveBeenCalledWith('Provide a project UUID with --project-uuid, or'); + expect(consola.info).toHaveBeenCalledWith( + 'Provide both --store-hash and --access-token to fetch and select a project.', + ); - expect(exitMock).toHaveBeenCalledWith(1); + expect(exitMock).toHaveBeenCalledWith(1); + }); }); From 075d2ae88634f47d76ca4ecf75c5cb923ef73dd6 Mon Sep 17 00:00:00 2001 From: jamesqquick Date: Thu, 5 Feb 2026 14:05:37 -0600 Subject: [PATCH 06/16] remove loading of default env files --- packages/catalyst/src/cli/program.ts | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/packages/catalyst/src/cli/program.ts b/packages/catalyst/src/cli/program.ts index 4a99a17ac5..f3811aa1eb 100644 --- a/packages/catalyst/src/cli/program.ts +++ b/packages/catalyst/src/cli/program.ts @@ -1,7 +1,5 @@ import { Command } from 'commander'; import { colorize } from 'consola/utils'; -import { config } from 'dotenv'; -import { resolve } from 'node:path'; import PACKAGE_INFO from '../../package.json'; @@ -17,22 +15,16 @@ import { consola } from './lib/logger'; export const program = new Command(); -config({ - path: [ - resolve(process.cwd(), '.env'), - resolve(process.cwd(), '.env.local'), - // Assumes the parent directory is the monorepo root: - resolve(process.cwd(), '..', '.env'), - resolve(process.cwd(), '..', '.env.local'), - ], -}); - consola.log(colorize('cyanBright', `◢ ${PACKAGE_INFO.name} v${PACKAGE_INFO.version}\n`)); program .name(PACKAGE_INFO.name) .version(PACKAGE_INFO.version) .description('CLI tool for Catalyst development') + .option( + '--env-file ', + 'Path to an environment variable file to load (relative to current directory)', + ) .addCommand(version) .addCommand(dev) .addCommand(start) From ff50393cf223719cd5d7c5c12a44511cf3cd4c02 Mon Sep 17 00:00:00 2001 From: jamesqquick Date: Thu, 5 Feb 2026 14:07:39 -0600 Subject: [PATCH 07/16] rename env variable prefix, write config properties to project.json, and define priority for config variables --- packages/catalyst/src/cli/commands/build.ts | 9 ++++----- .../catalyst/src/cli/commands/deploy.spec.ts | 8 ++++---- packages/catalyst/src/cli/commands/deploy.ts | 6 +++--- packages/catalyst/src/cli/commands/start.ts | 7 +++---- packages/catalyst/src/cli/index.ts | 17 +++++++++++++++++ packages/catalyst/src/cli/lib/project-config.ts | 4 ++++ 6 files changed, 35 insertions(+), 16 deletions(-) diff --git a/packages/catalyst/src/cli/commands/build.ts b/packages/catalyst/src/cli/commands/build.ts index 94898ba888..8e95f72e18 100644 --- a/packages/catalyst/src/cli/commands/build.ts +++ b/packages/catalyst/src/cli/commands/build.ts @@ -22,13 +22,12 @@ export const build = new Command('build') new Option( '--project-uuid ', 'Project UUID to be included in the deployment configuration.', - ).env('BIGCOMMERCE_PROJECT_UUID'), + ).env('CATALYST_PROJECT_UUID'), ) .addOption( - new Option('--framework ', 'The framework to use for the build.').choices([ - 'nextjs', - 'catalyst', - ]), + new Option('--framework ', 'The framework to use for the build.') + .env('CATALYST_FRAMEWORK') + .choices(['nextjs', 'catalyst']), ) .action(async (nextBuildOptions, options) => { const coreDir = process.cwd(); diff --git a/packages/catalyst/src/cli/commands/deploy.spec.ts b/packages/catalyst/src/cli/commands/deploy.spec.ts index 22e7c81ddf..a612caa217 100644 --- a/packages/catalyst/src/cli/commands/deploy.spec.ts +++ b/packages/catalyst/src/cli/commands/deploy.spec.ts @@ -306,19 +306,19 @@ test('--dry-run skips upload and deployment', async () => { test('reads from env options', () => { const envVariables = parseEnvironmentVariables([ - 'BIGCOMMERCE_STORE_HASH=123', - 'BIGCOMMERCE_STOREFRONT_TOKEN=456', + 'CATALYST_STORE_HASH=123', + 'CATALYST_ACCESS_TOKEN=456', ]); expect(envVariables).toEqual([ { type: 'secret', - key: 'BIGCOMMERCE_STORE_HASH', + key: 'CATALYST_STORE_HASH', value: '123', }, { type: 'secret', - key: 'BIGCOMMERCE_STOREFRONT_TOKEN', + key: 'CATALYST_ACCESS_TOKEN', value: '456', }, ]); diff --git a/packages/catalyst/src/cli/commands/deploy.ts b/packages/catalyst/src/cli/commands/deploy.ts index eee50458f6..ffadd240bd 100644 --- a/packages/catalyst/src/cli/commands/deploy.ts +++ b/packages/catalyst/src/cli/commands/deploy.ts @@ -284,7 +284,7 @@ export const deploy = new Command('deploy') '--store-hash ', 'BigCommerce store hash. Can be found in the URL of your store Control Panel.', ) - .env('BIGCOMMERCE_STORE_HASH') + .env('CATALYST_STORE_HASH') .makeOptionMandatory(), ) .addOption( @@ -292,7 +292,7 @@ export const deploy = new Command('deploy') '--access-token ', 'BigCommerce access token. Can be found after creating a store-level API account.', ) - .env('BIGCOMMERCE_ACCESS_TOKEN') + .env('CATALYST_ACCESS_TOKEN') .makeOptionMandatory(), ) .addOption( @@ -304,7 +304,7 @@ export const deploy = new Command('deploy') new Option( '--project-uuid ', 'BigCommerce intrastructure project UUID. Can be found via the BigCommerce API (GET /v3/infrastructure/projects).', - ).env('BIGCOMMERCE_PROJECT_UUID'), + ).env('CATALYST_PROJECT_UUID'), ) .addOption( new Option( diff --git a/packages/catalyst/src/cli/commands/start.ts b/packages/catalyst/src/cli/commands/start.ts index 2a0c68cbe3..2f56fbc35b 100644 --- a/packages/catalyst/src/cli/commands/start.ts +++ b/packages/catalyst/src/cli/commands/start.ts @@ -15,10 +15,9 @@ export const start = new Command('start') 'Pass additional options to the start command. If framework is Next.js, see https://nextjs.org/docs/api-reference/cli#start for available options.', ) .addOption( - new Option('--framework ', 'The framework to use for the preview').choices([ - 'catalyst', - 'nextjs', - ]), + new Option('--framework ', 'The framework to use for the preview') + .env('CATALYST_FRAMEWORK') + .choices(['catalyst', 'nextjs']), ) .action(async (startOptions, options) => { try { diff --git a/packages/catalyst/src/cli/index.ts b/packages/catalyst/src/cli/index.ts index 42c5a8a765..04cb57509d 100644 --- a/packages/catalyst/src/cli/index.ts +++ b/packages/catalyst/src/cli/index.ts @@ -1,4 +1,21 @@ #!/usr/bin/env node +import { config } from 'dotenv'; +import { resolve } from 'node:path'; + import { program } from './program'; +// Load --env-file before parse so option resolution uses this order: +// 1. Explicit flags 2. --env-file (if passed) 3. process.env (shell) 4. project.json +const envFileIndex = process.argv.findIndex( + (arg) => arg === '--env-file' || arg.startsWith('--env-file='), +); +if (envFileIndex !== -1) { + const arg = process.argv[envFileIndex]; + const path = + arg === '--env-file' ? process.argv[envFileIndex + 1] : arg.slice('--env-file='.length); + if (path) { + config({ path: resolve(process.cwd(), path), override: true }); + } +} + program.parse(process.argv); diff --git a/packages/catalyst/src/cli/lib/project-config.ts b/packages/catalyst/src/cli/lib/project-config.ts index 20cd65f98d..3ee0770b74 100644 --- a/packages/catalyst/src/cli/lib/project-config.ts +++ b/packages/catalyst/src/cli/lib/project-config.ts @@ -4,6 +4,8 @@ import { join } from 'path'; export interface ProjectConfigSchema { projectUuid: string; framework: 'catalyst' | 'nextjs'; + storeHash?: string; + accessToken?: string; telemetry: { enabled: boolean; anonymousId: string; @@ -22,6 +24,8 @@ export function getProjectConfig(rootDir = process.cwd()) { enum: ['catalyst', 'nextjs'], default: 'nextjs', }, + storeHash: { type: 'string' }, + accessToken: { type: 'string' }, telemetry: { type: 'object', properties: { From 7475d6f8a70ce31c6e7c835b567fb585d23c0e10 Mon Sep 17 00:00:00 2001 From: jamesqquick Date: Thu, 5 Feb 2026 14:30:17 -0600 Subject: [PATCH 08/16] updated error message logging --- packages/catalyst/src/cli/commands/build.ts | 2 +- packages/catalyst/src/cli/commands/deploy.ts | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/catalyst/src/cli/commands/build.ts b/packages/catalyst/src/cli/commands/build.ts index 8e95f72e18..1e18558c9c 100644 --- a/packages/catalyst/src/cli/commands/build.ts +++ b/packages/catalyst/src/cli/commands/build.ts @@ -21,7 +21,7 @@ export const build = new Command('build') .addOption( new Option( '--project-uuid ', - 'Project UUID to be included in the deployment configuration.', + 'Project UUID to be included in the deployment configuration. Can also be set via the CATALYST_PROJECT_UUID environment variable.', ).env('CATALYST_PROJECT_UUID'), ) .addOption( diff --git a/packages/catalyst/src/cli/commands/deploy.ts b/packages/catalyst/src/cli/commands/deploy.ts index ffadd240bd..025569498c 100644 --- a/packages/catalyst/src/cli/commands/deploy.ts +++ b/packages/catalyst/src/cli/commands/deploy.ts @@ -290,7 +290,7 @@ export const deploy = new Command('deploy') .addOption( new Option( '--access-token ', - 'BigCommerce access token. Can be found after creating a store-level API account.', + 'BigCommerce access token. Can be found after creating a store-level API account. Can also be set via the CATALYST_ACCESS_TOKEN environment variable.', ) .env('CATALYST_ACCESS_TOKEN') .makeOptionMandatory(), @@ -303,7 +303,7 @@ export const deploy = new Command('deploy') .addOption( new Option( '--project-uuid ', - 'BigCommerce intrastructure project UUID. Can be found via the BigCommerce API (GET /v3/infrastructure/projects).', + 'BigCommerce infrastructure project UUID. Can be found via the BigCommerce API (GET /v3/infrastructure/projects). Can also be set via the CATALYST_PROJECT_UUID environment variable.', ).env('CATALYST_PROJECT_UUID'), ) .addOption( @@ -324,7 +324,7 @@ export const deploy = new Command('deploy') if (!projectUuid) { throw new Error( - 'Project UUID is required. Please run either `catalyst project link` or `catalyst project create` or this command again with --project-uuid .', + 'Project UUID is required. Please run either `catalyst link` or provide --project-uuid (or set the CATALYST_PROJECT_UUID environment variable).', ); } From e0734dade18736dffec27f1bb0b4adb1a521c02a Mon Sep 17 00:00:00 2001 From: jamesqquick Date: Fri, 6 Feb 2026 08:53:41 -0600 Subject: [PATCH 09/16] update env variable names and log messages --- .../catalyst/src/cli/commands/link.spec.ts | 328 ++++++++++++++++++ packages/catalyst/src/cli/commands/project.ts | 61 +++- 2 files changed, 370 insertions(+), 19 deletions(-) create mode 100644 packages/catalyst/src/cli/commands/link.spec.ts diff --git a/packages/catalyst/src/cli/commands/link.spec.ts b/packages/catalyst/src/cli/commands/link.spec.ts new file mode 100644 index 0000000000..ecab10e6d3 --- /dev/null +++ b/packages/catalyst/src/cli/commands/link.spec.ts @@ -0,0 +1,328 @@ +import { Command } from 'commander'; +import Conf from 'conf'; +import { http, HttpResponse } from 'msw'; +import { afterAll, afterEach, beforeAll, 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 { link } from './project'; + +let exitMock: MockInstance; + +let tmpDir: string; +let cleanup: () => Promise; +let config: Conf; + +const { mockIdentify } = vi.hoisted(() => ({ + mockIdentify: vi.fn(), +})); + +const projectUuid1 = 'a23f5785-fd99-4a94-9fb3-945551623923'; +const projectUuid2 = 'b23f5785-fd99-4a94-9fb3-945551623924'; +const projectUuid3 = 'c23f5785-fd99-4a94-9fb3-945551623925'; +const storeHash = 'test-store'; +const accessToken = 'test-token'; + +beforeAll(async () => { + consola.mockTypes(() => vi.fn()); + + vi.mock('../lib/telemetry', () => { + return { + Telemetry: vi.fn().mockImplementation(() => { + return { + identify: mockIdentify, + isEnabled: vi.fn(() => true), + track: vi.fn(), + analytics: { + closeAndFlush: vi.fn(), + }, + }; + }), + }; + }); + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + exitMock = vi.spyOn(process, 'exit').mockImplementation(() => null as never); + + [tmpDir, cleanup] = await mkTempDir(); + + config = getProjectConfig(tmpDir); +}); + +afterEach(() => { + vi.clearAllMocks(); +}); + +afterAll(async () => { + vi.restoreAllMocks(); + exitMock.mockRestore(); + + await cleanup(); +}); + +test('properly configured Command instance', () => { + expect(link).toBeInstanceOf(Command); + expect(link.name()).toBe('link'); + expect(link.description()).toBe( + 'Link your local Catalyst project to a BigCommerce infrastructure project. You can provide a project UUID directly, or fetch and select from available projects using your store credentials.', + ); + expect(link.options).toEqual( + expect.arrayContaining([ + expect.objectContaining({ flags: '--store-hash ' }), + expect.objectContaining({ flags: '--access-token ' }), + expect.objectContaining({ flags: '--api-host ', defaultValue: 'api.bigcommerce.com' }), + expect.objectContaining({ flags: '--project-uuid ' }), + expect.objectContaining({ flags: '--root-dir ', defaultValue: process.cwd() }), + ]), + ); +}); + +test('sets projectUuid when called with --project-uuid', async () => { + await program.parseAsync([ + 'node', + 'catalyst', + 'project', + 'link', + '--project-uuid', + projectUuid1, + '--root-dir', + tmpDir, + ]); + + expect(consola.start).toHaveBeenCalledWith( + 'Writing project UUID to .bigcommerce/project.json...', + ); + expect(consola.success).toHaveBeenCalledWith( + 'Project UUID written to .bigcommerce/project.json.', + ); + expect(exitMock).toHaveBeenCalledWith(0); + expect(config.get('projectUuid')).toBe(projectUuid1); + expect(config.get('framework')).toBe('catalyst'); +}); + +test('fetches projects and prompts user to select one', async () => { + const consolaPromptMock = vi + .spyOn(consola, 'prompt') + .mockImplementation(async (message, opts) => { + // Assert the prompt message and options + expect(message).toContain( + 'Select a project or create a new project (Press to select).', + ); + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const options = (opts as { options: Array<{ label: string; value: string }> }).options; + + expect(options).toHaveLength(3); + expect(options[0]).toMatchObject({ label: 'Project One', value: projectUuid1 }); + expect(options[1]).toMatchObject({ + label: 'Project Two', + value: projectUuid2, + }); + expect(options[2]).toMatchObject({ label: 'Create a new project', value: 'create' }); + + // Simulate selecting the second option + return new Promise((resolve) => resolve(projectUuid2)); + }); + + await program.parseAsync([ + 'node', + 'catalyst', + 'project', + 'link', + '--store-hash', + storeHash, + '--access-token', + accessToken, + '--root-dir', + tmpDir, + ]); + + expect(mockIdentify).toHaveBeenCalledWith(storeHash); + + expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); + expect(consola.success).toHaveBeenCalledWith('Projects fetched.'); + + expect(consola.start).toHaveBeenCalledWith( + 'Writing project UUID to .bigcommerce/project.json...', + ); + expect(consola.success).toHaveBeenCalledWith( + 'Project UUID written to .bigcommerce/project.json.', + ); + + expect(exitMock).toHaveBeenCalledWith(0); + + expect(config.get('projectUuid')).toBe(projectUuid2); + expect(config.get('framework')).toBe('catalyst'); + + consolaPromptMock.mockRestore(); +}); + +test('prompts to create a new project', async () => { + const consolaPromptMock = vi + .spyOn(consola, 'prompt') + .mockImplementationOnce(async (message, opts) => { + // Assert the prompt message and options + expect(message).toContain( + 'Select a project or create a new project (Press to select).', + ); + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const options = (opts as { options: Array<{ label: string; value: string }> }).options; + + expect(options).toHaveLength(3); + expect(options[0]).toMatchObject({ label: 'Project One', value: projectUuid1 }); + expect(options[1]).toMatchObject({ + label: 'Project Two', + value: projectUuid2, + }); + expect(options[2]).toMatchObject({ label: 'Create a new project', value: 'create' }); + + // Simulate selecting the create option + return new Promise((resolve) => resolve('create')); + }) + .mockImplementationOnce(async (message) => { + expect(message).toBe('Enter a name for the new project:'); + + return new Promise((resolve) => resolve('New Project')); + }); + + await program.parseAsync([ + 'node', + 'catalyst', + 'project', + 'link', + '--store-hash', + storeHash, + '--access-token', + accessToken, + '--root-dir', + tmpDir, + ]); + + expect(mockIdentify).toHaveBeenCalledWith(storeHash); + + expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); + expect(consola.success).toHaveBeenCalledWith('Projects fetched.'); + + expect(consola.success).toHaveBeenCalledWith('Project "New Project" created successfully.'); + + expect(exitMock).toHaveBeenCalledWith(0); + + expect(config.get('projectUuid')).toBe(projectUuid3); + expect(config.get('framework')).toBe('catalyst'); + + consolaPromptMock.mockRestore(); +}); + +test('prompts to create a new project', async () => { + server.use( + http.post('https://:apiHost/stores/:storeHash/v3/infrastructure/projects', () => + HttpResponse.json({}, { status: 502 }), + ), + ); + + const consolaPromptMock = vi + .spyOn(consola, 'prompt') + .mockImplementationOnce(async (message, opts) => { + // Assert the prompt message and options + expect(message).toContain( + 'Select a project or create a new project (Press to select).', + ); + + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + const options = (opts as { options: Array<{ label: string; value: string }> }).options; + + expect(options).toHaveLength(3); + expect(options[0]).toMatchObject({ label: 'Project One', value: projectUuid1 }); + expect(options[1]).toMatchObject({ + label: 'Project Two', + value: projectUuid2, + }); + expect(options[2]).toMatchObject({ label: 'Create a new project', value: 'create' }); + + // Simulate selecting the create option + return new Promise((resolve) => resolve('create')); + }) + .mockImplementationOnce(async (message) => { + expect(message).toBe('Enter a name for the new project:'); + + return new Promise((resolve) => resolve('New Project')); + }); + + await program.parseAsync([ + 'node', + 'catalyst', + 'project', + 'link', + '--store-hash', + storeHash, + '--access-token', + accessToken, + '--root-dir', + tmpDir, + ]); + + expect(mockIdentify).toHaveBeenCalledWith(storeHash); + + expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); + expect(consola.success).toHaveBeenCalledWith('Projects fetched.'); + + expect(consola.error).toHaveBeenCalledWith( + 'Failed to create project, is the name already in use?', + ); + + expect(exitMock).toHaveBeenCalledWith(1); + + consolaPromptMock.mockRestore(); +}); + +test('errors when infrastructure projects API is not found', async () => { + server.use( + http.get('https://:apiHost/stores/:storeHash/v3/infrastructure/projects', () => + HttpResponse.json({}, { status: 403 }), + ), + ); + + await program.parseAsync([ + 'node', + 'catalyst', + 'project', + 'link', + '--store-hash', + storeHash, + '--access-token', + accessToken, + '--root-dir', + tmpDir, + ]); + + expect(mockIdentify).toHaveBeenCalledWith(storeHash); + + expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); + expect(consola.error).toHaveBeenCalledWith( + 'Infrastructure Projects API not enabled. If you are part of the alpha, contact support@bigcommerce.com to enable it.', + ); +}); + +test('errors when no projectUuid, storeHash, or accessToken are provided', async () => { + await program.parseAsync(['node', 'catalyst', 'project', 'link', '--root-dir', tmpDir]); + + expect(consola.start).not.toHaveBeenCalled(); + expect(consola.success).not.toHaveBeenCalled(); + expect(consola.error).toHaveBeenCalledWith('Insufficient information to link a project.'); + expect(consola.info).toHaveBeenCalledWith( + 'This command requires either a project UUID or a combination of store hash and access token.', + ); + expect(consola.info).toHaveBeenCalledWith( + 'Project UUID: use the --project-uuid flag or the CATALYST_PROJECT_UUID environment variable.', + ); + expect(consola.info).toHaveBeenCalledWith( + 'Store hash and access token: each can be set via its flag (--store-hash, --access-token) or the corresponding environment variable (CATALYST_STORE_HASH, CATALYST_ACCESS_TOKEN).', + ); + + expect(exitMock).toHaveBeenCalledWith(1); +}); diff --git a/packages/catalyst/src/cli/commands/project.ts b/packages/catalyst/src/cli/commands/project.ts index a97980e2e2..6db6c3fae8 100644 --- a/packages/catalyst/src/cli/commands/project.ts +++ b/packages/catalyst/src/cli/commands/project.ts @@ -12,14 +12,14 @@ const list = new Command('list') .addOption( new Option( '--store-hash ', - 'BigCommerce store hash. Can be found in the URL of your store Control Panel.', - ).env('BIGCOMMERCE_STORE_HASH'), + 'BigCommerce store hash. Can be found in the URL of your store Control Panel. Can also be set via the CATALYST_STORE_HASH environment variable.', + ).env('CATALYST_STORE_HASH'), ) .addOption( new Option( '--access-token ', - 'BigCommerce access token. Can be found after creating a store-level API account.', - ).env('BIGCOMMERCE_ACCESS_TOKEN'), + 'BigCommerce access token. Can be found after creating a store-level API account. Can also be set via the CATALYST_ACCESS_TOKEN environment variable.', + ).env('CATALYST_ACCESS_TOKEN'), ) .addOption( new Option('--api-host ', 'BigCommerce API host. The default is api.bigcommerce.com.') @@ -30,8 +30,9 @@ const list = new Command('list') try { if (!options.storeHash || !options.accessToken) { consola.error('Insufficient information to list projects.'); + consola.info('This command requires a combination of store hash and access token.'); consola.info( - 'Provide both --store-hash and --access-token (or set BIGCOMMERCE_STORE_HASH and BIGCOMMERCE_ACCESS_TOKEN).', + 'Store hash and access token: each can be set via its flag (--store-hash, --access-token) or the corresponding environment variable (CATALYST_STORE_HASH, CATALYST_ACCESS_TOKEN).', ); process.exit(1); @@ -71,14 +72,14 @@ const create = new Command('create') .addOption( new Option( '--store-hash ', - 'BigCommerce store hash. Can be found in the URL of your store Control Panel.', - ).env('BIGCOMMERCE_STORE_HASH'), + 'BigCommerce store hash. Can be found in the URL of your store Control Panel. Can also be set via the CATALYST_STORE_HASH environment variable.', + ).env('CATALYST_STORE_HASH'), ) .addOption( new Option( '--access-token ', - 'BigCommerce access token. Can be found after creating a store-level API account.', - ).env('BIGCOMMERCE_ACCESS_TOKEN'), + 'BigCommerce access token. Can be found after creating a store-level API account. Can also be set via the CATALYST_ACCESS_TOKEN environment variable.', + ).env('CATALYST_ACCESS_TOKEN'), ) .addOption( new Option('--api-host ', 'BigCommerce API host. The default is api.bigcommerce.com.') @@ -94,8 +95,9 @@ const create = new Command('create') try { if (!options.storeHash || !options.accessToken) { consola.error('Insufficient information to create a project.'); + consola.info('This command requires a combination of store hash and access token.'); consola.info( - 'Provide both --store-hash and --access-token (or set BIGCOMMERCE_STORE_HASH and BIGCOMMERCE_ACCESS_TOKEN).', + 'Store hash and access token: each can be set via its flag (--store-hash, --access-token) or the corresponding environment variable (CATALYST_STORE_HASH, CATALYST_ACCESS_TOKEN).', ); process.exit(1); @@ -122,6 +124,8 @@ const create = new Command('create') consola.start('Writing project UUID to .bigcommerce/project.json...'); config.set('projectUuid', data.uuid); config.set('framework', 'catalyst'); + config.set('storeHash', options.storeHash); + config.set('accessToken', options.accessToken); consola.success('Project UUID written to .bigcommerce/project.json.'); process.exit(0); @@ -138,14 +142,14 @@ export const link = new Command('link') .addOption( new Option( '--store-hash ', - 'BigCommerce store hash. Can be found in the URL of your store Control Panel.', - ).env('BIGCOMMERCE_STORE_HASH'), + 'BigCommerce store hash. Can be found in the URL of your store Control Panel. Can also be set via the CATALYST_STORE_HASH environment variable.', + ).env('CATALYST_STORE_HASH'), ) .addOption( new Option( '--access-token ', - 'BigCommerce access token. Can be found after creating a store-level API account.', - ).env('BIGCOMMERCE_ACCESS_TOKEN'), + 'BigCommerce access token. Can be found after creating a store-level API account. Can also be set via the CATALYST_ACCESS_TOKEN environment variable.', + ).env('CATALYST_ACCESS_TOKEN'), ) .addOption( new Option('--api-host ', 'BigCommerce API host. The default is api.bigcommerce.com.') @@ -154,7 +158,7 @@ export const link = new Command('link') ) .option( '--project-uuid ', - 'BigCommerce infrastructure project UUID. Can be found via the BigCommerce API (GET /v3/infrastructure/projects). Use this to link directly without fetching projects.', + 'BigCommerce infrastructure project UUID. Can be found via the BigCommerce API (GET /v3/infrastructure/projects). Use this to link directly without fetching projects. Can also be set via the CATALYST_PROJECT_UUID environment variable.', ) .option( '--root-dir ', @@ -165,10 +169,19 @@ export const link = new Command('link') try { const config = getProjectConfig(options.rootDir); - const writeProjectConfig = (uuid: string) => { + const writeProjectConfig = ( + uuid: string, + opts?: { storeHash?: string; accessToken?: string }, + ) => { consola.start('Writing project UUID to .bigcommerce/project.json...'); config.set('projectUuid', uuid); config.set('framework', 'catalyst'); + if (opts?.storeHash !== undefined) { + config.set('storeHash', opts.storeHash); + } + if (opts?.accessToken !== undefined) { + config.set('accessToken', opts.accessToken); + } consola.success('Project UUID written to .bigcommerce/project.json.'); }; @@ -230,14 +243,24 @@ export const link = new Command('link') consola.success(`Project "${data.name}" created successfully.`); } - writeProjectConfig(projectUuid); + writeProjectConfig(projectUuid, { + storeHash: options.storeHash, + accessToken: options.accessToken, + }); process.exit(0); } consola.error('Insufficient information to link a project.'); - consola.info('Provide a project UUID with --project-uuid, or'); - consola.info('Provide both --store-hash and --access-token to fetch and select a project.'); + consola.info( + 'This command requires either a project UUID or a combination of store hash and access token.', + ); + consola.info( + 'Project UUID: use the --project-uuid flag or the CATALYST_PROJECT_UUID environment variable.', + ); + consola.info( + 'Store hash and access token: each can be set via its flag (--store-hash, --access-token) or the corresponding environment variable (CATALYST_STORE_HASH, CATALYST_ACCESS_TOKEN).', + ); process.exit(1); } catch (error) { consola.error(error instanceof Error ? error.message : error); From b2e6a61274ff35f0fe3a714811256a3275ace5bc Mon Sep 17 00:00:00 2001 From: jamesqquick Date: Fri, 6 Feb 2026 09:22:50 -0600 Subject: [PATCH 10/16] fixed testing for console messages --- .../catalyst/src/cli/commands/project.spec.ts | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/packages/catalyst/src/cli/commands/project.spec.ts b/packages/catalyst/src/cli/commands/project.spec.ts index 784dbc915b..ddc23b97b8 100644 --- a/packages/catalyst/src/cli/commands/project.spec.ts +++ b/packages/catalyst/src/cli/commands/project.spec.ts @@ -141,7 +141,10 @@ describe('project create', () => { expect(consola.error).toHaveBeenCalledWith('Insufficient information to create a project.'); expect(consola.info).toHaveBeenCalledWith( - 'Provide both --store-hash and --access-token (or set BIGCOMMERCE_STORE_HASH and BIGCOMMERCE_ACCESS_TOKEN).', + 'This command requires a combination of store hash and access token.', + ); + expect(consola.info).toHaveBeenCalledWith( + 'Store hash and access token: each can be set via its flag (--store-hash, --access-token) or the corresponding environment variable (CATALYST_STORE_HASH, CATALYST_ACCESS_TOKEN).', ); expect(exitMock).toHaveBeenCalledWith(1); }); @@ -212,7 +215,10 @@ describe('project list', () => { expect(consola.error).toHaveBeenCalledWith('Insufficient information to list projects.'); expect(consola.info).toHaveBeenCalledWith( - 'Provide both --store-hash and --access-token (or set BIGCOMMERCE_STORE_HASH and BIGCOMMERCE_ACCESS_TOKEN).', + 'This command requires a combination of store hash and access token.', + ); + expect(consola.info).toHaveBeenCalledWith( + 'Store hash and access token: each can be set via its flag (--store-hash, --access-token) or the corresponding environment variable (CATALYST_STORE_HASH, CATALYST_ACCESS_TOKEN).', ); expect(exitMock).toHaveBeenCalledWith(1); }); @@ -466,9 +472,14 @@ describe('project link', () => { expect(consola.start).not.toHaveBeenCalled(); expect(consola.success).not.toHaveBeenCalled(); expect(consola.error).toHaveBeenCalledWith('Insufficient information to link a project.'); - expect(consola.info).toHaveBeenCalledWith('Provide a project UUID with --project-uuid, or'); expect(consola.info).toHaveBeenCalledWith( - 'Provide both --store-hash and --access-token to fetch and select a project.', + 'This command requires either a project UUID or a combination of store hash and access token.', + ); + expect(consola.info).toHaveBeenCalledWith( + 'Project UUID: use the --project-uuid flag or the CATALYST_PROJECT_UUID environment variable.', + ); + expect(consola.info).toHaveBeenCalledWith( + 'Store hash and access token: each can be set via its flag (--store-hash, --access-token) or the corresponding environment variable (CATALYST_STORE_HASH, CATALYST_ACCESS_TOKEN).', ); expect(exitMock).toHaveBeenCalledWith(1); From f90f9f5baa1a8c84e3bd2ecace37454f4ad4fab9 Mon Sep 17 00:00:00 2001 From: jamesqquick Date: Fri, 6 Feb 2026 14:31:23 -0600 Subject: [PATCH 11/16] update logging, rename access token, write properties to config file, and read all variables from config file --- packages/catalyst/src/cli/commands/build.ts | 2 +- packages/catalyst/src/cli/commands/deploy.ts | 40 ++++++------ .../catalyst/src/cli/commands/link.spec.ts | 22 ++++++- .../catalyst/src/cli/commands/project.spec.ts | 62 ++++++++++++++----- packages/catalyst/src/cli/commands/project.ts | 57 +++++++++-------- 5 files changed, 119 insertions(+), 64 deletions(-) diff --git a/packages/catalyst/src/cli/commands/build.ts b/packages/catalyst/src/cli/commands/build.ts index 1e18558c9c..c78c33f901 100644 --- a/packages/catalyst/src/cli/commands/build.ts +++ b/packages/catalyst/src/cli/commands/build.ts @@ -59,7 +59,7 @@ export const build = new Command('build') if (!projectUuid) { throw new Error( - 'Project UUID is required. Please run `catalyst project create` or `catalyst project link` or this command again with --project-uuid .', + 'Project UUID is required. This can be set via the --project-uuid flag, the CATALYST_PROJECT_UUID environment variable, or the projectUuid property in the .bigcommerce/project.json file.', ); } diff --git a/packages/catalyst/src/cli/commands/deploy.ts b/packages/catalyst/src/cli/commands/deploy.ts index 025569498c..90bc7d0da2 100644 --- a/packages/catalyst/src/cli/commands/deploy.ts +++ b/packages/catalyst/src/cli/commands/deploy.ts @@ -282,18 +282,14 @@ export const deploy = new Command('deploy') .addOption( new Option( '--store-hash ', - 'BigCommerce store hash. Can be found in the URL of your store Control Panel.', - ) - .env('CATALYST_STORE_HASH') - .makeOptionMandatory(), + 'BigCommerce store hash. Can be found in the URL of your store Control Panel. Can also be set via the CATALYST_STORE_HASH environment variable or the storeHash property in the .bigcommerce/project.json file.', + ).env('CATALYST_STORE_HASH'), ) .addOption( new Option( '--access-token ', - 'BigCommerce access token. Can be found after creating a store-level API account. Can also be set via the CATALYST_ACCESS_TOKEN environment variable.', - ) - .env('CATALYST_ACCESS_TOKEN') - .makeOptionMandatory(), + 'BigCommerce access token. Can be found after creating a store-level API account. Can also be set via the CATALYST_ACCESS_TOKEN environment variable or the accessToken property in the .bigcommerce/project.json file.', + ).env('CATALYST_ACCESS_TOKEN'), ) .addOption( new Option('--api-host ', 'BigCommerce API host. The default is api.bigcommerce.com.') @@ -303,7 +299,7 @@ export const deploy = new Command('deploy') .addOption( new Option( '--project-uuid ', - 'BigCommerce infrastructure project UUID. Can be found via the BigCommerce API (GET /v3/infrastructure/projects). Can also be set via the CATALYST_PROJECT_UUID environment variable.', + 'BigCommerce infrastructure project UUID. Can be found via the BigCommerce API (GET /v3/infrastructure/projects). Can also be set via the CATALYST_PROJECT_UUID environment variable or the projectUuid property in the .bigcommerce/project.json file.', ).env('CATALYST_PROJECT_UUID'), ) .addOption( @@ -318,16 +314,24 @@ export const deploy = new Command('deploy') try { const config = getProjectConfig(); - await telemetry.identify(options.storeHash); - + const storeHash = options.storeHash ?? config.get('storeHash'); + const accessToken = options.accessToken ?? config.get('accessToken'); const projectUuid = options.projectUuid ?? config.get('projectUuid'); + if (!storeHash || !accessToken) { + throw new Error( + 'Store hash and access token are required. Can be set via the --store-hash and --access-token flags, the CATALYST_STORE_HASH and CATALYST_ACCESS_TOKEN environment variables, or the storeHash and accessToken properties in the .bigcommerce/project.json file.', + ); + } + if (!projectUuid) { throw new Error( - 'Project UUID is required. Please run either `catalyst link` or provide --project-uuid (or set the CATALYST_PROJECT_UUID environment variable).', + 'Project UUID is required. This can be set via the --project-uuid flag, the CATALYST_PROJECT_UUID environment variable, or the projectUuid property in the .bigcommerce/project.json file.', ); } + await telemetry.identify(storeHash); + await generateBundleZip(); if (options.dryRun) { @@ -341,8 +345,8 @@ export const deploy = new Command('deploy') } const uploadSignature = await generateUploadSignature( - options.storeHash, - options.accessToken, + storeHash, + accessToken, options.apiHost, ); @@ -353,16 +357,16 @@ export const deploy = new Command('deploy') const { deployment_uuid: deploymentUuid } = await createDeployment( projectUuid, uploadSignature.upload_uuid, - options.storeHash, - options.accessToken, + storeHash, + accessToken, options.apiHost, environmentVariables, ); await getDeploymentStatus( deploymentUuid, - options.storeHash, - options.accessToken, + storeHash, + accessToken, options.apiHost, ); } catch (error) { diff --git a/packages/catalyst/src/cli/commands/link.spec.ts b/packages/catalyst/src/cli/commands/link.spec.ts index ecab10e6d3..0a475557a1 100644 --- a/packages/catalyst/src/cli/commands/link.spec.ts +++ b/packages/catalyst/src/cli/commands/link.spec.ts @@ -309,8 +309,26 @@ test('errors when infrastructure projects API is not found', async () => { }); test('errors when no projectUuid, storeHash, or accessToken are provided', async () => { + const savedCatalystStoreHash = process.env.CATALYST_STORE_HASH; + const savedCatalystAccessToken = process.env.CATALYST_ACCESS_TOKEN; + const savedCatalystProjectUuid = process.env.CATALYST_PROJECT_UUID; + delete process.env.CATALYST_STORE_HASH; + delete process.env.CATALYST_ACCESS_TOKEN; + delete process.env.CATALYST_PROJECT_UUID; + + const projectConfig = getProjectConfig(tmpDir); + projectConfig.delete('storeHash'); + projectConfig.delete('accessToken'); + projectConfig.delete('projectUuid'); + await program.parseAsync(['node', 'catalyst', 'project', 'link', '--root-dir', tmpDir]); + if (savedCatalystStoreHash !== undefined) process.env.CATALYST_STORE_HASH = savedCatalystStoreHash; + if (savedCatalystAccessToken !== undefined) + process.env.CATALYST_ACCESS_TOKEN = savedCatalystAccessToken; + if (savedCatalystProjectUuid !== undefined) + process.env.CATALYST_PROJECT_UUID = savedCatalystProjectUuid; + expect(consola.start).not.toHaveBeenCalled(); expect(consola.success).not.toHaveBeenCalled(); expect(consola.error).toHaveBeenCalledWith('Insufficient information to link a project.'); @@ -318,10 +336,10 @@ test('errors when no projectUuid, storeHash, or accessToken are provided', async 'This command requires either a project UUID or a combination of store hash and access token.', ); expect(consola.info).toHaveBeenCalledWith( - 'Project UUID: use the --project-uuid flag or the CATALYST_PROJECT_UUID environment variable.', + 'Project UUID: This can be set via the --project-uuid flag, the CATALYST_PROJECT_UUID environment variable, or the projectUuid property in the .bigcommerce/project.json file.', ); expect(consola.info).toHaveBeenCalledWith( - 'Store hash and access token: each can be set via its flag (--store-hash, --access-token) or the corresponding environment variable (CATALYST_STORE_HASH, CATALYST_ACCESS_TOKEN).', + 'Store hash and access token: Can be set via the --store-hash and --access-token flags, the CATALYST_STORE_HASH and CATALYST_ACCESS_TOKEN environment variables, or the storeHash and accessToken properties in the .bigcommerce/project.json file.', ); expect(exitMock).toHaveBeenCalledWith(1); diff --git a/packages/catalyst/src/cli/commands/project.spec.ts b/packages/catalyst/src/cli/commands/project.spec.ts index ddc23b97b8..bc53f7fba6 100644 --- a/packages/catalyst/src/cli/commands/project.spec.ts +++ b/packages/catalyst/src/cli/commands/project.spec.ts @@ -127,24 +127,29 @@ describe('project create', () => { }); test('with insufficient credentials exits with error', async () => { - // Unset env so Commander doesn't pick up BIGCOMMERCE_* and trigger the create flow (which would prompt for name) - const savedStoreHash = process.env.BIGCOMMERCE_STORE_HASH; - const savedAccessToken = process.env.BIGCOMMERCE_ACCESS_TOKEN; + // Unset env and project.json so the command takes the insufficient-credentials path + const savedStoreHash = process.env.CATALYST_STORE_HASH; + const savedAccessToken = process.env.CATALYST_ACCESS_TOKEN; - delete process.env.BIGCOMMERCE_STORE_HASH; - delete process.env.BIGCOMMERCE_ACCESS_TOKEN; + delete process.env.CATALYST_STORE_HASH; + delete process.env.CATALYST_ACCESS_TOKEN; + + const projectConfig = getProjectConfig(tmpDir); + projectConfig.delete('storeHash'); + projectConfig.delete('accessToken'); await program.parseAsync(['node', 'catalyst', 'project', 'create', '--root-dir', tmpDir]); - if (savedStoreHash !== undefined) process.env.BIGCOMMERCE_STORE_HASH = savedStoreHash; - if (savedAccessToken !== undefined) process.env.BIGCOMMERCE_ACCESS_TOKEN = savedAccessToken; + if (savedStoreHash !== undefined) process.env.CATALYST_STORE_HASH = savedStoreHash; + if (savedAccessToken !== undefined) + process.env.CATALYST_ACCESS_TOKEN = savedAccessToken; expect(consola.error).toHaveBeenCalledWith('Insufficient information to create a project.'); expect(consola.info).toHaveBeenCalledWith( 'This command requires a combination of store hash and access token.', ); expect(consola.info).toHaveBeenCalledWith( - 'Store hash and access token: each can be set via its flag (--store-hash, --access-token) or the corresponding environment variable (CATALYST_STORE_HASH, CATALYST_ACCESS_TOKEN).', + 'Store hash and access token: Can be set via the --store-hash and --access-token flags, the CATALYST_STORE_HASH and CATALYST_ACCESS_TOKEN environment variables, or the storeHash and accessToken properties in the .bigcommerce/project.json file.', ); expect(exitMock).toHaveBeenCalledWith(1); }); @@ -202,23 +207,28 @@ describe('project list', () => { }); test('with insufficient credentials exits with error', async () => { - const savedStoreHash = process.env.BIGCOMMERCE_STORE_HASH; - const savedAccessToken = process.env.BIGCOMMERCE_ACCESS_TOKEN; + const savedStoreHash = process.env.CATALYST_STORE_HASH; + const savedAccessToken = process.env.CATALYST_ACCESS_TOKEN; - delete process.env.BIGCOMMERCE_STORE_HASH; - delete process.env.BIGCOMMERCE_ACCESS_TOKEN; + delete process.env.CATALYST_STORE_HASH; + delete process.env.CATALYST_ACCESS_TOKEN; + + const projectConfig = getProjectConfig(tmpDir); + projectConfig.delete('storeHash'); + projectConfig.delete('accessToken'); await program.parseAsync(['node', 'catalyst', 'project', 'list']); - if (savedStoreHash !== undefined) process.env.BIGCOMMERCE_STORE_HASH = savedStoreHash; - if (savedAccessToken !== undefined) process.env.BIGCOMMERCE_ACCESS_TOKEN = savedAccessToken; + if (savedStoreHash !== undefined) process.env.CATALYST_STORE_HASH = savedStoreHash; + if (savedAccessToken !== undefined) + process.env.CATALYST_ACCESS_TOKEN = savedAccessToken; expect(consola.error).toHaveBeenCalledWith('Insufficient information to list projects.'); expect(consola.info).toHaveBeenCalledWith( 'This command requires a combination of store hash and access token.', ); expect(consola.info).toHaveBeenCalledWith( - 'Store hash and access token: each can be set via its flag (--store-hash, --access-token) or the corresponding environment variable (CATALYST_STORE_HASH, CATALYST_ACCESS_TOKEN).', + 'Store hash and access token: Can be set via the --store-hash and --access-token flags, the CATALYST_STORE_HASH and CATALYST_ACCESS_TOKEN environment variables, or the storeHash and accessToken properties in the .bigcommerce/project.json file.', ); expect(exitMock).toHaveBeenCalledWith(1); }); @@ -467,8 +477,26 @@ describe('project link', () => { }); test('errors when no projectUuid, storeHash, or accessToken are provided', async () => { + const savedStoreHash = process.env.CATALYST_STORE_HASH; + const savedAccessToken = process.env.CATALYST_ACCESS_TOKEN; + const savedCatalystProjectUuid = process.env.CATALYST_PROJECT_UUID; + delete process.env.CATALYST_STORE_HASH; + delete process.env.CATALYST_ACCESS_TOKEN; + delete process.env.CATALYST_PROJECT_UUID; + + const projectConfig = getProjectConfig(tmpDir); + projectConfig.delete('storeHash'); + projectConfig.delete('accessToken'); + projectConfig.delete('projectUuid'); + await program.parseAsync(['node', 'catalyst', 'project', 'link', '--root-dir', tmpDir]); + if (savedStoreHash !== undefined) process.env.CATALYST_STORE_HASH = savedStoreHash; + if (savedAccessToken !== undefined) + process.env.CATALYST_ACCESS_TOKEN = savedAccessToken; + if (savedCatalystProjectUuid !== undefined) + process.env.CATALYST_PROJECT_UUID = savedCatalystProjectUuid; + expect(consola.start).not.toHaveBeenCalled(); expect(consola.success).not.toHaveBeenCalled(); expect(consola.error).toHaveBeenCalledWith('Insufficient information to link a project.'); @@ -476,10 +504,10 @@ describe('project link', () => { 'This command requires either a project UUID or a combination of store hash and access token.', ); expect(consola.info).toHaveBeenCalledWith( - 'Project UUID: use the --project-uuid flag or the CATALYST_PROJECT_UUID environment variable.', + 'Project UUID: This can be set via the --project-uuid flag, the CATALYST_PROJECT_UUID environment variable, or the projectUuid property in the .bigcommerce/project.json file.', ); expect(consola.info).toHaveBeenCalledWith( - 'Store hash and access token: each can be set via its flag (--store-hash, --access-token) or the corresponding environment variable (CATALYST_STORE_HASH, CATALYST_ACCESS_TOKEN).', + 'Store hash and access token: Can be set via the --store-hash and --access-token flags, the CATALYST_STORE_HASH and CATALYST_ACCESS_TOKEN environment variables, or the storeHash and accessToken properties in the .bigcommerce/project.json file.', ); expect(exitMock).toHaveBeenCalledWith(1); diff --git a/packages/catalyst/src/cli/commands/project.ts b/packages/catalyst/src/cli/commands/project.ts index 6db6c3fae8..0837850f97 100644 --- a/packages/catalyst/src/cli/commands/project.ts +++ b/packages/catalyst/src/cli/commands/project.ts @@ -28,22 +28,26 @@ const list = new Command('list') ) .action(async (options) => { try { - if (!options.storeHash || !options.accessToken) { + const config = getProjectConfig(); + const storeHash = options.storeHash ?? config.get('storeHash'); + const accessToken = options.accessToken ?? config.get('accessToken'); + + if (!storeHash || !accessToken) { consola.error('Insufficient information to list projects.'); consola.info('This command requires a combination of store hash and access token.'); consola.info( - 'Store hash and access token: each can be set via its flag (--store-hash, --access-token) or the corresponding environment variable (CATALYST_STORE_HASH, CATALYST_ACCESS_TOKEN).', + 'Store hash and access token: Can be set via the --store-hash and --access-token flags, the CATALYST_STORE_HASH and CATALYST_ACCESS_TOKEN environment variables, or the storeHash and accessToken properties in the .bigcommerce/project.json file.', ); process.exit(1); return; } - await telemetry.identify(options.storeHash); + await telemetry.identify(storeHash); consola.start('Fetching projects...'); - const projects = await fetchProjects(options.storeHash, options.accessToken, options.apiHost); + const projects = await fetchProjects(storeHash, accessToken, options.apiHost); consola.success('Projects fetched.'); @@ -93,18 +97,22 @@ const create = new Command('create') ) .action(async (options) => { try { - if (!options.storeHash || !options.accessToken) { + const config = getProjectConfig(options.rootDir); + const storeHash = options.storeHash ?? config.get('storeHash'); + const accessToken = options.accessToken ?? config.get('accessToken'); + + if (!storeHash || !accessToken) { consola.error('Insufficient information to create a project.'); consola.info('This command requires a combination of store hash and access token.'); consola.info( - 'Store hash and access token: each can be set via its flag (--store-hash, --access-token) or the corresponding environment variable (CATALYST_STORE_HASH, CATALYST_ACCESS_TOKEN).', + 'Store hash and access token: Can be set via the --store-hash and --access-token flags, the CATALYST_STORE_HASH and CATALYST_ACCESS_TOKEN environment variables, or the storeHash and accessToken properties in the .bigcommerce/project.json file.', ); process.exit(1); return; } - await telemetry.identify(options.storeHash); + await telemetry.identify(storeHash); const newProjectName = await consola.prompt('Enter a name for the new project:', { type: 'text', @@ -112,20 +120,18 @@ const create = new Command('create') const data = await createProject( newProjectName, - options.storeHash, - options.accessToken, + storeHash, + accessToken, options.apiHost, ); consola.success(`Project "${data.name}" created successfully.`); - const config = getProjectConfig(options.rootDir); - consola.start('Writing project UUID to .bigcommerce/project.json...'); config.set('projectUuid', data.uuid); config.set('framework', 'catalyst'); - config.set('storeHash', options.storeHash); - config.set('accessToken', options.accessToken); + config.set('storeHash', storeHash); + config.set('accessToken', accessToken); consola.success('Project UUID written to .bigcommerce/project.json.'); process.exit(0); @@ -185,22 +191,21 @@ export const link = new Command('link') consola.success('Project UUID written to .bigcommerce/project.json.'); }; + const storeHash = options.storeHash ?? config.get('storeHash'); + const accessToken = options.accessToken ?? config.get('accessToken'); + if (options.projectUuid) { writeProjectConfig(options.projectUuid); process.exit(0); } - if (options.storeHash && options.accessToken) { - await telemetry.identify(options.storeHash); + if (storeHash && accessToken) { + await telemetry.identify(storeHash); consola.start('Fetching projects...'); - const projects = await fetchProjects( - options.storeHash, - options.accessToken, - options.apiHost, - ); + const projects = await fetchProjects(storeHash, accessToken, options.apiHost); consola.success('Projects fetched.'); @@ -233,8 +238,8 @@ export const link = new Command('link') const data = await createProject( newProjectName, - options.storeHash, - options.accessToken, + storeHash, + accessToken, options.apiHost, ); @@ -244,8 +249,8 @@ export const link = new Command('link') } writeProjectConfig(projectUuid, { - storeHash: options.storeHash, - accessToken: options.accessToken, + storeHash, + accessToken, }); process.exit(0); @@ -256,10 +261,10 @@ export const link = new Command('link') 'This command requires either a project UUID or a combination of store hash and access token.', ); consola.info( - 'Project UUID: use the --project-uuid flag or the CATALYST_PROJECT_UUID environment variable.', + 'Project UUID: This can be set via the --project-uuid flag, the CATALYST_PROJECT_UUID environment variable, or the projectUuid property in the .bigcommerce/project.json file.', ); consola.info( - 'Store hash and access token: each can be set via its flag (--store-hash, --access-token) or the corresponding environment variable (CATALYST_STORE_HASH, CATALYST_ACCESS_TOKEN).', + 'Store hash and access token: Can be set via the --store-hash and --access-token flags, the CATALYST_STORE_HASH and CATALYST_ACCESS_TOKEN environment variables, or the storeHash and accessToken properties in the .bigcommerce/project.json file.', ); process.exit(1); } catch (error) { From 98d634b5d7bf1b6f3e8d9ce745ea625b02e1ad24 Mon Sep 17 00:00:00 2001 From: jamesqquick Date: Fri, 6 Feb 2026 15:27:21 -0600 Subject: [PATCH 12/16] add configuration priority tests --- .../catalyst/src/cli/config-priority.spec.ts | 304 ++++++++++++++++++ 1 file changed, 304 insertions(+) create mode 100644 packages/catalyst/src/cli/config-priority.spec.ts diff --git a/packages/catalyst/src/cli/config-priority.spec.ts b/packages/catalyst/src/cli/config-priority.spec.ts new file mode 100644 index 0000000000..777f93a338 --- /dev/null +++ b/packages/catalyst/src/cli/config-priority.spec.ts @@ -0,0 +1,304 @@ +import { http, HttpResponse } from 'msw'; +import { mkdir, realpath, writeFile } from 'node:fs/promises'; +import { dirname, join } from 'node:path'; +import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, MockInstance, test, vi } from 'vitest'; + +import { handlers } from '../../tests/mocks/handlers'; +import { server } from '../../tests/mocks/node'; +import { textHistory } from '../../tests/mocks/spinner'; +import { consola } from './lib/logger'; +import { mkTempDir } from './lib/mk-temp-dir'; +import { program } from './program'; + +// eslint-disable-next-line import/dynamic-import-chunkname +vi.mock('yocto-spinner', () => import('../../tests/mocks/spinner')); + +const { mockIdentify } = vi.hoisted(() => ({ + mockIdentify: vi.fn(), +})); + +vi.mock('./lib/telemetry', () => ({ + Telemetry: vi.fn().mockImplementation(() => ({ + identify: mockIdentify, + isEnabled: vi.fn(() => true), + track: vi.fn(), + analytics: { closeAndFlush: vi.fn() }, + })), +})); + +let exitMock: MockInstance; +let tmpDir: string; +let cleanup: () => Promise; + +const projectUuidFromFlag = '11111111-fd99-4a94-9fb3-945551623923'; +const projectUuidFromEnv = '22222222-fd99-4a94-9fb3-945551623924'; +const projectUuidFromProjectJson = 'a23f5785-fd99-4a94-9fb3-945551623923'; + +const storeHashFromFlag = 'store-hash-flag'; +const storeHashFromEnv = 'store-hash-env'; +const storeHashFromProjectJson = 'store-hash-json'; + +const accessTokenFromFlag = 'access-token-flag'; +const accessTokenFromEnv = 'access-token-env'; +const accessTokenFromProjectJson = 'access-token-json'; + +const deploymentResponse = () => + HttpResponse.json({ + data: { deployment_uuid: '5b29c3c0-5f68-44fe-99e5-06492babf7be' }, + }); + +async function writeProjectJson(overrides: { + projectUuid?: string; + storeHash?: string; + accessToken?: string; +}) { + const path = join(tmpDir, '.bigcommerce', 'project.json'); + await mkdir(dirname(path), { recursive: true }); + const defaults = { + projectUuid: projectUuidFromProjectJson, + framework: 'catalyst' as const, + storeHash: storeHashFromProjectJson, + accessToken: accessTokenFromProjectJson, + telemetry: { enabled: true, anonymousId: 'test-id' }, + }; + await writeFile(path, JSON.stringify({ ...defaults, ...overrides })); +} + +function useCaptureHandlers(captured: { + storeHash: string; + projectUuid: string; + accessToken: string; +}) { + server.use( + http.post( + 'https://:apiHost/stores/:storeHash/v3/infrastructure/deployments/uploads', + ({ request, params }) => { + captured.storeHash = params.storeHash as string; + captured.accessToken = request.headers.get('X-Auth-Token') ?? request.headers.get('x-auth-token') ?? ''; + return HttpResponse.json({ + data: { + upload_url: 'https://mock-upload-url.com', + upload_uuid: '0e93ce5f-6f91-4236-87ec-ca79627f31ba', + }, + }); + }, + ), + ); + server.use( + http.post( + 'https://:apiHost/stores/:storeHash/v3/infrastructure/deployments', + async ({ request, params }) => { + captured.storeHash = (params.storeHash as string) || new URL(request.url).pathname.split('/')[2] || ''; + const body = (await request.json()) as { project_uuid?: string }; + captured.projectUuid = body.project_uuid ?? ''; + captured.accessToken = request.headers.get('X-Auth-Token') ?? request.headers.get('x-auth-token') ?? ''; + return deploymentResponse(); + }, + ), + ); +} + +beforeAll(async () => { + consola.mockTypes(() => vi.fn()); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + exitMock = vi.spyOn(process, 'exit').mockImplementation(() => null as never); + + [tmpDir, cleanup] = await mkTempDir(); + tmpDir = await realpath(tmpDir); + + // Minimal dist so deploy's generateBundleZip() passes; we only assert on config → request values. + const distDir = join(tmpDir, '.bigcommerce', 'dist'); + await mkdir(distDir, { recursive: true }); + await writeFile(join(distDir, 'worker.js'), ''); + await mkdir(join(distDir, 'assets'), { recursive: true }); + await writeFile(join(distDir, 'assets', 'placeholder'), ''); +}); + +beforeEach(() => { + process.chdir(tmpDir); + vi.clearAllMocks(); + textHistory.length = 0; + server.resetHandlers(); + server.use(...handlers); +}); + +afterEach(() => { + delete process.env.CATALYST_PROJECT_UUID; + delete process.env.CATALYST_STORE_HASH; + delete process.env.CATALYST_ACCESS_TOKEN; +}); + +afterAll(async () => { + server.resetHandlers(); + server.use(...handlers); + exitMock.mockRestore(); + await cleanup(); +}); + +describe('config resolution priority', () => { + describe('projectUuid', () => { + test('explicit flag overrides env and project.json', async () => { + await writeProjectJson({ projectUuid: projectUuidFromProjectJson }); + process.env.CATALYST_PROJECT_UUID = projectUuidFromEnv; + + const captured = { storeHash: '', projectUuid: '', accessToken: '' }; + useCaptureHandlers(captured); + + await program.parseAsync([ + 'node', + 'catalyst', + 'deploy', + '--store-hash', + storeHashFromFlag, + '--access-token', + accessTokenFromFlag, + '--project-uuid', + projectUuidFromFlag, + ]); + + expect(captured.projectUuid).toBe(projectUuidFromFlag); + }); + + test('environment variable overrides project.json when no flag', async () => { + await writeProjectJson({ projectUuid: projectUuidFromProjectJson }); + process.env.CATALYST_PROJECT_UUID = projectUuidFromEnv; + + const captured = { storeHash: '', projectUuid: '', accessToken: '' }; + useCaptureHandlers(captured); + + await program.parseAsync(['node', 'catalyst', 'deploy']); + + expect(captured.projectUuid).toBe(projectUuidFromEnv); + }); + + test('project.json used when no flag or env', async () => { + await writeProjectJson({ + projectUuid: projectUuidFromProjectJson, + storeHash: storeHashFromProjectJson, + accessToken: accessTokenFromProjectJson, + }); + + const captured = { storeHash: '', projectUuid: '', accessToken: '' }; + useCaptureHandlers(captured); + + await program.parseAsync(['node', 'catalyst', 'deploy']); + + expect(captured.projectUuid).toBe(projectUuidFromProjectJson); + }); + }); + + describe('storeHash', () => { + test('explicit flag overrides env and project.json', async () => { + await writeProjectJson({ storeHash: storeHashFromProjectJson }); + process.env.CATALYST_STORE_HASH = storeHashFromEnv; + + const captured = { storeHash: '', projectUuid: '', accessToken: '' }; + useCaptureHandlers(captured); + + await program.parseAsync([ + 'node', + 'catalyst', + 'deploy', + '--store-hash', + storeHashFromFlag, + '--access-token', + accessTokenFromFlag, + '--project-uuid', + projectUuidFromFlag, + ]); + + expect(captured.storeHash).toBe(storeHashFromFlag); + }); + + test('environment variable overrides project.json when no flag', async () => { + await writeProjectJson({ + storeHash: storeHashFromProjectJson, + accessToken: accessTokenFromProjectJson, + projectUuid: projectUuidFromProjectJson, + }); + process.env.CATALYST_STORE_HASH = storeHashFromEnv; + process.env.CATALYST_PROJECT_UUID = projectUuidFromEnv; + + const captured = { storeHash: '', projectUuid: '', accessToken: '' }; + useCaptureHandlers(captured); + + await program.parseAsync(['node', 'catalyst', 'deploy']); + + expect(captured.storeHash).toBe(storeHashFromEnv); + }); + + test('project.json used when no flag or env', async () => { + await writeProjectJson({ + projectUuid: projectUuidFromProjectJson, + storeHash: storeHashFromProjectJson, + accessToken: accessTokenFromProjectJson, + }); + + const captured = { storeHash: '', projectUuid: '', accessToken: '' }; + useCaptureHandlers(captured); + + await program.parseAsync(['node', 'catalyst', 'deploy']); + + expect(captured.storeHash).toBe(storeHashFromProjectJson); + }); + }); + + describe('accessToken', () => { + test('explicit flag overrides env and project.json', async () => { + await writeProjectJson({ accessToken: accessTokenFromProjectJson }); + process.env.CATALYST_STORE_HASH = storeHashFromEnv; + process.env.CATALYST_ACCESS_TOKEN = accessTokenFromEnv; + process.env.CATALYST_PROJECT_UUID = projectUuidFromEnv; + + const captured = { storeHash: '', projectUuid: '', accessToken: '' }; + useCaptureHandlers(captured); + + await program.parseAsync([ + 'node', + 'catalyst', + 'deploy', + '--store-hash', + storeHashFromFlag, + '--access-token', + accessTokenFromFlag, + '--project-uuid', + projectUuidFromFlag, + ]); + + expect(captured.accessToken).toBe(accessTokenFromFlag); + }); + + test('environment variable overrides project.json when no flag', async () => { + await writeProjectJson({ + storeHash: storeHashFromProjectJson, + accessToken: accessTokenFromProjectJson, + projectUuid: projectUuidFromProjectJson, + }); + process.env.CATALYST_STORE_HASH = storeHashFromEnv; + process.env.CATALYST_ACCESS_TOKEN = accessTokenFromEnv; + process.env.CATALYST_PROJECT_UUID = projectUuidFromEnv; + + const captured = { storeHash: '', projectUuid: '', accessToken: '' }; + useCaptureHandlers(captured); + + await program.parseAsync(['node', 'catalyst', 'deploy']); + + expect(captured.accessToken).toBe(accessTokenFromEnv); + }); + + test('project.json used when no flag or env', async () => { + await writeProjectJson({ + projectUuid: projectUuidFromProjectJson, + storeHash: storeHashFromProjectJson, + accessToken: accessTokenFromProjectJson, + }); + + const captured = { storeHash: '', projectUuid: '', accessToken: '' }; + useCaptureHandlers(captured); + + await program.parseAsync(['node', 'catalyst', 'deploy']); + + expect(captured.accessToken).toBe(accessTokenFromProjectJson); + }); + }); +}); From f9027c6d44e3637494c3dc7ee96bf4e2876500a6 Mon Sep 17 00:00:00 2001 From: jamesqquick Date: Mon, 9 Feb 2026 13:24:06 -0500 Subject: [PATCH 13/16] rebase on canary --- .../catalyst/src/cli/commands/project.spec.ts | 306 +----------------- 1 file changed, 3 insertions(+), 303 deletions(-) diff --git a/packages/catalyst/src/cli/commands/project.spec.ts b/packages/catalyst/src/cli/commands/project.spec.ts index bc53f7fba6..6ecde172b0 100644 --- a/packages/catalyst/src/cli/commands/project.spec.ts +++ b/packages/catalyst/src/cli/commands/project.spec.ts @@ -141,8 +141,7 @@ describe('project create', () => { await program.parseAsync(['node', 'catalyst', 'project', 'create', '--root-dir', tmpDir]); if (savedStoreHash !== undefined) process.env.CATALYST_STORE_HASH = savedStoreHash; - if (savedAccessToken !== undefined) - process.env.CATALYST_ACCESS_TOKEN = savedAccessToken; + if (savedAccessToken !== undefined) process.env.CATALYST_ACCESS_TOKEN = savedAccessToken; expect(consola.error).toHaveBeenCalledWith('Insufficient information to create a project.'); expect(consola.info).toHaveBeenCalledWith( @@ -220,8 +219,7 @@ describe('project list', () => { await program.parseAsync(['node', 'catalyst', 'project', 'list']); if (savedStoreHash !== undefined) process.env.CATALYST_STORE_HASH = savedStoreHash; - if (savedAccessToken !== undefined) - process.env.CATALYST_ACCESS_TOKEN = savedAccessToken; + if (savedAccessToken !== undefined) process.env.CATALYST_ACCESS_TOKEN = savedAccessToken; expect(consola.error).toHaveBeenCalledWith('Insufficient information to list projects.'); expect(consola.info).toHaveBeenCalledWith( @@ -492,8 +490,7 @@ describe('project link', () => { await program.parseAsync(['node', 'catalyst', 'project', 'link', '--root-dir', tmpDir]); if (savedStoreHash !== undefined) process.env.CATALYST_STORE_HASH = savedStoreHash; - if (savedAccessToken !== undefined) - process.env.CATALYST_ACCESS_TOKEN = savedAccessToken; + if (savedAccessToken !== undefined) process.env.CATALYST_ACCESS_TOKEN = savedAccessToken; if (savedCatalystProjectUuid !== undefined) process.env.CATALYST_PROJECT_UUID = savedCatalystProjectUuid; @@ -514,300 +511,3 @@ describe('project link', () => { }); }); -describe('project list', () => { - test('fetches and displays projects', async () => { - await program.parseAsync([ - 'node', - 'catalyst', - 'project', - 'list', - '--store-hash', - storeHash, - '--access-token', - accessToken, - ]); - - expect(mockIdentify).toHaveBeenCalledWith(storeHash); - expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); - expect(consola.success).toHaveBeenCalledWith('Projects fetched.'); - expect(consola.log).toHaveBeenCalledWith('Project One (a23f5785-fd99-4a94-9fb3-945551623923)'); - expect(consola.log).toHaveBeenCalledWith('Project Two (b23f5785-fd99-4a94-9fb3-945551623924)'); - expect(exitMock).toHaveBeenCalledWith(0); - }); - - test('with insufficient credentials exits with error', async () => { - const savedStoreHash = process.env.BIGCOMMERCE_STORE_HASH; - const savedAccessToken = process.env.BIGCOMMERCE_ACCESS_TOKEN; - - delete process.env.BIGCOMMERCE_STORE_HASH; - delete process.env.BIGCOMMERCE_ACCESS_TOKEN; - - await program.parseAsync(['node', 'catalyst', 'project', 'list']); - - if (savedStoreHash !== undefined) process.env.BIGCOMMERCE_STORE_HASH = savedStoreHash; - if (savedAccessToken !== undefined) process.env.BIGCOMMERCE_ACCESS_TOKEN = savedAccessToken; - - expect(consola.error).toHaveBeenCalledWith('Insufficient information to list projects.'); - expect(consola.info).toHaveBeenCalledWith( - 'Provide both --store-hash and --access-token (or set BIGCOMMERCE_STORE_HASH and BIGCOMMERCE_ACCESS_TOKEN).', - ); - expect(exitMock).toHaveBeenCalledWith(1); - }); -}); - -describe('project link', () => { - test('properly configured Command instance', () => { - expect(link).toBeInstanceOf(Command); - expect(link.name()).toBe('link'); - expect(link.description()).toBe( - 'Link your local Catalyst project to a BigCommerce infrastructure project. You can provide a project UUID directly, or fetch and select from available projects using your store credentials.', - ); - expect(link.options).toEqual( - expect.arrayContaining([ - expect.objectContaining({ flags: '--store-hash ' }), - expect.objectContaining({ flags: '--access-token ' }), - expect.objectContaining({ - flags: '--api-host ', - defaultValue: 'api.bigcommerce.com', - }), - expect.objectContaining({ flags: '--project-uuid ' }), - expect.objectContaining({ flags: '--root-dir ', defaultValue: process.cwd() }), - ]), - ); - }); - - test('sets projectUuid when called with --project-uuid', async () => { - await program.parseAsync([ - 'node', - 'catalyst', - 'project', - 'link', - '--project-uuid', - projectUuid1, - '--root-dir', - tmpDir, - ]); - - expect(consola.start).toHaveBeenCalledWith( - 'Writing project UUID to .bigcommerce/project.json...', - ); - expect(consola.success).toHaveBeenCalledWith( - 'Project UUID written to .bigcommerce/project.json.', - ); - expect(exitMock).toHaveBeenCalledWith(0); - expect(config.get('projectUuid')).toBe(projectUuid1); - expect(config.get('framework')).toBe('catalyst'); - }); - - test('fetches projects and prompts user to select one', async () => { - const consolaPromptMock = vi - .spyOn(consola, 'prompt') - .mockImplementation(async (message, opts) => { - expect(message).toContain( - 'Select a project or create a new project (Press to select).', - ); - - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const options = (opts as { options: Array<{ label: string; value: string }> }).options; - - expect(options).toHaveLength(3); - expect(options[0]).toMatchObject({ label: 'Project One', value: projectUuid1 }); - expect(options[1]).toMatchObject({ - label: 'Project Two', - value: projectUuid2, - }); - expect(options[2]).toMatchObject({ label: 'Create a new project', value: 'create' }); - - return new Promise((resolve) => resolve(projectUuid2)); - }); - - await program.parseAsync([ - 'node', - 'catalyst', - 'project', - 'link', - '--store-hash', - storeHash, - '--access-token', - accessToken, - '--root-dir', - tmpDir, - ]); - - expect(mockIdentify).toHaveBeenCalledWith(storeHash); - - expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); - expect(consola.success).toHaveBeenCalledWith('Projects fetched.'); - - expect(consola.start).toHaveBeenCalledWith( - 'Writing project UUID to .bigcommerce/project.json...', - ); - expect(consola.success).toHaveBeenCalledWith( - 'Project UUID written to .bigcommerce/project.json.', - ); - - expect(exitMock).toHaveBeenCalledWith(0); - - expect(config.get('projectUuid')).toBe(projectUuid2); - expect(config.get('framework')).toBe('catalyst'); - - consolaPromptMock.mockRestore(); - }); - - test('prompts to create a new project', async () => { - const consolaPromptMock = vi - .spyOn(consola, 'prompt') - .mockImplementationOnce(async (message, opts) => { - expect(message).toContain( - 'Select a project or create a new project (Press to select).', - ); - - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const options = (opts as { options: Array<{ label: string; value: string }> }).options; - - expect(options).toHaveLength(3); - expect(options[0]).toMatchObject({ label: 'Project One', value: projectUuid1 }); - expect(options[1]).toMatchObject({ - label: 'Project Two', - value: projectUuid2, - }); - expect(options[2]).toMatchObject({ label: 'Create a new project', value: 'create' }); - - return new Promise((resolve) => resolve('create')); - }) - .mockImplementationOnce(async (message) => { - expect(message).toBe('Enter a name for the new project:'); - - return new Promise((resolve) => resolve('New Project')); - }); - - await program.parseAsync([ - 'node', - 'catalyst', - 'project', - 'link', - '--store-hash', - storeHash, - '--access-token', - accessToken, - '--root-dir', - tmpDir, - ]); - - expect(mockIdentify).toHaveBeenCalledWith(storeHash); - - expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); - expect(consola.success).toHaveBeenCalledWith('Projects fetched.'); - - expect(consola.success).toHaveBeenCalledWith('Project "New Project" created successfully.'); - - expect(exitMock).toHaveBeenCalledWith(0); - - expect(config.get('projectUuid')).toBe(projectUuid3); - expect(config.get('framework')).toBe('catalyst'); - - consolaPromptMock.mockRestore(); - }); - - test('errors when create project API fails', async () => { - server.use( - http.post('https://:apiHost/stores/:storeHash/v3/infrastructure/projects', () => - HttpResponse.json({}, { status: 502 }), - ), - ); - - const consolaPromptMock = vi - .spyOn(consola, 'prompt') - .mockImplementationOnce(async (message, opts) => { - expect(message).toContain( - 'Select a project or create a new project (Press to select).', - ); - - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions - const options = (opts as { options: Array<{ label: string; value: string }> }).options; - - expect(options).toHaveLength(3); - expect(options[0]).toMatchObject({ label: 'Project One', value: projectUuid1 }); - expect(options[1]).toMatchObject({ - label: 'Project Two', - value: projectUuid2, - }); - expect(options[2]).toMatchObject({ label: 'Create a new project', value: 'create' }); - - return new Promise((resolve) => resolve('create')); - }) - .mockImplementationOnce(async (message) => { - expect(message).toBe('Enter a name for the new project:'); - - return new Promise((resolve) => resolve('New Project')); - }); - - await program.parseAsync([ - 'node', - 'catalyst', - 'project', - 'link', - '--store-hash', - storeHash, - '--access-token', - accessToken, - '--root-dir', - tmpDir, - ]); - - expect(mockIdentify).toHaveBeenCalledWith(storeHash); - - expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); - expect(consola.success).toHaveBeenCalledWith('Projects fetched.'); - - expect(consola.error).toHaveBeenCalledWith( - 'Failed to create project, is the name already in use?', - ); - - expect(exitMock).toHaveBeenCalledWith(1); - - consolaPromptMock.mockRestore(); - }); - - test('errors when infrastructure projects API is not found', async () => { - server.use( - http.get('https://:apiHost/stores/:storeHash/v3/infrastructure/projects', () => - HttpResponse.json({}, { status: 403 }), - ), - ); - - await program.parseAsync([ - 'node', - 'catalyst', - 'project', - 'link', - '--store-hash', - storeHash, - '--access-token', - accessToken, - '--root-dir', - tmpDir, - ]); - - expect(mockIdentify).toHaveBeenCalledWith(storeHash); - - expect(consola.start).toHaveBeenCalledWith('Fetching projects...'); - expect(consola.error).toHaveBeenCalledWith( - 'Infrastructure Projects API not enabled. If you are part of the alpha, contact support@bigcommerce.com to enable it.', - ); - }); - - test('errors when no projectUuid, storeHash, or accessToken are provided', async () => { - await program.parseAsync(['node', 'catalyst', 'project', 'link', '--root-dir', tmpDir]); - - expect(consola.start).not.toHaveBeenCalled(); - expect(consola.success).not.toHaveBeenCalled(); - expect(consola.error).toHaveBeenCalledWith('Insufficient information to link a project.'); - expect(consola.info).toHaveBeenCalledWith('Provide a project UUID with --project-uuid, or'); - expect(consola.info).toHaveBeenCalledWith( - 'Provide both --store-hash and --access-token to fetch and select a project.', - ); - - expect(exitMock).toHaveBeenCalledWith(1); - }); -}); From 5442ed10c37e574e9b0a0adc8784d93cf04693cb Mon Sep 17 00:00:00 2001 From: jamesqquick Date: Tue, 10 Feb 2026 14:46:38 -0500 Subject: [PATCH 14/16] fix linting --- packages/catalyst/src/cli/commands/deploy.ts | 7 +--- .../catalyst/src/cli/commands/link.spec.ts | 5 ++- .../catalyst/src/cli/commands/project.spec.ts | 5 ++- packages/catalyst/src/cli/commands/project.ts | 17 +++----- .../catalyst/src/cli/config-priority.spec.ts | 39 +++++++++++++++++-- packages/catalyst/src/cli/index.ts | 2 + 6 files changed, 51 insertions(+), 24 deletions(-) diff --git a/packages/catalyst/src/cli/commands/deploy.ts b/packages/catalyst/src/cli/commands/deploy.ts index 90bc7d0da2..9181eda255 100644 --- a/packages/catalyst/src/cli/commands/deploy.ts +++ b/packages/catalyst/src/cli/commands/deploy.ts @@ -363,12 +363,7 @@ export const deploy = new Command('deploy') environmentVariables, ); - await getDeploymentStatus( - deploymentUuid, - storeHash, - accessToken, - options.apiHost, - ); + await getDeploymentStatus(deploymentUuid, storeHash, accessToken, options.apiHost); } catch (error) { consola.error(error); process.exit(1); diff --git a/packages/catalyst/src/cli/commands/link.spec.ts b/packages/catalyst/src/cli/commands/link.spec.ts index 0a475557a1..fda90683c5 100644 --- a/packages/catalyst/src/cli/commands/link.spec.ts +++ b/packages/catalyst/src/cli/commands/link.spec.ts @@ -312,18 +312,21 @@ test('errors when no projectUuid, storeHash, or accessToken are provided', async const savedCatalystStoreHash = process.env.CATALYST_STORE_HASH; const savedCatalystAccessToken = process.env.CATALYST_ACCESS_TOKEN; const savedCatalystProjectUuid = process.env.CATALYST_PROJECT_UUID; + delete process.env.CATALYST_STORE_HASH; delete process.env.CATALYST_ACCESS_TOKEN; delete process.env.CATALYST_PROJECT_UUID; const projectConfig = getProjectConfig(tmpDir); + projectConfig.delete('storeHash'); projectConfig.delete('accessToken'); projectConfig.delete('projectUuid'); await program.parseAsync(['node', 'catalyst', 'project', 'link', '--root-dir', tmpDir]); - if (savedCatalystStoreHash !== undefined) process.env.CATALYST_STORE_HASH = savedCatalystStoreHash; + if (savedCatalystStoreHash !== undefined) + process.env.CATALYST_STORE_HASH = savedCatalystStoreHash; if (savedCatalystAccessToken !== undefined) process.env.CATALYST_ACCESS_TOKEN = savedCatalystAccessToken; if (savedCatalystProjectUuid !== undefined) diff --git a/packages/catalyst/src/cli/commands/project.spec.ts b/packages/catalyst/src/cli/commands/project.spec.ts index 6ecde172b0..b6a63ecc98 100644 --- a/packages/catalyst/src/cli/commands/project.spec.ts +++ b/packages/catalyst/src/cli/commands/project.spec.ts @@ -135,6 +135,7 @@ describe('project create', () => { delete process.env.CATALYST_ACCESS_TOKEN; const projectConfig = getProjectConfig(tmpDir); + projectConfig.delete('storeHash'); projectConfig.delete('accessToken'); @@ -213,6 +214,7 @@ describe('project list', () => { delete process.env.CATALYST_ACCESS_TOKEN; const projectConfig = getProjectConfig(tmpDir); + projectConfig.delete('storeHash'); projectConfig.delete('accessToken'); @@ -478,11 +480,13 @@ describe('project link', () => { const savedStoreHash = process.env.CATALYST_STORE_HASH; const savedAccessToken = process.env.CATALYST_ACCESS_TOKEN; const savedCatalystProjectUuid = process.env.CATALYST_PROJECT_UUID; + delete process.env.CATALYST_STORE_HASH; delete process.env.CATALYST_ACCESS_TOKEN; delete process.env.CATALYST_PROJECT_UUID; const projectConfig = getProjectConfig(tmpDir); + projectConfig.delete('storeHash'); projectConfig.delete('accessToken'); projectConfig.delete('projectUuid'); @@ -510,4 +514,3 @@ describe('project link', () => { expect(exitMock).toHaveBeenCalledWith(1); }); }); - diff --git a/packages/catalyst/src/cli/commands/project.ts b/packages/catalyst/src/cli/commands/project.ts index 0837850f97..21c429a49b 100644 --- a/packages/catalyst/src/cli/commands/project.ts +++ b/packages/catalyst/src/cli/commands/project.ts @@ -118,12 +118,7 @@ const create = new Command('create') type: 'text', }); - const data = await createProject( - newProjectName, - storeHash, - accessToken, - options.apiHost, - ); + const data = await createProject(newProjectName, storeHash, accessToken, options.apiHost); consola.success(`Project "${data.name}" created successfully.`); @@ -182,12 +177,15 @@ export const link = new Command('link') consola.start('Writing project UUID to .bigcommerce/project.json...'); config.set('projectUuid', uuid); config.set('framework', 'catalyst'); + if (opts?.storeHash !== undefined) { config.set('storeHash', opts.storeHash); } + if (opts?.accessToken !== undefined) { config.set('accessToken', opts.accessToken); } + consola.success('Project UUID written to .bigcommerce/project.json.'); }; @@ -236,12 +234,7 @@ export const link = new Command('link') type: 'text', }); - const data = await createProject( - newProjectName, - storeHash, - accessToken, - options.apiHost, - ); + const data = await createProject(newProjectName, storeHash, accessToken, options.apiHost); projectUuid = data.uuid; diff --git a/packages/catalyst/src/cli/config-priority.spec.ts b/packages/catalyst/src/cli/config-priority.spec.ts index 777f93a338..6ddbe86be3 100644 --- a/packages/catalyst/src/cli/config-priority.spec.ts +++ b/packages/catalyst/src/cli/config-priority.spec.ts @@ -1,11 +1,22 @@ import { http, HttpResponse } from 'msw'; import { mkdir, realpath, writeFile } from 'node:fs/promises'; import { dirname, join } from 'node:path'; -import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, MockInstance, test, vi } from 'vitest'; +import { + afterAll, + afterEach, + beforeAll, + beforeEach, + describe, + expect, + MockInstance, + test, + vi, +} from 'vitest'; import { handlers } from '../../tests/mocks/handlers'; import { server } from '../../tests/mocks/node'; import { textHistory } from '../../tests/mocks/spinner'; + import { consola } from './lib/logger'; import { mkTempDir } from './lib/mk-temp-dir'; import { program } from './program'; @@ -53,7 +64,9 @@ async function writeProjectJson(overrides: { accessToken?: string; }) { const path = join(tmpDir, '.bigcommerce', 'project.json'); + await mkdir(dirname(path), { recursive: true }); + const defaults = { projectUuid: projectUuidFromProjectJson, framework: 'catalyst' as const, @@ -61,6 +74,7 @@ async function writeProjectJson(overrides: { accessToken: accessTokenFromProjectJson, telemetry: { enabled: true, anonymousId: 'test-id' }, }; + await writeFile(path, JSON.stringify({ ...defaults, ...overrides })); } @@ -74,7 +88,9 @@ function useCaptureHandlers(captured: { 'https://:apiHost/stores/:storeHash/v3/infrastructure/deployments/uploads', ({ request, params }) => { captured.storeHash = params.storeHash as string; - captured.accessToken = request.headers.get('X-Auth-Token') ?? request.headers.get('x-auth-token') ?? ''; + captured.accessToken = + request.headers.get('X-Auth-Token') ?? request.headers.get('x-auth-token') ?? ''; + return HttpResponse.json({ data: { upload_url: 'https://mock-upload-url.com', @@ -88,10 +104,15 @@ function useCaptureHandlers(captured: { http.post( 'https://:apiHost/stores/:storeHash/v3/infrastructure/deployments', async ({ request, params }) => { - captured.storeHash = (params.storeHash as string) || new URL(request.url).pathname.split('/')[2] || ''; + captured.storeHash = + (params.storeHash as string) || new URL(request.url).pathname.split('/')[2] || ''; + const body = (await request.json()) as { project_uuid?: string }; + captured.projectUuid = body.project_uuid ?? ''; - captured.accessToken = request.headers.get('X-Auth-Token') ?? request.headers.get('x-auth-token') ?? ''; + captured.accessToken = + request.headers.get('X-Auth-Token') ?? request.headers.get('x-auth-token') ?? ''; + return deploymentResponse(); }, ), @@ -108,6 +129,7 @@ beforeAll(async () => { // Minimal dist so deploy's generateBundleZip() passes; we only assert on config → request values. const distDir = join(tmpDir, '.bigcommerce', 'dist'); + await mkdir(distDir, { recursive: true }); await writeFile(join(distDir, 'worker.js'), ''); await mkdir(join(distDir, 'assets'), { recursive: true }); @@ -142,6 +164,7 @@ describe('config resolution priority', () => { process.env.CATALYST_PROJECT_UUID = projectUuidFromEnv; const captured = { storeHash: '', projectUuid: '', accessToken: '' }; + useCaptureHandlers(captured); await program.parseAsync([ @@ -164,6 +187,7 @@ describe('config resolution priority', () => { process.env.CATALYST_PROJECT_UUID = projectUuidFromEnv; const captured = { storeHash: '', projectUuid: '', accessToken: '' }; + useCaptureHandlers(captured); await program.parseAsync(['node', 'catalyst', 'deploy']); @@ -179,6 +203,7 @@ describe('config resolution priority', () => { }); const captured = { storeHash: '', projectUuid: '', accessToken: '' }; + useCaptureHandlers(captured); await program.parseAsync(['node', 'catalyst', 'deploy']); @@ -193,6 +218,7 @@ describe('config resolution priority', () => { process.env.CATALYST_STORE_HASH = storeHashFromEnv; const captured = { storeHash: '', projectUuid: '', accessToken: '' }; + useCaptureHandlers(captured); await program.parseAsync([ @@ -220,6 +246,7 @@ describe('config resolution priority', () => { process.env.CATALYST_PROJECT_UUID = projectUuidFromEnv; const captured = { storeHash: '', projectUuid: '', accessToken: '' }; + useCaptureHandlers(captured); await program.parseAsync(['node', 'catalyst', 'deploy']); @@ -235,6 +262,7 @@ describe('config resolution priority', () => { }); const captured = { storeHash: '', projectUuid: '', accessToken: '' }; + useCaptureHandlers(captured); await program.parseAsync(['node', 'catalyst', 'deploy']); @@ -251,6 +279,7 @@ describe('config resolution priority', () => { process.env.CATALYST_PROJECT_UUID = projectUuidFromEnv; const captured = { storeHash: '', projectUuid: '', accessToken: '' }; + useCaptureHandlers(captured); await program.parseAsync([ @@ -279,6 +308,7 @@ describe('config resolution priority', () => { process.env.CATALYST_PROJECT_UUID = projectUuidFromEnv; const captured = { storeHash: '', projectUuid: '', accessToken: '' }; + useCaptureHandlers(captured); await program.parseAsync(['node', 'catalyst', 'deploy']); @@ -294,6 +324,7 @@ describe('config resolution priority', () => { }); const captured = { storeHash: '', projectUuid: '', accessToken: '' }; + useCaptureHandlers(captured); await program.parseAsync(['node', 'catalyst', 'deploy']); diff --git a/packages/catalyst/src/cli/index.ts b/packages/catalyst/src/cli/index.ts index 04cb57509d..10ee9979c7 100644 --- a/packages/catalyst/src/cli/index.ts +++ b/packages/catalyst/src/cli/index.ts @@ -9,10 +9,12 @@ import { program } from './program'; const envFileIndex = process.argv.findIndex( (arg) => arg === '--env-file' || arg.startsWith('--env-file='), ); + if (envFileIndex !== -1) { const arg = process.argv[envFileIndex]; const path = arg === '--env-file' ? process.argv[envFileIndex + 1] : arg.slice('--env-file='.length); + if (path) { config({ path: resolve(process.cwd(), path), override: true }); } From 609175360449755f51ad1fe8043b4290ec38a6e5 Mon Sep 17 00:00:00 2001 From: jamesqquick Date: Tue, 10 Feb 2026 15:14:57 -0500 Subject: [PATCH 15/16] fix linting --- packages/catalyst/src/cli/config-priority.spec.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/catalyst/src/cli/config-priority.spec.ts b/packages/catalyst/src/cli/config-priority.spec.ts index 6ddbe86be3..b033641577 100644 --- a/packages/catalyst/src/cli/config-priority.spec.ts +++ b/packages/catalyst/src/cli/config-priority.spec.ts @@ -87,6 +87,7 @@ function useCaptureHandlers(captured: { http.post( 'https://:apiHost/stores/:storeHash/v3/infrastructure/deployments/uploads', ({ request, params }) => { + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions captured.storeHash = params.storeHash as string; captured.accessToken = request.headers.get('X-Auth-Token') ?? request.headers.get('x-auth-token') ?? ''; @@ -105,8 +106,10 @@ function useCaptureHandlers(captured: { 'https://:apiHost/stores/:storeHash/v3/infrastructure/deployments', async ({ request, params }) => { captured.storeHash = + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions (params.storeHash as string) || new URL(request.url).pathname.split('/')[2] || ''; + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions const body = (await request.json()) as { project_uuid?: string }; captured.projectUuid = body.project_uuid ?? ''; From 4671ec0fe6df8b50178706707b0717b595da2863 Mon Sep 17 00:00:00 2001 From: jamesqquick Date: Wed, 11 Feb 2026 06:21:39 -0500 Subject: [PATCH 16/16] add .bigcommerce to core gitignore --- core/.gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/core/.gitignore b/core/.gitignore index f4086e9252..347b1c3941 100644 --- a/core/.gitignore +++ b/core/.gitignore @@ -54,3 +54,5 @@ build-config.json # OpenNext .open-next .wrangler + +.bigcommerce