Skip to content

Add shopify store info --store <domain> command#7660

Draft
amcaplan wants to merge 21 commits into
mainfrom
ariel/store-info-command
Draft

Add shopify store info --store <domain> command#7660
amcaplan wants to merge 21 commits into
mainfrom
ariel/store-info-command

Conversation

@amcaplan
Copy link
Copy Markdown
Contributor

@amcaplan amcaplan commented May 28, 2026

Summary

  • Adds new shopify store info --store <domain> command that surfaces shop metadata from BP Destinations + BP Organizations APIs.
  • Baseline fields (domain, name, type, status, URLs, plan, billing currency, etc.) work without store auth for the shop.
  • When the shop has been authed via store auth, the Admin API is queried in parallel and additional fields (shop_owner, timezone, setup_required, plus) are folded into the same output — no extra flag needed.
  • Per-field graceful degradation via _field_errors when an individual backend fails. auth_status already tells callers why Admin-sourced fields are absent for unauthed shops, so those are silently omitted instead of cluttering _field_errors.
  • Supports --json for machine consumption.
  • Human-readable text output is grouped by what users actually look for: Store (identity + config), Access (URLs + auth), Plan (commercial), Activity (created/last access).

Implementation notes

  • Two-query BP path: destinations(search:)currentUserAccount.organizationForDestination(destinationPublicId:). Avoids the name-collision risk of resolving owning_org via parentDestinationName.
  • Reuses prepareAdminStoreGraphQLContext from store execute for the Admin API path.
  • Dates are normalized to UTC ISO strings in JSON and rendered as YYYY-MM-DD HH:MM:SS UTC in text.
  • Internal identifiers (BP destination id, organization id, Shopify shop id, is_main_shop) are intentionally not surfaced — the CLI addresses stores by domain.
  • Stacks on parent PRs that extract @shopify/store and @shopify/organizations.

Quality gates

  • pnpm vitest run (packages/store) — 168 tests passing
  • pnpm type-check
  • pnpm lint
  • pnpm refresh-manifests, pnpm refresh-readme, pnpm build-dev-docs — generated artifacts committed

Test plan

  • shopify store info --store <unauthed-store>.myshopify.com returns baseline fields without store auth
  • shopify store info --store <authed-store>.myshopify.com includes shop_owner, timezone, setup_required, plus
  • --json output matches the snake_case envelope in the issue
  • Missing --store surfaces a clear AbortError
  • Non-existent shop domain surfaces a clear AbortError
  • When BP Organizations returns 5xx, the rest of the output still renders and the affected fields show up under _field_errors

Out of scope (follow-up issue)

  • Preview stores (placeholder-owned shops, Preview URL minting, Claim URL refresh) — covered by a successor issue once supporting backend pieces land.
  • Default-to-active-store resolution when --store is omitted — deferred until LocalStorage.keys() lands in cli-kit (would unlock store list --authed too).

Closes shop/issues-develop#22724

🤖 Generated with Claude Code

Copy link
Copy Markdown
Contributor Author

amcaplan commented May 28, 2026


