From fd5baeec37a2aa8c9220eb77f9ef676df9ba48e5 Mon Sep 17 00:00:00 2001 From: Jorge Moya Date: Wed, 22 Apr 2026 15:16:34 -0500 Subject: [PATCH 1/5] LTRAC-443: feat(cli) - Show deployed URL in project list output Extend the projects list schema with a new nullable `deployed_url` field and render it beneath each project so users can recover the hosted storefront URL without having to run a fresh deploy. Previously, `catalyst project list` only printed `Name (uuid)`, and the only path to the `.catalyst-sandbox.store` URL was re-running `catalyst deploy` (which surfaces it via the SSE event stream). Surfacing the URL here also resolves the related feedback that it was unclear where a deploy lands. Projects without a successful deployment show `(not deployed)`. Refs LTRAC-443 Co-Authored-By: Claude --- .changeset/project-list-deployed-url.md | 5 +++++ packages/catalyst/src/cli/commands/project.spec.ts | 4 ++++ packages/catalyst/src/cli/commands/project.ts | 13 +++++++++++++ packages/catalyst/src/cli/lib/project.ts | 2 ++ packages/catalyst/tests/mocks/handlers.ts | 12 ++++++++++-- 5 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 .changeset/project-list-deployed-url.md diff --git a/.changeset/project-list-deployed-url.md b/.changeset/project-list-deployed-url.md new file mode 100644 index 0000000000..9859e5990a --- /dev/null +++ b/.changeset/project-list-deployed-url.md @@ -0,0 +1,5 @@ +--- +"@bigcommerce/catalyst": minor +--- + +Show the deployed URL for each project in `catalyst project list` output so users can recover the hosted storefront URL without having to redeploy. diff --git a/packages/catalyst/src/cli/commands/project.spec.ts b/packages/catalyst/src/cli/commands/project.spec.ts index 79a4933a71..09a51f4991 100644 --- a/packages/catalyst/src/cli/commands/project.spec.ts +++ b/packages/catalyst/src/cli/commands/project.spec.ts @@ -227,7 +227,11 @@ describe('project list', () => { 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( + expect.stringContaining('https://project-one.catalyst-sandbox.store'), + ); expect(consola.log).toHaveBeenCalledWith('Project Two (b23f5785-fd99-4a94-9fb3-945551623924)'); + expect(consola.log).toHaveBeenCalledWith(' (not deployed)'); expect(exitMock).toHaveBeenCalledWith(0); }); diff --git a/packages/catalyst/src/cli/commands/project.ts b/packages/catalyst/src/cli/commands/project.ts index a5ecde3193..e4816071c0 100644 --- a/packages/catalyst/src/cli/commands/project.ts +++ b/packages/catalyst/src/cli/commands/project.ts @@ -1,4 +1,5 @@ import { Command, Option } from 'commander'; +import { colorize } from 'consola/utils'; import { consola } from '../lib/logger'; import { createProject, fetchProjects } from '../lib/project'; @@ -53,6 +54,18 @@ Example: projects.forEach((p) => { consola.log(`${p.name} (${p.uuid})`); + + if (p.deployed_url) { + const url = p.deployed_url.startsWith('https://') + ? p.deployed_url + : `https://${p.deployed_url}`; + + consola.log(` ${colorize('blue', url)}`); + } else { + consola.log(' (not deployed)'); + } + + consola.log(''); }); process.exit(0); diff --git a/packages/catalyst/src/cli/lib/project.ts b/packages/catalyst/src/cli/lib/project.ts index 1dc66420ee..f742a2c298 100644 --- a/packages/catalyst/src/cli/lib/project.ts +++ b/packages/catalyst/src/cli/lib/project.ts @@ -7,6 +7,7 @@ const fetchProjectsSchema = z.object({ z.object({ uuid: z.string(), name: z.string(), + deployed_url: z.string().nullable(), }), ), }); @@ -14,6 +15,7 @@ const fetchProjectsSchema = z.object({ export interface ProjectListItem { uuid: string; name: string; + deployed_url: string | null; } export async function fetchProjects( diff --git a/packages/catalyst/tests/mocks/handlers.ts b/packages/catalyst/tests/mocks/handlers.ts index c16ea054db..1896d55c68 100644 --- a/packages/catalyst/tests/mocks/handlers.ts +++ b/packages/catalyst/tests/mocks/handlers.ts @@ -29,8 +29,16 @@ export const handlers = [ http.get('https://:apiHost/stores/:storeHash/v3/infrastructure/projects', () => HttpResponse.json({ data: [ - { uuid: 'a23f5785-fd99-4a94-9fb3-945551623923', name: 'Project One' }, - { uuid: 'b23f5785-fd99-4a94-9fb3-945551623924', name: 'Project Two' }, + { + uuid: 'a23f5785-fd99-4a94-9fb3-945551623923', + name: 'Project One', + deployed_url: 'https://project-one.catalyst-sandbox.store', + }, + { + uuid: 'b23f5785-fd99-4a94-9fb3-945551623924', + name: 'Project Two', + deployed_url: null, + }, ], }), ), From 5655f812cf9fc9f92f30b875257f6e23aa0a1c6e Mon Sep 17 00:00:00 2001 From: Jorge Moya Date: Thu, 23 Apr 2026 15:25:00 -0500 Subject: [PATCH 2/5] LTRAC-443: ref(cli) - Rename deployed_url to deployment_url MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Align the projects list field name with the existing `deployment_url` convention used on the ignition Deployments event stream, per reviewer feedback on bigcommerce/interfaces#3993 (Parth + Chance wanted a single consistent contract, not "either hostname or URL"). Contract is now strictly hostname-only (no scheme) — the CLI unconditionally prepends `https://` for display rather than defensively checking, which matches how the deploy command already handles `deployment_url` from the SSE stream. Refs LTRAC-443 Co-Authored-By: Claude --- packages/catalyst/src/cli/commands/project.ts | 8 ++------ packages/catalyst/src/cli/lib/project.ts | 4 ++-- packages/catalyst/tests/mocks/handlers.ts | 4 ++-- 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/packages/catalyst/src/cli/commands/project.ts b/packages/catalyst/src/cli/commands/project.ts index e4816071c0..cebf48b99d 100644 --- a/packages/catalyst/src/cli/commands/project.ts +++ b/packages/catalyst/src/cli/commands/project.ts @@ -55,12 +55,8 @@ Example: projects.forEach((p) => { consola.log(`${p.name} (${p.uuid})`); - if (p.deployed_url) { - const url = p.deployed_url.startsWith('https://') - ? p.deployed_url - : `https://${p.deployed_url}`; - - consola.log(` ${colorize('blue', url)}`); + if (p.deployment_url) { + consola.log(` ${colorize('blue', `https://${p.deployment_url}`)}`); } else { consola.log(' (not deployed)'); } diff --git a/packages/catalyst/src/cli/lib/project.ts b/packages/catalyst/src/cli/lib/project.ts index f742a2c298..1d03991b88 100644 --- a/packages/catalyst/src/cli/lib/project.ts +++ b/packages/catalyst/src/cli/lib/project.ts @@ -7,7 +7,7 @@ const fetchProjectsSchema = z.object({ z.object({ uuid: z.string(), name: z.string(), - deployed_url: z.string().nullable(), + deployment_url: z.string().nullable(), }), ), }); @@ -15,7 +15,7 @@ const fetchProjectsSchema = z.object({ export interface ProjectListItem { uuid: string; name: string; - deployed_url: string | null; + deployment_url: string | null; } export async function fetchProjects( diff --git a/packages/catalyst/tests/mocks/handlers.ts b/packages/catalyst/tests/mocks/handlers.ts index 1896d55c68..b38589d5d6 100644 --- a/packages/catalyst/tests/mocks/handlers.ts +++ b/packages/catalyst/tests/mocks/handlers.ts @@ -32,12 +32,12 @@ export const handlers = [ { uuid: 'a23f5785-fd99-4a94-9fb3-945551623923', name: 'Project One', - deployed_url: 'https://project-one.catalyst-sandbox.store', + deployment_url: 'project-one.catalyst-sandbox.store', }, { uuid: 'b23f5785-fd99-4a94-9fb3-945551623924', name: 'Project Two', - deployed_url: null, + deployment_url: null, }, ], }), From a689b754b86e00b11a57e18cd8ce0f7bcd88af4f Mon Sep 17 00:00:00 2001 From: Jorge Moya Date: Mon, 27 Apr 2026 17:41:53 -0500 Subject: [PATCH 3/5] LTRAC-443: feat(cli) - Render multiple deployment URLs per project Switch the project list output from a single deployment_url field to an array of deployment_urls, mirroring the proto contract change in bigcommerce/interfaces#3993. Each hostname renders on its own indented line under the project; an empty array still renders "(not deployed)". Future-proofs the CLI for vanity hostnames per project, which the ignition serializer now exposes via domainService.GetDomains. Refs LTRAC-443 Co-Authored-By: Claude --- packages/catalyst/src/cli/commands/project.spec.ts | 3 +++ packages/catalyst/src/cli/commands/project.ts | 8 +++++--- packages/catalyst/src/cli/lib/project.ts | 4 ++-- packages/catalyst/tests/mocks/handlers.ts | 4 ++-- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/packages/catalyst/src/cli/commands/project.spec.ts b/packages/catalyst/src/cli/commands/project.spec.ts index 09a51f4991..7c74da750a 100644 --- a/packages/catalyst/src/cli/commands/project.spec.ts +++ b/packages/catalyst/src/cli/commands/project.spec.ts @@ -230,6 +230,9 @@ describe('project list', () => { expect(consola.log).toHaveBeenCalledWith( expect.stringContaining('https://project-one.catalyst-sandbox.store'), ); + expect(consola.log).toHaveBeenCalledWith( + expect.stringContaining('https://vanity.project-one.example.com'), + ); expect(consola.log).toHaveBeenCalledWith('Project Two (b23f5785-fd99-4a94-9fb3-945551623924)'); expect(consola.log).toHaveBeenCalledWith(' (not deployed)'); expect(exitMock).toHaveBeenCalledWith(0); diff --git a/packages/catalyst/src/cli/commands/project.ts b/packages/catalyst/src/cli/commands/project.ts index cebf48b99d..1ea8629864 100644 --- a/packages/catalyst/src/cli/commands/project.ts +++ b/packages/catalyst/src/cli/commands/project.ts @@ -55,10 +55,12 @@ Example: projects.forEach((p) => { consola.log(`${p.name} (${p.uuid})`); - if (p.deployment_url) { - consola.log(` ${colorize('blue', `https://${p.deployment_url}`)}`); - } else { + if (p.deployment_urls.length === 0) { consola.log(' (not deployed)'); + } else { + p.deployment_urls.forEach((hostname) => { + consola.log(` ${colorize('blue', `https://${hostname}`)}`); + }); } consola.log(''); diff --git a/packages/catalyst/src/cli/lib/project.ts b/packages/catalyst/src/cli/lib/project.ts index 1d03991b88..1f034f53fc 100644 --- a/packages/catalyst/src/cli/lib/project.ts +++ b/packages/catalyst/src/cli/lib/project.ts @@ -7,7 +7,7 @@ const fetchProjectsSchema = z.object({ z.object({ uuid: z.string(), name: z.string(), - deployment_url: z.string().nullable(), + deployment_urls: z.array(z.string()), }), ), }); @@ -15,7 +15,7 @@ const fetchProjectsSchema = z.object({ export interface ProjectListItem { uuid: string; name: string; - deployment_url: string | null; + deployment_urls: string[]; } export async function fetchProjects( diff --git a/packages/catalyst/tests/mocks/handlers.ts b/packages/catalyst/tests/mocks/handlers.ts index b38589d5d6..2b52af1fe3 100644 --- a/packages/catalyst/tests/mocks/handlers.ts +++ b/packages/catalyst/tests/mocks/handlers.ts @@ -32,12 +32,12 @@ export const handlers = [ { uuid: 'a23f5785-fd99-4a94-9fb3-945551623923', name: 'Project One', - deployment_url: 'project-one.catalyst-sandbox.store', + deployment_urls: ['project-one.catalyst-sandbox.store', 'vanity.project-one.example.com'], }, { uuid: 'b23f5785-fd99-4a94-9fb3-945551623924', name: 'Project Two', - deployment_url: null, + deployment_urls: [], }, ], }), From 7c7b4c647d2b7b5e24d2e1aab711da235b0c8f4a Mon Sep 17 00:00:00 2001 From: Jorge Moya Date: Mon, 27 Apr 2026 21:15:44 -0500 Subject: [PATCH 4/5] LTRAC-593: docs(cli) - Update changeset for plural deployment URLs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Match the wire contract change in interfaces#3993 (singular `deployment_url` → repeated `deployment_urls`) so the user-facing release note describes the actual rendering: every URL per project is shown, including any vanity hostnames. Filename intentionally preserved — changesets pick up by `*.md` glob, so renaming would just churn git history. Refs LTRAC-593 Co-Authored-By: Claude --- .changeset/project-list-deployed-url.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/project-list-deployed-url.md b/.changeset/project-list-deployed-url.md index 9859e5990a..0ff85acf7f 100644 --- a/.changeset/project-list-deployed-url.md +++ b/.changeset/project-list-deployed-url.md @@ -2,4 +2,4 @@ "@bigcommerce/catalyst": minor --- -Show the deployed URL for each project in `catalyst project list` output so users can recover the hosted storefront URL without having to redeploy. +Show every deployed URL for each project in `catalyst project list` output (the canonical hostname plus any vanity hostnames) so users can recover the hosted storefront URLs without having to redeploy. From d8c3886f68ef27b5d29435ab38a12b0e798e524a Mon Sep 17 00:00:00 2001 From: Jorge Moya Date: Fri, 1 May 2026 11:51:05 -0500 Subject: [PATCH 5/5] LTRAC-443: ref(cli) - Use deployment_hostnames; tolerate deprecated url MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Track the proto rename + SSE deprecation in interfaces#3993: project list — ProjectListItem.deployment_urls -> deployment_hostnames on the schema and the rendering, matching the renamed proto field. The visible output is unchanged (still one https://-prefixed line per hostname, or "(not deployed)" for an empty array). deploy SSE consumer — extend DeploymentStatusSchema to accept both the deprecated singular `deployment_url` and the new plural `deployment_hostnames`. Reading logic prefers the new field and falls back to the deprecated one so older ignition builds (that haven't shipped the rename yet) still produce a usable deployment URL during the migration window. Drops the defensive `startsWith('https://')` guard on the post-deploy success message since the contract is now strictly hostname-only and the CLI unconditionally prepends the scheme. Mocks (handlers.ts) emit both fields on every SSE chunk so the test exercises the new path. Refs LTRAC-443 Co-Authored-By: Claude --- packages/catalyst/src/cli/commands/deploy.ts | 24 ++++++++++++------- packages/catalyst/src/cli/commands/project.ts | 4 ++-- packages/catalyst/src/cli/lib/project.ts | 4 ++-- packages/catalyst/tests/mocks/handlers.ts | 13 ++++++---- 4 files changed, 28 insertions(+), 17 deletions(-) diff --git a/packages/catalyst/src/cli/commands/deploy.ts b/packages/catalyst/src/cli/commands/deploy.ts index 78229faabb..6f15c08b37 100644 --- a/packages/catalyst/src/cli/commands/deploy.ts +++ b/packages/catalyst/src/cli/commands/deploy.ts @@ -65,7 +65,11 @@ const DeploymentStatusSchema = z.object({ progress: z.number(), }) .nullable(), - deployment_url: z.string().nullable(), + // Deprecated by ignition; prefer `deployment_hostnames`. Kept here so + // older ignition builds (that haven't shipped the rename yet) still + // parse cleanly during the transition window. + deployment_url: z.string().nullable().optional(), + deployment_hostnames: z.array(z.string()).optional(), error: z .object({ code: z.number(), @@ -285,7 +289,7 @@ export const getDeploymentStatus = async ( const decoder = new TextDecoder(); let done = false; - let deploymentUrl: string | undefined; + let deploymentHostname: string | undefined; while (!done) { // eslint-disable-next-line no-await-in-loop @@ -321,8 +325,12 @@ export const getDeploymentStatus = async ( spinner.text = STEPS[data.event.step]; } - if (data.deployment_url) { - deploymentUrl = data.deployment_url; + // Prefer the new plural field; fall back to the deprecated singular + // for older ignition builds during the transition window. + if (data.deployment_hostnames && data.deployment_hostnames.length > 0) { + deploymentHostname = data.deployment_hostnames[0]; + } else if (data.deployment_url) { + deploymentHostname = data.deployment_url; } }); } @@ -332,10 +340,10 @@ export const getDeploymentStatus = async ( spinner.success('Deployment completed successfully.'); - if (deploymentUrl) { - const url = deploymentUrl.startsWith('https://') ? deploymentUrl : `https://${deploymentUrl}`; - - consola.success(`View your deployment at: ${colorize('blue', url)}`); + if (deploymentHostname) { + consola.success( + `View your deployment at: ${colorize('blue', `https://${deploymentHostname}`)}`, + ); } }; diff --git a/packages/catalyst/src/cli/commands/project.ts b/packages/catalyst/src/cli/commands/project.ts index 1ea8629864..43691e765d 100644 --- a/packages/catalyst/src/cli/commands/project.ts +++ b/packages/catalyst/src/cli/commands/project.ts @@ -55,10 +55,10 @@ Example: projects.forEach((p) => { consola.log(`${p.name} (${p.uuid})`); - if (p.deployment_urls.length === 0) { + if (p.deployment_hostnames.length === 0) { consola.log(' (not deployed)'); } else { - p.deployment_urls.forEach((hostname) => { + p.deployment_hostnames.forEach((hostname) => { consola.log(` ${colorize('blue', `https://${hostname}`)}`); }); } diff --git a/packages/catalyst/src/cli/lib/project.ts b/packages/catalyst/src/cli/lib/project.ts index 1f034f53fc..dba5bbee16 100644 --- a/packages/catalyst/src/cli/lib/project.ts +++ b/packages/catalyst/src/cli/lib/project.ts @@ -7,7 +7,7 @@ const fetchProjectsSchema = z.object({ z.object({ uuid: z.string(), name: z.string(), - deployment_urls: z.array(z.string()), + deployment_hostnames: z.array(z.string()), }), ), }); @@ -15,7 +15,7 @@ const fetchProjectsSchema = z.object({ export interface ProjectListItem { uuid: string; name: string; - deployment_urls: string[]; + deployment_hostnames: string[]; } export async function fetchProjects( diff --git a/packages/catalyst/tests/mocks/handlers.ts b/packages/catalyst/tests/mocks/handlers.ts index 2b52af1fe3..099d5bc33d 100644 --- a/packages/catalyst/tests/mocks/handlers.ts +++ b/packages/catalyst/tests/mocks/handlers.ts @@ -32,12 +32,15 @@ export const handlers = [ { uuid: 'a23f5785-fd99-4a94-9fb3-945551623923', name: 'Project One', - deployment_urls: ['project-one.catalyst-sandbox.store', 'vanity.project-one.example.com'], + deployment_hostnames: [ + 'project-one.catalyst-sandbox.store', + 'vanity.project-one.example.com', + ], }, { uuid: 'b23f5785-fd99-4a94-9fb3-945551623924', name: 'Project Two', - deployment_urls: [], + deployment_hostnames: [], }, ], }), @@ -52,14 +55,14 @@ export const handlers = [ controller.enqueue( encoder.encode( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `data: {"deployment_status":"in_progress","deployment_uuid":"${params.deploymentUuid}","event":{"step":"processing","progress":75},"deployment_url":null}`, + `data: {"deployment_status":"in_progress","deployment_uuid":"${params.deploymentUuid}","event":{"step":"processing","progress":75},"deployment_url":null,"deployment_hostnames":[]}`, ), ); setTimeout(() => { controller.enqueue( encoder.encode( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `data: {"deployment_status":"in_progress","deployment_uuid":"${params.deploymentUuid}","event":{"step":"finalizing","progress":99},"deployment_url":null}`, + `data: {"deployment_status":"in_progress","deployment_uuid":"${params.deploymentUuid}","event":{"step":"finalizing","progress":99},"deployment_url":null,"deployment_hostnames":[]}`, ), ); }, 10); @@ -67,7 +70,7 @@ export const handlers = [ controller.enqueue( encoder.encode( // eslint-disable-next-line @typescript-eslint/restrict-template-expressions - `data: {"deployment_status":"completed","deployment_uuid":"${params.deploymentUuid}","event":null,"deployment_url":"https://example.com"}`, + `data: {"deployment_status":"completed","deployment_uuid":"${params.deploymentUuid}","event":null,"deployment_url":"example.com","deployment_hostnames":["example.com"]}`, ), ); controller.close();