static descriptionWithMarkdown = `Reads metadata for a store from the Business Platform Destinations and Organizations APIs.

Tier 1 and Tier 2 fields work without \`store auth\`. Tier 3 fields (shop owner, timezone, features, setup required) require \`store auth\` and are only included when \`--verbose\` is set.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: This reads like harness session language and could be clarified.

Comment thread packages/cli/README.md
-j, --json [env: SHOPIFY_FLAG_JSON] Output the result as JSON. Automatically disables color output.
-s, --store=<value> (required) [env: SHOPIFY_FLAG_STORE] The myshopify.com domain of the store to authenticate
against.
-s, --store=<value> (required) [env: SHOPIFY_FLAG_STORE] The myshopify.com domain of the store.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are losing some info here + below. I am debating whether it's better to have a shared flag entirely, or there's value in overriding the description.

@amcaplan amcaplan changed the title Add shopify store info <store> command Add shopify store info --store <domain> command May 28, 2026
@amcaplan amcaplan force-pushed the ariel/extract-organizations-package branch from d67e8d3 to c9531e3 Compare May 28, 2026 22:02
@amcaplan amcaplan force-pushed the ariel/store-info-command branch from 38490df to 979bd9a Compare May 28, 2026 22:02
Base automatically changed from ariel/extract-organizations-package to main May 28, 2026 22:14
amcaplan and others added 18 commits May 31, 2026 21:05
Adds a new read-only command that surfaces shop metadata from BP Destinations
and BP Organizations APIs, with optional Admin API enrichment via `--verbose`.
Supports `--json` output and per-field graceful degradation via a
`_field_errors` envelope.

Tier 1 + Tier 2 fields work without `store auth` for the shop; Tier 3 fields
(shop_owner, timezone, features, setup_required) require `store auth` and are
opt-in via `--verbose`.

Fixes #22724

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces repeated `if (value) result.field = value` chains with a
single `compact()` pass from `@shopify/cli-kit/common/object`,
flattening buildResult, applyVerboseFields, buildPlan, and mapAdminShop.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aligns with the rest of the store: namespace (e.g. store execute) which
takes the shop domain via `-s, --store` rather than a positional.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Hoists the `-s, --store` flag definition into packages/store/src/cli/flags.ts
following the themeFlags/appFlags precedent. `store auth`, `store execute`,
and `store info` now share the same flag declaration.

Side effect: the per-command verbal description ("…to authenticate against",
"…to execute against", "…to inspect") collapses into "The myshopify.com
domain of the store." — visible only in --help. The verb is redundant with
the command name.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BP's Destinations + Organizations APIs return primaryDomain/webUrl as
full URLs with scheme ("https://shop.myshopify.com"), not bare hosts.
Strict string equality against the requested store FQDN missed every
real shop. They also return `handle: null` and a non-subdomain
`shortName` (e.g. "ACT"), so admin_url construction had nothing to work
with.

- Extract host from URL fields before comparing (new info/host.ts).
- Derive the canonical myshopify subdomain from primaryDomain/webUrl
  and overwrite destination.handle with it so admin_url is built from
  the actual shop subdomain.
- Use primaryDomain (storefront URL) for primary_url instead of
  webUrl/url (both are admin URLs ending in /admin).
- Drop the misleading `url` field from OrganizationShopFields and the
  Org GraphQL selection — it's the admin URL, not the storefront.
- Correct features.branding type from boolean to string (it's an enum
  like "SHOPIFY", not a flag).

Verified live against ariel-caplan-test: Tier 1, 2, and 3 all populate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Keys render as "Store Type" instead of "store_type", and GraphQL enum
strings like APP_DEVELOPMENT render as "App Development". Currency
codes like USD pass through unchanged.
CLI commands take --store as a domain, so BP/Core shop IDs and the
owning-org id have no follow-on use for users of this command.
is_main_shop is a billing-contract internal that almost no user cares
about. Removed from both text and JSON output; org id is kept as an
internal-only type to drive the BP Organizations request.
Local-time output without a timezone is ambiguous when sharing output
across regions; rendering in UTC makes timestamps unambiguous regardless
of where the user is.
Drop references to "Tier", "Identity", and "Business Platform" from the
command description, section titles, and error reasons — those are
internal taxonomy that users don't recognize.
--verbose is a global "show your work" flag (more diagnostic output)
across the CLI; using it to mean "include more result fields" is a
mismatched concept. --full reads naturally as "give me the full set
of fields".
Drop the features sub-object: storefront is uniformly true, branding is
cosmetic, harmonizedSystemCode is niche cross-border tooling. Lift
shopifyPlus to a top-level `plus` boolean — it overlaps with plan.name
but is relevant enough on its own to deserve a clean signal rather than
requiring callers to regex-match plan names.

--full now adds exactly: shop_owner, timezone, setup_required, plus.
…uthed

The flag was redundant: we already know whether the store is
authenticated by the time we compose the result, so gating the Admin
fetch behind an opt-in flag just added friction without unlocking
anything. Now `store info` quietly includes shop_owner/timezone/
setup_required/plus when `store auth` exists for the store, and omits
them otherwise. The `auth_status: not authenticated` field already
serves as the discoverability hint, so we don't pollute output with
field errors for fields the user never asked for.
Replace data-source-based section names (Overview / Plan & lifecycle /
Admin details) with categories users actually think in: Store, Access,
Plan, Activity. Timezone is a store property, so it sits in Store
rather than Activity.
The --verbose flag was removed when Admin-sourced fields became
automatic for authed shops; the test still asserted on it.
knip flagged a handful of types and option interfaces that were
exported but only referenced inside their own file. Also drop the
stale --full and Tier-1/2 comments from the result envelope.
Both @Shopify/organizations and the store info plumbing were decoding
base64-encoded organization GIDs. Pull the helper into the
organizations package's models module and export it, then have
store info import it.

Returns string | undefined so each caller can decide what to do on
failure: fetchOrganizations still aborts (the id is required to
display the org); store info silently omits the id (the BP
Organizations request would have failed with a garbage id anyway).
The base64↔gid logic isn't organization-specific, so a helper named
decodeOrganizationGid in @Shopify/organizations was misleading.

Add three generic primitives in @shopify/cli-kit/common/gid:
- numericIdFromGid: extract /<digits> from a plain gid://...
- numericIdFromEncodedGid: same, but for base64-encoded gids
- encodeGid: base64-encode a plain gid

Thread these through every site that does the same work:
- @Shopify/organizations fetch.ts
- @shopify/store store info destinations
- app-management-client.ts (numberFromGid/idFromEncodedGid wrappers
  keep their domain semantics — auto-detect numeric strings,
  fail-fast on bad gids — but delegate the regex/base64 work)

Domain-validated extractors like extractBulkOperationId
(@shopify/app bulk operations) stay; they require a specific gid
prefix that the generic helper doesn't enforce.

Drops the store→organizations dependency that existed only for
this helper.
@amcaplan amcaplan force-pushed the ariel/store-info-command branch from 979bd9a to c1e7a69 Compare May 31, 2026 18:17
amcaplan and others added 3 commits May 31, 2026 21:23
Neither helper was specific to store info — extractHost is a generic
URL→host parser, and extractMyshopifyHandle is Shopify-domain logic
that already has neighbors in cli-kit. Move both into
@shopify/cli-kit/common/url alongside isValidURL and safeParseURL,
add direct tests, and delete the store/info/host.ts module.

extractHost now uses safeParseURL to avoid a bare try/catch.
Follow the repo convention used by other packages: queries live in
.graphql files under src/cli/api/graphql/<api>/queries/, with typed
documents generated alongside. Replaces hand-written inline query
strings and hand-typed response interfaces with the businessPlatform-
RequestDoc / businessPlatformOrganizationsRequestDoc / adminRequestDoc
typed-document variants.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The schema files are sourced from external repos at codegen time (mirroring how packages/app and packages/organizations work) and shouldn't be tracked.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

Differences in type declarations

We detected differences in the type declarations generated by Typescript for this branch compared to the baseline ('main' branch). Please, review them to ensure they are backward-compatible. Here are some important things to keep in mind:

  • Some seemingly private modules might be re-exported through public modules.
  • If the branch is behind main you might see odd diffs, rebase main into this branch.

New type declarations

packages/cli-kit/dist/public/common/gid.d.ts
/**
 * Extracts the trailing numeric id from a plain GraphQL global id like
 * `gid://shopify/Product/123`.
 *
 * @param gid - A plain GraphQL global id string.
 * @returns The trailing numeric id, or undefined when the string does not end with `/<digits>`.
 */
export declare function numericIdFromGid(gid: string): string | undefined;
/**
 * Decodes a base64-encoded GraphQL global id (for example, the form
 * Business Platform APIs return) and returns the trailing numeric id.
 *
 * @param gid - A base64-encoded GraphQL global id.
 * @returns The trailing numeric id, or undefined when the decoded string does not end with `/<digits>`.
 */
export declare function numericIdFromEncodedGid(gid: string): string | undefined;
/**
 * Encodes a plain GraphQL global id (`gid://...`) as base64, which is the
 * form some Business Platform endpoints require.
 *
 * @param gid - A plain GraphQL global id string to encode.
 * @returns The base64-encoded gid.
 */
export declare function encodeGid(gid: string): string;

Existing type declarations

packages/cli-kit/dist/public/common/url.d.ts
@@ -12,4 +12,20 @@ export declare function isValidURL(url: string): boolean;
  * @param url - The string to parse into a URL.
  * @returns A URL object if the parsing is successful, undefined otherwise.
  */
-export declare function safeParseURL(url: string): URL | undefined;
\ No newline at end of file
+export declare function safeParseURL(url: string): URL | undefined;
+/**
+ * Extracts the lowercased hostname from a URL-shaped string. Tolerates
+ * bare hosts (without a scheme) and inputs that come back from APIs as
+ * either  or .
+ *
+ * @param value - A URL or bare host string, possibly null/undefined.
+ * @returns The lowercased hostname, or undefined when the input is empty.
+ */
+export declare function extractHost(value: string | null | undefined): string | undefined;
+/**
+ * Extracts the subdomain handle from a  URL or host.
+ *
+ * @param value - A URL or host string, possibly null/undefined.
+ * @returns The myshopify subdomain handle, or undefined when the input isn't a  URL.
+ */
+export declare function extractMyshopifyHandle(value: string | null | undefined): string | undefined;
\ No newline at end of file

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: @shopify/cli @shopify/cli package issues

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants